From bb8a69e2d06473862f5294cca760d5348753e6db Mon Sep 17 00:00:00 2001 From: xtkuang <87661715@qq.com> Date: Fri, 7 Aug 2026 09:35:48 +0800 Subject: [PATCH] feat: expose motor API on all interfaces --- .env.example | 5 +++-- README.md | 26 +++++++++++++++++--------- script/launch.sh | 2 +- src/lingkong_motor_service/config.py | 6 ++---- tests/test_config.py | 7 ++++--- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index 5d191a3..dc89ee0 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/README.md b/README.md index 263c806..bd1cf7c 100644 --- a/README.md +++ b/README.md @@ -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 界面默认为 。真实硬件上只能运行一个服务 -进程,不要使用 `--reload` 或多个 worker。 +服务默认监听 `0.0.0.0:8000`,即全部 IPv4 网络接口。本机可打开 +,其他电脑应访问 +`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. 验证边界 diff --git a/script/launch.sh b/script/launch.sh index 6a14837..ef8404e 100755 --- a/script/launch.sh +++ b/script/launch.sh @@ -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 diff --git a/src/lingkong_motor_service/config.py b/src/lingkong_motor_service/config.py index a1681ba..7c4e89f 100644 --- a/src/lingkong_motor_service/config.py +++ b/src/lingkong_motor_service/config.py @@ -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") diff --git a/tests/test_config.py b/tests/test_config.py index 3e3a62d..9b8ba0a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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()