回答

收藏

[评测分享] 【AIMB-2210】AI远程获取小车状态

#板卡评测 #板卡评测 133 人阅读 | 0 人回复 | 2025-10-28

【前言】

如需要远程控制小车,必须实时获取到小车的状态。本篇将分享如何使用AIMB-2210强大的NPU来实现。
【前提】
【AIMB-2210】AI编写小车控制界面 - #板卡评测 - 与非网
在这一篇中,我详细的分享了如何制做远程控制的界面,这次的工程是在他的上面添加。
【硬件平台】
1、【AIMB-2210】主机
2、智能小车平台
【软件】
vscode+ai
【实现方法】
使用vscode打开文件,在AI对象中将数据解板协义发送给AI

然后提出按协议解析收到的数据包
在强大的【AIMB-2210】主机加持下,生成了代码如下:
  1. import serial
  2. import time
  3. import struct
  4. import tkinter as tk
  5. from tkinter import ttk, messagebox
  6. import serial.tools.list_ports
  7. import threading

  8. def calculate_checksum(data):
  9.     """
  10.     计算校验和:前面数据累加和的低8位
  11.     """
  12.     return sum(data) & 0xFF

  13. def parse_x_protocol_frame(frame):
  14.     """
  15.     解析X-Protocol帧
  16.    
  17.     Args:
  18.         frame: 完整的帧数据
  19.         
  20.     Returns:
  21.         dict: 解析出的数据字典
  22.     """
  23.     if len(frame) < 5:
  24.         return None
  25.         
  26.     # 检查帧头
  27.     if frame[0] != 0xAA or frame[1] != 0x55:
  28.         return None
  29.         
  30.     frame_len = frame[2]
  31.     if len(frame) < frame_len:
  32.         return None
  33.         
  34.     # 验证校验和
  35.     checksum = frame[frame_len - 1]
  36.     calculated_checksum = calculate_checksum(frame[:-1])
  37.     if checksum != calculated_checksum:
  38.         return None
  39.         
  40.     frame_code = frame[3]
  41.     data_bytes = frame[4:frame_len - 1]
  42.    
  43.     # 根据帧码解析数据
  44.     result = {"frame_code": frame_code}
  45.    
  46.     # 处理您实际使用的帧格式 (帧码 0x10)
  47.     if frame_code == 0x10:
  48.         # 数据部分有21字节,按照常见的传感器数据格式解析
  49.         # 前6字节是加速度数据 (3个16位整数)
  50.         try:
  51.             if len(data_bytes) >= 2:
  52.                 result["acc_x"] = struct.unpack('>h', data_bytes[0:2])[0]
  53.             if len(data_bytes) >= 4:
  54.                 result["acc_y"] = struct.unpack('>h', data_bytes[2:4])[0]
  55.             if len(data_bytes) >= 6:
  56.                 result["acc_z"] = struct.unpack('>h', data_bytes[4:6])[0]
  57.             
  58.             # 接下来6字节是陀螺仪数据 (3个16位整数)
  59.             if len(data_bytes) >= 8:
  60.                 result["gyro_x"] = struct.unpack('>h', data_bytes[6:8])[0]
  61.             if len(data_bytes) >= 10:
  62.                 result["gyro_y"] = struct.unpack('>h', data_bytes[8:10])[0]
  63.             if len(data_bytes) >= 12:
  64.                 result["gyro_z"] = struct.unpack('>h', data_bytes[10:12])[0]
  65.             
  66.             # 接下来4字节是速度数据 (2个16位整数)
  67.             if len(data_bytes) >= 14:
  68.                 result["vel_x"] = struct.unpack('>h', data_bytes[12:14])[0]
  69.             if len(data_bytes) >= 16:
  70.                 result["vel_y"] = struct.unpack('>h', data_bytes[14:16])[0]
  71.             
  72.             # 接下来2字节是角速度W
  73.             if len(data_bytes) >= 18:
  74.                 result["vel_w"] = struct.unpack('>h', data_bytes[16:18])[0]
  75.             
  76.             # 最后几个字节中取电压数据
  77.             if len(data_bytes) >= 20:
  78.                 result["voltage"] = struct.unpack('>h', data_bytes[18:20])[0]
  79.         except Exception as e:
  80.             print(f"数据解析错误: {e}")
  81.    
  82.     # 保留原有的解析逻辑以兼容其他帧格式
  83.     elif frame_code == 0x01:
  84.         if len(data_bytes) >= 6:
  85.             result["acc_x"] = struct.unpack('>h', data_bytes[0:2])[0]
  86.             result["acc_y"] = struct.unpack('>h', data_bytes[2:4])[0]
  87.             result["acc_z"] = struct.unpack('>h', data_bytes[4:6])[0]
  88.     elif frame_code == 0x02:
  89.         if len(data_bytes) >= 6:
  90.             result["gyro_x"] = struct.unpack('>h', data_bytes[0:2])[0]
  91.             result["gyro_y"] = struct.unpack('>h', data_bytes[2:4])[0]
  92.             result["gyro_z"] = struct.unpack('>h', data_bytes[4:6])[0]
  93.     elif frame_code == 0x03:
  94.         if len(data_bytes) >= 6:
  95.             result["vel_x"] = struct.unpack('>h', data_bytes[0:2])[0]
  96.             result["vel_y"] = struct.unpack('>h', data_bytes[2:4])[0]
  97.             result["vel_w"] = struct.unpack('>h', data_bytes[4:6])[0]
  98.     elif frame_code == 0x04:
  99.         if len(data_bytes) >= 2:
  100.             result["voltage"] = struct.unpack('>h', data_bytes[0:2])[0]
  101.             
  102.     return result

  103. def send_x_protocol_frame(ser, frame_code, data):
  104.     """
  105.     发送X-Protocol帧
  106.    
  107.     Args:
  108.         ser: 串口对象
  109.         frame_code: 帧码
  110.         data: 数据字节串
  111.     """
  112.     # 构造帧(不包括校验和)
  113.     frame = bytearray()
  114.     frame.append(0xAA)  # 帧头1
  115.     frame.append(0x55)  # 帧头2
  116.     frame.append(5 + len(data))  # 帧长
  117.     frame.append(frame_code)  # 帧码
  118.     frame.extend(data)  # 数据
  119.    
  120.     # 计算校验和(从第0位开始计算整个帧)
  121.     checksum = calculate_checksum(frame)
  122.     frame.append(checksum)  # 校验和
  123.    
  124.     # 发送帧
  125.     ser.write(frame)
  126.     print(f"发送帧: {' '.join(f'{b:02X}' for b in frame)}")
  127.     return len(frame)

  128. def send_values(ser, frame_code, value1, value2, value3):
  129.     """
  130.     发送三个16位整数
  131.    
  132.     Args:
  133.         ser: 串口对象
  134.         frame_code: 帧码
  135.         value1, value2, value3: 三个16位整数
  136.     """
  137.     # 构造6字节的有效数据
  138.     data = bytearray()
  139.     data.extend(struct.pack('>h', value1))  # 第一个16位整数
  140.     data.extend(struct.pack('>h', value2))  # 第二个16位整数
  141.     data.extend(struct.pack('>h', value3))  # 第三个16位整数
  142.    
  143.     # 发送帧
  144.     bytes_sent = send_x_protocol_frame(ser, frame_code, data)
  145.     print(f"发送数据: {value1}, {value2}, {value3}")
  146.     return bytes_sent

  147. class XProtocolGUI:
  148.     def __init__(self, root):
  149.         self.root = root
  150.         self.root.title("X-Protocol 串口发送工具")
  151.         self.root.geometry("400x500")
  152.         self.ser = None
  153.         self.receiving_thread = None
  154.         self.running = False
  155.         
  156.         self.create_widgets()
  157.         self.refresh_ports()
  158.         
  159.     def create_widgets(self):
  160.         # 主框架
  161.         main_frame = ttk.Frame(self.root, padding="10")
  162.         main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  163.         
  164.         # 串口选择
  165.         ttk.Label(main_frame, text="串口:").grid(row=0, column=0, sticky=tk.W, pady=5)
  166.         self.port_var = tk.StringVar()
  167.         self.port_combo = ttk.Combobox(main_frame, textvariable=self.port_var, width=15)
  168.         self.port_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=5)
  169.         
  170.         # 刷新按钮
  171.         refresh_btn = ttk.Button(main_frame, text="刷新", command=self.refresh_ports)
  172.         refresh_btn.grid(row=0, column=2, padx=(10,0), pady=5)
  173.         
  174.         # 连接按钮
  175.         self.connect_btn = ttk.Button(main_frame, text="连接", command=self.toggle_connection)
  176.         self.connect_btn.grid(row=0, column=3, padx=(10,0), pady=5)
  177.         
  178.         # 分隔线
  179.         separator = ttk.Separator(main_frame, orient='horizontal')
  180.         separator.grid(row=1, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)
  181.         
  182.         # 数据输入
  183.         ttk.Label(main_frame, text="数据1:").grid(row=2, column=0, sticky=tk.W, pady=5)
  184.         self.value1_var = tk.StringVar(value="100")
  185.         ttk.Entry(main_frame, textvariable=self.value1_var, width=10).grid(row=2, column=1, sticky=tk.W, pady=5)
  186.         
  187.         ttk.Label(main_frame, text="数据2:").grid(row=3, column=0, sticky=tk.W, pady=5)
  188.         self.value2_var = tk.StringVar(value="0")
  189.         ttk.Entry(main_frame, textvariable=self.value2_var, width=10).grid(row=3, column=1, sticky=tk.W, pady=5)
  190.         
  191.         ttk.Label(main_frame, text="数据3:").grid(row=4, column=0, sticky=tk.W, pady=5)
  192.         self.value3_var = tk.StringVar(value="0")
  193.         ttk.Entry(main_frame, textvariable=self.value3_var, width=10).grid(row=4, column=1, sticky=tk.W, pady=5)
  194.         
  195.         # 帧码输入
  196.         ttk.Label(main_frame, text="帧码:").grid(row=5, column=0, sticky=tk.W, pady=5)
  197.         self.frame_code_var = tk.StringVar(value="50")
  198.         ttk.Entry(main_frame, textvariable=self.frame_code_var, width=10).grid(row=5, column=1, sticky=tk.W, pady=5)
  199.         
  200.         # 发送按钮
  201.         self.send_btn = ttk.Button(main_frame, text="发送数据", command=self.send_data, state=tk.DISABLED)
  202.         self.send_btn.grid(row=6, column=0, columnspan=2, pady=20)
  203.         
  204.         # 显示区域
  205.         display_frame = ttk.LabelFrame(main_frame, text="接收到的数据", padding="10")
  206.         display_frame.grid(row=7, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=10)

  207.         # 创建多个标签用于显示数据
  208.         labels = [
  209.             ("帧码:", "frame_code"),
  210.             ("加速度X:", "acc_x"),
  211.             ("加速度Y:", "acc_y"),
  212.             ("加速度Z:", "acc_z"),
  213.             ("陀螺仪X:", "gyro_x"),
  214.             ("陀螺仪Y:", "gyro_y"),
  215.             ("陀螺仪Z:", "gyro_z"),
  216.             ("速度X:", "vel_x"),
  217.             ("速度Y:", "vel_y"),
  218.             ("速度W:", "vel_w"),
  219.             ("电池电压:", "voltage"),
  220.         ]
  221.         self.display_vars = {}
  222.         for i, (label_text, key) in enumerate(labels):
  223.             ttk.Label(display_frame, text=label_text).grid(row=i, column=0, sticky=tk.W)
  224.             var = tk.StringVar(value="---")
  225.             self.display_vars[key] = var
  226.             ttk.Label(display_frame, textvariable=var, width=15).grid(row=i, column=1, sticky=tk.W)
  227.         
  228.         # 状态栏
  229.         self.status_var = tk.StringVar(value="请选择串口并连接")
  230.         self.status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
  231.         self.status_bar.grid(row=8, column=0, columnspan=4, sticky=(tk.W, tk.E), pady=(10,0))
  232.         
  233.         # 配置网格权重
  234.         self.root.columnconfigure(0, weight=1)
  235.         self.root.rowconfigure(0, weight=1)
  236.         main_frame.columnconfigure(1, weight=1)
  237.         
  238.     def refresh_ports(self):
  239.         """刷新可用串口列表"""
  240.         ports = [port.device for port in serial.tools.list_ports.comports()]
  241.         self.port_combo['values'] = ports
  242.         if ports:
  243.             self.port_combo.set(ports[0])
  244.             
  245.     def toggle_connection(self):
  246.         """切换串口连接状态"""
  247.         if self.ser is None:
  248.             self.connect_serial()
  249.         else:
  250.             self.disconnect_serial()
  251.             
  252.     def connect_serial(self):
  253.         """连接串口"""
  254.         try:
  255.             port = self.port_var.get()
  256.             if not port:
  257.                 messagebox.showerror("错误", "请选择串口")
  258.                 return

  259.             self.ser = serial.Serial(port, 230400, timeout=1)
  260.             self.running = True
  261.             self.connect_btn.config(text="断开")
  262.             self.send_btn.config(state=tk.NORMAL)
  263.             self.status_var.set(f"已连接到 {port}")
  264.             print(f"串口 {port} 打开成功")

  265.             # 启动接收线程
  266.             self.start_receiving()

  267.         except Exception as e:
  268.             messagebox.showerror("串口错误", f"无法打开串口: {str(e)}")
  269.             self.ser = None
  270.             
  271.     def disconnect_serial(self):
  272.         """断开串口连接"""
  273.         self.running = False
  274.         if self.ser:
  275.             self.ser.close()
  276.             self.ser = None
  277.         self.connect_btn.config(text="连接")
  278.         self.send_btn.config(state=tk.DISABLED)
  279.         self.status_var.set("已断开连接")
  280.         
  281.     def start_receiving(self):
  282.         """启动接收线程"""
  283.         if self.ser is None:
  284.             return
  285.         self.receiving_thread = threading.Thread(target=self.receive_data, daemon=True)
  286.         self.receiving_thread.start()

  287.     def receive_data(self):
  288.         """接收并解析数据"""
  289.         buffer = bytearray()
  290.         while self.running and self.ser and self.ser.is_open:
  291.             try:
  292.                 # 逐字节读取
  293.                 byte = self.ser.read(1)
  294.                 if not byte:
  295.                     continue
  296.                 buffer.append(byte[0])
  297.                
  298.                 # 至少需要帧头3个字节才能判断帧长
  299.                 if len(buffer) >= 3:
  300.                     # 检查是否是帧头
  301.                     if buffer[0] == 0xAA and buffer[1] == 0x55:
  302.                         frame_len = buffer[2]  # 第三个字节是帧长度
  303.                         # 当缓冲区长度达到帧长度时尝试解析
  304.                         if len(buffer) >= frame_len and frame_len > 0:
  305.                             # 尝试解析
  306.                             parsed = parse_x_protocol_frame(buffer[:frame_len])
  307.                             if parsed:
  308.                                 # 使用after方法确保在主线程更新UI
  309.                                 self.root.after(0, self.update_display, parsed)
  310.                                 buffer = buffer[frame_len:]  # 移除已处理部分
  311.                             else:
  312.                                 buffer = buffer[1:]  # 移动窗口
  313.                     elif len(buffer) > 100:  # 如果缓冲区太长且没有找到帧头,则清空
  314.                         buffer = bytearray()
  315.             except Exception as e:
  316.                 print(f"接收异常: {e}")
  317.                 break
  318.     def update_display(self, data):
  319.         """更新显示区域"""
  320.         # print(f"更新显示: {data}")
  321.         for key, var in self.display_vars.items():
  322.             if key in data:
  323.                 # 对于帧码,显示为十六进制;其他数据显示为十进制
  324.                 if key == "frame_code":
  325.                     var.set(f"0x{data[key]:02X}")
  326.                 elif key == "voltage":
  327.                     # 电池电压需要除以100
  328.                     var.set(f"{data[key]/100:.3f}")
  329.                 elif key in ["vel_x", "vel_y", "vel_w"]:
  330.                     # 速度值需要除以1000
  331.                     var.set(f"{data[key]/1000:.3f}")
  332.                 else:
  333.                     var.set(f"{data[key]:.3f}")
  334.             else:
  335.                 # 如果某个字段不存在,保持原值或设置为默认值
  336.                 if var.get() == "---":
  337.                     pass  # 保持默认值

  338.     def send_data(self):
  339.         """发送数据"""
  340.         if not self.ser:
  341.             messagebox.showerror("错误", "请先连接串口")
  342.             return
  343.             
  344.         try:
  345.             # 获取输入值
  346.             value1 = int(self.value1_var.get())
  347.             value2 = int(self.value2_var.get())
  348.             value3 = int(self.value3_var.get())
  349.             frame_code = int(self.frame_code_var.get(), 16)  # 帧码以十六进制解析
  350.             
  351.             # 发送数据
  352.             send_values(self.ser, frame_code, value1, value2, value3)
  353.             self.status_var.set(f"已发送: {value1}, {value2}, {value3}")
  354.             
  355.         except ValueError as e:
  356.             messagebox.showerror("输入错误", "请输入有效的整数")
  357.         except Exception as e:
  358.             messagebox.showerror("发送错误", f"发送失败: {str(e)}")
  359.             
  360.     def on_closing(self):
  361.         """关闭程序时断开串口"""
  362.         self.running = False
  363.         if self.ser:
  364.             self.ser.close()
  365.         self.root.destroy()

  366. def main():
  367.     root = tk.Tk()
  368.     app = XProtocolGUI(root)
  369.     root.protocol("WM_DELETE_WINDOW", app.on_closing)
  370.     root.mainloop()

  371. if __name__ == "__main__":
  372.     main()
复制代码
【界面效果】
当我发送速度为100时,小车慢慢向前,同时反馈的速度也显示到界面了。

【总结】
【AIMB-2210】非常强大,配AI可以实现生成程序代码,是编程好帮手。下一步,将定位信采集,偿试使用自动运行。
分享到:
回复

使用道具 举报

您需要登录后才可以回帖 注册/登录

本版积分规则

关闭

站长推荐上一条 /3 下一条