python-can-flexible-serial 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Template python-can plugin for a flexible serial interface."""
2
+
3
+ from .bus import FlexibleSerial
4
+
5
+ __all__ = ["FlexibleSerial"]
flexible_serial/bus.py ADDED
@@ -0,0 +1,780 @@
1
+ """
2
+ A python-can interface plugin for a configurable, framed binary protocol
3
+ over a serial port. For example use over serial ports like "/dev/ttyS1" or
4
+ "/dev/ttyUSB0" on Linux machines or "COM1" on Windows.
5
+
6
+ Unlike python-can's fixed-layout ``serial``/``slcan`` interfaces, the frame
7
+ layout (SOF/EOF delimiters, ID width, optional CRC, optional flags byte,
8
+ optional timestamp) is configurable via constructor kwargs so it can be
9
+ matched to firmware on the other end of the link. See the README for the
10
+ full frame layout and kwarg reference, and
11
+ https://python-can.readthedocs.io/en/stable/plugin-interface.html for the
12
+ python-can plugin interface this implements.
13
+ """
14
+
15
+ import io
16
+ import logging
17
+ import struct
18
+ from collections.abc import Sequence
19
+ from typing import TypedDict, Callable, Any, Optional, cast
20
+ import binascii
21
+ import time
22
+ from enum import IntEnum
23
+
24
+ from can import (
25
+ BusABC,
26
+ CanInitializationError,
27
+ CanInterfaceNotImplementedError,
28
+ CanOperationError,
29
+ CanProtocol,
30
+ CanTimeoutError,
31
+ Message,
32
+ )
33
+ from can.typechecking import AutoDetectedConfig
34
+
35
+ logger = logging.getLogger("can.flexible_serial")
36
+
37
+ try:
38
+ import serial.tools.list_ports
39
+ except ImportError:
40
+ logger.warning(
41
+ "You won't be able to use the serial can backend without "
42
+ "the `pyserial` package installed!"
43
+ )
44
+ serial = None
45
+
46
+ # move these into the class
47
+ CAN_ERR_FLAG = 0x20000000
48
+ CAN_RTR_FLAG = 0x40000000
49
+ CAN_EFF_FLAG = 0x80000000
50
+ CAN_ID_MASK_EXT = 0x1FFFFFFF
51
+ CAN_ID_MASK_STD = 0x7FF
52
+
53
+ FLAG_BYTE_ERROR = 0x01
54
+ FLAG_BYTE_RTR = 0x02
55
+ FLAG_BYTE_EXTENDED = 0x04
56
+ FLAG_BYTE_FD = 0x08
57
+ FLAG_BYTE_FD_BRS = 0x10
58
+ FLAG_BYTE_IS_RX = 0x20
59
+
60
+ MAX_DATA_LEN_CLASSIC = 8
61
+ MAX_DATA_LEN_FD = 64
62
+
63
+ '''
64
+ Frame Layout (see README.md for the full reference):
65
+ - SOF -> 1 or more bytes
66
+ HEADER
67
+ - timestamp -> options: 0 or 4 bytes, uint32 milliseconds
68
+ - dlc -> 1 byte
69
+ - flags byte -> options 0 or 1 byte
70
+ - id -> options 2 or 4 bytes
71
+ DATA PAYLOAD -> 0 - 64 bytes
72
+ - CRC -> optional (0 or some num of bytes)
73
+ - EOF -> 1 or more bytes
74
+ '''
75
+
76
+ class TimeoutTimer():
77
+ def __init__(self, timeout: Optional[float]):
78
+ if timeout is None:
79
+ self._is_infinite = True
80
+ self._timeout = 0.0
81
+ else:
82
+ self._is_infinite = False
83
+ self._timeout = max(0.0, timeout)
84
+ self._start_time = time.monotonic()
85
+
86
+ def expired(self) -> bool:
87
+ if self._is_infinite:
88
+ return False
89
+
90
+ return (time.monotonic() - self._start_time) >= self._timeout
91
+
92
+ def remaining_time(self) -> float | None:
93
+ if self._is_infinite:
94
+ return None
95
+
96
+ elapsed = time.monotonic() - self._start_time
97
+ return max(0.0, self._timeout - elapsed)
98
+
99
+
100
+ class FlexibleSerial(BusABC):
101
+ """
102
+ Enable basic can communication over a serial device using a flexible binary protocol
103
+
104
+ .. note:: See :meth:`~_recv_internal` for some special semantics.
105
+ """
106
+
107
+ class SerialError(IntEnum):
108
+ NONE = 0
109
+ DESYNC = 1
110
+ BAD_DLC = 2
111
+ BAD_EOF = 3
112
+ BAD_CRC = 4
113
+
114
+ ERROR_TEXT = {
115
+ SerialError.DESYNC: "Start-of-Frame desync",
116
+ SerialError.BAD_DLC: f"DataLength Invalid (>{MAX_DATA_LEN_FD} bytes)",
117
+ SerialError.BAD_EOF: "End-of-Frame mismatch",
118
+ SerialError.BAD_CRC: "CRC Error",
119
+ }
120
+
121
+ CRC_TYPES = frozenset({"none","crc16-ccitt"})
122
+
123
+ class FrameKwargs(TypedDict):
124
+ """Constructor kwargs controlling the wire frame layout.
125
+
126
+ :ivar big_endian: Byte order for header/CRC fields.
127
+ :ivar sof: Start-of-frame delimiter (bytes, or hex string from CLI kwargs).
128
+ :ivar eof: End-of-frame delimiter (bytes, or hex string from CLI kwargs).
129
+ :ivar uint16_id: Use a 2-byte arbitration ID instead of 4-byte; rejects extended IDs.
130
+ :ivar crc_type: One of :attr:`CRC_TYPES` (``"none"``, ``"crc16-ccitt"``).
131
+ :ivar flags_byte: Carry extended/error/remote/FD metadata in a dedicated header byte.
132
+ :ivar no_timestamp: Omit the 4-byte (uint32 milliseconds) timestamp header field.
133
+ :ivar rx_bytes_callback: ``callback(raw_bytes, is_rx)`` invoked for every
134
+ frame written or chunk of buffer consumed while reading; useful for
135
+ logging/debugging raw wire traffic.
136
+ """
137
+ big_endian: bool
138
+ sof: bytes
139
+ eof: bytes
140
+ uint16_id: bool
141
+ crc_type: str
142
+ flags_byte: bool
143
+ no_timestamp: bool
144
+ rx_bytes_callback: Callable | None
145
+
146
+
147
+ DEFAULT_FRAME_KWARGS: FrameKwargs = {
148
+ "big_endian": False,
149
+ "sof": b"\xAA",
150
+ "eof": b"\xBB",
151
+ "uint16_id": False,
152
+ "crc_type": "none",
153
+ "flags_byte": False,
154
+ "no_timestamp": False,
155
+ "rx_bytes_callback": None
156
+ }
157
+
158
+ DataProcessedCallback = Callable[[bytes, bool], None]
159
+ ''' Provides arguments [data bytes, is_rx] '''
160
+
161
+ def __init__(
162
+ self,
163
+ channel: str,
164
+ baudrate: int = 115200,
165
+ timeout: float = 0.1,
166
+ rtscts: bool = False,
167
+ **kwargs: Any,
168
+ ) -> None:
169
+ """
170
+ :param channel:
171
+ The serial device to open. For example "/dev/ttyS1" or
172
+ "/dev/ttyUSB0" on Linux or "COM1" on Windows systems.
173
+
174
+ :param baudrate:
175
+ Baud rate of the serial device in bit/s (default 115200).
176
+
177
+ .. warning::
178
+ Some serial port implementations don't care about the baudrate.
179
+
180
+ :param timeout:
181
+ Timeout for the serial device read calls in seconds (default 0.1).
182
+
183
+ :param rtscts:
184
+ Turn hardware handshake (RTS/CTS) on and off (default false)
185
+
186
+ :param kwargs:
187
+ Frame-layout options (see :class:`FrameKwargs` and
188
+ :attr:`DEFAULT_FRAME_KWARGS` for defaults), plus any remaining
189
+ kwargs passed to the base class constructor, see
190
+ :meth:`can.BusABC.__init__`. All frame-layout kwargs accept
191
+ hex strings (e.g. ``"0xAA"``) in addition to their native type,
192
+ since python-can CLI tools pass ``--bus-kwargs`` as strings.
193
+
194
+ :raises ~can.exceptions.CanInitializationError:
195
+ If the given parameters are invalid.
196
+ :raises TypeError: If channel is empty/None
197
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
198
+ If the serial module is not installed.
199
+ """
200
+
201
+ if not serial:
202
+ raise CanInterfaceNotImplementedError("the serial module is not installed")
203
+
204
+ if not channel:
205
+ raise TypeError("Must specify a serial port.")
206
+
207
+ self.channel = channel
208
+ self.channel_info = f"Flexible serial interface: {channel}"
209
+ self._can_protocol = CanProtocol.CAN_20
210
+
211
+ self._rx_buffer = bytearray()
212
+
213
+ self._sof = self._coerce_bytes_kwarg(
214
+ "sof",
215
+ kwargs.pop("sof", self.DEFAULT_FRAME_KWARGS["sof"]),
216
+ )
217
+ self._eof = self._coerce_bytes_kwarg(
218
+ "eof",
219
+ kwargs.pop("eof", self.DEFAULT_FRAME_KWARGS["eof"]),
220
+ )
221
+
222
+ big_endian = self._pop_bool_kwarg(kwargs, "big_endian")
223
+ no_timestamp = self._pop_bool_kwarg(kwargs, "no_timestamp")
224
+ uint16_id = self._pop_bool_kwarg(kwargs, "uint16_id")
225
+ flags_byte = self._pop_bool_kwarg(kwargs, "flags_byte")
226
+
227
+ self._configure_header(
228
+ big_endian=big_endian,
229
+ no_timestamp=no_timestamp,
230
+ uint16_id=uint16_id,
231
+ flags_byte=flags_byte,
232
+ )
233
+ self._configure_crc(big_endian=big_endian, kwargs=kwargs)
234
+ self._configure_frame_offsets(no_timestamp=no_timestamp)
235
+
236
+ # Register user callback for when serial bytes are received/transmitted
237
+ self._pop_rx_bytes_callback(kwargs)
238
+
239
+ try:
240
+ self._ser = serial.serial_for_url(
241
+ channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts
242
+ )
243
+ except ValueError as error:
244
+ raise CanInitializationError(
245
+ "Could not create the serial device"
246
+ ) from error
247
+
248
+ logger.info(f"Opened Serial Port {channel} with frame settings:\r\n"
249
+ f"\tEndianness: {"big" if big_endian else "little"}\r\n"
250
+ f"\tSOF: {self._sof}\r\n"
251
+ f"\tEOF: {self._eof}\r\n"
252
+ f"\tTimestamp: {not no_timestamp}\r\n"
253
+ f"\tFlags Byte: {flags_byte}\r\n"
254
+ f"\tID Length: {2 if uint16_id else 4} bytes\r\n"
255
+ f"\tCRC: {self._crc_type}",
256
+ )
257
+
258
+ super().__init__(channel, **kwargs)
259
+
260
+ def shutdown(self) -> None:
261
+ """
262
+ Close the serial interface.
263
+ """
264
+ super().shutdown()
265
+ self._ser.close()
266
+
267
+ def send(self, msg: Message, timeout: Optional[float] = None) -> None:
268
+ """
269
+ Send a message over the serial device.
270
+
271
+ :param msg:
272
+ Message to send.
273
+
274
+ .. note:: If the timestamp is a float value it will be converted
275
+ to an integer.
276
+
277
+ :param timeout:
278
+ This parameter will be ignored. The timeout value of the channel is
279
+ used instead.
280
+
281
+ """
282
+
283
+ # Reject extended frames if uint16 ID is selected
284
+ if self._id_is_uint16_t and msg.is_extended_id:
285
+ raise ValueError("uint16_id=True does not support extended CAN IDs")
286
+
287
+ # Mask ID, optionally placing error flags into ID field if its extended AND there is flags byte
288
+ arbitration_id = msg.arbitration_id & (CAN_ID_MASK_EXT if msg.is_extended_id else CAN_ID_MASK_STD)
289
+ if not self._has_flags_byte and not self._id_is_uint16_t:
290
+ if msg.is_extended_id:
291
+ arbitration_id |= CAN_EFF_FLAG
292
+ if msg.is_error_frame:
293
+ arbitration_id |= CAN_ERR_FLAG
294
+ if msg.is_remote_frame:
295
+ arbitration_id |= CAN_RTR_FLAG
296
+
297
+ # Generate timestamp even when frame format options omits it
298
+ timestamp_int = max(0, min(int(msg.timestamp * 1000), 0xFFFFFFFF))
299
+
300
+ # Construct frame
301
+ frame = bytearray(self._sof)
302
+ frame += self._header_pack(timestamp_int, msg, arbitration_id)
303
+ frame += msg.data
304
+ frame += self._crc_calculate(memoryview(frame)[self._crc_calc_start_offset:])
305
+ frame += self._eof
306
+
307
+ try:
308
+ if timeout != self._ser.write_timeout:
309
+ self._ser.write_timeout = timeout
310
+ self._ser.write(frame)
311
+ if self._on_data_processed_callback:
312
+ self._on_data_processed_callback(frame, False)
313
+ except serial.PortNotOpenError as error:
314
+ raise CanOperationError("writing to closed port") from error
315
+ except serial.SerialTimeoutException as error:
316
+ raise CanTimeoutError() from error
317
+
318
+ @staticmethod
319
+ def decode_serial_can_error(msg: Message) -> str:
320
+ '''
321
+ Data Byte 0 | Data Byte 1
322
+ DESYNC | num bytes dropped
323
+ BAD_DLC | Received DLC
324
+ BAD_EOF | n/a
325
+ BAD_CRC | n/a
326
+ '''
327
+ if not msg.is_error_frame:
328
+ return ""
329
+ if len(msg.data) == 0:
330
+ return "Unknown, no data bytes to decode error type"
331
+ if msg.data[0] == FlexibleSerial.SerialError.DESYNC:
332
+ text = FlexibleSerial.ERROR_TEXT[FlexibleSerial.SerialError.DESYNC]
333
+ if len(msg.data) >= 2:
334
+ return text + f" -> {msg.data[1]} bytes dropped"
335
+ else:
336
+ return text
337
+ elif msg.data[0] == FlexibleSerial.SerialError.BAD_DLC:
338
+ text = FlexibleSerial.ERROR_TEXT[FlexibleSerial.SerialError.BAD_DLC]
339
+ if len(msg.data) >= 2:
340
+ return text + f" -> Rx data length = {msg.data[1]}"
341
+ else:
342
+ return text
343
+ elif msg.data[0] == FlexibleSerial.SerialError.BAD_EOF:
344
+ return FlexibleSerial.ERROR_TEXT[FlexibleSerial.SerialError.BAD_EOF]
345
+ elif msg.data[0] == FlexibleSerial.SerialError.BAD_CRC:
346
+ return FlexibleSerial.ERROR_TEXT[FlexibleSerial.SerialError.BAD_CRC]
347
+ else:
348
+ return f"Unknown data bytes error encoding: {msg.data.hex(' ')}"
349
+
350
+ def _recv_internal(
351
+ self, timeout: Optional[float]
352
+ ) -> tuple[Optional[Message], bool]:
353
+ """
354
+ Read a message from the serial device.
355
+
356
+ :param timeout: length to block until a message is received
357
+
358
+ :returns:
359
+ Received message and :obj:`False` (because no filtering as taken place).
360
+ """
361
+
362
+ timer = TimeoutTimer(timeout)
363
+ try:
364
+ while (True):
365
+ msg = self._try_decode_message_from_buffer()
366
+ if msg is not None:
367
+ return (msg, False)
368
+
369
+ # The parser needs more bytes; allow one serial read before
370
+ # checking the deadline again so recv(0) still polls hardware.
371
+ # update timeout with remaining time
372
+ self._ser.timeout = timer.remaining_time()
373
+ # get bytes in hw serial rx buffer, up to an eof delimiter match
374
+ new_rx_bytes = self._ser.read_until(self._eof)
375
+ self._rx_buffer.extend(new_rx_bytes)
376
+
377
+ msg = self._try_decode_message_from_buffer()
378
+ if msg is not None:
379
+ return (msg, False)
380
+
381
+ if timer.expired():
382
+ return (None, False)
383
+
384
+ except serial.SerialException as error:
385
+ raise CanOperationError("could not read from serial") from error
386
+
387
+ def fileno(self) -> int:
388
+ try:
389
+ return cast("int", self._ser.fileno())
390
+ except io.UnsupportedOperation:
391
+ raise NotImplementedError(
392
+ "fileno is not implemented using current CAN bus on this platform"
393
+ ) from None
394
+ except Exception as exception:
395
+ raise CanOperationError("Cannot fetch fileno") from exception
396
+
397
+ @staticmethod
398
+ def _detect_available_configs() -> Sequence[AutoDetectedConfig]:
399
+ configs: list[AutoDetectedConfig] = []
400
+ if serial is None:
401
+ return configs
402
+
403
+ for port in serial.tools.list_ports.comports():
404
+ configs.append({"interface": "flexible_serial", "channel": port.device})
405
+ return configs
406
+
407
+ @classmethod
408
+ def _pop_bool_kwarg(cls, kwargs: dict[str, Any], name: str) -> bool:
409
+ value = kwargs.pop(name, cls.DEFAULT_FRAME_KWARGS[name])
410
+ if not isinstance(value, bool):
411
+ raise TypeError(f"'{name}' must be bool, not {type(value).__name__}")
412
+ return value
413
+
414
+ def _pop_rx_bytes_callback(self, kwargs: dict[str, Any]):
415
+ value = kwargs.pop("rx_bytes_callback", None)
416
+ if value is None:
417
+ self._on_data_processed_callback = None
418
+ return
419
+ if not callable(value):
420
+ raise TypeError("'rx_bytes_callback' must be callable")
421
+ self._on_data_processed_callback = value
422
+
423
+ def _configure_header(
424
+ self,
425
+ *,
426
+ big_endian: bool,
427
+ no_timestamp: bool,
428
+ uint16_id: bool,
429
+ flags_byte: bool,
430
+ ) -> None:
431
+ self._has_timestamp = not no_timestamp
432
+ self._id_is_uint16_t = uint16_id
433
+ self._has_flags_byte = flags_byte
434
+
435
+ header_format = self._build_header_format(
436
+ big_endian=big_endian,
437
+ no_timestamp=no_timestamp,
438
+ uint16_id=uint16_id,
439
+ flags_byte=flags_byte,
440
+ )
441
+ self._header = struct.Struct(header_format)
442
+ self._header_pack = self._select_header_pack()
443
+
444
+ @staticmethod
445
+ def _build_header_format(
446
+ *,
447
+ big_endian: bool,
448
+ no_timestamp: bool,
449
+ uint16_id: bool,
450
+ flags_byte: bool,
451
+ ) -> str:
452
+ header_format = ">" if big_endian else "<" # Little/Big Endian
453
+ header_format += "" if no_timestamp else "I" # Timestamp -> 0 or 4 bytes
454
+ header_format += "B" # DLC -> 1 byte
455
+ header_format += "B" if flags_byte else "" # Flags -> 0 or 1 byte
456
+ header_format += "H" if uint16_id else "I" # ID -> 2 or 4 bytes
457
+ return header_format
458
+
459
+ def _select_header_pack(self) -> Callable[[int, Message, int], bytes]:
460
+ if (not self._has_timestamp and not self._has_flags_byte):
461
+ return self._pack_header_no_timestamp_no_flags
462
+ elif (not self._has_timestamp and self._has_flags_byte):
463
+ return self._pack_header_no_timestamp_yes_flags
464
+ elif (self._has_timestamp and not self._has_flags_byte):
465
+ return self._pack_header_yes_timestamp_no_flags
466
+ elif (self._has_timestamp and self._has_flags_byte):
467
+ return self._pack_header_yes_timestamp_yes_flags
468
+
469
+ raise RuntimeError("Unable to select header pack function")
470
+
471
+ def _configure_crc(self, *, big_endian: bool, kwargs: dict[str, Any]) -> None:
472
+ crc_type = kwargs.pop("crc_type", self.DEFAULT_FRAME_KWARGS["crc_type"])
473
+ if not isinstance(crc_type, str):
474
+ raise TypeError(f"'crc_type' must be str, not {type(crc_type).__name__}")
475
+
476
+ crc_type = crc_type.lower()
477
+ crc_type = "none" if crc_type == "" else crc_type
478
+ if crc_type not in self.CRC_TYPES:
479
+ raise ValueError(
480
+ f"'crc_type' {crc_type} not supported, must be one of {self.CRC_TYPES}"
481
+ )
482
+
483
+ self._crc_type = crc_type
484
+ if crc_type == "crc16-ccitt":
485
+ self._has_crc = True
486
+ self._crc_calculate = self._crc16_calculate_ccitt
487
+ self._crc_struct = struct.Struct((">" if big_endian else "<") + "H")
488
+ self._crc_size = self._crc_struct.size
489
+ else:
490
+ self._has_crc = False
491
+ self._crc_calculate = self._crc_calculate_none
492
+ self._crc_size = 0
493
+
494
+ def _configure_frame_offsets(self, *, no_timestamp: bool) -> None:
495
+ self._sof_len = len(self._sof)
496
+ self._eof_len = len(self._eof)
497
+ self._header_offset = self._sof_len
498
+ self._payload_offset = self._header_offset + self._header.size
499
+
500
+ self._crc_calc_start_offset = self._sof_len
501
+ self._crc_calc_start_offset += 0 if no_timestamp else 4
502
+ self._crc_calc_start_offset += 1
503
+
504
+ self._min_frame_length = self._payload_offset + self._crc_size + self._eof_len
505
+
506
+ @staticmethod
507
+ def _coerce_bytes_kwarg(name: str, value: Any) -> bytes:
508
+ if isinstance(value, (bytes, bytearray)):
509
+ byte_value = bytes(value)
510
+ elif isinstance(value, str):
511
+ byte_value = FlexibleSerial._parse_hex_bytes(name, value)
512
+ else:
513
+ raise TypeError(
514
+ f"'{name}' must be bytes or a hex string, "
515
+ f"not {type(value).__name__}"
516
+ )
517
+
518
+ if not byte_value:
519
+ raise ValueError(f"'{name}' must not be empty")
520
+ return byte_value
521
+
522
+ @staticmethod
523
+ def _parse_hex_bytes(name: str, value: str) -> bytes:
524
+ hex_value = value.strip()
525
+ hex_value = hex_value.replace("\\x", "").replace("\\X", "")
526
+ hex_value = hex_value.replace("0x", "").replace("0X", "")
527
+
528
+ for separator in (" ", "\t", "\r", "\n", "_", "-", ":", ","):
529
+ hex_value = hex_value.replace(separator, "")
530
+
531
+ if len(hex_value) % 2:
532
+ raise ValueError(
533
+ f"'{name}' hex string must contain an even number of digits"
534
+ )
535
+
536
+ try:
537
+ return bytes.fromhex(hex_value)
538
+ except ValueError as error:
539
+ raise ValueError(f"'{name}' must be bytes or a hex string") from error
540
+
541
+ # A different pack_header method for each possible header structure
542
+ # They all use the same arguments so the call to self._pack_header(...) in send() doesn't fail
543
+ def _pack_header_no_timestamp_no_flags(self, timestamp: int, msg: Message, arbitration_id: int) -> bytes:
544
+ return self._header.pack(msg.dlc, arbitration_id)
545
+ def _pack_header_no_timestamp_yes_flags(self, timestamp: int, msg: Message, arbitration_id: int) -> bytes:
546
+ return self._header.pack(msg.dlc, self._build_flags_byte(msg), arbitration_id)
547
+ def _pack_header_yes_timestamp_no_flags(self, timestamp: int, msg: Message, arbitration_id: int) -> bytes:
548
+ return self._header.pack(timestamp, msg.dlc, arbitration_id)
549
+ def _pack_header_yes_timestamp_yes_flags(self, timestamp: int, msg: Message, arbitration_id: int) -> bytes:
550
+ return self._header.pack(timestamp, msg.dlc, self._build_flags_byte(msg), arbitration_id)
551
+
552
+ def _crc_calculate_none(self, data: memoryview) -> bytes:
553
+ """ Return an empty bytes if CRC is not implemented """
554
+ return b""
555
+
556
+ def _crc16_calculate_ccitt(self, data: memoryview) -> bytes:
557
+ """
558
+ CRC16-CCITT with 0xFFFF start seed using polynomial 0x1021
559
+ """
560
+ crc = binascii.crc_hqx(data, 0xFFFF)
561
+ return self._crc_struct.pack(crc)
562
+
563
+ @staticmethod
564
+ def _build_flags_byte(msg: Message) -> int:
565
+ """ Build and return a packed byte filed with CAN message metadata"""
566
+ flags = 0
567
+ if msg.is_extended_id:
568
+ flags = FLAG_BYTE_EXTENDED
569
+ if msg.is_error_frame:
570
+ flags |= FLAG_BYTE_ERROR
571
+ if msg.is_remote_frame:
572
+ flags |= FLAG_BYTE_RTR
573
+ if msg.bitrate_switch:
574
+ flags |= FLAG_BYTE_FD_BRS
575
+ if msg.is_fd:
576
+ flags |= FLAG_BYTE_FD
577
+ if msg.is_rx:
578
+ flags |= FLAG_BYTE_IS_RX
579
+ return flags
580
+
581
+ def _build_rx_error_message(
582
+ self,
583
+ error: "FlexibleSerial.SerialError",
584
+ *,
585
+ timestamp: Optional[float] = None,
586
+ arbitration_id: int = 0,
587
+ is_extended_id: bool = False,
588
+ is_fd: bool = False,
589
+ is_rx: bool = True,
590
+ bitrate_switch: bool = False,
591
+ detail: Optional[int] = None,
592
+ ) -> Message:
593
+ if detail is None:
594
+ data = bytes([int(error)])
595
+ else:
596
+ data = bytes([int(error), max(0, min(detail, 0xFF))])
597
+
598
+ return Message(
599
+ timestamp=time.time() if timestamp is None else timestamp,
600
+ arbitration_id=arbitration_id,
601
+ is_extended_id=is_extended_id,
602
+ is_remote_frame=False,
603
+ is_error_frame=True,
604
+ dlc=len(data),
605
+ data=data,
606
+ is_fd=is_fd,
607
+ is_rx=is_rx,
608
+ bitrate_switch=bitrate_switch,
609
+ channel=self._ser.port
610
+ )
611
+
612
+ def _try_decode_message_from_buffer(self) -> Optional[Message]:
613
+ """Parse one complete message from the buffered serial bytes, if available."""
614
+
615
+ if not self._rx_buffer:
616
+ return None
617
+
618
+ # Search for SOF and preserve any partial SOF at the end of the buffer.
619
+ sof_index = self._rx_buffer.find(self._sof)
620
+ if sof_index < 0:
621
+ keep_len = 0
622
+ max_keep_len = min(len(self._rx_buffer), self._sof_len - 1)
623
+ for candidate_len in range(max_keep_len, 0, -1):
624
+ if self._sof.startswith(bytes(self._rx_buffer[-candidate_len:])):
625
+ keep_len = candidate_len
626
+ break
627
+
628
+ dropped_count = len(self._rx_buffer) - keep_len
629
+ if dropped_count <= 0:
630
+ return None
631
+
632
+ if self._on_data_processed_callback:
633
+ self._on_data_processed_callback(self._rx_buffer[:dropped_count], True)
634
+ del self._rx_buffer[:dropped_count]
635
+ return self._build_rx_error_message(
636
+ self.SerialError.DESYNC,
637
+ detail=dropped_count,
638
+ )
639
+
640
+ # Drop any junk before the next complete SOF.
641
+ if sof_index > 0:
642
+ if self._on_data_processed_callback:
643
+ self._on_data_processed_callback(self._rx_buffer[:sof_index], True)
644
+ del self._rx_buffer[:sof_index]
645
+ return self._build_rx_error_message(
646
+ self.SerialError.DESYNC,
647
+ detail=sof_index,
648
+ )
649
+
650
+ # Wait until the smallest valid frame can fit in the buffer.
651
+ if len(self._rx_buffer) < self._min_frame_length:
652
+ return None
653
+
654
+ # Unpack fixed header fields.
655
+ header = self._header.unpack_from(self._rx_buffer, self._header_offset)
656
+ header_idx = 0
657
+ if self._has_timestamp:
658
+ timestamp = header[header_idx] / 1000.0
659
+ header_idx += 1
660
+ else:
661
+ timestamp = time.time()
662
+
663
+ dlc = header[header_idx]
664
+ header_idx += 1
665
+
666
+ if self._has_flags_byte:
667
+ flags_byte = header[header_idx]
668
+ header_idx += 1
669
+ else:
670
+ flags_byte = None
671
+
672
+ arbitration_id = header[header_idx]
673
+
674
+ # Parse message metadata from flags byte, ID flags, or format defaults.
675
+ if self._has_flags_byte:
676
+ is_extended_id = bool(flags_byte & FLAG_BYTE_EXTENDED)
677
+ is_error_frame = bool(flags_byte & FLAG_BYTE_ERROR)
678
+ is_remote_frame = bool(flags_byte & FLAG_BYTE_RTR)
679
+ is_can_fd = bool(flags_byte & FLAG_BYTE_FD)
680
+ is_can_fd_brs = bool(flags_byte & FLAG_BYTE_FD_BRS)
681
+ is_rx = bool(flags_byte & FLAG_BYTE_IS_RX)
682
+ elif self._id_is_uint16_t:
683
+ is_extended_id = False
684
+ is_error_frame = False
685
+ is_remote_frame = False
686
+ is_can_fd = dlc > 8
687
+ is_can_fd_brs = False
688
+ is_rx = True
689
+ else:
690
+ is_extended_id = bool(arbitration_id & CAN_EFF_FLAG)
691
+ is_error_frame = bool(arbitration_id & CAN_ERR_FLAG)
692
+ is_remote_frame = bool(arbitration_id & CAN_RTR_FLAG)
693
+ is_can_fd = False
694
+ is_can_fd_brs = False
695
+ is_rx = True
696
+
697
+ # Mask protocol flag bits out of the CAN ID.
698
+ if is_extended_id:
699
+ arbitration_id = arbitration_id & CAN_ID_MASK_EXT
700
+ else:
701
+ arbitration_id = arbitration_id & CAN_ID_MASK_STD
702
+
703
+ # Validate DLC before trusting it as the payload length.
704
+ if dlc > (MAX_DATA_LEN_FD if is_can_fd else MAX_DATA_LEN_CLASSIC):
705
+ if self._on_data_processed_callback:
706
+ self._on_data_processed_callback(self._rx_buffer[0], True)
707
+ del self._rx_buffer[0]
708
+ return self._build_rx_error_message(
709
+ self.SerialError.BAD_DLC,
710
+ timestamp=timestamp,
711
+ arbitration_id=arbitration_id,
712
+ is_extended_id=is_extended_id,
713
+ is_fd=is_can_fd,
714
+ is_rx=is_rx,
715
+ bitrate_switch=is_can_fd_brs,
716
+ detail=dlc,
717
+ )
718
+
719
+ # Compute variable-length frame boundaries.
720
+ crc_offset = self._payload_offset + dlc
721
+ eof_offset = crc_offset + self._crc_size
722
+ frame_end = eof_offset + self._eof_len
723
+
724
+ # Wait until the complete frame is buffered.
725
+ if len(self._rx_buffer) < frame_end:
726
+ return None
727
+
728
+ # Validate EOF delimiter.
729
+ if self._rx_buffer[eof_offset:frame_end] != self._eof:
730
+ if self._on_data_processed_callback:
731
+ self._on_data_processed_callback(self._rx_buffer[0], True)
732
+ del self._rx_buffer[0]
733
+ return self._build_rx_error_message(
734
+ self.SerialError.BAD_EOF,
735
+ timestamp=timestamp,
736
+ arbitration_id=arbitration_id,
737
+ is_extended_id=is_extended_id,
738
+ is_fd=is_can_fd,
739
+ is_rx=is_rx,
740
+ bitrate_switch=is_can_fd_brs,
741
+ )
742
+
743
+ # Validate CRC when enabled.
744
+ if self._has_crc:
745
+ expected_crc = self._crc_calculate(
746
+ memoryview(self._rx_buffer)[self._crc_calc_start_offset:crc_offset]
747
+ )
748
+ received_crc = self._rx_buffer[crc_offset:eof_offset]
749
+ if expected_crc != received_crc:
750
+ if self._on_data_processed_callback:
751
+ self._on_data_processed_callback(self._rx_buffer[:frame_end], True)
752
+ del self._rx_buffer[:frame_end]
753
+ return self._build_rx_error_message(
754
+ self.SerialError.BAD_CRC,
755
+ timestamp=timestamp,
756
+ arbitration_id=arbitration_id,
757
+ is_extended_id=is_extended_id,
758
+ is_fd=is_can_fd,
759
+ is_rx=is_rx,
760
+ bitrate_switch=is_can_fd_brs,
761
+ )
762
+
763
+ # Build decoded CAN message and consume the frame bytes.
764
+ payload = bytearray(self._rx_buffer[self._payload_offset:crc_offset])
765
+ if self._on_data_processed_callback:
766
+ self._on_data_processed_callback(self._rx_buffer[:frame_end], True)
767
+ del self._rx_buffer[:frame_end]
768
+ return Message(
769
+ timestamp=timestamp,
770
+ arbitration_id=arbitration_id,
771
+ is_extended_id=is_extended_id,
772
+ is_remote_frame=is_remote_frame,
773
+ is_error_frame=is_error_frame,
774
+ dlc=dlc,
775
+ data=payload,
776
+ is_fd=is_can_fd,
777
+ is_rx=is_rx,
778
+ bitrate_switch=is_can_fd_brs,
779
+ channel=self._ser.port
780
+ )
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-can-flexible-serial
3
+ Version: 0.1.0
4
+ Summary: python-can interface plugin for a configurable binary serial CAN protocol
5
+ Project-URL: python-can plugin interface, https://python-can.readthedocs.io/en/stable/plugin-interface.html
6
+ Keywords: can,python-can,serial,plugin
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: python-can<5,>=4.4
11
+ Requires-Dist: pyserial>=3.5
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # python-can-flexible-serial
17
+
18
+ A [python-can](https://python-can.readthedocs.io/) interface plugin that speaks a
19
+ configurable, framed binary protocol over a serial port. It follows the
20
+ [python-can plugin interface](https://python-can.readthedocs.io/en/stable/plugin-interface.html),
21
+ so once installed it is usable anywhere python-can accepts an `interface=`
22
+ name — `can.Bus`, `python -m can.viewer`, `python -m can.logger`,
23
+ `python -m can.bridge`, etc. — alongside the other community interface
24
+ plugins listed on that page (`python-can-canine`, `python-can-cvector`,
25
+ `python-can-remote`, `python-can-sontheim`, `python-can-cando`,
26
+ `python-can-candle`, ...).
27
+
28
+ Unlike the fixed-layout `slcan` and `serial` interfaces bundled with
29
+ python-can, the frame layout here is configurable via constructor kwargs
30
+ (SOF/EOF delimiters, ID width, optional CRC, optional flags byte, optional
31
+ timestamp) so it can be adapted to match firmware on the other end of the
32
+ serial link without changing code.
33
+
34
+ ## Frame layout
35
+
36
+ | Section | Field | Size | Optional / Configurable |
37
+ |---|---|---|---|
38
+ | — | SOF | 1+ bytes | Configurable delimiter (default `0xAA`) |
39
+ | Header | timestamp | 0 or 4 bytes | Optional — omitted when `no_timestamp=True` |
40
+ | Header | dlc | 1 byte | Always present |
41
+ | Header | flags | 0 or 1 byte | Optional — only present when `flags_byte=True` |
42
+ | Header | id | 2 or 4 bytes | Sizeable — 2 bytes when `uint16_id=True`, else 4 |
43
+ | Payload | data | 0-64 bytes | Sizeable — length equals `dlc` |
44
+ | — | crc | 0 or N bytes | Optional — size/presence set by `crc_type` |
45
+ | — | EOF | 1+ bytes | Configurable delimiter (default `0xBB`) |
46
+
47
+ Details on each section:
48
+
49
+ - **SOF / EOF** — arbitrary-length byte sequences used purely as framing
50
+ delimiters. They are never covered by the CRC.
51
+ - **timestamp** *(optional, fixed 4 bytes when present)* — `uint32`
52
+ milliseconds. Omit entirely with `no_timestamp=True` to save 4 bytes per
53
+ frame if the far end doesn't supply/need one; python-can still assigns a
54
+ local receive timestamp in that case.
55
+ - **dlc** *(always present, fixed 1 byte)* — raw payload length in bytes;
56
+ also used to size the `data` section on receive.
57
+ - **flags** *(optional, fixed 1 byte when present)* — see "Flags byte" below
58
+ for the bit layout. When absent, the same metadata is instead packed into
59
+ spare bits of the `id` field (see below).
60
+ - **id** *(always present, sizeable: 2 or 4 bytes)* — arbitration ID.
61
+ `uint16_id=True` shrinks it to 2 bytes but rejects extended (29-bit) IDs;
62
+ the default 4-byte field supports both standard and extended IDs.
63
+ - **data** *(sizeable, 0-64 bytes)* — the CAN payload; length is whatever
64
+ `dlc` says, up to 64 bytes for CAN FD-sized frames or 8 bytes otherwise.
65
+ - **crc** *(optional, sizeable: 0 or N bytes)* — set by `crc_type`
66
+ (`"none"` or `"crc16-ccitt"`); covers the header and payload, but not
67
+ SOF/EOF.
68
+
69
+ Byte order for the header and CRC fields is little-endian by default, or
70
+ big-endian when `big_endian=True`.
71
+
72
+ ### Flags byte
73
+
74
+ When `flags_byte=True`, extended/error/remote/FD metadata is carried in a
75
+ dedicated byte instead of being packed into spare bits of the arbitration ID:
76
+
77
+ | Bit | Mask | Meaning |
78
+ |------|------|---------|
79
+ | 0 | `0x01` | Error frame |
80
+ | 1 | `0x02` | Remote frame (RTR) |
81
+ | 2 | `0x04` | Extended (29-bit) ID |
82
+ | 3 | `0x08` | CAN FD frame |
83
+ | 4 | `0x10` | CAN FD bit-rate switch |
84
+ | 5 | `0x20` | Frame originated as RX (vs. TX) |
85
+
86
+ When `flags_byte=False`, extended/error/remote status is instead OR'd into
87
+ spare high bits of the 32-bit arbitration ID field (standard `python-can`
88
+ convention: `CAN_EFF_FLAG`, `CAN_ERR_FLAG`, `CAN_RTR_FLAG`). This only applies
89
+ when `uint16_id=False`, since a 16-bit ID field has no spare bits for flags.
90
+
91
+ ### Error frames
92
+
93
+ Protocol-level errors (frame desync, bad DLC, EOF mismatch, bad CRC) are
94
+ surfaced as `Message` objects with `is_error_frame=True` rather than raising
95
+ exceptions, matching how other python-can interfaces report bus errors. Use
96
+ `FlexibleSerial.decode_serial_can_error(msg)` to get a human-readable
97
+ description of an error message's contents.
98
+
99
+ ## Installation
100
+
101
+ How you install this depends on whether you're *using* it as a dependency
102
+ of some other project, or working on this repo itself.
103
+
104
+ ### Using this plugin from another project
105
+
106
+ If that project is managed with [uv](https://docs.astral.sh/uv/), add this
107
+ repo as an editable path dependency, pointing at wherever you've cloned it:
108
+
109
+ ```powershell
110
+ uv add --editable plugins/python-can-flexible-serial
111
+ ```
112
+
113
+ Run this from *that* project's directory, not from within this repo — `uv
114
+ add` adds a dependency to the project in your current directory, so running
115
+ it from inside `python-can-flexible-serial` itself is a self-dependency and
116
+ uv will refuse it.
117
+
118
+ With plain pip, from that project's environment:
119
+
120
+ ```powershell
121
+ python -m pip install -e /path/to/python-can-flexible-serial
122
+ ```
123
+
124
+ ### Working on this repo itself
125
+
126
+ Use `uv sync` (a uv-managed project installs itself editable by default):
127
+
128
+ ```powershell
129
+ uv sync
130
+ ```
131
+
132
+ Or with plain pip, from this repo's root:
133
+
134
+ ```powershell
135
+ python -m pip install -e .
136
+ ```
137
+
138
+ Either way, this registers the plugin with python-can via the `can.interface`
139
+ entry point (`flexible_serial = "flexible_serial.bus:FlexibleSerial"`), so it
140
+ becomes available as `interface="flexible_serial"` anywhere python-can
141
+ resolves interfaces, without any further imports.
142
+
143
+ ## Usage
144
+
145
+ ```python
146
+ import can
147
+
148
+ bus = can.interface.Bus(
149
+ interface="flexible_serial",
150
+ channel="COM3",
151
+ baudrate=115200,
152
+ )
153
+ ```
154
+
155
+ ### Constructor arguments
156
+
157
+ | Argument | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `channel` | `str` | *(required)* | Serial device, e.g. `"COM3"` or `"/dev/ttyUSB0"` |
160
+ | `baudrate` | `int` | `115200` | Serial baud rate |
161
+ | `timeout` | `float` | `0.1` | Serial read timeout, in seconds |
162
+ | `rtscts` | `bool` | `False` | Enable RTS/CTS hardware handshake |
163
+ | `sof` | `bytes` \| hex `str` | `0xAA` | Start-of-frame delimiter |
164
+ | `eof` | `bytes` \| hex `str` | `0xBB` | End-of-frame delimiter |
165
+ | `big_endian` | `bool` | `False` | Byte order for header/CRC fields |
166
+ | `uint16_id` | `bool` | `False` | Use a 2-byte arbitration ID instead of 4-byte; rejects extended IDs |
167
+ | `flags_byte` | `bool` | `False` | Carry frame metadata in a dedicated byte (see "Flags byte" above) |
168
+ | `no_timestamp` | `bool` | `False` | Omit the 4-byte timestamp header field |
169
+ | `crc_type` | `str` | `"none"` | One of `"none"`, `"crc16-ccitt"` |
170
+ | `rx_bytes_callback` | `Callable[[bytes, bool], None] \| None` | `None` | Called with `(raw_bytes, is_rx)` whenever a frame is sent or a chunk of buffer is consumed on receive; useful for logging/debugging the raw wire traffic |
171
+
172
+ `sof` and `eof` accept either raw `bytes` (when constructing a `Bus` from
173
+ Python) or a hex string (when passed as a CLI `--bus-kwargs` value, since
174
+ python-can CLI tools pass kwargs as strings):
175
+
176
+ ```powershell
177
+ python -m can.logger -i flexible_serial -c COM3 --bus-kwargs sof=0xAA eof=0x0D0A
178
+ ```
179
+
180
+ Prefer the `0x` prefix for CLI hex values, especially if the delimiter
181
+ contains only digits, since python-can auto-converts bare numeric kwargs.
182
+
183
+ ### CLI examples
184
+
185
+ ```powershell
186
+ python -m can.viewer -i flexible_serial -c COM6 --bus-kwargs sof=0x4040 eof=0x0A uint16_id=True crc_type=crc16-ccitt flags_byte=False no_timestamp=True
187
+ ```
188
+
189
+ Bridging to another interface:
190
+
191
+ ```powershell
192
+ python -m can.bridge --bus1-interface flexible_serial --bus1-channel COM7 --bus1-bus-kwargs sof=0x4040 eof=0x0A uint16_id=True crc_type=crc16-ccitt flags_byte=False no_timestamp=True --bus2-interface kvaser --bus2-channel 0 --bus2-bitrate 125000
193
+ ```
194
+
195
+ ### Autodetection
196
+
197
+ Like the built-in `serial` interface, available serial ports are surfaced
198
+ through python-can's autodetection API:
199
+
200
+ ```python
201
+ import can
202
+ print(can.detect_available_configs(interfaces=["flexible_serial"]))
203
+ ```
204
+
205
+ ## Layout
206
+
207
+ ```text
208
+ python-can-flexible-serial/
209
+ pyproject.toml
210
+ README.md
211
+ src/
212
+ flexible_serial/
213
+ __init__.py
214
+ bus.py
215
+ tests/
216
+ test_hex_bytes.py
217
+ test_bus.py
218
+ message_helper.py
219
+ ```
220
+
221
+ ## Development
222
+
223
+ Install with the `dev` extra to pull in test dependencies (`pytest`):
224
+
225
+ ```powershell
226
+ uv sync --extra dev
227
+ ```
228
+ or
229
+ ```powershell
230
+ python -m pip install -e .[dev]
231
+ ```
232
+
233
+ Run the test suite with:
234
+
235
+ ```
236
+ uv run -m pytest
237
+ ```
238
+ or
239
+ ```powershell
240
+ python -m pytest
241
+ ```
242
+
243
+ `tests/test_bus.py` scaffolds round-trip tests against two backends — a fully
244
+ in-memory mocked serial port and pyserial's `loop://` — mirroring the
245
+ structure of python-can's own `serial` interface test suite. The test
246
+ bodies are currently stubs (`skipTest`); fill them in as coverage is added.
247
+
248
+ ## Future improvement ideas
249
+
250
+ - **`can_filters` support** — already works today for free: `BusABC.recv()`
251
+ always calls `_matches_filters()` in software unless `_recv_internal`
252
+ reports the message as pre-filtered, and this interface never does. What's
253
+ still missing is a hardware-style optimization: implement the optional
254
+ `_apply_filters()` hook so frames can be dropped *before* the CRC check
255
+ runs, instead of always paying the full decode+CRC cost and filtering
256
+ afterward.
257
+ - Support the CAN FD protocol for the `BusABC` parent (`can_protocol`
258
+ currently reports `CAN_20` unconditionally).
259
+ - Add an option for arbitrary (non-CAN) data length up to 255 bytes, since
260
+ `dlc` is only 1 byte — useful for passing arbitrary serial payloads through
261
+ python-can tooling without being constrained to CAN/CAN FD payload sizes.
@@ -0,0 +1,8 @@
1
+ flexible_serial/__init__.py,sha256=0XQ0axiWmTV75TIN2M0pM71QNvG_VbDm2wirLRALeRk,129
2
+ flexible_serial/bus.py,sha256=QXFIIey4GF-jp2sXYVjNElWBQeEvz12dbvZSqA2Ocho,29230
3
+ python_can_flexible_serial-0.1.0.dist-info/licenses/LICENSE,sha256=T9834WtL-nnhksAVXTn15FNr5fYyS1KzyLodhEVbKm4,1067
4
+ python_can_flexible_serial-0.1.0.dist-info/METADATA,sha256=7TlEeJcNW4_UCjzT-0y3JKzHHngHHk7TQ8dy4UOoX7s,10036
5
+ python_can_flexible_serial-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ python_can_flexible_serial-0.1.0.dist-info/entry_points.txt,sha256=FvRVfuDmEVWkQEeHq4CEczaXichOYQpbSLzelMhE8V0,69
7
+ python_can_flexible_serial-0.1.0.dist-info/top_level.txt,sha256=CBMqcoYnde__UVfBfwco7r1FFNAkA4MPV5evBl4Bsws,16
8
+ python_can_flexible_serial-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [can.interface]
2
+ flexible_serial = flexible_serial.bus:FlexibleSerial
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Bulow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flexible_serial