Skip to content

Activate Electric Air Module (0x8E) for cell-level battery monitoring - #213

Open
DieterMayerOSS wants to merge 1 commit into
dgatf:masterfrom
DieterMayerOSS:feat/eam-activation
Open

Activate Electric Air Module (0x8E) for cell-level battery monitoring#213
DieterMayerOSS wants to merge 1 commit into
dgatf:masterfrom
DieterMayerOSS:feat/eam-activation

Conversation

@DieterMayerOSS

@DieterMayerOSS DieterMayerOSS commented May 14, 2026

Copy link
Copy Markdown
Contributor

Motivation

The HoTT Electric Air Module (EAM, sensor ID 0x8E) is the spec-defined module for electric models with cell-level battery monitoring. msrc had the frame struct defined but no dispatch — HOTT_ELECTRIC_AIR_MODULE_ID was marked "not used, for internal Hott telemetry" and never reached the polling switch.

Per HoTTv4 rules:

  • Only one sensor of each type may register on the bus, so EAM activation must be a deliberate user choice (some setups already have a Unisens-E or JLog as EAM)
  • EAM may run in parallel to airESC (0x8C); transmitters then show two separate sensor menus — one for ESC-specific data (BEC, RPM, ESC temp), one for battery-specific data (cells, total voltage, capacity, vario)

What this PR does

  • Adds an enable_eam config flag (reuses spare7, no struct layout change; defaults to false for backward compatibility)
  • Wires EAM into format_binary_packet and format_text_packet dispatch
  • Replaces the stale internal HOTT_ELECTRIC_* enum with one mirroring the wire-frame layout (7 cell slots, battery 1/2, temps, altitude, current, drive voltage, capacity, climb rates, RPM, speed)
  • Adds electric_air[] array to hott_sensors_t
  • Grows is_enabled[] from [4] to [5] so HOTT_TYPE_ELECTRIC = 4 is addressable (was latent OOB)
  • Adds electric_triggers_t enum + electric arrays in triggers_value_t / triggers_menu_t with sensible alarm defaults
  • Adds alarm_electric_air_t bitmask enum
  • When enable_eam is set, wires HW5 and Kontronik ESC data into electric_air[]: average cell voltage replicated across the auto-detected cell count, plus battery voltage, drive voltage, current, capacity, and ESC/BEC temperatures

The msrc_link GUI would need a corresponding "Enable EAM" checkbox to expose the flag to users — suggested as a follow-up.

Verification

  • Built clean with Pico SDK 2.2.0 / ARM GCC 15.2 (no warnings)
  • Three files touched: hott.c, shared.h (config struct), config.c (default)
  • Hardware test on real Graupner transmitter pending (battery + cabling — happy to follow up once tested)

@dgatf

dgatf commented May 14, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for the contribution. The implementation is clean and I appreciate the detailed work on the EAM packet.

At the moment I am not fully sure what EAM adds over the existing HoTT mapping:

  • Air-ESC (0x8C) already carries ESC-specific telemetry, including BEC voltage/current/temperature.
  • General Air (0x8D) already carries battery/cell-related telemetry. For example, Smart ESC / Spektrum Smart Battery, which provides real per-cell voltages, is already mapped to General Air cells.
  • For HW5/Kontronik, this PR only replicates average cell voltage across the EAM cell slots, so it is not true per-cell telemetry.

There is also a possible compatibility concern because some HoTT receivers may already use Electric Air (0x8E) for their own telemetry, and HoTT only allows one sensor of each type on the bus.

So I would like to better understand the specific benefit:

  • Does your transmitter display EAM data in a way that Air-ESC + General Air cannot?
  • Are some values missing or shown incorrectly with the current MSRC mapping?
  • Is there a specific receiver/transmitter combination where EAM is required?

Also, since enable_eam defaults to false, msrc_link would need a corresponding GUI option before this is usable for normal users.

@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

Thank you for the thoughtful review.

You're right that for HW5/Kontronik the cell data is replication, not true per-cell — that's a fair call. But I'd like to offer a different framing for the EAM addition that I think addresses your concerns:

The UniSens-E (a well-established HoTT sensor from SM Modellbau) lets the user pick one of GAM / EAM / ESC / Vario as the module role. That's how it coexists with other sensors on the bus — the HoTTv4 "one per type" rule means flexibility in which type you announce as is the actual feature.

