inital commit
commit
cfe7772b7d
@ -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
|
||||
}
|
@ -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…
Reference in New Issue