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.
Files changed (141) hide show
  1. {openbricks-2.6.2 → openbricks-2.7.1}/CHANGELOG.md +40 -0
  2. {openbricks-2.6.2/openbricks.egg-info → openbricks-2.7.1}/PKG-INFO +1 -1
  3. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/drivebase_core.c +82 -46
  4. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/drivebase_core.h +12 -0
  5. {openbricks-2.6.2 → openbricks-2.7.1/openbricks.egg-info}/PKG-INFO +1 -1
  6. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/__init__.py +1 -1
  7. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_docs/offline-docs.zip +0 -0
  8. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/cli.py +8 -0
  9. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/flash.py +38 -0
  10. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/run.py +9 -10
  11. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_cli.py +2 -0
  12. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_flash.py +78 -0
  13. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_run.py +18 -16
  14. {openbricks-2.6.2 → openbricks-2.7.1}/MANIFEST.in +0 -0
  15. {openbricks-2.6.2 → openbricks-2.7.1}/README.md +0 -0
  16. {openbricks-2.6.2 → openbricks-2.7.1}/examples/color_drive.py +0 -0
  17. {openbricks-2.6.2 → openbricks-2.7.1}/examples/repeat_mission.py +0 -0
  18. {openbricks-2.6.2 → openbricks-2.7.1}/examples/scored_mission.py +0 -0
  19. {openbricks-2.6.2 → openbricks-2.7.1}/examples/unified_chassis_dims.py +0 -0
  20. {openbricks-2.6.2 → openbricks-2.7.1}/examples/wall_avoid.py +0 -0
  21. {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander.py +0 -0
  22. {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander_hardware_style.py +0 -0
  23. {openbricks-2.6.2 → openbricks-2.7.1}/examples/wander_with_gyro.py +0 -0
  24. {openbricks-2.6.2 → openbricks-2.7.1}/examples/wro_elementary_walkthrough.py +0 -0
  25. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/motor_process_core.c +0 -0
  26. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/motor_process_core.h +0 -0
  27. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/observer_core.c +0 -0
  28. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/observer_core.h +0 -0
  29. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/servo_core.c +0 -0
  30. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/servo_core.h +0 -0
  31. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/st_move_core.c +0 -0
  32. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/st_move_core.h +0 -0
  33. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/trajectory_core.c +0 -0
  34. {openbricks-2.6.2 → openbricks-2.7.1}/native/_shared/trajectory_core.h +0 -0
  35. {openbricks-2.6.2 → openbricks-2.7.1}/native/openbricks_sim_native.c +0 -0
  36. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/SOURCES.txt +0 -0
  37. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/dependency_links.txt +0 -0
  38. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/entry_points.txt +0 -0
  39. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/requires.txt +0 -0
  40. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks.egg-info/top_level.txt +0 -0
  41. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/__main__.py +0 -0
  42. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_nus.py +0 -0
  43. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_ports.py +0 -0
  44. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/_signing.py +0 -0
  45. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/docs.py +0 -0
  46. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/log.py +0 -0
  47. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/mpycompile.py +0 -0
  48. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/pasteprobe.py +0 -0
  49. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/scan.py +0 -0
  50. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/servo_id.py +0 -0
  51. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/stop.py +0 -0
  52. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_dev/upload.py +0 -0
  53. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/__init__.py +0 -0
  54. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/__main__.py +0 -0
  55. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/chassis.py +0 -0
  56. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/cli.py +0 -0
  57. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/lego_mjcf.py +0 -0
  58. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/randomization.py +0 -0
  59. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/robot.py +0 -0
  60. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/runtime.py +0 -0
  61. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/shim.py +0 -0
  62. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/world.py +0 -0
  63. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_line/world.xml +0 -0
  64. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_walls/README.md +0 -0
  65. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_walls/world.xml +0 -0
  66. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_zones/README.md +0 -0
  67. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/practice_zones/world.xml +0 -0
  68. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/README.md +0 -0
  69. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/mat.png +0 -0
  70. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/amplifier.ldr +0 -0
  71. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/cable.ldr +0 -0
  72. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/clef.ldr +0 -0
  73. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/congas.ldr +0 -0
  74. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/guitar.ldr +0 -0
  75. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/keyboard.ldr +0 -0
  76. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/microphone.ldr +0 -0
  77. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_black.ldr +0 -0
  78. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_blue.ldr +0 -0
  79. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_green.ldr +0 -0
  80. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_red.ldr +0 -0
  81. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_white.ldr +0 -0
  82. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/note_yellow.ldr +0 -0
  83. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/props/speaker.ldr +0 -0
  84. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_elementary_robot_rockstars/world.xml +0 -0
  85. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/README.md +0 -0
  86. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/mat.png +0 -0
  87. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/artefact.ldr +0 -0
  88. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/barrier.ldr +0 -0
  89. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/dirt.ldr +0 -0
  90. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/parrot.ldr +0 -0
  91. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/red_tower.ldr +0 -0
  92. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/visitor.ldr +0 -0
  93. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/props/yellow_tower.ldr +0 -0
  94. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_junior_heritage_heroes/world.xml +0 -0
  95. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/README.md +0 -0
  96. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mat.png +0 -0
  97. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/mosaic_frame.stl +0 -0
  98. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_bowl.ldr +0 -0
  99. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/cement_element.ldr +0 -0
  100. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/masonry_trowel.ldr +0 -0
  101. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/mosaic_tile.ldr +0 -0
  102. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/rect_trowel.ldr +0 -0
  103. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_black_red.ldr +0 -0
  104. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/props/senior_barrier_red_blue.ldr +0 -0
  105. {openbricks-2.6.2 → openbricks-2.7.1}/openbricks_sim/worlds/wro_2026_senior_mosaic_masters/world.xml +0 -0
  106. {openbricks-2.6.2 → openbricks-2.7.1}/pyproject.toml +0 -0
  107. {openbricks-2.6.2 → openbricks-2.7.1}/setup.cfg +0 -0
  108. {openbricks-2.6.2 → openbricks-2.7.1}/setup.py +0 -0
  109. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_chassis.py +0 -0
  110. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_cibuildwheel_config.py +0 -0
  111. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_color_sensor.py +0 -0
  112. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_distance_sensor.py +0 -0
  113. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_docs_cli.py +0 -0
  114. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_imu.py +0 -0
  115. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_lego_mjcf.py +0 -0
  116. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_line_follow_sim.py +0 -0
  117. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_log.py +0 -0
  118. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_mpycompile.py +0 -0
  119. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_drivebase.py +0 -0
  120. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_motor_process.py +0 -0
  121. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_observer.py +0 -0
  122. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_servo.py +0 -0
  123. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_native_trajectory.py +0 -0
  124. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_no_stale_names.py +0 -0
  125. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_nus.py +0 -0
  126. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_pasteprobe.py +0 -0
  127. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_project_urls.py +0 -0
  128. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_randomization.py +0 -0
  129. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_release_tags.py +0 -0
  130. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_robot.py +0 -0
  131. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_runtime.py +0 -0
  132. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_scan.py +0 -0
  133. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_sdist_build.py +0 -0
  134. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_servo_id.py +0 -0
  135. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_shim.py +0 -0
  136. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_signing.py +0 -0
  137. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_sim_cli.py +0 -0
  138. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_stop.py +0 -0
  139. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_upload.py +0 -0
  140. {openbricks-2.6.2 → openbricks-2.7.1}/tests/test_wheel_bundles_worlds.py +0 -0
  141. {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.6.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, pbio integrator.c rules (2.6.0):
387
- // decreasing the magnitude is always allowed; growth only happens
388
- // in the band DEADZONE <= |remaining to endpoint| <= 2*(actuation
389
- // saturation error), is rate-capped per tick, and the total clamps
390
- // at the value whose ki-term alone commands ACTUATION_MAX. The
391
- // deadzone stops stiction hunting at rest; the upper bound stops a
392
- // far-from-target cruise from winding the integral up; the clamp
393
- // bounds the authority a wound integral can ever have.
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 ta = (target_err < 0) ? -target_err : target_err;
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 ((ta >= (ob_float_t)OB_DRIVEBASE_INTEG_DEADZONE_WHEEL_DEG
422
- && ta <= upper) || decrease) {
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: the landing's own
535
- // feed-forward must be slow before done may latch, or a
536
- // plant crossing the window at speed gets its wheels
537
- // released mid-motion by the then= dispatch.
538
- ob_float_t vf = (fwd_ff_vel < 0) ? -fwd_ff_vel : fwd_ff_vel;
539
- ob_float_t vt = (turn_ff_vel < 0) ? -turn_ff_vel : turn_ff_vel;
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
- && vf < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS
543
- && vt < (ob_float_t)OB_DRIVEBASE_ARRIVAL_SPEED_TOL_DPS) {
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
- // Remaining-to-endpoint errors gate WHERE the integral may grow
710
- // (pbio's target_error): during cruise the endpoint is far and
711
- // the integral stays parked; through the decel ramp and settle
712
- // it engages and squeezes out the tracking lag P alone cannot.
713
- ob_float_t fwd_end = db->fwd_active
714
- ? db->fwd.start + db->fwd.direction
715
- * ((db->fwd.distance < 0) ? -db->fwd.distance : db->fwd.distance)
716
- : db->fwd_hold;
717
- ob_float_t turn_end = db->turn_active
718
- ? db->turn.start + db->turn.direction
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
- fwd_end - sum_pos, dt_s,
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
- turn_end - diff_pos, dt_s,
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.6.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
@@ -1,4 +1,4 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  """openbricks — host-side CLI for flashing and running code on hubs."""
3
3
 
4
- __version__ = "2.6.2"
4
+ __version__ = "2.7.1"
@@ -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
- # One-shot runs stage to their OWN slot (CLI >= 1.97.0): ``run`` used
63
- # to share /program.mpy with ``upload``, so every run — including
64
- # calibrations and diagnostics — silently replaced the program the
65
- # BUTTON launches. Bench 2026-08-14: upload aa.py, run
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 and the report was "button does nothing". The button
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, _RUN_MPY_TARGET_PATH, _RUN_TARGET_PATH)
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 run slot's
343
- # source path, and no sibling delete was composed. The
344
- # button's /program.* is never touched.
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"'/run.py'", joined)
347
- self.assertNotIn(b"'/program.py'", joined)
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 RUN slot and calls
371
- # run_program on it — never the button's /program.*.
372
- self.assertIn(b"'/run.mpy'", joined)
373
- self.assertNotIn(b"'/program.py'", joined)
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
- # One-shot runs stage to their OWN slot (1.97.0) — the
526
- # BUTTON's /program.mpy must never appear in a run session.
527
- self.assertIn(b"'/run.mpy'", joined)
528
- self.assertNotIn(b"'/program.mpy'", joined)
529
- self.assertNotIn(b"os.remove('/program.py')", joined)
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._RUN_TARGET_PATH, False, None
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