msrc currently auto-publishes as ESC (when an ESC is configured) and GAM (when enable_lipo is set). For users who already have a UniSens-E or similar as GAM on their bus, msrc-as-GAM collides. EAM is the natural alternative slot, and that's the gap this PR fills.

So the use case is:

  • User has a HW5 or Kontronik ESC providing total voltage + avg cell + cell count (what I have)
  • User has another sensor as GAM (UniSens-E, JLog, INA3221 elsewhere)
  • User wants msrc to announce additional electric-specific data without colliding with the GAM sensor
  • enable_eam (defaults false) gives them that switch

That said — your enable_eam would be more honest as part of a broader pattern: making all msrc module roles (ESC / GAM / EAM / Vario) explicitly togglable like UniSens-E does, rather than the current implicit auto-enable. I'm happy to take that direction in a follow-up PR if it's welcome.

For this specific PR: would you accept it as-is (EAM as opt-in only), or would you prefer I close it and submit (a) a smaller PR that just routes HW5/Kontronik cells to general_air[], (b) the broader "make all module roles explicitly configurable" refactor, or (c) something else?

@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

Thank you for the careful review — your concerns are valid, and I want to step back and propose a broader direction rather than defending the EAM-specific PR.

Looking at the established pattern in the HoTT ecosystem: UniSens-E (SM Modellbau) — probably the most widely used HoTT telemetry sensor in the community — lets the user pick one role for the sensor: GAM, EAM, ESC, or Vario. The HoTTv4 "one per type" rule isn't a limitation, it's the actual design contract: each smart sensor announces as one type, and several sensors can coexist on the bus by picking different types.

msrc currently auto-publishes on multiple types simultaneously (ESC + GAM + Vario), which is convenient when msrc is alone on the bus but causes collisions when combined with other sensors (UniSens-E, JLog).

Would you be open to a broader refactor where msrc adopts the same single-role pattern? Concretely:

  • New config field hott_module_role = ESC | GAM | EAM | Vario
  • msrc publishes only on the selected role; sensor data routes accordingly
  • Backward compatibility via either (a) default = ESC + version migration for existing configs, or (b) keeping per-role enable flags with current auto-defaults so existing setups keep working

If yes, I'd close this PR and open a new one for the broader change. If you'd rather keep the current multi-role auto behavior, I'll close this PR and submit a smaller alternative that just routes HW5/Kontronik cell_voltage to general_air[HOTT_GENERAL_CELL_*] (using your existing INA3221/Smart-ESC/Spektrum cell path).

Either direction is fine — what would you prefer?

@dgatf

dgatf commented May 14, 2026

Copy link
Copy Markdown
Owner

Thank you, this is a good and well-reasoned proposal.

I need to analyze it more carefully before deciding the right direction. I agree that the current multi-module HoTT behavior can be convenient, but it can also create conflicts when other HoTT sensors are present on the bus.

Your proposed broader approach may be better than adding EAM as a special case, but I want to review the implications for existing users, config compatibility, msrc_link, and the current ESC/GAM/Vario routing before making a decision.

So please give me a bit of time to think about it. I will review the HoTT mapping and then decide whether a broader configurable HoTT role/output model makes sense, or whether it is better to keep the current behavior and make only a smaller change.

@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

No rush — take all the time you need. Architecture decisions are exactly the kind of thing worth thinking through carefully, especially when backwards-compatibility and the msrc_link binary protocol are involved.

If it would help your evaluation, I'm happy to:

  • share the local prototype branch (rough but builds and runs end-to-end)
  • post a screenshot of the GUI role-selector tab
  • sketch a backwards-compat plan for existing flashed configs (e.g. legacy multi-role mode triggered by hott_module_role value 0xFF, or per-role enable flags as a hybrid)

Otherwise I'll just wait for your decision and act accordingly.

DieterMayerOSS added a commit to DieterMayerOSS/msrc that referenced this pull request May 15, 2026
…E style)

After review feedback on PR dgatf#213, this generalizes the EAM activation
into a module-role selector that lets the user choose which HoTT module
types msrc announces on the bus -- matching the pattern established by
UniSens-E, oXs_on_RP2040, and openXsensor.

Per HoTTv4 "one sensor per type per bus", smart sensors coexist by
each announcing as a different type. msrc's previous behaviour (auto-
enable on every type whose data source was present) caused collisions
when other smart sensors were on the bus. This change gives the user
explicit per-role control.

Architecture changes:
- shared.h: `spare7` repurposed as `hott_modules_enabled` (uint8
  bitmask). Bit 0 = ESC (0x8C), bit 1 = GAM (0x8D), bit 2 = EAM (0x8E),
  bit 3 = Vario (0x89). Same byte position, no layout change.
