Skip to content

feat(radio): add output channel mapping (srcCh field)#7224

Draft
raphaelcoeffic wants to merge 109 commits intomodel-arenafrom
output-mapping
Draft

feat(radio): add output channel mapping (srcCh field)#7224
raphaelcoeffic wants to merge 109 commits intomodel-arenafrom
output-mapping

Conversation

@raphaelcoeffic
Copy link
Copy Markdown
Member

Decouple mix channel outputs from RF outputs by adding a srcCh field to LimitData. Each output can now source from any mix channel (not just its own index), or be disabled entirely. Default value 0 preserves backward-compatible 1:1 mapping.

  • Add srcCh to LimitData struct and YAML schemas (all targets)
  • Mixer: ex_chans stays identity-mapped for mix chaining; only channelOutputs follows the mapping with per-output limits
  • Color LCD: Source field in output edit, mapped ex_chans for min/max highlighting, ComboChannelBar uses mapped source
  • B&W LCD (128x64, 212x64): Source field in output edit/list views
  • Lua: srcCh exposed in model.getOutput()/setOutput()
  • All reset handlers clear srcCh

raphaelcoeffic and others added 30 commits March 25, 2026 08:10
…ctions

Phase 1 of dynamic array allocation plan: introduce accessor functions
for all ModelData arrays (mixes, expos, curves, logical switches,
custom functions) and route all access through them. This prepares for
Phase 2 where the static arrays will be replaced by an arena allocator.

Key changes:
- Create expos.cpp/expos.h centralizing expo operations (insertExpo,
  deleteExpo, copyExpo, moveExpo, getExpoCount) mirroring mixes.cpp
- Add curveHeaderAddress(), curvePointsBase(), customFnAddress() accessors
- Replace all direct g_model.{mixData,expoData,curves,points,logicalSw,
  customFn}[] access with accessor function calls
- Remove duplicated expo insert/delete/copy code from colorlcd and
  stdlcd GUI files
- Unify Lua API insertExpo to use centralized function across all targets

No data layout changes - all accessor functions still return pointers
into the existing static arrays. CHKSIZE checks and YAML output are
unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce the arena allocator infrastructure and extend the YAML
tree walker to support externally-stored arrays (YDT_EXTERN_ARRAY).

ModelArena: manages a contiguous buffer where dynamic model data
(mixes, expos, curves, etc.) will be stored. Supports section-based
layout with insert/delete operations that shift subsequent sections.
Arena backing is a static buffer sized per platform (4KB STM32F4,
8KB STM32H7, 64KB sim/companion), designed to be replaceable with
heap or SDRAM in the future.

YAML extern arrays: new YDT_EXTERN_ARRAY node type allows the tree
walker to serialize/deserialize arrays stored outside the main struct.
A get_ptr callback provides the base pointer and element count at
runtime. The walker redirects data access via per-state data_override
pointers, keeping existing YAML format fully compatible.

No functional changes yet - all existing tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add yaml_get_mix_ptr, yaml_get_expo_ptr, yaml_get_curves_ptr,
yaml_get_points_ptr, yaml_get_logical_sw_ptr, yaml_get_custom_fn_ptr
to yaml_datastructs_funcs.cpp. These will be used as callbacks for
YAML_EXTERN_ARRAY nodes once the struct migration is complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove mixData[], expoData[], curves[], points[], logicalSw[], and
customFn[] from ModelData struct and store them in a separate arena
managed by ModelArena. ModelData shrinks from ~6.5KB to ~2.5KB.

Key changes:
- ModelData now contains ModelDynData dyn (counts) instead of 6 arrays
- All accessor functions (mixAddress, expoAddress, curveHeaderAddress,
  curvePointsBase, lswAddress, customFnAddress) redirect to arena
- 22 generated YAML files updated: YAML_ARRAY -> YAML_EXTERN_ARRAY
  for the 6 arena-backed arrays, with get_ptr callbacks
