2026-06-20 21:51:17 +08:00
2026-06-14 23:38:56 +08:00
2026-06-16 22:45:15 +08:00
2026-06-14 20:53:43 +08:00
2026-06-16 22:45:15 +08:00
2026-06-16 22:45:15 +08:00
2026-06-20 21:51:17 +08:00
2026-06-14 20:53:43 +08:00
2026-06-14 20:53:43 +08:00
2026-06-20 21:46:03 +08:00
2026-06-20 21:46:03 +08:00
2026-06-20 21:50:17 +08:00
2026-06-20 21:46:03 +08:00
2026-06-20 21:46:03 +08:00

Origincar Simulation

基于 MotrixSim 的阿克曼小车仿真,支持键盘控制和 HTTP API 局域网共享。

仿真截图

依赖

依赖 说明 安装
Python ≥3.13 运行环境 brew install pythonpython.org
uv 包管理器 curl -LsSf https://astral.sh/uv/install.sh | sh
motrixsim-core 物理仿真引擎 uv sync 自动安装
pillow 摄像头图像转 JPEG uv sync 自动安装
# 克隆后一键安装
uv sync

mxpython 是 motrixsim-core 自带的启动器,macOS 上必须使用主线程需保留给渲染Linux / Windows 可直接用 python

启动

# macOS
uv run mxpython main.py

# Linux / Windows
uv run python main.py

HTTP API端口 8765

仿真启动后,局域网内其他设备可通过 HTTP 获取摄像头图像、IMU 数据和发送控制指令。

GET /image

获取前置摄像头最新 JPEG 图像。浏览器可直接打开。

# 浏览器
http://<仿真机IP>:8765/image

# curl
curl http://1{IP}:8765/image -o frame.jpg

# Python
import requests
r = requests.get("http://{IP}:8765/image")
with open("frame.jpg", "wb") as f:
    f.write(r.content)

GET /state

获取小车当前状态JSON

curl http://{IP}:8765/state

返回示例:

{
  "x": -2.0, "y": -2.3, "z": 0.08, "yaw_deg": 0.0,
  "vx": 0.98, "vy": 0.0, "wx": 0.0, "wy": 0.0, "wz": 0.01,
  "speed_target": 1.0, "speed_actual": 0.98, "steer_target": 0.0,
  "speed_amp": 1.0, "steer_amp": 0.35
}

GET /scan

模拟 LSLIDAR N10 2D 激光雷达360°, 450 点)。

curl http://{IP}:8765/scan

返回示例:

{
  "angle_min": -3.14, "angle_max": 3.14, "angle_increment": 0.014,
  "range_min": 0.15, "range_max": 3.0,
  "ranges": [0.52, 0.51, 0.53, ...]
}

GET /odom

里程计数据(匹配 nav_msgs/Odometry)。

curl http://{IP}:8765/odom

返回示例:

{
  "x": -2.0, "y": -2.3, "yaw": 3.14,
  "vx": 0.98, "vy": 0.0, "wz": 0.01,
  "orientation": {"w": 0.0, "x": 0.0, "y": 0.0, "z": 1.0}
}

GET /power

电池电压(模拟 12V

curl http://{IP}:8765/power
# → {"voltage": 12.0}

GET /imu

获取 IMU 数据(含 MPU6050 噪声JSON。

curl http://{IP}:8765/imu

返回示例:

{
  "orientation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
  "angular_velocity": {"x": 0.001, "y": -0.003, "z": 0.008},
  "linear_acceleration": {"x": 0.02, "y": -0.01, "z": 9.81}
}

噪声参数MPU6050 级别):

  • 陀螺仪白噪声 σ = 0.01 rad/sbias 随机游走
  • 加速度计白噪声 σ = 0.05 m/s²bias 随机游走

POST /cmd

发送控制指令JSON

curl -X POST http://{IP}:8765/cmd \
  -H "Content-Type: application/json" \
  -d '{"speed": 1.0, "steer": 0.3}'

匹配串口协议11 字节帧),对接 cmd_vel/Twist

字段 类型 说明
vx float 线速度 m/s对应 X_speed
wz float 角速度 rad/s对应 Z_speed,固件内用 bicycle model 转阿克曼转向角)
curl -X POST http://{IP}:8765/cmd \
  -H "Content-Type: application/json" \
  -d '{"vx": 1.0, "wz": 0.5}'

Python 客户端示例

import time
import requests

IP = "192.168.1.100"

# 获取图像
r = requests.get(f"http://{IP}:8765/image")
with open("frame.jpg", "wb") as f:
    f.write(r.content)

# 获取状态
state = requests.get(f"http://{IP}:8765/state").json()
print(f"位置: ({state['x']}, {state['y']}), 速度: {state['speed_actual']} m/s")

