openbricks 2.6.2__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openbricks-2.6.2 → openbricks-2.7.0}/CHANGELOG.md +18 -0
- {openbricks-2.6.2/openbricks.egg-info → openbricks-2.7.0}/PKG-INFO +1 -1
- {openbricks-2.6.2 → openbricks-2.7.0/openbricks.egg-info}/PKG-INFO +1 -1
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/__init__.py +1 -1
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/_docs/offline-docs.zip +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/cli.py +8 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/flash.py +38 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/run.py +9 -10
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_cli.py +2 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_flash.py +78 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_run.py +18 -16
- {openbricks-2.6.2 → openbricks-2.7.0}/MANIFEST.in +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/color_drive.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/repeat_mission.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/scored_mission.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/unified_chassis_dims.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/wall_avoid.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/wander.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/wander_hardware_style.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/wander_with_gyro.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/examples/wro_elementary_walkthrough.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/drivebase_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/drivebase_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/motor_process_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/motor_process_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/observer_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/observer_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/servo_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/servo_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/st_move_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/st_move_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/trajectory_core.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/_shared/trajectory_core.h +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/native/openbricks_sim_native.c +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks.egg-info/SOURCES.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks.egg-info/dependency_links.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks.egg-info/entry_points.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks.egg-info/requires.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks.egg-info/top_level.txt +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/__main__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/_nus.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/_ports.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/_signing.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/docs.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/log.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/mpycompile.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/pasteprobe.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/scan.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/servo_id.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/stop.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_dev/upload.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/__init__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/__main__.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/chassis.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/lego_mjcf.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/randomization.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/robot.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/runtime.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/shim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/world.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/practice_line/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/practice_walls/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/practice_walls/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/practice_zones/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/practice_zones/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/amplifier.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/cable.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/clef.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/congas.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/guitar.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/keyboard.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/microphone.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_black.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_blue.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_green.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_red.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_white.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_yellow.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/speaker.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/artefact.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/barrier.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/dirt.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/parrot.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/red_tower.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/visitor.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/yellow_tower.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/README.md +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mat.png +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mosaic_frame.stl +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_bowl.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_element.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/masonry_trowel.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/mosaic_tile.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/rect_trowel.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_black_red.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_red_blue.ldr +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/world.xml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/pyproject.toml +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/setup.cfg +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/setup.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_chassis.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_cibuildwheel_config.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_color_sensor.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_distance_sensor.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_docs_cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_imu.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_lego_mjcf.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_line_follow_sim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_log.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_mpycompile.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_native_drivebase.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_native_motor_process.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_native_observer.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_native_servo.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_native_trajectory.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_no_stale_names.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_nus.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_pasteprobe.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_project_urls.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_randomization.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_release_tags.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_robot.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_runtime.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_scan.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_sdist_build.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_servo_id.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_shim.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_signing.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_sim_cli.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_stop.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_upload.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_wheel_bundles_worlds.py +0 -0
- {openbricks-2.6.2 → openbricks-2.7.0}/tests/test_world.py +0 -0
|
@@ -3,6 +3,24 @@
|
|
|
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.0 — run is upload-then-run; flash --with-qtr-init
|
|
7
|
+
|
|
8
|
+
Two CLI changes, both user-requested:
|
|
9
|
+
|
|
10
|
+
* **`run` stages to the button slot** (reversing the 1.97.0 run-slot
|
|
11
|
+
split, deliberately different from Pybricks): the script lands at
|
|
12
|
+
`/program.mpy` before executing, so a run that fails midway stays
|
|
13
|
+
rerunnable with the start button — no BLE round trip. The accepted
|
|
14
|
+
price: one-shot tools (calibrations, diagnostics) replace the
|
|
15
|
+
button program; re-upload your mission afterwards.
|
|
16
|
+
* **`flash --with-qtr-init`** stores a starter QTR calibration at
|
|
17
|
+
`/qtr.cal` (reference-bench sweep, default pins 1-10, the exact
|
|
18
|
+
`save_calibration` format) so line-follow examples work on a
|
|
19
|
+
fresh hub. Re-run `examples/qtr_calibrate.py` for your own mat
|
|
20
|
+
and lighting.
|
|
21
|
+
|
|
22
|
+
CLI-side: `pipx upgrade openbricks` (no reflash needed).
|
|
23
|
+
|
|
6
24
|
## 2.6.2 — done requires standstill; per-axis settle diagnostics
|
|
7
25
|
|
|
8
26
|
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.0
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbricks
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
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
|
|
File without changes
|
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.0}/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.0}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/README.md
RENAMED
|
File without changes
|
{openbricks-2.6.2 → openbricks-2.7.0}/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.0}/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
|