Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 104 additions & 49 deletions SIMPLE_MONITOR_README.md
Original file line number Diff line number Diff line change
@@ -1,105 +1,160 @@
# Simple Sensor Monitoring System

## Overview
A lightweight, elegant monitoring system for ESP32 electric ultralight sensors. Based on modern C++ patterns with easy extensibility.
A lightweight, category-based monitoring system for ESP32 electric ultralight sensors. Features intelligent alert suppression when controllers disconnect and modern OOP design with device categories.

## Design Philosophy
- **Simple**: ~50 lines of core code vs complex alerting systems
- **Flexible**: Easy to add new sensors and output methods
- **Modern**: Uses `std::function` and lambdas for clean sensor definitions
- **Extensible**: Interface-based design for multiple output channels
- **Safety-First**: Automatically suppresses ESC alerts when ESC disconnected, BMS alerts when BMS disconnected
- **Category-Based**: Organizes sensors by device type (ESC, BMS, ALTIMETER, INTERNAL) for grouped management
- **Modern OOP**: Interface-based design with virtual methods and clean inheritance
- **Thread-Safe**: Queue-based telemetry snapshots for safe multi-task operation

## Usage

### Current ESC Temperature Monitoring
### Device Categories & Alert Suppression
```cpp
// Automatically monitors:
enum class SensorCategory {
ESC, // ESC temps, motor temp, ESC errors
BMS, // Battery voltage, current, temps, SOC
ALTIMETER, // Barometric sensors
INTERNAL // CPU temp, system sensors
};

// When ESC disconnects: All ESC alerts automatically cleared
// When BMS disconnects: All BMS alerts automatically cleared
// ALTIMETER/INTERNAL: Always monitored (no connection dependency)
```

### Current Monitoring Coverage
```cpp
// ESC Category (suppressed when ESC disconnected):
// - ESC MOS: Warning 90°C, Critical 110°C
// - ESC MCU: Warning 80°C, Critical 95°C
// - ESC CAP: Warning 85°C, Critical 100°C
// - Motor: Warning 90°C, Critical 110°C
// - ESC Error Conditions (overcurrent, overtemp, etc.)

// BMS Category (suppressed when BMS disconnected):
// - Battery voltage, current, SOC, cell voltages
// - BMS temperatures, charge/discharge MOS status
```

### Adding New Sensors
```cpp
// Example: Battery voltage monitoring
static SensorMonitor batteryVoltage = {
"BatteryVolt",
{.warnLow = 70, .warnHigh = 105, .critLow = 65, .critHigh = 110},
[]() { return bmsTelemetryData.battery_voltage; },
AlertLevel::OK,
&serialLogger
};
sensors.push_back(&batteryVoltage);
// Example: New ESC sensor
static SensorMonitor* newEscSensor = new SensorMonitor(
SensorID::ESC_New_Sensor, // Unique ID from enum
SensorCategory::ESC, // Category determines suppression behavior
{.warnLow = 10, .warnHigh = 80, .critLow = 5, .critHigh = 90},
[]() { return escTelemetryData.new_value; }, // Data source
&multiLogger // Fan-out to multiple outputs
);
monitors.push_back(newEscSensor);

// Example: Boolean error monitor
static BooleanMonitor* newError = new BooleanMonitor(
SensorID::ESC_New_Error,
SensorCategory::ESC,
[]() { return checkErrorCondition(); },
true, // Alert when condition is true
AlertLevel::CRIT_HIGH,
&multiLogger
);
monitors.push_back(newError);
```

### Example Output
```
[15234] [WARN_HIGH] ESC_MOS_Temp = 92.50
[15467] [CRIT_HIGH] Motor_Temp = 112.30
[15890] [OK] ESC_MOS_Temp = 88.20
[16123] ESC disconnected - clearing all ESC alerts
[16450] [WARN_LOW] BMS_SOC = 12.3
```

## Easy Extensions

