inital commit
This commit is contained in:
commit
cfe7772b7d
3 changed files with 404 additions and 0 deletions
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Go port of the POCSAG::Encode Perl module.
|
||||||
|
|
364
pocsag-encode.go
Normal file
364
pocsag-encode.go
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
package pocsagencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message is a single POCSAG Alphanumeric message
|
||||||
|
type Message struct {
|
||||||
|
Addr uint32
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger *log.Logger
|
||||||
|
|
||||||
|
// SetLogger can be passed a *log.Logger to enable log output
|
||||||
|
// Example pocsagencoder.SetLogger(log.New(os.Stdout, "POCSAG ", log.LstdFlags))
|
||||||
|
func SetLogger(Logger *log.Logger) {
|
||||||
|
logger = Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugf(format string, args ...interface{}) {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Printf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The POCSAG transmission starts with 576 bit reversals (101010...).
|
||||||
|
// That's 576/8 == 72 bytes of 0xAA.
|
||||||
|
var pocsagPreambleWord uint32 = 0xAAAAAAAA
|
||||||
|
|
||||||
|
// The Frame Synchronisation (FS) code is 32 bits:
|
||||||
|
// 01111100 11010010 00010101 11011000
|
||||||
|
var pocsagFrameSyncWord uint32 = 0x7CD215D8
|
||||||
|
|
||||||
|
// The Idle Codeword:
|
||||||
|
// 01111010 10001001 11000001 10010111
|
||||||
|
var pocsagIdleWord uint32 = 0x7A89C197
|
||||||
|
|
||||||
|
// calcBchAndParity calculates the binary checksum and parity for a codeword
|
||||||
|
func calcBchAndParity(cw uint32) uint32 {
|
||||||
|
|
||||||
|
// make sure the 11 LSB are 0.
|
||||||
|
cw &= 0xFFFFF800
|
||||||
|
|
||||||
|
parity := 0
|
||||||
|
|
||||||
|
// calculate bch
|
||||||
|
localCw := cw
|
||||||
|
for bit := 1; bit <= 21; bit++ {
|
||||||
|
if cw&0x80000000 > 0 {
|
||||||
|
cw ^= 0xED200000
|
||||||
|
}
|
||||||
|
cw = cw << 1
|
||||||
|
}
|
||||||
|
localCw |= (cw >> 21)
|
||||||
|
// at this point $local_cw has codeword with bch
|
||||||
|
|
||||||
|
// calculate parity
|
||||||
|
cw = localCw
|
||||||
|
for bit := 1; bit <= 32; bit++ {
|
||||||
|
if cw&0x80000000 > 0 {
|
||||||
|
parity++
|
||||||
|
}
|
||||||
|
cw = cw << 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// turn last bit to 1 depending on parity
|
||||||
|
cw_with_parity := localCw
|
||||||
|
if parity%2 != 0 {
|
||||||
|
cw_with_parity = localCw + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
debugf(" bch_and_parity returning %X\n", cw_with_parity)
|
||||||
|
return cw_with_parity
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Given the numeric destination address and function, generate an address codeword.
|
||||||
|
//
|
||||||
|
|
||||||
|
// sub _address_codeword($$)
|
||||||
|
func addressCodeword(inAddr uint32, function byte) uint32 {
|
||||||
|
// POCSAG recommendation 1.3.2
|
||||||
|
// The three least significant bits are not transmitted but
|
||||||
|
// serve to define the frame in which the address codeword
|
||||||
|
// must be transmitted.
|
||||||
|
// So we take them away.
|
||||||
|
// shift address to right by two bits to remove the least significant bits
|
||||||
|
addr := inAddr >> 3
|
||||||
|
|
||||||
|
// truncate address to 18 bits
|
||||||
|
addr &= 0x3FFFF
|
||||||
|
|
||||||
|
// truncate function to 2 bits
|
||||||
|
function &= 0x3
|
||||||
|
|
||||||
|
// codeword without parity
|
||||||
|
codeword := addr<<13 | uint32(function)<<11
|
||||||
|
|
||||||
|
debugf(" generated address codeword for %d function %d: %X\n", inAddr, function, codeword)
|
||||||
|
|
||||||
|
return calcBchAndParity(codeword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendMessageCodeword appends a message content codeword to the message, calculating bch+parity for it
|
||||||
|
func appendMessageCodeword(word uint32) uint32 {
|
||||||
|
return calcBchAndParity(word | 1<<31)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverseBits reverses the bits in a byte. Used to encode characters in a text message,
|
||||||
|
//since the opposite order is used when transmitting POCSAG text.
|
||||||
|
func reverseBits(in byte) byte {
|
||||||
|
out := byte(0)
|
||||||
|
|
||||||
|
for i := byte(0); i < 7; i++ {
|
||||||
|
out |= ((in >> i) & 0x01) << (6 - i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendContentText appends text message content to the transmission blob
|
||||||
|
func appendContentText(content string) (int, []uint32) {
|
||||||
|
out := make([]uint32, 0)
|
||||||
|
debugf("appendContentText: %s", content)
|
||||||
|
|
||||||
|
bitpos := 0
|
||||||
|
word := uint32(0)
|
||||||
|
leftbits := 0
|
||||||
|
pos := 0
|
||||||
|
|
||||||
|
// walk through characters in message
|
||||||
|
for i, r := range content {
|
||||||
|
// make sure it's 7 bits
|
||||||
|
char := byte(r & 0x7f)
|
||||||
|
|
||||||
|
debugf(" char %d: %d [%X]\n", i, char, char)
|
||||||
|
|
||||||
|
char = reverseBits(char)
|
||||||
|
|
||||||
|
// if the bits won't fit:
|
||||||
|
if bitpos+7 > 20 {
|
||||||
|
space := 20 - bitpos
|
||||||
|
// leftbits least significant bits of $char are left over in the next word
|
||||||
|
leftbits = 7 - space
|
||||||
|
debugf(" bits of char won't fit since bitpos is %d, got %d bits free, leaving %d bits in next word", bitpos, space, leftbits)
|
||||||
|
}
|
||||||
|
|
||||||
|
word |= (uint32(char) << (31 - 7 - bitpos))
|
||||||
|
|
||||||
|
bitpos += 7
|
||||||
|
|
||||||
|
if bitpos >= 20 {
|
||||||
|
debugf(" appending word: %X\n", word)
|
||||||
|
out = append(out, appendMessageCodeword(word))
|
||||||
|
pos++
|
||||||
|
word = 0
|
||||||
|
bitpos = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if leftbits > 0 {
|
||||||
|
word |= (uint32(char) << (31 - leftbits))
|
||||||
|
bitpos = leftbits
|
||||||
|
leftbits = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bitpos > 0 {
|
||||||
|
debugf(" got %d bits in word at end of text, word: %X", bitpos, word)
|
||||||
|
step := 0
|
||||||
|
for bitpos < 20 {
|
||||||
|
if step == 2 {
|
||||||
|
word |= (1 << (30 - bitpos))
|
||||||
|
}
|
||||||
|
bitpos++
|
||||||
|
step++
|
||||||
|
if step == 7 {
|
||||||
|
step = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, appendMessageCodeword(word))
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos, out
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendMessage appends a single message to the end of the transmission blob.
|
||||||
|
func appendMessage(startpos int, msg *Message) (int, []uint32) {
|
||||||
|
// expand the parameters of the message
|
||||||
|
addr := msg.Addr
|
||||||
|
function := byte(0)
|
||||||
|
type_ := 'a'
|
||||||
|
content := msg.Content
|
||||||
|
|
||||||
|
debugf("append_message: addr %d function %d type %d content %s", addr, function, type_, content)
|
||||||
|
|
||||||
|
// the starting frame is selected based on the three least significant bits
|
||||||
|
frameAddr := addr & 7
|
||||||
|
frameAddrCw := frameAddr * 2 // or << 2 ?
|
||||||
|
|
||||||
|
debugf(" frame_addr is %d, current position %d", frameAddr, startpos)
|
||||||
|
|
||||||
|
// append idle codewords, until we're in the right frame for this address
|
||||||
|
tx := make([]uint32, 0)
|
||||||
|
pos := 0
|
||||||
|
for uint32(startpos+pos)%16 != frameAddrCw {
|
||||||
|
debugf(" inserting IDLE codewords in position %d (%d)", startpos+pos, (startpos+pos)%16)
|
||||||
|
tx = append(tx, pocsagIdleWord)
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, append the address codeword, containing the function and the address
|
||||||
|
// (sans 3 least significant bits, which are indicated by the starting frame,
|
||||||
|
// which the receiver is waiting for)
|
||||||
|
tx = append(tx, addressCodeword(addr, function))
|
||||||
|
pos++
|
||||||
|
|
||||||
|
// Next, append the message contents
|
||||||
|
contentEncLen, contentEnc := appendContentText(content)
|
||||||
|
|
||||||
|
tx = append(tx, contentEnc...)
|
||||||
|
pos += contentEncLen
|
||||||
|
|
||||||
|
// Return the current frame position and the binary string to be appended
|
||||||
|
return pos, tx
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertSCS inserts Synchronisation Codewords before every 8 POCSAG frames
|
||||||
|
// (frame is SC+ 64 bytes of address and message codewords)
|
||||||
|
func insertSCS(tx []uint32) []uint32 {
|
||||||
|
out := make([]uint32, 0)
|
||||||
|
|
||||||
|
// each batch is SC + 8 frames, each frame is 2 codewords,
|
||||||
|
// each codeword is 32 bits, so we must insert an SC
|
||||||
|
// every (8*2*32) bits == 64 bytes
|
||||||
|
txLen := len(tx)
|
||||||
|
for i := 0; i < txLen; i += 16 {
|
||||||
|
// put in the CW and 64 the next 64 bytes
|
||||||
|
out = append(out, pocsagFrameSyncWord)
|
||||||
|
end := i + 16
|
||||||
|
if end > txLen {
|
||||||
|
end = txLen
|
||||||
|
}
|
||||||
|
out = append(out, tx[i:end]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectMsg selects the optimal next message to be appended, trying to
|
||||||
|
// minimize the amount of idle codewords transmitted
|
||||||
|
func selectMsg(pos int, msgListRef []*Message) int {
|
||||||
|
currentPick := -1
|
||||||
|
currentDist := 0
|
||||||
|
posFrame := uint32(pos/2) % 8
|
||||||
|
|
||||||
|
debugf("select_msg pos %d: %d", pos, posFrame)
|
||||||
|
|
||||||
|
for i := 0; i < len(msgListRef); i++ {
|
||||||
|
addr := msgListRef[i].Addr
|
||||||
|
frameAddr := addr & 7
|
||||||
|
distance := int(frameAddr - posFrame)
|
||||||
|
if distance < 0 {
|
||||||
|
distance += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
debugf(" considering list item %d: %d - frame addr %d distance %d\n", i, addr, frameAddr, distance)
|
||||||
|
|
||||||
|
if frameAddr == posFrame {
|
||||||
|
debugf(" exact match %d: %d - frame addr %d\n", i, addr, frameAddr)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentPick == -1 {
|
||||||
|
debugf(" first option %d: %d - frame addr %d distance %d\n", i, addr, frameAddr, distance)
|
||||||
|
currentPick = i
|
||||||
|
currentDist = distance
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if distance < currentDist {
|
||||||
|
debugf(" better option %d: %d - frame addr %d distance %d", i, addr, frameAddr, distance)
|
||||||
|
currentPick = i
|
||||||
|
currentDist = distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPick
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a transmission from an array of given messages, to fit with the maximum lenght
|
||||||
|
// The function returns the an array of Uint32 to be keyed over the air in FSK, and
|
||||||
|
// any messages which did not fit in the transmission, given the maximum
|
||||||
|
// transmission length (in bytes) given in the first parameter. They can be passed
|
||||||
|
// in the next Generate() call and sent in the next brrraaaap.
|
||||||
|
func Generate(maxLen int, preambleBits int, messages []*Message) ([]uint32, []*Message) {
|
||||||
|
txWithoutScs := make([]uint32, 0)
|
||||||
|
debugf("generate_transmission, maxlen: %d", maxLen)
|
||||||
|
|
||||||
|
pos := 0
|
||||||
|
for len(messages) > 0 {
|
||||||
|
// figure out an optimal next message to minimize the amount of required idle codewords
|
||||||
|
// TODO: do a deeper search, considering the length of the message and a possible
|
||||||
|
// optimal next recipient
|
||||||
|
optimalNextMsg := selectMsg(pos, messages)
|
||||||
|
msg := messages[optimalNextMsg]
|
||||||
|
messages = append(messages[:optimalNextMsg], messages[optimalNextMsg+1:]...)
|
||||||
|
|
||||||
|
appendLen, x := appendMessage(pos, msg)
|
||||||
|
|
||||||
|
nextLen := pos + appendLen + 2
|
||||||
|
// initial sync codeword + one for every 16 codewords
|
||||||
|
nextLen += 1 + int((nextLen-1)/16)
|
||||||
|
nextLenBytes := nextLen * 4
|
||||||
|
debugf("after this message of %d codewords, burst will be %d codewords and %d bytes long\n", appendLen, nextLen, nextLenBytes)
|
||||||
|
|
||||||
|
if nextLenBytes > maxLen {
|
||||||
|
if pos == 0 {
|
||||||
|
debugf("burst would become too large (%d > %d) with first message alone - discarding!", nextLenBytes, maxLen)
|
||||||
|
} else {
|
||||||
|
debugf("burst would become too large (%d > %d) - returning msg in queue", nextLenBytes, maxLen)
|
||||||
|
messages = append([]*Message{msg}, messages...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txWithoutScs = append(txWithoutScs, x...)
|
||||||
|
pos += appendLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the burst is empty, return it as completely empty
|
||||||
|
if pos == 0 {
|
||||||
|
return []uint32{}, messages
|
||||||
|
}
|
||||||
|
|
||||||
|
// append a couple of IDLE codewords, otherwise many pagers will
|
||||||
|
// happily decode the junk in the end and show it to the recipient
|
||||||
|
txWithoutScs = append(txWithoutScs, pocsagIdleWord)
|
||||||
|
txWithoutScs = append(txWithoutScs, pocsagIdleWord)
|
||||||
|
|
||||||
|
burstLen := len(txWithoutScs)
|
||||||
|
debugf("transmission without SCs: %d bytes, %d codewords\n%X\n", burstLen*4, burstLen, txWithoutScs)
|
||||||
|
|
||||||
|
// put SC every 8 frames
|
||||||
|
burst := insertSCS(txWithoutScs)
|
||||||
|
|
||||||
|
burstLen = len(burst)
|
||||||
|
debugf("transmission with SCs: %d bytes, %d codewords\n%X\n", burstLen*4, burstLen, burst)
|
||||||
|
|
||||||
|
if preambleBits > 0 {
|
||||||
|
preambleWords := preambleBits / 32
|
||||||
|
if preambleBits%32 > 0 {
|
||||||
|
preambleWords++
|
||||||
|
}
|
||||||
|
preamble := make([]uint32, preambleWords)
|
||||||
|
for i := range preamble {
|
||||||
|
preamble[i] = pocsagPreambleWord
|
||||||
|
}
|
||||||
|
burst = append(preamble, burst...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return burst, messages
|
||||||
|
}
|
38
pocsagencode_test.go
Normal file
38
pocsagencode_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package pocsagencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "log"
|
||||||
|
// "os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Encode(t *testing.T) {
|
||||||
|
// SetLogger(log.New(os.Stdout, "POCSAG ", log.LstdFlags))
|
||||||
|
enc, left := Generate(3000, 576, []*Message{
|
||||||
|
&Message{1300100, "happy christmas!"},
|
||||||
|
})
|
||||||
|
if len(left) != 0 {
|
||||||
|
t.Errorf("expect no message left, got %v", left)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect := []uint32{
|
||||||
|
// 18 words, 576 bits of preamble
|
||||||
|
0xAAAAAAAA, 0xAAAAAAAA,
|
||||||
|
0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA,
|
||||||
|
0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA, 0xAAAAAAAA,
|
||||||
|
// The real dat starts here
|
||||||
|
0x7CD215D8, 0x7A89C197, 0x7A89C197, 0x7A89C197, 0x7A89C197, 0x7A89C197, 0x7A89C197, 0x7A89C197,
|
||||||
|
0x7A89C197, 0x4F5A0109, 0x8B861C9F, 0xC3CF04CD, 0xD8C5A3C6, 0xF979C8DE, 0xBDB878F3, 0x9E110386,
|
||||||
|
0x7A89C197, 0x7CD215D8, 0x7A89C197,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(enc) != len(expect) {
|
||||||
|
t.Errorf("expected:\n%X\ngot:\n%X\n", expect, enc)
|
||||||
|
} else {
|
||||||
|
for i, w := range expect {
|
||||||
|
if w != enc[i] {
|
||||||
|
t.Errorf("expected:%X got:%X at index %d\n", w, enc[i], i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue