openbricks 2.6.2__tar.gz → 2.7.1__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.
- {openbricks-2.6.2 → openbricks-2.7.1}/CHANGELOG.md +40 -0
- {openbricks-2.6.2/openbricks.egg-info → openbricks-2.7.1}/PKG-INFO +1 -1
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/drivebase_core.c +82 -46
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/drivebase_core.h +12 -0
- {openbricks-2.6.2 → openbricks-2.7.1/openbricks.egg-info}/PKG-INFO +1 -1
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/__init__.py +1 -1
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_docs/offline-docs.zip +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/cli.py +8 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/flash.py +38 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/run.py +9 -10
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_cli.py +2 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_flash.py +78 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_run.py +18 -16
- {openbricks-2.6.2 → openbricks-2.7.1}/MANIFEST.in +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/color_drive.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/repeat_mission.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/scored_mission.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/unified_chassis_dims.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/wall_avoid.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander_hardware_style.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander_with_gyro.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/examples/wro_elementary_walkthrough.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/motor_process_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/motor_process_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/observer_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/observer_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/servo_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/servo_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/st_move_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/st_move_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/trajectory_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/trajectory_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/native/openbricks_sim_native.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/SOURCES.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/dependency_links.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/entry_points.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/requires.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/top_level.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/__main__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_nus.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_ports.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_signing.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/docs.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/log.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/mpycompile.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/pasteprobe.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/scan.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/servo_id.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/stop.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/upload.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/__init__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/__main__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/chassis.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/lego_mjcf.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/randomization.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/robot.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/runtime.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/shim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/world.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_line/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_walls/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_walls/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_zones/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_zones/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/amplifier.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/cable.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/clef.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/congas.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/guitar.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/keyboard.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/microphone.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_black.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_blue.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_green.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_red.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_white.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_yellow.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/speaker.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/artefact.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/barrier.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/dirt.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/parrot.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/red_tower.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/visitor.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/yellow_tower.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mosaic_frame.stl +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_bowl.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_element.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/masonry_trowel.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/mosaic_tile.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/rect_trowel.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_black_red.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_red_blue.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/pyproject.toml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/setup.cfg +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/setup.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_chassis.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_cibuildwheel_config.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_color_sensor.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_distance_sensor.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_docs_cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_imu.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_lego_mjcf.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_line_follow_sim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_log.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_mpycompile.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_drivebase.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_motor_process.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_observer.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_servo.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_trajectory.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_no_stale_names.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_nus.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_pasteprobe.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_project_urls.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_randomization.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_release_tags.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_robot.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_runtime.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_scan.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_sdist_build.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_servo_id.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_shim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_signing.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_sim_cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_stop.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_upload.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_wheel_bundles_worlds.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_world.py +0 -0
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
Versions the unified `openbricks` PyPI package (CLI + MuJoCo sim).
|
|
4
4
|
Firmware versions are tracked separately on the `v*` tag namespace.
|
|
5
5
|
|
|
6
|
+
## 2.7.1 — the integral cancels the FF deficit during the move
|
|
7
|
+
|
|
8
|
+
The end-of-run stutter, root-caused with 2.6.2's per-axis stats:
|
|
9
|
+
the forward axis expired 100 wheel-deg (~77 mm) behind with the
|
|
10
|
+
integral supplying 51 dps of a ~250 dps feed-forward deficit — all
|
|
11
|
+
three landings spent walking the robot in, pulse by pulse. pbio's
|
|
12
|
+
integral only grows NEAR the target (remaining-distance band); a
|
|
13
|
+
plant trailing 100 deg entered that band 0.16 s before expiry.
|
|
14
|
+
Their design assumes a factory-calibrated feed-forward; ours isn't.
|
|
15
|
+
|
|
16
|
+
* Integral growth now gates on TRACKING error (deadzone up to the
|
|
17
|
+
P-linear region): it engages from move start and cancels the
|
|
18
|
+
deficit during the motion. Harness: expiry residual 32.8 -> 11.5
|
|
19
|
+
wheel-deg, integral carrying 420 dps at expiry.
|
|
20
|
+
* Standstill now reads MEASURED axis speed (EMA of the position
|
|
21
|
+
derivative, glitch-clamped) — pbio's actual signal; command- and
|
|
22
|
+
ff-based checks each failed a real case.
|
|
23
|
+
* The integral retires by a ~35 ms bleed after done instead of an
|
|
24
|
+
instant zero (which was itself a command step).
|
|
25
|
+
|
|
26
|
+
Both sides move: flash 2.7.1 AND `pipx upgrade openbricks`.
|
|
27
|
+
|
|
28
|
+
## 2.7.0 — run is upload-then-run; flash --with-qtr-init
|
|
29
|
+
|
|
30
|
+
Two CLI changes, both user-requested:
|
|
31
|
+
|
|
32
|
+
* **`run` stages to the button slot** (reversing the 1.97.0 run-slot
|
|
33
|
+
split, deliberately different from Pybricks): the script lands at
|
|
34
|
+
`/program.mpy` before executing, so a run that fails midway stays
|
|
35
|
+
rerunnable with the start button — no BLE round trip. The accepted
|
|
36
|
+
price: one-shot tools (calibrations, diagnostics) replace the
|
|
37
|
+
button program; re-upload your mission afterwards.
|
|
38
|
+
* **`flash --with-qtr-init`** stores a starter QTR calibration at
|
|
39
|
+
`/qtr.cal` (reference-bench sweep, default pins 1-10, the exact
|
|
40
|
+
`save_calibration` format) so line-follow examples work on a
|
|
41
|
+
fresh hub. Re-run `examples/qtr_calibrate.py` for your own mat
|
|
42
|
+
and lighting.
|
|
43
|
+
|
|
44
|
+
CLI-side: `pipx upgrade openbricks` (no reflash needed).
|
|
45
|
+
|
|
6
46
|
## 2.6.2 — done requires standstill; per-axis settle diagnostics
|
|
7
47
|
|
|
8
48
|
Bench (the twitch that survived 2.6.0, now measured): the final
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbricks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: openbricks — host-side CLI + MuJoCo-backed simulator for the openbricks MicroPython firmware. Flash, list, run, upload, log; ``openbricks sim`` for the physics simulator.
|
|
5
5
|
Author: openbricks contributors
|
|
6
6
|
License: MIT
|
|
@@ -57,6 +57,11 @@ void ob_drivebase_init(ob_drivebase_t *db,
|
|
|
57
57
|
db->integ_diff = 0.0;
|
|
58
58
|
db->ki = (ob_float_t)OB_DRIVEBASE_DEFAULT_KI;
|
|
59
59
|
db->last_tick_ms = 0;
|
|
60
|
+
db->meas_vel_sum = 0.0;
|
|
61
|
+
db->meas_vel_diff = 0.0;
|
|
62
|
+
db->prev_sum_pos = 0.0;
|
|
63
|
+
db->prev_diff_pos = 0.0;
|
|
64
|
+
db->have_prev_pos = false;
|
|
60
65
|
db->landings = 0;
|
|
61
66
|
db->landing_active = false;
|
|
62
67
|
db->landing_best_err = 0.0;
|
|
@@ -383,17 +388,25 @@ void ob_drivebase_stop(ob_drivebase_t *db) {
|
|
|
383
388
|
// ---------------------------------------------------------------------
|
|
384
389
|
// Per-tick control law
|
|
385
390
|
|
|
386
|
-
// Position-integral update
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
391
|
+
// Position-integral update (2.7.x): decreasing the magnitude is
|
|
392
|
+
// always allowed; growth happens whenever the TRACKING error is in
|
|
393
|
+
// the band DEADZONE <= |err| <= (P-linear region), is rate-capped
|
|
394
|
+
// per tick, and the total clamps at the value whose ki-term alone
|
|
395
|
+
// commands ACTUATION_MAX. Deliberate deviation from pbio's
|
|
396
|
+
// remaining-distance band (2026-08-17, bench-measured): their
|
|
397
|
+
// near-target-only integral assumes a factory-calibrated
|
|
398
|
+
// feed-forward; ours under-delivered ~250 dps on the reference
|
|
399
|
+
// curve, the plant ran ~100 wheel-deg behind the reference the
|
|
400
|
+
// whole move, and it entered pbio's band only 0.16 s before expiry
|
|
401
|
+
// — integral at 51 dps of a 250 dps deficit, three landings, the
|
|
402
|
+
// end-of-run stutter. Gating on tracking error engages the
|
|
403
|
+
// integral from move start, so it cancels the FF deficit DURING
|
|
404
|
+
// the motion and the reference arrives with the plant on it. The
|
|
405
|
+
// deadzone still stops stiction hunting at rest; the band's upper
|
|
406
|
+
// edge (P saturation) still refuses to wind against a hard stall —
|
|
407
|
+
// the fault latch and watchdog own that case.
|
|
394
408
|
static ob_float_t db_integ_update(ob_float_t *integ,
|
|
395
409
|
ob_float_t err,
|
|
396
|
-
ob_float_t target_err,
|
|
397
410
|
ob_float_t dt_s,
|
|
398
411
|
ob_float_t kp,
|
|
399
412
|
ob_float_t ki) {
|
|
@@ -413,13 +426,12 @@ static ob_float_t db_integ_update(ob_float_t *integ,
|
|
|
413
426
|
mag_next = (next < 0) ? -next : next;
|
|
414
427
|
decrease = mag_next < mag_cur;
|
|
415
428
|
}
|
|
416
|
-
ob_float_t
|
|
429
|
+
ob_float_t ea = (err < 0) ? -err : err;
|
|
417
430
|
ob_float_t upper = (kp > 0)
|
|
418
431
|
? ((ob_float_t)OB_DRIVEBASE_ACTUATION_MAX_DPS / kp)
|
|
419
|
-
* (ob_float_t)2.0
|
|
420
432
|
: (ob_float_t)0.0;
|
|
421
|
-
if ((
|
|
422
|
-
&&
|
|
433
|
+
if ((ea >= (ob_float_t)OB_DRIVEBASE_INTEG_DEADZONE_WHEEL_DEG
|
|
434
|
+
&& ea <= upper) || decrease) {
|
|
423
435
|
*integ = next;
|
|
424
436
|
}
|
|
425
437
|
if (ki > 0) {
|
|
@@ -512,6 +524,32 @@ void ob_drivebase_tick(ob_drivebase_t *db, long now_ms) {
|
|
|
512
524
|
? db->heading_override_wheel_deg
|
|
513
525
|
: db_diff_pos_encoder(db);
|
|
514
526
|
|
|
527
|
+
// 3b. Measured axis speeds: EMA of the position derivative
|
|
528
|
+
// (~20 ms window at the 1 kHz tick — smooths the 1-count
|
|
529
|
+
// quantization step, fast enough for a 60 dps threshold).
|
|
530
|
+
if (dt_s > (ob_float_t)0.0) {
|
|
531
|
+
if (db->have_prev_pos) {
|
|
532
|
+
// Clamp each raw sample to a physically plausible bound:
|
|
533
|
+
// a heading-frame jump (gyro re-base, injected test
|
|
534
|
+
// heading) is a position STEP whose derivative would
|
|
535
|
+
// poison the EMA for ~100 ms.
|
|
536
|
+
ob_float_t vmax = (ob_float_t)OB_DRIVEBASE_ACTUATION_MAX_DPS
|
|
537
|
+
* (ob_float_t)4.0;
|
|
538
|
+
ob_float_t rs = (sum_pos - db->prev_sum_pos) / dt_s;
|
|
539
|
+
ob_float_t rd = (diff_pos - db->prev_diff_pos) / dt_s;
|
|
540
|
+
if (rs > vmax) { rs = vmax; }
|
|
541
|
+
if (rs < -vmax) { rs = -vmax; }
|
|
542
|
+
if (rd > vmax) { rd = vmax; }
|
|
543
|
+
if (rd < -vmax) { rd = -vmax; }
|
|
544
|
+
ob_float_t a = (ob_float_t)0.1;
|
|
545
|
+
db->meas_vel_sum += a * (rs - db->meas_vel_sum);
|
|
546
|
+
db->meas_vel_diff += a * (rd - db->meas_vel_diff);
|
|
547
|
+
}
|
|
548
|
+
db->prev_sum_pos = sum_pos;
|
|
549
|
+
db->prev_diff_pos = diff_pos;
|
|
550
|
+
db->have_prev_pos = true;
|
|
551
|
+
}
|
|
552
|
+
|
|
515
553
|
// 4. Coupled P + feedforward.
|
|
516
554
|
ob_float_t sum_err = fwd_target - sum_pos;
|
|
517
555
|
ob_float_t diff_err = turn_target - diff_pos;
|
|
@@ -531,26 +569,24 @@ void ob_drivebase_tick(ob_drivebase_t *db, long now_ms) {
|
|
|
531
569
|
ob_float_t de_now = db->turn_hold - diff_pos;
|
|
532
570
|
se_now = (se_now < 0) ? -se_now : se_now;
|
|
533
571
|
de_now = (de_now < 0) ? -de_now : de_now;
|
|
534
|
-
// ...and pbio's STANDSTILL condition
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
572
|
+
// ...and pbio's STANDSTILL condition on MEASURED speed —
|
|
573
|
+
// theirs tests the plant, not the command. A command-based
|
|
574
|
+
// check deadlocks: a wound integral commands hundreds of
|
|
575
|
+
// dps into an arrived robot, and with error ~0 it never
|
|
576
|
+
// unwinds; an ff-based check let the same integral latch
|
|
577
|
+
// done with the wheels genuinely turning.
|
|
578
|
+
ob_float_t mv_sum = (db->meas_vel_sum < 0)
|
|
579
|
+
? -db->meas_vel_sum : db->meas_vel_sum;
|
|
580
|
+
ob_float_t mv_diff = (db->meas_vel_diff < 0)
|
|
581
|
+
? -db->meas_vel_diff : db->meas_vel_diff;
|
|
540
582
|
if (se_now < (ob_float_t)OB_DRIVEBASE_DONE_TOL_WHEEL_DEG
|
|
541
583
|
&& de_now < (ob_float_t)OB_DRIVEBASE_DONE_TOL_WHEEL_DEG
|
|
542
|
-
&&
|
|
543
|
-
&&
|
|
584
|
+
&& mv_sum < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS
|
|
585
|
+
&& mv_diff < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS) {
|
|
544
586
|
db->fwd_active = false;
|
|
545
587
|
db->turn_active = false;
|
|
546
588
|
db->landing_active = false;
|
|
547
589
|
db->done = true;
|
|
548
|
-
// Arrived: the integral's job is over. Left wound, it
|
|
549
|
-
// keeps commanding ki*integ against ZERO error through
|
|
550
|
-
// the inter-move hold — the probe measured an arrived
|
|
551
|
-
// robot still pushed at 204 dps.
|
|
552
|
-
db->integ_sum = 0.0;
|
|
553
|
-
db->integ_diff = 0.0;
|
|
554
590
|
}
|
|
555
591
|
}
|
|
556
592
|
|
|
@@ -692,38 +728,38 @@ void ob_drivebase_tick(ob_drivebase_t *db, long now_ms) {
|
|
|
692
728
|
// beyond the forgive limit is a move that DIDN'T HAPPEN
|
|
693
729
|
// — done stays false and the caller's watchdog raises
|
|
694
730
|
// loudly.
|
|
731
|
+
ob_float_t amv_sum = (db->meas_vel_sum < 0)
|
|
732
|
+
? -db->meas_vel_sum : db->meas_vel_sum;
|
|
733
|
+
ob_float_t amv_diff = (db->meas_vel_diff < 0)
|
|
734
|
+
? -db->meas_vel_diff : db->meas_vel_diff;
|
|
695
735
|
bool arrived =
|
|
696
736
|
se < (ob_float_t)OB_DRIVEBASE_DONE_TOL_WHEEL_DEG
|
|
697
|
-
&& de < (ob_float_t)OB_DRIVEBASE_DONE_TOL_WHEEL_DEG
|
|
737
|
+
&& de < (ob_float_t)OB_DRIVEBASE_DONE_TOL_WHEEL_DEG
|
|
738
|
+
&& amv_sum < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS
|
|
739
|
+
&& amv_diff < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS;
|
|
698
740
|
bool capped = (now_ms - db->settle_start_ms)
|
|
699
741
|
>= (long)OB_DRIVEBASE_SETTLE_MS
|
|
700
742
|
&& worst < (ob_float_t)
|
|
701
743
|
OB_DRIVEBASE_SETTLE_FORGIVE_WHEEL_DEG;
|
|
702
744
|
if (arrived || capped) {
|
|
703
745
|
db->done = true;
|
|
704
|
-
db->integ_sum = 0.0; // see the arrival-cut latch
|
|
705
|
-
db->integ_diff = 0.0;
|
|
706
746
|
}
|
|
707
747
|
}
|
|
708
748
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
* ((db->turn.distance < 0) ? -db->turn.distance : db->turn.distance)
|
|
720
|
-
: db->turn_hold;
|
|
749
|
+
if (db->done) {
|
|
750
|
+
// Post-move hold: retire the integral SMOOTHLY (~35 ms
|
|
751
|
+
// half-life). An instant zero at the done latch was itself
|
|
752
|
+
// a command step; left wound instead, it pushed an arrived
|
|
753
|
+
// robot at 204 dps through the inter-move hold. The bleed
|
|
754
|
+
// is both: shaped on the way out, gone within a tenth of a
|
|
755
|
+
// second.
|
|
756
|
+
db->integ_sum *= (ob_float_t)0.98;
|
|
757
|
+
db->integ_diff *= (ob_float_t)0.98;
|
|
758
|
+
}
|
|
721
759
|
ob_float_t integ_sum = db_integ_update(&db->integ_sum, sum_err,
|
|
722
|
-
|
|
723
|
-
db->kp_sum, db->ki);
|
|
760
|
+
dt_s, db->kp_sum, db->ki);
|
|
724
761
|
ob_float_t integ_diff = db_integ_update(&db->integ_diff, diff_err,
|
|
725
|
-
|
|
726
|
-
db->kp_diff, db->ki);
|
|
762
|
+
dt_s, db->kp_diff, db->ki);
|
|
727
763
|
|
|
728
764
|
ob_float_t fwd_cmd = fwd_ff_vel + db->kp_sum * sum_err
|
|
729
765
|
+ db->ki * integ_sum;
|
|
@@ -164,6 +164,18 @@ typedef struct {
|
|
|
164
164
|
ob_float_t ki;
|
|
165
165
|
long last_tick_ms; // for the integral dt (0 = none yet)
|
|
166
166
|
|
|
167
|
+
// Measured axis speeds (2.7.x) — EMA of the position derivative,
|
|
168
|
+
// wheel-deg/s. pbio's standstill condition tests the PLANT's
|
|
169
|
+
// speed, not the command: a wound integral can command hundreds
|
|
170
|
+
// of dps into a robot that is demonstrably at rest, and a
|
|
171
|
+
// command-based check then deadlocks done (the integral only
|
|
172
|
+
// unwinds on opposing error, and error is ~0 at arrival).
|
|
173
|
+
ob_float_t meas_vel_sum;
|
|
174
|
+
ob_float_t meas_vel_diff;
|
|
175
|
+
ob_float_t prev_sum_pos;
|
|
176
|
+
ob_float_t prev_diff_pos;
|
|
177
|
+
bool have_prev_pos;
|
|
178
|
+
|
|
167
179
|
// Landing settle bookkeeping (2.6.0). ``expiry_residual`` is the
|
|
168
180
|
// worst-axis error captured at the move's FIRST profile expiry —
|
|
169
181
|
// the bench-measurable answer to "how big is the gap the settle
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbricks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: openbricks — host-side CLI + MuJoCo-backed simulator for the openbricks MicroPython firmware. Flash, list, run, upload, log; ``openbricks sim`` for the physics simulator.
|
|
5
5
|
Author: openbricks contributors
|
|
6
6
|
License: MIT
|
|
Binary file
|
|
@@ -77,6 +77,14 @@ def _build_parser():
|
|
|
77
77
|
"--baud", default="460800",
|
|
78
78
|
help="esptool flash baud rate. Default: 460800.",
|
|
79
79
|
)
|
|
80
|
+
p_flash.add_argument(
|
|
81
|
+
"--with-qtr-init", action="store_true",
|
|
82
|
+
help="After flashing, store a starter QTR line-sensor "
|
|
83
|
+
"calibration at /qtr.cal (recorded on the reference "
|
|
84
|
+
"bench, default pins 1-10) so line-follow examples work "
|
|
85
|
+
"out of the box. Re-run examples/qtr_calibrate.py for a "
|
|
86
|
+
"calibration measured on your own mat and lighting.",
|
|
87
|
+
)
|
|
80
88
|
p_flash.add_argument(
|
|
81
89
|
"--skip-erase", action="store_true",
|
|
82
90
|
help="Skip erase_flash (faster dev loop; leaves stale NVS keys).",
|
|
@@ -364,6 +364,41 @@ def _wait_for_repl(mpremote, port, timeout_s=20):
|
|
|
364
364
|
)
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
# Starter QTR calibration shipped by ``--with-qtr-init`` — a real
|
|
368
|
+
# bench sweep (2026-08-17, QTRX-HD-15A on the WRO mat, default pins
|
|
369
|
+
# 1-10; the per-element spans from that run's log). Format matches
|
|
370
|
+
# ``QTRArray.save_calibration``: {"pins", "min", "max"}. A starter
|
|
371
|
+
# file gets a fresh flash line-following immediately; heights, mats
|
|
372
|
+
# and lighting differ, so examples/qtr_calibrate.py remains the way
|
|
373
|
+
# to a calibration measured on YOUR rig.
|
|
374
|
+
_QTR_CAL_PATH = "/qtr.cal"
|
|
375
|
+
_QTR_STARTER_CAL = {
|
|
376
|
+
"pins": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
377
|
+
"min": [9346, 4048, 3888, 3728, 3984, 3888, 3824, 3968, 4593, 8658],
|
|
378
|
+
"max": [44922, 37849, 36024, 35816, 36056, 36536, 32888, 36552,
|
|
379
|
+
40057, 43834],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _write_qtr_starter_cal(mpremote, port):
|
|
384
|
+
"""Store the starter QTR calibration on the hub filesystem and
|
|
385
|
+
verify it parses back — a corrupt or truncated write must fail
|
|
386
|
+
HERE, not as a RuntimeError in the user's first line-follow."""
|
|
387
|
+
import json
|
|
388
|
+
payload = json.dumps(_QTR_STARTER_CAL)
|
|
389
|
+
snippet = (
|
|
390
|
+
"f = open(%r, 'w'); f.write(%r); f.close(); "
|
|
391
|
+
"import json; d = json.load(open(%r)); "
|
|
392
|
+
"print('qtr-cal-ok', len(d['min']), len(d['max']))"
|
|
393
|
+
) % (_QTR_CAL_PATH, payload, _QTR_CAL_PATH)
|
|
394
|
+
rc, out, err = _mpremote_exec(mpremote, port, snippet)
|
|
395
|
+
if rc != 0 or "qtr-cal-ok 10 10" not in out:
|
|
396
|
+
_die("failed to write starter QTR calibration:\n" + (err or out))
|
|
397
|
+
print("starter QTR calibration written to %s (re-run "
|
|
398
|
+
"examples/qtr_calibrate.py for your own mat/lighting)"
|
|
399
|
+
% _QTR_CAL_PATH)
|
|
400
|
+
|
|
401
|
+
|
|
367
402
|
def _write_hub_name(mpremote, port, name):
|
|
368
403
|
# The name already passed validation in ``_validate_name``; plain
|
|
369
404
|
# bytes literal is safe.
|
|
@@ -645,6 +680,9 @@ def run(args):
|
|
|
645
680
|
_die("verification failed: wrote %r, read back %r" % (args.name, readback))
|
|
646
681
|
print("hub name %r written and verified" % readback)
|
|
647
682
|
|
|
683
|
+
if getattr(args, "with_qtr_init", False):
|
|
684
|
+
_write_qtr_starter_cal(mpremote, args.port)
|
|
685
|
+
|
|
648
686
|
flashed_version = _write_fw_marker(mpremote, args.port,
|
|
649
687
|
firmware_verdict)
|
|
650
688
|
|
|
@@ -59,16 +59,15 @@ _TARGET_PATH = "/program.py"
|
|
|
59
59
|
# removes the other — see _compose_runner(remove_stale=...).
|
|
60
60
|
_MPY_TARGET_PATH = "/program.mpy"
|
|
61
61
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
62
|
+
# ``run`` stages to the BUTTON slot (/program.*) — an upload-then-run
|
|
63
|
+
# (user decision 2026-08-17, reversing the 1.97.0 run-slot split and
|
|
64
|
+
# deliberately different from Pybricks): a run that fails mid-way
|
|
65
|
+
# leaves the program on the hub, so the start button reruns it
|
|
66
|
+
# without another BLE round trip. The accepted price is the 1.97.0
|
|
67
|
+
# hazard returning: running a calibration or diagnostic script
|
|
68
|
+
# replaces the button program (bench 2026-08-14: upload aa.py, run
|
|
66
69
|
# qtr_calibrate, press start -> the robot invisibly re-ran the
|
|
67
|
-
# calibration
|
|
68
|
-
# never reads /run.*, so the two intents can no longer clobber
|
|
69
|
-
# each other.
|
|
70
|
-
_RUN_TARGET_PATH = "/run.py"
|
|
71
|
-
_RUN_MPY_TARGET_PATH = "/run.mpy"
|
|
70
|
+
# calibration) — re-upload your mission after one-shot tools.
|
|
72
71
|
|
|
73
72
|
# First firmware whose launcher runs /program.mpy (native exec_mpy).
|
|
74
73
|
_MIN_MPY_FIRMWARE = (1, 92, 0)
|
|
@@ -744,7 +743,7 @@ async def _run_session(link, name, user_bytes, mpy_bytes):
|
|
|
744
743
|
await _enter_raw_repl(blink, link)
|
|
745
744
|
try:
|
|
746
745
|
target, use_mpy, remove_stale = await _pick_staging(
|
|
747
|
-
blink, link
|
|
746
|
+
blink, link)
|
|
748
747
|
payload = mpy_bytes if use_mpy else user_bytes
|
|
749
748
|
runner = _compose_runner(target, remove_stale)
|
|
750
749
|
await _stage_file(blink, link, target, payload, name)
|
|
@@ -232,6 +232,8 @@ class DispatchTests(unittest.TestCase):
|
|
|
232
232
|
("stop", ["stop", "-n", "X"]),
|
|
233
233
|
("log", ["log", "-n", "X"]),
|
|
234
234
|
("servo_id", ["servo-id", "-p", "/p", "3"]),
|
|
235
|
+
("pasteprobe", ["paste-probe", "-n", "X"]),
|
|
236
|
+
("docs", ["docs"]),
|
|
235
237
|
]
|
|
236
238
|
for mod_name, argv in cases:
|
|
237
239
|
seen.clear()
|
|
@@ -524,6 +524,37 @@ class RunHappyPathTests(unittest.TestCase):
|
|
|
524
524
|
stderr=""),
|
|
525
525
|
])
|
|
526
526
|
|
|
527
|
+
def test_full_flow_with_qtr_init_writes_the_cal(self):
|
|
528
|
+
# --with-qtr-init inserts one mpremote exec between the name
|
|
529
|
+
# readback and the fw marker: the /qtr.cal write+verify.
|
|
530
|
+
snippets = []
|
|
531
|
+
|
|
532
|
+
def _fake_call(cmd):
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
run_responses = iter([
|
|
536
|
+
MagicMock(returncode=1, stdout="", stderr="no repl"),
|
|
537
|
+
MagicMock(returncode=0, stdout="ok\n", stderr=""),
|
|
538
|
+
MagicMock(returncode=0, stdout="wrote: 'RobotA'\n", stderr=""),
|
|
539
|
+
MagicMock(returncode=0, stdout="RobotA\n", stderr=""),
|
|
540
|
+
MagicMock(returncode=0, stdout="qtr-cal-ok 10 10\n", stderr=""),
|
|
541
|
+
MagicMock(returncode=0, stdout="9.9.9\n", stderr=""),
|
|
542
|
+
MagicMock(returncode=0, stdout="marker: 9.9.9:customized\n",
|
|
543
|
+
stderr=""),
|
|
544
|
+
])
|
|
545
|
+
|
|
546
|
+
def _fake_run(cmd, capture_output=True, text=True, timeout=None):
|
|
547
|
+
snippets.append(cmd[-1])
|
|
548
|
+
return next(run_responses)
|
|
549
|
+
|
|
550
|
+
with patch("subprocess.call", side_effect=_fake_call), \
|
|
551
|
+
patch("subprocess.run", side_effect=_fake_run):
|
|
552
|
+
rc = flash.run(self._run_args(with_qtr_init=True))
|
|
553
|
+
|
|
554
|
+
self.assertEqual(rc, 0)
|
|
555
|
+
self.assertTrue(any("'/qtr.cal'" in sn for sn in snippets),
|
|
556
|
+
"qtr cal write never composed")
|
|
557
|
+
|
|
527
558
|
def test_full_flow_success(self):
|
|
528
559
|
# subprocess.call for erase_flash / write_flash / final reset.
|
|
529
560
|
call_history = []
|
|
@@ -703,6 +734,53 @@ class ToolMissingTests(unittest.TestCase):
|
|
|
703
734
|
self.assertIn("esptool not found", str(ctx.exception))
|
|
704
735
|
|
|
705
736
|
|
|
737
|
+
class QtrStarterCalTests(unittest.TestCase):
|
|
738
|
+
"""``flash --with-qtr-init`` stores the reference-bench starter
|
|
739
|
+
calibration at /qtr.cal in exactly the format
|
|
740
|
+
``QTRArray.save_calibration`` writes — pins list included, since
|
|
741
|
+
``load_calibration`` refuses a file recorded for other wiring."""
|
|
742
|
+
|
|
743
|
+
def test_snippet_writes_valid_cal_json(self):
|
|
744
|
+
import json as _json
|
|
745
|
+
captured = {}
|
|
746
|
+
|
|
747
|
+
def _fake_run(cmd, capture_output=True, text=True, timeout=None):
|
|
748
|
+
captured["snippet"] = cmd[-1]
|
|
749
|
+
return MagicMock(returncode=0,
|
|
750
|
+
stdout="qtr-cal-ok 10 10\n", stderr="")
|
|
751
|
+
|
|
752
|
+
with patch("subprocess.run", side_effect=_fake_run):
|
|
753
|
+
flash._write_qtr_starter_cal("mpremote", "/dev/ttyUSB0")
|
|
754
|
+
|
|
755
|
+
snippet = captured["snippet"]
|
|
756
|
+
self.assertIn("'/qtr.cal'", snippet)
|
|
757
|
+
# The staged payload is the repr of a JSON document with the
|
|
758
|
+
# save_calibration schema: 10 pins, 10 mins, 10 maxes, and
|
|
759
|
+
# every span positive (min < max) — a starter file that fails
|
|
760
|
+
# load_calibration would be worse than no file.
|
|
761
|
+
start = snippet.index("f.write(") + len("f.write(")
|
|
762
|
+
payload_repr = snippet[start:snippet.index("); f.close()")]
|
|
763
|
+
payload = eval(payload_repr) # repr of a str literal
|
|
764
|
+
data = _json.loads(payload)
|
|
765
|
+
self.assertEqual(data["pins"], list(range(1, 11)))
|
|
766
|
+
self.assertEqual(len(data["min"]), 10)
|
|
767
|
+
self.assertEqual(len(data["max"]), 10)
|
|
768
|
+
for lo, hi in zip(data["min"], data["max"]):
|
|
769
|
+
self.assertTrue(0 < lo < hi <= 65535, (lo, hi))
|
|
770
|
+
|
|
771
|
+
def test_verification_failure_dies(self):
|
|
772
|
+
def _fake_run(cmd, capture_output=True, text=True, timeout=None):
|
|
773
|
+
return MagicMock(returncode=0, stdout="garbage\n", stderr="")
|
|
774
|
+
|
|
775
|
+
with patch("subprocess.run", side_effect=_fake_run):
|
|
776
|
+
try:
|
|
777
|
+
flash._write_qtr_starter_cal("mpremote", "/p")
|
|
778
|
+
except flash.FlashError as e:
|
|
779
|
+
self.assertIn("starter QTR calibration", str(e))
|
|
780
|
+
else:
|
|
781
|
+
self.fail("must die on unverified write")
|
|
782
|
+
|
|
783
|
+
|
|
706
784
|
class NameWriteSnippetTests(unittest.TestCase):
|
|
707
785
|
"""The mpremote ``exec`` snippet must embed the name in a form that
|
|
708
786
|
``openbricks._read_hub_name`` will accept back (bytes via set_blob)."""
|
|
@@ -339,13 +339,13 @@ class RunFlowTests(unittest.TestCase):
|
|
|
339
339
|
|
|
340
340
|
self.assertEqual(rc, 0)
|
|
341
341
|
joined = b"".join(fake.writes)
|
|
342
|
-
# The SOURCE crossed the link, targeting the
|
|
343
|
-
# source path
|
|
344
|
-
# button
|
|
342
|
+
# The SOURCE crossed the link, targeting the BUTTON slot's
|
|
343
|
+
# source path (run = upload-then-run since 2.7.0, so a failed
|
|
344
|
+
# run stays button-rerunnable), and no sibling delete was
|
|
345
|
+
# composed on the source path.
|
|
345
346
|
self.assertIn(b"hello from hub", joined)
|
|
346
|
-
self.assertIn(b"'/
|
|
347
|
-
self.assertNotIn(b"'/
|
|
348
|
-
self.assertNotIn(b"'/program.mpy'", joined)
|
|
347
|
+
self.assertIn(b"'/program.py'", joined)
|
|
348
|
+
self.assertNotIn(b"'/run.py'", joined)
|
|
349
349
|
self.assertNotIn(b"os.remove", joined)
|
|
350
350
|
self.assertIn("predates precompiled", err.getvalue())
|
|
351
351
|
self.assertIn("1.91.1", err.getvalue())
|
|
@@ -367,10 +367,10 @@ class RunFlowTests(unittest.TestCase):
|
|
|
367
367
|
self.assertNotIn("===", out.getvalue())
|
|
368
368
|
|
|
369
369
|
joined = b"".join(fake.writes)
|
|
370
|
-
# Confirm the staging writes the
|
|
371
|
-
#
|
|
372
|
-
self.assertIn(b"'/
|
|
373
|
-
self.assertNotIn(b"'/
|
|
370
|
+
# Confirm the staging writes the BUTTON slot (run =
|
|
371
|
+
# upload-then-run since 2.7.0) and calls run_program on it.
|
|
372
|
+
self.assertIn(b"'/program.mpy'", joined)
|
|
373
|
+
self.assertNotIn(b"'/run.mpy'", joined)
|
|
374
374
|
self.assertIn(b"launcher.run_program", joined)
|
|
375
375
|
# Confirm the control-byte sequence of raw-paste mode.
|
|
376
376
|
self.assertIn(b"\x03\x03", joined) # Ctrl-C interrupt
|
|
@@ -522,11 +522,13 @@ class InlineCommandTests(unittest.TestCase):
|
|
|
522
522
|
self.assertIn(b"M\\x06", joined) # .mpy header inside the
|
|
523
523
|
self.assertNotIn(b"print('hello')", joined) # staged repr
|
|
524
524
|
self.assertIn(b"launcher.run_program", joined)
|
|
525
|
-
#
|
|
526
|
-
#
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
self.
|
|
525
|
+
# run stages to the BUTTON slot (2.7.0: upload-then-run, so
|
|
526
|
+
# a failed run stays rerunnable by start press) — the .mpy
|
|
527
|
+
# staging removes a stale source sibling so the launcher's
|
|
528
|
+
# source-wins resolution can't run old code.
|
|
529
|
+
self.assertIn(b"'/program.mpy'", joined)
|
|
530
|
+
self.assertNotIn(b"'/run.mpy'", joined)
|
|
531
|
+
self.assertIn(b"os.remove('/program.py')", joined)
|
|
530
532
|
|
|
531
533
|
|
|
532
534
|
def test_connect_failure_propagates_as_run_error(self):
|
|
@@ -857,7 +859,7 @@ class HostInterruptForwardingTests(unittest.TestCase):
|
|
|
857
859
|
return None
|
|
858
860
|
|
|
859
861
|
async def _stub_pick(blink, l, *slot_pair):
|
|
860
|
-
return run_mod.
|
|
862
|
+
return run_mod._TARGET_PATH, False, None
|
|
861
863
|
|
|
862
864
|
patches = [
|
|
863
865
|
("_enter_raw_repl", _raw_repl),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/mat.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/README.md
RENAMED
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mat.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/world.xml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|