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, Burst) { out := make(Burst, 0) debugf("appendContentText: %s", content) bitpos := uint32(0) word := uint32(0) leftbits := uint32(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) << int(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 << int(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, Burst) { // 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(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 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: 3000, 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 }