litearm-python 1.2.0__py3-none-any.whl

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,508 @@
1
+ Metadata-Version: 2.4
2
+ Name: litearm-python
3
+ Version: 1.2.0
4
+ Summary: LiteArm STM32 直连后端 —— 薄协议 SDK (固件承担 S曲线/IK/动力学/控制律)
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: pyserial>=3.4
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7; extra == "dev"
12
+ Dynamic: license-file
13
+
14
+ # litearm-python
15
+
16
+ Python SDK for the LiteArm robotic arm: drive the arm from a PC over one USB serial cable,
17
+ straight to its firmware — no server or middleware in between. Trajectory planning, kinematics
18
+ and dynamics are all carried by the firmware.
19
+
20
+ > Full interface reference: [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md).
21
+
22
+ ## Features
23
+
24
+ - **Pure Python**: Python 3.9+, poses are plain lists / tuples, no numpy needed
25
+ - **A single dependency**: `pyserial`, nothing else
26
+ - **Direct over USB**: one cable to the firmware, no server in between
27
+ - **The firmware does the planning**: kinematics and dynamics live there; the PC sends points and decides arrival
28
+ - **Full motion API**: joint moves, Cartesian lines / arcs / waypoints, hand-guiding
29
+ - **Tool point (TCP) offset**: a host-side xyz + rpy layer, so `move_*` and `ik()` take tool
30
+ poses while `get_tcp()` keeps returning the flange
31
+ - **Payload identification**: measure the mass and centre of mass of the end-of-arm load from
32
+ the arm's own torque, then hand the result to `set_payload()`
33
+ - **Safety built in**: fail-closed in child processes, a dedicated emergency stop, and every irreversible command flagged
34
+
35
+ ## Install
36
+
37
+ | Item | Requirement |
38
+ | --- | --- |
39
+ | Python | 3.9 or later |
40
+ | Dependencies | `pyserial >= 3.4` (the only one) |
41
+ | Firmware | `Litearm1.5.0` or later |
42
+ | Connection | USB CDC serial, VID:PID `1d50:606f` |
43
+
44
+ From the repository root:
45
+
46
+ ```bash
47
+ pip install -e .
48
+ python3 -c "import litearm; print(litearm.__version__)" # verify
49
+ ```
50
+
51
+ Linux needs serial permissions — check what the device node itself says first:
52
+
53
+ ```bash
54
+ ls -l /dev/ttyACM0
55
+ ```
56
+
57
+ If it is `crw-rw-rw-` you do not need the group at all and can open it as you are. Only when it
58
+ is `crw-rw----` do you need to add yourself to `dialout`:
59
+
60
+ ```bash
61
+ sudo usermod -aG dialout $USER # log in again for it to take effect
62
+ ```
63
+
64
+ `crw-rw-rw-` usually comes from a udev rule rather than the `dialout` group:
65
+
66
+ ```
67
+ # /etc/udev/rules.d/99-litearm.rules
68
+ SUBSYSTEM=="tty", ATTRS{idVendor}=="1d50", ATTRS{idProduct}=="606f", MODE="0666"
69
+ ```
70
+
71
+ That rule belongs to no package — it was dropped in when the machine was set up. `MODE="0666"`
72
+ means **any local user on this machine can drive the arm**. That is deliberate, not an
73
+ oversight.
74
+
75
+ ## Quick start
76
+
77
+ ```python
78
+ import litearm as pa
79
+
80
+ # connect: find the port and check the firmware version
81
+ with pa.Arm().connect() as arm:
82
+ print(arm.firmware, arm.n) # version string, joint count
83
+
84
+ # after power-up you must reset() first: the arm boots into the
85
+ # EMERGENCY latch, and until that is cleared enable() is refused
86
+ # with ERR{0x10,0x06}
87
+ arm.reset()
88
+
89
+ # enable (required before any motion)
90
+ arm.enable()
91
+
92
+ # joint move: the firmware plans the S-curve and completes it
93
+ # start from a pose with every joint well away from its limits:
94
+ # fully extended (q ≈ 0) is a singular pose and no Cartesian
95
+ # motion can be planned out of it
96
+ arm.movej([0.0, 0.4, -0.8, -1.2, 0.0, 0.6, 0.0], speed=0.3)
97
+
98
+ # Cartesian line: a pose is 3 position values (m) + 3 orientation values (rad)
99
+ # take the measured pose as the start and move position only — copying
100
+ # the orientation saves you from having to work one out
101
+ tcp = arm.get_tcp().value
102
+ arm.move_l((tcp[0], tcp[1], tcp[2] - 0.05, *tcp[3:]), speed=0.5)
103
+
104
+ # read state
105
+ print(arm.get_state().value.q) # current joint angles
106
+ print(arm.get_tcp().value) # current tool pose
107
+
108
+ # inverse kinematics: pose → joint angles
109
+ print(arm.ik((tcp[0], tcp[1], tcp[2] - 0.07, *tcp[3:])))
110
+
111
+ # go home
112
+ arm.home()
113
+ # leaving the with block disconnects
114
+ ```
115
+
116
+ `reset()` is a step after power-up, not an option: the arm boots into the `EMERGENCY` latch
117
+ (`mode_name == "EMERGENCY"`, `FAULT` in `flags`, a non-zero `joint_fault`), and until that is
118
+ cleared `enable()` throws `ERR{0x10,0x06}` **immediately** — without sending a single retry.
119
+ `reset()` puts `mode` back to `INIT` and clears the fault bits, and the whole quick start runs
120
+ from there.
121
+
122
+ `Arm().connect()` is the only entry point. Every session carries a read thread, so **you must
123
+ `close()` it** — `with` does that for you. Pass `port` to pick the serial port; leave it out and
124
+ the SDK auto-discovers it (VID:PID `1d50:606f`). The SDK **reads no environment variables**.
125
+
126
+ Every `arm` below refers to that connected session object; the snippets show only the steps
127
+ that section is about.
128
+
129
+ ## API reference
130
+
131
+ ### Connection
132
+
133
+ ```python
134
+ arm = pa.Arm(port="/dev/ttyACM0").connect() # omit port to auto-discover
135
+ print(arm.firmware) # version string, e.g. "Litearm1.8.0-7J"
136
+ print(arm.n) # joint count
137
+ arm.close() # disconnect (a with block does this for you)
138
+ ```
139
+
140
+ ### Joint motion
141
+
142
+ All three need `enable()` first: `movej` is a single shot (the firmware plans the S-curve and
143
+ completes it), `movej_sync` is a synchronised point to point, and `home` goes home.
144
+
145
+ ```python
146
+ arm.enable() # required before motion
147
+ arm.movej([0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], speed=0.3) # single shot
148
+ arm.home() # go home
149
+ ```
150
+
151
+ **Do not** read `get_state().enabled` on the line after `enable()`. It returns before the
152
+ state stream reflects the change — **2.0-3.1 ms** to return, while the flag takes
153
+ **17.6-48.5 ms** to turn true (n=11; one earlier run on the same unit read 7.0-7.8 ms —
154
+ the lag is state-dependent, but always several frames, never one). So the check reads
155
+ `False` and means nothing. **Poll until it flips** — a fixed 10 ms wait is not enough.
156
+
157
+ ### Cartesian motion
158
+
159
+ ```python
160
+ # Offset the *measured* pose. Reachability depends on the arm's geometry, not on the
161
+ # request, so a hard-coded absolute pose can simply have no IK solution.
162
+ p = arm.get_tcp().value
163
+ P1 = (p[0] + 0.02, p[1], p[2], p[3], p[4], p[5])
164
+ P2 = (p[0] + 0.04, p[1], p[2], p[3], p[4], p[5])
165
+ P3 = (p[0] + 0.06, p[1], p[2], p[3], p[4], p[5])
166
+
167
+ arm.move_l(P1, speed=0.3) # straight line
168
+ arm.move_p(P1) # joint-space point to point — not a line
169
+ arm.move_path([P1, P2, P3], speed=0.3) # visit the waypoints in turn, sharp corners
170
+ arm.move_c(arm.get_tcp().value, P1, P2) # arc — start must be the measured TCP
171
+
172
+ plan = arm.move_l(P2, wait=False) # still blocks for the plan reply
173
+ print(plan.settled) # False — we did not wait for the arm to stop
174
+ arm.set_speed(50) # global governor: integer percentage 0..100
175
+ ```
176
+
177
+ ⚠ **The arm boots at its zero pose, which is the fully-extended singularity.** Cartesian
178
+ commands from there have no usable IK solution and are refused (`IKError`, `err=1`). Bend a
179
+ joint first (`arm.movej([0.0, -0.5, 0.0, -1.0, 0.0, 0.0, 0.0], speed=0.3)`) — see Quick start.
180
+
181
+ ### State / kinematics
182
+
183
+ ```python
184
+ state = arm.get_state().value
185
+ print(state.q, state.dq, state.tau) # joint angles / velocity / torque
186
+ print(state.enabled, state.faulted) # enabled? faulted?
187
+ print(arm.get_tcp().value) # flange pose (firmware forward kinematics)
188
+ print(arm.ik((0.30, 0.0, 0.35, 3.1416, 0.0, 0.0))) # pose → joint angles
189
+ ```
190
+
191
+ ### Tool point (TCP) offset
192
+
193
+ The firmware's TCP *is* the flange. To control a tool point instead, declare the offset once —
194
+ `move_p` / `move_l` / `move_c` / `move_path` / `ik()` then take **tool** poses, including the
195
+ `move_c` start check and the `move_p` arrival test, while `get_tcp()` keeps returning the flange:
196
+
197
+ ```python
198
+ arm.set_tcp_offset((0.0, 0.0, 0.18)) # metres / radians, in the flange frame
199
+ print(arm.get_tcp().value) # flange — firmware truth, unchanged
200
+ print(arm.get_tool_tcp().value) # the same pose plus the offset
201
+ arm.clear_tcp_offset() # back to controlling the flange
202
+ ```
203
+
204
+ The weight of the tool is a separate setting — `set_payload()`. Nothing here reaches the firmware.
205
+
206
+ ### End-of-arm payload
207
+
208
+ `set_payload(mass, com)` writes the load **you** measured. `litearm.payload` measures it: it holds
209
+ at the poses of an empty-load baseline, subtracts that baseline configuration by configuration,
210
+ and solves for the mass and the centre of mass in the flange frame. Apply the result with
211
+ `litearm.payload.apply(arm, result)`.
212
+
213
+ ```bash
214
+ # 1) turn a legacy baseline CSV into the JSON this package reads (once)
215
+ python3 -c "
216
+ from litearm.payload import Baseline
217
+ Baseline.from_csv('runs/20260918_151253_空载基准/noload_baseline.csv').save('empty.json')"
218
+
219
+ # 2) hold the load, then measure it (moves the arm through the baseline poses)
220
+ python3 examples/09_payload_identify.py --empty-baseline empty.json --go
221
+ ```
222
+
223
+ The regression matrix comes from the firmware (`0x39` evaluates `G(q)` at any `q`, and the
224
+ payload is composited in as a point mass), so nothing on the PC side needs a dynamics model and
225
+ nothing new is pulled in as a dependency. The **baseline is not optional**: subtracting it is what
226
+ cancels the arm's own model error — an absolute solve feeds that error straight into the answer.
227
+ The probe's side effects and the acceptance thresholds are in §5.16 of
228
+ [the developer guide](docs/DEVELOPER_GUIDE.md).
229
+
230
+ ### Life / safety
231
+
232
+ ```python
233
+ arm.emergency_stop() # emergency stop: no preconditions at all
234
+ arm.reset() # clear faults + re-anchor (not an MCU reboot)
235
+ arm.clear_faults() # clears the FAULT bit only, not latches
236
+ arm.disable() # the arm is no longer held once disabled
237
+ ```
238
+
239
+ `clear_faults()` **cannot clear a latched state**: the `FAULT` bit goes away, `EMERGENCY` mode
240
+ and `FB_STALE` come straight back, and the arm still will not enable. Only `reset()` recovers
241
+ from those two — `clear_faults()` clears the single `ARM_FLAG_FAULT` bit, while the enable gate
242
+ looks at latch conditions that only `reset()` clears.
243
+
244
+ ### Feed-forward / dynamics
245
+
246
+ ```python
247
+ arm.set_payload(1.0) # always call this after changing payload
248
+ arm.set_gravity_scale([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
249
+ arm.ff_preset(1) # 0 all off / 1 factory / 2 all on
250
+ print(arm.get_ff_scalar(4).value) # read payload_mass back
251
+ ```
252
+
253
+ ### Hand-guiding
254
+
255
+ ```python
256
+ with arm.zero_g():
257
+ input("Drag the arm, then press Enter")
258
+ ```
259
+
260
+ ### Passthrough / servo
261
+
262
+ ```python
263
+ arm.send_mit(0, 0.0, 0.0, 30.0, 1.0, 0.0) # bypasses planning; keep alive at ≥10 Hz
264
+ arm.joint_follow([0.0]*7, [0.0]*7, [30.0]*7, [1.0]*7) # no tau: the firmware computes it
265
+ ```
266
+
267
+ ### Parameters (`arm.params.*`)
268
+
269
+ ```python
270
+ p = arm.params.get_joint_param(0).value # J1's kp / kd / tau_max / soft limits
271
+ arm.params.set_joint_param(0, 30.0, 1.0, 20.0) # idx, kp, kd, tau_max
272
+ for jp in arm.params.all_joint_params():
273
+ print(jp.idx, jp.kp, jp.q_min, jp.q_max)
274
+ ```
275
+
276
+ ### Dynamics model (`arm.model.*`)
277
+
278
+ ```python
279
+ print(arm.model.probe()) # does the firmware support this?
280
+ print(arm.model.status().value) # override / staged_mask / dirty
281
+ print(arm.model.get_gravity([0.0] * 7).value) # G(q)
282
+ ```
283
+
284
+ ### Data capture (`arm.log.*`)
285
+
286
+ ```python
287
+ arm.log.start(300) # record 300 ticks, then stop
288
+ r = arm.log.reader()
289
+ print(r.total()) # ticks written so far
290
+ for s in r.samples()[:3]:
291
+ print(s.tick, s.q_ref, s.dq, s.tau)
292
+ ```
293
+
294
+ ### Firmware self-test (`arm.diag.*`)
295
+
296
+ ```python
297
+ print(arm.diag.kin_bench().value) # CAN link diagnostic counters
298
+ ```
299
+
300
+ ### License and activation
301
+
302
+ An arm that has never been activated rejects `enable()` with `ERR{0x10,0x08}` and answers every
303
+ other command normally, so it stays diagnosable. `arm.license()` reads the record — a bare
304
+ `LicenseInfo`, not a `Msg`; `arm.activate()` submits a vendor-issued credential and needs
305
+ `disable()` first. See [the developer guide §5.15](docs/DEVELOPER_GUIDE.md#515-license-and-activation).
306
+
307
+ ### Persistence
308
+
309
+ ```python
310
+ arm.disable() # required first; enabled calls are refused
311
+ arm.save_params() # writes flash, irreversible
312
+ arm.enable()
313
+ ```
314
+
315
+ `save_params()` **requires the arm to be disabled**: called while enabled (or with an `enable`
316
+ in flight) it returns `ERR{0x25,0x04}`. And the moment it is disabled the arm is no longer held
317
+ by the position loop — **side-mounted or under load, support it or move it to a low pose first**,
318
+ or it drops under its own weight.
319
+
320
+ ### Read-only properties
321
+
322
+ ```python
323
+ print(arm.n, arm.firmware, arm.fw_version) # joint count / version string / version tuple
324
+ print(arm.q_tol, arm.dq_tol, arm.move_timeout) # arrival criteria and motion timeout
325
+ print(arm.zero_g_active, arm.last_reset_reason)
326
+ ```
327
+
328
+ ## Command line
329
+
330
+ ```bash
331
+ litearm-python status # read-only
332
+ litearm-python fw # version string + axis count
333
+ litearm-python tcp # current pose + frame rate
334
+ litearm-python movej -0.1 0 0 0 0 0 0 --speed 0.3
335
+ litearm-python home
336
+ ```
337
+
338
+ `status` / `fw` / `tcp` are read-only; the rest really drive the arm.
339
+
340
+ **Do not** expect the `litearm-python` name to resolve as written: the console script is
341
+ installed into `~/.local/bin`, which is not on `PATH` by default (see Install), so these
342
+ lines report `command not found`. Use `python3 -m litearm <subcommand>` — it is equivalent —
343
+ or add `~/.local/bin` to `PATH`.
344
+
345
+ ## Things to watch out for
346
+
347
+ Start with the [troubleshooting guide](TROUBLESHOOTING.md): it has ready-made commands for
348
+ finding who holds the serial port, what to do about each `enable()` rejection code, and how to
349
+ tell Cartesian failures apart.
350
+
351
+ ### Read the payload via `.value`
352
+
353
+ The 11 "read one frame" getters return the envelope `Msg(value, hz, timestamp)` — `.value` is
354
+ the payload itself, while `hz` / `timestamp` are how often frames of that kind arrive and when
355
+ the last one landed. If **both are 0**, no frame of that kind has ever arrived.
356
+
357
+ ```python
358
+ print(arm.get_state()) # Msg(value=RobotState(...), hz=100.2, timestamp=...)
359
+ print(arm.get_state().value.q) # [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
360
+ ```
361
+
362
+ **That "both are 0" test has an exception**: the **first** frame a process reads counts `hz` as
363
+ 0 — `print(arm.get_state())` right after connecting shows `hz=0.0`, but the frame is real and the
364
+ same session reads `hz≈98.7` a few seconds later. To decide whether frames of a kind have ever
365
+ arrived, look at `timestamp` (or read again later), not at a single `hz`.
366
+
367
+ ### How to read `mode` / `flags`
368
+
369
+ `RobotState.mode_name` is the firmware's motion mode. It has exactly 8 values:
370
+
371
+ | `mode` | `mode_name` | Meaning |
372
+ | --- | --- | --- |
373
+ | 0 | `INIT` | Booted / not enabled. What you see after `reset()` with no motion |
374
+ | 1 | `MOVE_J` | Joint trajectory in flight (`movej` / `home`) |
375
+ | 2 | `MOVE_P` | Cartesian point to point |
376
+ | 3 | `MOVE_JS` | Joint passthrough / servo in flight |
377
+ | 4 | `MOVE_MIT` | `send_mit` in flight |
378
+ | 5 | `MIT_ALL` | `send_mit_all` in flight |
379
+ | 6 | `EMERGENCY` | Emergency-stop latch. **This is where the arm boots**, see [Quick start](#quick-start) |
380
+ | 7 | `ZERO_G` | Zero-gravity hand-guiding |
381
+
382
+ Values outside the table show up as the raw number. On real hardware you see `INIT` / `MOVE_J` /
383
+ `EMERGENCY` / `ZERO_G`; the firmware documents `MOVE_P` as "the PC computes IK and turns it into
384
+ `move_j` before B3", so you normally never see it.
385
+
386
+ `flags` / `flag_names` are the safety bits: `FAULT` / `WD_TRIPPED` / `FB_STALE` / `TEMP_WARN` /
387
+ `POS_VIOL` / `OVERSPEED`. Two of them are easy to misread:
388
+
389
+ - **`WD_TRIPPED` is normal, not a fault.** It stays lit while the arm is enabled but idle
390
+ (`mode != MOVE_J`) — that is the firmware's command watchdog (0.1 s) with no motion command
391
+ keeping it alive.
392
+ - **`FB_STALE` is a live condition**, and clearing fault bits does not clear it. See
393
+ [Life / safety](#life--safety).
394
+
395
+ `mode == ZERO_G` says the **arm** is in zero-gravity, held by the firmware; `arm.zero_g_active`
396
+ says **this session** is still running the keep-alive. The two can disagree: kill the process and
397
+ the arm stays in `ZERO_G` while a new session reads `zero_g_active` as `False`.
398
+
399
+ ### Multiprocessing: a forked child must not use an inherited `Arm`
400
+
401
+ Commands **really go out on the wire**, but the parent's read thread consumes the replies — you
402
+ only see a "no response" timeout, and retrying means **sending the command twice**. Reading
403
+ state is subtler: it does not raise, it just **returns an eternally stale value**.
404
+
405
+ So this library is **fail-closed**: any command in a child process immediately raises
406
+ `ForkedSessionError`, and not a single byte goes out.
407
+
408
+ For a child to use the arm, the **parent must `close()` and release the port first**, then fork,
409
+ then create a new `Arm` inside the child:
410
+
411
+ ```python
412
+ import multiprocessing
413
+
414
+ def worker():
415
+ a = pa.Arm().connect() # create it inside the child
416
+
417
+ a = pa.Arm().connect()
418
+ a.close() # without this the child cannot connect
419
+ p = multiprocessing.Process(target=worker)
420
+ p.start()
421
+ ```
422
+
423
+ `close()` **is still callable** in the child (it only clears session state and never touches the
424
+ transport, so it does not hang) — but do not expect it to free the serial port.
425
+
426
+ ### Safety rules
427
+
428
+ 1. **After `disable()` the arm is no longer held by the position loop** — under load it sags.
429
+ 2. **`movej` does not check joint limits** — an out-of-range target is clamped by the firmware
430
+ and the arm **still travels the full stroke**.
431
+ 3. **`move_js` / `send_mit` need keep-alive at ≥10 Hz** — otherwise the 0.1 s watchdog drops
432
+ stiffness and the arm sags slowly.
433
+ 4. **`enter_dfu()` is a terminal-state operation** — afterwards every entry point stops working
434
+ and you need a new `Arm` after flashing.
435
+
436
+ ### Irreversible commands: do not run these on a calibrated arm
437
+
438
+ These overwrite or erase that unit's per-arm identified dynamics model, with **no undo**:
439
+
440
+ | Entry point | Effect |
441
+ | --- | --- |
442
+ | `save_params()` | writes the current RAM to flash |
443
+ | `arm.model.commit()` | applies staged dynamics model changes |
444
+ | `arm.model.revert()` | rolls the dynamics model back (does not touch flash, so it returns on power-up) |
445
+ | `arm.params.reset_factory()` | restores factory settings |
446
+
447
+ Only do this on a board whose calibration has no value. **`arm.model.set_jm()` should never be
448
+ called** — a wrong joint mapping can make the arm flail, and there is no dependable way back.
449
+
450
+ When stress-testing the CAN link, run `candump` (read-only) only, never `cangen`: `can0` *is*
451
+ the motor bus.
452
+
453
+ ## Examples
454
+
455
+ See [examples/README.md](examples/README.md):
456
+
457
+ - `01_hello.py` — handshake + firmware version + reading state
458
+ - `02_movej.py` — joint motion
459
+ - `03_move_p.py` — Cartesian point to point
460
+ - `04_ik_tcp.py` — inverse kinematics and the current pose
461
+ - `05_ff_tune.py` — dynamics / control-law tuning
462
+ - `06_cartesian.py` — Cartesian lines / arcs / waypoints
463
+ - `07_vel_jitter_trace.py` — 300 Hz per-tick capture
464
+
465
+ The examples are **read-only by default**; anything that moves needs `--go`. Run them from
466
+ the repository root:
467
+
468
+ ```bash
469
+ source env.sh # exports PYTHONPATH / PYTHON_BIN / LITEARM_PORT
470
+ python3 examples/01_hello.py
471
+ ./run_example.sh 02_movej.py --go
472
+ ```
473
+
474
+ ## Development
475
+
476
+ ```bash
477
+ pip install -e ".[dev]"
478
+ pytest # full offline run, never touches hardware
479
+ LITEARM_LIVE=1 pytest # plus hardware tests, moves a little
480
+ ```
481
+
482
+ Running the tests does not require installing the package; `tests/conftest.py` sets `sys.path`
483
+ itself. Do not set `LITEARM_LIVE` with nobody present, and never call `enter_dfu()` /
484
+ `reset_factory()`.
485
+
486
+ **On a machine with ROS 2 installed, `pytest` dies during collection and none of this repository
487
+ is involved**: pytest auto-loads the plugins under `/opt/ros/...` by entry point, and `launch`
488
+ needs `lark`, which is not installed:
489
+
490
+ ```
491
+ ModuleNotFoundError: No module named 'lark'
492
+ ```
493
+
494
+ A virtualenv does not stop it — `PYTHONPATH` takes priority over the venv's `site-packages`.
495
+ Either of these works:
496
+
497
+ ```bash
498
+ PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest
499
+ env -u PYTHONPATH pytest
500
+ ```
501
+
502
+ `-p no:launch_testing` is **not** enough: the `launch_ros` entry point gets loaded anyway.
503
+
504
+ On Windows use `env.ps1` / `env.cmd` and `run_example.ps1` / `run_example.cmd`.
505
+
506
+ ## License
507
+
508
+ MIT
@@ -0,0 +1,23 @@
1
+ litearm/__init__.py,sha256=8tCm70g30CsRSqBDa3uqwqwQuSvyT4Z87SlstKk4hPQ,6004
2
+ litearm/__main__.py,sha256=w9hCRg_CvVYA9wtBaA__gm3Jxl9YCHK1IxFQG8OrwBE,55
3
+ litearm/_protocol.py,sha256=-dX-DCO-gE47Mc2XffqiyGKar1gnJfPDFecwkjwahyE,29256
4
+ litearm/_rot.py,sha256=MtG8gmx0WuvEyA3x4FsnYIa6tj2sYhq-CaS4Ut-S1FQ,10576
5
+ litearm/arm.py,sha256=nkd9HMmdakOdEAYtgCPXtH-frMX_s7aBLbzfcFh3PMo,159106
6
+ litearm/cart.py,sha256=2pFTJEtsFMUo7pWjrm9XZXhZCAVWJIwGv0kefebtBq0,96241
7
+ litearm/diagnostics.py,sha256=lbyaFlWgY4TbZIW6AKqgPQUcwKgWb-T1J-NS_OcsaIg,9652
8
+ litearm/errors.py,sha256=s02ineUPP8MCgCnwxwWlu_HoWVt0beD6_qYpO3pjQyk,32270
9
+ litearm/log.py,sha256=2sM5Op7OuZQT1q88L3rfO68ZD97S-Fyc7bukRBXUU1s,9840
10
+ litearm/model.py,sha256=gUMNe-63XWVLUqxIvX41-QDP07JExPwFM6k6Qr7awFI,12164
11
+ litearm/params.py,sha256=epInU50_5Ynf6hR2rZS4TTuNUCRbKgs5w361snY1gUo,5506
12
+ litearm/payload.py,sha256=Fq7Z7cxu__vttnhCYr6KIWTR6BU0vkW1Bm1azh_DD24,34158
13
+ litearm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ litearm/state.py,sha256=VGsdy0whowSRbTPaUM4snVka8o6IUgyFAs5iaCdahno,8862
15
+ litearm/testing.py,sha256=AZ3zWMScd_UNTDgt6NUQI_Svg6Xk9J7XmW_etX2RoFU,55515
16
+ litearm/tool.py,sha256=2PgLJM2olLaWlpMSlCKEYrNhey7ksmgu2Sd8pvNv33k,7582
17
+ litearm/transport.py,sha256=-Id2zlIPJWhGQ5HjnUgOPx4Nb6N0ntrwHMsXfAQ8o6g,21547
18
+ litearm_python-1.2.0.dist-info/licenses/LICENSE,sha256=yVuuHRzgI17MzTVgt3LsHvuX80innw--CmNPDCzO_iw,11358
19
+ litearm_python-1.2.0.dist-info/METADATA,sha256=9qSgbF8n639Xiwi4LHPYz12GsMrMgaaS3vS0O-8znoY,21019
20
+ litearm_python-1.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
+ litearm_python-1.2.0.dist-info/entry_points.txt,sha256=yFrsF49DqQ-8bHbrksyDuSxQOEER7i-9sWixm07AFDE,52
22
+ litearm_python-1.2.0.dist-info/top_level.txt,sha256=gqkLqmKbrqDYltZitHGepKU9jIMwLPTijtJpTcoWJ1Y,8
23
+ litearm_python-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ litearm-python = litearm.arm:main