You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

456 lines
12 KiB
Go

package pocsagencode
import (
"log"
)
// Message is a single POCSAG Alphanumeric message
type Message struct {
Addr uint32
Function uint8
Content string
IsNumeric bool
}
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, Burst) {
out := make(Burst, 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) << uint(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) << uint(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 << uint(30-bitpos))
}
bitpos++
step++
if step == 7 {
step = 0
}
}
out = append(out, appendMessageCodeword(word))
pos++
}
return pos, out
}
// appendContentNumeric appends numeric message content to the transmission blob
func appendContentNumeric(content string) (int, Burst) {
out := make(Burst, 0)
debugf("appendContentNumeric: %s", content)
// 084 2.6]195-3U7[
charMap := map[byte]byte{
'0': 0, '8': 1, '4': 2, ' ': 3, '2': 4, '.': 5, '6': 6, ']': 7,
'1': 8, '9': 9, '5': 10, '-': 11, '3': 12, 'U': 13, '7': 14, '[': 15,
}
bitpos := 0
word := uint32(0)
leftbits := 0
pos := 0
// walk through characters in message
for i, r := range content {
var char byte
var ok bool
// set char from the charMap, and skip the character is not in the map
if char, ok = charMap[byte(r)]; !ok {
debugf("skipping invlaid char '%s'", string(r))
continue
}
debugf(" char %d: %d [%X]\n", i, char, char)
// if the bits won't fit:
if bitpos+4 > 20 {
space := 20 - bitpos
// leftbits least significant bits of $char are left over in the next word
leftbits = 4 - 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) << uint(31-4-bitpos))
bitpos += 4
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) << uint(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 << uint(30-bitpos))
}
bitpos++
step++
if step == 4 {
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, Burst) {
// expand the parameters of the message
addr := msg.Addr
function := byte(msg.Function)
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(Burst, 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
var contentEncLen int
var contentEnc Burst
if msg.IsNumeric {
contentEncLen, contentEnc = appendContentNumeric(content)
} else {
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 Burst) Burst {
out := make(Burst, 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(messages []*Message, optionFns ...OptionFn) (Burst, []*Message) {
options := &Options{
MaxLen: 2000,
PreambleBits: 576,
}
for _, opt := range optionFns {
opt(options)
}
txWithoutScs := make(Burst, 0)
debugf("generate_transmission, maxlen: %d", options.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 > int(options.MaxLen) {
if pos == 0 {
debugf("burst would become too large (%d > %d) with first message alone - discarding!", nextLenBytes, options.MaxLen)
} else {
debugf("burst would become too large (%d > %d) - returning msg in queue", nextLenBytes, options.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 Burst{}, 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 options.PreambleBits > 0 {
preambleWords := options.PreambleBits / 32
if options.PreambleBits%32 > 0 {
preambleWords++
}
preamble := make(Burst, preambleWords)
for i := range preamble {
preamble[i] = pocsagPreambleWord
}
burst = append(preamble, burst...)
}
return burst, messages
}