]>
Commit | Line | Data |
---|---|---|
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 | |
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+' | |
12bbd670 | 80 | inputs = ['pjon_link'] |
65a1134a GS |
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 |