Skip to content

Refactor bus.py (2449 lines) — split the register map, the encoding, the real bus and the fake; stop shipping a 677-line test double in the runtime package #49

Description

@OriNachum

The state

arm101/hardware/bus.py is 2449 lines holding three classes and ~80 methods:

lines what it is
OverloadError 415– the error type
MotorBus 465– the abstract contract
FeetechBus 845– the real serial implementation
FakeBus 1773–2449 a test double — 677 lines, 28% of the file — shipping inside the runtime package

Plus the register map, the encoding helpers (encode_offset / decode_offset), the retry policy constants, and the EEPROM-settle constants, all at module scope.

Why now

Three separate hardware findings in the last two days each landed as another dense docstring in this one file, because there is nowhere else for them to go:

Each is correct and each is load-bearing. Together they have made the single most safety-critical file in the repo the hardest one to read.

The specific smell that prompted this

FakeBus is a test double in the shipped wheel. It exists because the runtime package has zero third-party dependencies and we wanted offline development — a good reason — but the consequence is that import arm101.hardware.bus pulls 677 lines of simulation into every production import, and the real/fake split is invisible at the module boundary.

It has also now demonstrably failed at its job: FakeBus/ServoModelBus could not model the reported-frame goal comparison, so an entire class of bug (the stale-goal lurch, #47) was invisible to the whole suite until tests/_rolling_servo.py::RollingServoBus was written. A fake that cannot reproduce the hardware's failure modes is not protecting the code that depends on it.

Suggested shape (not prescriptive)

  • bus/registers.py — the address map, widths, the EEPROM/SRAM boundary.
  • bus/encoding.pyencode_offset/decode_offset, the sign-magnitude rules, load_magnitude, is_overload.
  • bus/protocol.pyMotorBus (the contract) + OverloadError.
  • bus/feetech.py — the real serial bus and its retry/lock/settle policy.
  • bus/fake.py — the double. Consider moving it to tests/ and making the offline path an explicit, opt-in import, so the runtime never ships a simulation.

Whatever the split, the invariants must survive as tests, not as prose: tests/test_eeprom_limit_write_guard.py already pins the writable-register set ({5, 6, 31, 40, 41, 42, 46, 48, 55}) and the "no addr 9/11, ever" rule by parsing the module and resolving every write address statically. That test is the safety net that makes this refactor tractable — it will catch a register that moves or a write that appears.

The new asymmetry to preserve

write_offset now retries, verified by read-back — the only write in the file that retries at all. Every other write retries nothing, deliberately (#21, #38). That distinction is subtle and easy to flatten in a refactor: a blind write retry is how the Lock bug arose; a verified retry (read the register back, repeat only what the read proves is absent) is safe. Keep them visibly different.

Not urgent, and deliberately not now

Filed rather than done because it surfaced mid-hardware-run with the arm powered and PR #48 green. Restructuring the most safety-critical file in the repo at that moment would make a new bug indistinguishable from the one being chased.

Related: #21, #38, #43, #47

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions