feetech-cli 0.1.0__tar.gz → 0.1.2__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 (24) hide show
  1. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/PKG-INFO +66 -10
  2. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/README.md +65 -9
  3. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/__init__.py +1 -1
  4. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/cli.py +17 -4
  5. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/registers.py +61 -1
  6. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/tui.py +6 -1
  7. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/PKG-INFO +66 -10
  8. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/SOURCES.txt +1 -0
  9. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/pyproject.toml +1 -1
  10. feetech_cli-0.1.2/tests/test_registers.py +29 -0
  11. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/LICENSE +0 -0
  12. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/controller.py +0 -0
  13. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/protocol.py +0 -0
  14. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli/simulator.py +0 -0
  15. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/dependency_links.txt +0 -0
  16. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/entry_points.txt +0 -0
  17. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/requires.txt +0 -0
  18. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/feetech_cli.egg-info/top_level.txt +0 -0
  19. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/setup.cfg +0 -0
  20. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/tests/test_cli.py +0 -0
  21. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/tests/test_controller.py +0 -0
  22. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/tests/test_protocol.py +0 -0
  23. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/tests/test_terminal.py +0 -0
  24. {feetech_cli-0.1.0 → feetech_cli-0.1.2}/tests/test_tui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feetech-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Command line tool to inspect and configure FEETECH STS/SMS/SCS serial bus servos
5
5
  Author-email: Iori Yanokura <ab.ioryz@gmail.com>
6
6
  License-Expression: MIT
@@ -31,14 +31,17 @@ or watch a live status screen.
31
31
 
32
32
  ## Install
33
33
 
