#!/usr/bin/env python3 """Operate an IDS-CAN switched load over socketcan, with the auth exchange. Acts as controller node 01: request a challenge -> read the module's fresh challenge -> return remote_control_response(challenge) -> send the opcode x3. Pure stdlib (raw AF_CAN socket); needs can0 already up (slcand). Usage: idscan_cmd.py e.g. idscan_cmd.py F8 on """ import socket import struct import sys import time from ids_can_auth import remote_control_response CAN_EFF_FLAG = 0x80000000 FMT = "=IB3x8s" # can_id, can_dlc, pad/res, data[8] ARM_PAGE, RESP_PAGE, CHAL_PAGE = 0x42, 0x43, 0x42 def eff(i): return i | CAN_EFF_FLAG def pack(can_id, data=b""): return struct.pack(FMT, eff(can_id), len(data), data.ljust(8, b"\x00")) def unpack(frame): cid, dlc, data = struct.unpack(FMT, frame) is_eff = bool(cid & CAN_EFF_FLAG) return is_eff, cid & 0x1FFFFFFF, dlc, data[:dlc] def status_id(node): # 11-bit page-3 live-value broadcast return (3 << 8) | node def read_status(s, node, timeout=2.0): want = status_id(node) end = time.time() + timeout while time.time() < end: s.settimeout(max(0.01, end - time.time())) try: is_eff, cid, dlc, data = unpack(s.recv(16)) except socket.timeout: break if not is_eff and cid == want: return data return None def actuate(node, op): arm_id = (0x01 << 18) | (0 << 16) | (node << 8) | ARM_PAGE # 01->node p42 resp_id = (0x01 << 18) | (0 << 16) | (node << 8) | RESP_PAGE # 01->node p43 chal_id = (node << 18) | (1 << 16) | (0x01 << 8) | CHAL_PAGE # node->01 p42 opcode_id = (0x0006 << 16) | (node << 8) | op # 0006 s = socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW) s.bind(("can0",)) before = read_status(s, node, 1.5) print(f" before: page3 = {before.hex(' ') if before else '(none)'}") s.send(pack(arm_id, b"\x00\x04")) # arm # await the module's fresh challenge chal = None s.settimeout(0.3) end = time.time() + 0.3 while time.time() < end: try: is_eff, cid, dlc, data = unpack(s.recv(16)) except socket.timeout: break if is_eff and cid == chal_id and dlc == 6 and data[:2] == b"\x00\x04": chal = data[2:6] break if chal is None: print(" !! no challenge from module — aborting (load not actuated)") return False ch_int = int.from_bytes(chal, "big") rr = remote_control_response(ch_int) print(f" challenge {ch_int:08X} -> response {rr:08X}") s.send(pack(resp_id, b"\x00\x04" + rr.to_bytes(4, "big"))) # response time.sleep(0.004) for _ in range(3): # opcode x3 s.send(pack(opcode_id)) time.sleep(0.006) time.sleep(0.3) after = read_status(s, node, 1.5) print(f" after: page3 = {after.hex(' ') if after else '(none)'}") s.close() return True if __name__ == "__main__": if len(sys.argv) != 3 or sys.argv[2] not in ("on", "off"): sys.exit(__doc__) node = int(sys.argv[1], 16) op = 0x01 if sys.argv[2] == "on" else 0x00 print(f"node {node:02X} -> {sys.argv[2]}") actuate(node, op)