From 00b5fd1b99877ac09ca747f2b30b727a8dfc8077 Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Sun, 19 Jul 2026 23:25:45 -0700 Subject: [PATCH 1/8] AIM Network: Migrate av-libraries to v0.7.0 protocol --- .../examples/esp32_canbus/src/main.cpp | 5 +- .../examples/stm32_canbus/src/main.cpp | 5 +- aim_network/src/aim_can_driver.cpp | 170 ------------------ aim_network/src/aim_can_driver.h | 56 ------ aim_network/src/aim_catalog.h | 54 ++---- aim_network/src/aim_control.cpp | 8 +- aim_network/src/aim_esp32_can_core.cpp | 5 +- aim_network/src/aim_esp32_can_core.h | 10 +- aim_network/src/aim_network.cpp | 55 +++++- aim_network/src/aim_network.h | 82 ++++++--- aim_network/src/aim_stm32_can_core.cpp | 18 +- aim_network/src/aim_stm32_can_core.h | 20 +-- 12 files changed, 163 insertions(+), 325 deletions(-) delete mode 100644 aim_network/src/aim_can_driver.cpp delete mode 100644 aim_network/src/aim_can_driver.h diff --git a/aim_network/examples/esp32_canbus/src/main.cpp b/aim_network/examples/esp32_canbus/src/main.cpp index 861c881..4656a62 100644 --- a/aim_network/examples/esp32_canbus/src/main.cpp +++ b/aim_network/examples/esp32_canbus/src/main.cpp @@ -1,8 +1,7 @@ -// AIM Network v0.6.x minimal example — ESP32 listener. +// AIM Network v0.7.0 minimal example — ESP32 listener. // Receives sensor, time, and heartbeat frames and logs them; heartbeats itself // via service(). Pairs with the STM32 publisher example. -#include #include #include @@ -14,7 +13,7 @@ static constexpr int kCanTxPin = 1; static constexpr uint8_t kMaxRxFramesPerLoop = 8U; static Logger g_log(Serial, static_cast(aim::Source::Lcm), LogLevel::INFO); -static AimCanDriver g_canHw(kCanBaud, kCanRxPin, kCanTxPin); +static AimCanHardware g_canHw(kCanBaud, kCanRxPin, kCanTxPin); static AimNetwork g_aim(&g_canHw, aim::Source::Lcm); void setup(void) { diff --git a/aim_network/examples/stm32_canbus/src/main.cpp b/aim_network/examples/stm32_canbus/src/main.cpp index 1adc7e9..58d1325 100644 --- a/aim_network/examples/stm32_canbus/src/main.cpp +++ b/aim_network/examples/stm32_canbus/src/main.cpp @@ -1,8 +1,7 @@ -// AIM Network v0.6.x minimal example — STM32 publisher + time master. +// AIM Network v0.7.0 minimal example — STM32 publisher + time master. // Sends a fake altitude ramp at 10 Hz and a TIME sync at 1 Hz, heartbeats via // service(), and logs heartbeats received from other nodes. -#include #include #include #include @@ -16,7 +15,7 @@ static aim::Job g_tick1000{1000U, 0U}; static constexpr uint8_t kMaxRxFramesPerLoop = 8U; static Logger g_log(Serial, static_cast(aim::Source::Ucm), LogLevel::INFO); -static AimCanDriver g_canHw(kCanBaud, CAN1); +static AimCanHardware g_canHw(kCanBaud, CAN1); static AimNetwork g_aim(&g_canHw, aim::Source::Ucm); void setup(void) { diff --git a/aim_network/src/aim_can_driver.cpp b/aim_network/src/aim_can_driver.cpp deleted file mode 100644 index ac67345..0000000 --- a/aim_network/src/aim_can_driver.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "aim_can_driver.h" - -#include -#include - -#if defined(ARDUINO_ARCH_STM32) - -AimCanDriver::AimCanDriver(uint32_t baud, CAN_TypeDef* canbus) - : _initialized(false), - _canCore(baud, canbus) { -} - -void AimCanDriver::getStm32Stats(AimStm32CanCore::Stats& stats) const { - _canCore.getStats(stats); -} - -void AimCanDriver::clearStm32Stats() { - _canCore.clearStats(); -} - -#elif defined(ARDUINO_ARCH_ESP32) - -AimCanDriver::AimCanDriver(uint32_t baud, int rxPin, int txPin) - : _initialized(false), - _canCore(baud, rxPin, txPin) { -} - -void AimCanDriver::getEsp32Stats(AimEsp32CanCore::Stats& stats) const { - _canCore.getStats(stats); -} - -void AimCanDriver::clearEsp32Stats() { - _canCore.clearStats(); -} - -#endif - -bool AimCanDriver::packMsg(const aim::Msg& msg, CanCoreFrame& can_msg) { - AIM_ASSERT(static_cast(msg.source) != 0U); - - const uint8_t prio = aim::priorityFor(msg.cls, msg.subject); - can_msg.id = aim::encodeId(prio, msg.cls, msg.subject, msg.source); - can_msg.dlc = 8U; - - // Both MCUs are little-endian — straight copies, no byte swapping (spec). - (void)memcpy(&can_msg.data[0], &msg.timestampMs, sizeof(msg.timestampMs)); - (void)memcpy(&can_msg.data[4], msg.b, sizeof(msg.b)); - return true; -} - -bool AimCanDriver::unpackMsg(const CanCoreFrame& can_msg, aim::Msg& msg) { - AIM_ASSERT((can_msg.id & ~aim::kExtIdMask) == 0U); - - if (can_msg.dlc != 8U) { - return false; - } - - uint8_t prio = 0U; - if (!aim::decodeId(can_msg.id, msg, prio)) { - return false; - } - - (void)memcpy(&msg.timestampMs, &can_msg.data[0], sizeof(msg.timestampMs)); - (void)memcpy(msg.b, &can_msg.data[4], sizeof(msg.b)); - return true; -} - -void AimCanDriver::logFailure(bool isBeginFailure, uint32_t canId) const { - (void)isBeginFailure; - (void)canId; - -#ifndef FLIGHT_BUILD - const char* const op = isBeginFailure ? "begin" : "tx"; - -#if defined(ARDUINO_ARCH_STM32) - AimStm32CanCore::Stats stats = {}; - _canCore.getStats(stats); - LOG_ERROR( - "AimCanDriver fail op=%s id=0x%08lX txOk=%lu txErr=%lu rxOk=%lu rxErr=%lu drops=%lu busOff=%lu warn=%lu passive=%lu err=0x%08lX esr=0x%08lX", - op, - static_cast(canId), - static_cast(stats.txFrames), - static_cast(stats.txHalErrors), - static_cast(stats.rxFrames), - static_cast(stats.rxHalErrors), - static_cast(stats.txQueueDrops), - static_cast(stats.busOffEvents), - static_cast(stats.errorWarningEvents), - static_cast(stats.errorPassiveEvents), - static_cast(stats.lastHalError), - static_cast(stats.lastEsr)); -#elif defined(ARDUINO_ARCH_ESP32) - AimEsp32CanCore::Stats stats = {}; - _canCore.getStats(stats); - LOG_ERROR( - "AimCanDriver fail op=%s id=0x%08lX txOk=%lu txErr=%lu rxOk=%lu rxErr=%lu drops=%lu filtered=%lu busOff=%lu err=0x%08lX", - op, - static_cast(canId), - static_cast(stats.txFrames), - static_cast(stats.txErrors), - static_cast(stats.rxFrames), - static_cast(stats.rxErrors), - 0UL, - static_cast(stats.filteredFrames), - static_cast(stats.busOffRecoveries), - static_cast(stats.lastError)); -#endif - -#endif // FLIGHT_BUILD -} - -bool AimCanDriver::begin(uint16_t classAcceptMask) { - if (_initialized) { - return true; - } - - if (!_canCore.setClassMask(classAcceptMask)) { - LOG_ERROR("AimCanDriver begin failed: invalid class mask=0x%04X", static_cast(classAcceptMask)); - return false; - } - - _initialized = _canCore.begin(); - if (!_initialized) { - logFailure(true); - } - - return _initialized; -} - -bool AimCanDriver::transmit(const aim::Msg& msg) { - if (!_initialized) { - LOG_ERROR("AimCanDriver transmit failed: driver not initialized"); - return false; - } - - CanCoreFrame can_msg = {}; - if (!packMsg(msg, can_msg)) { - LOG_ERROR("AimCanDriver transmit failed: packMsg rejected frame"); - return false; - } - - const bool sent = _canCore.transmit(can_msg); - if (!sent) { - logFailure(false, can_msg.id); - } - - return sent; -} - -bool AimCanDriver::receive(aim::Msg& msg) { - if (!_initialized) { - LOG_ERROR("AimCanDriver receive failed: driver not initialized"); - return false; - } - - CanCoreFrame can_msg = {}; - if (!_canCore.receive(can_msg)) { - return false; - } - - if (!unpackMsg(can_msg, msg)) { - LOG_ERROR( - "AimCanDriver receive failed: unpack rejected id=0x%08lX dlc=%u", - static_cast(can_msg.id), - static_cast(can_msg.dlc)); - return false; - } - - return true; -} diff --git a/aim_network/src/aim_can_driver.h b/aim_network/src/aim_can_driver.h deleted file mode 100644 index 770491c..0000000 --- a/aim_network/src/aim_can_driver.h +++ /dev/null @@ -1,56 +0,0 @@ -// aim_can_driver.h — Platform-aware CAN driver for AimNetwork -// Supports STM32 (HAL core in AimStm32CanCore) and ESP32 (TWAI) - -#ifndef AIM_CAN_DRIVER_H -#define AIM_CAN_DRIVER_H - -#include "aim_network.h" - -#if defined(ARDUINO_ARCH_STM32) - #include "aim_stm32_can_core.h" -#elif defined(ARDUINO_ARCH_ESP32) - #include "aim_esp32_can_core.h" -#else - #error "AimCanDriver: Unsupported platform" -#endif - - -class AimCanDriver { -public: - -#if defined(ARDUINO_ARCH_STM32) - AimCanDriver(uint32_t baud, CAN_TypeDef* canbus); - - void getStm32Stats(AimStm32CanCore::Stats& stats) const; - void clearStm32Stats(); -#elif defined(ARDUINO_ARCH_ESP32) - AimCanDriver(uint32_t baud, int rxPin = -1, int txPin = -1); - - void getEsp32Stats(AimEsp32CanCore::Stats& stats) const; - void clearEsp32Stats(); -#endif - - // classAcceptMask: OR of aim::classBit() values this node receives. - bool begin(uint16_t classAcceptMask); - bool transmit(const aim::Msg& msg); - bool receive(aim::Msg& msg); - -private: - bool _initialized; - -#if defined(ARDUINO_ARCH_STM32) - using CanCoreFrame = AimStm32CanCore::Frame; - AimStm32CanCore _canCore; - -#elif defined(ARDUINO_ARCH_ESP32) - using CanCoreFrame = AimEsp32CanCore::Frame; - AimEsp32CanCore _canCore; -#endif - - static bool packMsg(const aim::Msg& msg, CanCoreFrame& can_msg); - static bool unpackMsg(const CanCoreFrame& can_msg, aim::Msg& msg); - - void logFailure(bool isBeginFailure, uint32_t canId = 0U) const; -}; - -#endif // AIM_CAN_DRIVER_H diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index bdf86cc..ea70db4 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -12,20 +12,20 @@ namespace aim { // Wire schema version, carried in every HEARTBEAT frame. Independent of the // library semver in library.json — do not merge them. -static constexpr uint8_t kSchemaVersion = 2U; +static constexpr uint8_t kSchemaVersion = 3U; // --- CAN ID fields (29-bit extended ID) --- -// | 28:27 prio | 26:23 class | 22:15 subject | 14:11 source | 10:0 reserved=0 | +// | 28:25 class | 24:17 subject | 16:13 source | 12:0 reserved=0 | enum class Class : uint8_t { - Cmd = 0x0, // UCM → LCM only - Ack = 0x1, // LCM → UCM only - State = 0x2, // valve owner → all - Sensor = 0x3, // owner → all - Time = 0x4, // time master → all - Heartbeat = 0x5, // each node → all - Event = 0x6, // owner → all - Debug = 0x7, // any; never parsed by flight logic + Event = 0x0, // Highest priority (SafeStateEntered clears the bus immediately) + Time = 0x1, // High priority to minimize clock synchronization jitter/error + Cmd = 0x2, + Ack = 0x3, + State = 0x4, + Sensor = 0x5, + Heartbeat = 0x6, + Debug = 0x7, // Lowest priority }; enum class Source : uint8_t { @@ -43,7 +43,6 @@ namespace subject { // Valves (shared across CMD / ACK / STATE) static constexpr uint8_t Heartbeat = 0x00; static constexpr uint8_t Av204 = 0x01; // UCM, Vent valve -static constexpr uint8_t AvSpare = 0x02; // UCM, Ground side valve now DON'T ENERGIZE static constexpr uint8_t Av203 = 0x03; // LCM, CAN-commanded static constexpr uint8_t Av205 = 0x04; // LCM, CAN-commanded // Power FETs @@ -52,15 +51,12 @@ static constexpr uint8_t PwrSolLcm = 0x06; static constexpr uint8_t PwrPtLcm = 0x07; // Sensors (value = i32, scaling fixed here) static constexpr uint8_t Pt202 = 0x10; // UCM, PSI x100 -static constexpr uint8_t PtSpare1 = 0x11; // UCM, PSI x100, no longer needed static constexpr uint8_t Pt204 = 0x12; // LCM, PSI x100 -static constexpr uint8_t PtSpare2 = 0x13; // LCM, PSI x100, no longer needed static constexpr uint8_t TcLowerValve = 0x18; // LCM, Celsius x100 static constexpr uint8_t Volt24Ucm = 0x20; // mV static constexpr uint8_t VoltSolLcm = 0x21; // mV static constexpr uint8_t BattVolt = 0x28; // mV -static constexpr uint8_t GpsLat = 0x30; // degrees x10^7 -static constexpr uint8_t GpsLon = 0x31; // degrees x10^7 +static constexpr uint8_t GpsPosition = 0x30; // Fused GPS Position (lat: b[0..3], lon: b[4..7]) static constexpr uint8_t GpsNumSats = 0x32; // count static constexpr uint8_t Altitude = 0x38; // meters x100 // Events @@ -71,26 +67,6 @@ static constexpr uint8_t SafeStateEntered = 0x42; // detail: reason code static constexpr uint8_t TimeSync = 0x50; } // namespace subject -// Priority is fixed per message definition — never chosen by callers. -// 0 = safety/control, 1 = coordination, 2 = fast telemetry, 3 = background. -static inline uint8_t priorityFor(Class cls, uint8_t subj) { - switch (cls) { - case Class::Cmd: - case Class::Ack: - return 0U; - case Class::Event: - return (subj == subject::SafeStateEntered) ? 0U : 3U; - case Class::Time: - case Class::State: - return 1U; - case Class::Sensor: - // INFO: higher priority for LCM sensors - return (subj >= subject::Pt204) ? 2U : 3U; - default: - return 3U; - } -} - // --- Payload enums (bytes 4–7, layouts per class in the protocol doc) --- enum class ControlState : uint8_t { @@ -114,6 +90,14 @@ enum class NodeState : uint8_t { Fault = 3, }; +static inline constexpr uint16_t classBit(Class cls) { + return static_cast(1U << static_cast(cls)); +} + +static inline constexpr bool isZeroTimestamp(Class cls, uint8_t subject) { + return (cls == Class::Sensor && subject == subject::GpsPosition); +} + } // namespace aim #endif // AIM_CATALOG_H diff --git a/aim_network/src/aim_control.cpp b/aim_network/src/aim_control.cpp index c3c5476..5d16e0c 100644 --- a/aim_network/src/aim_control.cpp +++ b/aim_network/src/aim_control.cpp @@ -127,7 +127,9 @@ void controlServiceTx(Control& c, uint32_t nowMs, AimNetwork& aim) { cmd.cls = Class::Cmd; cmd.subject = c.subject; cmd.b[0] = c.seq; - cmd.b[1] = c.desiredOpen ? static_cast(ControlState::Open) : static_cast(ControlState::Closed); + cmd.b[1] = c.desiredOpen ? + static_cast(ControlState::Open) + : static_cast(ControlState::Closed); if (aim.send(cmd)) { c.dirty = false; c.lastSentMs = nowMs; @@ -138,7 +140,9 @@ void controlBuildState(const Control& c, aim::Msg& out) { out = aim::Msg{}; out.cls = Class::State; out.subject = c.subject; - out.b[0] = c.state ? static_cast(ControlState::Open) : static_cast(ControlState::Closed); + out.b[0] = c.state ? + static_cast(ControlState::Open) + : static_cast(ControlState::Closed); out.b[1] = c.energized ? 1U : 0U; out.b[2] = static_cast(c.hall); } diff --git a/aim_network/src/aim_esp32_can_core.cpp b/aim_network/src/aim_esp32_can_core.cpp index 6bbc2c8..6c8490a 100644 --- a/aim_network/src/aim_esp32_can_core.cpp +++ b/aim_network/src/aim_esp32_can_core.cpp @@ -1,4 +1,5 @@ #include "aim_esp32_can_core.h" +#include "aim_network.h" #if defined(ARDUINO_ARCH_ESP32) @@ -194,7 +195,7 @@ bool AimEsp32CanCore::recoverBusOff() { return begin(); } -bool AimEsp32CanCore::transmit(const Frame& frame) { +bool AimEsp32CanCore::transmit(const aim::Frame& frame) { AIM_ASSERT(frame.dlc <= 8U); if (!_initialized) { @@ -261,7 +262,7 @@ bool AimEsp32CanCore::shouldAcceptId(const uint32_t id) const { return (src != 0U) && ((_classMask & (1U << cls)) != 0U); } -bool AimEsp32CanCore::receive(Frame& frame) { +bool AimEsp32CanCore::receive(aim::Frame& frame) { AIM_ASSERT(_classMask != 0U); if (!_initialized) { diff --git a/aim_network/src/aim_esp32_can_core.h b/aim_network/src/aim_esp32_can_core.h index 47206ed..07afea1 100644 --- a/aim_network/src/aim_esp32_can_core.h +++ b/aim_network/src/aim_esp32_can_core.h @@ -3,17 +3,15 @@ #if defined(ARDUINO_ARCH_ESP32) -#include "aim_network.h" +#include "aim_can_frame.h" +#include "aim_catalog.h" #include "aim_safety.h" -#include "aim_can_frame.h" #include #include class AimEsp32CanCore { public: - using Frame = aim::Frame; - struct Stats { uint32_t txFrames; uint32_t rxFrames; @@ -35,8 +33,8 @@ class AimEsp32CanCore { bool setClassMask(uint16_t mask); bool begin(); - bool transmit(const Frame& frame); - bool receive(Frame& frame); + bool transmit(const aim::Frame& frame); + bool receive(aim::Frame& frame); void getStats(Stats& stats) const; void clearStats(); diff --git a/aim_network/src/aim_network.cpp b/aim_network/src/aim_network.cpp index e3b6080..a3031a5 100644 --- a/aim_network/src/aim_network.cpp +++ b/aim_network/src/aim_network.cpp @@ -1,14 +1,20 @@ #include "aim_network.h" -#include "aim_can_driver.h" +#if defined(ARDUINO_ARCH_STM32) + #include "aim_stm32_can_core.h" +#elif defined(ARDUINO_ARCH_ESP32) + #include "aim_esp32_can_core.h" +#endif + #include "aim_safety.h" #include -AimNetwork::AimNetwork(AimCanDriver* hardware, aim::Source self) +AimNetwork::AimNetwork(AimCanHardware* hardware, aim::Source self) : _hw(hardware), _self(self), _timeOffset(0), - _lastTxMs(0U) { + _lastTxMs(0U), + _lastSyncTimeMs(0U) { AIM_ASSERT(static_cast(self) != 0U); AIM_ASSERT(static_cast(self) <= 0xEU); } @@ -19,7 +25,12 @@ bool AimNetwork::begin(uint16_t classAcceptMask) { return false; } - return _hw->begin(classAcceptMask); + if (!_hw->setClassMask(classAcceptMask)) { + LOG_ERROR("AimNetwork begin failed: setClassMask rejected mask=0x%04X", static_cast(classAcceptMask)); + return false; + } + + return _hw->begin(); } bool AimNetwork::send(aim::Msg& m) { @@ -31,7 +42,24 @@ bool AimNetwork::send(aim::Msg& m) { m.source = _self; m.timestampMs = syncedMillis(); - const bool sent = _hw->transmit(m); + if ((m.cls == aim::Class::Time) && (m.subject == aim::subject::TimeSync)) { + (void)memcpy(m.b, &m.timestampMs, sizeof(m.timestampMs)); + m.offsetMs = 0; + _lastSyncTimeMs = m.timestampMs; + } else if (aim::isZeroTimestamp(m.cls, m.subject)) { + m.offsetMs = 0; + } else { + uint32_t baseTime = (_lastSyncTimeMs == 0U) ? 0U : _lastSyncTimeMs; + uint32_t offset = m.timestampMs - baseTime; + m.offsetMs = static_cast(offset & 0xFFFFU); + } + + aim::Frame frame = {}; + if (!aim::packMsg(m, frame)) { + return false; + } + + const bool sent = _hw->transmit(frame); if (sent) { _lastTxMs = millis(); // local clock on purpose — synced time steps } else { @@ -47,14 +75,27 @@ bool AimNetwork::receive(aim::Msg& m) { return false; } - if (!_hw->receive(m)) { + aim::Frame frame = {}; + if (!_hw->receive(frame)) { + return false; + } + + if (!aim::unpackMsg(frame, m)) { return false; } // Only the TimeSync subject disciplines the clock; other (future) Time-class // subjects must not step every clock on the bus. if ((m.cls == aim::Class::Time) && (m.subject == aim::subject::TimeSync)) { - syncTime(m.timestampMs); + uint32_t remoteTimeMs = 0U; + (void)memcpy(&remoteTimeMs, m.b, sizeof(remoteTimeMs)); + syncTime(remoteTimeMs); + _lastSyncTimeMs = remoteTimeMs; + m.timestampMs = remoteTimeMs; + } else if (aim::isZeroTimestamp(m.cls, m.subject)) { + m.timestampMs = syncedMillis(); + } else { + m.timestampMs = _lastSyncTimeMs + m.offsetMs; } return true; diff --git a/aim_network/src/aim_network.h b/aim_network/src/aim_network.h index b54cbbb..8bc03d1 100644 --- a/aim_network/src/aim_network.h +++ b/aim_network/src/aim_network.h @@ -1,6 +1,6 @@ -// aim_network.h — AIM Network v0.6.x protocol layer. -// 29-bit extended CAN ID: prio[2] | class[4] | subject[8] | source[4] | reserved[11]=0. -// Payload: bytes 0–3 = uint32 LE ms-since-start-of-day timestamp, bytes 4–7 per class. +// aim_network.h — AIM Network v0.7.0 protocol layer. +// 29-bit flat extended CAN ID: class[4] | subject[8] | source[4] | reserved[13]=0. +// Standard Payload: bytes 0-1 = uint16 relative offsetMs, bytes 2-7 per class. #ifndef AIM_NETWORK_H #define AIM_NETWORK_H @@ -10,26 +10,39 @@ #include #include "aim_catalog.h" +#include "aim_can_frame.h" +#include "aim_safety.h" + +// Conditional compilation mapping for the core hardware drivers. +#if defined(ARDUINO_ARCH_STM32) + #include "aim_stm32_can_core.h" + using AimCanHardware = AimStm32CanCore; +#elif defined(ARDUINO_ARCH_ESP32) + #include "aim_esp32_can_core.h" + using AimCanHardware = AimEsp32CanCore; +#else + class AimCanHardware; +#endif namespace aim { -static constexpr char kNetworkVersionString[] = "0.6.0"; +static constexpr char kNetworkVersionString[] = "0.7.0"; static constexpr uint32_t kHeartbeatTxIntervalMs = 1000U; // --- CAN ID layout --- -static constexpr uint8_t kIdPrioShift = 27U; -static constexpr uint8_t kIdClassShift = 23U; -static constexpr uint8_t kIdSubjectShift = 15U; -static constexpr uint8_t kIdSourceShift = 11U; +static constexpr uint8_t kIdClassShift = 25U; +static constexpr uint8_t kIdSubjectShift = 17U; +static constexpr uint8_t kIdSourceShift = 13U; static constexpr uint32_t kExtIdMask = 0x1FFFFFFFU; struct Msg { Class cls = Class::Debug; uint8_t subject = 0U; Source source = static_cast(0U); // set by AimNetwork::send() - uint32_t timestampMs = 0U; // stamped by send() from syncedMillis() - uint8_t b[4] = {}; // bytes 4–7, layout per class: + uint32_t timestampMs = 0U; // Memory-only timestamp (reconstructed on RX, syncedMillis on TX) + uint16_t offsetMs = 0U; // 16-bit relative millisecond offset (carried on wire) + uint8_t b[8] = {}; // bytes 2–7 (or 0-7 if zero-timestamp), layout per class: // CMD b[0]=seq b[1]=desired_state // ACK b[0]=seq b[1]=result // STATE b[0]=commanded b[1]=energized b[2]=hall @@ -49,40 +62,64 @@ struct Msg { } }; -static inline uint32_t encodeId(uint8_t prio, Class cls, uint8_t subj, Source src) { - return (static_cast(prio & 0x3U) << kIdPrioShift) | - (static_cast(static_cast(cls) & 0xFU) << kIdClassShift) | +static inline uint32_t encodeId(Class cls, uint8_t subj, Source src) { + return (static_cast(static_cast(cls) & 0xFU) << kIdClassShift) | (static_cast(subj) << kIdSubjectShift) | (static_cast(static_cast(src) & 0xFU) << kIdSourceShift); } -// Reserved bits (10:0) are masked off and ignored per protocol invariant. +// Reserved bits (12:0) are masked off and ignored per protocol invariant. // Returns false only on source == 0 (wiring-fault canary). -static inline bool decodeId(uint32_t id, Msg& m, uint8_t& prio) { +static inline bool decodeId(uint32_t id, Msg& m) { const uint8_t src = static_cast((id >> kIdSourceShift) & 0xFU); if (src == 0U) { return false; } - prio = static_cast((id >> kIdPrioShift) & 0x3U); m.cls = static_cast((id >> kIdClassShift) & 0xFU); m.subject = static_cast((id >> kIdSubjectShift) & 0xFFU); m.source = static_cast(src); return true; } -static constexpr uint16_t classBit(Class cls) { - return static_cast(1U << static_cast(cls)); +static inline bool packMsg(const Msg& msg, Frame& frame) { + AIM_ASSERT(static_cast(msg.source) != 0U); + frame.id = encodeId(msg.cls, msg.subject, msg.source); + frame.dlc = 8U; + + if (isZeroTimestamp(msg.cls, msg.subject)) { + (void)memcpy(frame.data, msg.b, 8); + } else { + (void)memcpy(&frame.data[0], &msg.offsetMs, sizeof(msg.offsetMs)); + (void)memcpy(&frame.data[2], msg.b, 6); + } + return true; } -} // namespace aim +static inline bool unpackMsg(const Frame& frame, Msg& msg) { + AIM_ASSERT((frame.id & ~kExtIdMask) == 0U); + if (frame.dlc != 8U) { + return false; + } + if (!decodeId(frame.id, msg)) { + return false; + } + if (isZeroTimestamp(msg.cls, msg.subject)) { + (void)memcpy(msg.b, frame.data, 8); + msg.offsetMs = 0; + } else { + (void)memcpy(&msg.offsetMs, &frame.data[0], sizeof(msg.offsetMs)); + (void)memcpy(msg.b, &frame.data[2], 6); + } + return true; +} +} // namespace aim -class AimCanDriver; class AimNetwork { public: - AimNetwork(AimCanDriver* hardware, aim::Source self); + AimNetwork(AimCanHardware* hardware, aim::Source self); // Returns false on driver/filter init failure — apps must check. bool begin(uint16_t classAcceptMask); @@ -105,10 +142,11 @@ class AimNetwork { private: void syncTime(uint32_t remoteMillis); - AimCanDriver* _hw; + AimCanHardware* _hw; aim::Source _self; int32_t _timeOffset; uint32_t _lastTxMs; + uint32_t _lastSyncTimeMs; }; #endif // AIM_NETWORK_H diff --git a/aim_network/src/aim_stm32_can_core.cpp b/aim_network/src/aim_stm32_can_core.cpp index 3a076e6..a3791d2 100644 --- a/aim_network/src/aim_stm32_can_core.cpp +++ b/aim_network/src/aim_stm32_can_core.cpp @@ -1,4 +1,5 @@ #include "aim_stm32_can_core.h" +#include "aim_network.h" #if defined(ARDUINO_ARCH_STM32) @@ -160,7 +161,7 @@ AimStm32CanCore::AimStm32CanCore(uint32_t baud, CAN_TypeDef* canbus) _stats{}, _lastErrorFlags(0U), _hcan{} { - static_assert(sizeof(Frame::data) == 8U, "CAN frame data must be 8 bytes"); + static_assert(sizeof(aim::Frame::data) == 8U, "CAN frame data must be 8 bytes"); } bool AimStm32CanCore::setClassMask(uint16_t mask) { @@ -417,7 +418,7 @@ bool AimStm32CanCore::begin() { return true; } -bool AimStm32CanCore::enqueueTx(const Frame& frame) { +bool AimStm32CanCore::enqueueTx(const aim::Frame& frame) { const uint32_t primask = enterCritical(); if (_txCount >= kTxQueueSize) { _stats.txQueueDrops = _stats.txQueueDrops + 1U; @@ -432,7 +433,7 @@ bool AimStm32CanCore::enqueueTx(const Frame& frame) { return true; } -bool AimStm32CanCore::pushRx(const Frame& frame) { +bool AimStm32CanCore::pushRx(const aim::Frame& frame) { const uint32_t primask = enterCritical(); if (_rxCount >= kRxQueueSize) { _stats.rxQueueDrops = _stats.rxQueueDrops + 1U; @@ -448,7 +449,7 @@ bool AimStm32CanCore::pushRx(const Frame& frame) { return true; } -bool AimStm32CanCore::dequeueRx(Frame& frame) { +bool AimStm32CanCore::dequeueRx(aim::Frame& frame) { const uint32_t primask = enterCritical(); if (_rxCount == 0U) { exitCritical(primask); @@ -513,7 +514,7 @@ bool AimStm32CanCore::flushTxMailboxes() { break; } - Frame frame = _txQueue[_txTail]; + aim::Frame frame = _txQueue[_txTail]; _txTail = static_cast((_txTail + 1U) % kTxQueueSize); _txCount = static_cast(_txCount - 1U); @@ -533,6 +534,7 @@ bool AimStm32CanCore::flushTxMailboxes() { uint8_t payload[8] = {}; (void)memcpy(payload, frame.data, frame.dlc); + // why only mailbox zero here? uint32_t mailbox = 0U; const HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(&_hcan, &header, payload, &mailbox); @@ -573,7 +575,7 @@ bool AimStm32CanCore::pollRx() { } if ((header.IDE == CAN_ID_EXT) && (header.RTR == CAN_RTR_DATA) && (header.DLC <= 8U)) { - Frame frame = {}; + aim::Frame frame = {}; frame.id = header.ExtId & aim::kExtIdMask; frame.dlc = static_cast(header.DLC); (void)memcpy(frame.data, data, frame.dlc); @@ -587,7 +589,7 @@ bool AimStm32CanCore::pollRx() { return true; } -bool AimStm32CanCore::transmit(const Frame& frame) { +bool AimStm32CanCore::transmit(const aim::Frame& frame) { AIM_ASSERT(frame.dlc <= 8U); if (!_initialized) { @@ -607,7 +609,7 @@ bool AimStm32CanCore::transmit(const Frame& frame) { return flushTxMailboxes(); } -bool AimStm32CanCore::receive(Frame& frame) { +bool AimStm32CanCore::receive(aim::Frame& frame) { AIM_ASSERT(_canbus != nullptr); if (!_initialized) { diff --git a/aim_network/src/aim_stm32_can_core.h b/aim_network/src/aim_stm32_can_core.h index 89f196f..11b0259 100644 --- a/aim_network/src/aim_stm32_can_core.h +++ b/aim_network/src/aim_stm32_can_core.h @@ -1,7 +1,8 @@ #ifndef AIM_STM32_CAN_CORE_H #define AIM_STM32_CAN_CORE_H -#include "aim_network.h" +#include "aim_can_frame.h" +#include "aim_catalog.h" #include "aim_safety.h" #if defined(ARDUINO_ARCH_STM32) @@ -36,7 +37,6 @@ #endif #include #include -#include "aim_can_frame.h" #if defined(CAN1) #define AIM_STM32_DEFAULT_CANBUS CAN1 @@ -46,8 +46,6 @@ class AimStm32CanCore { public: - using Frame = aim::Frame; - struct Stats { uint32_t txFrames; uint32_t rxFrames; @@ -70,8 +68,8 @@ class AimStm32CanCore { bool setClassMask(uint16_t mask); bool begin(); - bool transmit(const Frame& frame); - bool receive(Frame& frame); + bool transmit(const aim::Frame& frame); + bool receive(aim::Frame& frame); void getStats(Stats& stats) const; void clearStats(); @@ -86,9 +84,9 @@ class AimStm32CanCore { static constexpr uint8_t kRxQueueSize = 16U; static constexpr uint8_t kMaxRxPollIterations = 8U; - bool enqueueTx(const Frame& frame); - bool dequeueRx(Frame& frame); - bool pushRx(const Frame& frame); + bool enqueueTx(const aim::Frame& frame); + bool dequeueRx(aim::Frame& frame); + bool pushRx(const aim::Frame& frame); bool flushTxMailboxes(); bool pollRx(); bool configureFilter(); @@ -103,12 +101,12 @@ class AimStm32CanCore { CAN_TypeDef* _canbus; bool _initialized; - Frame _txQueue[kTxQueueSize]; + aim::Frame _txQueue[kTxQueueSize]; uint8_t _txHead; uint8_t _txTail; uint8_t _txCount; - Frame _rxQueue[kRxQueueSize]; + aim::Frame _rxQueue[kRxQueueSize]; uint8_t _rxHead; uint8_t _rxTail; uint8_t _rxCount; From 6be07ce2478dd315ecc310e49221def8508521dd Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Sun, 19 Jul 2026 23:27:41 -0700 Subject: [PATCH 2/8] Docs: Update protocol specification to v0.7.0 --- aim_protocol_v0.6.0.md | 164 ----------------------------------------- aim_protocol_v0.7.0.md | 137 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 164 deletions(-) delete mode 100644 aim_protocol_v0.6.0.md create mode 100644 aim_protocol_v0.7.0.md diff --git a/aim_protocol_v0.6.0.md b/aim_protocol_v0.6.0.md deleted file mode 100644 index d1ca3b1..0000000 --- a/aim_protocol_v0.6.0.md +++ /dev/null @@ -1,164 +0,0 @@ -# AIM Network Protocol — v0.6.0 DRAFT (for review) - -CAN 2.0B, 29-bit extended ID, 500 kbps. All multi-byte fields **little-endian** -(decided: both MCUs are LE, no interop requirement with anyone else; zero byte-swapping). - -**Unique-ID invariant:** every frame carries its sender's source bits, therefore no two nodes -can ever transmit the same CAN ID. Any future ID-layout change must preserve this. - -Items marked **[?]** need confirmation. - ---- - -## 1. CAN ID Layout - -``` -| Bit 28:27 | Bit 26:23 | Bit 22:15 | Bit 14:11 | Bit 10:0 | -| Priority | Class | Subject | Source | Reserved = 0 | -| 2 bits | 4 bits | 8 bits | 4 bits | 11 bits | -``` - -Rules: - -- **Priority is fixed per message definition** (column in the catalog), never chosen at runtime. -- **Reserved bits are always 0.** Transmit 0, receivers ignore. Not allocated until a real need exists. -- **Subject identifies the data, not the route.** Same subject is reused across classes - (e.g. subject `VALVE_4` appears as CMD, ACK, and STATE). -- **Source is traceability, not routing.** No destination field; consumers filter by class/subject. - -### Priority levels (2 bits) - -| Value | Meaning | Traffic | -|---|---|---| -| 0 | Safety / control | SAFE_STATE event, valve CMD + ACK | -| 1 | Coordination | TIME sync, valve STATE | -| 2 | Fast telemetry | PTs | -| 3 | Background | TC, voltages, GPS, altitude, heartbeat, events, debug | - -### Class (4 bits) — 8 defined, 8 spare - -| Class | Name | Direction | -|---|---|---| -| 0x0 | CMD | UCM → LCM only | -| 0x1 | ACK | LCM → UCM only | -| 0x2 | STATE | valve owner → all | -| 0x3 | SENSOR | owner → all | -| 0x4 | TIME | time master → all | -| 0x5 | HEARTBEAT | each node → all | -| 0x6 | EVENT | owner → all | -| 0x7 | DEBUG | any (never parsed by flight logic) | - -### Source (4 bits) - -| ID | Node | -|---|---| -| 0x1 | Comms | -| 0x2 | UCM | -| 0x3 | LCM | -| 0x4 | Altimeter | -| 0x5 | GPS | -| 0x6 | Power | -| 0x7–0xE | spare (test rig, debug dongle, future boards) | - -0x0 unused (an all-zero ID field is a common wiring-fault symptom; don't make it valid). - ---- - -## 2. Data Field Layout (8 bytes, by class) - -All frames: **bytes 0–3 = timestamp, ms since start of day (uint32)**, from sender's synced clock. - -| Class | Bytes 4–7 | -|---|---| -| CMD | seq (u8), desired_state (u8), 0, 0 | -| ACK | seq (u8), result (u8), 0, 0 | -| STATE | commanded (u8), energized (u8), hall (u8), 0 | -| SENSOR | value (i32, scaling per subject) | -| TIME | flags (u8: bit0 = GPS-disciplined), 0, 0, 0 — timestamp field IS the payload | -| HEARTBEAT | node_state (u8), error_bits (u16), schema_version (u8) | -| EVENT | detail (u8, per subject), 0, 0, 0 | - -Control state enum (used in CMD desired, STATE commanded/hall): `0=CLOSED, 1=OPEN, 2=UNKNOWN, 3=FAULT`. -`energized`: `0=OFF, 1=ON`. Controls without feedback/sensing (e.g. N2 supply) report hall = UNKNOWN always. - -ACK result enum: `0=ACCEPTED, 1=REJECT_SAFE_STATE, 2=REJECT_BAD_SUBJECT, 3=REJECT_BAD_STATE_VALUE` -(spare values reserved for faults discovered in testing). - -**Command idempotency rule:** a CMD re-received with an already-processed seq number is re-ACKed -with the original result and otherwise ignored. UCM retry = timeout + resend, no handshake needed. - -Node state enum: `0=INIT, 1=NOMINAL, 2=SAFE_STATE, 3=FAULT` - ---- - -## 3. Subject Catalog - -### Controls (subjects shared by CMD / ACK / STATE) - -| Subject | Name | Board | Hall? | Fail-safe bias | CAN-commanded? | STATE rate | -|---|---|---|---|---|---|---| -| 0x01 | VALVE_1 | UCM | yes | open (spring) | no (Wi-Fi/local) | 1-2 Hz | -| 0x02 | VALVE_2 | UCM | no (COTS) | open (spring) | no (Wi-Fi/local) | 1-2 Hz | -| 0x03 | VALVE_3 | LCM | yes | open (N2O pressure) | **yes** | 1-2 Hz | -| 0x04 | VALVE_4 | LCM | yes | closed | **yes** | 1-2 Hz | - -*UCM publishes STATE for its own valves so Comms/LoRa sees all four through one mechanism.* - -### Sensors (class SENSOR, value = i32) - -| Subject | Name | Board | Units (scaling) | Rate | Prio | -|---|---|---|---|---|---| -| 0x10 | PT_1 | UCM | PSI ×100 | 50 Hz | 2 | -| 0x11 | PT_2 | UCM | PSI ×100 | 50 Hz | 2 | -| 0x12 | PT_3 | LCM | PSI ×100 | 50 Hz | 2 | -| 0x13 | PT_4 | LCM | PSI ×100 | 50 Hz | 2 | -| 0x18 | TC_CHAMBER | LCM | °C ×100 | 5 Hz | 3 | -| 0x20 | SOLENOID_VOLT_UCM | UCM | mV | 5 Hz | 3 | -| 0x21 | SOLENOID_VOLT_LCM | LCM | mV | 5 Hz | 3 | -| 0x28 | BATT_VOLT | Power | mV | 1 Hz | 3 | -| 0x30 | GPS_LAT | GPS | degrees ×10⁷ | 5 Hz | 3 | -| 0x31 | GPS_LON | GPS | degrees ×10⁷ | 5 Hz | 3 | -| 0x32 | GPS_NUM_SATS | GPS | count | 1 Hz | 3 | -| 0x38 | ALTITUDE | Altimeter | meters ×100 | 10 Hz | 3 | - -All sensor values are scaled signed integers — no floats on the wire. Scaling lives in -this table and the generated header, nowhere else. - -### Events (class EVENT, sent at prio 0 or 3 as listed; repeat 3× at 100 ms spacing — events are state changes, not single frames) - -| Subject | Name | Owner | detail byte | Prio | -|---|---|---|---|---| -| 0x40 | LOW_POWER | Power | 0=exit, 1=enter | 3 | -| 0x41 | LAUNCH_DETECT | Power | 1=detected | 3 | -| 0x42 | SAFE_STATE_ENTERED | any | reason code | 0 | - -### Time / Heartbeat - -| Subject | Name | Notes | -|---|---|---| -| 0x50 | TIME_SYNC | 1 Hz (50 ppm crystal drift ⇒ sub-ms skew). Master = UCM pre-launch, GPS post-launch (flag bit0 says which). | -| 0x00 | HEARTBEAT | Subject 0; source field identifies node. Sent only if node silent > T/2. | - ---- - -## 4. Liveness & Safing (summary of agreed behavior) - -- Liveness timer per watched node resets on **any valid frame** from it. -- LCM watches UCM; UCM watches LCM; Comms watches everyone (telemetry health only). -- Timeout → de-energize all local solenoids (hardware fail-safe positions). Timeout values TBD — consider different pad vs. flight values. -- Post-launch safing: LCM-local timer (4–5 min) armed by LAUNCH_DETECT; runs even if bus is dead. -- ACK means "command received and accepted" — never "valve moved." Physical confirmation is the - STATE frame (commanded / energized / hall), judged by humans at the GUI. - -## 5. Extensibility reserves (deliberate, do not fill without need) - -8 spare classes · ~230 spare subjects · 8 spare source IDs · 11 reserved ID bits · -schema_version in every heartbeat (bump on any catalog change; mismatch = ground abort item). - -## Open items - -1. PT names/locations ×4; all sensor rates -2. Heartbeat/liveness timeout values, pad vs flight -3. Who owns LAUNCH_DETECT (Power physical vs Altimeter accel) -4. Midnight rollover policy: clock steps backward at 00:00; post-processing must detect the - negative jump. Accepted quirk — written here so it's never a 2 a.m. mystery. diff --git a/aim_protocol_v0.7.0.md b/aim_protocol_v0.7.0.md new file mode 100644 index 0000000..8e8b221 --- /dev/null +++ b/aim_protocol_v0.7.0.md @@ -0,0 +1,137 @@ +# AIM Network Protocol — v0.7.0 DRAFT + +CAN 2.0B, 29-bit extended ID, 500 kbps. All multi-byte fields **little-endian** +(decided: both MCUs are LE, no interop requirement with anyone else; zero byte-swapping). + +**Unique-ID invariant:** every frame carries its sender's source bits, therefore no two nodes +can ever transmit the same CAN ID. Any future ID-layout change must preserve this. + +--- + +## 1. CAN ID Layout + +The 29-bit extended identifier is ordered as a flat sequence to natively dictate hardware arbitration priorities on the CAN bus: + +``` +| Bit 28:25 | Bit 24:17 | Bit 16:13 | Bit 12:0 | +| Class | Subject | Source | Reserved = 0 | +| 4 bits | 8 bits | 4 bits | 13 bits | +``` + +Rules: +- **Priority is implicit in the CAN ID.** Class determines priority first, followed by Subject, and then Source. +- **Reserved bits are always 0.** Transmit 0, receivers ignore. Not allocated until a real need exists. +- **Subject identifies the data, not the route.** Same subject is reused across classes + (e.g. subject `Av204` appears as CMD, ACK, and STATE). +- **Source is traceability, not routing.** No destination field; consumers filter by class/subject. + +### Class Priority (4 bits) + +| Class | Name | Direction | Description | +|---|---|---|---| +| 0x0 | EVENT | owner → all | Abort/Safety transition events (Highest Priority) | +| 0x1 | TIME | time master → all | Clock synchronization broadcasts | +| 0x2 | CMD | UCM → LCM only | Valve actuation commands | +| 0x3 | ACK | LCM → UCM only | Command receipt/result handshakes | +| 0x4 | STATE | valve owner → all | Valve physical position feedback | +| 0x5 | SENSOR | owner → all | Transducer, thermocouple, and health telemetry | +| 0x6 | HEARTBEAT | each node → all | Routine node status reports | +| 0x7 | DEBUG | any | Diagnostic output (never parsed by flight logic) | + +### Source Node ID (4 bits) + +| ID | Node | +|---|---| +| 0x1 | Comms | +| 0x2 | UCM | +| 0x3 | LCM | +| 0x4 | Altimeter | +| 0x5 | GPS | +| 0x6 | Power | +| 0x7–0xE | spare (test rig, debug dongle, future boards) | + +*0x0 is unused (an all-zero ID field is a common wiring-fault symptom; don't make it valid).* + +--- + +## 2. Data Field Layout (8 bytes, by class) + +Standard payloads use a **16-bit millisecond offset** in bytes 0-1 (relative to the 1 Hz `TimeSync` master frame) to reclaim 2 bytes of the payload for data, leaving 6 bytes (`b[6]`) of raw payload. + +Certain subjects (such as `GpsPosition`) are designated as **Zero-Timestamp** frames via a compile-time layout trait. These frames do not carry the millisecond offset and utilize the full 8 bytes of the CAN frame payload for data (`b[8]`). + +### Wire Formats + +| Class | Byte 0–1 | Byte 2–7 (Data b[6]) | +|---|---|---| +| **EVENT** | offsetMs (u16) | detail (u8), 0, 0, 0, 0, 0 | +| **TIME** (Sync)* | offsetMs = 0 | timestampMs (u32), flags (u8: bit0=GPS-disciplined), 0 | +| **CMD** | offsetMs (u16) | seq (u8), desired_state (u8), 0, 0, 0, 0 | +| **ACK** | offsetMs (u16) | seq (u8), result (u8), 0, 0, 0, 0 | +| **STATE** | offsetMs (u16) | commanded (u8), energized (u8), hall (u8), 0, 0, 0 | +| **SENSOR** (Std) | offsetMs (u16) | value (i32, scaling per subject), 0, 0 | +| **SENSOR** (GPS) | GPS Lat (i32) (Byte 0–3) | GPS Lon (i32) (Byte 4–7) *(Zero-Timestamp format)* | +| **HEARTBEAT** | offsetMs (u16) | node_state (u8), error_bits (u16), schema_version (u8), 0, 0 | + +*\*For TIME class / TimeSync subject, the timestampMs payload represents the master node's synchronized Unix-relative millis, baseline offset is 0.* + +* **Control state enum** (used in CMD desired, STATE commanded/hall): `0=CLOSED, 1=OPEN, 2=UNKNOWN, 3=FAULT`. +* `energized`: `0=OFF, 1=ON`. Controls without feedback/sensing (e.g. N2 supply) report hall = UNKNOWN always. +* **ACK result enum**: `0=ACCEPTED, 1=REJECT_SAFE_STATE, 2=REJECT_BAD_SUBJECT, 3=REJECT_BAD_STATE_VALUE` +* **Command idempotency rule:** a CMD re-received with an already-processed seq number is re-ACKed with the original result and otherwise ignored. +* **Node state enum**: `0=INIT, 1=NOMINAL, 2=SAFE_STATE, 3=FAULT` + +--- + +## 3. Subject Catalog + +### Controls (subjects shared by CMD / ACK / STATE) + +| Subject | Name | Board | Hall? | Fail-safe bias | CAN-commanded? | STATE rate | +|---|---|---|---|---|---|---| +| 0x01 | Av204 | UCM | yes | open (spring) | no (Wi-Fi/local) | 1-2 Hz | +| 0x03 | Av203 | LCM | yes | open (N2O pressure) | **yes** | 1-2 Hz | +| 0x04 | Av205 | LCM | yes | closed | **yes** | 1-2 Hz | + +### Sensors (class SENSOR, value = i32 unless noted) + +| Subject | Name | Board | Units (scaling) | Rate | Prio (Jitter Protection) | +|---|---|---|---|---|---| +| 0x10 | Pt202 | UCM | PSI ×100 | 50 Hz | High | +| 0x12 | Pt204 | LCM | PSI ×100 | 100+ Hz | High | +| 0x18 | TcLowerValve | LCM | °C ×100 | 5 Hz | Medium | +| 0x20 | Volt24Ucm | UCM | mV | 5 Hz | Medium | +| 0x21 | VoltSolLcm | LCM | mV | 5 Hz | Medium | +| 0x28 | BattVolt | Power | mV | 1 Hz | Low | +| 0x30 | GpsPosition | GPS | lat (i32) + lon (i32) | 5 Hz | Low | +| 0x32 | GpsNumSats | GPS | count | 1 Hz | Low | +| 0x38 | Altitude | Altimeter | meters ×100 | 10 Hz | Low | + +### Events (class EVENT) + +Sent at Event priority class (`0x0`); repeat 3× at 100 ms spacing on state change. + +| Subject | Name | Owner | detail byte | +|---|---|---|---| +| 0x40 | LowPower | Power | 0=exit, 1=enter | +| 0x41 | LaunchDetect | Power | 1=detected | +| 0x42 | SafeStateEntered | any | reason code | + +### Time / Heartbeat + +| Subject | Name | Notes | +|---|---|---| +| 0x50 | TimeSync | 1 Hz master sync. Master = UCM pre-launch, GPS post-launch. | +| 0x00 | Heartbeat | Subject 0; source field identifies node. Sent only if node silent > T/2. | + +--- + +## 4. Liveness & Safing +- Liveness timer per watched node resets on **any valid frame** from it. +- Timeout $\rightarrow$ de-energize all local solenoids (hardware fail-safe positions). +- Post-launch safing: LCM-local timer (4–5 min) armed by LAUNCH_DETECT; runs even if bus is dead. +- ACK means "command received and accepted" — never "valve moved." Physical confirmation is the STATE frame. + +## 5. Extensibility reserves +8 spare classes · ~230 spare subjects · 8 spare source IDs · 13 reserved ID bits · +schema_version (3U) in every heartbeat (mismatch = ground abort item). From 2c19a4c5112f342d4d83d8a4c6dfa930f41e6b13 Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Mon, 20 Jul 2026 12:15:42 -0700 Subject: [PATCH 3/8] Simplify: Remove speculative GPS-disciplined flags from TimeSync --- aim_network/examples/stm32_canbus/src/main.cpp | 2 +- aim_protocol_v0.7.0.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aim_network/examples/stm32_canbus/src/main.cpp b/aim_network/examples/stm32_canbus/src/main.cpp index 58d1325..b9fe75f 100644 --- a/aim_network/examples/stm32_canbus/src/main.cpp +++ b/aim_network/examples/stm32_canbus/src/main.cpp @@ -67,7 +67,7 @@ void loop(void) { aim::Msg m = {}; m.cls = aim::Class::Time; m.subject = aim::subject::TimeSync; - // Timestamp field IS the payload; b[0] flags bit0=0 (not GPS-disciplined). + // Broadcast clock synchronization baseline if (!g_aim.send(m)) { LOG_ERROR("Time TX failed"); } diff --git a/aim_protocol_v0.7.0.md b/aim_protocol_v0.7.0.md index 8e8b221..070c1d8 100644 --- a/aim_protocol_v0.7.0.md +++ b/aim_protocol_v0.7.0.md @@ -65,7 +65,7 @@ Certain subjects (such as `GpsPosition`) are designated as **Zero-Timestamp** fr | Class | Byte 0–1 | Byte 2–7 (Data b[6]) | |---|---|---| | **EVENT** | offsetMs (u16) | detail (u8), 0, 0, 0, 0, 0 | -| **TIME** (Sync)* | offsetMs = 0 | timestampMs (u32), flags (u8: bit0=GPS-disciplined), 0 | +| **TIME** (Sync)* | offsetMs = 0 | timestampMs (u32), 0, 0 | | **CMD** | offsetMs (u16) | seq (u8), desired_state (u8), 0, 0, 0, 0 | | **ACK** | offsetMs (u16) | seq (u8), result (u8), 0, 0, 0, 0 | | **STATE** | offsetMs (u16) | commanded (u8), energized (u8), hall (u8), 0, 0, 0 | @@ -121,7 +121,7 @@ Sent at Event priority class (`0x0`); repeat 3× at 100 ms spacing on state chan | Subject | Name | Notes | |---|---|---| -| 0x50 | TimeSync | 1 Hz master sync. Master = UCM pre-launch, GPS post-launch. | +| 0x50 | TimeSync | 1 Hz clock master synchronization baseline. | | 0x00 | Heartbeat | Subject 0; source field identifies node. Sent only if node silent > T/2. | --- From c4af76eca45bdc4c56dc94669c3cb41e049cf43d Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Mon, 20 Jul 2026 13:19:59 -0700 Subject: [PATCH 4/8] Refactor: Upgraded CAN bus to 1 Mbps, reordered priorities, and added Acceleration & Velocity --- .../examples/esp32_canbus/src/main.cpp | 2 +- .../examples/stm32_canbus/src/main.cpp | 2 +- aim_network/src/aim_catalog.h | 36 +++++++++-------- aim_network/src/aim_esp32_can_core.cpp | 17 +++----- aim_protocol_v0.7.0.md | 40 ++++++++++--------- 5 files changed, 47 insertions(+), 50 deletions(-) diff --git a/aim_network/examples/esp32_canbus/src/main.cpp b/aim_network/examples/esp32_canbus/src/main.cpp index 4656a62..404c058 100644 --- a/aim_network/examples/esp32_canbus/src/main.cpp +++ b/aim_network/examples/esp32_canbus/src/main.cpp @@ -6,7 +6,7 @@ #include static constexpr uint32_t kSerialBaud = 115200U; -static constexpr uint32_t kCanBaud = 500000U; +static constexpr uint32_t kCanBaud = 1000000U; static constexpr int kCanRxPin = 2; static constexpr int kCanTxPin = 1; diff --git a/aim_network/examples/stm32_canbus/src/main.cpp b/aim_network/examples/stm32_canbus/src/main.cpp index b9fe75f..64437a7 100644 --- a/aim_network/examples/stm32_canbus/src/main.cpp +++ b/aim_network/examples/stm32_canbus/src/main.cpp @@ -7,7 +7,7 @@ #include static constexpr uint32_t kSerialBaud = 115200U; -static constexpr uint32_t kCanBaud = 500000U; +static constexpr uint32_t kCanBaud = 1000000U; static aim::Job g_tick100{100U, 0U}; static aim::Job g_tick1000{1000U, 0U}; diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index ea70db4..ffd683b 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -30,11 +30,11 @@ enum class Class : uint8_t { enum class Source : uint8_t { // 0x0 deliberately invalid: an all-zero ID field is a wiring-fault symptom. - Comms = 0x1, - Ucm = 0x2, - Lcm = 0x3, - Altimeter = 0x4, - Gps = 0x5, + Ucm = 0x1, + Lcm = 0x2, + Altimeter = 0x3, + Gps = 0x4, + Comms = 0x5, Power = 0x6, // 0x7–0xE spare (test rig, debug dongle, future boards) }; @@ -42,23 +42,25 @@ enum class Source : uint8_t { namespace subject { // Valves (shared across CMD / ACK / STATE) static constexpr uint8_t Heartbeat = 0x00; -static constexpr uint8_t Av204 = 0x01; // UCM, Vent valve -static constexpr uint8_t Av203 = 0x03; // LCM, CAN-commanded -static constexpr uint8_t Av205 = 0x04; // LCM, CAN-commanded +static constexpr uint8_t Av203 = 0x01; // LCM, CAN-commanded +static constexpr uint8_t Av205 = 0x02; // LCM, CAN-commanded +static constexpr uint8_t Av204 = 0x03; // UCM, Vent valve // Power FETs static constexpr uint8_t PwrPtUcm = 0x05; static constexpr uint8_t PwrSolLcm = 0x06; static constexpr uint8_t PwrPtLcm = 0x07; // Sensors (value = i32, scaling fixed here) -static constexpr uint8_t Pt202 = 0x10; // UCM, PSI x100 -static constexpr uint8_t Pt204 = 0x12; // LCM, PSI x100 -static constexpr uint8_t TcLowerValve = 0x18; // LCM, Celsius x100 -static constexpr uint8_t Volt24Ucm = 0x20; // mV -static constexpr uint8_t VoltSolLcm = 0x21; // mV -static constexpr uint8_t BattVolt = 0x28; // mV -static constexpr uint8_t GpsPosition = 0x30; // Fused GPS Position (lat: b[0..3], lon: b[4..7]) -static constexpr uint8_t GpsNumSats = 0x32; // count -static constexpr uint8_t Altitude = 0x38; // meters x100 +static constexpr uint8_t Pt204 = 0x10; // LCM, PSI x100 +static constexpr uint8_t Pt202 = 0x11; // UCM, PSI x100 +static constexpr uint8_t Acceleration = 0x12; // Altimeter, m/s^2 x100 +static constexpr uint8_t Velocity = 0x13; // Altimeter, m/s x100 +static constexpr uint8_t TcLowerValve = 0x15; // LCM, Celsius x100 +static constexpr uint8_t Altitude = 0x18; // meters x100 +static constexpr uint8_t GpsPosition = 0x19; // Fused GPS Position (lat: b[0..3], lon: b[4..7]) +static constexpr uint8_t BattVolt = 0x20; // mV +static constexpr uint8_t GpsNumSats = 0x21; // count +static constexpr uint8_t Volt24Ucm = 0x32; // mV +static constexpr uint8_t VoltSolLcm = 0x33; // mV // // Events static constexpr uint8_t LowPower = 0x40; // detail: 0=exit 1=enter static constexpr uint8_t LaunchDetect = 0x41; // detail: 1=detected diff --git a/aim_network/src/aim_esp32_can_core.cpp b/aim_network/src/aim_esp32_can_core.cpp index 6c8490a..528ef34 100644 --- a/aim_network/src/aim_esp32_can_core.cpp +++ b/aim_network/src/aim_esp32_can_core.cpp @@ -35,19 +35,12 @@ bool AimEsp32CanCore::validatePins() const { } bool AimEsp32CanCore::configureTiming(twai_timing_config_t& config) const { + if (_baud == 1000000U) { + config = TWAI_TIMING_CONFIG_1MBITS(); + return true; + } if (_baud == 500000U) { - // Match the STM32 bxCAN sample point exactly. The STM32 nodes run - // 1 + BS1(13) + BS2(2) = 16 TQ → 87.5% sample point. The IDF macro - // TWAI_TIMING_CONFIG_500KBITS() samples at 80% (20 TQ), and that - // mismatch causes the ESP32 to read back bit errors on its own long - // data frames (RX is fine; it resyncs on the STM32's edges). With an - // 80 MHz source: brp=10, 1 + tseg_1(13) + tseg_2(2) = 16 TQ → 500 kbps. - config = {}; - config.brp = 10; - config.tseg_1 = 13; - config.tseg_2 = 2; - config.sjw = 2; // max for tseg_2=2; extra resync margin - config.triple_sampling = false; + config = TWAI_TIMING_CONFIG_500KBITS(); return true; } if (_baud == 250000U) { diff --git a/aim_protocol_v0.7.0.md b/aim_protocol_v0.7.0.md index 070c1d8..3ccea0a 100644 --- a/aim_protocol_v0.7.0.md +++ b/aim_protocol_v0.7.0.md @@ -1,6 +1,6 @@ -# AIM Network Protocol — v0.7.0 DRAFT +# AIM Network Protocol — v0.7.0 -CAN 2.0B, 29-bit extended ID, 500 kbps. All multi-byte fields **little-endian** +CAN 2.0B, 29-bit extended ID, 1 Mbps. All multi-byte fields **little-endian** (decided: both MCUs are LE, no interop requirement with anyone else; zero byte-swapping). **Unique-ID invariant:** every frame carries its sender's source bits, therefore no two nodes @@ -42,11 +42,11 @@ Rules: | ID | Node | |---|---| -| 0x1 | Comms | -| 0x2 | UCM | -| 0x3 | LCM | -| 0x4 | Altimeter | -| 0x5 | GPS | +| 0x1 | UCM | +| 0x2 | LCM | +| 0x3 | Altimeter | +| 0x4 | GPS | +| 0x5 | Comms | | 0x6 | Power | | 0x7–0xE | spare (test rig, debug dongle, future boards) | @@ -89,23 +89,25 @@ Certain subjects (such as `GpsPosition`) are designated as **Zero-Timestamp** fr | Subject | Name | Board | Hall? | Fail-safe bias | CAN-commanded? | STATE rate | |---|---|---|---|---|---|---| -| 0x01 | Av204 | UCM | yes | open (spring) | no (Wi-Fi/local) | 1-2 Hz | -| 0x03 | Av203 | LCM | yes | open (N2O pressure) | **yes** | 1-2 Hz | -| 0x04 | Av205 | LCM | yes | closed | **yes** | 1-2 Hz | +| 0x01 | Av203 | LCM | yes | open (N2O pressure) | **yes** | 1-2 Hz | +| 0x02 | Av205 | LCM | yes | closed | **yes** | 1-2 Hz | +| 0x03 | Av204 | UCM | yes | open (spring) | no (Wi-Fi/local) | 1-2 Hz | ### Sensors (class SENSOR, value = i32 unless noted) | Subject | Name | Board | Units (scaling) | Rate | Prio (Jitter Protection) | |---|---|---|---|---|---| -| 0x10 | Pt202 | UCM | PSI ×100 | 50 Hz | High | -| 0x12 | Pt204 | LCM | PSI ×100 | 100+ Hz | High | -| 0x18 | TcLowerValve | LCM | °C ×100 | 5 Hz | Medium | -| 0x20 | Volt24Ucm | UCM | mV | 5 Hz | Medium | -| 0x21 | VoltSolLcm | LCM | mV | 5 Hz | Medium | -| 0x28 | BattVolt | Power | mV | 1 Hz | Low | -| 0x30 | GpsPosition | GPS | lat (i32) + lon (i32) | 5 Hz | Low | -| 0x32 | GpsNumSats | GPS | count | 1 Hz | Low | -| 0x38 | Altitude | Altimeter | meters ×100 | 10 Hz | Low | +| 0x10 | Pt204 | LCM | PSI ×100 | 100+ Hz | High | +| 0x11 | Pt202 | UCM | PSI ×100 | 50 Hz | High | +| 0x12 | Acceleration | Altimeter | m/s² ×100 | 50 Hz | High | +| 0x13 | Velocity | Altimeter | m/s ×100 | 50 Hz | High | +| 0x15 | TcLowerValve | LCM | °C ×100 | 5 Hz | High | +| 0x18 | Altitude | Altimeter | meters ×100 | 10 Hz | High | +| 0x19 | GpsPosition | GPS | lat (i32) + lon (i32) | 5 Hz | High | +| 0x20 | BattVolt | Power | mV | 1 Hz | Medium | +| 0x21 | GpsNumSats | GPS | count | 1 Hz | Medium | +| 0x32 | Volt24Ucm | UCM | mV | 5 Hz | Low | +| 0x33 | VoltSolLcm | LCM | mV | 5 Hz | Low | ### Events (class EVENT) From 0446357072b0bdba8325002a8081a2350ea2c4dd Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Mon, 20 Jul 2026 13:44:48 -0700 Subject: [PATCH 5/8] Robustness: Implement clock sentinel, wrap-around protection, and Cmd/Ack Zero-TS exceptions --- aim_network/src/aim_catalog.h | 2 +- aim_network/src/aim_network.cpp | 21 ++++++++++++++++----- aim_network/src/aim_stm32_can_core.cpp | 1 - 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index ffd683b..87cbc7d 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -97,7 +97,7 @@ static inline constexpr uint16_t classBit(Class cls) { } static inline constexpr bool isZeroTimestamp(Class cls, uint8_t subject) { - return (cls == Class::Sensor && subject == subject::GpsPosition); + return (cls == Class::Cmd) || (cls == Class::Ack) || (cls == Class::Sensor && subject == subject::GpsPosition); } } // namespace aim diff --git a/aim_network/src/aim_network.cpp b/aim_network/src/aim_network.cpp index a3031a5..ef9da22 100644 --- a/aim_network/src/aim_network.cpp +++ b/aim_network/src/aim_network.cpp @@ -14,7 +14,7 @@ AimNetwork::AimNetwork(AimCanHardware* hardware, aim::Source self) _self(self), _timeOffset(0), _lastTxMs(0U), - _lastSyncTimeMs(0U) { + _lastSyncTimeMs(0xFFFFFFFFU) { AIM_ASSERT(static_cast(self) != 0U); AIM_ASSERT(static_cast(self) <= 0xEU); } @@ -49,9 +49,16 @@ bool AimNetwork::send(aim::Msg& m) { } else if (aim::isZeroTimestamp(m.cls, m.subject)) { m.offsetMs = 0; } else { - uint32_t baseTime = (_lastSyncTimeMs == 0U) ? 0U : _lastSyncTimeMs; - uint32_t offset = m.timestampMs - baseTime; - m.offsetMs = static_cast(offset & 0xFFFFU); + if (_lastSyncTimeMs == 0xFFFFFFFFU) { + m.offsetMs = 0xFFFFU; + } else { + uint32_t delta = m.timestampMs - _lastSyncTimeMs; + if (delta > 65535U) { + m.offsetMs = 0xFFFFU; + } else { + m.offsetMs = static_cast(delta); + } + } } aim::Frame frame = {}; @@ -95,7 +102,11 @@ bool AimNetwork::receive(aim::Msg& m) { } else if (aim::isZeroTimestamp(m.cls, m.subject)) { m.timestampMs = syncedMillis(); } else { - m.timestampMs = _lastSyncTimeMs + m.offsetMs; + if (_lastSyncTimeMs == 0xFFFFFFFFU) { + m.timestampMs = 0U; + } else { + m.timestampMs = _lastSyncTimeMs + m.offsetMs; + } } return true; diff --git a/aim_network/src/aim_stm32_can_core.cpp b/aim_network/src/aim_stm32_can_core.cpp index a3791d2..3ca910e 100644 --- a/aim_network/src/aim_stm32_can_core.cpp +++ b/aim_network/src/aim_stm32_can_core.cpp @@ -534,7 +534,6 @@ bool AimStm32CanCore::flushTxMailboxes() { uint8_t payload[8] = {}; (void)memcpy(payload, frame.data, frame.dlc); - // why only mailbox zero here? uint32_t mailbox = 0U; const HAL_StatusTypeDef status = HAL_CAN_AddTxMessage(&_hcan, &header, payload, &mailbox); From 83d085b0ec77200afa9cec7680100b49d80e5abf Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Thu, 23 Jul 2026 14:08:31 -0700 Subject: [PATCH 6/8] feat(aim_network): add TelemetryMode subject 0x44 and v0.7.0 catalog helpers --- aim_network/src/aim_catalog.h | 45 ++++++++++++++++------------------- aim_network/src/aim_control.h | 14 +++++++++++ aim_network/src/aim_network.h | 10 ++++++++ aim_protocol_v0.7.0.md | 1 + 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index 87cbc7d..310811f 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -49,40 +49,35 @@ static constexpr uint8_t Av204 = 0x03; // UCM, Vent valve static constexpr uint8_t PwrPtUcm = 0x05; static constexpr uint8_t PwrSolLcm = 0x06; static constexpr uint8_t PwrPtLcm = 0x07; -// Sensors (value = i32, scaling fixed here) -static constexpr uint8_t Pt204 = 0x10; // LCM, PSI x100 -static constexpr uint8_t Pt202 = 0x11; // UCM, PSI x100 -static constexpr uint8_t Acceleration = 0x12; // Altimeter, m/s^2 x100 -static constexpr uint8_t Velocity = 0x13; // Altimeter, m/s x100 -static constexpr uint8_t TcLowerValve = 0x15; // LCM, Celsius x100 -static constexpr uint8_t Altitude = 0x18; // meters x100 -static constexpr uint8_t GpsPosition = 0x19; // Fused GPS Position (lat: b[0..3], lon: b[4..7]) +// Sensors (wire value = i32 fixed-point) +static constexpr uint8_t Pt204 = 0x10; // LCM, PSI (x100) +static constexpr uint8_t Pt202 = 0x11; // UCM, PSI (x100) +static constexpr uint8_t Acceleration = 0x12; // Altimeter, m/s^2 (x100) +static constexpr uint8_t TcLowerValve = 0x15; // LCM, Celsius (x100) +static constexpr uint8_t Altitude = 0x18; // meters (x100) +static constexpr uint8_t GpsPosition = 0x19; // lat: b[0..3], lon: b[4..7] (1e-7 deg) static constexpr uint8_t BattVolt = 0x20; // mV static constexpr uint8_t GpsNumSats = 0x21; // count static constexpr uint8_t Volt24Ucm = 0x32; // mV -static constexpr uint8_t VoltSolLcm = 0x33; // mV // +static constexpr uint8_t VoltSolLcm = 0x33; // mV // Events -static constexpr uint8_t LowPower = 0x40; // detail: 0=exit 1=enter -static constexpr uint8_t LaunchDetect = 0x41; // detail: 1=detected -static constexpr uint8_t SafeStateEntered = 0x42; // detail: reason code +static constexpr uint8_t LowPower = 0x40; // detail: 0=exit 1=enter +static constexpr uint8_t LaunchDetect = 0x41; // detail: 1=detected +static constexpr uint8_t GroundPowerStatus = 0x42; // detail: 1=conected +static constexpr uint8_t SafeStateEntered = 0x43; // detail: reason code +static constexpr uint8_t TelemetryMode = 0x44; // detail: 0=Idle (1 Hz), 1=Active (100 Hz) // Time static constexpr uint8_t TimeSync = 0x50; } // namespace subject -// --- Payload enums (bytes 4–7, layouts per class in the protocol doc) --- +// --- Payload enums --- -enum class ControlState : uint8_t { - Closed = 0, - Open = 1, - Unknown = 2, // controls without feedback/sensing report Unknown - Fault = 3, -}; - -enum class AckResult : uint8_t { - Accepted = 0, - RejectSafeState = 1, - RejectBadSubject = 2, - RejectBadStateValue = 3, +enum class FlightPhase : uint8_t { + Preflight = 0, + Boost = 1, + Coast = 2, + Decent = 3, + Landed = 4, }; enum class NodeState : uint8_t { diff --git a/aim_network/src/aim_control.h b/aim_network/src/aim_control.h index 8841d1d..92d3a9f 100644 --- a/aim_network/src/aim_control.h +++ b/aim_network/src/aim_control.h @@ -30,6 +30,20 @@ namespace aim { +enum class ControlState : uint8_t { + Closed = 0, + Open = 1, + Unknown = 2, // controls without feedback/sensing report Unknown + Fault = 3, +}; + +enum class AckResult : uint8_t { + Accepted = 0, + RejectSafeState = 1, + RejectBadSubject = 2, + RejectBadStateValue = 3, +}; + enum class ControlKind : uint8_t { Local, Remote }; // Resend an un-Acked Cmd after this window (Remote controls only). diff --git a/aim_network/src/aim_network.h b/aim_network/src/aim_network.h index 8bc03d1..84e4fc1 100644 --- a/aim_network/src/aim_network.h +++ b/aim_network/src/aim_network.h @@ -60,6 +60,16 @@ struct Msg { void setSensorValue(int32_t v) { (void)memcpy(b, &v, sizeof(v)); } + + void setGpsPosition(int32_t &lon, int32_t &lat) { + (void)memcpy(b, &lon, sizeof(lon)); + (void)memcpy(b + sizeof(lon), &lat, sizeof(lat)); + } + + void getGpsPosition(int32_t &lon, int32_t &lat) { + (void)memcpy(&lon, b, sizeof(lon)); + (void)memcpy(&lat, b + sizeof(lon), sizeof(lat)); + } }; static inline uint32_t encodeId(Class cls, uint8_t subj, Source src) { diff --git a/aim_protocol_v0.7.0.md b/aim_protocol_v0.7.0.md index 3ccea0a..20d8763 100644 --- a/aim_protocol_v0.7.0.md +++ b/aim_protocol_v0.7.0.md @@ -118,6 +118,7 @@ Sent at Event priority class (`0x0`); repeat 3× at 100 ms spacing on state chan | 0x40 | LowPower | Power | 0=exit, 1=enter | | 0x41 | LaunchDetect | Power | 1=detected | | 0x42 | SafeStateEntered | any | reason code | +| 0x44 | TelemetryMode | UCM / QLCP | 0=Idle (1 Hz), 1=Active (100 Hz) | ### Time / Heartbeat From 2b45b15c7270442a2311a0352419947b4832d5c2 Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Fri, 24 Jul 2026 13:22:15 -0700 Subject: [PATCH 7/8] fix(catalog): correct Acceleration unit comment and add const to getGpsPosition - Update Acceleration catalog comment to note mm/s^2 scale (1G = 9810) - Mark aim::Msg::getGpsPosition as const member function --- aim_network/src/aim_catalog.h | 2 +- aim_network/src/aim_network.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index 310811f..9a2a935 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -52,7 +52,7 @@ static constexpr uint8_t PwrPtLcm = 0x07; // Sensors (wire value = i32 fixed-point) static constexpr uint8_t Pt204 = 0x10; // LCM, PSI (x100) static constexpr uint8_t Pt202 = 0x11; // UCM, PSI (x100) -static constexpr uint8_t Acceleration = 0x12; // Altimeter, m/s^2 (x100) +static constexpr uint8_t Acceleration = 0x12; // Altimeter, mm/s² (= m/s² × 1000; 1G = 9810) static constexpr uint8_t TcLowerValve = 0x15; // LCM, Celsius (x100) static constexpr uint8_t Altitude = 0x18; // meters (x100) static constexpr uint8_t GpsPosition = 0x19; // lat: b[0..3], lon: b[4..7] (1e-7 deg) diff --git a/aim_network/src/aim_network.h b/aim_network/src/aim_network.h index 84e4fc1..7b44350 100644 --- a/aim_network/src/aim_network.h +++ b/aim_network/src/aim_network.h @@ -66,7 +66,7 @@ struct Msg { (void)memcpy(b + sizeof(lon), &lat, sizeof(lat)); } - void getGpsPosition(int32_t &lon, int32_t &lat) { + void getGpsPosition(int32_t &lon, int32_t &lat) const { (void)memcpy(&lon, b, sizeof(lon)); (void)memcpy(&lat, b + sizeof(lon), sizeof(lat)); } From 6362c22c100df2182a0b32805d88e602b1e71f99 Mon Sep 17 00:00:00 2001 From: Tristan Alderson Date: Sun, 26 Jul 2026 23:53:37 -0400 Subject: [PATCH 8/8] feat(aim_network): expose syncTime, add volatile thread safety, and add spare PT catalog slots - Make AimNetwork::syncTime public to allow external server time ingestion - Declare _timeOffset as volatile int32_t for lock-free cross-core reads - Add PtSpare1 (0x13) and PtSpare2 (0x14) pressure transducer subjects - Add timestamp field to Sensor struct for data tracking - Align GpsPosition catalog documentation comment to (lon, lat) byte order --- aim_network/src/aim_catalog.h | 4 +++- aim_network/src/aim_network.h | 5 +++-- aim_network/src/aim_sensor.h | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aim_network/src/aim_catalog.h b/aim_network/src/aim_catalog.h index 9a2a935..d982184 100644 --- a/aim_network/src/aim_catalog.h +++ b/aim_network/src/aim_catalog.h @@ -53,9 +53,11 @@ static constexpr uint8_t PwrPtLcm = 0x07; static constexpr uint8_t Pt204 = 0x10; // LCM, PSI (x100) static constexpr uint8_t Pt202 = 0x11; // UCM, PSI (x100) static constexpr uint8_t Acceleration = 0x12; // Altimeter, mm/s² (= m/s² × 1000; 1G = 9810) +static constexpr uint8_t PtSpare1 = 0x13; // UCM, spare ADC channel, PSI (x100) +static constexpr uint8_t PtSpare2 = 0x14; // LCM, spare ADC channel, PSI (x100) static constexpr uint8_t TcLowerValve = 0x15; // LCM, Celsius (x100) static constexpr uint8_t Altitude = 0x18; // meters (x100) -static constexpr uint8_t GpsPosition = 0x19; // lat: b[0..3], lon: b[4..7] (1e-7 deg) +static constexpr uint8_t GpsPosition = 0x19; // lon: b[0..3], lat: b[4..7] (1e-7 deg) static constexpr uint8_t BattVolt = 0x20; // mV static constexpr uint8_t GpsNumSats = 0x21; // count static constexpr uint8_t Volt24Ucm = 0x32; // mV diff --git a/aim_network/src/aim_network.h b/aim_network/src/aim_network.h index 7b44350..79c1f0b 100644 --- a/aim_network/src/aim_network.h +++ b/aim_network/src/aim_network.h @@ -149,12 +149,13 @@ class AimNetwork { // backward at midnight — never use it for scheduling or liveness timing. uint32_t syncedMillis() const; -private: + // Disciplines the local clock offset relative to remote master time (ms). void syncTime(uint32_t remoteMillis); +private: AimCanHardware* _hw; aim::Source _self; - int32_t _timeOffset; + volatile int32_t _timeOffset; uint32_t _lastTxMs; uint32_t _lastSyncTimeMs; }; diff --git a/aim_network/src/aim_sensor.h b/aim_network/src/aim_sensor.h index d89c7fe..629f2dc 100644 --- a/aim_network/src/aim_sensor.h +++ b/aim_network/src/aim_sensor.h @@ -29,6 +29,7 @@ struct Sensor { const char* name = ""; // human label for console + telemetry uint8_t subject = 0U; // aim::subject:: id matched on Sensor frames int32_t value = 0; // last value, catalog-scaled wire integer + uint32_t timestamp = 0; float toEng = 1.0f; // value * toEng = engineering units const char* unit = ""; // engineering unit label (console display) bool fresh = false; // a value is known (Local: sampled, Remote: received)