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