servomotor-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. servomotor_mcp-0.1.0/.github/workflows/publish.yml +42 -0
  2. servomotor_mcp-0.1.0/.gitignore +7 -0
  3. servomotor_mcp-0.1.0/LICENSE +21 -0
  4. servomotor_mcp-0.1.0/PKG-INFO +139 -0
  5. servomotor_mcp-0.1.0/README.md +108 -0
  6. servomotor_mcp-0.1.0/examples/claude_desktop_config.json +18 -0
  7. servomotor_mcp-0.1.0/examples/demo_prompts.md +35 -0
  8. servomotor_mcp-0.1.0/hardware_tests/README.md +73 -0
  9. servomotor_mcp-0.1.0/hardware_tests/RESULTS.txt +49 -0
  10. servomotor_mcp-0.1.0/hardware_tests/demo_live.py +78 -0
  11. servomotor_mcp-0.1.0/hardware_tests/discover_motor.py +69 -0
  12. servomotor_mcp-0.1.0/hardware_tests/hw_test_advanced.py +76 -0
  13. servomotor_mcp-0.1.0/hardware_tests/hw_test_basic.py +87 -0
  14. servomotor_mcp-0.1.0/hardware_tests/hw_test_endurance.py +63 -0
  15. servomotor_mcp-0.1.0/hardware_tests/hw_test_extended.py +91 -0
  16. servomotor_mcp-0.1.0/hardware_tests/hw_test_sequence.py +76 -0
  17. servomotor_mcp-0.1.0/hardware_tests/hw_test_serialbus.py +87 -0
  18. servomotor_mcp-0.1.0/mcpb/PACKAGING.md +37 -0
  19. servomotor_mcp-0.1.0/mcpb/manifest.json +67 -0
  20. servomotor_mcp-0.1.0/pyproject.toml +50 -0
  21. servomotor_mcp-0.1.0/server.json +45 -0
  22. servomotor_mcp-0.1.0/src/servomotor_mcp/__init__.py +20 -0
  23. servomotor_mcp-0.1.0/src/servomotor_mcp/__main__.py +4 -0
  24. servomotor_mcp-0.1.0/src/servomotor_mcp/motors.py +333 -0
  25. servomotor_mcp-0.1.0/src/servomotor_mcp/safety.py +113 -0
  26. servomotor_mcp-0.1.0/src/servomotor_mcp/sequencer.py +75 -0
  27. servomotor_mcp-0.1.0/src/servomotor_mcp/server.py +186 -0
  28. servomotor_mcp-0.1.0/tests/test_mock_bus.py +63 -0
  29. servomotor_mcp-0.1.0/tests/test_safety.py +66 -0
  30. servomotor_mcp-0.1.0/tests/test_sequencer.py +62 -0
  31. servomotor_mcp-0.1.0/uv.lock +1032 -0
