feetech-cli 0.1.2__tar.gz → 0.1.3__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.
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/PKG-INFO +88 -1
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/README.md +87 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/__init__.py +1 -1
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/cli.py +269 -20
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/controller.py +251 -9
- feetech_cli-0.1.3/feetech_cli/devices.py +590 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/protocol.py +38 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/registers.py +8 -1
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/simulator.py +110 -3
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli/tui.py +16 -2
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/PKG-INFO +88 -1
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/SOURCES.txt +2 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/pyproject.toml +1 -1
- feetech_cli-0.1.3/tests/test_devices.py +429 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/LICENSE +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/dependency_links.txt +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/entry_points.txt +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/requires.txt +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/feetech_cli.egg-info/top_level.txt +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/setup.cfg +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/tests/test_cli.py +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/tests/test_controller.py +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/tests/test_protocol.py +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/tests/test_registers.py +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/tests/test_terminal.py +0 -0
- {feetech_cli-0.1.2 → feetech_cli-0.1.3}/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.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -466,3 +466,90 @@ Sources:
|
|
|
466
466
|
## License
|
|
467
467
|
|
|
468
468
|
MIT
|
|
469
|
+
|
|
470
|
+
## Other devices on the bus
|
|
471
|
+
|
|
472
|
+
A FEETECH bus can carry more than servos: any board that speaks the same
|
|
473
|
+
framing can sit next to the joints it works with. Its control table is its
|
|
474
|
+
own, though, and reading it through the servo table gives nonsense (an id of
|
|
475
|
+
48, a 250 kbps speed) while writing through it corrupts the board.
|
|
476
|
+
|
|
477
|
+
feetech-cli itself only knows FEETECH servos. Any other device is described by
|
|
478
|
+
a JSON **device file**, normally shipped by whoever makes the device, and
|
|
479
|
+
loaded from
|
|
480
|
+
|
|
481
|
+
1. `--device-file PATH` (a file or a directory of them, repeatable),
|
|
482
|
+
2. `$FEETECH_CLI_DEVICES` (files or directories, separated like `PATH`),
|
|
483
|
+
3. `~/.config/feetech-cli/devices/*.json` (or `$XDG_CONFIG_HOME/...`).
|
|
484
|
+
|
|
485
|
+
With a device file loaded, every id is identified once by reading the bytes its
|
|
486
|
+
`match` names; a match gets the file's control table, everything else is a
|
|
487
|
+
servo as before. With none loaded nothing extra is sent on the bus.
|
|
488
|
+
|
|
489
|
+
Two boards that share a model number -- the same firmware on two board
|
|
490
|
+
revisions, say -- are told apart by listing more than one place to look:
|
|
491
|
+
|
|
492
|
+
```json
|
|
493
|
+
"match": [{"address": 0, "bytes": "3412455842"}, {"address": 40, "bytes": "03"}]
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"name": "Example sensor board",
|
|
499
|
+
"match": {"address": 0, "bytes": "3412455842"},
|
|
500
|
+
"settle_time": 1.0,
|
|
501
|
+
"registers": {
|
|
502
|
+
"model_number": {"address": 0, "size": 2},
|
|
503
|
+
"id": {"address": 7, "size": 1, "writable": true},
|
|
504
|
+
"baud_rate": {"address": 8, "size": 1, "writable": true},
|
|
505
|
+
"mode": {"address": 20, "size": 1, "writable": true,
|
|
506
|
+
"values": {"0": "off", "1": "on"}},
|
|
507
|
+
"pressure": {"address": 24, "size": 2, "signed": true,
|
|
508
|
+
"scale": 0.01, "unit": "kPa", "digits": 2},
|
|
509
|
+
"acc_range_g": {"address": 12, "size": 2},
|
|
510
|
+
"acc_z": {"address": 26, "size": 2, "signed": true, "unit": "g",
|
|
511
|
+
"scale_by": {"register": "acc_range_g", "divisor": 32768}},
|
|
512
|
+
"error": {"address": 30, "size": 1, "bits": {"0": "imu", "3": "adc"}}
|
|
513
|
+
},
|
|
514
|
+
"info": ["id", "mode", "pressure", "acc_z", "error"],
|
|
515
|
+
"summary": ["mode", "pressure"]
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
| key | meaning |
|
|
520
|
+
|---|---|
|
|
521
|
+
| `name` | what the device is called in messages |
|
|
522
|
+
| `match` | `address` and hex `bytes` found there on this device and no other; or a list of such objects, all of which must match (checked in order, so a servo is ruled out by the first READ) |
|
|
523
|
+
| `registers` | name to `address`, `size` (1 or 2), `writable`, `signed` (two's complement), and optionally how to show it: `values` (number to label), `bits` (bit number to label), `scale` or `scale_by` (value × another register ÷ `divisor`), `unit`, `digits`; a writable register may add `settle_time`, the seconds the device stays silent after a write to it (while it saves the value to flash, say), and the write waits for it to answer before reading back |
|
|
524
|
+
| `settle_time` | seconds the device may stay silent after an id or speed change (default 1) |
|
|
525
|
+
| `baudrate_table` | `"sts"` (default) or an object of `baud_rate` value to bits per second |
|
|
526
|
+
| `eeprom_lock` | whether EEPROM writes need the servo-style `lock` register (default false) |
|
|
527
|
+
| `info`, `summary` | registers `feetech info` and the `scan` line show |
|
|
528
|
+
| `description` | free text |
|
|
529
|
+
|
|
530
|
+
A file with a misspelt key, an unknown register in a list, or bytes that are
|
|
531
|
+
not hex is rejected with the file and key named. A broken file is never
|
|
532
|
+
skipped: that would identify the device as a servo and write servo registers
|
|
533
|
+
into it.
|
|
534
|
+
|
|
535
|
+
```console
|
|
536
|
+
$ feetech --device-file my_board.json --max-id 20 scan
|
|
537
|
+
Found 2 device(s) on /dev/ttyUSB0 at 1.00Mbps:
|
|
538
|
+
id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
|
|
539
|
+
id 10 Example sensor board (model 0x1234, fw 2) mode 0 (off) pressure -4210 (-42.10 kPa)
|
|
540
|
+
$ feetech --device-file my_board.json info 10
|
|
541
|
+
$ feetech --device-file my_board.json registers --device my_board
|
|
542
|
+
$ feetech --device-file my_board.json --simulate scan # try a file without hardware
|
|
543
|
+
$ feetech --device-file my_board.json watch 10 acc_z pressure # live, Ctrl-C to stop
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`watch ID [REGISTER ...]` redraws the named registers (default: what `info`
|
|
547
|
+
shows) every `--interval` seconds, reading neighbouring registers in one READ
|
|
548
|
+
so six IMU axes cost one round trip. Off a terminal it prints one line per
|
|
549
|
+
refresh, for logging; `--count` stops after that many. It works for servos too.
|
|
550
|
+
|
|
551
|
+
`set-id` and `set-baud` write the device's own `id` and `baud_rate` registers
|
|
552
|
+
directly: no torque, no EEPROM lock, then poll for up to `settle_time` while it
|
|
553
|
+
commits them. Servo-only commands (`move`, `torque`, `mode`, `set-zero`,
|
|
554
|
+
`selftest`, `factory-reset`) refuse such a device, and the status screen leaves
|
|
555
|
+
it out.
|
|
@@ -441,3 +441,90 @@ Sources:
|
|
|
441
441
|
## License
|
|
442
442
|
|
|
443
443
|
MIT
|
|
444
|
+
|
|
445
|
+
## Other devices on the bus
|
|
446
|
+
|
|
447
|
+
A FEETECH bus can carry more than servos: any board that speaks the same
|
|
448
|
+
framing can sit next to the joints it works with. Its control table is its
|
|
449
|
+
own, though, and reading it through the servo table gives nonsense (an id of
|
|
450
|
+
48, a 250 kbps speed) while writing through it corrupts the board.
|
|
451
|
+
|
|
452
|
+
feetech-cli itself only knows FEETECH servos. Any other device is described by
|
|
453
|
+
a JSON **device file**, normally shipped by whoever makes the device, and
|
|
454
|
+
loaded from
|
|
455
|
+
|
|
456
|
+
1. `--device-file PATH` (a file or a directory of them, repeatable),
|
|
457
|
+
2. `$FEETECH_CLI_DEVICES` (files or directories, separated like `PATH`),
|
|
458
|
+
3. `~/.config/feetech-cli/devices/*.json` (or `$XDG_CONFIG_HOME/...`).
|
|
459
|
+
|
|
460
|
+
With a device file loaded, every id is identified once by reading the bytes its
|
|
461
|
+
`match` names; a match gets the file's control table, everything else is a
|
|
462
|
+
servo as before. With none loaded nothing extra is sent on the bus.
|
|
463
|
+
|
|
464
|
+
Two boards that share a model number -- the same firmware on two board
|
|
465
|
+
revisions, say -- are told apart by listing more than one place to look:
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
"match": [{"address": 0, "bytes": "3412455842"}, {"address": 40, "bytes": "03"}]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```json
|
|
472
|
+
{
|
|
473
|
+
"name": "Example sensor board",
|
|
474
|
+
"match": {"address": 0, "bytes": "3412455842"},
|
|
475
|
+
"settle_time": 1.0,
|
|
476
|
+
"registers": {
|
|
477
|
+
"model_number": {"address": 0, "size": 2},
|
|
478
|
+
"id": {"address": 7, "size": 1, "writable": true},
|
|
479
|
+
"baud_rate": {"address": 8, "size": 1, "writable": true},
|
|
480
|
+
"mode": {"address": 20, "size": 1, "writable": true,
|
|
481
|
+
"values": {"0": "off", "1": "on"}},
|
|
482
|
+
"pressure": {"address": 24, "size": 2, "signed": true,
|
|
483
|
+
"scale": 0.01, "unit": "kPa", "digits": 2},
|
|
484
|
+
"acc_range_g": {"address": 12, "size": 2},
|
|
485
|
+
"acc_z": {"address": 26, "size": 2, "signed": true, "unit": "g",
|
|
486
|
+
"scale_by": {"register": "acc_range_g", "divisor": 32768}},
|
|
487
|
+
"error": {"address": 30, "size": 1, "bits": {"0": "imu", "3": "adc"}}
|
|
488
|
+
},
|
|
489
|
+
"info": ["id", "mode", "pressure", "acc_z", "error"],
|
|
490
|
+
"summary": ["mode", "pressure"]
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
| key | meaning |
|
|
495
|
+
|---|---|
|
|
496
|
+
| `name` | what the device is called in messages |
|
|
497
|
+
| `match` | `address` and hex `bytes` found there on this device and no other; or a list of such objects, all of which must match (checked in order, so a servo is ruled out by the first READ) |
|
|
498
|
+
| `registers` | name to `address`, `size` (1 or 2), `writable`, `signed` (two's complement), and optionally how to show it: `values` (number to label), `bits` (bit number to label), `scale` or `scale_by` (value × another register ÷ `divisor`), `unit`, `digits`; a writable register may add `settle_time`, the seconds the device stays silent after a write to it (while it saves the value to flash, say), and the write waits for it to answer before reading back |
|
|
499
|
+
| `settle_time` | seconds the device may stay silent after an id or speed change (default 1) |
|
|
500
|
+
| `baudrate_table` | `"sts"` (default) or an object of `baud_rate` value to bits per second |
|
|
501
|
+
| `eeprom_lock` | whether EEPROM writes need the servo-style `lock` register (default false) |
|
|
502
|
+
| `info`, `summary` | registers `feetech info` and the `scan` line show |
|
|
503
|
+
| `description` | free text |
|
|
504
|
+
|
|
505
|
+
A file with a misspelt key, an unknown register in a list, or bytes that are
|
|
506
|
+
not hex is rejected with the file and key named. A broken file is never
|
|
507
|
+
skipped: that would identify the device as a servo and write servo registers
|
|
508
|
+
into it.
|
|
509
|
+
|
|
510
|
+
```console
|
|
511
|
+
$ feetech --device-file my_board.json --max-id 20 scan
|
|
512
|
+
Found 2 device(s) on /dev/ttyUSB0 at 1.00Mbps:
|
|
513
|
+
id 1 model 777 position 2048 (+0.0 deg) 12.1V 35C
|
|
514
|
+
id 10 Example sensor board (model 0x1234, fw 2) mode 0 (off) pressure -4210 (-42.10 kPa)
|
|
515
|
+
$ feetech --device-file my_board.json info 10
|
|
516
|
+
$ feetech --device-file my_board.json registers --device my_board
|
|
517
|
+
$ feetech --device-file my_board.json --simulate scan # try a file without hardware
|
|
518
|
+
$ feetech --device-file my_board.json watch 10 acc_z pressure # live, Ctrl-C to stop
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`watch ID [REGISTER ...]` redraws the named registers (default: what `info`
|
|
522
|
+
shows) every `--interval` seconds, reading neighbouring registers in one READ
|
|
523
|
+
so six IMU axes cost one round trip. Off a terminal it prints one line per
|
|
524
|
+
refresh, for logging; `--count` stops after that many. It works for servos too.
|
|
525
|
+
|
|
526
|
+
`set-id` and `set-baud` write the device's own `id` and `baud_rate` registers
|
|
527
|
+
directly: no torque, no EEPROM lock, then poll for up to `settle_time` while it
|
|
528
|
+
commits them. Servo-only commands (`move`, `torque`, `mode`, `set-zero`,
|
|
529
|
+
`selftest`, `factory-reset`) refuse such a device, and the status screen leaves
|
|
530
|
+
it out.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import sys
|
|
5
|
+
import time
|
|
5
6
|
|
|
6
7
|
import colorama
|
|
7
8
|
from colorama import Fore
|
|
@@ -9,11 +10,15 @@ from colorama import Style
|
|
|
9
10
|
|
|
10
11
|
from feetech_cli.controller import FeetechServoController
|
|
11
12
|
from feetech_cli.controller import list_candidate_ports
|
|
13
|
+
from feetech_cli.devices import SERVO
|
|
14
|
+
from feetech_cli.devices import DeviceFileError
|
|
15
|
+
from feetech_cli.devices import default_device_dir
|
|
16
|
+
from feetech_cli.devices import format_value
|
|
17
|
+
from feetech_cli.devices import load_profiles
|
|
12
18
|
from feetech_cli.protocol import FeetechError
|
|
13
19
|
from feetech_cli.registers import BAUDRATE_INDEX
|
|
14
20
|
from feetech_cli.registers import BAUDRATE_TABLE
|
|
15
21
|
from feetech_cli.registers import CENTER_POSITION
|
|
16
|
-
from feetech_cli.registers import CONTROL_TABLE
|
|
17
22
|
from feetech_cli.registers import DEFAULT_MAX_ID
|
|
18
23
|
from feetech_cli.registers import DEFAULT_MIN_ID
|
|
19
24
|
from feetech_cli.registers import INFO_REGISTERS
|
|
@@ -68,6 +73,14 @@ def build_parser():
|
|
|
68
73
|
"--scs", action="store_true",
|
|
69
74
|
help="talk to the older big endian SCS series instead of STS/SMS",
|
|
70
75
|
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--device-file", action="append", default=[], metavar="PATH",
|
|
78
|
+
help=(
|
|
79
|
+
"JSON file (or directory of them) describing a non-servo device that "
|
|
80
|
+
"may share the bus; repeatable. Also read from $FEETECH_CLI_DEVICES "
|
|
81
|
+
"and ~/.config/feetech-cli/devices/*.json."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
71
84
|
parser.add_argument(
|
|
72
85
|
"--simulate", action="store_true",
|
|
73
86
|
help=(
|
|
@@ -108,6 +121,23 @@ def build_parser():
|
|
|
108
121
|
)
|
|
109
122
|
set_baud.add_argument("-y", "--yes", action="store_true", help="do not ask for confirmation")
|
|
110
123
|
|
|
124
|
+
watch = sub.add_parser(
|
|
125
|
+
"watch", help="show registers of one device live (IMU, pressure, position, ...)"
|
|
126
|
+
)
|
|
127
|
+
watch.add_argument("id", type=int, help="device id")
|
|
128
|
+
watch.add_argument(
|
|
129
|
+
"registers", nargs="*",
|
|
130
|
+
help="register names (default: what 'feetech info' shows for that device)",
|
|
131
|
+
)
|
|
132
|
+
watch.add_argument(
|
|
133
|
+
"-i", "--interval", type=float, default=0.1,
|
|
134
|
+
help="seconds between refreshes (default: %(default)s)",
|
|
135
|
+
)
|
|
136
|
+
watch.add_argument(
|
|
137
|
+
"-n", "--count", type=int, default=None,
|
|
138
|
+
help="stop after this many refreshes (default: until Ctrl-C)",
|
|
139
|
+
)
|
|
140
|
+
|
|
111
141
|
read = sub.add_parser("read", help="read one register")
|
|
112
142
|
read.add_argument("id", type=int, help="servo id")
|
|
113
143
|
read.add_argument("register", help="register name, see 'feetech registers'")
|
|
@@ -117,7 +147,11 @@ def build_parser():
|
|
|
117
147
|
write.add_argument("register", help="register name, see 'feetech registers'")
|
|
118
148
|
write.add_argument("value", type=int, help="value to write")
|
|
119
149
|
|
|
120
|
-
sub.add_parser("registers", help="list the register names of the control table")
|
|
150
|
+
registers = sub.add_parser("registers", help="list the register names of the control table")
|
|
151
|
+
registers.add_argument(
|
|
152
|
+
"--device", default="servo",
|
|
153
|
+
help="whose control table to list: 'servo' (default) or a loaded device file's name",
|
|
154
|
+
)
|
|
121
155
|
|
|
122
156
|
move = sub.add_parser("move", help="command a servo to a position")
|
|
123
157
|
move.add_argument("id", type=int, help="servo id")
|
|
@@ -232,6 +266,7 @@ def make_controller(args):
|
|
|
232
266
|
little_endian=not args.scs,
|
|
233
267
|
min_id=args.min_id,
|
|
234
268
|
max_id=args.max_id,
|
|
269
|
+
device_profiles=args.device_profiles,
|
|
235
270
|
)
|
|
236
271
|
|
|
237
272
|
|
|
@@ -250,10 +285,18 @@ def attach_simulator(controller, args):
|
|
|
250
285
|
Parsed arguments.
|
|
251
286
|
"""
|
|
252
287
|
from feetech_cli.protocol import PacketHandler
|
|
288
|
+
from feetech_cli.simulator import SimulatedBoard
|
|
253
289
|
from feetech_cli.simulator import build_demo_bus
|
|
254
290
|
|
|
255
291
|
baudrate = args.baudrate or 1000000
|
|
256
292
|
bus = build_demo_bus(servo_ids=(1, 2, 3), baudrate=baudrate)
|
|
293
|
+
# One simulated device per loaded device file, so a new file can be tried
|
|
294
|
+
# before the hardware exists.
|
|
295
|
+
boards = []
|
|
296
|
+
for offset, profile in enumerate(args.device_profiles):
|
|
297
|
+
if "id" in profile.control_table and "baud_rate" in profile.control_table:
|
|
298
|
+
boards.append(SimulatedBoard(profile, board_id=10 + offset))
|
|
299
|
+
bus.servos.extend(boards)
|
|
257
300
|
controller.requested_port = bus.port
|
|
258
301
|
controller.requested_baudrate = baudrate
|
|
259
302
|
|
|
@@ -272,8 +315,9 @@ def attach_simulator(controller, args):
|
|
|
272
315
|
|
|
273
316
|
controller._open_port = open_port
|
|
274
317
|
controller.is_simulated = True
|
|
318
|
+
extra = "".join(f", {b.profile.name} at id {b.servo_id}" for b in boards)
|
|
275
319
|
print(
|
|
276
|
-
f"{Fore.YELLOW}Simulated bus: three servos at ids 1, 2 and 3. "
|
|
320
|
+
f"{Fore.YELLOW}Simulated bus: three servos at ids 1, 2 and 3{extra}. "
|
|
277
321
|
f"No hardware is involved.{Style.RESET_ALL}"
|
|
278
322
|
)
|
|
279
323
|
|
|
@@ -335,11 +379,16 @@ def command_scan(args, controller):
|
|
|
335
379
|
if not found:
|
|
336
380
|
_print_empty_bus(controller)
|
|
337
381
|
return 1
|
|
382
|
+
profiles = {servo_id: controller.device_profile(servo_id) for servo_id in found}
|
|
383
|
+
noun = "servo(s)" if all(p.is_servo for p in profiles.values()) else "device(s)"
|
|
338
384
|
print(
|
|
339
|
-
f"Found {len(found)}
|
|
385
|
+
f"Found {len(found)} {noun} on {controller.serial.port} "
|
|
340
386
|
f"at {format_baud(controller.baudrate)}bps:"
|
|
341
387
|
)
|
|
342
388
|
for servo_id in found:
|
|
389
|
+
if not profiles[servo_id].is_servo:
|
|
390
|
+
print(_device_summary(controller, servo_id, profiles[servo_id]))
|
|
391
|
+
continue
|
|
343
392
|
model = controller.read_register(servo_id, "model_number")
|
|
344
393
|
position = controller.read_register(servo_id, "present_position")
|
|
345
394
|
voltage = controller.read_register(servo_id, "present_voltage")
|
|
@@ -353,6 +402,70 @@ def command_scan(args, controller):
|
|
|
353
402
|
return 0
|
|
354
403
|
|
|
355
404
|
|
|
405
|
+
def _device_summary(controller, servo_id, profile):
|
|
406
|
+
"""One scan line for a device that is not a servo.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
controller : FeetechServoController
|
|
411
|
+
An open controller.
|
|
412
|
+
servo_id : int
|
|
413
|
+
Device id.
|
|
414
|
+
profile : DeviceProfile
|
|
415
|
+
What the device was identified as.
|
|
416
|
+
|
|
417
|
+
Returns
|
|
418
|
+
-------
|
|
419
|
+
str
|
|
420
|
+
The line to print: the device's name, its model and firmware when its
|
|
421
|
+
device file names them, then its ``summary`` registers.
|
|
422
|
+
"""
|
|
423
|
+
table = profile.control_table
|
|
424
|
+
header = [name for name in ("model_number", "firmware_version") if name in table]
|
|
425
|
+
shown = list(profile.summary_registers)
|
|
426
|
+
values = controller.dump(servo_id, _with_references(profile, header + shown))
|
|
427
|
+
details = []
|
|
428
|
+
if "model_number" in table:
|
|
429
|
+
model = values["model_number"]
|
|
430
|
+
details.append("model " + ("?" if model is None else f"0x{model:04X}"))
|
|
431
|
+
if "firmware_version" in table:
|
|
432
|
+
details.append(f"fw {_plain_value(values['firmware_version'])}")
|
|
433
|
+
line = f" id {servo_id:>3} {profile.name}"
|
|
434
|
+
if details:
|
|
435
|
+
line += f" ({', '.join(details)})"
|
|
436
|
+
for name in shown:
|
|
437
|
+
line += f" {name} {_annotate(name, values[name], values, profile)}"
|
|
438
|
+
return line
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _with_references(profile, names):
|
|
442
|
+
"""Add the registers that ``scale_by`` rules of ``names`` need.
|
|
443
|
+
|
|
444
|
+
Parameters
|
|
445
|
+
----------
|
|
446
|
+
profile : DeviceProfile
|
|
447
|
+
The device's profile.
|
|
448
|
+
names : list of str
|
|
449
|
+
Registers about to be shown.
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
list of str
|
|
454
|
+
``names`` followed by any missing reference registers.
|
|
455
|
+
"""
|
|
456
|
+
out = list(names)
|
|
457
|
+
for name in names:
|
|
458
|
+
rule = profile.formats.get(name, {})
|
|
459
|
+
if "scale_by" in rule and rule["scale_by"][0] not in out:
|
|
460
|
+
out.append(rule["scale_by"][0])
|
|
461
|
+
return out
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _plain_value(value):
|
|
465
|
+
"""Render an optional register value."""
|
|
466
|
+
return "?" if value is None else str(value)
|
|
467
|
+
|
|
468
|
+
|
|
356
469
|
def resolve_servo_id(controller, servo_id):
|
|
357
470
|
"""Pick the servo to act on when the user did not name one.
|
|
358
471
|
|
|
@@ -403,25 +516,31 @@ def command_info(args, controller):
|
|
|
403
516
|
Process exit status.
|
|
404
517
|
"""
|
|
405
518
|
servo_id = resolve_servo_id(controller, args.id)
|
|
406
|
-
|
|
407
|
-
|
|
519
|
+
profile = controller.device_profile(servo_id)
|
|
520
|
+
table = profile.control_table
|
|
521
|
+
if args.all:
|
|
522
|
+
names = list(table)
|
|
523
|
+
else:
|
|
524
|
+
names = list(profile.info_registers or INFO_REGISTERS)
|
|
525
|
+
values = controller.dump(servo_id, _with_references(profile, names))
|
|
408
526
|
width = max(len(name) for name in names)
|
|
409
|
-
|
|
527
|
+
kind = "Servo" if profile.is_servo else profile.name[:1].upper() + profile.name[1:]
|
|
528
|
+
print(f"{kind} {servo_id} on {controller.serial.port} at {format_baud(controller.baudrate)}bps")
|
|
410
529
|
for name in names:
|
|
411
|
-
register =
|
|
530
|
+
register = table[name]
|
|
412
531
|
value = values[name]
|
|
413
532
|
print(
|
|
414
533
|
" {:<{width}} addr {:>3} {}".format(
|
|
415
534
|
name,
|
|
416
535
|
register.address,
|
|
417
|
-
_annotate(name, value, values),
|
|
536
|
+
_annotate(name, value, values, profile),
|
|
418
537
|
width=width,
|
|
419
538
|
)
|
|
420
539
|
)
|
|
421
540
|
return 0
|
|
422
541
|
|
|
423
542
|
|
|
424
|
-
def _annotate(name, value, context=None):
|
|
543
|
+
def _annotate(name, value, context=None, profile=SERVO):
|
|
425
544
|
"""Render a register value with a unit or label where one applies.
|
|
426
545
|
|
|
427
546
|
Parameters
|
|
@@ -433,6 +552,9 @@ def _annotate(name, value, context=None):
|
|
|
433
552
|
context : dict or None, optional
|
|
434
553
|
Other register values of the same servo. ``present_current`` uses
|
|
435
554
|
``model_number`` and ``present_load`` from it.
|
|
555
|
+
profile : DeviceProfile, optional
|
|
556
|
+
The device's kind. For a device file, its own display rules apply and
|
|
557
|
+
the servo ones never do.
|
|
436
558
|
|
|
437
559
|
Returns
|
|
438
560
|
-------
|
|
@@ -441,6 +563,11 @@ def _annotate(name, value, context=None):
|
|
|
441
563
|
"""
|
|
442
564
|
if value is None:
|
|
443
565
|
return "no data"
|
|
566
|
+
if not profile.is_servo:
|
|
567
|
+
if name == "baud_rate" and value in profile.baudrate_table:
|
|
568
|
+
return f"{value} ({format_baud(profile.baudrate_table[value])}bps)"
|
|
569
|
+
rendered = format_value(profile, name, value, context)
|
|
570
|
+
return str(value) if rendered is None else rendered
|
|
444
571
|
if name == "baud_rate":
|
|
445
572
|
baud = BAUDRATE_TABLE.get(value)
|
|
446
573
|
if baud is None:
|
|
@@ -483,19 +610,46 @@ def command_set_id(args, controller):
|
|
|
483
610
|
int
|
|
484
611
|
Process exit status.
|
|
485
612
|
"""
|
|
613
|
+
kind, memory = _kind_and_memory(controller, args.old_id)
|
|
486
614
|
if not confirm(
|
|
487
|
-
f"Change
|
|
615
|
+
f"Change {kind.lower()} {args.old_id} to id {args.new_id}? This writes the {memory}.",
|
|
488
616
|
args.yes,
|
|
489
617
|
):
|
|
490
618
|
print("Cancelled.")
|
|
491
619
|
return 1
|
|
492
620
|
confirmed = controller.set_id(args.old_id, args.new_id)
|
|
493
621
|
print(
|
|
494
|
-
f"{Fore.GREEN}
|
|
622
|
+
f"{Fore.GREEN}{kind} {args.old_id} is now id {confirmed}.{Style.RESET_ALL}"
|
|
495
623
|
)
|
|
496
624
|
return 0
|
|
497
625
|
|
|
498
626
|
|
|
627
|
+
def _kind_and_memory(controller, servo_id):
|
|
628
|
+
"""Name a device and the memory a setting change writes, for messages.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
controller : FeetechServoController
|
|
633
|
+
An open controller.
|
|
634
|
+
servo_id : int
|
|
635
|
+
Device id.
|
|
636
|
+
|
|
637
|
+
Returns
|
|
638
|
+
-------
|
|
639
|
+
tuple of (str, str)
|
|
640
|
+
``("Servo", "servo EEPROM")`` or, for another kind of device, its
|
|
641
|
+
capitalised name and ``"board's flash"``.
|
|
642
|
+
"""
|
|
643
|
+
try:
|
|
644
|
+
profile = controller.device_profile(servo_id)
|
|
645
|
+
except FeetechError:
|
|
646
|
+
# Absent ids are reported by set_id / set_baudrate themselves.
|
|
647
|
+
return "Servo", "servo EEPROM"
|
|
648
|
+
if profile.is_servo:
|
|
649
|
+
return "Servo", "servo EEPROM"
|
|
650
|
+
return profile.name[:1].upper() + profile.name[1:], "board's flash"
|
|
651
|
+
|
|
652
|
+
|
|
499
653
|
def command_set_baud(args, controller):
|
|
500
654
|
"""Change the bus speed a servo uses.
|
|
501
655
|
|
|
@@ -511,16 +665,17 @@ def command_set_baud(args, controller):
|
|
|
511
665
|
int
|
|
512
666
|
Process exit status.
|
|
513
667
|
"""
|
|
668
|
+
kind, memory = _kind_and_memory(controller, args.id)
|
|
514
669
|
if not confirm(
|
|
515
|
-
f"Set
|
|
516
|
-
f"This writes the
|
|
670
|
+
f"Set {kind.lower()} {args.id} to {format_baud(args.new_baudrate)}bps? "
|
|
671
|
+
f"This writes the {memory}.",
|
|
517
672
|
args.yes,
|
|
518
673
|
):
|
|
519
674
|
print("Cancelled.")
|
|
520
675
|
return 1
|
|
521
676
|
confirmed = controller.set_baudrate(args.id, args.new_baudrate)
|
|
522
677
|
print(
|
|
523
|
-
f"{Fore.GREEN}
|
|
678
|
+
f"{Fore.GREEN}{kind} {args.id} now runs at {format_baud(confirmed)}bps.{Style.RESET_ALL}"
|
|
524
679
|
)
|
|
525
680
|
print(f"Pass --baudrate {confirmed} on later calls, or let it be auto detected.")
|
|
526
681
|
return 0
|
|
@@ -542,10 +697,14 @@ def command_read(args, controller):
|
|
|
542
697
|
Process exit status.
|
|
543
698
|
"""
|
|
544
699
|
value = controller.read_register(args.id, args.register)
|
|
700
|
+
profile = controller.device_profile(args.id)
|
|
545
701
|
context = None
|
|
546
|
-
if
|
|
702
|
+
if not profile.is_servo:
|
|
703
|
+
references = _with_references(profile, [args.register])[1:]
|
|
704
|
+
context = controller.dump(args.id, references) if references else None
|
|
705
|
+
elif args.register == "present_current":
|
|
547
706
|
context = controller.dump(args.id, ["model_number", "present_load"])
|
|
548
|
-
print(f"{args.register} = {_annotate(args.register, value, context)}")
|
|
707
|
+
print(f"{args.register} = {_annotate(args.register, value, context, profile)}")
|
|
549
708
|
return 0
|
|
550
709
|
|
|
551
710
|
|
|
@@ -573,7 +732,8 @@ def command_write(args, controller):
|
|
|
573
732
|
)
|
|
574
733
|
return 1
|
|
575
734
|
value = controller.write_register(args.id, args.register, args.value)
|
|
576
|
-
|
|
735
|
+
profile = controller.device_profile(args.id)
|
|
736
|
+
print(f"{args.register} = {_annotate(args.register, value, None, profile)}")
|
|
577
737
|
if value != args.value:
|
|
578
738
|
print(
|
|
579
739
|
f"{Fore.RED}Read back {value} but {args.value} was written; "
|
|
@@ -583,6 +743,72 @@ def command_write(args, controller):
|
|
|
583
743
|
return 0
|
|
584
744
|
|
|
585
745
|
|
|
746
|
+
def command_watch(args, controller):
|
|
747
|
+
"""Show registers of one device, refreshed in place.
|
|
748
|
+
|
|
749
|
+
Each refresh reads every shown register (plus any a ``scale_by`` rule
|
|
750
|
+
needs) in as few READs as possible. On a terminal the block is redrawn in
|
|
751
|
+
place; otherwise one line per refresh is printed, which suits logging.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
args : argparse.Namespace
|
|
756
|
+
Parsed arguments.
|
|
757
|
+
controller : FeetechServoController
|
|
758
|
+
An open controller.
|
|
759
|
+
|
|
760
|
+
Returns
|
|
761
|
+
-------
|
|
762
|
+
int
|
|
763
|
+
Process exit status.
|
|
764
|
+
"""
|
|
765
|
+
profile = controller.device_profile(args.id)
|
|
766
|
+
names = list(args.registers) or list(profile.info_registers or INFO_REGISTERS)
|
|
767
|
+
unknown = [name for name in names if name not in profile.control_table]
|
|
768
|
+
if unknown:
|
|
769
|
+
print(
|
|
770
|
+
f"{Fore.RED}Unknown register(s) {', '.join(unknown)} for a {profile.name}; "
|
|
771
|
+
f"see 'feetech registers --device {profile.key}'.{Style.RESET_ALL}"
|
|
772
|
+
)
|
|
773
|
+
return 1
|
|
774
|
+
to_read = _with_references(profile, names)
|
|
775
|
+
interactive = sys.stdout.isatty()
|
|
776
|
+
width = max(len(name) for name in names)
|
|
777
|
+
drawn = 0
|
|
778
|
+
refreshes = 0
|
|
779
|
+
started = time.monotonic()
|
|
780
|
+
try:
|
|
781
|
+
while args.count is None or refreshes < args.count:
|
|
782
|
+
tick = time.monotonic()
|
|
783
|
+
values = controller.read_registers(args.id, to_read)
|
|
784
|
+
refreshes += 1
|
|
785
|
+
rate = refreshes / max(time.monotonic() - started, 1e-6)
|
|
786
|
+
if interactive:
|
|
787
|
+
lines = [
|
|
788
|
+
f"{profile.name} {args.id} {rate:5.1f} Hz (Ctrl-C to stop)"
|
|
789
|
+
] + [
|
|
790
|
+
" {:<{width}} {}".format(
|
|
791
|
+
name, _annotate(name, values[name], values, profile), width=width
|
|
792
|
+
)
|
|
793
|
+
for name in names
|
|
794
|
+
]
|
|
795
|
+
if drawn:
|
|
796
|
+
sys.stdout.write(f"\x1b[{drawn}F")
|
|
797
|
+
sys.stdout.write("".join(f"\x1b[2K{line}\n" for line in lines))
|
|
798
|
+
sys.stdout.flush()
|
|
799
|
+
drawn = len(lines)
|
|
800
|
+
else:
|
|
801
|
+
print(" ".join(
|
|
802
|
+
f"{name}={_annotate(name, values[name], values, profile)}" for name in names
|
|
803
|
+
))
|
|
804
|
+
remaining = args.interval - (time.monotonic() - tick)
|
|
805
|
+
if remaining > 0 and (args.count is None or refreshes < args.count):
|
|
806
|
+
time.sleep(remaining)
|
|
807
|
+
except KeyboardInterrupt:
|
|
808
|
+
print()
|
|
809
|
+
return 0
|
|
810
|
+
|
|
811
|
+
|
|
586
812
|
def command_registers(args):
|
|
587
813
|
"""List the control table.
|
|
588
814
|
|
|
@@ -596,8 +822,18 @@ def command_registers(args):
|
|
|
596
822
|
int
|
|
597
823
|
Process exit status.
|
|
598
824
|
"""
|
|
599
|
-
|
|
600
|
-
for
|
|
825
|
+
known = {SERVO.key: SERVO}
|
|
826
|
+
known.update({profile.key: profile for profile in args.device_profiles})
|
|
827
|
+
if args.device not in known:
|
|
828
|
+
print(
|
|
829
|
+
f"{Fore.RED}No device '{args.device}'. Known: {', '.join(sorted(known))}. "
|
|
830
|
+
f"Load its file with --device-file or put it in {default_device_dir()}."
|
|
831
|
+
f"{Style.RESET_ALL}"
|
|
832
|
+
)
|
|
833
|
+
return 1
|
|
834
|
+
table = known[args.device].control_table
|
|
835
|
+
width = max(len(name) for name in table)
|
|
836
|
+
for name, register in sorted(table.items(), key=lambda kv: kv[1].address):
|
|
601
837
|
print(
|
|
602
838
|
" {:<{width}} addr {:>3} {} byte {:<6} {}".format(
|
|
603
839
|
name, register.address, register.size, register.area,
|
|
@@ -728,6 +964,13 @@ def command_monitor(args, controller):
|
|
|
728
964
|
Process exit status.
|
|
729
965
|
"""
|
|
730
966
|
found = controller.scan()
|
|
967
|
+
others = [sid for sid in found if not controller.is_servo(sid)]
|
|
968
|
+
found = [sid for sid in found if sid not in others]
|
|
969
|
+
for sid in others:
|
|
970
|
+
print(
|
|
971
|
+
f"{Fore.YELLOW}id {sid} is a {controller.device_profile(sid).name}, not a "
|
|
972
|
+
f"servo; 'feetech info {sid}' shows it.{Style.RESET_ALL}"
|
|
973
|
+
)
|
|
731
974
|
if not found:
|
|
732
975
|
# Start anyway and wait. A servo plugged in later
|
|
733
976
|
# is picked up without restarting the tool.
|
|
@@ -878,6 +1121,7 @@ BUS_COMMANDS = {
|
|
|
878
1121
|
"set-baud": command_set_baud,
|
|
879
1122
|
"read": command_read,
|
|
880
1123
|
"write": command_write,
|
|
1124
|
+
"watch": command_watch,
|
|
881
1125
|
"move": command_move,
|
|
882
1126
|
"torque": command_torque,
|
|
883
1127
|
"mode": command_mode,
|
|
@@ -904,6 +1148,11 @@ def main(argv=None):
|
|
|
904
1148
|
colorama.init()
|
|
905
1149
|
parser = build_parser()
|
|
906
1150
|
args = parser.parse_args(argv)
|
|
1151
|
+
try:
|
|
1152
|
+
args.device_profiles = load_profiles(args.device_file)
|
|
1153
|
+
except DeviceFileError as error:
|
|
1154
|
+
print(f"{Fore.RED}Device file: {error}{Style.RESET_ALL}")
|
|
1155
|
+
return 2
|
|
907
1156
|
|
|
908
1157
|
if args.command == "ports":
|
|
909
1158
|
return command_ports(args)
|