commit a95885556854afbe1e5f740f95de22eb4661015a Author: David Högborg Date: Wed Oct 21 10:51:01 2015 +0100 Initial public commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b7911c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +gopocsag \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b485dfb --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f67ea2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# go-pocsag + +A parser for POCSAG pager protocol implemented in Go \ No newline at end of file diff --git a/internal/datatypes/bit.go b/internal/datatypes/bit.go new file mode 100644 index 0000000..1686667 --- /dev/null +++ b/internal/datatypes/bit.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()) +} diff --git a/internal/datatypes/bit_test.go b/internal/datatypes/bit_test.go new file mode 100644 index 0000000..71fdd38 --- /dev/null +++ b/internal/datatypes/bit_test.go @@ -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)) +} diff --git a/internal/pocsag/common.go b/internal/pocsag/common.go new file mode 100644 index 0000000..1bd56a9 --- /dev/null +++ b/internal/pocsag/common.go @@ -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 +} diff --git a/internal/pocsag/file.go b/internal/pocsag/file.go new file mode 100644 index 0000000..88b47c7 --- /dev/null +++ b/internal/pocsag/file.go @@ -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 + +} diff --git a/internal/pocsag/pocsag.go b/internal/pocsag/pocsag.go new file mode 100644 index 0000000..05d1e0f --- /dev/null +++ b/internal/pocsag/pocsag.go @@ -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 + +} diff --git a/internal/pocsag/stream.go b/internal/pocsag/stream.go new file mode 100644 index 0000000..e3c3c91 --- /dev/null +++ b/internal/pocsag/stream.go @@ -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 +} diff --git a/internal/utils/util.go b/internal/utils/util.go new file mode 100644 index 0000000..4d2250f --- /dev/null +++ b/internal/utils/util.go @@ -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) + } +} diff --git a/internal/utils/util_test.go b/internal/utils/util_test.go new file mode 100644 index 0000000..b0b7036 --- /dev/null +++ b/internal/utils/util_test.go @@ -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") +} diff --git a/internal/wav/wav.go b/internal/wav/wav.go new file mode 100644 index 0000000..86dd1f9 --- /dev/null +++ b/internal/wav/wav.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b585070 --- /dev/null +++ b/main.go @@ -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) + } +}