@@ -0,0 +1,42 @@
1
+ name: Publish to PyPI
2
+
3
+ # Builds and publishes servomotor-mcp to PyPI on a version tag (e.g. v0.1.0),
4
+ # using PyPI Trusted Publishing (OIDC) — no API token needed.
5
+ #
6
+ # ONE-TIME SETUP (do before the first tag):
7
+ # On PyPI → the servomotor-mcp project → Settings → Publishing → add a "pending publisher":
8
+ # Owner: Gearotons
9
+ # Repository: servomotor-mcp
10
+ # Workflow: publish.yml
11
+ # Environment: pypi (matches the `environment:` below; optional but recommended)
12
+ # (If the existing `servomotor` project uses an API token instead, you can swap this for that
13
+ # method — see DEPLOY_INSTRUCTIONS.md. Trusted publishing is preferred.)
14
+ #
15
+ # RELEASE: git tag v0.1.0 && git push --tags (or run this workflow manually)
16
+
17
+ on:
18
+ push:
19
+ tags: ["v*"]
20
+ workflow_dispatch:
21
+
22
+ jobs:
23
+ build-and-publish:
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write # required for trusted publishing
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.11"
33
+ - name: Build sdist + wheel
34
+ run: |
35
+ python -m pip install --upgrade build
36
+ python -m build
37
+ - name: Verify the build
38
+ run: |
39
+ python -m pip install --upgrade twine
40
+ twine check dist/*
41
+ - name: Publish to PyPI (trusted publishing)
42
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gearotons
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: servomotor-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server to drive Gearotons M17 open-source servomotors from natural language (Claude / any MCP client) over RS-485.
5
+ Project-URL: Homepage, https://gearotons.com
6
+ Project-URL: Repository, https://github.com/Gearotons/servomotor-mcp
7
+ Project-URL: Issues, https://github.com/Gearotons/servomotor-mcp/issues
8
+ Project-URL: Documentation, https://github.com/Gearotons/servomotor-mcp#readme
9
+ Author: Gearotons
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,claude,mcp,nema17,open-source,robotics,rs485,servomotor
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Manufacturing
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: System :: Hardware
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: mcp>=1.2.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Provides-Extra: serial
29
+ Requires-Dist: servomotor; extra == 'serial'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # servomotor-mcp
33
+
34
+ **Drive open-source [Gearotons M17](https://gearotons.com) servomotors from natural language.**
35
+
36
+ An [MCP](https://modelcontextprotocol.io) server that exposes the M17 — a NEMA-17
37
+ integrated, closed-loop, RS-485 servomotor — as a small set of safe, high-level tools.
38
+ Connect it to Claude Desktop, Claude Code, or any MCP client and control real motors by
39
+ just *asking*:
40
+
41
+ > "Home everything, then draw a square with the X and Y axes."
42
+
43
+ It ships with a **mock backend**, so you can try the whole thing with **no hardware**.
44
+
45
+ > The first servomotor with an official MCP server. Open hardware, open firmware, open
46
+ > software — and now an open, AI-native control interface.
47
+
48
+ ---
49
+
50
+ ## Quickstart (no hardware, ~2 minutes)
51
+
52
+ ```bash
53
+ # Run the server directly with uv (recommended):
54
+ uvx --from servomotor-mcp servomotor-mcp
55
+
56
+ # or install it:
57
+ pip install servomotor-mcp
58
+ servomotor-mcp
59
+ ```
60
+
61
+ Then add it to **Claude Desktop** — copy the block from
62
+ [`examples/claude_desktop_config.json`](examples/claude_desktop_config.json) into your
63
+ `claude_desktop_config.json`, restart Claude Desktop, and ask:
64
+ *"What motors do you have connected?"* See [`examples/demo_prompts.md`](examples/demo_prompts.md)
65
+ for the full scripted demo.
66
+
67
+ ## Drive real motors
68
+
69
+ Plug an M17 (or a daisy-chain of them) into a USB↔RS-485 adapter and switch the backend:
70
+
71
+ ```bash
72
+ pip install 'servomotor-mcp[serial]' # pulls in the Gearotons servomotor library
73
+ GEAROTONS_MOTOR_BACKEND=serial \
74
+ GEAROTONS_SERIAL_PORT=/dev/ttyUSB0 \
75
+ servomotor-mcp
76
+ ```
77
+
78
+ ## Tools
79
+
80
+ | Tool | What it does |
81
+ |---|---|
82
+ | `list_motors` | Discover motors on the bus + positions (call first). |
83
+ | `move_to` | Move a motor to an absolute angle (closed-loop). |
84
+ | `move_relative` | Nudge a motor by a delta. |
85
+ | `trapezoid_move` | Smooth accel/decel move (best for arms/plotters). |
86
+ | `home` | Home one or all motors. |
87
+ | `get_status` | Position / moving / voltage / errors. |
88
+ | `stop` | Halt one or all motors. |
89
+ | `reset` | Recover a motor from a latched fault (firmware system reset, ~2 s). |
90
+ | `run_sequence` | Run a choreographed sequence ("draw a square"). |
91
+
92
+ ## Safety rails (in the server, not the model)
93
+
94
+ The LLM can hallucinate a tool call; these make that safe — they run on every request:
95
+
96
+ - **Alias allow-list** — only configured motors can be driven.
97
+ - **Position limits** — absolute/relative targets are clamped to a per-motor software
98
+ range, so a bad command can't drive an axis into a hard stop.
99
+ - **Speed clamp** — requested speeds are capped.
100
+
101
+ Clamps are *reported back* to the model (in `safety_notes`) rather than silently applied,
102
+ so it can see what actually happened and adjust. Configure via env:
103
+
104
+ ```bash
105
+ GEAROTONS_MOTOR_ALIASES="x,y,z"
106
+ GEAROTONS_LIMITS='{"x":{"min_deg":-180,"max_deg":180,"max_speed":300},"z":{"min_deg":0,"max_deg":90}}'
107
+ ```
108
+
109
+ ## How it works
110
+
111
+ ```
112
+ natural language → Claude → MCP tool calls → this server → RS-485 → M17 motors
113
+ ```
114
+
115
+ The server is a thin, safety-checked wrapper over the Gearotons `servomotor` Python
116
+ library (high-level, unit-aware commands — no DIR/STEP timing). The same intents run
117
+ against the mock backend for development and CI.
118
+
119
+ ## Develop / test
120
+
121
+ ```bash
122
+ pip install -e '.[dev]'
123
+ pytest # safety + mock-bus tests, no hardware needed
124
+ GEAROTONS_MOCK_SIM_SECONDS=0.4 servomotor-mcp # lifelike timing for demos
125
+ ```
126
+
127
+ ## Status
128
+
129
+ - ✅ MCP server, full tool surface, safety rails, mock backend — done; 16 unit tests pass.
130
+ - ✅ `SerialBus` real-hardware backend — **verified against a physical M17** (fw 0.15.0.0):
131
+ discovery, telemetry, closed-loop moves at **0.000° repeatability**, and a live safety
132
+ clamp (a 200° command on a ±90° motor stopped at 90°). See [`hardware_tests/`](hardware_tests/).
133
+ - 🔜 Live FastMCP-over-stdio run with Claude Desktop needs a Python ≥3.10 host (tool logic
134
+ already hardware-verified); plus the physical demo build + video. See [demo spec](../SPEC.md).
135
+
136
+ ## License
137
+
138
+ MIT. Hardware, firmware, and software for the M17 are open-source —
139
+ see [github.com/tomrodinger/servomotor](https://github.com/tomrodinger/servomotor).
@@ -0,0 +1,108 @@
1
+ # servomotor-mcp
2
+
3
+ **Drive open-source [Gearotons M17](https://gearotons.com) servomotors from natural language.**
4
+
5
+ An [MCP](https://modelcontextprotocol.io) server that exposes the M17 — a NEMA-17
6
+ integrated, closed-loop, RS-485 servomotor — as a small set of safe, high-level tools.
7
+ Connect it to Claude Desktop, Claude Code, or any MCP client and control real motors by
8
+ just *asking*:
9
+
10
+ > "Home everything, then draw a square with the X and Y axes."
11
+
12
+ It ships with a **mock backend**, so you can try the whole thing with **no hardware**.
13
+
14
+ > The first servomotor with an official MCP server. Open hardware, open firmware, open
15
+ > software — and now an open, AI-native control interface.
16
+
17
+ ---
18
+
19
+ ## Quickstart (no hardware, ~2 minutes)
20
+
21
+ ```bash
22
+ # Run the server directly with uv (recommended):
23
+ uvx --from servomotor-mcp servomotor-mcp
24
+
25
+ # or install it:
26
+ pip install servomotor-mcp
27
+ servomotor-mcp
28
+ ```
29
+
30
+ Then add it to **Claude Desktop** — copy the block from
31
+ [`examples/claude_desktop_config.json`](examples/claude_desktop_config.json) into your
32
+ `claude_desktop_config.json`, restart Claude Desktop, and ask:
33
+ *"What motors do you have connected?"* See [`examples/demo_prompts.md`](examples/demo_prompts.md)
34
+ for the full scripted demo.
35
+
36
+ ## Drive real motors
37
+
38
+ Plug an M17 (or a daisy-chain of them) into a USB↔RS-485 adapter and switch the backend:
39
+
40
+ ```bash
41
+ pip install 'servomotor-mcp[serial]' # pulls in the Gearotons servomotor library
42
+ GEAROTONS_MOTOR_BACKEND=serial \
43
+ GEAROTONS_SERIAL_PORT=/dev/ttyUSB0 \
44
+ servomotor-mcp
45
+ ```
46
+
47
+ ## Tools
48
+
49
+ | Tool | What it does |
50
+ |---|---|
51
+ | `list_motors` | Discover motors on the bus + positions (call first). |
52
+ | `move_to` | Move a motor to an absolute angle (closed-loop). |
53
+ | `move_relative` | Nudge a motor by a delta. |
54
+ | `trapezoid_move` | Smooth accel/decel move (best for arms/plotters). |
55
+ | `home` | Home one or all motors. |
56
+ | `get_status` | Position / moving / voltage / errors. |
57
+ | `stop` | Halt one or all motors. |
58
+ | `reset` | Recover a motor from a latched fault (firmware system reset, ~2 s). |
59
+ | `run_sequence` | Run a choreographed sequence ("draw a square"). |
60
+
61
+ ## Safety rails (in the server, not the model)
62
+
63
+ The LLM can hallucinate a tool call; these make that safe — they run on every request:
64
+
65
+ - **Alias allow-list** — only configured motors can be driven.
66
+ - **Position limits** — absolute/relative targets are clamped to a per-motor software
67
+ range, so a bad command can't drive an axis into a hard stop.
68
+ - **Speed clamp** — requested speeds are capped.
69
+
70
+ Clamps are *reported back* to the model (in `safety_notes`) rather than silently applied,
71
+ so it can see what actually happened and adjust. Configure via env:
72
+
73
+ ```bash
74
+ GEAROTONS_MOTOR_ALIASES="x,y,z"
75
+ GEAROTONS_LIMITS='{"x":{"min_deg":-180,"max_deg":180,"max_speed":300},"z":{"min_deg":0,"max_deg":90}}'
76
+ ```
77
+
78
+ ## How it works
79
+
80
+ ```
81
+ natural language → Claude → MCP tool calls → this server → RS-485 → M17 motors
82
+ ```
83
+
84
+ The server is a thin, safety-checked wrapper over the Gearotons `servomotor` Python
85
+ library (high-level, unit-aware commands — no DIR/STEP timing). The same intents run
86
+ against the mock backend for development and CI.
87
+
88
+ ## Develop / test
89
+
90
+ ```bash
91
+ pip install -e '.[dev]'
92
+ pytest # safety + mock-bus tests, no hardware needed
93
+ GEAROTONS_MOCK_SIM_SECONDS=0.4 servomotor-mcp # lifelike timing for demos
94
+ ```
95
+
96
+ ## Status
97
+
98
+ - ✅ MCP server, full tool surface, safety rails, mock backend — done; 16 unit tests pass.
99
+ - ✅ `SerialBus` real-hardware backend — **verified against a physical M17** (fw 0.15.0.0):
100
+ discovery, telemetry, closed-loop moves at **0.000° repeatability**, and a live safety
101
+ clamp (a 200° command on a ±90° motor stopped at 90°). See [`hardware_tests/`](hardware_tests/).
102
+ - 🔜 Live FastMCP-over-stdio run with Claude Desktop needs a Python ≥3.10 host (tool logic
103
+ already hardware-verified); plus the physical demo build + video. See [demo spec](../SPEC.md).
104
+
105
+ ## License
106
+
107
+ MIT. Hardware, firmware, and software for the M17 are open-source —
108
+ see [github.com/tomrodinger/servomotor](https://github.com/tomrodinger/servomotor).
@@ -0,0 +1,18 @@
1
+ {
2
+ "//": "Add this block to Claude Desktop's MCP config (claude_desktop_config.json).",
3
+ "//1": "macOS: ~/Library/Application Support/Claude/claude_desktop_config.json",
4
+ "//2": "Restart Claude Desktop after editing. Mock backend = no hardware needed.",
5
+ "mcpServers": {
6
+ "gearotons-motor": {
7
+ "command": "uvx",
8
+ "args": ["--from", "servomotor-mcp", "servomotor-mcp"],
9
+ "env": {
10
+ "GEAROTONS_MOTOR_BACKEND": "mock",
11
+ "GEAROTONS_MOTOR_ALIASES": "x,y,z",
12
+ "GEAROTONS_MOCK_SIM_SECONDS": "0.4",
13
+ "GEAROTONS_LIMITS": "{\"x\": {\"min_deg\": -180, \"max_deg\": 180, \"max_speed\": 300}, \"y\": {\"min_deg\": -180, \"max_deg\": 180}, \"z\": {\"min_deg\": 0, \"max_deg\": 90}}"
14
+ }
15
+ },
16
+ "//real-hardware": "To drive real motors, set GEAROTONS_MOTOR_BACKEND=serial and GEAROTONS_SERIAL_PORT=/dev/ttyUSB0, and install with the [serial] extra."
17
+ }
18
+ }
@@ -0,0 +1,35 @@
1
+ # Demo prompts — talking to the M17 motors
2
+
3
+ These are the natural-language prompts for the demo video / live demo, once the server is
4
+ connected to Claude Desktop (or any MCP client). With the mock backend they run with no
5
+ hardware; with the serial backend they drive real motors. The point: **no API knowledge,
6
+ no code — just plain English.**
7
+
8
+ ## Warm-up (proves discovery + control)
9
+ 1. "What motors do you have connected?"
10
+ → calls `list_motors`, names x / y / z and their positions.
11
+ 2. "Home everything."
12
+ → `home` (all).
13
+ 3. "Point the X axis to 90 degrees, smoothly over 2 seconds."
14
+ → `trapezoid_move(x, 90, 2)`.
15
+ 4. "Where is everything right now?"
16
+ → `get_status`.
17
+
18
+ ## The headline shot — "draw a square"
19
+ 5. "Draw a square: move X and Y to trace a 90-degree box, then come back to the start."
20
+ → Claude composes a `run_sequence` of `move_to`/`trapezoid_move` steps. This is the
21
+ clip that goes in the launch posts — one sentence, real motion.
22
+
23
+ ## Show the safety rails (great for the technical audience / HN comments)
24
+ 6. "Spin Z to 300 degrees."
25
+ → Z is limited to 0–90°; the move is **clamped to 90°** and Claude reports the clamp
26
+ from `safety_notes` ("I limited that to 90° because Z's configured range is 0–90").
27
+ Demonstrates that the guardrails live in the server, not the model's goodwill.
28
+
29
+ ## The "talk to your hardware" moment
30
+ 7. "Wave hello with the Z axis."
31
+ → Claude improvises a small back-and-forth `run_sequence`. Good, human closer.
32
+
33
+ > Recording notes: pen plotter reads best on camera (see SPEC.md). Keep the terminal /
34
+ > Claude transcript on screen next to the hardware so viewers see prompt → tool call →
35
+ > motion. ~60–90s total. End on the square.
@@ -0,0 +1,73 @@
1
+ # Hardware tests — Gearotons M17 MCP server
2
+
3
+ Scripts that validate the MCP server's motor backend against a **real M17** over RS-485.
4
+ All four passed on 2026-06-08 against a physical motor (fw 0.15.0.0) — see results below and
5
+ `../../../logs/hardware-2026-06-08.md`.
6
+
7
+ ## Setup
8
+
9
+ `pyserial` is the only runtime dep for these (already present on the test machine). Point
10
+ `PYTHONPATH` at the open-source `servomotor` library + the MCP package `src/`:
11
+
12
+ ```bash
13
+ export SERVOMOTOR=/Users/sandbox1/servomotor/python_programs # or: pip install servomotor
14
+ export PKG=../src
15
+ cd hardware_tests
16
+ ```
17
+
18
+ The motor was auto-discovered on `/dev/cu.usbserial-210`, device **alias 88**. Adjust the
19
+ port/alias args if yours differ.
20
+
21
+ ## The tests
22
+
23
+ | Script | What it proves | Needs |
24
+ |---|---|---|
25
+ | `discover_motor.py` | Finds which of the connected RS-485 adapters has a motor; reports unique ID + alias. | servomotor |
26
+ | `hw_test_basic.py` | Read-only telemetry (product/fw/status/voltage/temp/position) + one bounded ±18° move. | servomotor |
27
+ | `hw_test_serialbus.py` | The **MCP tool code path** (SafetyPolicy clamp → `SerialBus`), incl. a live safety clamp. | servomotor + `src/` |
28
+ | `hw_test_advanced.py` | `identify` LED blink + 12-target repeatability + a "wave" demo routine. | servomotor + `src/` |
29
+ | `hw_test_extended.py` | PID-error readout, bounded velocity move, throughput timing, enable/disable reliability. | servomotor + `src/` |
30
+ | `hw_test_sequence.py` | The `run_sequence` engine (the "draw"/"wave" path): a draw routine, a sequence-level clamp, and abort-on-disallowed-motor. | servomotor + `src/` |
31
+ | `hw_test_endurance.py` | N bounded moves with temperature/voltage/error sampling — reliability + thermal data. | servomotor + `src/` |
32
+
33
+ ```bash
34
+ PYTHONPATH=$SERVOMOTOR python3 discover_motor.py
35
+ PYTHONPATH=$SERVOMOTOR python3 hw_test_basic.py
36
+ PYTHONPATH=$PKG:$SERVOMOTOR python3 hw_test_serialbus.py
37
+ PYTHONPATH=$PKG:$SERVOMOTOR python3 hw_test_advanced.py
38
+ PYTHONPATH=$PKG:$SERVOMOTOR python3 hw_test_extended.py
39
+ PYTHONPATH=$PKG:$SERVOMOTOR python3 hw_test_sequence.py
40
+ ```
41
+
42
+ ## Verified results (2026-06-08, physical M17)
43
+
44
+ - **Discovery:** motor on `/dev/cu.usbserial-210`, unique_id `0x99856389A2B46555`, alias 88;
45
+ other three adapters empty.
46
+ - **Telemetry:** M17, fw **0.15.0.0**, status `[0,0]` (healthy), supply **20.0 V**, **35 °C**.
47
+ - **Bounded move:** +18° → read back 17.9999°, return → 0.000° net drift.
48
+ - **SerialBus + safety:** absolute moves exact; **live clamp** — commanded 200° on a ±90°-
49
+ limited motor → physically stopped at **90.00°** with the clamp note surfaced. ✅
50
+ - **Repeatability:** 12 random targets across ±80° → **0.000° mean & max error** (tol 0.5°).
51
+ - **Demo routine + LED identify:** clean.
52
+ - **Extended:** 20-move throughput 2.87 moves/s @ 0.000° worst error; 5/5 enable/disable
53
+ cycles clean. (Findings: `move_with_velocity` needs unit calibration before exposing a spin
54
+ tool; `get_max_pid_error` returned reset sentinels — read it after a move run for a real
55
+ number. Neither affects the 8 shipped MCP tools.)
56
+
57
+ Every test force-disables the MOSFETs and closes the port in a `finally` block.
58
+
59
+ ## Unit tests (no hardware)
60
+
61
+ The mock backend + safety rails + sequencer are covered by `../tests/` (21 tests):
62
+
63
+ ```bash
64
+ PYTHONPATH=../src python3 -m pytest -q ../tests # 21 passed
65
+ ```
66
+
67
+ ## Note on the live MCP-over-stdio test
68
+
69
+ Running the actual FastMCP server end-to-end (MCP client → server → motor) needs the `mcp`
70
+ SDK, which requires **Python ≥3.10**; the test machine has 3.9, so that step is deferred to
71
+ a 3.10+ host (e.g. the dev machine). It is low-risk: `hw_test_serialbus.py` already exercises
72
+ the identical tool logic (clamp → `SerialBus`) on hardware — only generic FastMCP transport
73
+ plumbing is left unverified.
@@ -0,0 +1,49 @@
1
+ # Gearotons M17 MCP server — consolidated test results
2
+ # Generated 2026-06-08 against the real motor on /dev/cu.usbserial-210 (alias 88)
3
+
4
+ ## Unit tests (mock + safety; no hardware, no mcp)
5
+ ................ [100%]
6
+ 16 passed in 0.02s
7
+
8
+ ## Hardware: basic telemetry + bounded move
9
+ [ERR] ping (echoes 10-byte payload): SystemExit: 1
10
+ [ok ] product_info: ['M17 ', 3, [0, 5, 1], 1330, 11062357502496892245, 0]
11
+ [ok ] firmware_version: [[0, 0, 15, 0], 0]
12
+ [ok ] product_description: 'Servomotor\x00'
13
+ [ok ] status: [0, 0]
14
+ [ok ] supply_voltage: 20200.0
15
+ [ok ] temperature: 36
16
+ [ok ] position (deg): -14.499975585937499
17
+ [ok ] comprehensive_position: [-14.499975585937499, -10.693981933593749, 0.0]
18
+ [ok ] position after +18 deg: 3.5000244140624996
19
+ [ok ] position after return: -14.499975585937499
20
+ net displacement: +0.000 deg (expect ~0)
21
+ MOTION TEST PASSED
22
+
23
+ ## Hardware: SerialBus + safety clamp (MCP tool path)
24
+ move_to(x, 45) -> pos=45.00° v=20.2V
25
+ move_to(x, 0) -> pos=0.00° v=20.2V
26
+ move_to(x, -30) -> pos=-30.00° v=20.2V
27
+ --- move_relative (tool path) ---
28
+ move_relative(+15) -> pos=-15.00°
29
+ --- LIVE SAFETY CLAMP: command 200° on a motor limited to 90° ---
30
+ move_to(x, 200) -> pos=90.00° v=20.2V | ['target 200° clamped to 90° (range -90..90)']
31
+ motor stopped at 90.00° (<=90 expected) -> CLAMP WORKED ✅
32
+ move_to(x, 30) -> pos=30.00° v=20.2V
33
+ move_to(x, -30) -> pos=-30.00° v=20.2V
34
+ move_to(x, 0) -> pos=0.00° v=20.2V
35
+ homed x -> 0.00°
36
+ SERIALBUS INTEGRATION TEST PASSED
37
+
38
+ ## Hardware: advanced (identify + repeatability + wave)
39
+ --- 1. identify (LED should blink on the motor) ---
40
+ identify() sent — check the motor LED
41
+ mean error=0.000° max error=0.000° tolerance=0.5°
42
+ ADVANCED TEST PASSED (max err 0.000° vs tol 0.5°)
43
+
44
+ ## Hardware: extended (PID err, velocity, throughput, reliability)
45
+ --- 1. enable + get_max_pid_error (closed-loop error, in degrees) ---
46
+ [ok ] max_pid_error [min,max]: [235929.5998901367, -235929.59999999998]
47
+ 20 moves in 7.0s = 2.87 moves/s; worst position error 0.000°
48
+ 5/5 enable+disable cycles clean
49
+ EXTENDED TEST PASSED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Watchable demo: drive the real M17 through the MCP server's tool code path.
3
+
4
+ Prints each plain-language intent + the tool it maps to, then moves the motor with a pause
5
+ so you can watch. This is exactly what an AI assistant does via the MCP server — here a
6
+ script plays the role of the AI so you can see the motor respond. Ends with the safety clamp.
7
+
8
+ Run:
9
+ PYTHONPATH=../src:/Users/sandbox1/servomotor/python_programs python3 demo_live.py
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import time
14
+
15
+ from servomotor_mcp.motors import SerialBus
16
+ from servomotor_mcp.safety import MotorLimits, SafetyPolicy
17
+
18
+ NAME = "x"
19
+ PAUSE = 1.2
20
+
21
+
22
+ def say(intent, tool):
23
+ print(f'\n 🗣 "{intent}"\n -> MCP tool: {tool}')
24
+
25
+
26
+ def main() -> int:
27
+ bus = SerialBus(port="/dev/cu.usbserial-210", motor_map={NAME: 88})
28
+ policy = SafetyPolicy(allowed=frozenset({NAME}),
29
+ limits={NAME: MotorLimits(min_deg=-90, max_deg=90, max_speed=300)})
30
+ m = bus._m[NAME]
31
+ try:
32
+ print("=" * 64)
33
+ print(" GEAROTONS M17 — live demo (WATCH THE MOTOR)")
34
+ print(" A script is standing in for the AI; it calls the same MCP tools.")
35
+ print("=" * 64)
36
+
37
+ say("Which motor are you? (blink so I can see you)", "identify")
38
+ m.identify(); print(" -> the motor's LED should be blinking now"); time.sleep(2.0)
39
+
40
+ say("Home — go to zero.", "home()")
41
+ bus.home(NAME); print(f" -> position: {bus.get_status(NAME)[0].position_deg:.1f}°"); time.sleep(PAUSE)
42
+
43
+ say("Point to 60 degrees.", "move_to(x, 60)")
44
+ s = bus.move_to(NAME, 60); print(f" -> position: {s.position_deg:.1f}°"); time.sleep(PAUSE)
45
+
46
+ say("Now go to minus 60 degrees.", "move_to(x, -60)")
47
+ s = bus.move_to(NAME, -60); print(f" -> position: {s.position_deg:.1f}°"); time.sleep(PAUSE)
48
+
49
+ say("Wave hello.", "run_sequence([...])")
50
+ for deg in (25, -25, 18, -18, 0):
51
+ bus.move_to(NAME, deg, speed=240)
52
+ print(f" -> back to {bus.get_status(NAME)[0].position_deg:.1f}°"); time.sleep(PAUSE)
53
+
54
+ print("\n" + "-" * 64)
55
+ print(" SAFETY DEMO — the limits live in the software, not the AI's goodwill.")
56
+ print(" This motor is configured with a max of 90°.")
57
+ say("Spin all the way to 300 degrees!", "move_to(x, 300)")
58
+ target, note = policy.clamp_absolute(NAME, 300)
59
+ s = bus.move_to(NAME, target)
60
+ print(f" -> {note}")
61
+ print(f" -> motor STOPPED at {s.position_deg:.1f}° (refused to exceed 90°)")
62
+ time.sleep(PAUSE)
63
+
64
+ say("Okay, home and rest.", "home() + stop()")
65
+ bus.home(NAME); bus.stop(NAME)
66
+ print(f" -> position: {bus.get_status(NAME)[0].position_deg:.1f}°, motor released")
67
+
68
+ print("\n" + "=" * 64)
69
+ print(" Demo done. That's the MCP toolset driving your real motor.")
70
+ print(" With the Python-3.10 setup, Claude calls these same tools from chat.")
71
+ print("=" * 64)
72
+ return 0
73
+ finally:
74
+ bus.close()
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main())
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """Probe every RS-485 serial port and report which one(s) have a Gearotons motor.
3
+
4
+ Tom connected ONE M17 via one of four USB-RS485 adapters; the other three are empty.
5
+ This finds it by running the library's device detection on each port and reporting the
6
+ unique ID + alias of whatever answers.
7
+
8
+ Run:
9
+ PYTHONPATH=/Users/sandbox1/servomotor/python_programs python3 discover_motor.py
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import glob
14
+ import sys
15
+
16
+ import servomotor
17
+ from servomotor import communication
18
+ from servomotor.device_detection import detect_devices_iteratively
19
+
20
+ DEFAULT_PORTS = sorted(glob.glob("/dev/cu.usbserial-*"))
21
+ N_DETECTIONS = 3 # consecutive successful detections the lib requires to trust a result
22
+
23
+
24
+ def probe(port: str) -> list:
25
+ """Return the list of Devices found on `port` (empty if none / no answer)."""
26
+ communication.serial_port = port
27
+ # alias 255 = broadcast/ALL; detection enumerates whoever is on the bus
28
+ servomotor.M3(alias_or_unique_id=255, verbose=0)
29
+ servomotor.open_serial_port()
30
+ try:
31
+ return detect_devices_iteratively(N_DETECTIONS, verbose=False) or []
32
+ finally:
33
+ try:
34
+ servomotor.close_serial_port()
35
+ except Exception:
36
+ pass
37
+
38
+
39
+ def main() -> int:
40
+ ports = sys.argv[1:] or DEFAULT_PORTS
41
+ print(f"Probing {len(ports)} port(s): {ports}\n")
42
+ found = {}
43
+ for port in ports:
44
+ try:
45
+ devices = probe(port)
46
+ except Exception as exc: # empty adapter -> timeout/serial error; keep going
47
+ print(f" {port:32s} -> no device ({type(exc).__name__}: {exc})")
48
+ continue
49
+ if devices:
50
+ for d in devices:
51
+ print(f" {port:32s} -> MOTOR unique_id=0x{d.unique_id:016X} alias={d.alias}")
52
+ found[port] = devices
53
+ else:
54
+ print(f" {port:32s} -> no device (no response)")
55
+
56
+ print()
57
+ if not found:
58
+ print("RESULT: no motor detected on any port.")
59
+ return 1
60
+ for port, devices in found.items():
61
+ print(f"RESULT: motor on {port} -> "
62
+ + ", ".join(f"uid=0x{d.unique_id:016X} alias={d.alias}" for d in devices))
63
+ # Emit the first found port on the last line for easy scripting/capture.
64
+ print(f"MOTOR_PORT={next(iter(found))}")
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())