- config.c: default 0x0B (ESC + GAM + Vario, EAM off) preserves
  pre-bitmask behaviour for new flashed configs.
- hott.c: existing auto-enable wiring kept intact; a final masking
  pass at the end of set_config() clears `is_enabled[HOTT_TYPE_*]` for
  roles the user disabled. Pre-bitmask configs (`hott_modules_enabled
  == 0`) fall back to the legacy default at runtime, so existing flashed
  units keep working without a reflash or config wipe.

EAM (0x8E) frame implementation:
- Replace stale internal `HOTT_ELECTRIC_*` enum (was marked "shouldnt
  be used") with one mirroring the wire-frame layout: 7 cell slots,
  battery 1/2, two temperatures, altitude, current, drive voltage,
  capacity, two climb rates, RPM, speed.
- Add `electric_air[HOTT_ELECTRIC_COUNT]` to `hott_sensors_t`.
- Grow `is_enabled[]` from 4 to 5 so `HOTT_TYPE_ELECTRIC` (=4) is
  addressable (was a latent OOB).
- Add `electric_triggers_t` enum + electric arrays in
  `triggers_value_t` / `triggers_menu_t`, with sensible alarm defaults.
- Add `alarm_electric_air_t` bitmask enum.
- Add binary frame handler in `format_binary_packet` with canonical
  scaling (cells 0.02 V steps, battery 0.1 V, temp +20 offset,
  altitude +500 offset, current 0.1 A, capacity 10 mAh,
  climb rate * 100 + 30000).
- Add text-mode menu handler in `format_text_packet`.
- Wire HW5 and Kontronik cell_voltage/voltage/current/capacity/temps
  into `electric_air[]` when the EAM bit is set. Cell voltage is
  replicated across the auto-detected cell count (HW5 and Kontronik
  only report average cell voltage, not per-cell).

msrc_link GUI:
- Four checkboxes "ESC / GAM / EAM / Vario" replace the previous
  attempt at an EAM-only checkbox. Receiver tab, under cbReceiver.
- Read/write of the bitmask in setUiFromConfig / getConfigFromUi.
- Legacy auto-default (0x0B) displayed when read value is 0.

Built with Pico SDK 2.2.0 / ARM GCC 15.2 and Qt 6.11.1 / MinGW 13.10.
@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

Pivoted the implementation to address your concerns more comprehensively. The PR now does what we discussed in the comments:

Multi-checkbox hott_modules_enabled bitmask instead of single enable_eam flag. User picks any combination of ESC / GAM / EAM / Vario via four checkboxes on the Receiver tab. Default is 0x0B (ESC + GAM + Vario, EAM off) — matches current msrc behaviour exactly, so users who don't touch the checkboxes notice nothing.

Backward-compatible at runtime. Existing flashed configs have 0x00 in that byte (it was previously spare7). The firmware treats 0x00 as "legacy auto" and applies the default 0x0B at startup, so no reflash or config wipe needed.

Implementation strategy — instead of gating each ESC/GAM/Vario auto-enable inline (which would have touched ~10 sites in set_config), I kept the existing auto-enable wiring intact and added a final masking pass at the end of set_config that clears is_enabled[HOTT_TYPE_*] for roles the user opted out of. Smaller diff, easier to reason about, easier to revert if a single role causes issues.

EAM frame kept from the previous revision — that part of the work is still useful regardless of how role selection happens.

This also gives users a way to coexist msrc with a UniSens-E or JLog on the same bus: uncheck the role the other sensor occupies. Aligns msrc with the multi-sensor coexistence story.

Let me know if you'd like this scope dialed back further, or if a different role-selection UI (radio buttons / single combo / per-role auto-detect) would fit better.

DieterMayerOSS added a commit to DieterMayerOSS/msrc that referenced this pull request May 15, 2026
…E style)

After review feedback on PR dgatf#213, this generalizes the EAM activation
into a module-role selector that lets the user choose which HoTT module
types msrc announces on the bus -- matching the pattern established by
UniSens-E, oXs_on_RP2040, and openXsensor.

Per HoTTv4 "one sensor per type per bus", smart sensors coexist by
each announcing as a different type. msrc's previous behaviour (auto-
enable on every type whose data source was present) caused collisions
when other smart sensors were on the bus. This change gives the user
explicit per-role control.