# 发送控制指令串口协议格式vx + wz
requests.post(f"http://{IP}:8765/cmd", json={"vx": 1.0, "wz": 0.5})
time.sleep(2)
requests.post(f"http://{IP}:8765/cmd", json={"vx": 0.0, "wz": 0.0})

ROS2 桥接示例(仿真机 HTTP → ROS2 局域网):

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image, Imu, LaserScan
from nav_msgs.msg import Odometry
from geometry_msgs.msg import Twist, Quaternion
from std_msgs.msg import Float32
import requests, math, numpy as np, cv2

IP = "192.168.1.100"

class OrigincarBridge(Node):
    def __init__(self):
        super().__init__("origincar_bridge")
        self.pub_scan = self.create_publisher(LaserScan, "/scan", 10)
        self.pub_odom = self.create_publisher(Odometry, "/odom", 10)
        self.pub_imu  = self.create_publisher(Imu, "/imu/data_raw", 10)
        self.pub_pwr  = self.create_publisher(Float32, "/PowerVoltage", 10)
        self.sub = self.create_subscription(Twist, "/cmd_vel", self.cmd_cb, 10)
        self.timer = self.create_timer(0.033, self.sync)  # ~30Hz

    def sync(self):
        # LaserScan
        data = requests.get(f"http://{IP}:8765/scan", timeout=1).json()
        msg = LaserScan()
        msg.header.frame_id = "laser"; msg.header.stamp = self.get_clock().now().to_msg()
        msg.angle_min = data["angle_min"]; msg.angle_max = data["angle_max"]
        msg.angle_increment = data["angle_increment"]
        msg.range_min = data["range_min"]; msg.range_max = data["range_max"]
        msg.ranges = data["ranges"]
        self.pub_scan.publish(msg)

        # Odometry
        odom = requests.get(f"http://{IP}:8765/odom", timeout=1).json()
        o = Odometry()
        o.header.frame_id = "odom"; o.child_frame_id = "base_link"
        o.pose.pose.position.x = odom["x"]; o.pose.pose.position.y = odom["y"]
        o.pose.pose.orientation = Quaternion(**odom["orientation"])
        o.twist.twist.linear.x = odom["vx"]; o.twist.twist.angular.z = odom["wz"]
        self.pub_odom.publish(o)

        # IMU
        imu_d = requests.get(f"http://{IP}:8765/imu", timeout=1).json()
        imu = Imu()
        imu.header.frame_id = "gyro_link"
        imu.orientation = Quaternion(**imu_d["orientation"])
        imu.angular_velocity.x = imu_d["angular_velocity"]["x"]
        imu.angular_velocity.y = imu_d["angular_velocity"]["y"]
        imu.angular_velocity.z = imu_d["angular_velocity"]["z"]
        imu.linear_acceleration.x = imu_d["linear_acceleration"]["x"]
        imu.linear_acceleration.y = imu_d["linear_acceleration"]["y"]
        imu.linear_acceleration.z = imu_d["linear_acceleration"]["z"]
        self.pub_imu.publish(imu)

        # Power
        p = Float32(data=requests.get(f"http://{IP}:8765/power", timeout=1).json()["voltage"])
        self.pub_pwr.publish(p)

    def cmd_cb(self, msg: Twist):
        requests.post(f"http://{IP}:8765/cmd", json={
            "vx": msg.linear.x, "wz": msg.angular.z
        })

rclpy.init()
node = OrigincarBridge()
rclpy.spin(node)

键盘控制

按键 方式 作用
W/S 按住 前进/后退
A/D 按住 左转/右转
↑/↓ 点按 速度幅度 ±0.2 m/s0~2.0
←/→ 点按 转向幅度 ±0.05 rad0~0.5
Space 点按 截图保存到 captures/

键盘与 HTTP /cmd 可同时使用:按住 W/A/S/D 时键盘优先,松开后自动切回 HTTP 控制。

锥桶编辑器

可视化在地图上放置锥桶,保存到 cones.json,仿真启动时自动加载。

uv run python cone_editor.py
操作 说明
左键点击地图 放置锥桶
右键点击地图 删除最近锥桶
鼠标移动 查看世界坐标
保存按钮 写入 cones.json

地图为 5m×5m左下角 (-2.5, -2.5),右上角 (2.5, 2.5)。

cones.json 格式:

[
  {"x": -1.45, "y": -2.29},
  {"x": -0.06, "y": -1.56},
  {"x":  1.82, "y": -0.91}
]

也可直接编辑此文件增删锥桶。

场景结构

scene.xml      — 场景描述(地图 + 围栏 + 小车,锥桶动态注入)
main.py        — 仿真控制 + HTTP API 服务
cone_editor.py — 锥桶可视化编辑器
cones.json     — 锥桶位置配置
cone.obj       — 锥体网格底20cm→顶5cm高27cm
cone_white.obj — 白色反光条网格
origincar.urdf / origincar.xacro — 小车模型(参考)
Description
No description provided
Readme 4.3 MiB
Languages
Python 100%