# SANPO USB to CAN The SANPO USB to CAN protocol controls CAN devices from Windows or Linux and forwards CAN data without requiring an additional driver. The raw CAN frame is wrapped with a SANPO header and trailer for transmission over the USB management serial port. These extra bytes are used only by the board; the CAN device receives the raw CAN frame without a SANPO header or trailer. For SANPO USB to CAN FD, see [SANPO USB to FDCAN](usb_fdcan). ## Connection After connecting the board over USB, six USB serial ports appear. Two management ports carry this protocol; Windows normally displays them as `SANPO Studio Management Port`. One management port accesses CAN-1/2 and the other accesses CAN-3/4. The other four ports are independent RS485 serial ports. `COM` and `/dev/ttyACM*` numbering is not guaranteed to follow the board silkscreen; initially connect only one CAN device to confirm the mapping. On Linux, you can run the Python [serial-port identification example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/rs485/identify_sanpo_serial_ports_v8.py) to identify the two management ports. ## Automatically Configure All CAN Bitrates (Recommended) Install the dependency: ```bash python -m pip install pyserial ``` The complete code below automatically finds both management ports of one connected SPINE V8 on Windows or Linux. It sends the same setting to both MCUs, thereby configuring CAN-1 through CAN-4 without requiring any management-port or board-interface mapping. This example configures classic CAN to 1 Mbps. The 5 Mbps CAN FD data-phase value is the required second command argument and does not affect classic CAN communication. ```python import re import time import serial from serial.tools import list_ports COMMANDS = ["AT+SETFDCAN=1000000,5000000"] def is_management_port(port): if (port.vid, port.pid) != (0x1209, 0x2323): return False info = " ".join(str(getattr(port, name, "") or "") for name in ("hwid", "interface", "location")).upper() return ("MI_01" in info or "MANAGEMENT PORT" in info or re.search(r":(?:X|\d+)\.1(?:\D|$)", info) is not None) def send_at(stream, command): stream.reset_input_buffer() stream.write(command.encode("ascii") + b"\r\n") stream.flush() deadline, reply = time.monotonic() + 1.0, bytearray() while time.monotonic() < deadline: reply.extend(stream.read(stream.in_waiting or 1)) if b"OK\r\n" in reply or b"ERR," in reply: break text = reply.decode("ascii", errors="replace").strip() if "OK" not in text: raise RuntimeError(f"{stream.port}: {command}: {text or 'no reply'}") print(f"{stream.port}: {command} -> {text}") ports = sorted(p.device for p in list_ports.comports() if is_management_port(p)) if len(ports) != 2: raise SystemExit(f"Expected 2 SANPO management ports, found {len(ports)}: {ports}") for device in ports: with serial.Serial(device, 1_000_000, timeout=0.05, write_timeout=1) as stream: time.sleep(0.05) for command in COMMANDS: send_at(stream, command) print("CAN-1..CAN-4 configured successfully.") ``` To use another classic CAN bitrate, change only the first number in `COMMANDS`. The `1_000_000` used to open the management port is only a CDC API parameter; it does not set the physical CAN or RS485 bitrate. Only the explicit `AT+SETFDCAN` command changes the physical bus settings. | Parameter | Supported values | | --- | --- | | CAN bitrate | `1000000`, `500000`, `250000`, `125000`, `100000` | ## CAN Extended Frame Send and receive use the same format: | Field | Length | Description | | --- | ---: | --- | | Header | 2 bytes | Fixed `45 54`, or `ET` | | Channel | 1 byte | CAN interface number | | CAN ID | 4 bytes | 29-bit extended ID, big-endian | | Data length | 1 byte | `0` to `8` | | CAN data | 0-8 bytes | Must match the preceding length | | Trailer | 2 bytes | Fixed `0D 0A` | Send 8 data bytes to Channel 1 with extended ID `0x0000FD01`: ```text 45 54 01 00 00 FD 01 08 01 02 03 04 05 06 07 08 0D 0A ``` ## CAN Standard Frame Send and receive use the same format: | Field | Length | Description | | --- | ---: | --- | | Header | 2 bytes | Fixed `53 54`, or `ST` | | Channel | 1 byte | CAN interface number | | Reserved | 2 bytes | Fixed `00 00` | | CAN ID | 2 bytes | 11-bit standard ID, big-endian | | Data length | 1 byte | `0` to `8` | | CAN data | 0-8 bytes | Must match the preceding length | | Trailer | 2 bytes | Fixed `0D 0A` | Send 4 data bytes to Channel 2 with standard ID `0x142`: ```text 53 54 02 00 00 01 42 04 11 22 33 44 0D 0A ``` ## Xiaomi CyberGear Example Example program: [USB to CAN CyberGear example](https://gitcode.com/sanpo/robot/blob/main/products/spine/v8/demo/can/usb2can_cybergear_demo_v8.py) Windows example: ```powershell python usb2can_cybergear_demo_v8.py --port COM8 --motors 1 --channel 1 ``` Linux example: ```bash sudo python3 usb2can_cybergear_demo_v8.py --port /dev/ttyACM0 --motors 1 --channel 1 ``` Adjust the serial port, interface, and motor ID for the actual installation. ## Troubleshooting | Symptom | Action | | --- | --- | | USB serial port cannot be found | Check the USB cable, power for the control section, and reconnect the board | | No reply after sending | Check the USB port mapping, Channel, CAN ID, bit rate, termination resistors, wiring, and device power | | The frame is sent to both CAN interfaces | Channel is `0x00`; select a specific channel | | Reply Channel differs from board CAN-3/4 | Replies use local `1/2` numbering on the current MCU; this is normal | | Reply format differs from this page | Send `AT+ET` as text, wait for `OK`, and retry |