Architecture changes:
- shared.h: `spare7` repurposed as `hott_modules_enabled` (uint8
  bitmask). Bit 0 = ESC (0x8C), bit 1 = GAM (0x8D), bit 2 = EAM (0x8E),
  bit 3 = Vario (0x89). Same byte position, no layout change.
- config.c: default 0x0B (ESC + GAM + Vario, EAM off) preserves
  pre-bitmask behaviour for new flashed configs.
- hott.c: existing auto-enable wiring kept intact; a final masking
  pass at the end of set_config() clears `is_enabled[HOTT_TYPE_*]` for
  roles the user disabled. Pre-bitmask configs (`hott_modules_enabled
  == 0`) fall back to the legacy default at runtime, so existing flashed
  units keep working without a reflash or config wipe.

EAM (0x8E) frame implementation:
- Replace stale internal `HOTT_ELECTRIC_*` enum (was marked "shouldnt
  be used") with one mirroring the wire-frame layout: 7 cell slots,
  battery 1/2, two temperatures, altitude, current, drive voltage,
  capacity, two climb rates, RPM, speed.
- Add `electric_air[HOTT_ELECTRIC_COUNT]` to `hott_sensors_t`.
- Grow `is_enabled[]` from 4 to 5 so `HOTT_TYPE_ELECTRIC` (=4) is
  addressable (was a latent OOB).
- Add `electric_triggers_t` enum + electric arrays in
  `triggers_value_t` / `triggers_menu_t`, with sensible alarm defaults.
- Add `alarm_electric_air_t` bitmask enum.
- Add binary frame handler in `format_binary_packet` with canonical
  scaling (cells 0.02 V steps, battery 0.1 V, temp +20 offset,
  altitude +500 offset, current 0.1 A, capacity 10 mAh,
  climb rate * 100 + 30000).
- Add text-mode menu handler in `format_text_packet`.
- Wire HW5 and Kontronik cell_voltage/voltage/current/capacity/temps
  into `electric_air[]` when the EAM bit is set. Cell voltage is
  replicated across the auto-detected cell count (HW5 and Kontronik
  only report average cell voltage, not per-cell).

msrc_link GUI:
- Four checkboxes "ESC / GAM / EAM / Vario" replace the previous
  attempt at an EAM-only checkbox. Receiver tab, under cbReceiver.
- Read/write of the bitmask in setUiFromConfig / getConfigFromUi.
- Legacy auto-default (0x0B) displayed when read value is 0.
- gbHottRoles visibility gated on HOTT receiver selection in
  on_cbReceiver_currentTextChanged(), with defensive setVisible(false)
  in the constructor -- same pattern as cbVarioAutoOffset and
  gbFuelPressure. Without this gate the GroupBox was shown for every
  protocol.

Built with Pico SDK 2.2.0 / ARM GCC 15.2 and Qt 6.11.1 / MinGW 13.10.
@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

Heads-up: force-pushed an amend that adds protocol-conditional
visibility for the new gbHottRoles GroupBox. Without it the
checkboxes were shown for every receiver protocol, not just HOTT.
Same pattern as cbVarioAutoOffset / gbFuelPressure. Diff is
+8 lines in mainwindow.cpp.

…E style)

After review feedback on PR dgatf#213, this generalizes the EAM activation
into a module-role selector that lets the user choose which HoTT module
types msrc announces on the bus -- matching the pattern established by
UniSens-E, oXs_on_RP2040, and openXsensor.

Per HoTTv4 "one sensor per type per bus", smart sensors coexist by
each announcing as a different type. msrc's previous behaviour (auto-
enable on every type whose data source was present) caused collisions
when other smart sensors were on the bus. This change gives the user
explicit per-role control.

Architecture changes:
- shared.h: `spare7` repurposed as `hott_modules_enabled` (uint8
  bitmask). Bit 0 = ESC (0x8C), bit 1 = GAM (0x8D), bit 2 = EAM (0x8E),
  bit 3 = Vario (0x89). Same byte position, no layout change.
- config.c: default 0x0B (ESC + GAM + Vario, EAM off) preserves
  pre-bitmask behaviour for new flashed configs.
- hott.c: existing auto-enable wiring kept intact; a final masking
  pass at the end of set_config() clears `is_enabled[HOTT_TYPE_*]` for
  roles the user disabled. Pre-bitmask configs (`hott_modules_enabled
  == 0`) fall back to the legacy default at runtime, so existing flashed
  units keep working without a reflash or config wipe.

