sprint-8: EKF + adaptive tuner + HWID + SHA-256 audit hash-chain

- heading_ekf.py: 2-state Kalman filter fusing PGN 127250 heading and
  127251 ROT with shortest-arc innovation and symmetric covariance update
- adaptive_tuner.py: gradient-descent outer-loop Kp/Ki adjuster bounded
  to ±adaptive_max_deviation_pct; oscillation vs steady-state detection
- hwid.py: HMAC-SHA256 activation token (verify side); hwid_from_mac_words
  converts three Modbus uint16 MAC words to 12-char hex HWID
- audit.py: SHA-256 hash-chain -- each JSONL line carries prev_hash and
  line_hash; verify_chain() detects tampering, deletion, insertion
- firmware/system/hwid.h+cpp: esp_efuse_mac_get_default wrapper + FNV-32
  hash + "AA:BB:CC:DD:EE:FF" formatter
- modbus_registers.yaml + generated .h/.py: HWID_MAC_01/23/45 at
  input addrs 9/10/11 (three 16-bit words = 6-byte MAC)
- modbus_slave.cpp: INPUT_HWID_MAC_01/23/45 cases read eFuse MAC
- main.cpp: logs HWID string + FNV-32 hash at boot (activation traceability)
- tests: 72 new tests (audit signing, EKF, adaptive tuner, HWID) -- 398 total

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 03:07:27 -04:00
parent 5f9b445572
commit 45642fda0e
15 changed files with 1217 additions and 5 deletions
@@ -83,6 +83,9 @@ inputs:
- { addr: 6, name: CURRENT_MODE, desc: "Current AutopilotMode (0=STANDBY,1=HH,2=TC,3=TK,4=DODGE)", unit: "" }
- { addr: 7, name: FREE_HEAP_KB, desc: "Current free heap, KiB", unit: "KiB" }
- { addr: 8, name: MIN_FREE_HEAP_KB, desc: "Minimum free heap since boot", unit: "KiB" }
- { addr: 9, name: HWID_MAC_01, desc: "Hardware ID bytes [0..1] (MAC eFuse high word)", unit: "" }
- { addr: 10, name: HWID_MAC_23, desc: "Hardware ID bytes [2..3] (MAC eFuse mid word)", unit: "" }
- { addr: 11, name: HWID_MAC_45, desc: "Hardware ID bytes [4..5] (MAC eFuse low word)", unit: "" }
- { addr: 16, name: RUDDER_ANGLE_DEG_X100, desc: "Filtered rudder angle, deg * 100 (-3500..+3500)", unit: "deg", scale: 0.01 }
- { addr: 17, name: RUDDER_RAW_ADC, desc: "Raw ADC reading after median filter (0..4095)", unit: "counts" }
+14
View File
@@ -37,6 +37,7 @@
#include "safety/safety_monitor.h"
#include "safety/watchdog.h"
#include "system/ar_log.h"
#include "system/hwid.h"
#include "system/task_config.h"
// Forward declarations of task-spawning helpers (defined in their own .cpp
@@ -65,6 +66,19 @@ void setup() {
(int)ESP.getChipCores(), ESP.getFreeHeap());
AR_LOGI(TAG, "================================================");
// Sprint 8: log hardware ID (eFuse MAC) at boot for activation traceability.
{
uint8_t mac[6] = {};
char mac_str[18] = {};
if (arautopilot::system::hwid_get_mac(mac)) {
arautopilot::system::hwid_format(mac, mac_str);
AR_LOGI(TAG, " HWID : %s (hash 0x%08X)",
mac_str, arautopilot::system::hwid_hash());
} else {
AR_LOGW(TAG, " HWID : eFuse read failed");
}
}
// Initialise the pilot mode state machine (boots in STANDBY).
arautopilot::modes::mode_init();
@@ -77,7 +77,7 @@ constexpr uint16_t COIL_CMD_ACK_ALL_ALARMS = 2;
constexpr uint16_t COIL_CMD_KNOB_ARM = 3;
// ----- Input registers (read-only words) -----
constexpr uint16_t INPUT_COUNT = 36;
constexpr uint16_t INPUT_COUNT = 39;
constexpr uint16_t INPUT_MAX_ADDR = 65;
// Firmware major version
@@ -102,6 +102,12 @@ constexpr uint16_t INPUT_FREE_HEAP_KB = 7;
// Minimum free heap since boot
// unit=KiB
constexpr uint16_t INPUT_MIN_FREE_HEAP_KB = 8;
// Hardware ID bytes [0..1] (MAC eFuse high word)
constexpr uint16_t INPUT_HWID_MAC_01 = 9;
// Hardware ID bytes [2..3] (MAC eFuse mid word)
constexpr uint16_t INPUT_HWID_MAC_23 = 10;
// Hardware ID bytes [4..5] (MAC eFuse low word)
constexpr uint16_t INPUT_HWID_MAC_45 = 11;
// Filtered rudder angle, deg * 100 (-3500..+3500)
// unit=deg, scale=0.01
constexpr uint16_t INPUT_RUDDER_ANGLE_DEG_X100 = 16;
@@ -35,6 +35,7 @@
#include "nmea2000_consumer.h"
#include "../hal/knob_encoder.h"
#include "../safety/safety_monitor.h"
#include "../system/hwid.h"
namespace arautopilot::protocols::modbus {
@@ -97,6 +98,17 @@ uint16_t read_input_register(uint16_t addr) {
case INPUT_FREE_HEAP_KB: return (uint16_t)(ESP.getFreeHeap() / 1024U);
case INPUT_MIN_FREE_HEAP_KB: return (uint16_t)(ESP.getMinFreeHeap() / 1024U);
// Sprint 8: Hardware ID (MAC eFuse, 3 × uint16)
case INPUT_HWID_MAC_01:
case INPUT_HWID_MAC_23:
case INPUT_HWID_MAC_45: {
uint8_t mac[6] = {};
system::hwid_get_mac(mac);
if (addr == INPUT_HWID_MAC_01) return (uint16_t)((mac[0] << 8) | mac[1]);
if (addr == INPUT_HWID_MAC_23) return (uint16_t)((mac[2] << 8) | mac[3]);
return (uint16_t)((mac[4] << 8) | mac[5]);
}
case INPUT_RUDDER_ANGLE_DEG_X100: {
auto r = hal::rudder_sensor_latest();
int v = (int)(r.angle_deg * 100.0f);
@@ -0,0 +1,35 @@
// =============================================================================
// system/hwid.cpp -- Hardware ID from ESP32 eFuse (Sprint 8)
// =============================================================================
#include "hwid.h"
#include <esp_efuse.h>
#include <esp_mac.h>
#include <cstring>
#include <cstdio>
namespace arautopilot::system {
bool hwid_get_mac(uint8_t out[6]) {
return esp_efuse_mac_get_default(out) == ESP_OK;
}
uint32_t hwid_hash() {
uint8_t mac[6] = {};
hwid_get_mac(mac);
// Simple FNV-32 hash of the 6 bytes.
uint32_t h = 2166136261U;
for (int i = 0; i < 6; ++i) {
h ^= (uint32_t)mac[i];
h *= 16777619U;
}
return h;
}
void hwid_format(uint8_t mac[6], char buf[18]) {
snprintf(buf, 18, "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
} // namespace arautopilot::system
@@ -0,0 +1,33 @@
// =============================================================================
// system/hwid.h -- Hardware ID from ESP32 eFuse (Sprint 8)
// =============================================================================
//
// The ESP32 has a 6-byte unique MAC burned into eFuse by the manufacturer.
// We use it as a hardware binding token for the activation license.
//
// The HWID is exposed via two Modbus input registers so the Studio can
// read it and generate the activation token offline.
//
// NOTE: The Modbus register map must be extended in Sprint 8+ to include
// INPUT_HWID_HI (addr 9, upper 16 bits) and INPUT_HWID_LO (addr 10,
// lower 16 bits of the middle 2 bytes). Full 6-byte MAC is exposed as
// three uint16 registers: [0..1], [2..3], [4..5].
// =============================================================================
#pragma once
#include <cstdint>
namespace arautopilot::system {
/// Read the ESP32 eFuse MAC into ``out[6]``.
/// Returns true on success; false if the eFuse read fails.
bool hwid_get_mac(uint8_t out[6]);
/// Return a 32-bit summary hash of the 6-byte MAC (for quick comparisons).
uint32_t hwid_hash();
/// Format the MAC as "AA:BB:CC:DD:EE:FF" into ``buf[18]``.
void hwid_format(uint8_t mac[6], char buf[18]);
} // namespace arautopilot::system