feetech-cli 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Iori Yanokura
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,412 @@
1
+ Metadata-Version: 2.4
2
+ Name: feetech-cli
3
+ Version: 0.1.0
4
+ Summary: Command line tool to inspect and configure FEETECH STS/SMS/SCS serial bus servos
5
+ Author-email: Iori Yanokura <ab.ioryz@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/iory/feetech-cli
8
+ Project-URL: Issues, https://github.com/iory/feetech-cli/issues
9
+ Keywords: feetech,sts3215,servo,robotics,serial
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: System :: Hardware
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pyserial>=3.5
22
+ Requires-Dist: colorama>=0.4.6
23
+ Requires-Dist: readchar>=4.0.5
24
+ Dynamic: license-file
25
+
26
+ # feetech-cli
27
+
28
+ Command line tool to inspect and configure FEETECH STS/SMS/SCS serial bus
29
+ servos (STS3215 and friends) — change servo ids, bus speeds, modes and limits,
30
+ or watch a live status screen.
31
+
32
+ ## Install
33
+
34
+ ```console
35
+ $ pip install feetech-cli
36
+ ```
37
+
38
+ or, to keep it in its own environment with the `feetech` command on your path:
39
+
40
+ ```console
41
+ $ uv tool install feetech-cli
42
+ ```
43
+
44
+ The package is also a Python library: `from feetech_cli import FeetechServoController`.
45
+
46
+ ## Quick start
47
+
48
+ Plug in a USB servo adapter, give the servo its own power supply, then:
49
+
50
+ ```console
51
+ $ feetech ports # which serial ports look like a servo bus
52
+ $ feetech scan # which servos answer (ids 1-16 by default)
53
+ $ feetech scan --all-baudrates # ...when you do not know the bus speed
54
+ $ feetech # interactive status screen
55
+ ```
56
+
57
+ The port and the bus speed are auto detected. Pass `--port` and `--baudrate`
58
+ to pin them.
59
+
60
+ No hardware handy? `--simulate` runs the whole interface against an in-process
61
+ servo bus (ids 1, 2 and 3), which is useful for trying the key bindings out:
62
+
63
+ ```console
64
+ $ feetech --simulate
65
+ ```
66
+
67
+ It only ever engages when you ask for it. A missing adapter or a silent bus
68
+ reports itself as such; it never quietly falls back to the simulator. The
69
+ screen header reads `--- SIMULATED SERVOS, NO HARDWARE ---` for as long as it
70
+ is running, so a simulation is never mistaken for a real bus.
71
+
72
+ ## Changing a servo id
73
+
74
+ The usual case — one servo on the bus, currently id 1, that should become id 5:
75
+
76
+ ```console
77
+ $ feetech set-id 1 5
78
+ Change servo 1 to id 5? This writes the servo EEPROM. [y/N] y
79
+ Servo 1 is now id 5.
80
+ ```
81
+
82
+ `set-id` refuses an id that another servo on the bus already answers to,
83
+ switches torque off first, unlocks the EEPROM, writes, locks it again, and
84
+ then pings the new id to confirm the change actually took. Add `-y` to skip
85
+ the prompt in scripts.
86
+
87
+ The same thing in the interactive screen: put the cursor on `Servo ID`, pick
88
+ the new id with the left/right arrows, press Enter. The chosen id is the one
89
+ shown in green.
90
+
91
+ | key | action |
92
+ |---|---|
93
+ | Up / Down | move between `Servo ID`, `Goal Position`, `Goal Velocity`, `Acceleration` |
94
+ | Left / Right | change **the selected row only** — see below |
95
+ | Enter | on `Servo ID`, write the highlighted id to the servo |
96
+ | `n` / `p` | switch to the next / previous servo on the bus |
97
+ | `r` | rescan the bus |
98
+ | `t` | toggle torque |
99
+ | `z` | move to 0 degrees |
100
+ | `0` | call the pose the servo is in **right now** 0 degrees (asks first, writes EEPROM) |
101
+ | `m` | cycle position / wheel / PWM / step mode |
102
+ | `q` | quit |
103
+
104
+ ```
105
+ --- FEETECH Servo Status ---
106
+ Bus: /dev/ttyUSB0 @ 1.00Mbps Servos: 1 4
107
+
108
+ >> Servo ID: 1 -> new id: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16]
109
+ Goal Position: 2048 (+0.0 deg)
110
+ Goal Velocity: 0
111
+ Acceleration: 0
112
+
113
+ Present Position: 2048 (+0.0 deg)
114
+ Voltage: 12.1 V
115
+ Temperature: 35 C
116
+ Torque: Disabled
117
+ Operating Mode: position (0)
118
+ Baud Rate: 1.00Mbps
119
+ EEPROM: locked
120
+ ```
121
+
122
+ The arrows do something different on every row, so the screen names the
123
+ selected row and what they will do to it:
124
+
125
+ ```
126
+ >> Servo ID: 1 -> new id: [1 2 3 4 5 ...]
127
+ Goal Position: 614 (-126.0 deg)
128
+ ...
129
+ Now on Servo ID: Left/Right pick the id, Enter writes it to the servo
130
+ ```
131
+
132
+ The cursor starts on `Servo ID`, where the arrows pick an id rather than move
133
+ the horn. Press Down to reach `Goal Position` before expecting the servo to
134
+ move.
135
+
136
+ ## Swapping servos while it runs
137
+
138
+ The interactive screen does not need restarting when you change servos. When
139
+ the bus goes quiet it switches to a waiting screen and keeps looking:
140
+
141
+ ```
142
+ --- FEETECH Servo Status ---
143
+
144
+ No servo is answering.
145
+
146
+ Plug a servo in and it will be picked up automatically.
147
+ Retrying once a second. Also worth checking:
148
+ - the USB adapter is still connected
149
+ - the servo has its own power supply, USB alone is not enough
150
+ - the servo id is between 1 and 16 (widen with --min-id / --max-id)
151
+
152
+ 'r' retry now, 'q' quit
153
+ ```
154
+
155
+ The id range is rescanned each time, so the replacement is found even when it
156
+ answers to a different id. Unplugging the USB adapter itself is handled too:
157
+ the port is closed and reopened rather than just rescanned.
158
+
159
+ It also starts this way. `feetech` with nothing on the bus opens the waiting
160
+ screen instead of exiting, so you can start the tool first and connect a servo
161
+ afterwards. The one shot commands (`scan`, `info`, `set-id`, ...) still fail
162
+ straight away, since they have nothing to wait for.
163
+
164
+ ## Torque
165
+
166
+ **A position command energises the servo by itself.** Measured on an STS3215:
167
+ with `torque_enable` explicitly set to 0, writing `goal_position` took it back
168
+ to 1 and the horn moved. Writes to `lock`, `homing_offset` and `acceleration`
169
+ did not do this, so it is specific to the position command.
170
+
171
+ So the arrow keys and `feetech move` always move the servo, even from a limp
172
+ standstill, and the joint stops being back drivable at that moment:
173
+
174
+ ```
175
+ >> Goal Position: 2216 (+14.8 deg) (arrows will energise the servo)
176
+ Present Position: 2216 (+14.8 deg)
177
+ Torque: Enabled
178
+ ```
179
+
180
+ To make a joint free to move by hand, switch torque **off** and leave it off:
181
+ press `t`, or run `feetech torque 1 off`. While it is off, `Present Position`
182
+ tracks the joint as you move it. Send any position command and it stiffens
183
+ again.
184
+
185
+ Pressing `t` to energise holds the position the horn is actually in, not the
186
+ goal last commanded. Otherwise the servo would snap to a stale goal the moment
187
+ it came on, which on an assembled robot can break something.
188
+
189
+ ## When a servo will not move
190
+
191
+ `selftest` is the check to run first. It verifies the settings that keep a
192
+ servo still, then commands a short series of moves with torque on and reads
193
+ back where the horn actually ended up, so "the tool is not sending anything"
194
+ is separated from "the servo is not acting on it":
195
+
196
+ ```console
197
+ $ feetech selftest 1
198
+ Settings that would stop it moving:
199
+ [ok ] operating mode is position: 0 (position)
200
+ [ok ] torque limit above zero: 1000
201
+ ...
202
+ Moves, starting from 996:
203
+ target before reached error secs result
204
+ 1046 996 1044 -2 0.44 moved and reached the target
205
+ ...
206
+ PASS: 5 of 5 commanded moves reached their target.
207
+ ```
208
+
209
+ Moves stay within `--span` counts of where the servo started (100 by default,
210
+ about 9 degrees) and the servo is put back where it was found, with torque
211
+ restored to whatever it was.
212
+
213
+ ## Zeroing a joint
214
+
215
+ Two different things, both available:
216
+
217
+ **Move the joint to 0 degrees** — the servo travels to encoder count 2048:
218
+
219
+ ```console
220
+ $ feetech move 1 --degree 0
221
+ ```
222
+
223
+ or press `z` in the interactive screen.
224
+
225
+ **Call the joint's current pose 0 degrees** — nothing moves, the servo just
226
+ starts reporting the pose it is already in as 0. This is how a joint is zeroed
227
+ after assembly:
228
+
229
+ ```console
230
+ $ feetech set-zero 1
231
+ Servo 1 currently reports 311 (-152.7 deg).
232
+ Call this pose 2048 (+0.0 deg)? This writes homing_offset to the servo EEPROM.
233
+ The servo will not move. [y/N] y
234
+ Servo 1 now reports 2048 (+0.0 deg) in the same pose.
235
+ homing_offset = -1652
236
+ ```
237
+
238
+ It writes the `homing_offset` register, where
239
+ `present_position = (raw_encoder - homing_offset) mod 4096` (verified on an
240
+ STS3215). Torque is left as it was found, and the goal is re-pointed at the
241
+ new reading so re-energising does not make the joint jump.
242
+
243
+ The wrap matters the second time a joint is zeroed. A servo that already
244
+ carries an offset reports a position that has crossed the 0/4095 boundary —
245
+ raw 628 with an offset of 904 reads 3820, not -276 — so the reading is
246
+ brought back into range before the new offset is worked out. Skipping that
247
+ asks the servo for an offset the register cannot hold and refuses a zeroing
248
+ that would have fitted.
249
+
250
+ In the interactive screen this is the `0` key. It asks before writing:
251
+
252
+ ```
253
+ Call this pose (2134, +7.6 deg) 0 degrees? This writes homing_offset to the
254
+ servo EEPROM. The servo will not move. Press 'y' to confirm, any other key
255
+ to cancel.
256
+ ```
257
+
258
+ To name the current pose something other than zero:
259
+
260
+ ```console
261
+ $ feetech set-zero 1 --degree 90
262
+ $ feetech set-zero 1 --position 1024
263
+ ```
264
+
265
+ The offset register holds ±2047 counts, so the pose has to be within about
266
+ 180 degrees of the target; `set-zero` refuses rather than writing a wrapped
267
+ value.
268
+
269
+ ## Other commands
270
+
271
+ ```console
272
+ $ feetech info 1 # dump the control table
273
+ $ feetech info 1 --all # ...every register, not just the useful ones
274
+ $ feetech set-baud 1 115200 # change the bus speed and reconnect
275
+ $ feetech move 1 --degree 90 # command a position
276
+ $ feetech move 1 --position 3072 # ...in encoder counts
277
+ $ feetech torque 1 off # let the horn move freely
278
+ $ feetech mode 1 wheel # continuous rotation
279
+ $ feetech read 1 present_load
280
+ $ feetech write 1 acceleration 20
281
+ $ feetech registers # names and addresses of the control table
282
+ $ feetech factory-reset 1
283
+ ```
284
+
285
+ ## More than one servo on the bus
286
+
287
+ Everything acts on one servo at a time. `scan` lists them all; every other
288
+ command works on the id it is given.
289
+
290
+ ```console
291
+ $ feetech scan
292
+ Found 3 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
293
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
294
+ id 2 model 777 position 1024 (-90.0 deg) 12.1V 36C
295
+ id 3 model 777 position 3072 (+90.0 deg) 12.0V 35C
296
+ ```
297
+
298
+ `info`, `set-zero` and `selftest` allow the id to be left out, but only when
299
+ exactly one servo answers. With several on the bus they name them and stop
300
+ rather than picking one:
301
+
302
+ ```console
303
+ $ feetech info
304
+ several servos answered (1, 2, 3), name the one to use
305
+ ```
306
+
307
+ The rest — `move`, `torque`, `mode`, `read`, `write`, `set-baud`,
308
+ `factory-reset` — always want the id. `set-id` additionally refuses to create
309
+ a collision:
310
+
311
+ ```console
312
+ $ feetech set-id 1 2
313
+ id 2 is already used by another servo on this bus
314
+ ```
315
+
316
+ There is no `--all` flag and no broadcast command. An EEPROM write that lands
317
+ on every joint of an assembled robot is not something a mistyped command
318
+ should be able to do. `FeetechServoController.sync_write_positions()` does
319
+ exist, and commands several servos in one packet, but it is a Python API
320
+ rather than a subcommand.
321
+
322
+ The interactive screen lists every servo found, with the selected one in
323
+ cyan:
324
+
325
+ ```
326
+ Bus: /dev/ttyUSB0 @ 1.00Mbps Servos: 1 2 3
327
+ ```
328
+
329
+ `n` and `p` move to the next and previous servo, `r` rescans. It opens on the
330
+ lowest id. Only the selected servo is read each frame, so the refresh rate
331
+ does not drop as servos are added to the bus, and every key — the arrows,
332
+ `t`, `z`, `m`, `0` — applies to that servo alone. A rescan keeps you on the
333
+ servo you were watching when it is still there, and falls back to the lowest
334
+ id when it is not.
335
+
336
+ ## Servo id range
337
+
338
+ Ids 1-16 are scanned by default, which covers a typical arm or hand. FEETECH
339
+ servos accept ids up to 253; widen the range when you need to:
340
+
341
+ ```console
342
+ $ feetech --max-id 253 scan
343
+ ```
344
+
345
+ `--min-id` and `--max-id` also set the ids offered by the interactive screen.
346
+
347
+ ## SCS series
348
+
349
+ The older SCS servos store 16 bit registers big endian. Pass `--scs` to talk
350
+ to them.
351
+
352
+ ## Safety notes
353
+
354
+ - Writing the EEPROM (`set-id`, `set-baud`, `mode`, `factory-reset`) asks for
355
+ confirmation unless `-y` is given, and refuses to run unattended without it.
356
+ - `set-id` and `set-baud` verify the change by re-addressing the servo
357
+ afterwards; they raise instead of reporting success if the servo does not
358
+ answer at its new identity.
359
+ - If a servo goes missing after a speed change, find it again with
360
+ `feetech scan --all-baudrates`.
361
+
362
+ ## Development
363
+
364
+ ```console
365
+ $ uv sync --all-groups
366
+ $ uv run pytest
367
+ $ uv run ruff check .
368
+ ```
369
+
370
+ The test suite runs against the simulated servo bus in
371
+ `feetech_cli/simulator.py`, so it needs no hardware. It covers the packet
372
+ layer, the EEPROM lock dance, id and baud rate changes, and the CLI.
373
+
374
+ `tests/test_terminal.py` additionally drives the interactive screen through a
375
+ real pty, sending real arrow key and Enter sequences, which is the only way to
376
+ cover `readchar` and the escape handling for real.
377
+
378
+ ## Protocol reference
379
+
380
+ FEETECH servos speak a Dynamixel-1.0 style protocol:
381
+
382
+ ```
383
+ instruction : 0xFF 0xFF ID LENGTH INSTRUCTION PARAM... CHECKSUM
384
+ status : 0xFF 0xFF ID LENGTH ERROR PARAM... CHECKSUM
385
+ ```
386
+
387
+ `LENGTH` is `len(params) + 2` and `CHECKSUM` is
388
+ `~(ID + LENGTH + INSTRUCTION + sum(params)) & 0xFF`.
389
+
390
+ Baud rate register values:
391
+
392
+ | value | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
393
+ |---|---|---|---|---|---|---|---|---|
394
+ | bps | 1000000 | 500000 | 250000 | 128000 | 115200 | 76800 | 57600 | 38400 |
395
+
396
+ Note that [LeRobot's table][lerobot] lists values 5-7 as 57600/38400/19200
397
+ instead. The table above is the one used here, and matches three independent
398
+ implementations.
399
+
400
+ Sources:
401
+
402
+ - STS3215 register reference: https://github.com/commanderfun/STS3215/blob/main/REGISTER_REFERENCE.md
403
+ - Hello Robot Stretch driver (baud table): https://github.com/hello-robot/stretch4_body/blob/master/stretch4_body/core/feetech/feetech_SM_servo.py
404
+ - AkariGroup feetech_setup (baud table): https://github.com/AkariGroup/feetech_setup/blob/main/set_baudrate.py
405
+ - LeRobot control table and sign-magnitude encodings: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
406
+ - FEETECH start tutorial: https://www.feetechrc.com/Data/feetechrc/upload/file/20201127/start%20%20tutorial201015.pdf
407
+
408
+ [lerobot]: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
409
+
410
+ ## License
411
+
412
+ MIT