Skip to content

Commit 05f37b3

Browse files
committed
support multichannel
1 parent e50b709 commit 05f37b3

2 files changed

Lines changed: 152 additions & 95 deletions

File tree

candle/candle_bus.py

Lines changed: 151 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,53 @@
11
import time
2-
from typing import Optional, Tuple, List, Union
2+
from typing import Optional, Tuple, List, Union, TypedDict
3+
from collections.abc import Sequence
34
import can
5+
from can.util import len2dlc
46
from can.typechecking import CanFilters, AutoDetectedConfig
57
import candle_api as api
68

79

8-
ISO_DLC = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)
10+
class ChannelConfig(TypedDict):
11+
bitrate: int
12+
sample_point: float
13+
data_bitrate: int
14+
data_sample_point: float
15+
fd: bool
16+
loop_back: bool
17+
listen_only: bool
18+
triple_sample: bool
19+
one_shot: bool
20+
bit_error_reporting: bool
21+
termination: Optional[bool]
22+
23+
24+
def convert_frame(channel: int, frame: api.CandleCanFrame, hardware_timestamp: bool) -> can.Message:
25+
return can.Message(
26+
timestamp=frame.timestamp if hardware_timestamp else time.monotonic(),
27+
arbitration_id=frame.can_id,
28+
is_extended_id=frame.frame_type.extended_id,
29+
is_remote_frame=frame.frame_type.remote_frame,
30+
is_error_frame=frame.frame_type.error_frame,
31+
channel=channel,
32+
dlc=frame.size, # https://github.com/hardbyte/python-can/issues/749
33+
data=bytearray(frame),
34+
is_fd=frame.frame_type.fd,
35+
is_rx=frame.frame_type.rx,
36+
bitrate_switch=frame.frame_type.bitrate_switch,
37+
error_state_indicator=frame.frame_type.error_state_indicator
38+
)
939

1040

1141
class CandleBus(can.bus.BusABC):
12-
def __init__(self, channel: Union[int, str], can_filters: Optional[CanFilters] = None,
42+
def __init__(self, channel: Union[int, str, Sequence[int]], can_filters: Optional[CanFilters] = None,
1343
bitrate: int = 1000000, sample_point: float = 87.5,
1444
data_bitrate: int = 5000000, data_sample_point: float = 87.5,
1545
fd: bool = False, loop_back: bool = False, listen_only: bool = False,
1646
triple_sample: bool = False, one_shot: bool = False, bit_error_reporting: bool = False,
1747
termination: Optional[bool] = None, vid: Optional[int] = None, pid: Optional[int] = None,
1848
manufacture: Optional[str] = None, product: Optional[str] = None,
19-
serial_number: Optional[str] = None, **kwargs) -> None:
49+
serial_number: Optional[str] = None, channel_configs: Optional[dict[int, ChannelConfig]] = None,
50+
**kwargs) -> None:
2051

