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-candle reconfigures 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_usb driver and appears as canX, use SocketCAN directly to avoid competing host drivers on the same USB interface.

Troubleshooting

Symptom

Action

Device not found

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

Access denied or device busy

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