EAM (0x8E) frame implementation:
- Replace stale internal `HOTT_ELECTRIC_*` enum (was marked "shouldnt
  be used") with one mirroring the wire-frame layout: 7 cell slots,
  battery 1/2, two temperatures, altitude, current, drive voltage,
  capacity, two climb rates, RPM, speed.
- Add `electric_air[HOTT_ELECTRIC_COUNT]` to `hott_sensors_t`.
- Grow `is_enabled[]` from 4 to 5 so `HOTT_TYPE_ELECTRIC` (=4) is
  addressable (was a latent OOB).
- Add `electric_triggers_t` enum + electric arrays in
  `triggers_value_t` / `triggers_menu_t`, with sensible alarm defaults.
- Add `alarm_electric_air_t` bitmask enum.
- Add binary frame handler in `format_binary_packet` with canonical
  scaling (cells 0.02 V steps, battery 0.1 V, temp +20 offset,
  altitude +500 offset, current 0.1 A, capacity 10 mAh,
  climb rate * 100 + 30000).
- Add text-mode menu handler in `format_text_packet`.
- Wire HW5 and Kontronik cell_voltage/voltage/current/capacity/temps
  into `electric_air[]` when the EAM bit is set. Cell voltage is
  replicated across the auto-detected cell count (HW5 and Kontronik
  only report average cell voltage, not per-cell).
- Raise STACK_RX_HOTT from 450 to 700 words: the electric_air[] array
  and the electric trigger arrays enlarge hott_task's stack frame by
  ~110 words. Without this bump the task overflowed its stack on boot,
  corrupting adjacent RAM and breaking USB enumeration. Found during
  hardware bring-up testing.

msrc_link GUI:
- Four checkboxes "ESC / GAM / EAM / Vario" replace the previous
  attempt at an EAM-only checkbox. Receiver tab, under cbReceiver.
- Read/write of the bitmask in setUiFromConfig / getConfigFromUi.
- Legacy auto-default (0x0B) displayed when read value is 0.
- gbHottRoles visibility gated on HOTT receiver selection in
  on_cbReceiver_currentTextChanged(), with defensive setVisible(false)
  in the constructor -- same pattern as cbVarioAutoOffset and
  gbFuelPressure. Without this gate the GroupBox was shown for every
  protocol.

Built with Pico SDK 2.2.0 / ARM GCC 15.2 and Qt 6.11.1 / MinGW 13.10.
@DieterMayerOSS

Copy link
Copy Markdown
Contributor Author

Force-pushed again — this time a real bug found during hardware bring-up on a Pico.

The EAM struct additions in this PR (electric_air[HOTT_ELECTRIC_COUNT] in hott_sensors_t, plus the electric arrays in triggers_value_t / triggers_menu_t) enlarge hott_task's stack frame by roughly 110 words. That overflowed STACK_RX_HOTT (was 450 + STACK_EXTRA).

On real hardware the symptom was nasty and non-obvious: the overflow corrupted adjacent RAM, the USB task's data got clobbered, and the board failed USB enumeration entirely — Windows just reported "USB device not recognized", no COM port at all. It builds clean and looks fine in review; it only shows up when you flash it.

Fix in this push: STACK_RX_HOTT raised 450 -> 700 words (constants.h). With that the board boots and enumerates normally, and an EAM frame built from synthetic sweep values comes through cleanly.

Flagging it explicitly because this class of bug — a struct grows, the task stack doesn't — is invisible to code review and CI. Might be worth a general note for future HoTT frame changes.

@dgatf

dgatf commented May 19, 2026

Copy link
Copy Markdown
Owner

Thanks for the update. I understand the goal better now, but I am still not fully convinced this should be merged as-is.

This is not fixing a current bug in MSRC; it adds a new configuration model for a relatively specific HoTT coexistence use case. That can be useful, but it also adds non-trivial complexity: new GUI state, a new persisted config field, legacy handling, an EAM packet path, new alarm/menu entries, extra stack usage, and more HoTT role combinations to test.

Before accepting this, I would prefer to test it on real hardware and confirm that the benefit is worth the added complexity. In particular, I want to verify whether using EAM instead of GAM actually preserves the useful telemetry in a practical setup, and whether the transmitter displays it in a way that is clearly better or necessary.

So for now I am not rejecting the idea, but I am not ready to merge it yet. I will need to test it and think a bit more about whether MSRC should support this level of HoTT role configurability, or whether it is better to keep the current simpler behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants