Skip to content

Commit 0fd66e0

Browse files
committed
Merge branch 'develop' into felixdivo-patch-socketcan
2 parents 146c8d9 + e0bb245 commit 0fd66e0

15 files changed

+437
-107
lines changed

.mergify.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pull_request_rules:
2+
- name: Automatic merge on up to date branch with dual approval
3+
conditions:
4+
- "#approved-reviews-by>=2"
5+
- "status-success=continuous-integration/travis-ci/pr"
6+
- "label!=work-in-progress"
7+
actions:
8+
merge:
9+
method: merge
10+
strict: true

can/broadcastmanager.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
:meth:`can.BusABC.send_periodic`.
66
"""
77

8-
from typing import Optional, Sequence, Tuple, Union, TYPE_CHECKING
8+
from typing import Optional, Sequence, Tuple, Union, Callable, TYPE_CHECKING
99

1010
from can import typechecking
1111

@@ -198,7 +198,7 @@ def __init__(
198198
class ThreadBasedCyclicSendTask(
199199
ModifiableCyclicTaskABC, LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC
200200
):
201-
"""Fallback cyclic send task using thread."""
201+
"""Fallback cyclic send task using daemon thread."""
202202

203203
def __init__(
204204
self,
@@ -207,13 +207,28 @@ def __init__(
207207
messages: Union[Sequence[Message], Message],
208208
period: float,
209209
duration: Optional[float] = None,
210+
on_error: Optional[Callable[[Exception], bool]] = None,
210211
):
212+
"""Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`.
213+
214+
The `on_error` is called if any error happens on `bus` while sending `messages`.
215+
If `on_error` present, and returns ``False`` when invoked, thread is
216+
stopped immediately, otherwise, thread continuiously tries to send `messages`
217+
ignoring errors on a `bus`. Absence of `on_error` means that thread exits immediately
218+
on error.
219+
220+
:param on_error: The callable that accepts an exception if any
221+
error happened on a `bus` while sending `messages`,
222+
it shall return either ``True`` or ``False`` depending
223+
on desired behaviour of `ThreadBasedCyclicSendTask`.
224+
"""
211225
super().__init__(messages, period, duration)
212226
self.bus = bus
213227
self.send_lock = lock
214228
self.stopped = True
215229
self.thread = None
216230
self.end_time = time.perf_counter() + duration if duration else None
231+
self.on_error = on_error
217232

218233
if HAS_EVENTS:
219234
self.period_ms: int = int(round(period * 1000, 0))
@@ -250,7 +265,11 @@ def _run(self):
250265
self.bus.send(self.messages[msg_index])
251266
except Exception as exc:
252267
log.exception(exc)
253-
break
268+
if self.on_error:
269+
if not self.on_error(exc):
270+
break
271+
else:
272+
break
254273
if self.end_time is not None and time.perf_counter() >= self.end_time:
255274
break
256275
msg_index = (msg_index + 1) % len(self.messages)

can/io/asc.py

Lines changed: 141 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
- under `test/data/logfile.asc`
77
"""
88

9-
from typing import cast, Any, Generator, IO, List, Optional, Tuple, Union
9+
from typing import cast, Any, Generator, IO, List, Optional, Union, Dict
1010
from can import typechecking
1111

1212
from datetime import datetime
@@ -53,122 +53,165 @@ def __init__(
5353
if not self.file:
5454
raise ValueError("The given file cannot be None")
5555
self.base = base
56+
self._converted_base = self._check_base(base)
57+
self.date = None
58+
self.timestamps_format = None
59+
self.internal_events_logged = None
5660

57-
@staticmethod
58-
def _extract_can_id(str_can_id: str, base: int) -> Tuple[int, bool]:
61+
def _extract_header(self):
62+
for line in self.file:
63+
line = line.strip()
64+
lower_case = line.lower()
65+
if lower_case.startswith("date"):
66+
self.date = line[5:]
67+
elif lower_case.startswith("base"):
68+
try:
69+
_, base, _, timestamp_format = line.split()
70+
except ValueError:
71+
raise Exception("Unsupported header string format: {}".format(line))
72+
self.base = base
73+
self._converted_base = self._check_base(self.base)
74+
self.timestamps_format = timestamp_format
75+
elif lower_case.endswith("internal events logged"):
76+
self.internal_events_logged = not lower_case.startswith("no")
77+
# Currently the last line in the header which is parsed
78+
break
79+
else:
80+
break
81+
82+
def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None:
5983
if str_can_id[-1:].lower() == "x":
60-
is_extended = True
61-
can_id = int(str_can_id[0:-1], base)
84+
msg_kwargs["is_extended_id"] = True
85+
can_id = int(str_can_id[0:-1], self._converted_base)
6286
else:
63-
is_extended = False
64-
can_id = int(str_can_id, base)
65-
return can_id, is_extended
87+
msg_kwargs["is_extended_id"] = False
88+
can_id = int(str_can_id, self._converted_base)
89+
msg_kwargs["arbitration_id"] = can_id
6690

6791
@staticmethod
6892
def _check_base(base: str) -> int:
6993
if base not in ["hex", "dec"]:
7094
raise ValueError('base should be either "hex" or "dec"')
7195
return BASE_DEC if base == "dec" else BASE_HEX
7296

97+
def _process_data_string(
98+
self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any]
99+
) -> None:
100+
frame = bytearray()
101+
data = data_str.split()
102+
for byte in data[:data_length]:
103+
frame.append(int(byte, self._converted_base))
104+
msg_kwargs["data"] = frame
105+
106+
def _process_classic_can_frame(
107+
self, line: str, msg_kwargs: Dict[str, Any]
108+
) -> Message:
109+
110+
# CAN error frame
111+
if line.strip()[0:10].lower() == "errorframe":
112+
# Error Frame
113+
msg_kwargs["is_error_frame"] = True
114+
else:
115+
abr_id_str, dir, rest_of_message = line.split(None, 2)
116+
msg_kwargs["is_rx"] = dir == "Rx"
117+
self._extract_can_id(abr_id_str, msg_kwargs)
118+
119+
if rest_of_message[0].lower() == "r":
120+
# CAN Remote Frame
121+
msg_kwargs["is_remote_frame"] = True
122+
remote_data = rest_of_message.split()
123+
if len(remote_data) > 1:
124+
dlc_str = remote_data[1]
125+
if dlc_str.isdigit():
126+
msg_kwargs["dlc"] = int(dlc_str, self._converted_base)
127+
else:
128+
# Classic CAN Message
129+
try:
130+
# There is data after DLC
131+
_, dlc_str, data = rest_of_message.split(None, 2)
132+
except ValueError:
133+
# No data after DLC
134+
_, dlc_str = rest_of_message.split(None, 1)
135+
data = ""
136+
137+
dlc = int(dlc_str, self._converted_base)
138+
msg_kwargs["dlc"] = dlc
139+
self._process_data_string(data, dlc, msg_kwargs)
140+
141+
return Message(**msg_kwargs)
142+
143+
def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message:
144+
channel, dir, rest_of_message = line.split(None, 2)
145+
# See ASCWriter
146+
msg_kwargs["channel"] = int(channel) - 1
147+
msg_kwargs["is_rx"] = dir == "Rx"
148+
149+
# CAN FD error frame
150+
if rest_of_message.strip()[:10].lower() == "errorframe":
151+
# Error Frame
152+
# TODO: maybe use regex to parse BRS, ESI, etc?
153+
msg_kwargs["is_error_frame"] = True
154+
else:
155+
can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split(
156+
None, 2
157+
)
158+
159+
if frame_name_or_brs.isdigit():
160+
brs = frame_name_or_brs
161+
esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3)
162+
else:
163+
brs, esi, dlc_str, data_length_str, data = rest_of_message.split(
164+
None, 4
165+
)
166+
167+
self._extract_can_id(can_id_str, msg_kwargs)
168+
msg_kwargs["bitrate_switch"] = brs == "1"
169+
msg_kwargs["error_state_indicator"] = esi == "1"
170+
dlc = int(dlc_str, self._converted_base)
171+
msg_kwargs["dlc"] = dlc
172+
data_length = int(data_length_str)
173+
174+
# CAN remote Frame
175+
msg_kwargs["is_remote_frame"] = data_length == 0
176+
177+
self._process_data_string(data, data_length, msg_kwargs)
178+
179+
return Message(**msg_kwargs)
180+
73181
def __iter__(self) -> Generator[Message, None, None]:
74-
base = self._check_base(self.base)
75182
# This is guaranteed to not be None since we raise ValueError in __init__
76183
self.file = cast(IO[Any], self.file)
77-
for line in self.file:
78-
# logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0])
79-
if line.split(" ")[0] == "base":
80-
base = self._check_base(line.split(" ")[1])
184+
self._extract_header()
81185

186+
for line in self.file:
82187
temp = line.strip()
83188
if not temp or not temp[0].isdigit():
189+
# Could be a comment
84190
continue
85-
is_fd = False
86-
is_rx = True
191+
msg_kwargs = {}
87192
try:
88-
timestamp, channel, dummy = temp.split(
89-
None, 2
90-
) # , frameType, dlc, frameData
193+
timestamp, channel, rest_of_message = temp.split(None, 2)
194+
timestamp = float(timestamp)
195+
msg_kwargs["timestamp"] = timestamp
91196
if channel == "CANFD":
92-
timestamp, _, channel, direction, dummy = temp.split(None, 4)
93-
is_fd = True
94-
is_rx = direction == "Rx"
197+
msg_kwargs["is_fd"] = True
198+
elif channel.isdigit():
199+
# See ASCWriter
200+
msg_kwargs["channel"] = int(channel) - 1
201+
else:
202+
# Not a CAN message. Possible values include "statistic", J1939TP
203+
continue
95204
except ValueError:
96-
# we parsed an empty comment
205+
# Some other unprocessed or unknown format
97206
continue
98-
timestamp = float(timestamp)
99-
try:
100-
# See ASCWriter
101-
channel = int(channel) - 1
102-
except ValueError:
103-
pass
104-
if dummy.strip()[0:10].lower() == "errorframe":
105-
msg = Message(timestamp=timestamp, is_error_frame=True, channel=channel)
106-
yield msg
107-
elif (
108-
not isinstance(channel, int)
109-
or dummy.strip()[0:10].lower() == "statistic:"
110-
or dummy.split(None, 1)[0] == "J1939TP"
111-
):
112-
pass
113-
elif dummy[-1:].lower() == "r":
114-
can_id_str, direction, _ = dummy.split(None, 2)
115-
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)
116-
msg = Message(
117-
timestamp=timestamp,
118-
arbitration_id=can_id_num & CAN_ID_MASK,
119-
is_extended_id=is_extended_id,
120-
is_remote_frame=True,
121-
is_rx=direction == "Rx",
122-
channel=channel,
123-
)
124-
yield msg
207+
208+
if "is_fd" not in msg_kwargs:
209+
msg = self._process_classic_can_frame(rest_of_message, msg_kwargs)
125210
else:
126-
brs = None
127-
esi = None
128-
data_length = 0
129-
try:
130-
# this only works if dlc > 0 and thus data is available
131-
if not is_fd:
132-
can_id_str, direction, _, dlc, data = dummy.split(None, 4)
133-
is_rx = direction == "Rx"
134-
else:
135-
can_id_str, frame_name, brs, esi, dlc, data_length, data = dummy.split(
136-
None, 6
137-
)
138-
if frame_name.isdigit():
139-
# Empty frame_name
140-
can_id_str, brs, esi, dlc, data_length, data = dummy.split(
141-
None, 5
142-
)
143-
except ValueError:
144-
# but if not, we only want to get the stuff up to the dlc
145-
can_id_str, _, _, dlc = dummy.split(None, 3)
146-
# and we set data to an empty sequence manually
147-
data = ""
148-
dlc = int(dlc, base)
149-
if is_fd:
150-
# For fd frames, dlc and data length might not be equal and
151-
# data_length is the actual size of the data
152-
dlc = int(data_length)
153-
frame = bytearray()
154-
data = data.split()
155-
for byte in data[0:dlc]:
156-
frame.append(int(byte, base))
157-
can_id_num, is_extended_id = self._extract_can_id(can_id_str, base)
158-
159-
yield Message(
160-
timestamp=timestamp,
161-
arbitration_id=can_id_num & CAN_ID_MASK,
162-
is_extended_id=is_extended_id,
163-
is_remote_frame=False,
164-
dlc=dlc,
165-
data=frame,
166-
is_fd=is_fd,
167-
is_rx=is_rx,
168-
channel=channel,
169-
bitrate_switch=is_fd and brs == "1",
170-
error_state_indicator=is_fd and esi == "1",
171-
)
211+
msg = self._process_fd_can_frame(rest_of_message, msg_kwargs)
212+
if msg is not None:
213+
yield msg
214+
172215
self.stop()
173216

174217

@@ -190,7 +233,7 @@ class ASCWriter(BaseIOHandler, Listener):
190233
"{id:>8} {symbolic_name:>32}",
191234
"{brs}",
192235
"{esi}",
193-
"{dlc}",
236+
"{dlc:x}",
194237
"{data_length:>2}",
195238
"{data}",
196239
"{message_duration:>8}",
@@ -281,10 +324,10 @@ def on_message_received(self, msg: Message) -> None:
281324
self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp)
282325
return
283326
if msg.is_remote_frame:
284-
dtype = "r"
327+
dtype = "r {:x}".format(msg.dlc) # New after v8.5
285328
data: List[str] = []
286329
else:
287-
dtype = "d {}".format(msg.dlc)
330+
dtype = "d {:x}".format(msg.dlc)
288331
data = ["{:02X}".format(byte) for byte in msg.data]
289332
arb_id = "{:X}".format(msg.arbitration_id)
290333
if msg.is_extended_id:

can/listener.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ class BufferedReader(Listener):
8282
be serviced. The messages can then be fetched with
8383
:meth:`~can.BufferedReader.get_message`.
8484
85-
Putting in messages after :meth:`~can.BufferedReader.stop` has be called will raise
85+
Putting in messages after :meth:`~can.BufferedReader.stop` has been called will raise
8686
an exception, see :meth:`~can.BufferedReader.on_message_received`.
8787
88-
:attr bool is_stopped: ``True`` iff the reader has been stopped
88+
:attr bool is_stopped: ``True`` if the reader has been stopped
8989
"""
9090

9191
def __init__(self):

doc/configuration.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ On Linux systems the config file is searched in the following paths:
4343

4444
On Windows systems the config file is searched in the following paths:
4545

46-
#. ``~/can.conf``
46+
#. ``%USERPROFILE%/can.conf``
4747
#. ``can.ini`` (current working directory)
48-
#. ``$APPDATA/can.ini``
48+
#. ``%APPDATA%/can.ini``
4949

5050
The configuration file sets the default interface and channel:
5151

test/data/example_data.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ def sort_messages(messages):
112112
[
113113
Message(is_fd=True, data=range(64)),
114114
Message(is_fd=True, data=range(8)),
115-
Message(is_fd=True, bitrate_switch=True),
116-
Message(is_fd=True, error_state_indicator=True),
115+
Message(is_fd=True, bitrate_switch=True, is_remote_frame=True),
116+
Message(is_fd=True, error_state_indicator=True, is_remote_frame=True),
117+
Message(is_fd=True, data=range(8), bitrate_switch=True),
118+
Message(is_fd=True, data=range(8), error_state_indicator=True),
117119
]
118120
)
119121

0 commit comments

Comments
 (0)