### SD Card Logging
```cpp
struct SDLogger : ILogger {
void log(const char* name, AlertLevel lvl, float v) override {
// Write timestamp, name, level, value to SD card
void log(SensorID id, AlertLevel lvl, float v) override {
File logFile = SD.open("/alerts.log", FILE_APPEND);
logFile.printf("%lu,%s,%d,%.2f\n", millis(), name, (int)lvl, v);
logFile.printf("%lu,%s,%d,%.2f\n", millis(), sensorIDToString(id), (int)lvl, v);
logFile.close();
}
};
```

### Display Alerts
### Custom Alert Processing
```cpp
struct DisplayLogger : ILogger {
void log(const char* name, AlertLevel lvl, float v) override {
// Show alert on LVGL display
if (lvl >= AlertLevel::WARN_HIGH) {
showAlertPopup(name, lvl, v);
struct CustomLogger : ILogger {
void log(SensorID id, AlertLevel lvl, float v) override {
// Custom processing based on sensor category
SensorCategory cat = getSensorCategory(id);
if (cat == SensorCategory::ESC && lvl >= AlertLevel::CRIT_HIGH) {
triggerEmergencyShutdown();
}
}
};
```

### Multiple Outputs
### Multiple Outputs (Built-in)
```cpp
// Log to both serial and SD card
static SerialLogger serialLog;
static SDLogger sdLog;
// MultiLogger automatically fans out to all registered sinks
multiLogger.addSink(&serialLogger); // Console output
multiLogger.addSink(&uiLogger); // LVGL alerts
multiLogger.addSink(&customLogger); // Your custom handler

sensorMonitor.logger = &serialLog; // Or use both with composite pattern
// All monitors automatically use multiLogger
```

## Architecture

```cpp
IMonitor (interface)
├── virtual SensorID getSensorID() = 0
├── virtual SensorCategory getCategory() = 0
└── virtual void check() = 0

SensorMonitor : IMonitor BooleanMonitor : IMonitor
├── SensorID id ├── SensorID id
├── SensorCategory category ├── SensorCategory category
├── Thresholds thr ├── std::function<bool()> read
├── std::function<float()> read ├── bool alertOnTrue
└── ILogger* logger └── AlertLevel level

ILogger (interface)
├── SerialLogger (serial console)
├── SDLogger (SD card - future)
└── DisplayLogger (LVGL display - future)

SensorMonitor
├── name (string identifier)
├── thresholds (warn/crit levels)
├── read (lambda function)
└── logger (output interface)
├── SerialLogger (debug output)
├── AlertUILogger (LVGL display)
└── MultiLogger (fan-out to multiple sinks)

Categories & Connection Logic:
├── ESC sensors → suppressed when escState != CONNECTED
├── BMS sensors → suppressed when bmsState != CONNECTED
├── ALTIMETER sensors → always active
└── INTERNAL sensors → always active
```

## Integration
- Monitors check every 40ms in main SPI communication task
- Only logs when alert level changes (no spam)
- Uses existing telemetry data (no additional sensor reads)
- Zero overhead when sensors are in OK state
- **Queue-Based**: Dedicated monitoring task receives telemetry snapshots via FreeRTOS queue
- **Thread-Safe**: No direct access to volatile telemetry data from monitoring task
- **Connection-Aware**: Automatically clears alerts when devices disconnect
- **Smart Suppression**: Only runs ESC monitors when ESC connected, BMS monitors when BMS connected
- **Change Detection**: Only logs when alert level changes (no spam)
- **Zero Overhead**: Minimal CPU usage when all sensors in OK state

## Memory Usage
- ~1KB total code size
- ~200 bytes per monitored sensor
- Minimal RAM footprint
- No dynamic allocation during runtime

This approach perfectly balances simplicity with extensibility - start with basic serial logging, easily expand to SD cards, displays, or remote monitoring as needed.
- **Code Size**: ~3KB total (includes OOP infrastructure, queue handling, UI integration)
- **Per Sensor**: ~150 bytes (SensorMonitor) or ~120 bytes (BooleanMonitor)
- **Static Allocation**: All monitors allocated at compile-time (embedded-friendly)
- **Queue Memory**: ~200 bytes for telemetry snapshot queue
- **Category Lookup**: O(1) via virtual methods (no external mapping tables)