2152
# If ignore_config is not set, can.util.cast_from_string may cause unexpected type conversions.
2253
if manufacture is not None:
@@ -29,11 +60,13 @@ def __init__(self, channel: Union[int, str], can_filters: Optional[CanFilters] =
2960
# Parse channel.
3061
if isinstance(channel, str):
3162
serial_number, channel_number = channel.split(':')
32-
self._channel_number = int(channel_number)
63+
self._channel_numbers = (int(channel_number),)
3364
elif isinstance(channel, int):
34-
self._channel_number = channel
65+
self._channel_numbers = (channel,)
66+
elif isinstance(channel, Sequence):
67+
self._channel_numbers = tuple(channel)
3568
else:
36-
raise TypeError("channel must be of type str or int")
69+
raise TypeError("Channel must be of type int, str or Sequence[int]")
3770

3871
# Find the device.
3972
self._device = self._find_device(vid, pid, manufacture, product, serial_number)
@@ -42,69 +75,82 @@ def __init__(self, channel: Union[int, str], can_filters: Optional[CanFilters] =
4275
self._device.open()
4376

4477
# Get the channel.
45-
self._channel = self._device[self._channel_number]
46-
self._hardware_timestamp = self._channel.feature.hardware_timestamp
47-
self.channel_info = f'{self._device.serial_number}:{self._channel_number}'
78+
self._channels = {i: self._device[i] for i in self._channel_numbers}
79+
self._hardware_timestamps = {i: self._channels[i].feature.hardware_timestamp for i in self._channel_numbers}
80+
self.channel_info = f'{self._device.serial_number}:{self._channel_numbers}'
4881

4982
# Reset channel.
50-
self._channel.reset()
51-
52-
# Set termination.
53-
if termination is not None:
54-
self._channel.set_termination(termination)
55-
56-
# Set bit timing.
57-
props_seg = 1
58-
if fd and self._channel.feature.fd:
59-
bit_timing_fd = can.BitTimingFd.from_sample_point(
60-
f_clock=self._channel.clock_frequency,
61-
nom_bitrate=bitrate,
62-
nom_sample_point=sample_point,
63-
data_bitrate=data_bitrate,
64-
data_sample_point=data_sample_point
65-
)
66-
67-
self._channel.set_bit_timing(
68-
props_seg,
69-
bit_timing_fd.nom_tseg1 - props_seg,
70-
bit_timing_fd.nom_tseg2,
71-
bit_timing_fd.nom_sjw,
72-
bit_timing_fd.nom_brp
73-
)
83+
[ch.reset() for ch in self._channels.values()]
7484

75-
self._channel.set_data_bit_timing(
76-
props_seg,
77-
bit_timing_fd.data_tseg1 - props_seg,
78-
bit_timing_fd.data_tseg2,
79-
bit_timing_fd.data_sjw,
80-
bit_timing_fd.data_brp
81-
)
82-
else:
83-
bit_timing = can.BitTiming.from_sample_point(
84-
f_clock=self._channel.clock_frequency,
85-
bitrate=bitrate,
86-
sample_point=sample_point,
87-
)
85+
# Configure channel.
86+
default_config = ChannelConfig(
87+
bitrate=bitrate, sample_point=sample_point, data_bitrate=data_bitrate, data_sample_point=data_sample_point,
88+
fd=fd, loop_back=loop_back, listen_only=listen_only, triple_sample=triple_sample, one_shot=one_shot,
89+
bit_error_reporting=bit_error_reporting, termination=termination
90+
)
8891

89-
self._channel.set_bit_timing(
90-
props_seg,
91-
bit_timing.tseg1 - props_seg,
92-
bit_timing.tseg2,
93-
bit_timing.sjw,
94-
bit_timing.brp
92+
for i, ch in self._channels.items():
93+
# Get channel configuration.
94+
cfg: ChannelConfig = default_config.copy()
95+
if channel_configs is not None:
96+
cfg |= channel_configs.get(i, {})
97+
98+
# Set termination.
99+
if cfg["termination"] is not None:
100+
ch.set_termination(termination)
101+
102+
# Set bit timing.
103+
props_seg = 1
104+
if cfg["fd"] and ch.feature.fd:
105+
bit_timing_fd = can.BitTimingFd.from_sample_point(
106+
f_clock=ch.clock_frequency,
107+
nom_bitrate=cfg["bitrate"],
108+
nom_sample_point=cfg["sample_point"],
109+
data_bitrate=cfg["data_bitrate"],
110+
data_sample_point=cfg["data_sample_point"]
111+
)
112+
113+
ch.set_bit_timing(
114+
props_seg,
115+
bit_timing_fd.nom_tseg1 - props_seg,
116+
bit_timing_fd.nom_tseg2,
117+
bit_timing_fd.nom_sjw,
118+
bit_timing_fd.nom_brp
119+
)
120+
121+
ch.set_data_bit_timing(
122+
props_seg,
123+
bit_timing_fd.data_tseg1 - props_seg,
124+
bit_timing_fd.data_tseg2,
125+
bit_timing_fd.data_sjw,
126+
bit_timing_fd.data_brp
127+
)
128+
else:
129+
bit_timing = can.BitTiming.from_sample_point(
130+
f_clock=ch.clock_frequency,
131+
bitrate=cfg["bitrate"],
132+
sample_point=cfg["sample_point"],
133+
)
134+
135+
ch.set_bit_timing(
136+
props_seg,
137+
bit_timing.tseg1 - props_seg,
138+
bit_timing.tseg2,
139+
bit_timing.sjw,
140+
bit_timing.brp
141+
)
142+
143+
# Open the channel.
144+
ch.start(
145+
hardware_timestamp=self._hardware_timestamps[i],
146+
fd=cfg["fd"],
147+
loop_back=cfg["loop_back"],
148+
listen_only=cfg["listen_only"],
149+
triple_sample=cfg["triple_sample"],
150+
one_shot=cfg["one_shot"],
151+
bit_error_reporting=cfg["bit_error_reporting"]
95152
)
96153

97-
# Open the channel.
98-
self._channel.start(
99-
hardware_timestamp=self._channel.feature.hardware_timestamp,
100-
fd=fd,
101-
loop_back=loop_back,
102-
listen_only=listen_only,
103-
triple_sample=triple_sample,
104-
one_shot=one_shot,
105-
bit_error_reporting=bit_error_reporting
106-
)
107-
108154
super().__init__(
109155
channel=channel,
110156
can_filters=can_filters,
@@ -129,37 +175,47 @@ def _find_device(vid: Optional[int] = None, pid: Optional[int] = None, manufactu
129175
else:
130176
raise can.exceptions.CanInitializationError('Device not found!')
131177

132-
def _recv_internal(
133-
self, timeout: Optional[float]
134-
) -> Tuple[Optional[can.Message], bool]:
135-
frame: Optional[api.CandleCanFrame] = None
178+
def _recv_internal(self, timeout: Optional[float]) -> Tuple[Optional[can.Message], bool]:
179+
# Check if there is a frame available.
180+
for i, ch in self._channels.items():
181+
frame = ch.receive_nowait()
182+
if frame is not None:
183+
return convert_frame(i, frame, self._hardware_timestamps[i]), False
184+
185+
# Do not block if timeout is None.
136186
if timeout is None:
137-
frame = self._channel.receive_nowait()
138-
else:
139-
try:
140-
frame = self._channel.receive(timeout)
141-
except TimeoutError:
142-
pass
143-
144-
if frame is not None:
145-
msg = can.Message(
146-
timestamp=frame.timestamp if self._hardware_timestamp else time.monotonic(),
147-
arbitration_id=frame.can_id,
148-
is_extended_id=frame.frame_type.extended_id,
149-
is_remote_frame=frame.frame_type.remote_frame,
150-
is_error_frame=frame.frame_type.error_frame,
151-
channel=self._channel_number,
152-
dlc=frame.size, # https://github.com/hardbyte/python-can/issues/749
153-
data=bytearray(frame),
154-
is_fd=frame.frame_type.fd,
155-
is_rx=frame.frame_type.rx,
156-
bitrate_switch=frame.frame_type.bitrate_switch,
157-
error_state_indicator=frame.frame_type.error_state_indicator
158-
)
159-
return msg, False
187+
return None, False
188+
189+
# Block until a frame is available.
190+
if not self._device.wait_for_frame(timeout):
191+
return None, False
192+
193+
# Check if there is a frame available.
194+
for i, ch in self._channels.items():
195+
frame = ch.receive_nowait()
196+
if frame is not None:
197+
return convert_frame(i, frame, self._hardware_timestamps[i]), False
198+
199+
# No frame available after timeout.
160200
return None, False
161201

162202
def send(self, msg: can.Message, timeout: Optional[float] = None) -> None:
203+
# Parse channel.
204+
target_channels: Tuple[api.CandleChannel, ...]
205+
if len(self._channel_numbers) == 1:
206+
# There is only one channel.
207+
target_channels = (self._channels[self._channel_numbers[0]],)
208+
else:
209+
if isinstance(msg.channel, str):
210+
serial_number, channel_number = msg.channel.split(':')
211+
target_channels = (self._channels[int(channel_number)],)
212+
elif isinstance(msg.channel, int):
213+
target_channels = (self._channels[msg.channel],)
214+
elif isinstance(msg.channel, Sequence):
215+
target_channels = tuple(self._channels[i] for i in msg.channel)
216+
else:
217+
raise TypeError("Channel must be of type int, str or Sequence[int]")
218+
163219
if timeout is None:
164220
timeout = 1.0
165221

@@ -174,17 +230,18 @@ def send(self, msg: can.Message, timeout: Optional[float] = None) -> None:
174230
error_state_indicator=msg.error_state_indicator
175231
),
176232
msg.arbitration_id,
177-
ISO_DLC.index(msg.dlc),
233+
len2dlc(msg.dlc),
178234
msg.data
179235
)
180236

181237
try:
182-
self._channel.send(frame, timeout)
238+
for ch in target_channels:
239+
ch.send(frame, timeout)
183240
except TimeoutError as exc:
184241
raise can.CanOperationError("The message could not be sent") from exc
185242

186243
def shutdown(self):
187-
self._channel.reset()
244+
[ch.reset() for ch in self._channels.values()]
188245
super().shutdown()
189246

190247
@staticmethod

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ keywords = ["gs_usb", "can", "candleLight"]
1515
dynamic = ["version"]
1616
dependencies = [
1717
"python-can >= 4.0.0",
18-
"candle-api == 0.0.10"
18+
"candle-api == 0.0.11"
1919
]
2020

2121
[project.urls]

0 commit comments

Comments
 (0)