Initial public commit
commit
a958855568
@ -0,0 +1 @@
|
||||
gopocsag
|
@ -0,0 +1,15 @@
|
||||
|
||||
build:
|
||||
go build -o gopocsag
|
||||
|
||||
clean:
|
||||
rm gopocsag
|
||||
rm -rf batches
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
docgen:
|
||||
godoc -html ./internal/datatypes/ > doc/datatypes.html
|
||||
godoc -html ./internal/utils/ > doc/utils.html
|
||||
godoc -html ./internal/wav/ > doc/wav.html
|
@ -0,0 +1,3 @@
|
||||
# go-pocsag
|
||||
|
||||
A parser for POCSAG pager protocol implemented in Go
|
@ -0,0 +1,17 @@
|
||||
package datatypes
|
||||
|
||||
// A bit is high or low, 0 or 1, true or false.
|
||||
type Bit bool
|
||||
|
||||
// Int returns the bit value as 0 or 1
|
||||
func (b Bit) Int() int {
|
||||
if b {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (b Bit) UInt8() uint8 {
|
||||
return uint8(b.Int())
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package datatypes
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
var _ = Suite(&TypeSuite{})
|
||||
|
||||
type TypeSuite struct{}
|
||||
|
||||
func (f *TypeSuite) Test_BitToInt(c *C) {
|
||||
high := Bit(true)
|
||||
low := Bit(false)
|
||||
c.Assert(high.Int(), Equals, 1)
|
||||
c.Assert(low.Int(), Equals, 0)
|
||||
}
|
||||
|
||||
func (f *TypeSuite) Test_BitToUInt8(c *C) {
|
||||
high := Bit(true)
|
||||
low := Bit(false)
|
||||
c.Assert(high.UInt8(), Equals, uint8(1))
|
||||
c.Assert(low.UInt8(), Equals, uint8(0))
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package pocsag
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
var (
|
||||
DEBUG bool
|
||||
)
|
||||
|
||||
var (
|
||||
green = color.New(color.FgGreen)
|
||||
red = color.New(color.FgRed)
|
||||
blue = color.New(color.FgBlue)
|
||||
)
|
||||
|
||||
// Tell the package to print debug data
|
||||
func SetDebug(d bool) {
|
||||
DEBUG = d
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package pocsag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/wav"
|
||||
)
|
||||
|
||||
// ReadWav reads a wav file from disc and puts it in memory for the
|
||||
// scanner to parse as a standard transmission
|
||||
func ReadWav(path string) *bytes.Buffer {
|
||||
|
||||
wavdata, err := wav.NewWavData(path)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
samplecount := int(wavdata.Subchunk2Size / uint32(wavdata.BitsPerSample/8))
|
||||
seconds := float32(samplecount) / float32(wavdata.SampleRate)
|
||||
|
||||
if DEBUG {
|
||||
fmt.Printf("Samples: %d\n", samplecount)
|
||||
fmt.Printf("Seconds: %0.3f\n", seconds)
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(wavdata.Data)
|
||||
return buffer
|
||||
|
||||
}
|
@ -0,0 +1,475 @@
|
||||
package pocsag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/datatypes"
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/utils"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const (
|
||||
POCSAG_PREAMBLE uint32 = 0x7CD215D8
|
||||
POCSAG_IDLE uint32 = 0x7A89C197
|
||||
POCSAG_BATCH_LEN int = 512
|
||||
POCSAG_CODEWORD_LEN int = 32
|
||||
)
|
||||
|
||||
type CodewordType string
|
||||
|
||||
const (
|
||||
CodewordTypeAddress CodewordType = "ADDRESS"
|
||||
CodewordTypeMessage CodewordType = "MESSAGE"
|
||||
CodewordTypeIdle CodewordType = "IDLE"
|
||||
)
|
||||
|
||||
type MessageType string
|
||||
|
||||
const (
|
||||
MessageTypeAuto MessageType = "auto"
|
||||
MessageTypeAlphanumeric MessageType = "alpha"
|
||||
MessageTypeBitcodedDecimal MessageType = "bcd"
|
||||
)
|
||||
|
||||
// ParsePOCSAG takes bits decoded from the stream and parses them for
|
||||
// batches of codewords then prints them using the specefied message type.
|
||||
func ParsePOCSAG(bits []datatypes.Bit, messagetype MessageType) {
|
||||
|
||||
pocsag := &POCSAG{}
|
||||
|
||||
batches, err := pocsag.ParseBatches(bits)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
for i, batch := range batches {
|
||||
println("")
|
||||
println("Batch: ", i)
|
||||
batch.Print()
|
||||
}
|
||||
}
|
||||
|
||||
messages := pocsag.ParseMessages(batches)
|
||||
for _, m := range messages {
|
||||
|
||||
green.Println("-- Message --------------")
|
||||
green.Println("Reciptient: ", m.ReciptientString())
|
||||
|
||||
if !m.IsValid() {
|
||||
red.Println("This message has parity check errors. Contents might be corrupted")
|
||||
}
|
||||
|
||||
println("")
|
||||
print(m.PayloadString(messagetype))
|
||||
println("")
|
||||
println("")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type POCSAG struct{}
|
||||
|
||||
// ParseBatches takes bits decoded from the stream and parses them for
|
||||
// batches of codewords.
|
||||
func (p *POCSAG) ParseBatches(bits []datatypes.Bit) ([]*Batch, error) {
|
||||
|
||||
batches := []*Batch{}
|
||||
|
||||
var start = -1
|
||||
var batchno = -1
|
||||
// synchornize with the decoded bits
|
||||
for a := 0; a < len(bits)-32; a += 1 {
|
||||
|
||||
bytes := utils.MSBBitsToBytes(bits[a:a+32], 8)
|
||||
|
||||
if isPreamble(bytes) {
|
||||
|
||||
batchno += 1
|
||||
start = a + 32
|
||||
|
||||
// for file output as bin data
|
||||
batchbits := bits[a : a+POCSAG_BATCH_LEN+32]
|
||||
stream := utils.MSBBitsToBytes(batchbits, 8)
|
||||
|
||||
if DEBUG {
|
||||
out, err := os.Create(fmt.Sprintf("batches/batch-%d.bin", batchno))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out.Write(stream)
|
||||
}
|
||||
|
||||
batch, err := NewBatch(bits[start : start+POCSAG_BATCH_LEN])
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
} else {
|
||||
batches = append(batches, batch)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if start < 0 {
|
||||
return nil, fmt.Errorf("could not obtain message sync")
|
||||
}
|
||||
|
||||
return batches, nil
|
||||
|
||||
}
|
||||
|
||||
// ParseMessages takes a bundle of codeword from a series of batches and
|
||||
// compiles them into messages.
|
||||
// A message starts with an address codeword and a bunch of message codewords follows
|
||||
// until either the batch ends or an idle codeword appears.
|
||||
func (p *POCSAG) ParseMessages(batches []*Batch) []*Message {
|
||||
|
||||
messages := []*Message{}
|
||||
|
||||
var message *Message
|
||||
for _, b := range batches {
|
||||
|
||||
for _, codeword := range b.Codewords {
|
||||
|
||||
switch codeword.Type {
|
||||
// append current and begin new message
|
||||
case CodewordTypeAddress:
|
||||
if message != nil {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
message = NewMessage(codeword)
|
||||
|
||||
// append current but dont start new
|
||||
case CodewordTypeIdle:
|
||||
if message != nil {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
message = nil
|
||||
|
||||
case CodewordTypeMessage:
|
||||
if message != nil {
|
||||
message.AddPayload(codeword)
|
||||
} else {
|
||||
red.Println("Message codeword without sync!")
|
||||
}
|
||||
|
||||
default:
|
||||
red.Println("Unknown codeword encounterd")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if message != nil {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// Message construct holds refernces to codewords.
|
||||
// The Payload is a seies of codewords of message type.
|
||||
type Message struct {
|
||||
Reciptient *Codeword
|
||||
Payload []*Codeword
|
||||
}
|
||||
|
||||
// NewMessage creates a new message construct ready to accept payload codewords
|
||||
func NewMessage(reciptient *Codeword) *Message {
|
||||
return &Message{
|
||||
Reciptient: reciptient,
|
||||
Payload: []*Codeword{},
|
||||
}
|
||||
}
|
||||
|
||||
// AddPayload codeword to a message. Must be codeword of CodewordTypeMessage type
|
||||
// to make sense.
|
||||
func (m *Message) AddPayload(codeword *Codeword) {
|
||||
m.Payload = append(m.Payload, codeword)
|
||||
}
|
||||
|
||||
// ReciptientString returns the reciptient address as a hexadecimal representation,
|
||||
// with the function bits as 0 or 1.
|
||||
func (m *Message) ReciptientString() string {
|
||||
bytes := utils.MSBBitsToBytes(m.Reciptient.Payload[0:17], 8)
|
||||
addr := uint(bytes[1])
|
||||
addr += uint(bytes[0]) << 8
|
||||
|
||||
return fmt.Sprintf("%X:%s%s", addr,
|
||||
utils.TernaryStr(bool(m.Reciptient.Payload[18]), "1", "0"),
|
||||
utils.TernaryStr(bool(m.Reciptient.Payload[19]), "1", "0"))
|
||||
}
|
||||
|
||||
// IsValid returns true if no parity bit check errors occurs in the message payload
|
||||
// or the reciptient address.
|
||||
func (m *Message) IsValid() bool {
|
||||
|
||||
if !m.Reciptient.ValidParity {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range m.Payload {
|
||||
if !c.ValidParity {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// PayloadString can try to decide to print the message as bitcoded decimal ("bcd") or
|
||||
// as an alphanumeric string. There is not always a clear indication which is correct,
|
||||
// so we can force either type by setting messagetype to something other than Auto.
|
||||
func (m *Message) PayloadString(messagetype MessageType) string {
|
||||
|
||||
bits := m.concactenateBits()
|
||||
alphanum := m.AlphaPayloadString(bits)
|
||||
|
||||
if messagetype == MessageTypeAuto {
|
||||
|
||||
if m.isAlphaNumericMessage(alphanum) {
|
||||
messagetype = MessageTypeAlphanumeric
|
||||
} else {
|
||||
messagetype = MessageTypeBitcodedDecimal
|
||||
}
|
||||
}
|
||||
|
||||
switch messagetype {
|
||||
case MessageTypeAlphanumeric:
|
||||
return alphanum
|
||||
case MessageTypeBitcodedDecimal:
|
||||
return utils.BitcodedDecimals(bits)
|
||||
default:
|
||||
return alphanum
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// AlphaPayloadString takes bits in LSB to MSB order and decodes them as
|
||||
// 7 bit bytes that will become ASCII text.
|
||||
// Characters outside of ASCII can occur, so we substitude the most common.
|
||||
func (m *Message) AlphaPayloadString(bits []datatypes.Bit) string {
|
||||
|
||||
str := string(utils.LSBBitsToBytes(bits, 7))
|
||||
|
||||
// translate to utf8
|
||||
charmap := map[string]string{
|
||||
"[": "Ä",
|
||||
"\\": "Ö",
|
||||
"]": "Ü",
|
||||
"{": "ä",
|
||||
"|": "ö",
|
||||
"}": "ü",
|
||||
"~": "ß",
|
||||
}
|
||||
for b, s := range charmap {
|
||||
str = strings.Replace(str, b, s, -1)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// concactenateBits to a single bitstream
|
||||
func (m *Message) concactenateBits() []datatypes.Bit {
|
||||
msgsbits := []datatypes.Bit{}
|
||||
|
||||
for _, cw := range m.Payload {
|
||||
if cw.Type == CodewordTypeMessage {
|
||||
msgsbits = append(msgsbits, cw.Payload...)
|
||||
}
|
||||
}
|
||||
return msgsbits
|
||||
}
|
||||
|
||||
// isAlphaNumericMessage tries to figure out if a message is in alpha-numeric format
|
||||
// or Bitcoded decimal format. There is not always a clear indication which is correct,
|
||||
// so we try to guess based on some assumptions:
|
||||
// 1) Alpha numeric messages contains mostly printable charaters.
|
||||
// 2) BCD messages are usually shorter.
|
||||
func (m *Message) isAlphaNumericMessage(persumed string) bool {
|
||||
|
||||
// MessageTypeAuto...
|
||||
// Start guessing
|
||||
|
||||
odds := 0
|
||||
specials := 0
|
||||
const alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 *.,-()<>\n\r"
|
||||
|
||||
for _, char := range persumed {
|
||||
r := rune(char)
|
||||
if strings.IndexRune(alphanum, r) < 0 {
|
||||
specials++
|
||||
}
|
||||
}
|
||||
|
||||
partspecial := float32(specials) / float32(len(persumed))
|
||||
if partspecial < 0.2 {
|
||||
odds += 2
|
||||
}
|
||||
|
||||
if partspecial >= 0.2 {
|
||||
odds += -1
|
||||
}
|
||||
|
||||
if partspecial >= 0.50 {
|
||||
odds += -2
|
||||
}
|
||||
|
||||
if len(persumed) > 25 {
|
||||
odds += 2
|
||||
}
|
||||
|
||||
if len(persumed) > 40 {
|
||||
odds += 3
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
fmt.Printf("odds: %d\nspecial: %d (%0.0f%%)\n\n", odds, specials, (partspecial * 100))
|
||||
}
|
||||
|
||||
return odds > 0
|
||||
}
|
||||
|
||||
//-----------------------------
|
||||
// Batch
|
||||
// Contains codewords. We dont care about frames, we keep the 16 codewords in a single list.
|
||||
type Batch struct {
|
||||
Codewords []*Codeword
|
||||
}
|
||||
|
||||
func NewBatch(bits []datatypes.Bit) (*Batch, error) {
|
||||
if len(bits) != POCSAG_BATCH_LEN {
|
||||
return nil, fmt.Errorf("invalid number of bits in batch: ", len(bits))
|
||||
}
|
||||
|
||||
words := []*Codeword{}
|
||||
for a := 0; a < len(bits); a = a + POCSAG_CODEWORD_LEN {
|
||||
word, err := NewCodeword(bits[a : a+POCSAG_CODEWORD_LEN])
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
} else {
|
||||
words = append(words, word)
|
||||
}
|
||||
}
|
||||
|
||||
b := &Batch{
|
||||
Codewords: words,
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Print will print a list with the codewords of this bach. FOr debugging.
|
||||
func (b *Batch) Print() {
|
||||
for _, w := range b.Codewords {
|
||||
w.Print()
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------
|
||||
// Codeword contains the actual data. There are two codewords per frame,
|
||||
// and there are 8 frames per batch.
|
||||
// Type can be either Address or Message, and a special Idle codeword will occur
|
||||
// from time to time.
|
||||
// Payload is a stream of bits, and ValidParity bit is set on creation for later
|
||||
// reference.
|
||||
type Codeword struct {
|
||||
Type CodewordType
|
||||
Payload []datatypes.Bit
|
||||
ParityBits []datatypes.Bit
|
||||
EvenParity datatypes.Bit
|
||||
ValidParity bool
|
||||
}
|
||||
|
||||
// NewCodeword takes 32 bits, creates a new codeword construct, sets the type and checks for parity errors.
|
||||
func NewCodeword(bits []datatypes.Bit) (*Codeword, error) {
|
||||
if len(bits) != 32 {
|
||||
return nil, fmt.Errorf("invalid number of bits for codeword: ", len(bits))
|
||||
}
|
||||
|
||||
mtype := CodewordTypeAddress
|
||||
if bits[0] == true {
|
||||
mtype = CodewordTypeMessage
|
||||
}
|
||||
|
||||
bytes := utils.MSBBitsToBytes(bits, 8)
|
||||
if isIdle(bytes) {
|
||||
mtype = CodewordTypeIdle
|
||||
}
|
||||
|
||||
c := &Codeword{
|
||||
Type: mtype,
|
||||
Payload: bits[1:21],
|
||||
ParityBits: bits[21:31],
|
||||
EvenParity: bits[31],
|
||||
ValidParity: utils.ParityCheck(bits[0:31], bits[31]),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Print the codeword contents and type to terminal. For debugging.
|
||||
func (c *Codeword) Print() {
|
||||
|
||||
payload := ""
|
||||
var color *color.Color = blue
|
||||
|
||||
switch c.Type {
|
||||
|
||||
case CodewordTypeAddress:
|
||||
payload = c.Adress()
|
||||
color = red
|
||||
|
||||
case CodewordTypeMessage:
|
||||
payload = ""
|
||||
color = green
|
||||
|
||||
default:
|
||||
color = blue
|
||||
|
||||
}
|
||||
|
||||
parity := utils.TernaryStr(c.ValidParity, "", "*")
|
||||
|
||||
color.Printf("%s %s %s\n", c.Type, payload, parity)
|
||||
}
|
||||
|
||||
func (c *Codeword) Adress() string {
|
||||
bytes := utils.MSBBitsToBytes(c.Payload[0:17], 8)
|
||||
addr := uint(bytes[1])
|
||||
addr += uint(bytes[0]) << 8
|
||||
|
||||
return fmt.Sprintf("%X:%s%s", addr,
|
||||
utils.TernaryStr(bool(c.Payload[18]), "1", "0"),
|
||||
utils.TernaryStr(bool(c.Payload[19]), "1", "0"))
|
||||
|
||||
}
|
||||
|
||||
// Utilities
|
||||
// isPreamble matches 4 bytes to the POCSAG preamble 0x7CD215D8
|
||||
func isPreamble(bytes []byte) bool {
|
||||
|
||||
var a uint32 = 0
|
||||
a += uint32(bytes[0]) << 24
|
||||
a += uint32(bytes[1]) << 16
|
||||
a += uint32(bytes[2]) << 8
|
||||
a += uint32(bytes[3])
|
||||
|
||||
return a == POCSAG_PREAMBLE
|
||||
}
|
||||
|
||||
// isIdle matches 4 bytes to the POCSAG idle codeword 0x7A89C197
|
||||
func isIdle(bytes []byte) bool {
|
||||
|
||||
var a uint32 = 0
|
||||
a += uint32(bytes[0]) << 24
|
||||
a += uint32(bytes[1]) << 16
|
||||
a += uint32(bytes[2]) << 8
|
||||
a += uint32(bytes[3])
|
||||
|
||||
return a == POCSAG_IDLE
|
||||
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
package pocsag
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/datatypes"
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/utils"
|
||||
)
|
||||
|
||||
type StreamReader struct {
|
||||
Stream *bufio.Reader
|
||||
// 0 for auto
|
||||
baud int
|
||||
}
|
||||
|
||||
// NewStreamReader returns a new stream reader for the source provided.
|
||||
// Set bauds 0 for automatic detection.
|
||||
func NewStreamReader(source io.Reader, bauds int) *StreamReader {
|
||||
|
||||
return &StreamReader{
|
||||
Stream: bufio.NewReader(source),
|
||||
baud: bauds,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// StartScan takes a channel on which bitstreams will be written when found and parsed.
|
||||
// The scanner will continue indefently and sleep for 3 ms per cycle to go easy on the system load.
|
||||
func (s *StreamReader) StartScan(bitstream chan []datatypes.Bit) {
|
||||
|
||||
fmt.Println("Starting transmission scanner")
|
||||
|
||||
for {
|
||||
|
||||
bytes := make([]byte, 4096)
|
||||
c, err := s.Stream.Read(bytes)
|
||||
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
stream := s.bToInt16(bytes[:c])
|
||||
|
||||
start, bitlength := s.ScanTransmissionStart(stream)
|
||||
|
||||
if start > 0 {
|
||||
|
||||
blue.Println("-- Transmission received at", time.Now(), "--------------")
|
||||
|
||||
transmission := s.ReadTransmission(stream[start:])
|
||||
|
||||
bits := utils.StreamToBits(transmission, bitlength)
|
||||
bitstream <- bits
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Millisecond)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// ReadTransmission reads the beginning and subsequent datapackages into
|
||||
// a new buffer until encounters noise instead of signal.
|
||||
func (s *StreamReader) ReadTransmission(beginning []int16) []int16 {
|
||||
|
||||
stream := make([]int16, 0)
|
||||
stream = append(stream, beginning...)
|
||||
|
||||
for {
|
||||
|
||||
bytes := make([]byte, 4096)
|
||||
c, _ := s.Stream.Read(bytes)
|
||||
|
||||
if c > 0 {
|
||||
|
||||
bstr := s.bToInt16(bytes[:c])
|
||||
stream = append(stream, bstr...)
|
||||
|
||||
if s.isNoise(bstr) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
// ScanTransmissionStart scans for repeated 1010101010101 pattern of bits in the
|
||||
// stream. A minimum of 400 samples is required to sucessfully sync the receiver with
|
||||
// the stream. ScanTransmissionStart looks for when the signal wave changes from high to low
|
||||
// and reversed, and measures the distance between those changes. They should correspond to
|
||||
// the bitlength determined by the current baud-rate. An attempt at guessing the baudrate is
|
||||
// also made when a repeated pattern is found.
|
||||
// retuned is the index of the stream on which the caller should begin reading bits, and
|
||||
// the estimated bitlength, the number of samples between each bit center in transmission stream.
|
||||
func (s *StreamReader) ScanTransmissionStart(stream []int16) (int, int) {
|
||||
|
||||
if len(stream) == 0 {
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
switches := []int{}
|
||||
prevsamp := stream[0]
|
||||
first_switch := -1
|
||||
|
||||
// find the indexes where we cross the 0-boundary
|
||||
// if we switch sides we store the index in an array for further analasys
|
||||
for a, sample := range stream {
|
||||
|
||||
if (prevsamp > 0 && sample < 0) || (prevsamp < 0 && sample > 0) {
|
||||
switches = append(switches, a)
|
||||
if first_switch < 0 {
|
||||
first_switch = a
|
||||
}
|
||||
}
|
||||
|
||||
prevsamp = sample
|
||||
}
|
||||
|
||||
// find the mean distance between boundary corsses
|
||||
sum := 0.0
|
||||
for a := 0; a < len(switches)-1; a += 1 {
|
||||
sum += float64(switches[a+1] - switches[a])
|
||||
}
|
||||
|
||||
mean_bitlength := sum / float64(len(switches)-1)
|
||||
|
||||
bitlength := float64(s.bitlength(int(mean_bitlength)))
|
||||
|
||||
// if bitlength is not on a scale of known baudrates then
|
||||
// we probably don't have a pocsag sync-transmission
|
||||
if bitlength < 0 {
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
fmt.Println("Mean bitlength:", mean_bitlength)
|
||||
fmt.Println("Determined bitlength:", bitlength)
|
||||
}
|
||||
|
||||
// look at every other sample to see if we have a repeating pattern with matching size
|
||||
confidence := 0
|
||||
for a := 0; a < len(switches)-3; a += 1 {
|
||||
|
||||
// length from switch a to a+1
|
||||
w1 := float64(switches[a+1] - switches[a])
|
||||
w2 := float64(switches[a+3] - switches[a+2])
|
||||
|
||||
// how much the persumed bits vary from eachother
|
||||
intravariance := (w1 / w2) - 1
|
||||
if intravariance < 0 {
|
||||
intravariance = intravariance * -1
|
||||
}
|
||||
|
||||
// how much the persumed bits vary from the determined bitlength
|
||||
baudvariance := (w1 / bitlength) - 1
|
||||
if baudvariance < 0 {
|
||||
baudvariance = baudvariance * -1
|
||||
}
|
||||
|
||||
// don't stray more than 20%
|
||||
if intravariance < 0.2 && baudvariance < 0.2 {
|
||||
confidence += 1
|
||||
} else {
|
||||
confidence = 0
|
||||
}
|
||||
|
||||
if confidence > 10 {
|
||||
|
||||
if DEBUG {
|
||||
fmt.Println("Found bitsync")
|
||||
}
|
||||
|
||||
return switches[a] + int(bitlength/2), int(bitlength)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// bitlength returns the proper bitlength from a calcualated mean distance between
|
||||
// wave transitions. If the baudrate is set by configuration then that is used instead.
|
||||
func (s *StreamReader) bitlength(mean int) int {
|
||||
|
||||
if mean > 150 && mean < 170 {
|
||||
return 160
|
||||
} else if mean > 75 && mean < 85 || s.baud == 600 {
|
||||
return 80
|
||||
} else if mean > 35 && mean < 45 || s.baud == 1200 {
|
||||
return 40
|
||||
} else if mean > 15 && mean < 25 || s.baud == 2400 {
|
||||
return 20
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// isNoise detects noise by calculating the number of times the signal goes over the 0-line
|
||||
// during a signal this value is between 25 and 50, but noise is above 100, usually around 300-400.
|
||||
func (s *StreamReader) isNoise(stream []int16) bool {
|
||||
|
||||
if len(stream) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
prevsamp := stream[0]
|
||||
switches := 0
|
||||
|
||||
// find the indexes where we cross the 0-boundary
|
||||
for _, sample := range stream {
|
||||
|
||||
if (prevsamp > 0 && sample < 0) || (prevsamp < 0 && sample > 0) {
|
||||
switches += 1
|
||||
}
|
||||
|
||||
prevsamp = sample
|
||||
}
|
||||
|
||||
return switches > 100
|
||||
}
|
||||
|
||||
// bToInt16 converts bytes to int16
|
||||
func (s *StreamReader) bToInt16(b []byte) (u []int16) {
|
||||
u = make([]int16, len(b)/2)
|
||||
for i, _ := range u {
|
||||
val := int16(b[i*2])
|
||||
val += int16(b[i*2+1]) << 8
|
||||
u[i] = val
|
||||
}
|
||||
return
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/datatypes"
|
||||
)
|
||||
|
||||
var (
|
||||
DEBUG bool
|
||||
)
|
||||
|
||||
var (
|
||||
green = color.New(color.FgGreen)
|
||||
red = color.New(color.FgRed)
|
||||
blue = color.New(color.FgBlue)
|
||||
)
|
||||
|
||||
func SetDebug(d bool) {
|
||||
DEBUG = d
|
||||
}
|
||||
|
||||
// StreamToBits converts samples to bits using the bitlength specified.
|
||||
// Observe that POCSAG signifies a high bit with a low frequency.
|
||||
func StreamToBits(stream []int16, bitlength int) []datatypes.Bit {
|
||||
|
||||
bits := make([]datatypes.Bit, (len(stream)/bitlength)+1)
|
||||
b := 0
|
||||
|
||||
for a := 0; a < len(stream); a += bitlength {
|
||||
|
||||
sample := stream[a]
|
||||
if a > 2 && a < len(stream)-2 {
|
||||
// let the samples before and after influence our sample, to prevent spike errors
|
||||
sample = (stream[a-1] / 2) + stream[a] + (stream[a+1] / 2)
|
||||
}
|
||||
|
||||
bits[b] = datatypes.Bit((sample < 0))
|
||||
b += 1
|
||||
|
||||
}
|
||||
|
||||
return bits
|
||||
}
|
||||
|
||||
// MSBBitsToBytes converts bitsream to bytes using MSB to LSB order.
|
||||
func MSBBitsToBytes(bits []datatypes.Bit, bitsPerByte int) []byte {
|
||||
|
||||
var b uint8
|
||||
bytes := []byte{}
|
||||
power := bitsPerByte - 1
|
||||
|
||||
for a := 0; a < len(bits); a += 1 {
|
||||
|
||||
bit := bits[a].UInt8()
|
||||
mod := a % bitsPerByte
|
||||
|
||||
if mod == 0 && a > 0 {
|
||||
bytes = append(bytes, b)
|
||||
b = 0
|
||||
}
|
||||
|
||||
pow := uint(power - mod)
|
||||
b += (bit * (1 << pow))
|
||||
|
||||
}
|
||||
|
||||
if len(bits)%bitsPerByte == 0 {
|
||||
bytes = append(bytes, b)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
// LSBBitsToBytes converts bitsream to bytes using LSB to MSB order.
|
||||
func LSBBitsToBytes(bits []datatypes.Bit, bitsPerByte int) []byte {
|
||||
|
||||
var b uint8
|
||||
bytes := []byte{}
|
||||
|
||||
for a := 0; a < len(bits); a += 1 {
|
||||
|
||||
bit := bits[a].UInt8()
|
||||
mod := a % bitsPerByte
|
||||
|
||||
if mod == 0 && a > 0 {
|
||||
bytes = append(bytes, b)
|
||||
b = 0
|
||||
}
|
||||
|
||||
pow := uint(mod)
|
||||
b += (bit * (1 << pow))
|
||||
|
||||
}
|
||||
|
||||
if len(bits)%bitsPerByte == 0 {
|
||||
bytes = append(bytes, b)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
// simple parity check
|
||||
func ParityCheck(bits []datatypes.Bit, even_bit datatypes.Bit) bool {
|
||||
|
||||
sum := even_bit.Int()
|
||||
for _, b := range bits {
|
||||
if b {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return (sum % 2) == 0
|
||||
}
|
||||
|
||||
// BitcodedDecimals takes 4 bits per decimal to create values between 0 and 15.
|
||||
// *) values 0-9 are used as is
|
||||
// *) values 10-14 are special characters translated by bcdSpecial()
|
||||
// *) value = 15 is not used.
|
||||
func BitcodedDecimals(bits []datatypes.Bit) string {
|
||||
|
||||
msg := ""
|
||||
var foo uint8 = 0
|
||||
|
||||
bitsPerByte := 4
|
||||
|
||||
for a := 0; a < len(bits); a += 1 {
|
||||
|
||||
bit := bits[a].UInt8()
|
||||
mod := a % bitsPerByte
|
||||
|
||||
if mod == 0 && a > 0 {
|
||||
msg += bcdChar(foo)
|
||||
foo = 0
|
||||
}
|
||||
|
||||
pow := uint(mod)
|
||||
foo += (bit * (1 << pow))
|
||||
|
||||
}
|
||||
|
||||
if len(bits)%bitsPerByte == 0 {
|
||||
msg += bcdChar(foo)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// bcdChar translates digits and non-digit bitcoded entitis to charaters as per POCSAG protocol
|
||||
func bcdChar(foo uint8) string {
|
||||
|
||||
if foo < 10 {
|
||||
return fmt.Sprintf("%d", foo)
|
||||
}
|
||||
|
||||
if foo == 10 {
|
||||
return ""
|
||||
}
|
||||
|
||||
chars := []string{
|
||||
"",
|
||||
"U",
|
||||
" ",
|
||||
"-",
|
||||
")",
|
||||
"(",
|
||||
}
|
||||
return chars[foo-10]
|
||||
}
|
||||
|
||||
func TernaryStr(condition bool, a, b string) string {
|
||||
if condition {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// PrintStream, used for debugging of streams
|
||||
func PrintStream(samples []int16) {
|
||||
for _, sample := range samples {
|
||||
PrintSample(sample)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintBitstream, used for debugging of streams
|
||||
func PrintBitstream(bits []datatypes.Bit) {
|
||||
for _, b := range bits {
|
||||
PrintSample(int16(b.Int()))
|
||||
}
|
||||
}
|
||||
|
||||
// PrintSample, used for debugging of streams
|
||||
func PrintSample(sample int16) {
|
||||
if sample > 0 {
|
||||
green.Printf("%d ", sample)
|
||||
} else {
|
||||
red.Printf("%d ", sample)
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "gopkg.in/check.v1"
|
||||
"testing"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/datatypes"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
var _ = Suite(&UtilitiesSuite{})
|
||||
|
||||
type UtilitiesSuite struct{}
|
||||
|
||||
func (f *UtilitiesSuite) Test_MSB_BitsToBytes_8_FF(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true, true, true, true, true,
|
||||
}
|
||||
c.Assert(MSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_MSB_BitsToBytes_16_FFFF(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true, true, true, true, true,
|
||||
true, true, true, true, true, true, true, true,
|
||||
}
|
||||
c.Assert(MSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF, 0xFF})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_MSB_BitsToBytes_16_FF00(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true, true, true, true, true,
|
||||
false, false, false, false, false, false, false, false,
|
||||
}
|
||||
c.Assert(MSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF, 0x00})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_LSB_BitsToBytes_8_FF(c *C) {
|
||||
bits := []datatypes.Bit{true, true, true, true, true, true, true, true}
|
||||
c.Assert(LSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_LSB_BitsToBytes_16_FFFF(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true, true, true, true, true,
|
||||
true, true, true, true, true, true, true, true,
|
||||
}
|
||||
c.Assert(LSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF, 0xFF})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_LSB_BitsToBytes_16_FF00(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
false, false, false, false, false, false, false, false,
|
||||
true, true, true, true, true, true, true, true,
|
||||
}
|
||||
c.Assert(LSBBitsToBytes(bits, 8), DeepEquals, []byte{0x00, 0xFF})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_LSB_BitsToBytes_16_00FF(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true, true, true, true, true,
|
||||
false, false, false, false, false, false, false, false,
|
||||
}
|
||||
c.Assert(LSBBitsToBytes(bits, 8), DeepEquals, []byte{0xFF, 0x00})
|
||||
}
|
||||
|
||||
// POCSAG speceific
|
||||
|
||||
func (f *UtilitiesSuite) Test_MSBBitsToBytes_Keyword_PREAMBLE(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
false, true, true, true,
|
||||
true, true, false, false,
|
||||
|
||||
true, true, false, true,
|
||||
false, false, true, false,
|
||||
|
||||
false, false, false, true,
|
||||
false, true, false, true,
|
||||
|
||||
true, true, false, true,
|
||||
true, false, false, false,
|
||||
}
|
||||
c.Assert(MSBBitsToBytes(bits, 8), DeepEquals, []byte{0x7C, 0xD2, 0x15, 0xD8})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_MSBBitsToBytes_Keyword_IDLE(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
false, true, true, true,
|
||||
true, false, true, false,
|
||||
|
||||
true, false, false, false,
|
||||
true, false, false, true,
|
||||
|
||||
true, true, false, false,
|
||||
false, false, false, true,
|
||||
|
||||
true, false, false, true,
|
||||
false, true, true, true,
|
||||
}
|
||||
c.Assert(MSBBitsToBytes(bits, 8), DeepEquals, []byte{0x7A, 0x89, 0xC1, 0x97})
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_BCD_Min(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
false, false, false, false,
|
||||
}
|
||||
c.Assert(BitcodedDecimals(bits), Equals, "0")
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_BCD_Max(c *C) {
|
||||
bits := []datatypes.Bit{
|
||||
true, true, true, true,
|
||||
}
|
||||
c.Assert(BitcodedDecimals(bits), Equals, "(")
|
||||
}
|
||||
|
||||
func (f *UtilitiesSuite) Test_BCD_10chars(c *C) {
|
||||
|
||||
bits := []datatypes.Bit{
|
||||
false, false, false, false,
|
||||
true, true, true, false,
|
||||
false, false, false, false,
|
||||
true, true, true, false,
|
||||
|
||||
true, false, false, false,
|
||||
true, false, false, true,
|
||||
|
||||
true, true, false, false,
|
||||
true, true, false, false,
|
||||
|
||||
false, false, false, true,
|
||||
true, false, true, false,
|
||||
}
|
||||
|
||||
c.Assert(BitcodedDecimals(bits), Equals, "0707193385")
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package wav
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
bin "encoding/binary"
|
||||
"os"
|
||||
)
|
||||
|
||||
type WavData struct {
|
||||
bChunkID [4]byte // B
|
||||
ChunkSize uint32 // L
|
||||
bFormat [4]byte // B
|
||||
|
||||
bSubchunk1ID [4]byte // B
|
||||
Subchunk1Size uint32 // L
|
||||
AudioFormat uint16 // L
|
||||
NumChannels uint16 // L
|
||||
SampleRate uint32 // L
|
||||
ByteRate uint32 // L
|
||||
BlockAlign uint16 // L
|
||||
BitsPerSample uint16 // L
|
||||
|
||||
bSubchunk2ID [4]byte // B
|
||||
Subchunk2Size uint32 // L
|
||||
Data []byte // L
|
||||
}
|
||||
|
||||
func NewWavData(fn string) (*WavData, error) {
|
||||
res, err := os.OpenFile(fn, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file := bufio.NewReader(res)
|
||||
|
||||
wav := &WavData{}
|
||||
bin.Read(file, bin.BigEndian, &wav.bChunkID)
|
||||
bin.Read(file, bin.LittleEndian, &wav.ChunkSize)
|
||||
bin.Read(file, bin.BigEndian, &wav.bFormat)
|
||||
|
||||
bin.Read(file, bin.BigEndian, &wav.bSubchunk1ID)
|
||||
bin.Read(file, bin.LittleEndian, &wav.Subchunk1Size)
|
||||
bin.Read(file, bin.LittleEndian, &wav.AudioFormat)
|
||||
bin.Read(file, bin.LittleEndian, &wav.NumChannels)
|
||||
bin.Read(file, bin.LittleEndian, &wav.SampleRate)
|
||||
bin.Read(file, bin.LittleEndian, &wav.ByteRate)
|
||||
bin.Read(file, bin.LittleEndian, &wav.BlockAlign)
|
||||
bin.Read(file, bin.LittleEndian, &wav.BitsPerSample)
|
||||
|
||||
bin.Read(file, bin.BigEndian, &wav.bSubchunk2ID)
|
||||
bin.Read(file, bin.LittleEndian, &wav.Subchunk2Size)
|
||||
|
||||
wav.Data = make([]byte, wav.Subchunk2Size)
|
||||
bin.Read(file, bin.LittleEndian, &wav.Data)
|
||||
|
||||
return wav, nil
|
||||
}
|
||||
|
||||
func (w *WavData) SampleCount() int {
|
||||
return int(len(w.Data) / 2)
|
||||
}
|
||||
|
||||
func (w *WavData) Sample(index int) int16 {
|
||||
in := index * 2
|
||||
return btoi16(w.Data[in : in+2])
|
||||
}
|
||||
|
||||
func btoi16(b []byte) int16 {
|
||||
value := int16(b[0])
|
||||
value += int16(b[1]) << 8
|
||||
return value
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/datatypes"
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/pocsag"
|
||||
"bitbucket.org/dhogborg/go-pocsag/internal/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
config *Config
|
||||
)
|
||||
|
||||
var (
|
||||
green = color.New(color.FgGreen)
|
||||
red = color.New(color.FgRed)
|
||||
blue = color.New(color.FgBlue)
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
input string
|
||||
baud int
|
||||
debug bool
|
||||
messagetype pocsag.MessageType
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "go-pocsag"
|
||||
app.Usage = "Parse audiostream for POCSAG messages"
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "input,i",
|
||||
Value: "",
|
||||
Usage: "wav file or data dump with signed 16 bit ints",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "baud,b",
|
||||
Value: 0,
|
||||
Usage: "Baud 600/1200/2400. Default auto",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Debug mode",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "type,t",
|
||||
Value: "auto",
|
||||
Usage: "Force message type: alpha, bcd, auto",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) {
|
||||
config = &Config{
|
||||
input: c.String("input"),
|
||||
baud: c.Int("baud"),
|
||||
debug: c.Bool("debug"),
|
||||
messagetype: pocsag.MessageType(c.String("type")),
|
||||
}
|
||||
|
||||
utils.SetDebug(config.debug)
|
||||
pocsag.SetDebug(config.debug)
|
||||
|
||||
Run()
|
||||
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func Run() {
|
||||
|
||||
var source io.Reader
|
||||
|
||||
if config.input == "-" {
|
||||
source = os.Stdin
|
||||
} else { // file reading
|
||||
source = pocsag.ReadWav(config.input)
|
||||
}
|
||||
|
||||
if source == nil {
|
||||
println("invalid input")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
reader := pocsag.NewStreamReader(source, config.baud)
|
||||
|
||||
bitstream := make(chan []datatypes.Bit, 1)
|
||||
go reader.StartScan(bitstream)
|
||||
|
||||
for {
|
||||
bits := <-bitstream
|
||||
pocsag.ParsePOCSAG(bits, config.messagetype)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue