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.

408 lines
13 KiB
Python

import ctypes
import struct
"""
** 11.09.2019 : Added Numeric Pager support by cuddlycheetah (github.com/cuddlycheetah)
** 14.10.2019 : Added Repeating Transmission + Single Preamble Mode (github.com/cuddlycheetah)
** 22.10.2019 : Made the Original (from rpitx) to a Python Encoder. (github.com/cuddlycheetah)
"""
#The sync word exists at the start of every batch.
#A batch is 16 words, a sync word occurs every 16 data words.
SYNC=0x7CD215D8
#The idle word is used as padding before the address word, at the end
#of a message to indicate that the message is finished. Interestingly, the
#idle word does not have a valid CRC code, the sync word does.
IDLE=0x7A89C197
#One frame consists of a pair of two words
FRAME_SIZE = 2
#One batch consists of 8 frames, 16 words
BATCH_SIZE = 16
#The preamble comes before a message, is a series of alternating
#1,0,1,0... bits, at least 576 bits. It exists to allow the receiver
#to synchronize with the transmitter
PREAMBLE_LENGTH = 576
#These bits appear as the first bit of a word, for an address word and
#one for a data word
FLAG_ADDRESS = 0x000000
FLAG_MESSAGE = 0x100000
#The last two bits of an address word's data represent the data type
#0x3 for text, 0x0 for numeric.
FLAG_TEXT_DATA = 0x3
FLAG_NUMERIC_DATA = 0x0
#Each data word can contain 20 bits of text information. Each character is
#7 bits wide, encoded. The bit order of the characters is reversed from
#the normal bit order; the most significant bit of a word corresponds to the
#least significant bit of a character it is encoding. The characters are split
#across the words of a message to ensure maximal usage of all bits.
TEXT_BITS_PER_WORD = 20
#As mentioned above, are 7 bit ASCII encoded
TEXT_BITS_PER_CHAR = 7
NUMERIC_BITS_PER_WORD = 20
NUMERIC_BITS_PER_DIGIT = 4
#Length of CRC codes in bits
CRC_BITS=10
#The CRC generator polynomial
CRC_GENERATOR=0b11101101001
'''*
* Calculate the CRC error checking code for the given word.
* Messages use a 10 bit CRC computed from the 21 data bits.
* This is calculated through a binary polynomial long division, returning
* the remainder.
* See https:#en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
* for more information.
'''
def crc(inputMsg):
#Align MSB of denominatorerator with MSB of message
denominator = CRC_GENERATOR << 20
#Message is right-padded with zeroes to the message length + crc length
msg = inputMsg << CRC_BITS
#We iterate until denominator has been right-shifted back to it's original value.
for column in range(20 + 1):
#Bit for the column we're aligned to
msgBit = (msg >> (30 - column)) & 1
#If the current bit is zero, don't modify the message self iteration
if msgBit != 0:
#While we would normally subtract in long division, XOR here.
msg ^= denominator
#Shift the denominator over to align with the next column
denominator >>= 1
#At self point 'msg' contains the CRC value we've calculated
return msg & 0x3FF
'''*
* Calculates the even parity bit for a message.
* If the number of bits in the message is even, 0, return 1.
'''
def parity(x):
#Our parity bit
p = 0
#We xor p with each bit of the input value. This works because
#xoring two one-bits will cancel out and leave a zero bit. Thus
#xoring any even number of one bits will result in zero, xoring
#any odd number of one bits will result in one.
for i in range(32):
p ^= (x & 1)
x >>= 1
return p
'''*
* Encodes a 21-bit message by calculating and adding a CRC code and parity bit.
'''
def encodeCodeword(msg):
fullCRC = (msg << CRC_BITS) | crc(msg)
p = parity(fullCRC)
return (fullCRC << 1) | p
'''*
* ASCII encode a null-terminated string as a series of codewords, written
* to (*out). Returns the number of codewords written. Caller should ensure
* that enough memory is allocated in (*out) to contain the message
*
* initial_offset indicates which word in the current batch the function is
* beginning at, that it can insert SYNC words at appropriate locations.
'''
def encodeASCII(initial_offset, text, buff):
#Number of words written to *out
numWordsWritten = 0
#Data for the current word we're writing
currentWord = 0
#Nnumber of bits we've written so far to the current word
currentNumBits = 0
#Position of current word in the current batch
wordPosition = initial_offset
for c in text:
#Encode the character bits backwards
for i in range(TEXT_BITS_PER_CHAR):
currentWord <<= 1
currentWord |= (ord(c) >> i) & 1
currentNumBits+=1
if currentNumBits == TEXT_BITS_PER_WORD:
#Add the MESSAGE flag to our current word and encode it.
buff.append(encodeCodeword(currentWord | FLAG_MESSAGE))
currentWord = 0
currentNumBits = 0
numWordsWritten+=1
wordPosition+=1
if wordPosition == BATCH_SIZE:
#We've filled a full batch, to insert a SYNC word
#and start a one.
buff.append(SYNC)
numWordsWritten+=1
wordPosition = 0
#Write remainder of message
if currentNumBits > 0:
#Pad out the word to 20 bits with zeroes
currentWord <<= 20 - currentNumBits
buff.append(encodeCodeword(currentWord | FLAG_MESSAGE))
numWordsWritten+=1
wordPosition+=1
if wordPosition == BATCH_SIZE:
#We've filled a full batch, to insert a SYNC word
#and start a one.
buff.append(SYNC)
numWordsWritten+=1
wordPosition = 0
return numWordsWritten
# Char Translationtable
mirrorTab = [0x00, 0x08, 0x04, 0x0c, 0x02, 0x0a, 0x06, 0x0e, 0x01, 0x09]
def encodeDigit(ch):
if ch >= '0' and ch <= '9':
return mirrorTab[ch - '0']
elif ch == ' ':
return 0x03
elif ch == 'u' or ch == 'U':
return 0x0d
elif ch == '-' or ch == '_':
return 0x0b
elif ch == '(' or ch == '[':
return 0x0f
elif ch == ')' or ch == ']':
return 0x07
else:
return 0x05
def encodeNumeric(initial_offset, text, buff):
#Number of words written to *out
numWordsWritten = 0
#Data for the current word we're writing
currentWord = 0
#Nnumber of bits we've written so far to the current word
currentNumBits = 0
#Position of current word in the current batch
wordPosition = initial_offset
for c in text:
#Encode the digit bits backwards
for i in range(NUMERIC_BITS_PER_DIGIT):
currentWord <<= 1
digit = encodeDigit(c)
digit = ((digit & 1) << 3) | ((digit & 2) << 1) | ((digit & 4) >> 1) | ((digit & 8) >> 3)
currentWord |= (digit >> i) & 1
currentNumBits+=1
if currentNumBits == NUMERIC_BITS_PER_WORD:
#Add the MESSAGE flag to our current word and encode it.
buff.append(encodeCodeword(currentWord | FLAG_MESSAGE))
currentWord = 0
currentNumBits = 0
numWordsWritten+=1
wordPosition+=1
if wordPosition == BATCH_SIZE:
#We've filled a full batch, to insert a SYNC word
#and start a one.
buff.append(SYNC)
numWordsWritten+=1
wordPosition = 0
#Write remainder of message
if currentNumBits > 0:
#Pad out the word to 20 bits with zeroes
currentWord <<= 20 - currentNumBits
buff.append(encodeCodeword(currentWord | FLAG_MESSAGE))
numWordsWritten+=1
wordPosition+=1
if wordPosition == BATCH_SIZE:
#We've filled a full batch, to insert a SYNC word
#and start a one.
buff.append(SYNC)
numWordsWritten+=1
wordPosition = 0
return numWordsWritten
'''*
* An address of 21 bits, only 18 of those bits are encoded in the address
* word itself. The remaining 3 bits are derived from which frame in the batch
* is the address word. This calculates the number of words (not framesnot )
* which must precede the address word so that it is in the right spot. These
* words will be filled with the idle value.
'''
def addressOffset(address):
return (address & 0x7) * FRAME_SIZE
'''*
* Calculates the length in words of a text POCSAG message, the address
* and the number of characters to be transmitted.
'''
def textMessageLength(repeatIndex, address, numChars):
numWords = 0
#Padding before address word.
numWords += addressOffset(address)
#Address word itself
numWords+=1
#numChars * 7 bits per character / 20 bits per word, up
numWords += (numChars * TEXT_BITS_PER_CHAR + (TEXT_BITS_PER_WORD - 1)) / TEXT_BITS_PER_WORD
#Idle word representing end of message
numWords+=1
#Pad out last batch with idles
numWords += BATCH_SIZE - (numWords % BATCH_SIZE)
#Batches consist of 16 words each and are preceded by a sync word.
#So we add one word for every 16 message words
numWords += numWords / BATCH_SIZE
#Preamble of 576 alternating 1,0,1, bits before the message
#Even though self comes first, add it to the length last so it
#doesn't affect the other word-based calculations
if repeatIndex == 0:
numWords += PREAMBLE_LENGTH / 32
return numWords
'''*
* Calculates the length in words of a numeric POCSAG message, the address
* and the number of characters to be transmitted.
'''
def numericMessageLength(repeatIndex, address, numChars):
numWords = 0
#Padding before address word.
numWords += addressOffset(address)
#Address word itself
numWords+=1
#numChars * 7 bits per character / 20 bits per word, up
numWords += (numChars * NUMERIC_BITS_PER_DIGIT + (NUMERIC_BITS_PER_WORD - 1)) / NUMERIC_BITS_PER_WORD
#Idle word representing end of message
numWords+=1
#Pad out last batch with idles
numWords += BATCH_SIZE - (numWords % BATCH_SIZE)
#Batches consist of 16 words each and are preceded by a sync word.
#So we add one word for every 16 message words
numWords += numWords / BATCH_SIZE
#Preamble of 576 alternating 1,0,1, bits before the message
#Even though self comes first, add it to the length last so it
#doesn't affect the other word-based calculations
if repeatIndex == 0:
numWords += PREAMBLE_LENGTH / 32
return numWords
def encodeTransmission(numeric, repeatIndex, address, fb, message, buff):
out=0
#Encode preamble
#Alternating 1,0,1, bits for 576 bits, for receiver to synchronize
#with transmitter
if repeatIndex == 0:
for i in range(PREAMBLE_LENGTH / 32):
buff.append(0xAAAAAAAA)
out+=1
start = out
#Sync
buff.append(SYNC)
out+=1
#Write out padding before adderss word
prefixLength = addressOffset(address)
for i in range(prefixLength):
buff.append(IDLE)
out+=1
#Write address word.
#The last two bits of word's data contain the message type (function bits)
#The 3 least significant bits are dropped, those are encoded by the
#word's location.
buff.append(encodeCodeword(((address >> 3) << 2) | fb))
out+=1
#Encode the message itself
if numeric == True:
out += encodeNumeric(addressOffset(address) + 1, message, buff)
else:
out += encodeASCII(addressOffset(address) + 1, message, buff)
#Finally, an IDLE word indicating the end of the message
buff.append(IDLE)
out+=1
#Pad out the last batch with IDLE to write multiple of BATCH_SIZE + 1
#words (+ 1 is there because of the SYNC words)
written = out - start
padding = (BATCH_SIZE + 1) - written % (BATCH_SIZE + 1)
for i in range(padding):
buff.append(IDLE)
out+=1
def parseAddress(address):
if address.find('A') > 0:
return [address[:address.index('A')], 0]
elif address.find('B') > 0:
return [address[:address.index('B')], 1]
elif address.find('C') > 0:
return [address[:address.index('C')], 2]
elif address.find('D') > 0:
return [address[:address.index('D')], 3]
else:
return [address, 3]
def encodeTXBatch(messages, repeatNum = 2, inverted = False):
transmission = []
pocsagData = []
msgIndex = 0
for message in messages:
msgNumeric = message[0]
msgAddrParsed = parseAddress(message[1])
msgAddress = int(msgAddrParsed[0])
msgBits = msgAddrParsed[1]
msgText = message[2]
for repeat in range(repeatNum):
encodeTransmission(msgNumeric, msgIndex, msgAddress, msgBits, msgText, transmission)
msgIndex += 1
for word in transmission:
uint = ctypes.c_uint32(word).value
if not inverted:
uint = ~uint
pocsagData.append(int((uint>>24) & 0xFF))
pocsagData.append(int((uint>>16) & 0xFF))
pocsagData.append(int((uint>>8) & 0xFF))
pocsagData.append(int(uint & 0xFF))
return pocsagData