## Key Benefits
1. **Safety**: ESC alerts disappear when ESC disconnects (no false warnings during connection issues)
2. **Maintainability**: Add new sensors by category, automatic suppression behavior
3. **Robustness**: Thread-safe design prevents race conditions between telemetry and monitoring
4. **Extensibility**: Clean OOP interfaces for new monitor types and output methods
5. **Performance**: Smart suppression reduces unnecessary checks when devices offline

This design balances safety, maintainability, and performance - essential for flight-critical embedded systems.
4 changes: 2 additions & 2 deletions inc/sp140/lvgl/lvgl_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
#define SCREEN_HEIGHT 128

// LVGL buffer size - optimize for our display
// Use 1/4 of the screen size to balance memory usage and performance
#define LVGL_BUFFER_SIZE (SCREEN_WIDTH * (SCREEN_HEIGHT / 4))
// Use half screen size for single flush to balance performance and memory usage
#define LVGL_BUFFER_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 2)

// LVGL refresh time in ms - match the config file setting
#define LVGL_REFRESH_TIME 40
Expand Down
3 changes: 2 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ lib_ignore =


[env:OpenPPG-CESP32S3-CAN-SP140]
platform = espressif32@6.11.0
platform = https://github.com/pioarduino/platform-espressif32.git#develop
board = m5stack-stamps3
framework = arduino
src_folder = sp140
Expand All @@ -35,6 +35,7 @@ build_flags =
-D LV_CONF_INCLUDE_SIMPLE
-I inc/sp140/lvgl
-D LV_LVGL_H_INCLUDE_SIMPLE
-D USBSerial=Serial

build_type = debug
debug_speed = 12000
Expand Down
12 changes: 6 additions & 6 deletions src/sp140/buzzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ static bool buzzerInitialized = false;
* @return Returns true if initialization was successful, false otherwise
*/
bool initBuzz() {
// Setup LEDC channel for buzzer
ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQUENCY, BUZZER_PWM_RESOLUTION);
ledcAttachPin(board_config.buzzer_pin, BUZZER_PWM_CHANNEL);
// Setup LEDC channel for buzzer (Arduino-ESP32 3.x API)
// Channels are implicitly allocated; attach pin to channel and set frequency/resolution
ledcAttach(board_config.buzzer_pin, BUZZER_PWM_FREQUENCY, BUZZER_PWM_RESOLUTION);
buzzerInitialized = true;
return true;
}
Expand All @@ -43,9 +43,9 @@ void startTone(uint16_t frequency) {
if (!buzzerInitialized || !ENABLE_BUZZ) return;

// Change the frequency for this channel
ledcChangeFrequency(BUZZER_PWM_CHANNEL, frequency, BUZZER_PWM_RESOLUTION);
ledcWriteTone(board_config.buzzer_pin, frequency);
// Set 50% duty cycle (square wave)
ledcWrite(BUZZER_PWM_CHANNEL, 128);
ledcWrite(board_config.buzzer_pin, 128);
}

