]> sigrok.org Git - libsigrokdecode.git/blame - decoders/pjon/pd.py
pjon: show "communication relations", and nits
[libsigrokdecode.git] / decoders / pjon / pd.py
CommitLineData
65a1134a
GS
1##
2## This file is part of the libsigrokdecode project.
3##
4## Copyright (C) 2020 Gerhard Sittig <gerhard.sittig@gmx.net>
5##
6## This program is free software; you can redistribute it and/or modify
7## it under the terms of the GNU General Public License as published by
8## the Free Software Foundation; either version 2 of the License, or
9## (at your option) any later version.
10##
11## This program is distributed in the hope that it will be useful,
12## but WITHOUT ANY WARRANTY; without even the implied warranty of
13## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14## GNU General Public License for more details.
15##
16## You should have received a copy of the GNU General Public License
17## along with this program; if not, see <http://www.gnu.org/licenses/>.
18##
19
20# See the https://www.pjon.org/ PJON project page and especially the
21# https://www.pjon.org/PJON-protocol-specification-v3.2.php protocol
22# specification, which can use different link layers.
23
24# TODO
65a1134a
GS
25# - Check for the correct order of optional fields (the spec is not as
26# explicit on these details as I'd expect).
27# - Check decoder's robustness, completeness, and correctness when more
28# captures become available. Currently there are only few, which only
29# cover minimal communication, and none of the protocol's flexibility.
30# The decoder was essentially written based on the available docs, and
31# then took some arbitrary choices and liberties to cope with real life
32# data from an example setup. Strictly speaking this decoder violates
33# the spec, and errs towards the usability side.
34
35import sigrokdecode as srd
36import struct
37
38ANN_RX_INFO, ANN_HDR_CFG, ANN_PKT_LEN, ANN_META_CRC, ANN_TX_INFO, \
39ANN_SVC_ID, ANN_PKT_ID, ANN_ANON_DATA, ANN_PAYLOAD, ANN_END_CRC, \
40ANN_SYN_RSP, \
41ANN_RELATION, \
42ANN_WARN, \
43 = range(13)
44
45def calc_crc8(data):
46 crc = 0
47 for b in data:
48 crc ^= b
49 for i in range(8):
50 odd = crc % 2
51 crc >>= 1
52 if odd:
53 crc ^= 0x97
54 return crc
55
56def calc_crc32(data):
57 crc = 0xffffffff
58 for b in data:
59 crc ^= b
60 for i in range(8):
61 odd = crc % 2
62 crc >>= 1
63 if odd:
64 crc ^= 0xedb88320
65 crc ^= 0xffffffff
66 return crc
67
68class Decoder(srd.Decoder):
69 api_version = 3
70 id = 'pjon'
71 name = 'PJON'
72 longname = 'PJON'
73 desc = 'The PJON protocol.'
74 license = 'gplv2+'
12bbd670 75 inputs = ['pjon_link']
65a1134a
GS
76 outputs = []
77 tags = ['Embedded']
78 annotations = (
79 ('rx_info', 'Receiver ID'),
80 ('hdr_cfg', 'Header config'),
81 ('pkt_len', 'Packet length'),
82 ('meta_crc', 'Meta CRC'),
83 ('tx_info', 'Sender ID'),
84 ('port', 'Service ID'),
85 ('pkt_id', 'Packet ID'),
86 ('anon', 'Anonymous data'),
87 ('payload', 'Payload'),
88 ('end_crc', 'End CRC'),
89 ('syn_rsp', 'Sync response'),
90 ('relation', 'Relation'),
91 ('warning', 'Warning'),
92 )
93 annotation_rows = (
94 ('fields', 'Fields', (
95 ANN_RX_INFO, ANN_HDR_CFG, ANN_PKT_LEN, ANN_META_CRC, ANN_TX_INFO,
96 ANN_SVC_ID, ANN_ANON_DATA, ANN_PAYLOAD, ANN_END_CRC, ANN_SYN_RSP,
97 )),
98 ('relations', 'Relations', (ANN_RELATION,)),
99 ('warnings', 'Warnings', (ANN_WARN,)),
100 )
101
102 def __init__(self):
103 self.reset()
104
105 def reset(self):
106 self.reset_frame()
107
108 def reset_frame(self):
9dc13f0d
GS
109 self.frame_ss = None
110 self.frame_es = None
111 self.frame_rx_id = None
112 self.frame_tx_id = None
113 self.frame_payload_text = None
65a1134a 114 self.frame_bytes = None
9dc13f0d 115 self.frame_has_ack = None
65a1134a
GS
116 self.ack_bytes = None
117 self.ann_ss = None
118 self.ann_es = None
119
120 def start(self):
121 self.out_ann = self.register(srd.OUTPUT_ANN)
122
123 def putg(self, ss, es, ann, data):
124 self.put(ss, es, self.out_ann, [ann, data])
125
126 def frame_flush(self):
127 if not self.frame_bytes:
128 return
9dc13f0d
GS
129 if not self.frame_ss or not self.frame_es:
130 return
131
132 # Emit "communication relation" details.
133 text = []
134 if self.frame_rx_id is not None:
135 text.append("RX {}".format(self.frame_rx_id[-1]))
136 if self.frame_tx_id is not None:
137 text.append("TX {}".format(self.frame_tx_id[-1]))
138 if self.frame_payload_text is not None:
139 text.append("DATA {}".format(self.frame_payload_text))
140 if self.frame_has_ack is not None:
141 text.append("ACK {:02x}".format(self.frame_has_ack))
142 if text:
143 text = " - ".join(text)
144 self.putg(self.frame_ss, self.frame_es, ANN_RELATION, [text])
65a1134a
GS
145
146 def handle_field_get_desc(self, idx = None):
147 '''Lookup description of a PJON frame field.'''
148 if not self.field_desc:
149 return None
150 if idx is None:
151 idx = self.field_desc_idx
152 if idx >= 0 and idx >= len(self.field_desc):
153 return None
154 if idx < 0 and abs(idx) > len(self.field_desc):
155 return None
156 desc = self.field_desc[idx]
157 return desc
158
159 def handle_field_add_desc(self, fmt, hdl, cls = None):
160 '''Register description for a PJON frame field.'''
161 item = {
162 'format': fmt,
163 'width': struct.calcsize(fmt),
164 'handler': hdl,
165 'anncls': cls,
166 }
167 self.field_desc.append(item)
168
169 def handle_field_seed_desc(self):
170 '''Seed list of PJON frame fields' descriptions.'''
171
172 # At the start of a PJON frame, the layout of only two fields
173 # is known. Subsequent fields (their presence, and width) depend
174 # on the content of the header config field.
175
176 self.field_desc = []
177 self.handle_field_add_desc('<B', self.handle_field_rx_id, ANN_RX_INFO)
178 self.handle_field_add_desc('<B', self.handle_field_config, ANN_HDR_CFG)
179
180 self.field_desc_idx = 0
181 self.field_desc_got = 0
182
9dc13f0d
GS
183 self.frame_ss = None
184 self.frame_es = None
65a1134a
GS
185 self.frame_rx_id = None
186 self.frame_is_broadcast = None
187 self.frame_tx_id = None
188 self.frame_payload = None
9dc13f0d 189 self.frame_payload_text = None
65a1134a
GS
190 self.frame_has_ack = None
191
192 def handle_field_rx_id(self, b):
193 '''Process receiver ID field of a PJON frame.'''
194
195 b = b[0]
196
65a1134a
GS
197 # Provide text presentation, caller emits frame field annotation.
198 if b == 255: # "not assigned"
199 id_txt = 'NA'
200 elif b == 0: # "broadcast"
201 id_txt = 'BC'
202 else: # unicast
203 id_txt = '{:d}'.format(b)
204 texts = [
205 'RX_ID {}'.format(id_txt),
206 '{}'.format(id_txt),
207 ]
9dc13f0d
GS
208
209 # Track RX info for communication relation emission.
210 self.frame_rx_id = (b, id_txt)
211 self.frame_is_broadcast = b == 0
212
65a1134a
GS
213 return texts
214
215 def handle_field_config(self, b):
216 '''Process header config field of a PJON frame.'''
217
218 # Caller provides a list of values. We want a single scalar.
219 b = b[0]
220
221 # Get the config flags.
222 self.cfg_shared = b & (1 << 0)
223 self.cfg_tx_info = b & (1 << 1)
224 self.cfg_sync_ack = b & (1 << 2)
225 self.cfg_async_ack = b & (1 << 3)
226 self.cfg_port = b & (1 << 4)
227 self.cfg_crc32 = b & (1 << 5)
228 self.cfg_len16 = b & (1 << 6)
229 self.cfg_pkt_id = b & (1 << 7)
230
231 # Get a textual presentation of the flags.
232 text = []
233 text.append('pkt_id' if self.cfg_pkt_id else '-') # packet number
234 text.append('len16' if self.cfg_len16 else '-') # 16bit length not 8bit
235 text.append('crc32' if self.cfg_crc32 else '-') # 32bit CRC not 8bit
236 text.append('svc_id' if self.cfg_port else '-') # port aka service ID
237 text.append('ack_mode' if self.cfg_async_ack else '-') # async response
238 text.append('ack' if self.cfg_sync_ack else '-') # synchronous response
239 text.append('tx_info' if self.cfg_tx_info else '-')
240 text.append('bus_id' if self.cfg_shared else '-') # "shared" vs "local"
241 text = ' '.join(text)
242 bits = '{:08b}'.format(b)
243 texts = [
244 'CFG {:s}'.format(text),
245 'CFG {}'.format(bits),
246 bits
247 ]
248
249 # TODO Come up with the most appropriate phrases for this logic.
250 # Are separate instruction groups with repeated conditions more
251 # readable than one common block which registers fields _and_
252 # updates the overhead size? Or is the latter preferrable due to
253 # easier maintenance and less potential for inconsistency?
254
255 # Get the size of variable width fields, to calculate the size
256 # of the packet overhead (the part that is not the payload data).
257 # This lets us derive the payload length when we later receive
258 # the packet length.
259 u8_fmt = '>B'
260 u16_fmt = '>H'
261 u32_fmt = '>L'
262 len_fmt = u16_fmt if self.cfg_len16 else u8_fmt
9dc13f0d 263 bus_fmt = '>4B'
65a1134a
GS
264 crc_fmt = u32_fmt if self.cfg_crc32 else u8_fmt
265 self.cfg_overhead = 0
266 self.cfg_overhead += struct.calcsize(u8_fmt) # receiver ID
267 self.cfg_overhead += struct.calcsize(u8_fmt) # header config
268 self.cfg_overhead += struct.calcsize(len_fmt) # packet length
269 self.cfg_overhead += struct.calcsize(u8_fmt) # initial CRC, always CRC8
270 # TODO Check for completeness and correctness.
271 if self.cfg_shared:
272 self.cfg_overhead += struct.calcsize(u32_fmt) # receiver bus
273 if self.cfg_tx_info:
274 if self.cfg_shared:
275 self.cfg_overhead += struct.calcsize(u32_fmt) # sender bus
276 self.cfg_overhead += struct.calcsize(u8_fmt) # sender ID
277 if self.cfg_port:
278 self.cfg_overhead += struct.calcsize(u16_fmt) # service ID
279 if self.cfg_pkt_id:
280 self.cfg_overhead += struct.calcsize(u16_fmt) # packet ID
281 self.cfg_overhead += struct.calcsize(crc_fmt) # end CRC
282
283 # Register more frame fields as we learn about their presence and
284 # format. Up to this point only receiver ID and header config were
285 # registered since their layout is fixed.
286 #
287 # Packet length and meta CRC are always present but can be of
288 # variable width. Optional fields follow the meta CRC and preceed
289 # the payload bytes. Notice that payload length isn't known here
290 # either, though its position is known already. The packet length
291 # is yet to get received. Subtracting the packet overhead from it
292 # (which depends on the header configuration) will provide that
293 # information.
294 #
295 # TODO Check for completeness and correctness.
296 # TODO Optionally fold overhead size arith and field registration
297 # into one block of instructions, to reduce the redundancy in the
298 # condition checks, and raise awareness for incomplete sequences
299 # during maintenance.
300 self.handle_field_add_desc(len_fmt, self.handle_field_pkt_len, ANN_PKT_LEN)
9dc13f0d 301 self.handle_field_add_desc(u8_fmt, self.handle_field_meta_crc, ANN_META_CRC)
65a1134a 302 if self.cfg_shared:
9dc13f0d 303 self.handle_field_add_desc(bus_fmt, self.handle_field_rx_bus, ANN_ANON_DATA)
65a1134a
GS
304 if self.cfg_tx_info:
305 if self.cfg_shared:
9dc13f0d
GS
306 self.handle_field_add_desc(bus_fmt, self.handle_field_tx_bus, ANN_ANON_DATA)
307 self.handle_field_add_desc(u8_fmt, self.handle_field_tx_id, ANN_ANON_DATA)
65a1134a
GS
308 if self.cfg_port:
309 self.handle_field_add_desc(u16_fmt, ['PORT {:d}', '{:d}'], ANN_ANON_DATA)
310 if self.cfg_pkt_id:
311 self.handle_field_add_desc(u16_fmt, ['PKT {:04x}', '{:04x}'], ANN_ANON_DATA)
312 pl_fmt = '>{:d}B'.format(0)
313 self.handle_field_add_desc(pl_fmt, self.handle_field_payload, ANN_PAYLOAD)
314 self.handle_field_add_desc(crc_fmt, self.handle_field_end_crc, ANN_END_CRC)
315
316 # Emit warning annotations for invalid flag combinations.
317 warn_texts = []
318 wants_ack = self.cfg_sync_ack or self.cfg_async_ack
319 if wants_ack and not self.cfg_tx_info:
320 warn_texts.append('ACK request without TX info')
321 if wants_ack and self.frame_is_broadcast:
322 warn_texts.append('ACK request for broadcast')
323 if self.cfg_sync_ack and self.cfg_async_ack:
324 warn_texts.append('sync and async ACK request')
325 if self.cfg_len16 and not self.cfg_crc32:
326 warn_texts.append('extended length needs CRC32')
327 if warn_texts:
328 warn_texts = ', '.join(warn_texts)
329 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
330
331 # Have the caller emit the annotation for configuration data.
332 return texts
333
334 def handle_field_pkt_len(self, b):
335 '''Process packet length field of a PJON frame.'''
336
337 # Caller provides a list of values. We want a single scalar.
338 b = b[0]
339
340 # The wire communicates the total packet length. Some of it is
341 # overhead (non-payload data), while its volume is variable in
342 # size (dpends on the header configuration).
343 #
344 # Derive the payload size from previously observed flags. Update
345 # the previously registered field description (the second last
346 # item in the list, before the end CRC).
347
348 pkt_len = b
349 pl_len = b - self.cfg_overhead
350 warn_texts = []
351 if pkt_len not in range(self.cfg_overhead, 65536):
352 warn_texts.append('suspicious packet length')
353 if pkt_len > 15 and not self.cfg_crc32:
354 warn_texts.append('length above 15 needs CRC32')
355 if pl_len < 1:
356 warn_texts.append('suspicious payload length')
357 pl_len = 0
358 if warn_texts:
359 warn_texts = ', '.join(warn_texts)
360 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
361 pl_fmt = '>{:d}B'.format(pl_len)
362
363 desc = self.handle_field_get_desc(-2)
364 desc['format'] = pl_fmt
365 desc['width'] = struct.calcsize(pl_fmt)
366
367 # Have the caller emit the annotation for the packet length.
368 # Provide information of different detail level for zooming.
369 texts = [
370 'LENGTH {:d} (PAYLOAD {:d})'.format(pkt_len, pl_len),
371 'LEN {:d} (PL {:d})'.format(pkt_len, pl_len),
372 '{:d} ({:d})'.format(pkt_len, pl_len),
373 '{:d}'.format(pkt_len),
374 ]
375 return texts
376
377 def handle_field_common_crc(self, have, is_meta):
378 '''Process a CRC field of a PJON frame.'''
379
380 # CRC algorithm and width are configurable, and can differ
381 # across meta and end checksums in a frame's fields.
382 caption = 'META' if is_meta else 'END'
383 crc_len = 8 if is_meta else 32 if self.cfg_crc32 else 8
384 crc_fmt = '{:08x}' if crc_len == 32 else '{:02x}'
385 have_text = crc_fmt.format(have)
386
387 # Check received against expected checksum. Emit warnings.
388 warn_texts = []
389 data = self.frame_bytes[:-1]
390 want = calc_crc32(data) if crc_len == 32 else calc_crc8(data)
391 if want != have:
392 want_text = crc_fmt.format(want)
393 warn_texts.append('CRC mismatch - want {} have {}'.format(want_text, have_text))
394 if warn_texts:
395 warn_texts = ', '.join(warn_texts)
396 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
397
398 # Provide text representation for frame field, caller emits
399 # the annotation.
400 texts = [
401 '{}_CRC {}'.format(caption, have_text),
402 'CRC {}'.format(have_text),
403 have_text,
404 ]
405 return texts
406
9dc13f0d 407 def handle_field_meta_crc(self, b):
65a1134a
GS
408 '''Process initial CRC (meta) field of a PJON frame.'''
409 # Caller provides a list of values. We want a single scalar.
410 b = b[0]
411 return self.handle_field_common_crc(b, True)
412
413 def handle_field_end_crc(self, b):
414 '''Process end CRC (total frame) field of a PJON frame.'''
415 # Caller provides a list of values. We want a single scalar.
416 b = b[0]
417 return self.handle_field_common_crc(b, False)
418
9dc13f0d
GS
419 def handle_field_common_bus(self, b):
420 '''Common handling of bus ID details. Used for RX and TX.'''
421 bus_id = b[:4]
422 bus_num = struct.unpack('>L', bytearray(bus_id))
423 bus_txt = '.'.join(['{:d}'.format(b) for b in bus_id])
424 return bus_num, bus_txt
425
426 def handle_field_rx_bus(self, b):
427 '''Process receiver bus ID field of a PJON frame.'''
428
429 # When we get here, there always should be an RX ID already.
430 bus_num, bus_txt = self.handle_field_common_bus(b[:4])
431 rx_txt = "{} {}".format(bus_txt, self.frame_rx_id[-1])
432 self.frame_rx_id = (bus_num, self.frame_rx_id[0], rx_txt)
433
434 # Provide text representation for frame field, caller emits
435 # the annotation.
436 texts = [
437 'RX_BUS {}'.format(bus_txt),
438 bus_txt,
439 ]
440 return texts
441
442 def handle_field_tx_bus(self, b):
443 '''Process transmitter bus ID field of a PJON frame.'''
444
445 # The TX ID field is optional, as is the use of bus ID fields.
446 # In the TX info case the TX bus ID is seen before the TX ID.
447 bus_num, bus_txt = self.handle_field_common_bus(b[:4])
448 self.frame_tx_id = (bus_num, None, bus_txt)
449
450 # Provide text representation for frame field, caller emits
451 # the annotation.
452 texts = [
453 'TX_BUS {}'.format(bus_txt),
454 bus_txt,
455 ]
456 return texts
457
458 def handle_field_tx_id(self, b):
459 '''Process transmitter ID field of a PJON frame.'''
460
461 b = b[0]
462
463 id_txt = "{:d}".format(b)
464 if self.frame_tx_id is None:
465 self.frame_tx_id = (b, id_txt)
466 else:
467 tx_txt = "{} {}".format(self.frame_tx_id[-1], id_txt)
468 self.frame_tx_id = (self.frame_tx_id[0], b, tx_txt)
469
470 # Provide text representation for frame field, caller emits
471 # the annotation.
472 texts = [
473 'TX_ID {}'.format(id_txt),
474 id_txt,
475 ]
476 return texts
477
65a1134a
GS
478 def handle_field_payload(self, b):
479 '''Process payload data field of a PJON frame.'''
480
9dc13f0d 481 text = ' '.join(['{:02x}'.format(v) for v in b])
65a1134a 482 self.frame_payload = b[:]
9dc13f0d 483 self.frame_payload_text = text
65a1134a 484
65a1134a
GS
485 texts = [
486 'PAYLOAD {}'.format(text),
487 text,
488 ]
489 return texts
490
491 def handle_field_sync_resp(self, b):
492 '''Process synchronous response for a PJON frame.'''
493
494 self.frame_has_ack = b
495
496 texts = [
497 'ACK {:02x}'.format(b),
498 '{:02x}'.format(b),
499 ]
500 return texts
501
502 def decode(self, ss, es, data):
503 ptype, pdata = data
504
505 # Start frame bytes accumulation when FRAME_INIT is seen. Flush
506 # previously accumulated frame bytes when a new frame starts.
507 if ptype == 'FRAME_INIT':
508 self.frame_flush()
509 self.reset_frame()
510 self.frame_bytes = []
511 self.handle_field_seed_desc()
9dc13f0d
GS
512 self.frame_ss = ss
513 self.frame_es = es
65a1134a
GS
514 return
515
516 # Use IDLE as another (earlier) trigger to flush frames. Also
517 # trigger flushes on FRAME-DATA which mean that the link layer
518 # inspection has seen the end of a protocol frame.
519 #
520 # TODO Improve usability? Emit warnings for PJON frames where
521 # FRAME_DATA was seen but FRAME_INIT wasn't? So that users can
522 # become aware of broken frames.
523 if ptype in ('IDLE', 'FRAME_DATA'):
524 self.frame_flush()
525 self.reset_frame()
526 return
527
528 # Switch from data bytes to response bytes when WAIT is seen.
529 if ptype == 'SYNC_RESP_WAIT':
530 self.ack_bytes = []
531 self.ann_ss, self.ann_es = None, None
532 return
533
534 # Accumulate data bytes as they arrive. Put them in the bucket
535 # which corresponds to its most recently seen leader.
536 if ptype == 'DATA_BYTE':
537 b = pdata
9dc13f0d 538 self.frame_es = es
65a1134a
GS
539
540 # Are we collecting response bytes (ACK)?
541 if self.ack_bytes is not None:
542 if not self.ann_ss:
543 self.ann_ss = ss
544 self.ack_bytes.append(b)
545 self.ann_es = es
546 text = self.handle_field_sync_resp(b)
547 if text:
548 self.putg(self.ann_ss, self.ann_es, ANN_SYN_RSP, text)
549 self.ann_ss, self.ann_es = None, None
550 return
551
552 # Are we collecting frame content?
553 if self.frame_bytes is not None:
554 if not self.ann_ss:
555 self.ann_ss = ss
556 self.frame_bytes.append(b)
557 self.ann_es = es
558
559 # Has the field value become available yet?
560 desc = self.handle_field_get_desc()
561 if not desc:
562 return
563 width = desc.get('width', None)
564 if not width:
565 return
566 self.field_desc_got += 1
567 if self.field_desc_got != width:
568 return
569
570 # Grab most recent received field as a byte array. Get
571 # the values that it contains.
572 fmt = desc.get('format', '>B')
573 raw = bytearray(self.frame_bytes[-width:])
574 values = struct.unpack(fmt, raw)
575
576 # Process the value, and get its presentation. Can be
577 # mere formatting, or serious execution of logic.
578 hdl = desc.get('handler', '{!r}')
579 if isinstance(hdl, str):
580 text = [hdl.format(*values)]
581 elif isinstance(hdl, (list, tuple)):
582 text = [f.format(*values) for f in hdl]
583 elif hdl:
584 text = hdl(values)
585 cls = desc.get('anncls', ANN_ANON_DATA)
586
587 # Emit annotation unless the handler routine already did.
588 if cls is not None and text:
589 self.putg(self.ann_ss, self.ann_es, cls, text)
590 self.ann_ss, self.ann_es = None, None
591
592 # Advance scan position for to-get-received field.
593 self.field_desc_idx += 1
594 self.field_desc_got = 0
595 return
596
597 # Unknown phase, not collecting. Not synced yet to the input?
598 return
599
600 # Unknown or unhandled kind of link layer output.
601 return