python-can-candle
python-can-candle lets Python access the SANPO SPINE CAN and CAN FD passthrough
interfaces directly on Windows or Linux. It exchanges standard CAN/CAN FD
frames, without a SANPO header or management-port protocol.
A complete board contains two MCUs. Each MCU provides two CAN/CAN FD channels, so a complete board normally exposes two USB devices and four channels. USB enumeration order and channel numbers cannot be directly mapped to the connector silkscreen; verify the physical mapping one channel at a time on first use.
When Linux already provides SocketCAN, SocketCAN remains the
preferred interface. python-can-candle is mainly useful for Windows Python
programs and for applications that need one cross-platform python-can API.
Installation
This document uses python-can-candle 1.2.4:
python -m pip install "python-can-candle==1.2.4"
SANPO SPINE V81 and later firmware uses Windows WCID to bind the system WinUSB driver automatically. Zadig and a vendor-specific driver are not required.
V81 firmware reports the USB product name
SANPO gs_usb CDC Composite fdV8; V82 and later firmware reports the shorter
name SANPO SPINE. The complete demo supplied with this documentation accepts
both names and uses the product string enumerated from each device, so the same
script works before and after a firmware upgrade. If an application explicitly
passes product=, the value must match the installed firmware. The basic
snippets below omit that filter so they remain compatible with both V81 and V82.
Check the installed versions:
python -m pip show python-can-candle candle-api python-can
Detect Channels
import can
for config in can.detect_available_configs("candle"):
print(config)
A complete board normally prints four entries with channel values similar to:
SERIAL_0:0
SERIAL_0:1
SERIAL_1:0
SERIAL_1:1
SERIAL_0 and SERIAL_1 are the USB serial numbers of the two MCUs. A serial
number can reliably distinguish the two MCUs, but cannot identify which pair of
physical board connectors it controls.
Classic CAN
The following example uses channel 0 of the selected MCU at 1 Mbit/s, sends a standard frame, and waits for a reply:
import can
serial = "replace with the actual serial number"
with can.Bus(
interface="candle",
channel=f"{serial}:0",
vid=0x1209,
pid=0x2323,
manufacture="SANPO",
bitrate=1_000_000,
sample_point=80.0,
ignore_config=True,
) as bus:
message = can.Message(
arbitration_id=0x123,
is_extended_id=False,
data=[0x11, 0x22, 0x33, 0x44],
)
bus.send(message, timeout=1.0)
reply = bus.recv(timeout=1.0)
print(reply)
The example ID and data have no device-specific meaning. Replace them with the protocol required by the connected device.
CAN FD
The following example configures a 1 Mbit/s arbitration phase and a 5 Mbit/s data phase, then sends a CAN FD frame with BRS enabled:
import can
serial = "replace with the actual serial number"
with can.Bus(
interface="candle",
channel=f"{serial}:0",
vid=0x1209,
pid=0x2323,
manufacture="SANPO",
fd=True,
bitrate=1_000_000,
sample_point=80.0,
data_bitrate=5_000_000,
data_sample_point=75.0,
ignore_config=True,
) as bus:
message = can.Message(
arbitration_id=0x123,
is_extended_id=False,
is_fd=True,
bitrate_switch=True,
data=bytes(range(16)),
)
bus.send(message, timeout=1.0)
SANPO SPINE CAN FD data phase rates are 2/3/4/5 Mbit/s. We recommend
data_sample_point=80.0 at 2/3/4 Mbit/s and data_sample_point=75.0 at
5 Mbit/s.
Two Channels on the Same MCU
Open the two channels managed by the same MCU through one CandleBus instance:
import can
with can.Bus(
interface="candle",
channel=[0, 1],
serial_number="replace with the actual serial number",
bitrate=1_000_000,
sample_point=80.0,
ignore_config=True,
) as bus:
message = can.Message(
arbitration_id=0x123,
is_extended_id=False,
channel=1,
data=[0x01, 0x02],
)
bus.send(message, timeout=1.0)
Message.channel selects the physical CAN channel on the host. It is not
written into the CAN data and is not a SANPO custom channel field. To access all
four CAN channels, create one CandleBus for each MCU.
Xiaomi CyberGear Example
Example program: python-can-candle CyberGear example
First enumerate devices without transmitting CAN frames:
python pythoncan_cybergear_demo_v8.py --list-devices
After confirming the two MCU serial numbers and physical channel mapping, run an unloaded initialization test. This command configures CAN and sends motor initialization, enable, and stop commands:
python pythoncan_cybergear_demo_v8.py \
--enable-motion \
--init-only \
--device-serials "SERIAL_0,SERIAL_1" \
--motor-map "0/0:1,2,3;0/1:4,5,6;1/0:7,8,9;1/1:10,11,12"
0/1 means channel 1 of the first MCU in --device-serials. The first run
must be unloaded; confirm the wiring and emergency stop, then adjust the motor
IDs and channel mapping for the actual installation.
Limitations
A single gs_usb interface cannot be opened simultaneously by CANgaroo,
python-can-candle, or another WinUSB program. Close CANgaroo’s measurement connection before running the Python example.Do not transmit to one physical CAN/CAN FD channel simultaneously through USB passthrough, management-port protocol mode, SPI, or an offline task.
python-can-candlereconfigures the bit rate when opening channels. These settings are not permanently saved as user configuration.On Linux, when the interface has already been attached by the kernel
gs_usbdriver and appears ascanX, use SocketCAN directly to avoid competing host drivers on the same USB interface.
Troubleshooting
Symptom |
Action |
|---|---|
|
Confirm both MCUs run V81 or later, unplug and reconnect USB, then enumerate again |
Only two channels appear on a complete board |
Usually only one MCU is recognized; check the other MCU’s firmware, power, and USB enumeration |
|
Close CANgaroo, other Python processes, and software using the WinUSB interface |
Frames transmit but the device does not reply |
Check the channel-to-connector mapping, bit rate, termination, device ID, and power |
CAN FD communication errors |
Confirm arbitration/data rates and sample points match every device on the bus |