34
+ With [uv](https://docs.astral.sh/uv/), which installs the `feetech` command in its
35
+ own environment and on your path:
36
+
34
37
  ```console
35
- $ pip install feetech-cli
38
+ $ uv tool install feetech-cli
36
39
  ```
37
40
 
38
- or, to keep it in its own environment with the `feetech` command on your path:
41
+ or with pip:
39
42
 
40
43
  ```console
41
- $ uv tool install feetech-cli
44
+ $ pip install feetech-cli
42
45
  ```
43
46
 
44
47
  The package is also a Python library: `from feetech_cli import FeetechServoController`.
@@ -71,13 +74,64 @@ is running, so a simulation is never mistaken for a real bus.
71
74
 
72
75
  ## Changing a servo id
73
76
 
74
- The usual case — one servo on the bus, currently id 1, that should become id 5:
77
+ New STS3215 servos all leave the factory as **id 1 at 1 Mbps**. Every servo on
78
+ a bus needs its own id, so a robot with several servos starts with giving each
79
+ one a different id — **one servo at a time**.
75
80
 
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
+ ### Walkthrough: numbering a batch of new servos
82
+
83
+ 1. **Connect exactly one servo** to the USB adapter, and power the adapter
84
+ from its own supply. USB alone does not power a servo.
85
+
86
+ 2. Check that it answers, and at which id:
87
+
88
+ ```console
89
+ $ feetech scan
90
+ Found 1 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
91
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
92
+ ```
93
+
94
+ 3. Give it its new id. Numbering three servos 1, 2 and 3, the first one can
95
+ keep id 1; the second becomes 2:
96
+
97
+ ```console
98
+ $ feetech set-id 1 2
99
+ Change servo 1 to id 2? This writes the servo EEPROM. [y/N] y
100
+ Servo 1 is now id 2.
101
+ ```
102
+
103
+ The id is stored in EEPROM, so it survives a power cycle.
104
+
105
+ 4. **Label the servo** with its new id (a piece of masking tape is enough).
106
+ Once they are mixed up, the only way to tell them apart again is to connect
107
+ them one by one.
108
+
109
+ 5. Unplug it, connect the next new servo, and repeat from step 2 with the
110
+ next id (`feetech set-id 1 3` for the third).
111
+
112
+ 6. With every servo numbered, daisy-chain them all and check the whole set
113
+ answers:
114
+
115
+ ```console
116
+ $ feetech scan
117
+ Found 3 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
118
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
119
+ id 2 model 777 position 2048 (+0.0 deg) 12.1V 35C
120
+ id 3 model 777 position 2048 (+0.0 deg) 12.1V 35C
121
+ ```
122
+
123
+ **Why one at a time.** Two new servos on the same bus both answer to id 1.
124
+ Their replies collide, and a write addressed to id 1 reaches both of them:
125
+ `set-id 1 2` would renumber both servos to 2, and the check that follows
126
+ would find "a servo at id 2" and report success. `set-id` cannot tell one
127
+ servo at an id from two, so keeping a single unnumbered servo on the bus is
128
+ up to you.
129
+
130
+ If `scan` finds nothing, the servo may have been set to another bus speed
131
+ before. `feetech scan --all-baudrates` tries every speed. Servos with an id
132
+ above 16 need `--max-id 253`.
133
+
134
+ ### What set-id does
81
135
 
82
136
  `set-id` refuses an id that another servo on the bus already answers to,
83
137
  switches torque off first, unlocks the EEPROM, writes, locks it again, and
@@ -404,6 +458,8 @@ Sources:
404
458
  - AkariGroup feetech_setup (baud table): https://github.com/AkariGroup/feetech_setup/blob/main/set_baudrate.py
405
459
  - LeRobot control table and sign-magnitude encodings: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
406
460
  - FEETECH start tutorial: https://www.feetechrc.com/Data/feetechrc/upload/file/20201127/start%20%20tutorial201015.pdf
461
+ - Factory default id 1: https://www.waveshare.com/wiki/ST3215_Servo ; id 1 and 1 Mbps: the STS3215 register reference above
462
+ - LeRobot SO-101 assembly, numbering the motors one at a time: https://huggingface.co/docs/lerobot/main/en/assemble_so101
407
463
 
408
464
  [lerobot]: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
409
465
 
@@ -6,14 +6,17 @@ or watch a live status screen.
6
6
 
7
7
  ## Install
8
8
 
9
+ With [uv](https://docs.astral.sh/uv/), which installs the `feetech` command in its
10
+ own environment and on your path:
11
+
9
12
  ```console
10
- $ pip install feetech-cli
13
+ $ uv tool install feetech-cli
11
14
  ```
12
15
 
13
- or, to keep it in its own environment with the `feetech` command on your path:
16
+ or with pip:
14
17
 
15
18
  ```console
16
- $ uv tool install feetech-cli
19
+ $ pip install feetech-cli
17
20
  ```
18
21
 
19
22
  The package is also a Python library: `from feetech_cli import FeetechServoController`.
@@ -46,13 +49,64 @@ is running, so a simulation is never mistaken for a real bus.
46
49
 
47
50
  ## Changing a servo id
48
51
 
49
- The usual case — one servo on the bus, currently id 1, that should become id 5:
52
+ New STS3215 servos all leave the factory as **id 1 at 1 Mbps**. Every servo on
53
+ a bus needs its own id, so a robot with several servos starts with giving each
54
+ one a different id — **one servo at a time**.
50
55
 
51
- ```console
52
- $ feetech set-id 1 5
53
- Change servo 1 to id 5? This writes the servo EEPROM. [y/N] y
54
- Servo 1 is now id 5.
55
- ```
56
+ ### Walkthrough: numbering a batch of new servos
57
+
58
+ 1. **Connect exactly one servo** to the USB adapter, and power the adapter
59
+ from its own supply. USB alone does not power a servo.
60
+
61
+ 2. Check that it answers, and at which id:
62
+
63
+ ```console
64
+ $ feetech scan
65
+ Found 1 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
66
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
67
+ ```
68
+
69
+ 3. Give it its new id. Numbering three servos 1, 2 and 3, the first one can
70
+ keep id 1; the second becomes 2:
71
+
72
+ ```console
73
+ $ feetech set-id 1 2
74
+ Change servo 1 to id 2? This writes the servo EEPROM. [y/N] y
75
+ Servo 1 is now id 2.
76
+ ```
77
+
78
+ The id is stored in EEPROM, so it survives a power cycle.
79
+
80
+ 4. **Label the servo** with its new id (a piece of masking tape is enough).
81
+ Once they are mixed up, the only way to tell them apart again is to connect
82
+ them one by one.
83
+
84
+ 5. Unplug it, connect the next new servo, and repeat from step 2 with the
85
+ next id (`feetech set-id 1 3` for the third).
86
+
87
+ 6. With every servo numbered, daisy-chain them all and check the whole set
88
+ answers:
89
+
90
+ ```console
91
+ $ feetech scan
92
+ Found 3 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
93
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
94
+ id 2 model 777 position 2048 (+0.0 deg) 12.1V 35C
95
+ id 3 model 777 position 2048 (+0.0 deg) 12.1V 35C
96
+ ```
97
+
98
+ **Why one at a time.** Two new servos on the same bus both answer to id 1.
99
+ Their replies collide, and a write addressed to id 1 reaches both of them:
100
+ `set-id 1 2` would renumber both servos to 2, and the check that follows
101
+ would find "a servo at id 2" and report success. `set-id` cannot tell one
102
+ servo at an id from two, so keeping a single unnumbered servo on the bus is
103
+ up to you.
104
+
105
+ If `scan` finds nothing, the servo may have been set to another bus speed
106
+ before. `feetech scan --all-baudrates` tries every speed. Servos with an id
107
+ above 16 need `--max-id 253`.
108
+
109
+ ### What set-id does
56
110
 
57
111
  `set-id` refuses an id that another servo on the bus already answers to,
58
112
  switches torque off first, unlocks the EEPROM, writes, locks it again, and
@@ -379,6 +433,8 @@ Sources:
379
433
  - AkariGroup feetech_setup (baud table): https://github.com/AkariGroup/feetech_setup/blob/main/set_baudrate.py
380
434
  - LeRobot control table and sign-magnitude encodings: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
381
435
  - FEETECH start tutorial: https://www.feetechrc.com/Data/feetechrc/upload/file/20201127/start%20%20tutorial201015.pdf
436
+ - Factory default id 1: https://www.waveshare.com/wiki/ST3215_Servo ; id 1 and 1 Mbps: the STS3215 register reference above
437
+ - LeRobot SO-101 assembly, numbering the motors one at a time: https://huggingface.co/docs/lerobot/main/en/assemble_so101
382
438
 
383
439
  [lerobot]: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
384
440
 
@@ -7,7 +7,7 @@ from feetech_cli.protocol import FeetechStatusError
7
7
  from feetech_cli.protocol import FeetechTimeoutError
8
8
  from feetech_cli.protocol import PacketHandler
9
9
 
10
- __version__ = "0.1.0"
10
+ __version__ = "0.1.2"
11
11
 
12
12
  __all__ = [
13
13
  "FeetechServoController",
@@ -20,6 +20,7 @@ from feetech_cli.registers import INFO_REGISTERS
20
20
  from feetech_cli.registers import OPERATING_MODES
21
21
  from feetech_cli.registers import degree_to_position
22
22
  from feetech_cli.registers import format_baud
23
+ from feetech_cli.registers import format_current
23
24
  from feetech_cli.registers import position_to_degree
24
25
  from feetech_cli.tui import ServoTui
25
26
 
@@ -411,13 +412,16 @@ def command_info(args, controller):
411
412
  value = values[name]
412
413
  print(
413
414
  " {:<{width}} addr {:>3} {}".format(
414
- name, register.address, _annotate(name, value), width=width
415
+ name,
416
+ register.address,
417
+ _annotate(name, value, values),
418
+ width=width,
415
419
  )
416
420
  )
417
421
  return 0
418
422
 
419
423
 
420
- def _annotate(name, value):
424
+ def _annotate(name, value, context=None):
421
425
  """Render a register value with a unit or label where one applies.
422
426
 
423
427
  Parameters
@@ -426,6 +430,9 @@ def _annotate(name, value):
426
430
  Register name.
427
431
  value : int or None
428
432
  Register value.
433
+ context : dict or None, optional
434
+ Other register values of the same servo. ``present_current`` uses
435
+ ``model_number`` and ``present_load`` from it.
429
436
 
430
437
  Returns
431
438
  -------
@@ -450,7 +457,10 @@ def _annotate(name, value):
450
457
  if name == "present_load":
451
458
  return f"{value} ({value / 10.0:+.1f} %)"
452
459
  if name == "present_current":
453
- return f"{value} ({value * 6.5:.0f} mA)"
460
+ context = context or {}
461
+ return format_current(
462
+ value, context.get("model_number"), context.get("present_load")
463
+ )
454
464
  if name == "torque_enable":
455
465
  return "{} ({})".format(value, "on" if value else "off")
456
466
  if name == "lock":
@@ -532,7 +542,10 @@ def command_read(args, controller):
532
542
  Process exit status.
533
543
  """
534
544
  value = controller.read_register(args.id, args.register)
535
- print(f"{args.register} = {_annotate(args.register, value)}")
545
+ context = None
546
+ if args.register == "present_current":
547
+ context = controller.dump(args.id, ["model_number", "present_load"])
548
+ print(f"{args.register} = {_annotate(args.register, value, context)}")
536
549
  return 0
537
550
 
538
551
 
@@ -126,7 +126,9 @@ CONTROL_TABLE = {
126
126
  "async_write_flag": _sram(64, 1, writable=False),
127
127
  "status": _sram(65, 1, writable=False),
128
128
  "moving": _sram(66, 1, writable=False),
129
- "present_current": _sram(69, 2, writable=False),
129
+ # Bit 15 carries the sign, as in FEETECH's SMS_STS::ReadCurrent:
130
+ # https://github.com/adityakamath/SCServo_Linux/blob/main/src/SMS_STS.cpp
131
+ "present_current": _sram(69, 2, writable=False, sign_bit=15),
130
132
  }
131
133
 
132
134
  #: Register value 0-7 written to ``baud_rate`` and the bus speed it selects.
@@ -161,6 +163,26 @@ OPERATING_MODES = {
161
163
  3: "step",
162
164
  }
163
165
 
166
+ #: ``(milliamps per count, what is measured)`` of ``present_current``, by
167
+ #: ``model_number``. Only models whose scale is backed by a document are
168
+ #: listed; for any other model the count is shown without a conversion rather
169
+ #: than with a guessed one.
170
+ #:
171
+ #: - 777 (STS3215): 6.5 mA,
172
+ #: https://www.mantech.co.za/datasheets/products/sts3215-200620a-1.pdf
173
+ #: - 4618 (HLS series, seen on an HLS3960M): 6.5 mA of motor *phase* current,
174
+ #: not supply current. At a low PWM duty the phase current is far larger
175
+ #: than what the power supply delivers. Source is a third party transcript
176
+ #: of the HLS memory table, not a FEETECH document:
177
+ #: https://wiki.aifitlab.com/feetech-servo-motor-docs/feetech-hls-servo-memory-table-analysis
178
+ CURRENT_MA_PER_COUNT = {
179
+ 777: (6.5, "supply"),
180
+ 4618: (6.5, "phase"),
181
+ }
182
+
183
+ #: ``present_load`` count that means 100 % PWM duty (the unit is 0.1 %).
184
+ LOAD_FULL_SCALE = 1000
185
+
164
186
  #: Encoder counts per full turn for the STS/SMS series.
165
187
  RESOLUTION = 4096
166
188
 
@@ -265,3 +287,41 @@ def format_baud(baud):
265
287
  if baud >= 1000:
266
288
  return f"{baud / 1000:.0f}k"
267
289
  return str(baud)
290
+
291
+
292
+ def format_current(current, model, load=None):
293
+ """Render a ``present_current`` reading, in mA when the scale is known.
294
+
295
+ For a model that reports motor phase current, the current drawn from the
296
+ power supply is also estimated as ``|phase current| * |PWM duty|``, the
297
+ duty being ``present_load`` in 0.1 % steps. The estimate ignores driver
298
+ losses and the servo's own idle draw, so it is a rough figure only.
299
+
300
+ Parameters
301
+ ----------
302
+ current : int
303
+ Signed ``present_current`` count.
304
+ model : int or None
305
+ ``model_number`` of the servo, ``None`` if it could not be read.
306
+ load : int or None, optional
307
+ Signed ``present_load`` count, needed for the supply estimate.
308
+
309
+ Returns
310
+ -------
311
+ str
312
+ For example ``"-147 (-956 mA)"``, ``"-506 (-3289 mA phase, ~987 mA
313
+ supply est.)"`` for a model that reports motor phase current, or
314
+ ``"-147 (mA scale unknown for model 1234)"`` when
315
+ :data:`CURRENT_MA_PER_COUNT` has no entry.
316
+ """
317
+ entry = CURRENT_MA_PER_COUNT.get(model)
318
+ if entry is None:
319
+ return f"{current} (mA scale unknown for model {model})"
320
+ scale, kind = entry
321
+ milliamps = current * scale
322
+ if kind != "phase":
323
+ return f"{current} ({milliamps:.0f} mA)"
324
+ if load is None:
325
+ return f"{current} ({milliamps:.0f} mA phase)"
326
+ supply = abs(milliamps) * abs(load) / LOAD_FULL_SCALE
327
+ return f"{current} ({milliamps:.0f} mA phase, ~{supply:.0f} mA supply est.)"
@@ -16,6 +16,7 @@ from feetech_cli.registers import CENTER_POSITION
16
16
  from feetech_cli.registers import OPERATING_MODES
17
17
  from feetech_cli.registers import RESOLUTION
18
18
  from feetech_cli.registers import format_baud
19
+ from feetech_cli.registers import format_current
19
20
  from feetech_cli.registers import position_to_degree
20
21
 
21
22
  #: Encoder counts moved per arrow key press, about five degrees.
@@ -69,6 +70,8 @@ STATUS_REGISTERS = [
69
70
  "present_voltage",
70
71
  "present_temperature",
71
72
  "present_current",
73
+ # Only needed to pick the mA scale for present_current.
74
+ "model_number",
72
75
  "torque_enable",
73
76
  "operating_mode",
74
77
  "moving",
@@ -603,7 +606,9 @@ class ServoTui:
603
606
  current = values.get("present_current")
604
607
  if current is None:
605
608
  return "no data"
606
- return f"{current} ({current * 6.5:.0f} mA)"
609
+ return format_current(
610
+ current, values.get("model_number"), values.get("present_load")
611
+ )
607
612
  if row == "Torque":
608
613
  torque = values.get("torque_enable")
609
614
  if torque is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feetech-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Command line tool to inspect and configure FEETECH STS/SMS/SCS serial bus servos
5
5
  Author-email: Iori Yanokura <ab.ioryz@gmail.com>
6
6
  License-Expression: MIT
@@ -31,14 +31,17 @@ or watch a live status screen.
31
31
 
32
32
  ## Install
33
33
 
34
+ With [uv](https://docs.astral.sh/uv/), which installs the `feetech` command in its
35
+ own environment and on your path:
36
+
34
37
  ```console
35
- $ pip install feetech-cli
38
+ $ uv tool install feetech-cli
36
39
  ```
37
40
 
38
- or, to keep it in its own environment with the `feetech` command on your path:
41
+ or with pip:
39
42
 
40
43
  ```console
41
- $ uv tool install feetech-cli
44
+ $ pip install feetech-cli
42
45
  ```
43
46
 
44
47
  The package is also a Python library: `from feetech_cli import FeetechServoController`.
@@ -71,13 +74,64 @@ is running, so a simulation is never mistaken for a real bus.
71
74
 
72
75
  ## Changing a servo id
73
76
 
74
- The usual case — one servo on the bus, currently id 1, that should become id 5:
77
+ New STS3215 servos all leave the factory as **id 1 at 1 Mbps**. Every servo on
78
+ a bus needs its own id, so a robot with several servos starts with giving each
79
+ one a different id — **one servo at a time**.
75
80
 
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
+ ### Walkthrough: numbering a batch of new servos
82
+
83
+ 1. **Connect exactly one servo** to the USB adapter, and power the adapter
84
+ from its own supply. USB alone does not power a servo.
85
+
86
+ 2. Check that it answers, and at which id:
87
+
88
+ ```console
89
+ $ feetech scan
90
+ Found 1 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
91
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
92
+ ```
93
+
94
+ 3. Give it its new id. Numbering three servos 1, 2 and 3, the first one can
95
+ keep id 1; the second becomes 2:
96
+
97
+ ```console
98
+ $ feetech set-id 1 2
99
+ Change servo 1 to id 2? This writes the servo EEPROM. [y/N] y
100
+ Servo 1 is now id 2.
101
+ ```
102
+
103
+ The id is stored in EEPROM, so it survives a power cycle.
104
+
105
+ 4. **Label the servo** with its new id (a piece of masking tape is enough).
106
+ Once they are mixed up, the only way to tell them apart again is to connect
107
+ them one by one.
108
+
109
+ 5. Unplug it, connect the next new servo, and repeat from step 2 with the
110
+ next id (`feetech set-id 1 3` for the third).
111
+
112
+ 6. With every servo numbered, daisy-chain them all and check the whole set
113
+ answers:
114
+
115
+ ```console
116
+ $ feetech scan
117
+ Found 3 servo(s) on /dev/ttyUSB0 at 1.00Mbps:
118
+ id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
119
+ id 2 model 777 position 2048 (+0.0 deg) 12.1V 35C
120
+ id 3 model 777 position 2048 (+0.0 deg) 12.1V 35C
121
+ ```
122
+
123
+ **Why one at a time.** Two new servos on the same bus both answer to id 1.
124
+ Their replies collide, and a write addressed to id 1 reaches both of them:
125
+ `set-id 1 2` would renumber both servos to 2, and the check that follows
126
+ would find "a servo at id 2" and report success. `set-id` cannot tell one
127
+ servo at an id from two, so keeping a single unnumbered servo on the bus is
128
+ up to you.
129
+
130
+ If `scan` finds nothing, the servo may have been set to another bus speed
131
+ before. `feetech scan --all-baudrates` tries every speed. Servos with an id
132
+ above 16 need `--max-id 253`.
133
+
134
+ ### What set-id does
81
135
 
82
136
  `set-id` refuses an id that another servo on the bus already answers to,
83
137
  switches torque off first, unlocks the EEPROM, writes, locks it again, and
@@ -404,6 +458,8 @@ Sources:
404
458
  - AkariGroup feetech_setup (baud table): https://github.com/AkariGroup/feetech_setup/blob/main/set_baudrate.py
405
459
  - LeRobot control table and sign-magnitude encodings: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
406
460
  - FEETECH start tutorial: https://www.feetechrc.com/Data/feetechrc/upload/file/20201127/start%20%20tutorial201015.pdf
461
+ - Factory default id 1: https://www.waveshare.com/wiki/ST3215_Servo ; id 1 and 1 Mbps: the STS3215 register reference above
462
+ - LeRobot SO-101 assembly, numbering the motors one at a time: https://huggingface.co/docs/lerobot/main/en/assemble_so101
407
463
 
408
464
  [lerobot]: https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/feetech/tables.py
409
465
 
@@ -17,5 +17,6 @@ feetech_cli.egg-info/top_level.txt
17
17
  tests/test_cli.py
18
18
  tests/test_controller.py
19
19
  tests/test_protocol.py
20
+ tests/test_registers.py
20
21
  tests/test_terminal.py
21
22
  tests/test_tui.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "feetech-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Command line tool to inspect and configure FEETECH STS/SMS/SCS serial bus servos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -0,0 +1,29 @@
1
+ """Tests for register decoding and rendering."""
2
+
3
+ from feetech_cli.protocol import from_sign_magnitude
4
+ from feetech_cli.registers import CONTROL_TABLE
5
+ from feetech_cli.registers import format_current
6
+
7
+
8
+ def test_present_current_decodes_bit_15_as_sign():
9
+ # Raw 0x8164 read from a holding servo: magnitude 356, negative.
10
+ sign_bit = CONTROL_TABLE["present_current"].sign_bit
11
+ assert from_sign_magnitude(0x8164, sign_bit) == -356
12
+
13
+
14
+ def test_format_current_converts_known_model():
15
+ assert format_current(-147, 777) == "-147 (-956 mA)"
16
+
17
+
18
+ def test_format_current_labels_phase_current():
19
+ assert format_current(-147, 4618) == "-147 (-956 mA phase)"
20
+
21
+
22
+ def test_format_current_estimates_supply_from_pwm_duty():
23
+ # 3289 mA phase at 30 % duty draws roughly 987 mA from the supply.
24
+ expected = "-506 (-3289 mA phase, ~987 mA supply est.)"
25
+ assert format_current(-506, 4618, load=-300) == expected
26
+
27
+
28
+ def test_format_current_leaves_unknown_model_unconverted():
29
+ assert format_current(-147, 1234) == "-147 (mA scale unknown for model 1234)"
File without changes
File without changes