- CHKSIZE checks updated for all radio variants
- Arena uses uint32_t for capacity/offsets (supports SDRAM arenas)
- Arena auto-initialized with static constructor in edgetx.cpp
- Tests updated to use accessor functions; MODEL_RESET clears arena

Firmware compiles on colorlcd (X10), 212x64 (X9E), 128x64 (X7).
All 98 unit tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Teach the YAML code generator (generate_yaml.py) to handle the new
extern_array annotation type. When the generator encounters a
CUST_EXTERN_ARRAY field in a struct, it:

1. Parses the tag name, max count, struct type, and get_ptr function
   from the annotation
2. Force-parses the referenced struct type (MixData, ExpoData, etc.)
   to compute the per-element bit size
3. Falls back to name-based parsing for fake structs (e.g. struct_signed_8)
4. Emits YAML_EXTERN_ARRAY(...) in the generated output via a new
   template clause

All 21 radio targets regenerated with correct bit sizes.
All 98 unit tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RTC backup: include arena data in RamBackupUncompressed struct.
  rambackupWrite() copies arena to backup, rambackupRestore() copies
  it back and restores arena layout from g_model.dyn.
- YAML read: clear arena before parsing model file.
- YAML get_ptr callbacks: return MAX_* counts instead of current counts
  so the parser accepts up to the maximum number of elements during
  read. The writer's isElmtEmpty() naturally skips zero elements.

All 98 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ERN_ARRAY

Pass C define names (MAX_MIXERS_HARD, etc.) through the YAML generator
as strings rather than converting to integers. The generator emits them
verbatim into the generated C++ code where the compiler resolves them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create customfn.h/customfn.cpp with insertCustomFn(), deleteCustomFn(),
clearCustomFn() and move customFnAddress() there from edgetx.cpp.
Replace direct memmove/memset operations on model custom functions
in all 3 GUI variants (colorlcd, 212x64, 128x64) with centralized
calls. Global (radio) custom functions keep the direct approach since
they remain in RadioData's static arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When readModelYaml() is called for a temporary model buffer (e.g.,
modelslist label operations), don't clear the arena - that would
destroy the active model's arena data.

Note: modelslist read-modify-write operations for non-active models
still have a known issue where writeFileYaml() serializes the active
model's arena arrays instead of the temp model's. This pre-dates the
arena refactor (the old round-trip was already lossy for NOBACKUP
fields). A proper fix requires text-based YAML modification or a
second temp arena.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The label rename/move operations in modelslist.cpp do a full model
round-trip (read YAML → modify header → write YAML) for non-current
models. With the arena architecture, this corrupts the file because:
- readModelYaml overwrites the active model's arena with the temp model
- writeFileYaml serializes the (now wrong) arena data back

This is a pre-existing design issue (the round-trip was already lossy
for NOBACKUP fields). Needs replacing with direct YAML text manipulation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the read-modify-write cycle in modelslist label operations
with direct YAML text patching for non-current models.

The old approach (readModelYaml → modify labels → writeFileYaml)
was problematic because:
1. readModelYaml extern array callbacks overwrite the active model's
   arena with the temp model's data
2. writeFileYaml would then serialize the wrong arena data

New approach:
- patchModelYamlLabels(): reads YAML file as text, finds the
  "labels:" field, replaces its value, writes back. No model
  deserialization needed.
- renameLabel(): builds new labels CSV from in-memory label map
  (no need to read the file for label data)
- updateModelFile(): uses patchModelYamlLabels() for non-current models

Also: readModelYaml() now saves/restores the arena when loading a
temp model buffer, preventing arena corruption from extern array
callbacks during any remaining temp reads (e.g. updateModelCell).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 new tests covering:
- ModelArena class: attach, layout, clear, insertSlot, deleteSlot,
  full arena check, section offset cascading
- Accessor functions: all 6 accessors point to arena memory, data
  round-trip through accessors, sections don't overlap
- Insert/delete: insertMix, deleteMix, insertExpo preserve existing
  data; insertCustomFn/deleteCustomFn/clearCustomFn work correctly
