feat: expose motor API on all interfaces

This commit is contained in:
xtkuang 2026-08-07 09:35:48 +08:00
parent 0dea53cabb
commit bb8a69e2d0
5 changed files with 27 additions and 19 deletions

View File

@ -19,7 +19,8 @@ MOTOR_CLOSE_CONTROL_RAW=400
# HTTP open/close 后持续广播直到 stop;服务正常退出会自动 Stop+Disable。
MOTOR_BROADCAST_FREQUENCY_HZ=10
MOTOR_HTTP_HOST=127.0.0.1
# 监听全部 IPv4 网络接口;客户端应使用本机真实局域网 IP,不能使用 0.0.0.0。
MOTOR_HTTP_HOST=0.0.0.0
MOTOR_HTTP_PORT=8000
# 绑定非本机地址时必须设置,请使用足够长的随机值。
# 留空表示不启用 API Key,任何能访问该端口的电脑都可调用接口。
MOTOR_API_TOKEN=

View File

@ -123,6 +123,9 @@ MOTOR_IDS=1,2
MOTOR_OPEN_CONTROL_RAW=400
MOTOR_CLOSE_CONTROL_RAW=400
MOTOR_BROADCAST_FREQUENCY_HZ=10
MOTOR_HTTP_HOST=0.0.0.0
MOTOR_HTTP_PORT=8000
MOTOR_API_TOKEN=
```
`MOTOR_OPEN_CONTROL_RAW` 和 `MOTOR_CLOSE_CONTROL_RAW` 是幅值;程序按电机 ID
@ -149,8 +152,12 @@ uv run --locked lingkong-motor-service
如果 `.env` 不存在,脚本会根据 `.env.example` 创建一份,完成硬件检查并设置
`MOTOR_HARDWARE_ENABLED=true` 后再次运行。
Swagger 界面默认为 <http://127.0.0.1:8000/docs>。真实硬件上只能运行一个服务
进程,不要使用 `--reload` 或多个 worker。
服务默认监听 `0.0.0.0:8000`,即全部 IPv4 网络接口。本机可打开
<http://127.0.0.1:8000/docs>,其他电脑应访问
`http://<服务器真实局域网IP>:8000/docs`;`0.0.0.0` 只是监听地址,不能作为客户端目标。
当前 `MOTOR_API_TOKEN` 留空,不启用 API Key,任何能访问 TCP 8000 端口的电脑都可调用
电机接口。请只在可信局域网中使用,并用主机或网络防火墙限制可访问范围。
真实硬件上只能运行一个服务进程,不要使用 `--reload` 或多个 worker。
## 5. 首次实机测试顺序
@ -216,12 +223,13 @@ uv run --locked python tests/test_can.py stop
完成上述低值方向验证后,正常操作顺序如下:
```bash
curl -X POST http://127.0.0.1:8000/v1/motor/open
SERVER_IP="<服务器真实局域网IP>"
curl -X POST "http://${SERVER_IP}:8000/v1/motor/open"
# 方向切换会自动执行零输出、Stop 和 20 ms 等待
curl -X POST http://127.0.0.1:8000/v1/motor/close
curl -X POST http://127.0.0.1:8000/v1/motor/open
curl -X POST http://127.0.0.1:8000/v1/motor/stop
curl http://127.0.0.1:8000/v1/motor/status
curl -X POST "http://${SERVER_IP}:8000/v1/motor/close"
curl -X POST "http://${SERVER_IP}:8000/v1/motor/open"
curl -X POST "http://${SERVER_IP}:8000/v1/motor/stop"
curl "http://${SERVER_IP}:8000/v1/motor/status"
```
`MOTOR_HARDWARE_ENABLED=true` 时,启动过程会自动 Enable。HTTP 只保留
@ -237,11 +245,11 @@ curl http://127.0.0.1:8000/v1/motor/status
`stop`。单次 `stop` 会自动重复两轮停止序列,不需要客户端再调用两次。
服务正常退出会自动执行两轮 Stop,再 Disable 并关闭 CAN 设备。
如果设置了 `MOTOR_API_TOKEN`,请求需携带:
当前配置不需要 API Key。如果以后显式设置了 `MOTOR_API_TOKEN`,请求才需携带:
```bash
curl -X POST -H "X-API-Key: your-token" \
http://127.0.0.1:8000/v1/motor/open
"http://${SERVER_IP}:8000/v1/motor/open"
```
## 7. 验证边界

View File

@ -56,6 +56,6 @@ export UV_PROJECT_ENVIRONMENT="${UV_PROJECT_ENVIRONMENT:-/tmp/lingkong-motor-ser
echo "Synchronizing the locked environment in ${UV_PROJECT_ENVIRONMENT}..."
uv sync --locked
echo "Starting Lingkong motor service on ${MOTOR_HTTP_HOST:-127.0.0.1}:${SERVICE_PORT}..."
echo "Starting Lingkong motor service on ${MOTOR_HTTP_HOST:-0.0.0.0}:${SERVICE_PORT}..."
echo "Motors auto-enable at startup; Ctrl-C performs Stop followed by Disable."
exec uv run --locked lingkong-motor-service

View File

@ -69,7 +69,7 @@ class Settings:
open_control_raw: int = 400
close_control_raw: int = 400
broadcast_frequency_hz: float = 10.0
http_host: str = "127.0.0.1"
http_host: str = "0.0.0.0"
http_port: int = 8000
api_token: str | None = None
@ -85,7 +85,7 @@ class Settings:
open_control_raw=_env_int("MOTOR_OPEN_CONTROL_RAW", 400),
close_control_raw=_env_int("MOTOR_CLOSE_CONTROL_RAW", 400),
broadcast_frequency_hz=_env_float("MOTOR_BROADCAST_FREQUENCY_HZ", 10.0),
http_host=os.getenv("MOTOR_HTTP_HOST", "127.0.0.1").strip(),
http_host=os.getenv("MOTOR_HTTP_HOST", "0.0.0.0").strip(),
http_port=_env_int("MOTOR_HTTP_PORT", 8000),
api_token=token,
)
@ -139,5 +139,3 @@ class Settings:
raise ConfigError("MOTOR_HTTP_HOST must not be empty")
if not 1 <= self.http_port <= 65_535:
raise ConfigError("MOTOR_HTTP_PORT must be in 1..65535")
if self.http_host not in {"127.0.0.1", "localhost", "::1"} and not self.api_token:
raise ConfigError("MOTOR_API_TOKEN is required when HTTP is exposed beyond localhost")

View File

@ -13,6 +13,8 @@ def test_defaults_match_connected_ms3008_installation() -> None:
assert settings.open_control_raw == 400
assert settings.close_control_raw == 400
assert settings.broadcast_frequency_hz == 10
assert settings.http_host == "0.0.0.0"
assert settings.api_token is None
@pytest.mark.parametrize("field", ["open_control_raw", "close_control_raw"])
@ -71,6 +73,5 @@ def test_broadcast_frequency_must_be_finite_and_bounded(frequency: float) -> Non
Settings(broadcast_frequency_hz=frequency).validate()
def test_non_loopback_http_requires_api_token() -> None:
with pytest.raises(ConfigError, match="MOTOR_API_TOKEN"):
Settings(http_host="0.0.0.0").validate()
def test_external_http_binding_does_not_require_api_token() -> None:
Settings(http_host="0.0.0.0", api_token=None).validate()