--- title: Parameter System Desing Document created_date: 2025-09-23 updated_date: 2025-09-23 aliases: tags: --- # Parameter System Desing Document > Teensy 4.0 • Teensyduino/Arduino framework • No RTOS • ETL containers • Chirp protocol for messaging > **Scope:** Flight Controller (FC) side only, with **name-as-canonical** parameters and **immediate apply** (no staging yet). > Enums such as TypeId, ParamStatus, ParamOp come from the Chirp side. Include the relevant Chirp headers; do not redefine them here. --- ## 1) Current Rules / Constraints * **Platform:** Teensy 4.0, Arduino/Teensyduino framework, no RTOS. * **Transport:** **Chirp** is the sole protocol for Get/Set requests and responses. * **Design principles** 1. **Name is canonical.** We identify parameters by a unique, human-readable path. 2. **Shallow ownership tree.** Each module owns a **single sub-tree**; keep names shallow: `module.param`. 3. **Module-facing interface.** Modules expose parameters via light interfaces; the manager never “reaches in” to private variables. * **Non-module params:** System-level, logging, build info, flight mode selectors, serial settings, etc. must also fit the same model. ### Current Chirp Message Structure ```xml ``` --- ## 2) Naming * **Hierarchical dot paths: (Either with `group.name` combination, or directly for `name`)** Examples: `attitudectrl.kp_roll`, `attitudectrl.torque_limit`, `servo_left_flap.offset_angle`, `serial_pilot.baud_rate`, `i2c_line1.clock`, `logger.sink_interface`, `system.fw_version`. * **Uniqueness:** Names are **case-sensitive** and must be unique within the FC. * **Ownership:** The **root token** (e.g., `attitudectrl`, `logger`, `serial_pilot`) identifies which module/adapter owns the parameter. --- ## 3) Core Components ### 3.1 ParamRegistry (all descriptors) A single in-memory catalog describing **every configurable parameter** on the FC. The registry answers **what** exists (metadata), **how to read** it, and **how to apply** a new value—**without** the manager touching module internals. #### Descriptor structure (Globally defined across every system in drone) * `participant` : which device owns this parameter set (here: `"flight_controller"`). *Note:* other devices (autopilot, co-pilot) will have their own registries in their repos. * `group` : high-level bucket **within the FC**, typically the module or subsystem root (`"attitudectrl"`, `"logger"`, `"system"`, `"serial_copilot"`, `"rc"`, `"imu_leftwing"`). * `name` : either the **full name of the parameter**, e.g., `"attitudectrl.kp_roll"` , or partial name so that group + name results in the unique name. * `type` : `TypeId` (shared enum across Chirp, e.g., `Float`, `Uint16`, …). * `length` : size in bytes for validation and arrays. * `default_value` : bytes for default initialization at boot. * `flags` : bitmask. Possible flags: * `Writable` — if **unset**, parameter is read-only. * `RebootRequired` — write accepted but **takes effect after reboot**. * `ImmediateApply` — explicitly safe for immediate side-effects. * **Accessors (delegates):** * `get(name) : bytes ` — read current value from the owning module. * `set(name, bytes) : status` — parse/validate module-local rules and update the module. > The registry owns **only metadata and function delegates**. Actual values remain inside modules. #### Example C++ sketch ```cpp struct ParamDescriptor { const char* participant; // "flight_controller" const char* group; // "attitudectrl", "logger", ... const char* name; // "attitudectrl.kp_roll" (canonical, unique) TypeId type; uint16_t length; // sizeof(float) etc. uint8_t flags; // bitmask of ParamFlag const uint8_t* default_bytes; // pointer to default value bytes // Delegates to module-owned code: etl::delegate getter; etl::delegate setter; }; class ParamRegistry { public: // Fixed capacity bool register_param(const ParamDescriptor& d); // returns false on duplicate name or full const ParamDescriptor* find(const char* name) const; // or find(group, name) pair depending on full name implementation private: etl::unordered_map, ParamDescriptor, NUM_PARAMS> m_by_name; // pre-sized buckets }; ``` **Note**: `TypeId` , `ParamStatus` definitions exist in relevant chirp header files. > **Getter/Setter notes** > > * `getter` returns the current committed value from the module into `out`. > * `setter` validates and updates the module, sets `reboot_required=true` if the new value won’t take effect until reboot. --- ### 3.2 ParameterManager (global singleton) The single entry point for **Get/Set** operations and the **Chirp** callback endpoint. **Responsibilities** * Own the **ParamRegistry**. * Expose a **simple API** used by both: * Internal code, and * The **Chirp handler** (by registering a callback for `ParameterRequest`). * Apply changes unless the descriptor is `RebootRequired`. **Minimal API sketch** ```cpp class ParameterManager { public: static ParameterManager& instance(); void init(ChirpHandler& chirp); // registers ParamRequest callback ParamRegistry& registry(); // Internal helpers ParamStatus get_by_name(const char* name, uint8_t* out, uint16_t& out_len, TypeId& out_type) const; ParamStatus set_by_name(const char* name, const uint8_t* in, uint16_t in_len, TypeId type, bool& reboot_required); // Chirp callback (wired to opcode for ParameterRequest) void chirp_param_request_callback(const chirp::ParameterRequest& req); private: ParameterManager() = default; ParamRegistry m_reg; }; ``` **Key behaviors** * **Name resolution** via registry. * **Immediate apply:** `set_by_name` calls the descriptor’s `setter`. If success: * If `RebootRequired` flag OR setter sets `reboot_required=true` → reply `RebootRequired`. * Else reply `Ok`. * **Get** calls the descriptor’s `getter`. * **Chirp bridge:** `chirp_param_request_callback` maps Chirp fields to the API above and produces a `ParameterResponse`. --- ## 4) Module Integration via `IFlightModule` Grouping flight controller modules under a capability interface like `IFlightModule` can be useful: * Keeps a **clean boundary**: modules **declare** what they own and how to apply changes—no global manager poking internal data. * Enables **testability**: I can unit-test a module’s param parsing/validation in PC native tests without wiring the entire FC. * Simplifies **registration**: each module lists its parameters and provides delegates once, during init. **Example interface** ```cpp struct IFlightModule { virtual const char* root_namespace() const = 0; // e.g., "attitudectrl" virtual void enumerate_params(ParamRegistry& reg) = 0; // Optional. modules can expose dedicated getters/setters per param as how it is currently. virtual ~IFlightModule() = default; }; ``` **How it’s used** * On boot, each module calls `enumerate_params(reg)`. * Inside `enumerate_params`, the module **registers its parameters** by building `ParamDescriptor`s with **delegates bound to specific setter/getter methods of `this`**. **Example — Attitude Controller registers `kp_roll`** ```cpp class AttitudeController : public IFlightModule { public: const char* root_namespace() const override { return "attitudectrl"; } void enumerate_params(ParamRegistry& reg) override { static const float DEFAULT_KP_ROLL = 10.0f; // Either defined here or as a member variable static const uint8_t* def = reinterpret_cast(&DEFAULT_KP_ROLL); ParamDescriptor d { "flight_controller", "attitudectrl", "kp_roll", // or "attitudectrl.kp_roll" TypeId::Float, (uint16_t)(sizeof(float)), (uint8_t)(ParamFlag::Writable | ParamFlag::ImmediateApply), def, // getter: copy current kp into out etl::delegate::create(this), // setter: parse float, validate, assign etl::delegate::create(this) }; reg.register_param(d); } // Example getter/setter implementations: ParamStatus get_kp_roll(uint8_t* out, uint16_t& out_len) { const uint16_t needed_length = static_cast(sizeof(float)); if (out_len < needed_length) return ParamStatus::InvalidType; memcpy(out, &m_kp_roll, needed_length); out_len = needed_length; return ParamStatus::Ok; } ParamStatus set_kp_gains(const uint8_t* in, uint16_t in_len, bool& reboot_required) { const uint16_t needed_length = static_cast(sizeof(float)); if (in_len != needed_length) return ParamStatus::InvalidType; float v; memcpy(&v, in, needed_length); if (v[i] < min || v[i] > max) return ParamStatus::InvalidValue; reboot_required = false; // takes effect immediately return ParamStatus::Ok; } /* rest of the class definition ... */ private: float m_kp_roll = 10.0f; ... }; ``` > This pattern can be replicated this pattern for other attitude controller params like `torque_limit`, indi related params etc. > Alternatively, we can alter the structure of `ParamDescriptor` in the example without redefining the current parameter structure of attitude controller, keeping the array structure for kp, kd gains, torque limits etc. Design supports multiple value arrays as single parameter: ```cpp ParamDescriptor d { "flight_controller", "attitudectrl", "kp_gains", // or "attitudectrl.kp_gains" TypeId::Float, (uint16_t)(3 * sizeof(float)), (uint8_t)(ParamFlag::Writable | ParamFlag::ImmediateApply), def, // getter: copy current kp into out etl::delegate::create(this), // setter: parse float, validate, assign etl::delegate::create(this) }; ``` where `kp_gains` is defined as: ```cpp private: etl::array m_kp_gains; ``` --- ## 5) Hardware Parameters via `IHardwareParamAdapter` Some parameters belong to **hardware drivers** (IMUs, serial baud, PWM ranges). I don’t want the ParameterManager to depend on driver APIs, and most drivers are used by multiple flight modules. **The adapter idea:** a **tiny object** owned by the module that uses the driver (or owned centrally) which exposes parameters for that hardware subtree, using the **same pattern** as a module: ```cpp struct IHardwareParamAdapter { virtual const char* root_namespace() const = 0; // e.g., "serial" or "imu" virtual void enumerate_params(ParamRegistry& reg) = 0; virtual ~IHardwareParamAdapter() = default; }; // Example: Serial adapter exposing baud rate class SerialParamAdapter : public IHardwareParamAdapter { public: const char* root_namespace() const override { return m_root_namespace; } void set_root_namespace(char* root_namespace) { m_root_namespace = root_namespace; } // These setters can be needed for multiple instances of serial e.g `serial_pilot` , `serial_copilot` void enumerate_params(ParamRegistry& reg) override { static const uint32_t DefaultBaud = 115200; static const uint8_t* def = reinterpret_cast(&DefaultBaud); ParamDescriptor baud { "flight_controller", m_root_namespace, "baud_rate", TypeId::Uint32, (uint16_t)sizeof(uint32_t), (uint8_t)(ParamFlag::Writable | ParamFlag::RebootRequired), def, etl::delegate::create(this), etl::delegate::create(this) }; reg.register_param(baud); } ParamStatus get_baud(uint8_t* out, uint16_t& out_len) { if (out_len < sizeof(uint32_t)) return ParamStatus::InvalidType; memcpy(out, &m_baud, sizeof(uint32_t)); out_len = sizeof(uint32_t); return ParamStatus::Ok; } ParamStatus set_baud(const uint8_t* in, uint16_t in_len, bool& reboot_required) { if (in_len != sizeof(uint32_t)) return ParamStatus::InvalidType; uint32_t v; memcpy(&v, in, sizeof(uint32_t)); if (v < 9600 || v > 3000000) return ParamStatus::InvalidValue; m_baud = v; reboot_required = true; // takes effect after re-init return ParamStatus::Ok; } private: char* m_root_namespace; uint32_t m_baud = 115200; Serial* m_serial; }; ``` **Why it’s useful** * Keeps **hardware side-effects** localized (re-init serial, reconfigure I2C, etc.). * Does not require any change to driver code * Multiple modules can **share** one adapter’s namespace (`serial.*`, `imu.*`). * Allows easy setup for *multiple hardware instances* (`serial_pilot`, `imu_leftwing`) * Adapters register like modules; the manager treats them equivalently. --- ## 6) Boot Sequence (step-by-step) 1. **Construct** the global singleton: ```cpp auto& paramManager = ParameterManager::instance(); ``` 2. **Initialize Chirp and register callback:** ```cpp paramManager.init(chirp_handler); // internally: chirp_handler.register_callback(OPCODE_PARAM_REQUEST, &ParameterManager::chirp_param_request_callback); ``` 3. **Create modules and adapters using factory method** (AttitudeController, Navigator, Logger, SerialParamAdapter, etc.). 4. **Register parameters**: Happens when modules are created, most likely in factory class. ```cpp attitudeController.enumerate_params(paramManager.registry()); logger.enumerate_params(paramManager.registry()); serialAdapter.enumerate_params(paramManager.registry()); // ... others ``` 5. **Apply defaults** (optional): * Iterate all descriptors and call their `setter(default_bytes, length)` to seed modules with defaults **once** at boot. 6. **Ready for runtime**: FC now answers Chirp Get/Set by **name**. --- ## 7) Runtime Update Flow (example) **Scenario:** ground station sets `attitudectrl.kp_roll = 18.0f`. 1. **Chirp RX:** `ChirpHandler` parses an incoming `ParameterRequest` with: * `op = Set` * `name = "attitudectrl.kp_roll"` * `type = Float` * `length = 4` * `value = { bytes of 18.0f }` * `transaction_id` = 42 (example) 2. **Callback dispatch:** `ChirpHandler` calls `ParameterManager::chirp_param_request_callback(req)`. 3. **Manager logic (simplified):** ```cpp void ParameterManager::chirp_param_request_callback(const chirp::ParameterRequest& req) { chirp::ParameterResponse resp{}; resp.transaction_id = req.transaction_id; resp.timestamp = micros64(); // or timeManager.time() etc. if (req.op == ParamOp::Set) { bool reboot_required = false; ParamStatus st = set_by_name((const char*)req.name, req.value, req.length, req.type, reboot_required); resp.status = to_wire_status(st, reboot_required); resp.name_size = req.name_size; memcpy(resp.name, req.name, req.name_size); // Echo type/length; optionally include the applied value via a getter or copy directly from request send_parameter_response(resp); } else if (req.op == ParamOp::Get) { uint8_t buf[256]; uint16_t len = sizeof(buf); TypeId t; ParamStatus st = get_by_name((const char*)req.name, buf, len, t); resp.status = to_wire_status(st, /*reboot_required*/false); resp.type = t; resp.length = len; memcpy(resp.value, buf, len); send_parameter_response(resp); } } ``` 4. **Validation path inside `set_by_name`:** * **Lookup**: registry.find(name) → descriptor `d`; if not found → `NotFound`. * **Type/length check**: `req.type == d.type` and `req.length == d.length`; else → `InvalidType`. * **Access check**: * If `!(d.flags & Writable)` → `AccessDenied`. * If `is_armed() && (d.flags & FlightLocked)` → `AccessDenied`. * **Apply**: * Call `d.setter(req.value, req.length, reboot_required)`. * If setter returns `InvalidValue` (e.g., out of bounds) → reply `InvalidValue`. * If success and `(d.flags & RebootRequired) || reboot_required` → reply `RebootRequired`. * Else → reply `Ok`. 5. **Module effect:** * AttitudeController’s `m_kp_roll` is updated to 18.0f and will be used by the next control step. --- ## 8) Validation Layers (where checks live) 1. **Manager-level (generic)** * Name exists in registry. * Type/length match the descriptor. * Flags / access policy (Writable, FlightLocked). 2. **Module-level (semantic)** * Bounds and relationships specific to the parameter. (Outside of min-max range, must be non-zero etc.) **Tiny helper for type/length check** ```cpp static bool check_wire_matches_desc(TypeId wire_type, uint16_t wire_len, const ParamDescriptor& d) { return (wire_type == d.type) && (wire_len == d.length); } ``` --- ## 9) Example: Wiring it Together **Register the Chirp callback once** ```cpp void ParameterManager::init(ChirpHandler& chirp) { chirp.register_callback(chirp::Opcode::ParameterRequest, etl::delegate::create(this)); } void ParameterManager::on_any_message(const chirp::MessageUnpacked& msg) { // translate msg → ParameterRequest or ignore if not ParamRequest chirp::ParameterRequest req; if (!decode_parameter_request(msg, req)) return; chirp_param_request_callback(req); } ``` **Set/Get APIs** ```cpp ParamStatus ParameterManager::set_by_name(const char* name, const uint8_t* in, uint16_t in_len, TypeId type, bool& reboot_req) { reboot_req = false; const ParamDescriptor* d = m_reg.find(name); if (!d) return ParamStatus::NotFound; if (!check_wire_matches_desc(type, in_len, *d)) return ParamStatus::InvalidType; if (!(d->flags & (uint8_t)ParamFlag::Writable)) return ParamStatus::AccessDenied; if (is_armed() && (d->flags & (uint8_t)ParamFlag::FlightLocked)) return ParamStatus::AccessDenied; ParamStatus st = d->setter(in, in_len, reboot_req); return st; } ParamStatus ParameterManager::get_by_name(const char* name, uint8_t* out, uint16_t& out_len, TypeId& out_type) const { const ParamDescriptor* d = m_reg.find(name); if (!d) return ParamStatus::NotFound; out_type = d->type; return d->getter(out, out_len); } ``` --- ## 10) Non-Module/System Parameters Treat these as **their own roots** or grouped under `system.*` / `logger.*` / `build.*`, etc. * `logger.level` — writable, immediate. * `system.flight_mode` — writable, may be `FlightLocked`. * `build.git_sha` — read-only (no `Writable` flag). * `serial_pilot.baud_rate` — `RebootRequired` (or requires port re-init), adapter-owned. They register exactly like module parameters (same `ParamDescriptor` pattern). --- ## 11) Future Considerations * Parameter **Store** and **StagingBuffer** (two-phase commit). → initial version applies immediately; I'd be a good idea to consider staging the changes and applying in a safe state for later versions. * Full-list enumeration & chunking (GetFullList) for sharing the parameter list with other devices. * Persistence to Flash/EEPROM --- ## 12) Checklist to Implement 1. Implement **ParamRegistry** with ETL containers. 2. Create **ParameterManager** singleton; wire to **ChirpHandler**. 3. Convert 1–2 modules (e.g., `attitudectrl`, `navigator`) to implement **enumerate\_params** and delegate setters/getters. 4. Add a **SerialParamAdapter** as a reference hardware example. 5. Bring up **Get/Set by name** end-to-end and test on PC and on Teensy. --- ## TODOs Add the full list of current parameters in flight controller, to this document. Try to fit them in currently proposed structure with proper naming, flags.