2 ## This file is part of the libsigrokdecode project.
4 ## Copyright (C) 2020 Gerhard Sittig <gerhard.sittig@gmx.net>
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.
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.
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/>.
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.
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.
35 import sigrokdecode as srd
38 ANN_RX_INFO, ANN_HDR_CFG, ANN_PKT_LEN, ANN_META_CRC, ANN_TX_INFO, \
39 ANN_SVC_ID, ANN_PKT_ID, ANN_ANON_DATA, ANN_PAYLOAD, ANN_END_CRC, \
68 class Decoder(srd.Decoder):
73 desc = 'The PJON protocol.'
75 inputs = ['pjon_link']
77 tags = ['Embedded/industrial']
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'),
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,
98 ('relations', 'Relations', (ANN_RELATION,)),
99 ('warnings', 'Warnings', (ANN_WARN,)),
108 def reset_frame(self):
111 self.frame_rx_id = None
112 self.frame_tx_id = None
113 self.frame_payload_text = None
114 self.frame_bytes = None
115 self.frame_has_ack = None
116 self.ack_bytes = None
121 self.out_ann = self.register(srd.OUTPUT_ANN)
123 def putg(self, ss, es, ann, data):
124 self.put(ss, es, self.out_ann, [ann, data])
126 def frame_flush(self):
127 if not self.frame_bytes:
129 if not self.frame_ss or not self.frame_es:
132 # Emit "communication relation" details.
133 # TODO Include the service ID (port number) as well?
135 if self.frame_rx_id is not None:
136 text.append("RX {}".format(self.frame_rx_id[-1]))
137 if self.frame_tx_id is not None:
138 text.append("TX {}".format(self.frame_tx_id[-1]))
139 if self.frame_payload_text is not None:
140 text.append("DATA {}".format(self.frame_payload_text))
141 if self.frame_has_ack is not None:
142 text.append("ACK {:02x}".format(self.frame_has_ack))
144 text = " - ".join(text)
145 self.putg(self.frame_ss, self.frame_es, ANN_RELATION, [text])
147 def handle_field_get_desc(self, idx = None):
148 '''Lookup description of a PJON frame field.'''
149 if not self.field_desc:
152 idx = self.field_desc_idx
153 if idx >= 0 and idx >= len(self.field_desc):
155 if idx < 0 and abs(idx) > len(self.field_desc):
157 desc = self.field_desc[idx]
160 def handle_field_add_desc(self, fmt, hdl, cls = None):
161 '''Register description for a PJON frame field.'''
164 'width': struct.calcsize(fmt),
168 self.field_desc.append(item)
170 def handle_field_seed_desc(self):
171 '''Seed list of PJON frame fields' descriptions.'''
173 # At the start of a PJON frame, the layout of only two fields
174 # is known. Subsequent fields (their presence, and width) depend
175 # on the content of the header config field.
178 self.handle_field_add_desc('<B', self.handle_field_rx_id, ANN_RX_INFO)
179 self.handle_field_add_desc('<B', self.handle_field_config, ANN_HDR_CFG)
181 self.field_desc_idx = 0
182 self.field_desc_got = 0
186 self.frame_rx_id = None
187 self.frame_is_broadcast = None
188 self.frame_tx_id = None
189 self.frame_payload = None
190 self.frame_payload_text = None
191 self.frame_has_ack = None
193 def handle_field_rx_id(self, b):
194 '''Process receiver ID field of a PJON frame.'''
198 # Provide text presentation, caller emits frame field annotation.
199 if b == 255: # "not assigned"
201 elif b == 0: # "broadcast"
204 id_txt = '{:d}'.format(b)
206 'RX_ID {}'.format(id_txt),
210 # Track RX info for communication relation emission.
211 self.frame_rx_id = (b, id_txt)
212 self.frame_is_broadcast = b == 0
216 def handle_field_config(self, b):
217 '''Process header config field of a PJON frame.'''
219 # Caller provides a list of values. We want a single scalar.
222 # Get the config flags.
223 self.cfg_shared = b & (1 << 0)
224 self.cfg_tx_info = b & (1 << 1)
225 self.cfg_sync_ack = b & (1 << 2)
226 self.cfg_async_ack = b & (1 << 3)
227 self.cfg_port = b & (1 << 4)
228 self.cfg_crc32 = b & (1 << 5)
229 self.cfg_len16 = b & (1 << 6)
230 self.cfg_pkt_id = b & (1 << 7)
232 # Get a textual presentation of the flags.
234 text.append('pkt_id' if self.cfg_pkt_id else '-') # packet number
235 text.append('len16' if self.cfg_len16 else '-') # 16bit length not 8bit
236 text.append('crc32' if self.cfg_crc32 else '-') # 32bit CRC not 8bit
237 text.append('svc_id' if self.cfg_port else '-') # port aka service ID
238 text.append('ack_mode' if self.cfg_async_ack else '-') # async response
239 text.append('ack' if self.cfg_sync_ack else '-') # synchronous response
240 text.append('tx_info' if self.cfg_tx_info else '-') # sender address
241 text.append('bus_id' if self.cfg_shared else '-') # "shared" vs "local"
242 text = ' '.join(text)
243 bits = '{:08b}'.format(b)
245 'CFG {:s}'.format(text),
246 'CFG {}'.format(bits),
250 # TODO Come up with the most appropriate phrases for this logic.
251 # Are separate instruction groups with repeated conditions more
252 # readable than one common block which registers fields _and_
253 # updates the overhead size? Or is the latter preferrable due to
254 # easier maintenance and less potential for inconsistency?
256 # Get the size of variable width fields, to calculate the size
257 # of the packet overhead (the part that is not the payload data).
258 # This lets us derive the payload length when we later receive
259 # the frame's total length.
263 len_fmt = u16_fmt if self.cfg_len16 else u8_fmt
265 crc_fmt = u32_fmt if self.cfg_crc32 else u8_fmt
266 self.cfg_overhead = 0
267 self.cfg_overhead += struct.calcsize(u8_fmt) # receiver ID
268 self.cfg_overhead += struct.calcsize(u8_fmt) # header config
269 self.cfg_overhead += struct.calcsize(len_fmt) # packet length
270 self.cfg_overhead += struct.calcsize(u8_fmt) # initial CRC, always CRC8
271 # TODO Check for completeness and correctness.
273 self.cfg_overhead += struct.calcsize(u32_fmt) # receiver bus
276 self.cfg_overhead += struct.calcsize(u32_fmt) # sender bus
277 self.cfg_overhead += struct.calcsize(u8_fmt) # sender ID
279 self.cfg_overhead += struct.calcsize(u16_fmt) # service ID
281 self.cfg_overhead += struct.calcsize(u16_fmt) # packet ID
282 self.cfg_overhead += struct.calcsize(crc_fmt) # end CRC
284 # Register more frame fields as we learn about their presence and
285 # format. Up to this point only receiver ID and header config were
286 # registered since their layout is fixed.
288 # Packet length and meta CRC are always present but can be of
289 # variable width. Optional fields follow the meta CRC and preceed
290 # the payload bytes. Notice that payload length isn't known here
291 # either, though its position is known already. The packet length
292 # is yet to get received. Subtracting the packet overhead from it
293 # (which depends on the header configuration) will provide that
296 # TODO Check for completeness and correctness.
297 # TODO Optionally fold overhead size arith and field registration
298 # into one block of instructions, to reduce the redundancy in the
299 # condition checks, and raise awareness for incomplete sequences
300 # during maintenance.
301 self.handle_field_add_desc(len_fmt, self.handle_field_pkt_len, ANN_PKT_LEN)
302 self.handle_field_add_desc(u8_fmt, self.handle_field_meta_crc, ANN_META_CRC)
304 self.handle_field_add_desc(bus_fmt, self.handle_field_rx_bus, ANN_ANON_DATA)
307 self.handle_field_add_desc(bus_fmt, self.handle_field_tx_bus, ANN_ANON_DATA)
308 self.handle_field_add_desc(u8_fmt, self.handle_field_tx_id, ANN_ANON_DATA)
310 self.handle_field_add_desc(u16_fmt, ['PORT {:d}', '{:d}'], ANN_ANON_DATA)
312 self.handle_field_add_desc(u16_fmt, ['PKT {:04x}', '{:04x}'], ANN_ANON_DATA)
313 pl_fmt = '>{:d}B'.format(0)
314 self.handle_field_add_desc(pl_fmt, self.handle_field_payload, ANN_PAYLOAD)
315 self.handle_field_add_desc(crc_fmt, self.handle_field_end_crc, ANN_END_CRC)
317 # Emit warning annotations for invalid flag combinations.
319 wants_ack = self.cfg_sync_ack or self.cfg_async_ack
320 if wants_ack and not self.cfg_tx_info:
321 warn_texts.append('ACK request without TX info')
322 if wants_ack and self.frame_is_broadcast:
323 warn_texts.append('ACK request for broadcast')
324 if self.cfg_sync_ack and self.cfg_async_ack:
325 warn_texts.append('sync and async ACK request')
326 if self.cfg_len16 and not self.cfg_crc32:
327 warn_texts.append('extended length needs CRC32')
329 warn_texts = ', '.join(warn_texts)
330 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
332 # Have the caller emit the annotation for configuration data.
335 def handle_field_pkt_len(self, b):
336 '''Process packet length field of a PJON frame.'''
338 # Caller provides a list of values. We want a single scalar.
341 # The wire communicates the total packet length. Some of it is
342 # overhead (non-payload data), while its volume is variable in
343 # size (depends on the header configuration).
345 # Derive the payload size from previously observed flags. Update
346 # the previously registered field description (the second last
347 # item in the list, before the end CRC).
350 pl_len = b - self.cfg_overhead
352 if pkt_len not in range(self.cfg_overhead, 65536):
353 warn_texts.append('suspicious packet length')
354 if pkt_len > 15 and not self.cfg_crc32:
355 warn_texts.append('length above 15 needs CRC32')
357 warn_texts.append('suspicious payload length')
360 warn_texts = ', '.join(warn_texts)
361 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
362 pl_fmt = '>{:d}B'.format(pl_len)
364 desc = self.handle_field_get_desc(-2)
365 desc['format'] = pl_fmt
366 desc['width'] = struct.calcsize(pl_fmt)
368 # Have the caller emit the annotation for the packet length.
369 # Provide information of different detail level for zooming.
371 'LENGTH {:d} (PAYLOAD {:d})'.format(pkt_len, pl_len),
372 'LEN {:d} (PL {:d})'.format(pkt_len, pl_len),
373 '{:d} ({:d})'.format(pkt_len, pl_len),
374 '{:d}'.format(pkt_len),
378 def handle_field_common_crc(self, have, is_meta):
379 '''Process a CRC field of a PJON frame.'''
381 # CRC algorithm and width are configurable, and can differ
382 # across meta and end checksums in a frame's fields.
383 caption = 'META' if is_meta else 'END'
384 crc_len = 8 if is_meta else 32 if self.cfg_crc32 else 8
385 crc_bytes = crc_len // 8
386 crc_fmt = '{:08x}' if crc_len == 32 else '{:02x}'
387 have_text = crc_fmt.format(have)
389 # Check received against expected checksum. Emit warnings.
391 data = self.frame_bytes[:-crc_bytes]
392 want = calc_crc32(data) if crc_len == 32 else calc_crc8(data)
394 want_text = crc_fmt.format(want)
395 warn_texts.append('CRC mismatch - want {} have {}'.format(want_text, have_text))
397 warn_texts = ', '.join(warn_texts)
398 self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
400 # Provide text representation for frame field, caller emits
403 '{}_CRC {}'.format(caption, have_text),
404 'CRC {}'.format(have_text),
409 def handle_field_meta_crc(self, b):
410 '''Process initial CRC (meta) field of a PJON frame.'''
411 # Caller provides a list of values. We want a single scalar.
413 return self.handle_field_common_crc(b, True)
415 def handle_field_end_crc(self, b):
416 '''Process end CRC (total frame) field of a PJON frame.'''
417 # Caller provides a list of values. We want a single scalar.
419 return self.handle_field_common_crc(b, False)
421 def handle_field_common_bus(self, b):
422 '''Common handling of bus ID details. Used for RX and TX.'''
424 bus_num = struct.unpack('>L', bytearray(bus_id))
425 bus_txt = '.'.join(['{:d}'.format(b) for b in bus_id])
426 return bus_num, bus_txt
428 def handle_field_rx_bus(self, b):
429 '''Process receiver bus ID field of a PJON frame.'''
431 # When we get here, there always should be an RX ID already.
432 bus_num, bus_txt = self.handle_field_common_bus(b[:4])
433 rx_txt = "{} {}".format(bus_txt, self.frame_rx_id[-1])
434 self.frame_rx_id = (bus_num, self.frame_rx_id[0], rx_txt)
436 # Provide text representation for frame field, caller emits
439 'RX_BUS {}'.format(bus_txt),
444 def handle_field_tx_bus(self, b):
445 '''Process transmitter bus ID field of a PJON frame.'''
447 # The TX ID field is optional, as is the use of bus ID fields.
448 # In the TX info case the TX bus ID is seen before the TX ID.
449 bus_num, bus_txt = self.handle_field_common_bus(b[:4])
450 self.frame_tx_id = (bus_num, None, bus_txt)
452 # Provide text representation for frame field, caller emits
455 'TX_BUS {}'.format(bus_txt),
460 def handle_field_tx_id(self, b):
461 '''Process transmitter ID field of a PJON frame.'''
465 id_txt = "{:d}".format(b)
466 if self.frame_tx_id is None:
467 self.frame_tx_id = (b, id_txt)
469 tx_txt = "{} {}".format(self.frame_tx_id[-1], id_txt)
470 self.frame_tx_id = (self.frame_tx_id[0], b, tx_txt)
472 # Provide text representation for frame field, caller emits
475 'TX_ID {}'.format(id_txt),
480 def handle_field_payload(self, b):
481 '''Process payload data field of a PJON frame.'''
483 text = ' '.join(['{:02x}'.format(v) for v in b])
484 self.frame_payload = b[:]
485 self.frame_payload_text = text
488 'PAYLOAD {}'.format(text),
493 def handle_field_sync_resp(self, b):
494 '''Process synchronous response for a PJON frame.'''
496 self.frame_has_ack = b
499 'ACK {:02x}'.format(b),
504 def decode(self, ss, es, data):
507 # Start frame bytes accumulation when FRAME_INIT is seen. Flush
508 # previously accumulated frame bytes when a new frame starts.
509 if ptype == 'FRAME_INIT':
512 self.frame_bytes = []
513 self.handle_field_seed_desc()
518 # Use IDLE as another (earlier) trigger to flush frames. Also
519 # trigger flushes on FRAME-DATA which mean that the link layer
520 # inspection has seen the end of a protocol frame.
522 # TODO Improve usability? Emit warnings for PJON frames where
523 # FRAME_DATA was seen but FRAME_INIT wasn't? So that users can
524 # become aware of broken frames.
525 if ptype in ('IDLE', 'FRAME_DATA'):
530 # Switch from data bytes to response bytes when WAIT is seen.
531 if ptype == 'SYNC_RESP_WAIT':
533 self.ann_ss, self.ann_es = None, None
536 # Accumulate data bytes as they arrive. Put them in the bucket
537 # which corresponds to its most recently seen leader.
538 if ptype == 'DATA_BYTE':
542 # Are we collecting response bytes (ACK)?
543 if self.ack_bytes is not None:
546 self.ack_bytes.append(b)
548 text = self.handle_field_sync_resp(b)
550 self.putg(self.ann_ss, self.ann_es, ANN_SYN_RSP, text)
551 self.ann_ss, self.ann_es = None, None
554 # Are we collecting frame content?
555 if self.frame_bytes is not None:
558 self.frame_bytes.append(b)
561 # Has the field value become available yet?
562 desc = self.handle_field_get_desc()
565 width = desc.get('width', None)
568 self.field_desc_got += 1
569 if self.field_desc_got != width:
572 # Grab most recent received field as a byte array. Get
573 # the values that it contains.
574 fmt = desc.get('format', '>B')
575 raw = bytearray(self.frame_bytes[-width:])
576 values = struct.unpack(fmt, raw)
578 # Process the value, and get its presentation. Can be
579 # mere formatting, or serious execution of logic.
580 hdl = desc.get('handler', '{!r}')
581 if isinstance(hdl, str):
582 text = [hdl.format(*values)]
583 elif isinstance(hdl, (list, tuple)):
584 text = [f.format(*values) for f in hdl]
587 cls = desc.get('anncls', ANN_ANON_DATA)
589 # Emit annotation unless the handler routine already did.
590 if cls is not None and text:
591 self.putg(self.ann_ss, self.ann_es, cls, text)
592 self.ann_ss, self.ann_es = None, None
594 # Advance scan position for to-get-received field.
595 self.field_desc_idx += 1
596 self.field_desc_got = 0
599 # Unknown phase, not collecting. Not synced yet to the input?
602 # Unknown or unhandled kind of link layer output.