185 lines
5.3 KiB
Python
185 lines
5.3 KiB
Python
"""CANalyst-II + MS3008 广播模式简单手动测试。"""
|
||
|
||
import argparse
|
||
import struct
|
||
import time
|
||
|
||
import can
|
||
|
||
CAN_CHANNEL = 0 # CANalyst-II 面板 CAN1
|
||
CAN_BITRATE = 1000000
|
||
CAN_DEVICE = 0
|
||
|
||
CONTROL_FRAME_ID = 0x280
|
||
COMMAND_FRAME_ID = 0x288
|
||
READ_STATUS_2 = 0x9C
|
||
MOTOR_ON_COMMAND = 0x88
|
||
STOP_COMMAND = 0x81
|
||
|
||
|
||
def open_can() -> can.BusABC:
|
||
"""打开 CANalyst-II:channel=0 对应面板 CAN1。"""
|
||
return can.Bus(
|
||
interface="canalystii",
|
||
channel=CAN_CHANNEL,
|
||
bitrate=CAN_BITRATE,
|
||
device=CAN_DEVICE,
|
||
)
|
||
|
||
|
||
def read_status(bus: can.BusABC, timeout: float = 0.5) -> dict[int, dict[str, int]]:
|
||
"""广播请求 ID 1、2 的状态2(0x9C)。"""
|
||
data = bytes([READ_STATUS_2, 0, READ_STATUS_2, 0, 0, 0, 0, 0])
|
||
message = can.Message(
|
||
arbitration_id=COMMAND_FRAME_ID,
|
||
is_extended_id=False,
|
||
data=data,
|
||
)
|
||
bus.send(message)
|
||
print(f"TX ID=0x{COMMAND_FRAME_ID:03X} DATA={data.hex(' ').upper()}")
|
||
|
||
reports: dict[int, dict[str, int]] = {}
|
||
deadline = time.monotonic() + timeout
|
||
while len(reports) < 2:
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
break
|
||
|
||
reply = bus.recv(timeout=remaining)
|
||
print(reply)
|
||
if reply is None:
|
||
break
|
||
|
||
motor_id = reply.arbitration_id - 0x140
|
||
if motor_id not in (1, 2) or len(reply.data) != 8 or reply.data[0] != READ_STATUS_2:
|
||
continue
|
||
|
||
temperature = struct.unpack_from("<b", reply.data, 1)[0]
|
||
power_raw, speed_dps, encoder_raw = struct.unpack_from("<hhH", reply.data, 2)
|
||
reports[motor_id] = {
|
||
"temperature_c": temperature,
|
||
"power_raw": power_raw,
|
||
"speed_dps": speed_dps,
|
||
"encoder_raw": encoder_raw,
|
||
}
|
||
print(
|
||
f"RX ID=0x{reply.arbitration_id:03X} "
|
||
f"DATA={bytes(reply.data).hex(' ').upper()} "
|
||
f"motor={motor_id} temp={temperature}C power={power_raw} "
|
||
f"speed={speed_dps}dps encoder={encoder_raw}"
|
||
)
|
||
|
||
missing = sorted({1, 2} - reports.keys())
|
||
if missing:
|
||
print(f"未收到电机 {missing} 的状态回包")
|
||
return reports
|
||
|
||
|
||
def torque_control(
|
||
bus: can.BusABC,
|
||
motor1: int,
|
||
motor2: int,
|
||
frequency_hz: float = 10.0,
|
||
) -> None:
|
||
"""
|
||
广播发送 ID 1、2 的控制量。
|
||
|
||
MS3008 中该值实际是 -850..850 的开环 powerControl raw,不是 N·m。
|
||
"""
|
||
if not -850 <= motor1 <= 850 or not -850 <= motor2 <= 850:
|
||
raise ValueError("MS3008 控制量必须在 -850..850")
|
||
|
||
enable_data = bytes([MOTOR_ON_COMMAND, 0, MOTOR_ON_COMMAND, 0, 0, 0, 0, 0])
|
||
bus.send(
|
||
can.Message(
|
||
arbitration_id=COMMAND_FRAME_ID,
|
||
is_extended_id=False,
|
||
data=enable_data,
|
||
)
|
||
)
|
||
print(f"TX ID=0x{COMMAND_FRAME_ID:03X} DATA={enable_data.hex(' ').upper()}")
|
||
time.sleep(0.02)
|
||
|
||
control_data = struct.pack("<hhhh", motor1, motor2, 0, 0)
|
||
control_message = can.Message(
|
||
arbitration_id=CONTROL_FRAME_ID,
|
||
is_extended_id=False,
|
||
data=control_data,
|
||
)
|
||
period = 1.0 / frequency_hz
|
||
next_send = time.monotonic()
|
||
count = 0
|
||
try:
|
||
while True:
|
||
bus.send(control_message)
|
||
count += 1
|
||
if count % 100 == 0:
|
||
print(
|
||
f"TX ID=0x{CONTROL_FRAME_ID:03X} "
|
||
f"DATA={control_data.hex(' ').upper()} count={count}"
|
||
)
|
||
next_send += period
|
||
now = time.monotonic()
|
||
if next_send > now:
|
||
time.sleep(next_send - now)
|
||
else:
|
||
next_send = now
|
||
finally:
|
||
stop(bus)
|
||
|
||
|
||
def stop(bus: can.BusABC) -> None:
|
||
"""连续两轮广播停止 ID 1、2(零输出 + 0x81)。"""
|
||
zero_data = bytes(8)
|
||
zero_message = can.Message(
|
||
arbitration_id=CONTROL_FRAME_ID,
|
||
is_extended_id=False,
|
||
data=zero_data,
|
||
)
|
||
data = bytes([STOP_COMMAND, 0, STOP_COMMAND, 0, 0, 0, 0, 0])
|
||
stop_message = can.Message(
|
||
arbitration_id=COMMAND_FRAME_ID,
|
||
is_extended_id=False,
|
||
data=data,
|
||
)
|
||
|
||
for attempt in range(2):
|
||
bus.send(zero_message)
|
||
print(f"TX ID=0x{CONTROL_FRAME_ID:03X} DATA={zero_data.hex(' ').upper()}")
|
||
bus.send(stop_message, timeout=0.1)
|
||
print(f"TX ID=0x{COMMAND_FRAME_ID:03X} DATA={data.hex(' ').upper()}")
|
||
if attempt == 0:
|
||
time.sleep(0.02)
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser()
|
||
commands = parser.add_subparsers(dest="command", required=True)
|
||
commands.add_parser("status", help="读取 ID 1、2 状态")
|
||
|
||
torque_parser = commands.add_parser("torque", help="发送 ID 1、2 控制量")
|
||
torque_parser.add_argument("motor1", type=int, help="ID 1 控制量")
|
||
torque_parser.add_argument("motor2", type=int, help="ID 2 控制量")
|
||
|
||
commands.add_parser("stop", help="停止 ID 1、2")
|
||
args = parser.parse_args()
|
||
|
||
bus = open_can()
|
||
print(bus)
|
||
try:
|
||
if args.command == "status":
|
||
read_status(bus)
|
||
elif args.command == "torque":
|
||
try:
|
||
torque_control(bus, args.motor1, args.motor2)
|
||
except KeyboardInterrupt:
|
||
print("已停止")
|
||
else:
|
||
stop(bus)
|
||
finally:
|
||
bus.shutdown()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|