-
Notifications
You must be signed in to change notification settings - Fork 0
/
pocsagencode.go
454 lines (370 loc) · 12 KB
/
pocsagencode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
package pocsagencode
import (
"log"
"strings"
)
type FunctionBits byte
const (
FunctionA = FunctionBits(0b00)
FunctionB = FunctionBits(0b01)
FunctionC = FunctionBits(0b10)
FunctionD = FunctionBits(0b11)
FunctionAlpha = FunctionBits(0b00)
FunctionNum = FunctionBits(0b11)
)
// Message is a single POCSAG Alphanumeric message
type Message struct {
Addr uint32
Function FunctionBits
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 [%b]\n", cw_with_parity, 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)
if mod5 := len(content) % 5; mod5 > 0 {
content = content + strings.Repeat(" ", 5-mod5)
debugf("padding to a multiple of 5 chars: '%s'", content)
}
// 084 2.6]195-3U7[
charMap := map[byte]byte{ // Bit reversals
'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 invalid char '%s'", string(r))
continue
}
debugf(" char %d: '%s' --> %d [%X]\n", i, string(r), 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
}
}
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
}