]> sigrok.org Git - libsigrokdecode.git/blob - decoders/pjon/pd.py
irmp: silence "illegal character encoding" compiler warning
[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 # - 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
35 import sigrokdecode as srd
36 import struct
37
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, \
40 ANN_SYN_RSP, \
41 ANN_RELATION, \
42 ANN_WARN, \
43     = range(13)
44
45 def 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
56 def 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
68 class Decoder(srd.Decoder):
69     api_version = 3
70     id = 'pjon'
71     name = 'PJON'
72     longname = 'PJON'
73     desc = 'The PJON protocol.'
74     license = 'gplv2+'
75     inputs = ['pjon_link']
76     outputs = []
77     tags = ['Embedded/industrial']
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):
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
114         self.frame_bytes = None
115         self.frame_has_ack = None
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
129         if not self.frame_ss or not self.frame_es:
130             return
131
132         # Emit "communication relation" details.
133         # TODO Include the service ID (port number) as well?
134         text = []
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))
143         if text:
144             text = " - ".join(text)
145             self.putg(self.frame_ss, self.frame_es, ANN_RELATION, [text])
146
147     def handle_field_get_desc(self, idx = None):
148         '''Lookup description of a PJON frame field.'''
149         if not self.field_desc:
150             return None
151         if idx is None:
152             idx = self.field_desc_idx
153         if idx >= 0 and idx >= len(self.field_desc):
154             return None
155         if idx < 0 and abs(idx) > len(self.field_desc):
156             return None
157         desc = self.field_desc[idx]
158         return desc
159
160     def handle_field_add_desc(self, fmt, hdl, cls = None):
161         '''Register description for a PJON frame field.'''
162         item = {
163             'format': fmt,
164             'width': struct.calcsize(fmt),
165             'handler': hdl,
166             'anncls': cls,
167         }
168         self.field_desc.append(item)
169
170     def handle_field_seed_desc(self):
171         '''Seed list of PJON frame fields' descriptions.'''
172
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.
176
177         self.field_desc = []
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)
180
181         self.field_desc_idx = 0
182         self.field_desc_got = 0
183
184         self.frame_ss = None
185         self.frame_es = None
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
192
193     def handle_field_rx_id(self, b):
194         '''Process receiver ID field of a PJON frame.'''
195
196         b = b[0]
197
198         # Provide text presentation, caller emits frame field annotation.
199         if b == 255: # "not assigned"
200             id_txt = 'NA'
201         elif b == 0: # "broadcast"
202             id_txt = 'BC'
203         else: # unicast
204             id_txt = '{:d}'.format(b)
205         texts = [
206             'RX_ID {}'.format(id_txt),
207             '{}'.format(id_txt),
208         ]
209
210         # Track RX info for communication relation emission.
211         self.frame_rx_id = (b, id_txt)
212         self.frame_is_broadcast = b == 0
213
214         return texts
215
216     def handle_field_config(self, b):
217         '''Process header config field of a PJON frame.'''
218
219         # Caller provides a list of values. We want a single scalar.
220         b = b[0]
221
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)
231
232         # Get a textual presentation of the flags.
233         text = []
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)
244         texts = [
245             'CFG {:s}'.format(text),
246             'CFG {}'.format(bits),
247             bits
248         ]
249
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?
255
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.
260         u8_fmt = '>B'
261         u16_fmt = '>H'
262         u32_fmt = '>L'
263         len_fmt = u16_fmt if self.cfg_len16 else u8_fmt
264         bus_fmt = '>4B'
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.
272         if self.cfg_shared:
273             self.cfg_overhead += struct.calcsize(u32_fmt) # receiver bus
274         if self.cfg_tx_info:
275             if self.cfg_shared:
276                 self.cfg_overhead += struct.calcsize(u32_fmt) # sender bus
277             self.cfg_overhead += struct.calcsize(u8_fmt) # sender ID
278         if self.cfg_port:
279             self.cfg_overhead += struct.calcsize(u16_fmt) # service ID
280         if self.cfg_pkt_id:
281             self.cfg_overhead += struct.calcsize(u16_fmt) # packet ID
282         self.cfg_overhead += struct.calcsize(crc_fmt) # end CRC
283
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.
287         #
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
294         # information.
295         #
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)
303         if self.cfg_shared:
304             self.handle_field_add_desc(bus_fmt, self.handle_field_rx_bus, ANN_ANON_DATA)
305         if self.cfg_tx_info:
306             if self.cfg_shared:
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)
309         if self.cfg_port:
310             self.handle_field_add_desc(u16_fmt, ['PORT {:d}', '{:d}'], ANN_ANON_DATA)
311         if self.cfg_pkt_id:
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)
316
317         # Emit warning annotations for invalid flag combinations.
318         warn_texts = []
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')
328         if warn_texts:
329             warn_texts = ', '.join(warn_texts)
330             self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
331
332         # Have the caller emit the annotation for configuration data.
333         return texts
334
335     def handle_field_pkt_len(self, b):
336         '''Process packet length field of a PJON frame.'''
337
338         # Caller provides a list of values. We want a single scalar.
339         b = b[0]
340
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).
344         #
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).
348
349         pkt_len = b
350         pl_len = b - self.cfg_overhead
351         warn_texts = []
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')
356         if pl_len < 1:
357             warn_texts.append('suspicious payload length')
358             pl_len = 0
359         if warn_texts:
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)
363
364         desc = self.handle_field_get_desc(-2)
365         desc['format'] = pl_fmt
366         desc['width'] = struct.calcsize(pl_fmt)
367
368         # Have the caller emit the annotation for the packet length.
369         # Provide information of different detail level for zooming.
370         texts = [
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),
375         ]
376         return texts
377
378     def handle_field_common_crc(self, have, is_meta):
379         '''Process a CRC field of a PJON frame.'''
380
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)
388
389         # Check received against expected checksum. Emit warnings.
390         warn_texts = []
391         data = self.frame_bytes[:-crc_bytes]
392         want = calc_crc32(data) if crc_len == 32 else calc_crc8(data)
393         if want != have:
394             want_text = crc_fmt.format(want)
395             warn_texts.append('CRC mismatch - want {} have {}'.format(want_text, have_text))
396         if warn_texts:
397             warn_texts = ', '.join(warn_texts)
398             self.putg(self.ann_ss, self.ann_es, ANN_WARN, [warn_texts])
399
400         # Provide text representation for frame field, caller emits
401         # the annotation.
402         texts = [
403             '{}_CRC {}'.format(caption, have_text),
404             'CRC {}'.format(have_text),
405             have_text,
406         ]
407         return texts
408
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.
412         b = b[0]
413         return self.handle_field_common_crc(b, True)
414
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.
418         b = b[0]
419         return self.handle_field_common_crc(b, False)
420
421     def handle_field_common_bus(self, b):
422         '''Common handling of bus ID details. Used for RX and TX.'''
423         bus_id = b[:4]
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
427
428     def handle_field_rx_bus(self, b):
429         '''Process receiver bus ID field of a PJON frame.'''
430
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)
435
436         # Provide text representation for frame field, caller emits
437         # the annotation.
438         texts = [
439             'RX_BUS {}'.format(bus_txt),
440             bus_txt,
441         ]
442         return texts
443
444     def handle_field_tx_bus(self, b):
445         '''Process transmitter bus ID field of a PJON frame.'''
446
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)
451
452         # Provide text representation for frame field, caller emits
453         # the annotation.
454         texts = [
455             'TX_BUS {}'.format(bus_txt),
456             bus_txt,
457         ]
458         return texts
459
460     def handle_field_tx_id(self, b):
461         '''Process transmitter ID field of a PJON frame.'''
462
463         b = b[0]
464
465         id_txt = "{:d}".format(b)
466         if self.frame_tx_id is None:
467             self.frame_tx_id = (b, id_txt)
468         else:
469             tx_txt = "{} {}".format(self.frame_tx_id[-1], id_txt)
470             self.frame_tx_id = (self.frame_tx_id[0], b, tx_txt)
471
472         # Provide text representation for frame field, caller emits
473         # the annotation.
474         texts = [
475             'TX_ID {}'.format(id_txt),
476             id_txt,
477         ]
478         return texts
479
480     def handle_field_payload(self, b):
481         '''Process payload data field of a PJON frame.'''
482
483         text = ' '.join(['{:02x}'.format(v) for v in b])
484         self.frame_payload = b[:]
485         self.frame_payload_text = text
486
487         texts = [
488             'PAYLOAD {}'.format(text),
489             text,
490         ]
491         return texts
492
493     def handle_field_sync_resp(self, b):
494         '''Process synchronous response for a PJON frame.'''
495
496         self.frame_has_ack = b
497
498         texts = [
499             'ACK {:02x}'.format(b),
500             '{:02x}'.format(b),
501         ]
502         return texts
503
504     def decode(self, ss, es, data):
505         ptype, pdata = data
506
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':
510             self.frame_flush()
511             self.reset_frame()
512             self.frame_bytes = []
513             self.handle_field_seed_desc()
514             self.frame_ss = ss
515             self.frame_es = es
516             return
517
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.
521         #
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'):
526             self.frame_flush()
527             self.reset_frame()
528             return
529
530         # Switch from data bytes to response bytes when WAIT is seen.
531         if ptype == 'SYNC_RESP_WAIT':
532             self.ack_bytes = []
533             self.ann_ss, self.ann_es = None, None
534             return
535
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':
539             b = pdata
540             self.frame_es = es
541
542             # Are we collecting response bytes (ACK)?
543             if self.ack_bytes is not None:
544                 if not self.ann_ss:
545                     self.ann_ss = ss
546                 self.ack_bytes.append(b)
547                 self.ann_es = es
548                 text = self.handle_field_sync_resp(b)
549                 if text:
550                     self.putg(self.ann_ss, self.ann_es, ANN_SYN_RSP, text)
551                 self.ann_ss, self.ann_es = None, None
552                 return
553
554             # Are we collecting frame content?
555             if self.frame_bytes is not None:
556                 if not self.ann_ss:
557                     self.ann_ss = ss
558                 self.frame_bytes.append(b)
559                 self.ann_es = es
560
561                 # Has the field value become available yet?
562                 desc = self.handle_field_get_desc()
563                 if not desc:
564                     return
565                 width = desc.get('width', None)
566                 if not width:
567                     return
568                 self.field_desc_got += 1
569                 if self.field_desc_got != width:
570                     return
571
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)
577
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]
585                 elif hdl:
586                     text = hdl(values)
587                 cls = desc.get('anncls', ANN_ANON_DATA)
588
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
593
594                 # Advance scan position for to-get-received field.
595                 self.field_desc_idx += 1
596                 self.field_desc_got = 0
597                 return
598
599             # Unknown phase, not collecting. Not synced yet to the input?
600             return
601
602         # Unknown or unhandled kind of link layer output.
603         return