]>
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 | |
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 | ||
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+' | |
12bbd670 | 75 | inputs = ['pjon_link'] |
65a1134a | 76 | outputs = [] |
6d073960 | 77 | tags = ['Embedded/industrial'] |
65a1134a GS |
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. | |
c32de941 | 133 | # TODO Include the service ID (port number) as well? |
9dc13f0d GS |
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]) | |
65a1134a GS |
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 | ||
9dc13f0d GS |
184 | self.frame_ss = None |
185 | self.frame_es = None | |
65a1134a GS |
186 | self.frame_rx_id = None |
187 | self.frame_is_broadcast = None | |
188 | self.frame_tx_id = None | |
189 | self.frame_payload = None | |
9dc13f0d | 190 | self.frame_payload_text = None |
65a1134a GS |
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 | ||
65a1134a GS |
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 | ] | |
9dc13f0d GS |
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 | ||
65a1134a GS |
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 | |
c32de941 | 240 | text.append('tx_info' if self.cfg_tx_info else '-') # sender address |
65a1134a GS |
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 | |
c32de941 | 259 | # the frame's total length. |
65a1134a GS |
260 | u8_fmt = '>B' |
261 | u16_fmt = '>H' | |
262 | u32_fmt = '>L' | |
263 | len_fmt = u16_fmt if self.cfg_len16 else u8_fmt | |
9dc13f0d | 264 | bus_fmt = '>4B' |
65a1134a GS |
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) | |
9dc13f0d | 302 | self.handle_field_add_desc(u8_fmt, self.handle_field_meta_crc, ANN_META_CRC) |
65a1134a | 303 | if self.cfg_shared: |
9dc13f0d | 304 | self.handle_field_add_desc(bus_fmt, self.handle_field_rx_bus, ANN_ANON_DATA) |
65a1134a GS |
305 | if self.cfg_tx_info: |
306 | if self.cfg_shared: | |
9dc13f0d GS |
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) | |
65a1134a GS |
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 | |
c32de941 | 343 | # size (depends on the header configuration). |
65a1134a GS |
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 | |
303f43e7 | 385 | crc_bytes = crc_len // 8 |
65a1134a GS |
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 = [] | |
303f43e7 | 391 | data = self.frame_bytes[:-crc_bytes] |
65a1134a GS |
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 | ||
9dc13f0d | 409 | def handle_field_meta_crc(self, b): |
65a1134a GS |
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 | ||
9dc13f0d GS |
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 | ||
65a1134a GS |
480 | def handle_field_payload(self, b): |
481 | '''Process payload data field of a PJON frame.''' | |
482 | ||
9dc13f0d | 483 | text = ' '.join(['{:02x}'.format(v) for v in b]) |
65a1134a | 484 | self.frame_payload = b[:] |
9dc13f0d | 485 | self.frame_payload_text = text |
65a1134a | 486 | |
65a1134a GS |
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() | |
9dc13f0d GS |
514 | self.frame_ss = ss |
515 | self.frame_es = es | |
65a1134a GS |
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 | |
9dc13f0d | 540 | self.frame_es = es |
65a1134a GS |
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 |