Skip to content

Commit 074dd2a

Browse files
refactor: Move EEPROM layout to hal and update test structure
1 parent f622e06 commit 074dd2a

5 files changed

Lines changed: 99 additions & 110 deletions

File tree

PLAN.md

Lines changed: 86 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,85 @@ Approach: **hybrid** — extract pure logic into `core/` first, then test (inver
88

99
---
1010

11-
## Goal Architecture (target)
12-
13-
Five layers, dependencies point inward only. The **HAL** sits between domain ports and the actual hardware libraries, so swapping AccelStepper/TMCStepper/EEPROM/SSD1306/Wi-Fi never touches `core/` or `ports/`.
14-
15-
```
16-
+----------------------------------------------------------------+
17-
| app/ per-board composition root (main.cpp + setup |
18-
| | wiring, replaces the legacy .ino entry point) |
19-
| v |
20-
| adapters/ bind domain ports to HAL (and to non-HAL libs |
21-
| | such as TinyGPS data structs); thin glue. |
22-
| v |
23-
| hal/ Hardware Abstraction Layer — pure C++ interfaces |
24-
| | for physical components + per-platform backends: |
25-
| | IGpioPin, ISerialPort, ISpiBus, II2cBus, |
26-
| | IEeprom, IStepperMotor, ITmcDriver, IOledPanel,|
27-
| | ICharLcd, IButtonMatrix, ITimerService, |
28-
| | ISystemClock, IWifiStack. |
29-
| | Backends: hal/arduino/, hal/esp32/, hal/avr/. |
30-
| | Host-side fakes for unit tests live under |
31-
| | unit_tests/test_common/ (test code, not src/). |
32-
| v |
33-
| ports/ domain-level interfaces consumed by core: |
34-
| | IClock, ILogger, IPersistentStore, IStepperAxis, |
35-
| | IMotorDriver, IDisplay, IInfoDisplay, ITransport,|
36-
| | IHomingSensor, IEndSwitch, IGps. |
37-
| v |
38-
| core/ pure domain logic, no Arduino, host-testable: |
39-
| CoordinateMath, MountState, SlewController, |
40-
| GuidingController, TrackingController, |
41-
| ParkingController, HomingController, |
42-
| FocusController, SiderealClock, MeadeParser, |
43-
| MountConfig, EventBus. |
44-
+----------------------------------------------------------------+
11+
## Architecture
12+
13+
### Goals
14+
15+
| # | Goal | Rationale |
16+
|---|------|-----------|
17+
| 1 | **Make domain logic host-testable** | The `native` PIO env can't compile `src/` root files (they transitively include `<Arduino.h>` via `inc/Globals.hpp`). Pure logic must live in `src/core/` with zero Arduino deps so tests run in milliseconds, not on hardware. |
18+
| 2 | **Shrink the `Mount` god-object** | `Mount.cpp` is ~5000 LOC holding all domain behavior. End state: `Mount` is a thin facade (≤ 500 LOC) over focused controllers in `core/`. |
19+
| 3 | **Eliminate `#ifdef` from domain code** | Feature flags for axes, driver types, and sensors currently produce 2^N untested build combinations. These become runtime polymorphism behind interfaces — the composition root makes one choice; `core/` never branches. |
20+
| 4 | **Swap hardware without touching logic** | Changing stepper library, display type, or EEPROM backend must not require edits to `core/` or `ports/`. |
21+
| 5 | **Coverage that means something** | Target: `core/` ≥ 85% line coverage with tests that verify behavior (not tautologies). |
22+
23+
### Requirements & Constraints
24+
25+
| Category | Constraint | Why |
26+
|----------|-----------|-----|
27+
| **Hardware** | Must build & run on all 5 boards (AVR_MEGA2560, MKS_GEN_L_V1/V2/V21, ESP32_ESP32DEV) | Existing user base; CI matrix enforces this. |
28+
| **Memory** | AVR_MEGA2560 has 256 KB flash, 8 KB SRAM | Flash overflow is caught at link time. Mitigation: keep vtable count ≤ ~12, mark adapters `final`, consider template-based static dispatch as escape hatch. |
29+
| **C++ standard** | C++17 for `core/` (needs `etl::optional`, `if constexpr`); boards may require C++14 fallback with ETL polyfills | `native` env already uses `-std=gnu++17`. Board envs will be audited per phase. |
30+
| **Testability** | `core/` must never include `<Arduino.h>` or use `String`, `Serial`, `millis()`, `LOG()` | Enforced by CI grep check. `native` env only compiles `core/`, `ports/`, `adapters/`. |
31+
| **Back-compat** | Meade LX200 serial protocol behavior is invariant | External interfaces (Stellarium, ASCOM) must not break. Internal C++ APIs may change freely. |
32+
| **Shippable increments** | Every phase/step must be independently shippable — all boards green, all tests green | No flag days; thin overlays preserve old call-sites during transition. |
33+
| **Tooling** | PlatformIO + Googletest + FakeIt (bundled with ArduinoFake) + gcovr | Already configured in `platformio.ini` and CI (`ci.yml`). |
34+
35+
### Layers & Dependencies
36+
37+
Dependencies point inward only. No layer may reference a layer above it.
38+
39+
```mermaid
40+
graph TD
41+
APP["app/<br/>Composition Root"]
42+
ADAPTERS["adapters/<br/>Port → HAL glue"]
43+
PORTS["ports/<br/>Domain interfaces"]
44+
CORE["core/<br/>Pure domain logic"]
45+
HAL["hal/<br/>Hardware abstraction<br/>interfaces + backends"]
46+
HW["Hardware / Libraries<br/>Arduino, AccelStepper, TMCStepper,<br/>EEPROM, SSD1306, TinyGPS++"]
47+
48+
APP --> ADAPTERS
49+
APP --> HAL
50+
APP --> PORTS
51+
APP --> CORE
52+
ADAPTERS --> PORTS
53+
ADAPTERS --> HAL
54+
CORE --> PORTS
55+
HAL --> HW
56+
57+
style CORE fill:#e1f5fe
58+
style PORTS fill:#fff9c4
59+
style ADAPTERS fill:#dcedc8
60+
style HAL fill:#f3e5f5
61+
style APP fill:#ffe0b2
62+
style HW fill:#e0e0e0
4563
```
4664

47-
**HAL vs Ports — why both:**
48-
- `hal/` describes *what the hardware can do* (pin toggles, UART bytes, timer ticks). One backend per platform; one in-memory backend for tests.
49-
- `ports/` describes *what the domain needs* (axis position, persistent value, "now"). Adapters compose one or more HAL services to satisfy a port — e.g. `AccelStepperAxis` adapter implements `IStepperAxis` using `IStepperMotor` + `ITimerService` from HAL.
50-
51-
Cross-cutting:
52-
- **Configuration** becomes a runtime `MountConfig` struct populated at composition time from `Configuration.hpp` constants (single translation unit reads the macros). `#ifdef` no longer leaks into `core/` or `ports/`; HAL backend selection is the only place feature flags survive.
53-
- **Time** is a `IClock` port backed by `hal::ISystemClock`; `core/` never calls `millis()` directly.
54-
- **Logging** is an `ILogger` port backed by `hal::ISerialPort`; `core/` never includes `Serial`.
55-
- Optional axes (`AZ`, `ALT`, `Focus`) become `etl::optional<AxisController>` or null-object pattern — no `#ifdef` branches in controllers.
56-
57-
`Mount.cpp` ends up as a thin **facade** (≤ 500 LOC) over `core/` controllers, retained for Meade protocol back-compat; gradually deprecated.
65+
| Layer | Directory | Depends on | Contains | Test strategy |
66+
|-------|-----------|------------|----------|---------------|
67+
| **core/** | `src/core/` | `ports/` only | Domain controllers (`SlewController`, `TrackingController`, …), algorithms (`SiderealClock`, `CoordinateMath`, …), `MeadeParser`, `MountState`, `EventBus`, `MountConfig` | Pure unit tests with FakeIt-faked ports. No hardware, no Arduino. `native` env. |
68+
| **ports/** | `src/ports/` | nothing | Domain interfaces: `IClock`, `ILogger`, `IPersistentStore`, `IStepperAxis`, `IMotorDriver`, `IDisplay`, `IHomingSensor`, `IEndSwitch`, `IGps`, `ITransport` | Pure abstract interfaces — no implementation to test. |
69+
| **adapters/** | `src/adapters/` | `ports/` + `hal/` | Thin glue: `AccelStepperAxis`, `EepromPersistentStore`, `SerialLogger`, `Tmc2209Driver`, `HallHomingSensor`, `SerialTransport`, `WifiTransport`, `MeadeCommandAdapter` | Integration tests: adapter → faked HAL port. |
70+
| **hal/** | `src/hal/` | hardware libraries only | Interfaces (`IGpioPin`, `ISerialPort`, `IStepperMotor`, `ITmcDriver`, `ISystemClock`, `ITimerService`, …) + per-platform backends (`hal/arduino/`, `hal/avr/`, `hal/esp32/`) | Not host-tested (wraps hardware). Verified by on-device smoke tests. |
71+
| **app/** | `src/app/` | all layers | Composition root. Board-specific configuration comes from a header (e.g. `Configuration_local_oaeboardv1.hpp`, selected by the PIO environment) which populates a runtime `MountConfig` struct. Wires up the object graph. Replaces the legacy `.ino` entry point. | Verified by on-device smoke tests across all 5 boards. |
72+
73+
**Why both `hal/` and `ports/`:**
74+
- `hal/` describes *what the hardware can do* (pin toggles, UART bytes, timer ticks). One backend per platform.
75+
- `ports/` describes *what the domain needs* (axis position, persistent value, "now"). Adapters compose one or more HAL services to satisfy a port — e.g. `AccelStepperAxis` implements `IStepperAxis` using `IStepperMotor` + `ITimerService` from HAL.
76+
- This split means a new stepper library requires a new HAL backend, not a port change. A new domain requirement (e.g. "axis acceleration profile") requires a port change, not a HAL change.
77+
78+
### Cross-cutting rules
79+
80+
| Rule | Enforcement |
81+
|------|-------------|
82+
| `core/` never includes `<Arduino.h>`, `String`, `Serial`, `millis()`, `LOG()`, or any board pin header | CI grep check (Phase 4+) |
83+
| `core/` and `ports/` contain zero `#ifdef` for feature flags | CI grep check (Phase 4+) |
84+
| Time = `IClock` port (backed by `hal::ISystemClock`); no direct `millis()` | Interface contract |
85+
| Logging = `ILogger` port (backed by `hal::ISerialPort`); no direct `Serial.print()` | Interface contract |
86+
| Configuration = runtime `MountConfig` struct populated once in `app/` from `Configuration*.hpp` macros | Single translation unit reads macros |
87+
| Optional axes (AZ, ALT, Focus) = `etl::optional` or null-object pattern; no `#ifdef` branches | Composition root constructs or injects null |
88+
| ISR-safety: `IStepperAxis::Snapshot()` returns an ISR-safe state snapshot; mutation methods are main-loop-only | Settled in Phase 2 before Phase 3 controller extraction |
89+
| `Mount` becomes a thin facade (≤ 500 LOC) over `core/` controllers, retained for Meade back-compat, gradually deprecated | Shrinks each phase as controllers are extracted |
5890

5991
---
6092

@@ -111,7 +143,7 @@ The parser is pure and tested. The executor (`MeadeCommandProcessor`) is an adap
111143
### Phase 1 — Extract pure domain logic into `core/`, then test
112144
*The original plan called for testing before extraction. That doesn't work here: every `src/` root file includes `inc/Globals.hpp``<Arduino.h>`, and uses `String`/`LOG()`/Arduino APIs. The `native` PIO env only builds `core/`, `ports/`, `adapters/` — not `src/` root files. **New approach: extract pure subsets into `core/` first, then write exhaustive tests.** Each step is independently shippable. No behavior change.*
113145

114-
**Structure:** Data containers (`DayTime`, `Declination`, `Latitude`, `Longitude`) go under `src/core/types/`. Algorithm modules (`SiderealClock`, `CoordinateMath`, `CalendarMath`, `CoordinateFormatter`, `EepromLayout`) live at `src/core/` root. Mirrors the `core/meade/` subfolder convention.
146+
**Structure:** Data containers (`DayTime`, `Declination`, `Latitude`, `Longitude`) go under `src/core/types/`. Algorithm modules (`SiderealClock`, `CoordinateMath`, `CalendarMath`, `CoordinateFormatter`) live at `src/core/` root. EEPROM layout constants (`EepromLayout`) live in `src/hal/` — they describe a storage format, not domain behavior.
115147

116148
```
117149
src/core/
@@ -125,7 +157,6 @@ src/core/
125157
├── CoordinateMath.hpp/.cpp # algorithm consuming coordinate types
126158
├── CalendarMath.hpp/.cpp # pure date arithmetic
127159
├── CoordinateFormatter.hpp/.cpp # char-buffer formatting
128-
└── EepromLayout.hpp # struct layouts & validation constants
129160
```
130161

131162
The `src/` originals become **thin overlays**: they `#include` the `core/` version and add only Arduino-dependent methods (`ParseFromMeade`, `ToString`, `formatString`). This preserves back-compat — no flag day.
@@ -198,9 +229,9 @@ The ~200-line function computes stepper positions from RA/DEC targets. Core math
198229
`EPROMStore` uses Arduino EEPROM API directly. Data-validation logic (magic markers `0xCE`/`0xCF`, struct layouts) is separable.
199230

200231
**Plan:**
201-
1. Create `src/core/EepromLayout.hpp` — struct definitions and validation constants (no Arduino deps).
232+
1. Create `src/hal/EepromLayout.hpp` — struct definitions and validation constants (no Arduino deps; EEPROM layout is a storage format, not domain logic).
202233
2. Full `IPersistentStore` port + adapter comes in Phase 2.
203-
3. Write `test_core/test_eeprom_layout.cpp` — struct size checks, magic marker validation.
234+
3. Write `test_hal/test_eeprom_layout.cpp` — struct size checks, magic marker validation.
204235

205236
**Verify:** `pio test -e native -v` — all new tests pass; `pio run -e <board>` for all 5 boards builds green per step; `scripts/test-coverage.sh` shows ≥ 85% line coverage on new `core/` files; manual: mount slews to known coordinates on real hardware.
206237

@@ -235,7 +266,7 @@ The ~200-line function computes stepper positions from RA/DEC targets. Core math
235266
6. Replace direct `millis()`, `digitalWrite()`, `EEPROMStore::` calls inside `Mount` with port calls; replace `LOG()` macro with `_logger->log(...)`.
236267
7. **ISR contract for `IStepperAxis`:** This phase MUST settle the threading model. `Mount::interruptLoop()` is called from ISR context (AVR Timer ISR / ESP32 FreeRTOS task). The `IStepperAxis` interface must specify which methods are ISR-safe and which are main-loop-only. `Snapshot()` provides an ISR-safe state read. This contract is a prerequisite for Phase 3's `SlewController`.
237268

238-
**Verify:** All Phase 1 tests still green; new contract tests for each port using HAL fakes from `unit_tests/test_common/hal_fakes/` (e.g., `EepromPersistentStore` round-trips via an in-memory `FakeEeprom`); firmware builds all 5 boards (toolchain enforces flash limits).
269+
**Verify:** All Phase 1 tests still green; firmware builds all 5 boards (toolchain enforces flash limits).
239270

240271
### Phase 3 — Decompose `Mount` into controllers
241272
*Strangler-fig: move responsibilities out of `Mount` into `core/` controllers, one at a time. Mount becomes a facade.*
@@ -285,7 +316,7 @@ Each step: extract → add focused unit tests with FakeIt-faked ports → remove
285316
1. Mount facade slimmed to a thin compat shim (or removed if no external dependents).
286317
2. Move display-related code paths off the Mount → display direct call into the `EventBus`.
287318
3. Architecture doc (`docs/architecture.md`) with the layer diagram, port catalog, and "where to add a new feature" guide.
288-
4. Ratchet CI coverage gate to its final value (`core/` ≥ 85%, `adapters/` ≥ 40%).
319+
4. Ratchet CI coverage gate to its final value (`core/` ≥ 85%).
289320

290321
**Verify:** Architecture doc reviewed; CI gates final; full board matrix green; smoke-tested on at least one real mount.
291322

@@ -302,7 +333,7 @@ Each step: extract → add focused unit tests with FakeIt-faked ports → remove
302333
- [`src/Declination.cpp`](src/Declination.cpp), [`src/Latitude.cpp`](src/Latitude.cpp), [`src/Longitude.cpp`](src/Longitude.cpp) — extract pure subsets to `core/types/` (Step 2)
303334
- [`src/Sidereal.cpp`](src/Sidereal.cpp) — extract pure math to `core/SiderealClock` (Step 3)
304335
- [`src/Mount.cpp`](src/Mount.cpp) — extract `calculateRAandDECSteppers` to `core/CoordinateMath` (Step 4), `getLocalDate` to `core/CalendarMath` (Step 5), `RAString`/`DECString` to `core/CoordinateFormatter` (Step 6)
305-
- [`src/EPROMStore.cpp`](src/EPROMStore.cpp) — extract validation logic to `core/EepromLayout.hpp` (Step 7)
336+
- [`src/EPROMStore.cpp`](src/EPROMStore.cpp) — extract validation logic to `hal/EepromLayout.hpp` (Step 7)
306337

307338
### New files created in Phase 1
308339
- `src/core/types/DayTime.hpp`, `src/core/types/DayTime.cpp`
@@ -313,10 +344,10 @@ Each step: extract → add focused unit tests with FakeIt-faked ports → remove
313344
- `src/core/CoordinateMath.hpp`, `src/core/CoordinateMath.cpp`
314345
- `src/core/CalendarMath.hpp`, `src/core/CalendarMath.cpp`
315346
- `src/core/CoordinateFormatter.hpp`, `src/core/CoordinateFormatter.cpp`
316-
- `src/core/EepromLayout.hpp`
347+
- `src/hal/EepromLayout.hpp`
317348
- `unit_tests/test_core/test_daytime.cpp`, `test_declination.cpp`, `test_latitude.cpp`, `test_longitude.cpp`
318349
- `unit_tests/test_core/test_sidereal.cpp`, `test_coordinate_math.cpp`, `test_calendar.cpp`, `test_coordinate_format.cpp`
319-
- `unit_tests/test_core/test_eeprom_layout.cpp`
350+
- `unit_tests/test_hal/test_eeprom_layout.cpp`
320351

321352
### Phase 2 targets
322353
- [`src/HallSensorHoming.cpp`](src/HallSensorHoming.cpp), [`src/EndSwitches.cpp`](src/EndSwitches.cpp), [`src/Gyro.cpp`](src/Gyro.cpp), [`src/LcdMenu.cpp`](src/LcdMenu.cpp), [`src/SSD1306_128x64_Display.cpp`](src/SSD1306_128x64_Display.cpp), [`src/WifiControl.cpp`](src/WifiControl.cpp), [`src/LcdButtons.cpp`](src/LcdButtons.cpp) — become adapters behind ports.
@@ -362,8 +393,7 @@ Manual smoke checklist (per shippable phase end):
362393
- **Back-compat:** Meade serial protocol behavior is invariant (external interface); internal C++ APIs may change freely.
363394
- **Out of scope:** UI menu screens (`c*_menu*.hpp`) refactor; new features; supporting new boards; replacing AccelStepper/TMCStepper libraries; switching build system.
364395
- **Extract-before-test:** All `src/` root files include `inc/Globals.hpp``<Arduino.h>` and are untestable in the `native` PIO env (which only builds `core/`, `ports/`, `adapters/`). Pure logic is extracted into `core/` first, then tested — inverted from the typical test-first order. Only already-pure code (`core/meade/`, `MappedDict`) gets tests before extraction.
365-
- **`core/types/` for data containers:** `DayTime`, `Declination`, `Latitude`, `Longitude` grouped under `src/core/types/` as pure data. Algorithm modules (`SiderealClock`, `CoordinateMath`, etc.) live at `core/` root. Mirrors the `core/meade/` subfolder convention. Can be split further if needed.
366-
- **Thin overlays preserve back-compat:** Original `src/` files become overlays that `#include` the `core/` version and add only Arduino-dependent methods (`ParseFromMeade`, `ToString`, `formatString`). No flag day — each step is independently shippable.
396+
367397

368398
## Further Considerations
369399

platformio.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ build_src_flags =
157157
-Werror
158158
-Wpedantic
159159
-Wshadow
160-
test_lib_deps =
161-
ArduinoFake@^0.4.0
162160
extra_scripts = pre:scripts/test-coverage.py
163-
test_filter = test_core
161+
test_filter =
162+
test_core
163+
test_hal
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
#include <stdint.h>
44

5-
namespace core
5+
namespace hal
66
{
77

8-
/// Pure EEPROM layout constants (no Arduino deps).
8+
/// EEPROM layout constants — magic markers, flags, and storage addresses.
99
/// Mirrors the enum definitions in EPROMStore.hpp for validation and testing.
1010
struct EepromLayout {
1111
// Magic marker values
@@ -51,4 +51,4 @@ struct EepromLayout {
5151
static constexpr float SteppingStorageNormalized = 25600.0;
5252
};
5353

54-
} // namespace core
54+
} // namespace hal

0 commit comments

Comments
 (0)