/**
Expand All @@ -55,7 +55,7 @@ void stopTone() {
if (!buzzerInitialized) return;

// Set duty cycle to 0 to stop the tone
ledcWrite(BUZZER_PWM_CHANNEL, 0);
ledcWrite(board_config.buzzer_pin, 0);
}

/**
Expand Down
14 changes: 9 additions & 5 deletions src/sp140/esc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ static CanardAdapter adapter;
static uint8_t memory_pool[1024] __attribute__((aligned(8)));
static SineEsc esc(adapter);
static unsigned long lastSuccessfulCommTimeMs = 0; // Store millis() time of last successful ESC comm
static bool escReady = false;


STR_ESC_TELEMETRY_140 escTelemetryData = {
Expand All @@ -36,14 +37,13 @@ void initESC() {
}

adapter.begin(memory_pool, sizeof(memory_pool));
adapter.setLocalNodeId(LOCAL_NODE_ID);
esc.begin(0x20); // Default ID for the ESC
adapter.setLocalNodeId(LOCAL_NODE_ID);

// Set idle throttle only if ESC is found
const uint16_t IdleThrottle_us = 10000; // 1000us (0.1us resolution)
esc.setThrottleSettings2(IdleThrottle_us);
// Defer sending throttle until after first adapter process to avoid null pointer in CANARD
adapter.processTxRxOnce();
vTaskDelay(pdMS_TO_TICKS(20)); // Wait for ESC to process the command
vTaskDelay(pdMS_TO_TICKS(20)); // Give ESC time to be ready
escReady = true;
}

/**
Expand All @@ -54,6 +54,10 @@ void initESC() {
* Important: The ESC requires messages at least every 300ms or it will reset
*/
void setESCThrottle(int throttlePWM) {
// Ensure TWAI/ESC subsystem is initialized
if (!escTwaiInitialized || !escReady) {
return;
}
// Input validation
if (throttlePWM < 1000 || throttlePWM > 2000) {
return; // Ignore invalid throttle values
Expand Down
16 changes: 8 additions & 8 deletions src/sp140/extra-data.ino
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ void updateBMSTelemetry(const STR_BMS_TELEMETRY_140& telemetry) {

class MetricAltCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();

if (value.length() == 1) { // Ensure we only get a single byte
USBSerial.print("New: ");
Expand All @@ -243,7 +243,7 @@ class MetricAltCallbacks: public BLECharacteristicCallbacks {

class PerformanceModeCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();

if (value.length() == 1) {
uint8_t mode = value[0];
Expand All @@ -262,7 +262,7 @@ class PerformanceModeCallbacks: public BLECharacteristicCallbacks {

class ScreenRotationCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();

if (value.length() == 1) {
uint8_t rotation = value[0];
Expand Down Expand Up @@ -293,7 +293,7 @@ class ThrottleValueCallbacks: public BLECharacteristicCallbacks {
return; // Only allow updates while in cruise mode
}

std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();
if (value.length() == 2) { // Expecting 2 bytes for PWM value
uint16_t newPWM = (value[0] << 8) | value[1];

Expand Down Expand Up @@ -325,14 +325,14 @@ class MyServerCallbacks: public BLEServerCallbacks {

class TimeCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();

if (value.length() == sizeof(time_t)) { // Expecting just a unix timestamp
struct timeval tv;
time_t timestamp;

// Copy the incoming timestamp
memcpy(&timestamp, value.data(), sizeof(timestamp));
memcpy(&timestamp, value.c_str(), sizeof(timestamp));

// Apply timezone offset
timestamp += deviceData.timezone_offset;
Expand Down Expand Up @@ -360,11 +360,11 @@ class TimeCallbacks: public BLECharacteristicCallbacks {

class TimezoneCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
String value = pCharacteristic->getValue();

if (value.length() == 4) { // Expecting 4 bytes for timezone offset
int32_t offset;
memcpy(&offset, value.data(), sizeof(offset));
memcpy(&offset, value.c_str(), sizeof(offset));

deviceData.timezone_offset = offset;
writeDeviceData();
Expand Down
2 changes: 1 addition & 1 deletion src/sp140/lvgl/lvgl_main_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ void setupMainScreen(bool darkMode) {
lv_obj_set_style_border_color(critical_border, LVGL_RED, LV_PART_MAIN);
lv_obj_set_style_bg_opa(critical_border, LV_OPA_0, LV_PART_MAIN); // Transparent background
lv_obj_set_style_radius(critical_border, 0, LV_PART_MAIN); // Sharp corners
lv_obj_add_flag(critical_border, LV_OBJ_FLAG_HIDDEN); // Initially hidden
lv_obj_set_style_border_opa(critical_border, LV_OPA_0, LV_PART_MAIN); // Initially invisible border
// Move border to front so it's visible over all other elements
lv_obj_move_foreground(critical_border);
}
Expand Down
Loading
Loading