- Model reset: arena data is cleared

Total: 121 tests (23 new + 98 existing), all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…el arrays

Move the hard safety caps (MAX_MIXERS_HARD, MAX_EXPOS_HARD, etc.)
from model_arena.h to dataconstants.h where they logically belong
alongside the per-radio MAX_* constants.

Size parallel state arrays to MAX_MIXERS_HARD:
- mixState[MAX_MIXERS_HARD] (was MAX_MIXERS)
- act[MAX_MIXERS_HARD] (was MAX_MIXERS)
- activeMixes[MAX_MIXERS_HARD] (stack-local in mixer.cpp)

This ensures the parallel arrays can accommodate the maximum number
of mixes the arena could hold (128), not just the per-radio default (64).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite the plan document to reflect completed work (Phase 1 + 2)
and detail the Phase 3 plan for dynamic arena sizing:

- Populate ModelDynData counts after model load
- Compact arena layout based on actual usage
- Arena-aware insert/delete (insertInSection/deleteFromSection)
- Dynamic loop bounds in mixer hot path
- GUI memory indicator
- STM32F4 arena sizing considerations
- Known issues (modelslist temp reads, curveEnd sizing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document three categories of limits that constrain how far the
hard caps can be raised:

1. Bit-field cross-references: struct fields that reference arena
   items by index (e.g. LimitData.curve:int8_t caps curves at 128,
   CurveRef.value:11 allows 2048, swtch:10 signed limits enum range)

2. Accessor function parameter types: all use uint8_t idx, capping
   at 255 elements. Sufficient for current hard caps (128) but must
   change to uint16_t if any array exceeds 255.

3. YAML infrastructure: YamlNode.elmts:12 allows 4095 elements,
   not a bottleneck. val_len uint8_t for parsing also fine.

Includes per-array bottleneck summary and recommendations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add detailed analysis of the MixSources enum range vs the 10-bit
signed srcRaw field. On H7 radios with 99 telemetry sensors, the
enum reaches 568 which overflows the 0-511 positive range — a
pre-existing bug where sensors 81-99 can't be used as mix sources.

Update bottleneck summary: mixes/expos can reach 255 (uint8_t idx),
curves capped at 128 (LimitData.curve:int8_t), logical switches
stuck at 64 (enum range pressure in 10-bit fields). Any increase
to items in the MixSources enum requires widening srcRaw.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 new tests verify that enum ranges fit in their storage bit-fields:

- SrcRawCanHoldAllMixSources: checks MIXSRC_LAST_TELEM <= 511
  (srcRaw:10 signed). Will fail on H7 builds (99 sensors, enum=568)
  documenting the pre-existing overflow bug.
- SrcRawRoundTrip: writes source values through actual MixData
  bitfield and verifies they survive. Detects corruption on overflow.
- SwtchCanHoldAllSwitchSources: checks SWSRC_LAST <= 511
- CurveRefCanHoldAllCurves: checks MAX_CURVES_HARD <= 2047
- LimitCurveCanHoldAllCurves: checks MAX_CURVES_HARD <= 127

Total: 126 tests, all passing on X10 (60 sensors).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the long-term direction of replacing the monolithic
MixSources/SwitchSources enums with structured type+index references.
This removes the 10-bit enum range pressure that currently limits
H7 radios to ~75 usable telemetry sensors as mix sources, and
makes item counts independent of each other.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use 8-bit type + 8-bit flags + 16-bit index for source and switch
references (32-bit word-aligned for ARM Cortex-M). The flags byte
replaces the sign-bit inversion hack, and 16-bit index gives 65536
items per type. Note size impact: MixData grows ~10 bytes but this
is offset by dynamic arena sizing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The srcRaw:10 overflow is a live bug on H7 and blocks raising any
arena limits. Move structured source/switch references (SourceRef,
SwitchRef) from Phase 5 to Phase 3b as top priority.

Add detailed migration strategy, per-struct size impact analysis,
and file inventory for the refactoring. Dynamic arena sizing moves
to Phase 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Define 32-bit word-aligned structured reference types to replace
the monolithic MixSources/SwitchSources enums:

SourceRef: type(8) + flags(8) + index(16)
  - 22 source types covering all current MixSources categories
  - flags byte for inversion (replaces negative srcRaw encoding)
  - 16-bit index gives 65536 items per type

SwitchRef: type(8) + flags(8) + index(16)
  - 12 switch types covering all current SwitchSources categories
  - flags byte for inversion (replaces negative swtch encoding)

Conversion functions for gradual migration:
  - sourceRefFromMixSrc() / mixSrcFromSourceRef()
  - switchRefFromSwSrc() / swSrcFromSwitchRef()

22 unit tests verify round-trip conversion for all source/switch
types including inputs, sticks, channels, telemetry, logical
switches, flight modes, inverted sources, etc.

148 total tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove sourceRefFromMixSrc/mixSrcFromSourceRef and switchRefFromSwSrc/
swSrcFromSwitchRef conversion functions. The old MixSources/SwitchSources
enums and the new SourceRef/SwitchRef types should not coexist at
runtime — the only backward compatibility interface is YAML.

Add ValueOrSource struct (4 bytes, 32-bit aligned) to replace the
11-bit SourceNumVal union. Used for weight/offset/curve fields that
hold either a numeric value or a full source reference. When isSource=1,
the value field doubles as the source index and srcType identifies the
source type.

17 struct-level tests (SourceRef, SwitchRef, ValueOrSource).
143 total tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace bit-packed fields in MixData, ExpoData, and CurveRef with
32-bit structured types:
- srcRaw:10 → SourceRef (4 bytes)
- swtch:10 → SwitchRef (4 bytes)
- weight:11 / offset:11 → ValueOrSource (4 bytes each)
- CurveRef: type:5 + value:11 → type:8 + ValueOrSource (6 bytes)

MixData: 20 → 35 bytes
ExpoData: 18 → 33 bytes
CurveRef: 2 → 6 bytes

~110 compile errors remain in call sites that use old integer-based
access patterns. These need mechanical updating to use the new types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update mixer.cpp, mixes.cpp, expos.cpp, model_init.cpp, curves.cpp,
edgetx.cpp for the new structured types:

- mixer.cpp: add sourceRefToMixSrc()/switchRefToSwSrc() bridge
  functions, update getSourceNumFieldValue() to take ValueOrSource,
  update all srcRaw/swtch/weight/offset/curve.value accesses
- mixes.cpp: SourceRef initialization, ValueOrSource.setNumeric()
- expos.cpp: same pattern
- model_init.cpp: same pattern
- curves.cpp: ValueOrSource.numericValue() for curve dispatch,
  valueOrSourceToLegacy() bridge for string formatting
- edgetx.cpp: type-based checks instead of enum range checks

75 errors remain in GUI and Lua code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update all GUI, Lua API, and test code for the new structured types.

GUI (colorlcd):
- input_edit, mixer_edit: explicit lambdas with bridge conversions
  for source/switch/weight/offset/curve editors
- input_source: SourceRef↔mixsrc_t bridges for source picker
- model_mixes, model_inputs: ValueOrSource for weight display,
  SourceRef for source display, SwitchRef for switch display
- curve_param: ValueOrSource for curve value editing
- getset_helpers.h: updated for new field types
- gui_common: srcRaw.isNone() for channel usage checks

Lua API (api_model.cpp):
- mixSrcToSourceRef() / swSrcToSwitchRef() reverse bridges for
  SET path (Lua integers → structured types)
- valueOrSourceToLuaInt() / luaIntToValueOrSource() replace old
  sourceNumValToLuaInt / luaIntToSourceNumval
- All getMix/setMix/getInput/insertInput updated

Tests:
- mixer.cpp: all srcRaw/weight/swtch assignments updated
- model_arena.cpp: arena tests use new types, bit-field capacity
  tests replaced with SourceRef/SwitchRef round-trip tests
- lua.cpp: field assertions check .type/.index/.numericValue()

Bridge functions (temporary, until all code uses SourceRef natively):
- sourceRefToMixSrc(), switchRefToSwSrc() in mixer.cpp (non-static)
- valueOrSourceToLegacy() in curves.cpp (non-static)

Firmware compiles (X10), all 143 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix 128x64 and 212x64 input/mix editors and common stdlcd code:
- model_input_edit, model_mix_edit (both LCD sizes): bridge
  conversions for srcRaw, swtch, weight, offset fields
- model_curves, model_mixes, model_inputs, draw_functions:
  drawSource/drawSwitch/drawCurveRef use bridge functions
- valueOrSourceToLegacy() moved outside COLORLCD guard

All 3 targets build (X10, X9E, X7). 143 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite DYNAMIC_ARRAYS_PLAN.md to reflect:
- Phase 3b complete: SourceRef/SwitchRef/ValueOrSource in MixData,
  ExpoData, CurveRef (all 3 LCD targets build, 143 tests pass)
- Updated arena element sizes (MixData 20→35, ExpoData 18→33)
- STM32F4 arena undersize now critical (6208 > 4096)
- Remaining structs to migrate (LogicalSwitchData, CustomFunctionData,
  FlightModeData, TimerData, SwashRingData, RadioData)
- Bridge function consolidation needed
- YAML serialization update needed for new types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Convert all remaining 10-bit swtch/srcRaw fields to structured types:
- LogicalSwitchData: v1/v3 widened to int16_t, andsw→SwitchRef
- CustomFunctionData: swtch→SwitchRef, func→uint8_t
- FlightModeData: swtch→SwitchRef
- TimerData: swtch→SwitchRef
- SwashRingData: all 3 source fields→SourceRef
- ScriptDataInput: source→SourceRef
- ModuleData.crsf: crsfArmingTrigger→SwitchRef
- RadioData: backlightSrc/volumeSrc→SourceRef

Bridge functions consolidated: mixSrcToSourceRef()/swSrcToSwitchRef()
moved to mixer.cpp and declared in myeeprom.h. All static duplicates
across GUI files removed.

CFN_SWITCH/CFN_EMPTY macros updated for SwitchRef.
All CHKSIZE values updated for all radio variants.
All 3 LCD targets build (X10, X9E, X7).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite _getValue() to dispatch on SourceRef.type instead of
MixSources enum ranges. getValue(SourceRef) handles inversion
via ref.isInverted() instead of negative mixsrc_t encoding.

Backward-compatible getValue(mixsrc_t) overload retained as a
thin wrapper for callers that still use legacy enum values.

Mixer hot path updated to pass SourceRef directly, eliminating
sourceRefToMixSrc() roundtrips in applyExpos() and
evalFlightModeMixes().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

# Conflicts:
#	radio/src/mixer.cpp
Rewrite getSwitch() to dispatch on SwitchRef.type instead of
SwitchSources enum ranges. Inversion handled via ref.isInverted()
instead of negative swsrc_t encoding.

Backward-compatible getSwitch(swsrc_t) inline overload retained
for callers that still use legacy enum values.

Mixer hot path and core engine (switches.cpp, mixer.cpp, timers.cpp,
functions.cpp, edgetx.cpp) updated to pass SwitchRef directly,
eliminating switchRefToSwSrc() roundtrips.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
raphaelcoeffic and others added 25 commits March 25, 2026 08:11
…expr

SourceRef_(), SwitchRef_(), toUint32(), and fromUint32() were not
constexpr, causing the Lua etxcst_entries LROT table to require
runtime initialization (.data) instead of being a compile-time
constant (.rodata).  This added ~2.5 KB to the .data segment,
overflowing FLASH on X9D+2019.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…class

Move RadioData::customFn[64] from a fixed array to a separate
g_radioArena, saving ~1 KB in RadioData for users who don't use
global special functions.

Refactor Arena class to be configured by a const ArenaDesc descriptor
(numSections + elemSizes[]) instead of hardcoded 6-section arrays.
All Arena methods are compiled once — no template monomorphisation.
Model arena has 6 sections, radio arena has 1.

- Add g_radioArena + radioArenaInit()
- Add globalFnAddress/globalFnAllocAt/insertGlobalFn/deleteGlobalFn
  accessors mirroring the model custom function pattern
- evalFunctions() takes explicit count parameter, removing the
  pointer-identity hack
- Replace all g_eeGeneral.customFn[] direct access with accessors
- layout() takes uint16_t counts[] — remove ModelDynData struct
- Update RTC backup to save/restore both arenas via counts()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
simuGetNumLogicalSwitches() and simuCopyLogicalSwitches() used
MAX_LOGICAL_SWITCHES (64) instead of the actual arena section count.
This caused the simulator to report/evaluate 64 logical switches
even when the model only had a few allocated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eliminate the old GV_RANGE sentinel-range encoding (which depended on
MAX_GVARS) from LimitData min/max/offset fields. Replace with a clean
bit-15 flag scheme: bit 15 = 1 means gvar reference (index in bits
0-14), bit 15 = 0 means numeric value (15-bit signed in bits 0-14).

Widen LimitData fields from 11-bit bitfields to full int16_t, ppmCenter
from 10 to 12 bits (+2 bytes per LimitData, +64 bytes per model).

Add GV_ENCODE/GV_DECODE helpers at all numeric read/write boundaries.
Add GVarLimitTest suite (7 tests) covering encoding round-trips, gvar
resolution through LIMIT_MAX/MIN/OFS, and end-to-end mixer clamping.

Fix generate_yaml.py to use -std=c++14 and only count actual errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move FlightModeData[MAX_FLIGHT_MODES] and GVarData[MAX_GVARS] from
fixed ModelData arrays into three new arena sections: ARENA_FLIGHT_MODES,
ARENA_GVAR_DATA, and ARENA_GVAR_VALUES (placeholder for step 5b-2).

- Bump ARENA_MAX_SECTIONS from 6 to 9
- flightModeAddress() now returns arena pointer
- Add gvarDataAddress(), getFlightModeCount(), getGVarCount() accessors
- Convert all ~39 files from g_model.flightModeData[i] / g_model.gvars[i]
  to arena accessor calls
- Update RTC backup pack/unpack for new sections
- Pre-allocate FM/GVar arena sections before YAML load and model init
- Regenerate all yaml_datastructs_*.cpp

Gvar values remain embedded in FlightModeData for now (extracted in 5b-2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove gvar_t gvars[MAX_GVARS] from FlightModeData struct. Gvar values
now live in a flat ARENA_GVAR_VALUES matrix indexed as
base[fm * numGVars + gv].

GVAR_VALUE() becomes an inline function accessing the arena matrix.
FlightModeData shrinks from ~46 to ~28 bytes (X9D+2019).

Add gvarValues as a new CUST_EXTERN_ARRAY in ModelData for YAML
serialization. Update fmd_is_active, model init, sdcard_yaml sentinel
init, and RTC backup to use the flat matrix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace MAX_FLIGHT_MODES/MAX_GVARS with getFlightModeCount()/getGVarCount()
in loop bounds, GUI ranges, Lua bounds checks, and source availability.

Keep compile-time MAX for: runtime state arrays (fp_act, lswFm),
enum ranges (dataconstants.h), bitfields, template params, YAML hard
limits, and switch loop initialization (lswFm needs full reset).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a table to model_arena.h documenting the RAM cost outside the arena
for each section type — the runtime state arrays (mixState, lswFm,
fp_act, etc.) that scale with hard limits. This helps evaluate the cost
of raising limits on constrained targets.

Largest cost: lswFm = MAX_FLIGHT_MODES × MAX_LOGICAL_SWITCHES × 4 bytes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace individual get_ptr/ensure_capacity function pointers in the
_extern_array union member with a single const driver pointer. This
shrinks the union member from 3 pointers to 2 while allowing future
callbacks without growing YamlNode.

Wire up gvar_is_active via the driver so gvarValues with default
sentinel (GVAR_MAX+1) or zero are skipped during YAML output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add getCurveCount(), getCurvePoints(), curveAllocAt() to properly
  allocate both ARENA_CURVES and ARENA_POINTS when creating curves
- Bounds-check getCurvePoints(), loadCurves(), isCurveUsed() against
  arena section count to prevent reading adjacent section data
- Fix colorlcd and B&W UI to use curveAllocAt() + storageDirty() when
  creating new curves
- B&W UI: show only used curves + one empty slot instead of MAX_CURVES
- Wire up fmd_is_active, cfn_is_active, isAlwaysActive for curves in
  ExternArrayDriver instances
- Fix curveedit smooth toggle missing SET_DIRTY()
- Add curve round-trip verification to YamlRoundTrip test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use indexed CUST_EXTERN_ARRAY for curves (matching main) so sparse
  curve slots preserve their index in YAML
- Add curve_is_active callback using a bitmap populated by loadCurves()
  for O(1) checks instead of scanning all points on every save
- Add Arena::trimSectionTo() for exact section count trimming
- Add curveTrimTrailing() to reclaim both ARENA_CURVES and ARENA_POINTS
  when trailing curve slots are cleared
- Color UI: call curveTrimTrailing() on curve clear
- B&W UI: call curveTrimTrailing() on EXIT, setCurveUsed() on enter
- Fix curveedit smooth toggle missing SET_DIRTY()
- Add unit tests for trimSectionTo and curveTrimTrailing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cache section base pointers and loop counts before tight loops
instead of calling arena accessors on every iteration. This avoids
repeated indirection through g_modelArena._base + _offsets[] on
each step. Loop over actual arena counts (getMixCount, getExpoCount,
getLswCount) instead of MAX_* constants to skip empty slots. Remove
redundant memset of static dummy in lswAddress. Cache lswAddress
result in evalLogicalSwitches instead of calling it 3 times.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Avoid relying on static init ordering.
curveIsEmpty only checked the CurveHeader for zeros, but a default
STANDARD curve with 5 points has an all-zero header. The UI marks
curves via setCurveUsed(), so curveIsEmpty must also consult the
flag bitmap to avoid trimming curves that have data.

Fix tests to match UI behaviour: use lswAllocAt instead of
lswAddress for out-of-range writes, and call setCurveUsed after
curveAllocAt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Arena::ensureSectionCapacity() can reallocate the buffer, but
YamlTreeWalker held stale data_override pointers into the old buffer.
Refresh data_override from get_ptr() after ensure_capacity in both
toNextElmt() and the IDX handler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Legacy GV6 format is 1-based; convert to 0-based index (GV6 → index 5)
- Negative GVars (-GV6) now encode inversion via negative value in
  ValueOrSource (value = -(index+1)), decoded by toSourceRef()
- setSource() preserves GVar inversion flag as negative value
- New format !gv(5) round-trips correctly through setSource/toSourceRef

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drawValueOrSource and editValueOrSource fell through to drawSource for
GVar sources, which used getSourceString (! prefix for inversion).
Use drawGVarName instead, matching main branch behavior (- prefix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Decouple mix channel outputs from RF outputs by adding a srcCh field
to LimitData. Each output can now source from any mix channel (not just
its own index), or be disabled entirely. Default value 0 preserves
backward-compatible 1:1 mapping.

- Add srcCh to LimitData struct and YAML schemas (all targets)
- Mixer: ex_chans stays identity-mapped for mix chaining; only
  channelOutputs follows the mapping with per-output limits
- Color LCD: Source field in output edit, mapped ex_chans for
  min/max highlighting, ComboChannelBar uses mapped source
- B&W LCD (128x64, 212x64): Source field in output edit/list views
- Lua: srcCh exposed in model.getOutput()/setOutput()
- All reset handlers clear srcCh
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.

1 participant