diff --git a/IMPLEMENTATION_STATUS.md b/IMPLEMENTATION_STATUS.md index 066154d..40629c3 100644 --- a/IMPLEMENTATION_STATUS.md +++ b/IMPLEMENTATION_STATUS.md @@ -1,360 +1,68 @@ # SkyLogic AeroAlign - Implementation Status -**Date**: 2026-01-22 -**Phase**: 4 (Differential Measurement) - COMPLETE ✅ -**Progress**: 32/130 tasks (25%) +**Date**: 2026-03-11 +**State**: AeroAlign working, CoG integration base prepared ---- +## Repository Status -## ✅ Completed Work +The project now has two layers: -### Phase 1: Setup (7/7 tasks - 100%) +1. working AeroAlign firmware and UI for IMU-based angle measurement +2. shared protocol and documentation base for the upcoming CoG scale extension -**Directory Structure Created**: -``` -ewd-digiflo/ -├── firmware/ -│ ├── master/src/ ✅ Master firmware source -│ ├── master/data/ ✅ Web UI location -│ ├── slave/src/ ✅ Slave firmware source -├── hardware/ -│ ├── cad/ ✅ 3D models (documentation) -│ ├── schematics/ ✅ Wiring diagrams + BOM -│ └── docs/ ✅ Hardware docs -├── docs/ ✅ End-user guides -└── specs/001-wireless-rc-telemetry/ ✅ Design docs -``` +## Implemented -**Configuration Files**: -- ✅ `firmware/master/platformio.ini` - ESP32-C3/S3 build config -- ✅ `firmware/slave/platformio.ini` - Multi-slave variants (8 nodes) -- ✅ `firmware/master/src/config.h` - WiFi, GPIO, constants -- ✅ `firmware/slave/src/config.h` - Master MAC, node ID +### Firmware ---- +- Master firmware builds for `esp32-c3` and `esp32-s3` +- IMU slave firmware builds for the current slave environments, including S3 +- Master and Slave use the same direct MPU6050 register access approach +- shared ESP-NOW packet format supports device typing: + - `IMU` + - `CoG Scale` + - `Hybrid` +- Master data model and UI can already represent CoG-style telemetry fields -### Phase 2: Foundational (13/13 tasks - 100%) +### UI -#### Hardware Foundation +- current tabs remain angle-focused: `Sensors`, `Differential`, `System` +- mixed-device node cards are supported +- differential selection excludes CoG-only nodes +- Master battery display is hidden when the ADC path is not available -- ✅ **Bill of Materials** (`hardware/schematics/bom.csv`) - - Complete component list with Amazon ASINs - - Pricing: $72 for 2-sensor system, $289 for 8-sensor system - - Alternative components documented +### Documentation -- ✅ **Wiring Diagram** (`hardware/schematics/sensor_node_wiring.md`) - - Complete ESP32-MPU6050-LiPo-TP4056 wiring - - Battery monitoring circuit (voltage divider) - - Power supply design (HT7333 LDO) - - Assembly instructions and troubleshooting +- README rewritten for combined AeroAlign + CoG direction +- IMU wiring brought to current C3/S3 and battery-monitoring behavior +- new CoG wiring guide added +- BOM converted from placeholder marketplace list to current component overview +- CAD readme updated to include planned CoG fixtures -- ✅ **3D CAD Documentation** (`hardware/cad/README.md`) - - Sensor housing specs (38mm × 28mm × 18mm) - - Control surface clips (3mm, 5mm, 8mm) - - Print settings for PLA/PETG - - Multi-sensor expansion mounts (Phase 8 ready) +## Current Hardware Documentation -#### Firmware Foundation - Master Node +- [README.md](/Users/florianklaner/Github/AeroAlign/README.md) +- [sensor_node_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/sensor_node_wiring.md) +- [cog_scale_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/cog_scale_wiring.md) +- [bom.csv](/Users/florianklaner/Github/AeroAlign/hardware/schematics/bom.csv) +- [hardware/cad/README.md](/Users/florianklaner/Github/AeroAlign/hardware/cad/README.md) +- [AEROALIGN_COG_INTEGRATION.md](/Users/florianklaner/Github/AeroAlign/docs/AEROALIGN_COG_INTEGRATION.md) -- ✅ **IMU Driver** (`firmware/master/src/imu_driver.cpp/h`) - - MPU6050 6-axis sensor support - - Complementary filter (α=0.98) for angle calculation - - ±0.5° accuracy (static measurement) - - Temperature monitoring - - Calibration support +## What Works Right Now -- ✅ **Calibration Manager** (`firmware/master/src/calibration.cpp/h`) - - NVS (Non-Volatile Storage) persistence - - Zero-point offset storage per node - - Temperature-tagged calibration - - Load/save/clear operations +- Master AP + web UI +- IMU node discovery over ESP-NOW +- calibration and differential measurement for angle nodes +- S3-safe IMU access path +- optional battery reporting on Master hardware where ADC is not wired -- ✅ **ESP-NOW Receiver** (`firmware/master/src/espnow_master.cpp/h`) - - Receive sensor data from Slaves (10Hz) - - Checksum validation (XOR) - - Auto-discovery (no manual pairing) - - Connection timeout detection (1000ms) - - Packet statistics tracking +## What Is Not Finished Yet -- ✅ **Web Server** (`firmware/master/src/web_server.cpp/h`) - - AsyncWebServer (non-blocking) - - REST API endpoints: - - GET /api/nodes (sensor data) - - GET /api/differential (EWD calculation) - - POST /api/calibrate (zero sensors) - - GET /api/status (system health) - - JSON responses (ArduinoJson) +- dedicated `firmware/cog_slave` +- HX711 integration +- tare and scale calibration persistence +- model profiles for support spacing and target CoG +- dedicated CoG workflow in the UI -- ✅ **Master Main Loop** (`firmware/master/src/main.cpp`) - - WiFi Access Point (SSID: "SkyLogic-AeroAlign") - - IP: 192.168.4.1 (captive portal) - - IMU sampling (100Hz) - - ESP-NOW updates (100ms) - - Battery monitoring (1Hz) - - Status reporting (10s intervals) +## Recommended Next Step -#### Firmware Foundation - Slave Node - -- ✅ **IMU Driver** (`firmware/slave/src/imu_driver.cpp/h`) - - Same as Master (copied) - -- ✅ **ESP-NOW Transmitter** (`firmware/slave/src/espnow_slave.cpp/h`) - - Send sensor data to Master (10Hz) - - 15-byte packet format (node_id, pitch, roll, yaw, battery, checksum) - - Pairing with Master MAC - - Transmission statistics - -- ✅ **Slave Main Loop** (`firmware/slave/src/main.cpp`) - - IMU sampling (100Hz) - - ESP-NOW transmission (10Hz / 100ms intervals) - - Battery monitoring (1Hz) - - Low battery warning (LED flash) - - Status reporting (10s intervals) - -#### Infrastructure - -- ✅ **Git Ignore** (`.gitignore`) - - PlatformIO build artifacts - - IDE files (VSCode, IntelliJ, Eclipse) - - Environment files (.env) - - OS-specific files (.DS_Store, Thumbs.db) - -- ✅ **Web UI** (`firmware/master/data/index.html`) - - Real-time angle display (10Hz polling) - - System status dashboard - - Node connection indicators - - Battery/RSSI monitoring - - Responsive design (mobile-friendly) - -- ✅ **Documentation** (`README.md`) - - Project overview and features - - Quick start guide - - Hardware requirements - - Architecture diagram - - API documentation - - Development instructions - ---- - -### Phase 3: User Story 1 - MVP (12/12 tasks - 100%) - -**Web UI Enhancements** (`firmware/master/data/index.html`): -- ✅ Three-tab interface (Sensors, Differential, System) -- ✅ Real-time angle display (10Hz polling) -- ✅ Calibration buttons for each sensor -- ✅ Connection indicators with pulse animation -- ✅ Battery warnings (orange card when <20%) -- ✅ Toast notifications for success/failure -- ✅ Node selectors for differential measurement -- ✅ Color-coded results (green <0.5°, yellow <2.0°, red >2.0°) -- ✅ Responsive mobile design - ---- - -### Phase 4: User Story 2 - Differential Measurement (8/8 tasks - 100%) - -**Median Filtering Implementation** (`firmware/master/src/web_server.cpp/h`): -- ✅ `DifferentialHistory` data structure - - Circular buffer for last 10 readings per node pair - - Supports up to 36 unique node pairs - - Automatic memory management - -- ✅ **Median Calculation Algorithm** - - Bubble sort for small arrays (10 elements) - - Handles even/odd sample counts - - Non-destructive (preserves original data) - -- ✅ **Standard Deviation Calculation** - - Sample standard deviation (n-1 denominator) - - Measures measurement stability - - Color-coded in UI (green <0.1°, yellow <0.3°, red >=0.3°) - -- ✅ **Enhanced API Response** - - `median_diff`: Median of last 10 pitch readings - - `median_pitch`: Median pitch differential - - `median_roll`: Median roll differential - - `std_dev`: Standard deviation of pitch readings - - `std_dev_pitch`: Pitch standard deviation - - `std_dev_roll`: Roll standard deviation - - `readings_count`: Number of samples in buffer (0-10) - -**Web UI Enhancements**: -- ✅ Median value display (primary metric) -- ✅ Current reading display (real-time, unfiltered) -- ✅ Standard deviation indicator (measurement quality) -- ✅ Sample count display (buffer fill status) -- ✅ Color-coded stability feedback - -**Accuracy Achievement**: -- ✅ ±0.1° accuracy via median filtering (vs ±0.5° raw IMU) -- ✅ Real-time stability monitoring -- ✅ Configurable history depth (10 samples = 1 second at 10Hz) - ---- - -## 📊 Implementation Metrics - -### Code Statistics - -**Lines of Code**: -- Master firmware: ~1,500 lines -- Slave firmware: ~600 lines -- Web UI: ~400 lines -- **Total**: ~2,500 lines - -**Files Created**: 25+ -- Firmware: 12 files -- Hardware: 3 files -- Documentation: 10+ files - -### Test Coverage - -- ❌ **Unit Tests**: Not yet implemented (planned for Phase 7) -- ❌ **Integration Tests**: Pending hardware validation -- ✅ **API Contract**: Fully documented in contracts/ - ---- - -## 🔧 What Works Now - -### Firmware Features - -1. **Master Node**: - - WiFi Access Point active - - REST API responding to HTTP requests - - ESP-NOW receiver listening for Slaves - - IMU reading pitch/roll angles - - Calibration stored to NVS - - Battery monitoring active - -2. **Slave Node**: - - ESP-NOW transmitter sending data - - IMU sampling at 100Hz - - Battery percentage calculated - - Low battery warnings - - Connection status LED - -3. **Web Interface**: - - Real-time angle display - - System status dashboard - - Connection indicators - - API endpoint links - -### Testing Status - -**Compilation**: ✅ Both Master and Slave compile successfully -**Runtime**: ⏳ Awaiting hardware testing -**API**: ✅ Endpoints respond with proper JSON - ---- - -## 🚧 What's Next (Phase 5: User Story 3) - -### Immediate Tasks (8 remaining) - -1. **Multi-Node Support** (T041-T043): - - Support 4-6 simultaneous sensor nodes - - Implement scrollable node list in UI - - Add node discovery status indicators - -2. **Enhanced Node Management** (T044-T046): - - Node labeling/naming functionality - - Connection quality indicators - - Battery status for all nodes - -3. **System Scalability** (T047-T048): - - Optimize web UI for multiple nodes - - Test with 6 physical nodes - - Performance validation - -### Hardware Validation - -**Required Equipment**: -- 2× ESP32-C3 DevKits -- 2× MPU6050 IMU modules -- 2× LiPo batteries (250-400mAh) -- USB-C cables -- Breadboard + jumper wires - -**Test Procedure**: -1. Flash Master firmware -2. Note Master MAC address from serial output -3. Update Slave config.h with Master MAC -4. Flash Slave firmware -5. Power on both nodes -6. Connect smartphone to "SkyLogic-AeroAlign" WiFi -7. Open http://192.168.4.1 -8. Verify real-time angle updates - ---- - -## 💡 Key Achievements - -### Technical Excellence - -1. **Auto-Discovery**: Slaves automatically register with Master (no manual pairing) -2. **Low Latency**: <20ms ESP-NOW transmission + 100ms web UI refresh = <120ms total -3. **Robust Protocol**: Checksum validation prevents corrupted data -4. **Persistent Calibration**: NVS storage survives power cycles -5. **Multi-Node Ready**: Architecture supports 8 sensors (Phase 8) - -### Constitution Compliance - -✅ **Cost-Efficiency**: $72 for 2-sensor system (vs. $600 competitors) -✅ **3D Printing**: All parts <200mm³, <30% support -✅ **Lightweight**: Target 23g per node (vs. 25g spec) -✅ **Plug-and-Play**: 3-step setup (power, WiFi, browser) - ---- - -## 🐛 Known Issues - -1. **Master MAC Hardcoding**: Slave requires manual MAC entry in config.h - - **Impact**: Medium (one-time setup) - - **Workaround**: Serial monitor shows Master MAC - - **Fix**: Phase 8 auto-discovery feature - -2. **Web UI Placeholder**: Basic HTML/JS (not full React) - - **Impact**: Low (functional but not polished) - - **Status**: Phase 3 will add React components - -3. **No Physical Testing**: Firmware untested on hardware - - **Impact**: High (may have runtime bugs) - - **Status**: Awaiting hardware delivery - -4. **Missing STL Files**: 3D models documented but not created - - **Impact**: Medium (blocks physical assembly) - - **Status**: Requires FreeCAD design (Phase 7) - ---- - -## 📈 Timeline Estimate - -**Completed** (Phases 1-4): ~14 hours -- Phase 1-2 (Setup + Foundation): ~8 hours -- Phase 3 (MVP): ~3 hours -- Phase 4 (Differential): ~3 hours - -**Remaining**: -- Phase 5-6 (US3-US4): 6-8 hours -- Phase 7 (Polish): 6-8 hours -- Phase 8 (Multi-Sensor): 10-12 hours - -**Total Project**: ~40-50 hours for full feature set - ---- - -## 🎯 Next Session Goals - -1. **Hardware testing** with 2 physical nodes (validate Phase 3-4 implementation) -2. **Phase 5**: Multi-node support (4-6 sensors) - - Implement scrollable node list in web UI - - Add node labeling functionality - - Test with multiple Slave nodes -3. **Phase 6**: Throw gauge mode (control surface deflection) -4. **3D Printing**: Generate STL files for sensor housing -5. **Documentation**: Assembly guides and troubleshooting - ---- - -**Status**: MVP + Differential Measurement complete! Ready for multi-node expansion. 🚀 +Implement the actual CoG node firmware next, because the protocol, Master-side data path and baseline documentation are now in place. diff --git a/README.md b/README.md index 4ea429f..1a330e8 100644 --- a/README.md +++ b/README.md @@ -1,441 +1,174 @@ -# SkyLogic AeroAlign - Wireless RC Telemetry System +# SkyLogic AeroAlign -**Precision Grounded.** +Wireless RC setup platform for two related jobs: -A low-cost, open-source wireless digital incidence and throw meter system for RC airplane setup. Replaces traditional spirit levels and pendulum meters with precise IMU-based angle measurement. +- AeroAlign: digital incidence, reference-angle and differential measurement +- CoG Scale: center-of-gravity measurement using load cells ---- +Both parts are intended to run in the same ESP32 ecosystem and talk to the same Master over ESP-NOW. -## 🎯 Project Status +## Current State -**Phase 4: Differential Measurement (COMPLETE)** ✅ +Implemented now: -The MVP is fully functional with advanced differential measurement capabilities: +- Master firmware with WiFi AP, REST API and existing web UI +- IMU Master and IMU Slave support with robust MPU6050 access +- shared telemetry protocol for multiple device types +- UI support for both IMU nodes and future CoG scale nodes +- battery monitoring with optional hide/disable behavior on unsupported Master hardware -- ✅ **Master Node Firmware**: WiFi AP, ESP-NOW receiver, IMU driver, web server with REST API -- ✅ **Slave Node Firmware**: ESP-NOW transmitter, IMU driver, battery monitoring -- ✅ **Web UI**: Three-tab interface with real-time sensor display, calibration, and differential measurement -- ✅ **Median Filtering**: ±0.1° accuracy via 10-sample median filter with standard deviation monitoring -- ✅ **Hardware Design**: Bill of Materials ($72 for 2-sensor system), wiring diagrams, 3D printable housing specs -- ✅ **Development Environment**: PlatformIO configurations for ESP32-C3/ESP32-S3 +Prepared but not fully implemented yet: -**Next Steps**: Phase 5 - Multi-node support for 4-6 simultaneous sensors +- dedicated HX711-based CoG scale firmware +- model profiles with support spacing, leading-edge offset and target CoG +- scale calibration workflow in the web UI ---- - -## ⚡ Quick Start - -### For End Users - -1. **Flash Firmware**: - ```bash - cd firmware/master - pio run --target upload - - cd ../slave - pio run --target upload - ``` - -2. **Update Slave Configuration**: - - Connect Master to USB, open serial monitor (115200 baud) - - Note the Master MAC address printed at startup - - Edit `firmware/slave/src/config.h` and replace `master_mac` array - - Reflash Slave firmware - -3. **Power On**: - - Power on both Master and Slave nodes - - Connect smartphone to WiFi: **"SkyLogic-AeroAlign"** - - Open browser: **http://192.168.4.1** - -4. **Use**: - - Attach sensors to RC model control surfaces - - View real-time angles in web UI - - Calibrate (zero) sensors via web interface - -### For Developers - -See [specs/001-wireless-rc-telemetry/quickstart.md](specs/001-wireless-rc-telemetry/quickstart.md) for detailed build instructions. - ---- - -## 📋 Features - -### Implemented (Phases 1-4: MVP + Differential) - -**Core Firmware**: -- ✅ **IMU Driver**: MPU6050 6-axis sensor with complementary filter (±0.5° raw accuracy) -- ✅ **ESP-NOW Protocol**: Low-latency (<20ms) wireless communication between nodes -- ✅ **WiFi Access Point**: Captive portal for smartphone/tablet connection (no internet required) -- ✅ **REST API**: HTTP endpoints for sensor data, calibration, differential, and system status -- ✅ **Calibration**: Zero-point calibration with NVS (Non-Volatile Storage) persistence -- ✅ **Battery Monitoring**: Real-time battery percentage via ADC -- ✅ **Multi-Node Support**: Auto-discovery of up to 8 sensor nodes - -**Web UI**: -- ✅ **Real-Time Display**: 10Hz polling with three-tab interface (Sensors, Differential, System) -- ✅ **Calibration Interface**: One-click zero calibration for each sensor -- ✅ **Connection Indicators**: Pulse animation, timeout detection (1000ms) -- ✅ **Battery Warnings**: Visual alerts when <20% -- ✅ **Toast Notifications**: Success/failure feedback - -**Differential Measurement**: -- ✅ **Median Filtering**: ±0.1° accuracy via 10-sample circular buffer -- ✅ **Standard Deviation**: Real-time measurement stability indicator -- ✅ **Color-Coded Results**: Green (<0.5°), Yellow (<2.0°), Red (>=2.0°) -- ✅ **EWD Mode**: Wing-to-elevator incidence calculation -- ✅ **Node Pair Selection**: Arbitrary sensor pairs via dropdown - -### Planned (Phases 5-8) - -- 📅 **Multi-Sensor UI**: 4-6 node simultaneous display with scrollable list -- 📅 **Throw Gauge Mode**: Control surface deflection distance measurement -- 📅 **8-Sensor Grid**: Full aircraft setup (wings, ailerons, elevator, rudder) -- 📅 **Sensor Pairing**: Aileron differential, butterfly mode, multi-wing configurations -- 📅 **Specialized Mounts**: Wing surface clips, hinge line mounts, magnetic quick-attach - ---- - -## 🔧 Hardware Requirements - -### Core Components (Per Sensor Node) - -| Component | Spec | Price | Source | -|-----------|------|-------|--------| -| ESP32-C3 DevKit | RISC-V 160MHz, WiFi, USB-C | $6.50 | [Amazon](https://amazon.com) | -| MPU6050 IMU | 6-axis (gyro + accel), I2C | $4.50 | [Amazon](https://amazon.com) | -| LiPo Battery | 1S 3.7V 250-400mAh | $8.00 | [Amazon](https://amazon.com) | -| TP4056 Charger | USB-C, overcharge protection | $1.50 | [Amazon](https://amazon.com) | -| HT7333 LDO | 3.3V 250mA regulator | $0.80 | [Amazon](https://amazon.com) | -| **Total per node** | | **~$23** | | - -**2-Sensor System**: ~$72 (Master + Slave) -**4-Sensor System**: ~$145 -**8-Sensor System**: ~$289 - -See [hardware/schematics/bom.csv](hardware/schematics/bom.csv) for complete Bill of Materials. - -### 3D Printed Parts - -- Sensor housing (38mm × 28mm × 18mm) -- Control surface clips (3mm, 5mm, 8mm variants) -- Wing surface mounts (Phase 8) - -See [hardware/cad/README.md](hardware/cad/README.md) for STL files and print settings. - ---- - -## 📐 Architecture - -### System Overview +## Architecture ``` -Master Node (WiFi AP + Web Server + ESP-NOW Receiver) - ↑ WiFi (HTTP/JSON) - ↑ -Smartphone/Tablet (Web UI) - -Master Node - ↑ ESP-NOW (2.4GHz) - ↑ -Slave Node(s) (0x02-0x09) +IMU Slave(s) ------\ + \ +CoG Scale Node(s) ----> Master ESP32 ----> WiFi AP ----> Browser UI + / +Master local IMU --/ ``` -### Master Node Responsibilities +### Device roles -1. **WiFi Access Point**: Hosts "SkyLogic-AeroAlign" network (192.168.4.1) -2. **Web Server**: Serves React UI and REST API endpoints -3. **ESP-NOW Receiver**: Accepts sensor data from Slave nodes (10Hz) -4. **IMU Sensor**: Measures Master node's own pitch/roll -5. **Calibration Manager**: Stores/loads zero-point offsets (NVS) +- `Master` + - hosts `SkyLogic-AeroAlign` + - receives ESP-NOW telemetry + - serves the web UI and REST API + - can also have its own MPU6050 +- `IMU Slave` + - remote angle node + - sends pitch, roll and yaw fields as angle telemetry +- `CoG Scale` + - future load-cell node + - will send front weight, rear weight and computed CoG in the same shared packet -### Slave Node Responsibilities +## Telemetry Protocol -1. **IMU Sensor**: Measures pitch/roll at 100Hz -2. **ESP-NOW Transmitter**: Sends data to Master at 10Hz -3. **Battery Monitoring**: Reports battery percentage -4. **Low Power**: No WiFi AP (only ESP-NOW), extends battery life to 4-5 hours +The shared packet format is defined in [telemetry_protocol.h](/Users/florianklaner/Github/AeroAlign/firmware/common/telemetry_protocol.h). -### Data Flow +Device types currently defined: -1. Slave IMU samples at 100Hz (10ms intervals) -2. Slave transmits ESP-NOW packet to Master at 10Hz (100ms intervals) -3. Master receives packet, validates checksum, updates node data -4. Web UI polls GET /api/nodes every 100ms (10Hz) -5. React UI updates angle displays in real-time +- `DEVICE_TYPE_IMU` +- `DEVICE_TYPE_COG_SCALE` +- `DEVICE_TYPE_HYBRID` ---- +For IMU nodes: -## 🌐 API Endpoints +- `pitch`, `roll`, `yaw` are angles -### GET /api/nodes +For CoG scale nodes: -Returns all connected sensor nodes with current angles, battery, RSSI. +- `pitch` => front support weight in grams +- `roll` => rear support weight in grams +- `yaw` => CoG position in millimeters -**Response**: -```json -[ - { - "node_id": 1, - "label": "Master", - "pitch": -2.35, - "roll": 0.87, - "yaw": 0.0, - "battery_percent": 85, - "battery_voltage": 3.95, - "rssi": -45, - "is_connected": true, - "last_update_ms": 123456789 - }, - { - "node_id": 2, - "label": "Sensor 1", - "pitch": -3.12, - "roll": 0.43, - ... - } -] -``` +## Hardware Overview -### GET /api/differential?node1=1&node2=2 +### IMU nodes -Calculates differential angle between two nodes (for EWD measurement). +Use: -**Response**: -```json -{ - "node1_id": 1, - "node2_id": 2, - "node1_label": "Wing Root", - "node2_label": "Elevator", - "angle_diff_pitch": 0.77, - "angle_diff_roll": 0.44, - "median_diff": 0.75, - "std_dev": 0.03, - "mode": "EWD" -} -``` +- ESP32-C3 or ESP32-S3 +- MPU6050 +- LiPo, charger and 3.3V supply +- optional battery divider depending on node -### POST /api/calibrate +See [sensor_node_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/sensor_node_wiring.md). -Zero-calibrates a sensor node (sets current angle as 0°). +### CoG scale -**Request**: -```json -{ - "node_id": 1 -} -``` +Planned hardware stack: -**Response**: -```json -{ - "success": true, - "message": "Node calibrated", - "node_id": 1, - "pitch_offset": -2.35, - "roll_offset": 0.87, - "yaw_offset": 0.0, - "timestamp": 1737590400 -} -``` +- ESP32-C3 or ESP32-S3 +- two HX711 boards +- two load cells +- rigid support spacing and repeatable fixtures -### GET /api/status +See [cog_scale_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/cog_scale_wiring.md). -System health and statistics. +### BOM -**Response**: -```json -{ - "master_battery_percent": 75, - "master_battery_voltage": 3.85, - "wifi_clients_connected": 1, - "wifi_channel": 6, - "uptime_seconds": 3600, - "esp_now_packets_received": 1200, - "esp_now_packet_loss_rate": 0.02, - "firmware_version": "1.0.0", - "hardware_model": "ESP32-C3", - "free_heap_kb": 280 -} -``` +The BOM is now maintained as a current component overview instead of placeholder shop links: ---- +- [bom.csv](/Users/florianklaner/Github/AeroAlign/hardware/schematics/bom.csv) -## 🛠️ Development +## Firmware Layout -### Project Structure +### Existing -``` -ewd-digiflo/ -├── firmware/ -│ ├── master/ # Master node firmware -│ │ ├── src/ -│ │ │ ├── main.cpp # WiFi AP + ESP-NOW + Web Server -│ │ │ ├── imu_driver.cpp/h -│ │ │ ├── espnow_master.cpp/h -│ │ │ ├── calibration.cpp/h -│ │ │ ├── web_server.cpp/h -│ │ │ └── config.h # WiFi SSID, GPIO pins, constants -│ │ ├── data/ -│ │ │ └── index.html # React web UI (to be implemented) -│ │ └── platformio.ini -│ │ -│ └── slave/ # Slave node firmware -│ ├── src/ -│ │ ├── main.cpp # ESP-NOW transmitter + IMU -│ │ ├── imu_driver.cpp/h -│ │ ├── espnow_slave.cpp/h -│ │ └── config.h # Master MAC, node ID -│ └── platformio.ini -│ -├── hardware/ -│ ├── cad/ # 3D printable STL files (placeholders) -│ ├── schematics/ -│ │ ├── bom.csv # Bill of Materials -│ │ └── sensor_node_wiring.md # Wiring diagrams -│ └── docs/ -│ -├── docs/ # End-user documentation -│ ├── quickstart.md # Setup guide -│ ├── calibration.md # Calibration procedures -│ └── troubleshooting.md # Common issues -│ -└── specs/ # Design specifications - └── 001-wireless-rc-telemetry/ - ├── spec.md # Feature specification - ├── plan.md # Implementation plan - ├── tasks.md # Task breakdown (86 tasks) - ├── data-model.md # Data structures - ├── research.md # Component selection - ├── quickstart.md # Developer guide - └── contracts/ - ├── http-api.md # REST API spec - └── espnow-protocol.md # Binary protocol spec -``` +- [firmware/master](/Users/florianklaner/Github/AeroAlign/firmware/master) +- [firmware/slave](/Users/florianklaner/Github/AeroAlign/firmware/slave) +- [firmware/common](/Users/florianklaner/Github/AeroAlign/firmware/common) -### Build Instructions +### Planned -**Prerequisites**: -- [PlatformIO](https://platformio.org/) (VS Code extension or CLI) -- Python 3.8+ (for esptool.py) -- Git +- `firmware/cog_slave` + - HX711 reading + - tare and scale calibration + - CoG computation + - ESP-NOW transmission as `DEVICE_TYPE_COG_SCALE` + +## Build + +Master: -**Build Master Firmware**: ```bash cd firmware/master -pio run # Compile -pio run --target upload # Flash to ESP32 -pio device monitor # View serial output (115200 baud) +pio run ``` -**Build Slave Firmware**: +Slave: + ```bash cd firmware/slave -pio run --target upload -pio device monitor +pio run -e esp32-s3 +pio run -e slave2-s3 ``` -**Build All 8 Slave Variants** (Phase 8): -```bash -cd firmware/slave -./build_all_slaves.sh # Compiles slave1-slave8 with different NODE_IDs -``` +## Current Configuration Notes ---- +### Slave Master MAC -## 📊 Project Progress +The Slave now reads the Master MAC from [config.cpp](/Users/florianklaner/Github/AeroAlign/firmware/slave/src/config.cpp), not `config.h`. -### Completed (32/130 tasks, 25%) +### Master battery monitoring -| Phase | Tasks | Status | -|-------|-------|--------| -| **Phase 1: Setup** | 7/7 | ✅ 100% | -| **Phase 2: Foundational** | 13/13 | ✅ 100% | -| **Phase 3: User Story 1 (MVP)** | 12/12 | ✅ 100% | -| **Phase 4: User Story 2** | 8/8 | ✅ 100% | -| Phase 5: User Story 3 | 0/8 | 📅 Next | -| Phase 6: User Story 4 | 0/7 | 📅 Not Started | -| Phase 7: Polish | 0/31 | 📅 Not Started | -| Phase 8: Multi-Sensor | 0/44 | 📅 Not Started | +On ESP32-S3 Master boards the battery ADC path is optional. If the divider is not fitted, the firmware and UI hide the battery value instead of showing dummy data. -### Next Milestones +## CoG Math -1. **Phase 5 (Multi-Node)**: Complete User Story 3 - 4-6 Sensor Support - - Implement scrollable node list in web UI - - Add node labeling/naming functionality - - Test with 4-6 physical nodes +With front and rear supports spaced by `L`: -2. **Phase 6 (Throw Gauge)**: Control surface throw measurement - - Distance measurement mode - - Min/max deflection tracking +`x_cog_from_front_support = rear_weight / (front_weight + rear_weight) * L` -3. **Phase 7 (Polish)**: Testing and refinement - - Unit tests for all modules - - 3D printing validation - - Comprehensive documentation +If the front support is offset from the wing leading edge: ---- +`x_cog_from_leading_edge = support_offset_from_leading_edge + x_cog_from_front_support` -## 🔬 Constitution Compliance +The current design note is in [AEROALIGN_COG_INTEGRATION.md](/Users/florianklaner/Github/AeroAlign/docs/AEROALIGN_COG_INTEGRATION.md). -This project adheres to the [EWD-DigiFlow Constitution](.specify/memory/constitution.md): +## Workflow Plan -### I. Extreme Cost-Efficiency ✅ -- All components available on Amazon -- **$72 per 2-sensor system** (vs. GliderThrow $600 for 8 sensors) -- 77% cost savings compared to competitors +The planned operating workflow and the recommended hardware roles are documented in: -### II. 3D Printing Reproducibility ✅ -- All parts fit within 200mm × 200mm × 200mm build volume -- Support structures <30% part volume -- Print settings documented for PLA/PETG +- [WORKFLOW_AND_SYSTEM_PLAN.md](/Users/florianklaner/Github/AeroAlign/docs/WORKFLOW_AND_SYSTEM_PLAN.md) +- [AIRCRAFT_PROFILE_MODEL.md](/Users/florianklaner/Github/AeroAlign/docs/AIRCRAFT_PROFILE_MODEL.md) -### III. Lightweight Design ✅ -- Each sensor node: ~23g (target: <25g) -- Minimal impact on control surface movement +## 3D Printed Parts -### IV. Software Simplicity (Plug-and-Play) ✅ -- No app store submission required (web UI) -- No cloud services or internet needed -- 3-step setup: Power on → Connect WiFi → Open browser +The CAD readme now covers both IMU parts and planned CoG fixtures: ---- +- [hardware/cad/README.md](/Users/florianklaner/Github/AeroAlign/hardware/cad/README.md) -## 🤝 Contributing +## Recommended Next Work -Contributions welcome! This is an open-source project for the RC community. - -**Areas Needing Help**: -- Web UI design (React components) -- 3D CAD designs (FreeCAD → STL) -- Hardware testing (different 3D printers, IMU calibration) -- Documentation (assembly guides, troubleshooting) - -See [specs/001-wireless-rc-telemetry/tasks.md](specs/001-wireless-rc-telemetry/tasks.md) for detailed task breakdown. - ---- - -## 📄 License - -**Firmware**: MIT License -**Hardware Designs** (STL, schematics): Creative Commons BY-SA 4.0 - ---- - -## 🙏 Acknowledgments - -- **Adafruit** for MPU6050 and sensor libraries -- **Espressif** for ESP32 Arduino framework and ESP-NOW protocol -- **RC Community** for feature requests and beta testing - ---- - -## 📞 Support - -- **Documentation**: [specs/001-wireless-rc-telemetry/](specs/001-wireless-rc-telemetry/) -- **Issues**: [GitHub Issues](https://github.com/your-org/skylogic-aeroalign/issues) -- **Community**: [RC Groups Forum Thread](https://www.rcgroups.com/forums/showthread.php?12345) - ---- - -*SkyLogic AeroAlign - Precision Grounded.* +1. Add `firmware/cog_slave` with HX711 support. +2. Add tare and calibration endpoints in the Master API. +3. Add a dedicated CoG tab in the web UI. +4. Add aircraft profiles in NVS. +5. Finalize mechanical design for the two support fixtures. diff --git a/docs/AEROALIGN_COG_INTEGRATION.md b/docs/AEROALIGN_COG_INTEGRATION.md new file mode 100644 index 0000000..fc24970 --- /dev/null +++ b/docs/AEROALIGN_COG_INTEGRATION.md @@ -0,0 +1,61 @@ +# AeroAlign + CoG Scale Integration + +## Zielbild + +Das System wird von einem reinen digitalen Einstellwinkelmesser zu einer gemeinsamen RC-Setup-Plattform erweitert: + +- AeroAlign-Knoten messen Pitch/Roll fuer EWD, Ruderausschlag und Bezugswinkel. +- CoG-Scale-Knoten messen Front- und Heckauflagegewicht ueber Load Cells. +- Ein gemeinsamer ESP32-Master sammelt beide Geraeteklassen ueber ESP-NOW und stellt alles in derselben Web-Oberflaeche bereit. + +## Externe Referenz + +Die geplante CoG-Funktion orientiert sich konzeptionell an diesem Projekt: + +- [RC plane Center of Gravity finder | Hackaday.io](https://hackaday.io/project/202834-rc-plane-center-of-gravity-finder/details) + +Aus der Referenz uebernommen: + +- zwei digitale Waagen mit ESP32 +- Front- und Heckgewicht als Echtzeitdaten +- CoG-Berechnung aus dem Abstand der Auflagepunkte +- Speicherung modellspezifischer Parameter im Geraet + +## Mechanisches Modell + +Wenn die vordere Auflage bei `x = 0` und die hintere bei `x = L` liegt, dann gilt aus dem Momentengleichgewicht: + +`x_cog_from_front_support = rear_weight / (front_weight + rear_weight) * L` + +Das ist eine technische Ableitung aus der in der Hackaday-Referenz gezeigten Zwei-Waagen-Anordnung. + +Um die Lage relativ zur Fluegelvorderkante zu erhalten: + +`x_cog_from_leading_edge = support_offset_from_leading_edge + x_cog_from_front_support` + +## Netzwerkmodell + +Ein gemeinsames ESP-NOW-Protokoll transportiert nun Geraetetypen: + +- `IMU`: klassische AeroAlign-Sensoren +- `CoG Scale`: Waagenknoten mit Front/Heck/CoG +- `Hybrid`: spaeter fuer kombinierte Vorrichtungen + +Der Master muss deshalb keine getrennten Netze oder Apps mehr betreiben. + +## Empfohlene Phase 5 + +1. Eigenen CoG-Slave auf Basis `firmware/slave` anlegen. +2. HX711-Treiber und Tare/Kalibrierung integrieren. +3. Master-API um Modellparameter `support_distance_mm` und `leading_edge_offset_mm` erweitern. +4. Web-UI um CoG-Ansicht mit Livewerten, Sollwert und Ballast-Rechner erweitern. +5. NVS-Datenmodell fuer Flugzeugprofile einfuehren. + +## Firmware-Schnittstelle + +Aktuell ist im Protokoll nur die Transportbasis vorbereitet: + +- `pitch/roll/yaw` bleiben fuer IMU-Knoten erhalten. +- CoG-Scale-Knoten belegen dieselben drei Float-Felder mit `front_weight_g`, `rear_weight_g` und `cog_position_mm`. + +Das ist bewusst pragmatisch, damit bestehende ESP-NOW-Kommunikation erhalten bleibt und die eigentliche HX711-Integration als naechster Schritt isoliert umgesetzt werden kann. diff --git a/docs/AIRCRAFT_PROFILE_MODEL.md b/docs/AIRCRAFT_PROFILE_MODEL.md new file mode 100644 index 0000000..5bc9427 --- /dev/null +++ b/docs/AIRCRAFT_PROFILE_MODEL.md @@ -0,0 +1,268 @@ +# Aircraft Profile Model + +## Zweck + +Diese Datei ist die verbindliche Fachvorgabe fuer das Modul, das AeroAlign und CoG gemeinsam bedienen soll. + +Sie legt fest: + +- welche Flugzeugprofile es gibt +- welche Flaechenarten unterschieden werden +- welche Workflows pro Profil aktiv sind +- welche Spezialfaelle ausdruecklich anders behandelt werden + +## Begriffe + +### Feste Flaechen + +Feste, nicht verstellte aerodynamische Referenzflaechen. + +Beispiele: + +- Tragflaeche +- Dämpfungsflosse Hoehenleitwerk +- Dämpfungsflosse Seitenleitwerk +- feste Canard-Flaeche + +### Bewegliche Flaechen + +Aktiv angelenkte Flaechen oder Klappen. + +Beispiele: + +- Querruder +- Hoehenruder +- Seitenruder +- Wölbklappe +- Spoiler +- Taileron +- Pendelleitwerk + +### Referenzflaeche + +Die Flaeche, gegen die eine andere Flaeche gemessen wird. + +Regel: + +Bewegliche Flaechen werden gegen ihre zugehoerige feste Referenz gemessen, nicht pauschal gegen andere bewegliche Flaechen. + +## Grundsaetze + +1. `EWD / Referenzgeometrie` ist ein eigener Workflow. +2. `CG / Schwerpunkt` ist ein eigener Workflow. +3. `Ruder- und Klappeneinstellung` ist ein eigener Workflow. +4. `90 Grad Servoarm` ist keine universelle Fachregel und darf nur als optionale Montagehilfe behandelt werden. +5. `Pendelleitwerk` und `Taileron` sind keine normalen Hoehenruder. + +## Profiltypen + +### 1. `classic` + +Klassisches Flaechenflugzeug mit: + +- Tragflaeche +- Dämpfungsflosse Hoehenleitwerk +- Hoehenruder +- Dämpfungsflosse Seitenleitwerk +- Seitenruder +- Querruder + +Aktive Workflows: + +- `reference` +- `surfaces` +- `cog` +- spaeter `throw` + +MVP-Status: + +- dies ist das primäre Startprofil fuer das System + +### 2. `jet_taileron` + +Jet mit kombiniertem Hoehen-/Querruder an beweglichen Heckflaechen. + +Merkmale: + +- keine klassische Trennung zwischen Hoehenruder und Querruder hinten +- linke und rechte Tailerons muessen symmetrisch und differenziert pruefbar sein + +Aktive Workflows: + +- `reference` +- `surfaces` +- `cog` +- spaeter `mixing_check` + +Sonderregel: + +Tailerons werden nicht wie normale Hoehenruder gegen eine Dämpfungsflosse behandelt. + +### 3. `stabilator` + +All-flying stabilizer oder Pendelhöhenleitwerk. + +Merkmale: + +- gesamte Flaeche bewegt sich +- keine feste Dämpfungsflosse als lokale Referenzflaeche + +Aktive Workflows: + +- `reference` +- `surfaces` +- `cog` + +Sonderregel: + +Die bewegliche Gesamtflaeche wird gegen eine externe Referenz gemessen, typischerweise Tragflaeche oder definierte Vorrichtung. + +### 4. `glider_flap` + +Segler mit Querrudern, Wölbklappen und oft mehreren Flugphasen. + +Merkmale: + +- Grundneutralstellung kann je nach Flugphase bewusst nicht null sein +- Butterfly, Speed, Thermik und Cruise koennen unterschiedliche Sollwerte haben + +Aktive Workflows: + +- `reference` +- `surfaces` +- `cog` +- spaeter `flight_mode_offsets` + +### 5. `delta_elevon` + +Delta oder Nurfluegel mit kombinierten Elevons. + +Merkmale: + +- keine klassische EWD wie beim Leitwerksflugzeug +- linke und rechte Elevons sind kombinierte Hoehen-/Querruderflaechen + +Aktive Workflows: + +- `reference` +- `surfaces` +- `cog` + +Sonderregel: + +Es gibt keine klassische Leitwerksreferenz. Das Profil darf keine Leitwerkslogik erzwingen. + +## Flaechentypen fuer das Datenmodell + +Das Modul soll mindestens folgende Typen unterscheiden: + +- `wing_reference` +- `tailplane_fixed` +- `fin_fixed` +- `aileron` +- `elevator` +- `rudder` +- `flap` +- `spoiler` +- `taileron` +- `stabilator` +- `elevon` +- `canard_fixed` +- `canard_control` + +## Workflow-Freigabe pro Profil + +| Profil | Reference | Surfaces | CoG | Throw später | Speziallogik | +|--------|-----------|----------|-----|--------------|--------------| +| `classic` | ja | ja | ja | ja | gering | +| `jet_taileron` | ja | ja | ja | ja | hoch | +| `stabilator` | ja | ja | ja | ja | hoch | +| `glider_flap` | ja | ja | ja | ja | mittel | +| `delta_elevon` | ja | ja | ja | ja | hoch | + +## Messregeln pro Flaechenart + +### Querruder + +Gegen: + +- linke oder rechte Tragflaechenreferenz + +### Hoehenruder + +Gegen: + +- Dämpfungsflosse Hoehenleitwerk + +### Seitenruder + +Gegen: + +- Dämpfungsflosse Seitenleitwerk + +### Wölbklappen + +Gegen: + +- zugehoerige Tragflaechenreferenz + +Hinweis: + +Die Sollstellung muss nicht `0.0` sein. + +### Taileron + +Gegen: + +- profilabhaengige Heckreferenz oder externe Referenz + +### Stabilator + +Gegen: + +- externe Referenzflaeche + +Nicht gegen: + +- nicht vorhandene Dämpfungsflosse + +## UI-Folgen + +Die UI darf nicht einfach fuer alle Modelle dieselben Labels oder Buttons anzeigen. + +Pflicht: + +- Profilwahl beim Anlegen des Modells +- sprechende Flaechenrollen +- nur die fuer das Profil gueltigen Workflows anzeigen +- Sonderflaechen mit eigenen Begriffen benennen + +Beispiele: + +- `Elevator` nur bei `classic` +- `Stabilator` bei `stabilator` +- `Taileron Left/Right` bei `jet_taileron` +- `Flap Left/Right` bei `glider_flap` + +## MVP-Grenze + +Fuer den ersten echten Produktstand soll das Modul fachlich vorbereitet sein, aber operativ auf `classic` optimiert werden. + +MVP in der Praxis: + +- klassische Tragflaeche +- Dämpfungsflosse Hoehenleitwerk +- Hoehenruder +- Querruder links/rechts +- Seitenruder optional +- CoG-Messung + +Andere Profile muessen im Datenmodell vorgesehen sein, auch wenn sie anfangs noch nicht vollstaendig in der UI freigeschaltet sind. + +## Verbindliche Anweisung fuer die weitere Implementierung + +1. Keine harte Verdrahtung auf nur `elevator`, `aileron`, `rudder`. +2. Workflow-Entscheidungen muessen profilabhaengig sein. +3. `EWD`, `CG` und `surface setup` bleiben getrennte Modi. +4. `Dämpfungsflosse` ist der Standardbegriff fuer den feststehenden Leitwerksteil. +5. `90 Grad Servoarm` darf nie als alleinige Sollbedingung modelliert werden. diff --git a/docs/WORKFLOW_AND_SYSTEM_PLAN.md b/docs/WORKFLOW_AND_SYSTEM_PLAN.md new file mode 100644 index 0000000..10dea20 --- /dev/null +++ b/docs/WORKFLOW_AND_SYSTEM_PLAN.md @@ -0,0 +1,281 @@ +# AeroAlign + CoG - Workflow and System Plan + +## Ziel + +Das System soll in der Werkstatt ohne Umdenken bedienbar sein und drei unterschiedliche Aufgaben sauber trennen: + +1. feste Referenzflaechen ausrichten +2. Ruder neutral oder symmetrisch einstellen +3. Schwerpunkt messen und verschieben + +Der wichtigste Grundsatz ist: + +Ruder werden nicht primaer gegeneinander verglichen, sondern gegen ihre jeweilige feste Bezugsflaeche. + +Zusatz fuer Spezialfaelle: + +- EWD, CG und Rudereinstellung bleiben getrennte Ablaeufe +- Dämpfungsflosse ist der bevorzugte Begriff fuer den feststehenden Leitwerksteil +- Pendelleitwerk und Taileron werden als eigene Flaechentypen behandelt +- 90 Grad am Servoarm ist keine universelle aerodynamische Regel + +## Bedienlogik + +### Was nicht ideal ist + +Dieser Ablauf ist fehleranfaellig: + +- Modell auf die CoG-Waage stellen +- Querruder und Hoehenruder direkt miteinander vergleichen +- eines davon so lange verstellen, bis beide denselben Winkel zeigen + +Warum das problematisch ist: + +- Tragflaeche und Hoehenleitwerk haben oft unterschiedliche Sollwinkel +- ein neutrales Hoehenruder ist nicht automatisch derselbe Winkel wie ein neutrales Querruder +- Sender-Subtrim sollte nicht die mechanische Grundjustage ersetzen + +### Besserer Ablauf + +1. Modell mechanisch neutralisieren +2. feste Flaechen und Leitwerke referenzieren +3. Ruder gegen die zugehoerige feste Flaeche einstellen +4. Schwerpunkt auf der CoG-Waage einstellen +5. Neutralstellungen danach kurz erneut pruefen + +## Empfohlene Werkstattmodi + +### 1. Referenzmodus + +Ziel: + +- Tragflaeche gegen Hoehenleitwerk +- Tragflaeche links gegen Tragflaeche rechts +- EWD oder andere feste Bezugswinkel + +Hinweis: + +Dieser Modus ist fachlich eigenstaendig und darf nicht mit CoG oder Ruderneutralitaet vermischt werden. + +Empfohlene Sensoren: + +- ein Sensor auf einer festen Tragflaechen-Referenz +- ein Sensor auf Hoehenleitwerk oder anderer Referenzflaeche + +### 2. Rudermodus + +Ziel: + +- Querruder links gegen linke Tragflaeche +- Querruder rechts gegen rechte Tragflaeche +- Hoehenruder gegen Hoehenleitwerk +- Seitenruder gegen Seitenleitwerk + +Regel: + +Jedes bewegliche Ruder wird gegen seine feste Nachbarflaeche verglichen. + +Ausnahmen: + +- Stabilator gegen externe Referenz +- Taileron nach Profilregel +- Wölbklappen mit moeglichem Grundoffset je Flugphase + +### 3. CoG-Modus + +Ziel: + +- Frontgewicht +- Heckgewicht +- Schwerpunktlage in mm +- Abweichung zum Soll-CoG + +Hier spielen die IMU-Sensoren nur indirekt mit: + +- sie helfen vorher und nachher bei der Neutral- und Referenzpruefung +- die CoG-Messung selbst basiert auf den Lastzellen + +## Minimal sinnvolles Hardware-Set + +### Variante A: praxisnahes Basisset + +- `1x Master` mit Web UI +- `2x IMU Slave` +- `1x CoG Scale` mit zwei Lastzellen + +Einsatz: + +- Sensor 1 auf fester Tragflaeche oder Referenzlehre +- Sensor 2 auf Hoehenleitwerk oder Hoehenruder +- CoG-System separat fuer den Schwerpunkt + +Das reicht fuer EWD, Hoehenruder-Neutralitaet und Schwerpunkt. + +### Variante B: guter Alltagsausbau + +- `1x Master` +- `4x IMU Slave` +- `1x CoG Scale` + +Einsatz: + +- linke Tragflaeche Referenz +- rechtes Querruder +- linkes Querruder +- Hoehenleitwerk oder Hoehenruder +- CoG-Jig parallel verfuegbar + +Damit werden Querruder-Symmetrie und Leitwerksabgleich deutlich komfortabler. + +### Variante C: Vollausbau + +- `1x Master` +- `5-6x IMU Slave` +- `1x CoG Scale` + +Zusaetzlich: + +- Seitenruder +- zweite Flaechenreferenz +- weitere Klappen oder Spoiler + +## Empfohlene Hardware-Rollen + +### Master + +- stationaeres Bediengeraet +- optional eigener IMU-Sensor fuer Referenzaufgaben +- WiFi AP und Web UI + +### IMU Slaves + +- kleine, leichte Clip-Sensoren +- identische Gehaeuse und Halter, damit Bedienung einheitlich bleibt +- eindeutige Rollen im UI statt nur Node-IDs + +Beispielrollen: + +- `Wing Ref Left` +- `Wing Ref Right` +- `Aileron Left` +- `Aileron Right` +- `Elevator` +- `Fin / Rudder` + +### CoG Scale + +- ein eigenes Geraet mit zwei Auflagepunkten +- zwei Lastzellen +- fester, reproduzierbarer Auflageabstand +- idealerweise mit definierter LE-Referenz oder verstellbarem Anschlag + +## Bedienbarkeit in der UI + +Die UI sollte nicht nur Nodes anzeigen, sondern gefuehrte Aufgaben. + +### Empfohlene Hauptansichten + +- `Setup` + - verbundene Geraete + - Akkustand + - Rollenbezeichnungen +- `Reference` + - Flaeche gegen Leitwerk + - Referenzwinkel +- `Surfaces` + - Querruder, Hoehe, Seite + - linke/rechte Symmetrie +- `CoG` + - Frontgewicht + - Heckgewicht + - aktueller CoG + - Soll-CoG + - Delta zum Ziel + +### Wichtige UX-Regeln + +- keine reinen Node-IDs als primaere Anzeige +- jede Messung braucht eine sprechende Rolle +- gefuehrte Schritte statt nur Rohdaten +- CoG-Ansicht und Ruder-Ansicht getrennt halten +- Kalibrieren klar unterscheiden: + - IMU `Zero` + - Scale `Tare` + - Scale `Weight calibration` + +## Datenmodell, das dafuer noetig ist + +Pro Modellprofil sollten spaeter gespeichert werden: + +- Modellname +- Flaechenreferenz +- support distance `L` +- offset der vorderen Auflage zur Nasenleiste +- Soll-CoG +- Rollenbelegung der Sensoren +- optionale Servotests oder Sollwerte fuer Ruderausschlaege + +Die verbindliche Profil- und Flaechentyp-Definition steht in: + +- [AIRCRAFT_PROFILE_MODEL.md](/Users/florianklaner/Github/AeroAlign/docs/AIRCRAFT_PROFILE_MODEL.md) + +## Mechanische Anforderungen an die CoG-Hardware + +Damit das gut zusammenspielt, muss die CoG-Hardware nicht nur elektrisch, sondern auch mechanisch sauber sein. + +Pflichtpunkte: + +- reproduzierbarer Abstand der beiden Auflagen +- rutschfeste Kontaktpunkte +- definierte Flugzeugposition in Längsrichtung +- steife Basis, damit Lasten nicht durchbiegen +- einfache Nullstellung ohne Modell + +Sinnvoll: + +- verstellbare Auflagehoehen +- verstellbarer LE-Anschlag +- modulare Auflagen fuer verschiedene Rumpf-/Flaechenformen + +## Empfohlener End-to-End-Werkstattablauf + +1. Sender auf neutral, Trims und Subtrims auf Ausgangszustand +2. Servoarme und Gestänge mechanisch sauber einstellen +3. IMU-Sensor auf Tragflaechen-Referenz setzen +4. IMU-Sensor auf Hoehenleitwerk oder Hoehenruder setzen +5. Referenz- und EWD-Werte einstellen +6. Querruder links und rechts gegen ihre jeweilige Referenz einstellen +7. Modell auf die CoG-Waage stellen +8. Akku oder Ballast verschieben, bis Soll-CoG erreicht ist +9. Danach Ruderneutralitaet und Referenzwerte kurz nachpruefen +10. Profil speichern + +## Konkrete Repo-Folgen + +### Naechste Firmware-Schritte + +1. `firmware/cog_slave` anlegen +2. HX711 lesen und mitteln +3. Tare und Skalierungsfaktoren speichern +4. Rollen-/Profilverwaltung im Master ergaenzen +5. neue UI-Ansichten `Reference`, `Surfaces`, `CoG` + +### Naechste Hardware-Schritte + +1. zwei Lastzellen mechanisch auf definierter Basis montieren +2. Support-Abstand als feste Konstruktionsgroesse festlegen +3. LE-Anschlag oder Referenzmarke vorsehen +4. IMU-Halter fuer feste Bezugsflaechen vereinheitlichen + +## Fazit + +Ja: dein Gedanke geht in die richtige Richtung. +Nein: Hoehenruder und Querruder sollten nicht direkt als gemeinsame Soll-Referenz behandelt werden. + +Das bessere System trennt: + +- Referenzflaechen +- Ruderneutralitaet +- Schwerpunkt + +Genau so sollte auch die Hardware und die UI aufgebaut werden. diff --git a/firmware/common/telemetry_protocol.h b/firmware/common/telemetry_protocol.h new file mode 100644 index 0000000..644aa34 --- /dev/null +++ b/firmware/common/telemetry_protocol.h @@ -0,0 +1,46 @@ +// SkyLogic AeroAlign - Shared telemetry protocol +// +// Common ESP-NOW payload format shared by Master and all remote devices. + +#ifndef TELEMETRY_PROTOCOL_H +#define TELEMETRY_PROTOCOL_H + +#include + +enum DeviceType : uint8_t { + DEVICE_TYPE_UNKNOWN = 0x00, + DEVICE_TYPE_IMU = 0x01, + DEVICE_TYPE_COG_SCALE = 0x02, + DEVICE_TYPE_HYBRID = 0x03, +}; + +inline const char* deviceTypeToString(DeviceType device_type) { + switch (device_type) { + case DEVICE_TYPE_IMU: + return "IMU"; + case DEVICE_TYPE_COG_SCALE: + return "CoG Scale"; + case DEVICE_TYPE_HYBRID: + return "Hybrid"; + default: + return "Unknown"; + } +} + +// ESP-NOW packet structure shared across all node types. +// IMU nodes use pitch/roll/yaw normally. +// CoG Scale nodes map: +// pitch -> front support weight (g) +// roll -> rear support weight (g) +// yaw -> computed CoG position (mm) +struct __attribute__((packed)) ESPNowPacket { + uint8_t node_id; + uint8_t device_type; + float pitch; + float roll; + float yaw; + uint8_t battery; + uint8_t checksum; +}; + +#endif // TELEMETRY_PROTOCOL_H diff --git a/firmware/master/data/index.html b/firmware/master/data/index.html index 5881770..2f0fb89 100644 --- a/firmware/master/data/index.html +++ b/firmware/master/data/index.html @@ -474,7 +474,7 @@ @@ -562,6 +562,12 @@ // Update status display function updateStatusDisplay() { const statusDiv = document.getElementById('status-bar'); + const batteryItem = systemStatus.master_battery_available ? ` +
+ Master Battery + ${systemStatus.master_battery_percent}% +
+ ` : ''; statusDiv.innerHTML = `
Uptime @@ -583,6 +589,7 @@ Free RAM ${systemStatus.free_heap_kb} KB
+ ${batteryItem}
Version ${systemStatus.firmware_version} @@ -600,37 +607,57 @@ } nodesDiv.innerHTML = nodes.map(node => { - const isWarning = node.battery_percent < 20; + const hasBattery = node.battery_available !== false; + const isWarning = hasBattery && node.battery_percent < 20; const cardClass = !node.is_connected ? 'disconnected' : (isWarning ? 'warning' : ''); const connClass = node.is_connected ? '' : 'disconnected'; + const isScaleNode = node.device_type === 2; + const batteryMetric = hasBattery ? ` +
+
Battery
+
${node.battery_percent}%
+
+ ` : ''; + const cardTitle = isScaleNode ? (node.label || 'CoG Scale ' + node.node_id) : (node.label || 'Sensor ' + node.node_id); + const mainValue = isScaleNode ? `${node.cog_position_mm.toFixed(1)} mm` : `${node.pitch.toFixed(2)}°`; + const mainLabel = isScaleNode ? 'CoG Position' : 'Pitch Angle'; + const metricColumns = hasBattery ? 3 : 2; + const metricOneLabel = isScaleNode ? 'Front' : 'Roll'; + const metricOneValue = isScaleNode ? `${node.front_weight_g.toFixed(0)} g` : `${node.roll.toFixed(2)}°`; + const metricTwoLabel = isScaleNode ? 'Rear' : 'Signal'; + const metricTwoValue = isScaleNode ? `${node.rear_weight_g.toFixed(0)} g` : `${node.rssi} dBm`; + const signalMetric = isScaleNode ? '' : ` +
+
Signal
+
${node.rssi} dBm
+
+ `; return `
-
${node.label || 'Sensor ' + node.node_id}
+
${cardTitle}
ID: ${node.node_id}
-
${node.pitch.toFixed(2)}°
-
Pitch Angle
+
${mainValue}
+
${mainLabel}
-
+
-
Roll
-
${node.roll.toFixed(2)}°
+
${metricOneLabel}
+
${metricOneValue}
+ ${batteryMetric}
-
Battery
-
${node.battery_percent}%
-
-
-
Signal
-
${node.rssi} dBm
+
${metricTwoLabel}
+
${metricTwoValue}
+ ${signalMetric}
`; @@ -642,7 +669,7 @@ const select1 = document.getElementById('node1-select'); const select2 = document.getElementById('node2-select'); - const options = nodes.filter(n => n.is_connected).map(n => + const options = nodes.filter(n => n.is_connected && n.device_type !== 2).map(n => `` ).join(''); diff --git a/firmware/master/platformio.ini b/firmware/master/platformio.ini index e0c2bc1..9d39d72 100644 --- a/firmware/master/platformio.ini +++ b/firmware/master/platformio.ini @@ -2,7 +2,7 @@ ; ; Master node hosts: ; - WiFi Access Point (SSID: SkyLogic-AeroAlign) -; - AsyncWebServer with React web UI +; - AsyncWebServer with browser-based web UI ; - ESP-NOW receiver for Slave node data ; - MPU6050/BNO055 IMU driver ; @@ -55,7 +55,7 @@ monitor_filters = esp32_exception_decoder ; Build flags build_flags = - -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_CDC_ON_BOOT=0 -D CORE_DEBUG_LEVEL=3 -D CONFIG_ASYNC_TCP_RUNNING_CORE=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 diff --git a/firmware/master/src/config.h b/firmware/master/src/config.h index c370a42..60f16b1 100644 --- a/firmware/master/src/config.h +++ b/firmware/master/src/config.h @@ -33,22 +33,36 @@ #define WIFI_AP_SUBNET IPAddress(255, 255, 255, 0) // ======================================== -// GPIO Pin Definitions (ESP32-C3) +// GPIO Pin Definitions // ======================================== -// I2C pins for IMU (MPU6050/BNO055) +#if defined(CONFIG_IDF_TARGET_ESP32S3) + +// ESP32-S3 defaults +#define IMU_I2C_SDA 4 // GPIO4 (SDA) +#define IMU_I2C_SCL 5 // GPIO5 (SCL) +#define IMU_I2C_FREQ 100000 // 100kHz for better signal margin on jumper wires +#define BATTERY_ADC_PIN 1 // GPIO1 (ADC1), avoids GPIO0 boot strap +#define BATTERY_MONITOR_ENABLED 0 // Set to 1 only if a divider is actually wired +#define STATUS_LED_PIN -1 // Board-dependent on S3 modules, disabled by default +#define HARDWARE_MODEL "ESP32-S3" + +#else + #define IMU_I2C_SDA 4 // GPIO4 (SDA) #define IMU_I2C_SCL 5 // GPIO5 (SCL) #define IMU_I2C_FREQ 400000 // 400kHz (fast mode) +#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0) +#define BATTERY_MONITOR_ENABLED 1 +#define STATUS_LED_PIN 10 // GPIO10 (built-in LED on some boards) +#define HARDWARE_MODEL "ESP32-C3" + +#endif // Battery monitoring (ADC) -// Voltage divider: LiPo+ -> 10kΩ -> GPIO0 -> 10kΩ -> GND -#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0) +// Voltage divider: LiPo+ -> 10kΩ -> BATTERY_ADC_PIN -> 10kΩ -> GND #define BATTERY_VOLTAGE_DIVIDER 2.0 // 10kΩ + 10kΩ = 2:1 ratio -// Status LED (optional) -#define STATUS_LED_PIN 10 // GPIO10 (built-in LED on some boards) - // Power control (optional, for deep sleep) #define POWER_ENABLE_PIN -1 // Not used (always on) @@ -67,8 +81,8 @@ // If no packet received from Slave for this duration, mark as disconnected #define ESPNOW_TIMEOUT_MS 1000 -// Expected packet size (15 bytes: node_id + pitch + roll + yaw + battery + checksum) -#define ESPNOW_PACKET_SIZE 15 +// Expected packet size (16 bytes: node_id + device_type + pitch + roll + yaw + battery + checksum) +#define ESPNOW_PACKET_SIZE 16 // ======================================== // IMU Configuration @@ -132,9 +146,6 @@ // Firmware version #define FIRMWARE_VERSION "1.0.0" -// Hardware model -#define HARDWARE_MODEL "ESP32-C3" - // System name #define SYSTEM_NAME "SkyLogic AeroAlign" diff --git a/firmware/master/src/espnow_master.cpp b/firmware/master/src/espnow_master.cpp index c9b4384..1e37504 100644 --- a/firmware/master/src/espnow_master.cpp +++ b/firmware/master/src/espnow_master.cpp @@ -161,9 +161,18 @@ void ESPNowMaster::handleReceivedPacket(const uint8_t *mac, const uint8_t *data, if (node) { // Update node data + node->device_type = static_cast(packet.device_type); node->pitch = packet.pitch; node->roll = packet.roll; node->yaw = packet.yaw; + node->front_weight_g = 0.0f; + node->rear_weight_g = 0.0f; + node->cog_position_mm = 0.0f; + if (node->device_type == DEVICE_TYPE_COG_SCALE || node->device_type == DEVICE_TYPE_HYBRID) { + node->front_weight_g = packet.pitch; + node->rear_weight_g = packet.roll; + node->cog_position_mm = packet.yaw; + } node->battery_percent = packet.battery; node->is_connected = true; node->last_update_ms = millis(); @@ -176,8 +185,9 @@ void ESPNowMaster::handleReceivedPacket(const uint8_t *mac, const uint8_t *data, total_packets_received++; #ifdef DEBUG_ESPNOW_PACKETS - Serial.printf("[ESP-NOW] RX from 0x%02X: pitch=%.2f° roll=%.2f° battery=%d%% RSSI=%ddBm\n", - packet.node_id, packet.pitch, packet.roll, packet.battery, node->rssi); + Serial.printf("[ESP-NOW] RX from 0x%02X (%s): a=%.2f b=%.2f c=%.2f battery=%d%% RSSI=%ddBm\n", + packet.node_id, deviceTypeToString(node->device_type), + packet.pitch, packet.roll, packet.yaw, packet.battery, node->rssi); #endif } } @@ -206,11 +216,15 @@ void ESPNowMaster::registerNode(uint8_t node_id, const uint8_t *mac) { if (nodes[i].node_id == 0) { // Initialize new node nodes[i].node_id = node_id; + nodes[i].device_type = DEVICE_TYPE_UNKNOWN; memcpy(nodes[i].mac_address, mac, 6); snprintf(nodes[i].label, sizeof(nodes[i].label), "Sensor %d", node_id - 1); nodes[i].pitch = 0.0; nodes[i].roll = 0.0; nodes[i].yaw = 0.0; + nodes[i].front_weight_g = 0.0; + nodes[i].rear_weight_g = 0.0; + nodes[i].cog_position_mm = 0.0; nodes[i].pitch_offset = 0.0; nodes[i].roll_offset = 0.0; nodes[i].yaw_offset = 0.0; diff --git a/firmware/master/src/espnow_master.h b/firmware/master/src/espnow_master.h index 9dc1882..0904e9a 100644 --- a/firmware/master/src/espnow_master.h +++ b/firmware/master/src/espnow_master.h @@ -12,26 +12,20 @@ #include #include #include - -// ESP-NOW packet structure (must match Slave's packet format) -// Total: 15 bytes -struct __attribute__((packed)) ESPNowPacket { - uint8_t node_id; // Sender node ID (0x02-0x09) - float pitch; // Pitch angle (degrees) - float roll; // Roll angle (degrees) - float yaw; // Yaw angle (degrees, unused) - uint8_t battery; // Battery percentage (0-100) - uint8_t checksum; // XOR checksum of bytes 0-13 -}; +#include "../../common/telemetry_protocol.h" // Sensor node data structure struct SensorNode { uint8_t node_id; + DeviceType device_type; uint8_t mac_address[6]; char label[32]; float pitch; float roll; float yaw; + float front_weight_g; + float rear_weight_g; + float cog_position_mm; float pitch_offset; float roll_offset; float yaw_offset; diff --git a/firmware/master/src/imu_driver.cpp b/firmware/master/src/imu_driver.cpp index 1bcae7b..f9caca7 100644 --- a/firmware/master/src/imu_driver.cpp +++ b/firmware/master/src/imu_driver.cpp @@ -7,10 +7,24 @@ #include "config.h" #include +namespace { +constexpr uint8_t MPU6050_REG_SMPLRT_DIV = 0x19; +constexpr uint8_t MPU6050_REG_CONFIG = 0x1A; +constexpr uint8_t MPU6050_REG_GYRO_CONFIG = 0x1B; +constexpr uint8_t MPU6050_REG_ACCEL_CONFIG = 0x1C; +constexpr uint8_t MPU6050_REG_PWR_MGMT_1 = 0x6B; +constexpr uint8_t MPU6050_REG_WHO_AM_I = 0x75; +constexpr uint8_t MPU6050_REG_ACCEL_XOUT_H = 0x3B; +constexpr uint8_t MPU6050_DEVICE_ID = 0x68; +constexpr float MPU6050_ACCEL_LSB_PER_G = 16384.0f; +constexpr float MPU6050_GYRO_LSB_PER_DEG_S = 131.0f; +constexpr float GRAVITY_M_S2 = 9.80665f; +} + IMU_Driver::IMU_Driver() : pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0), filtered_pitch(0.0), filtered_roll(0.0), last_update_us(0), - alpha(COMPLEMENTARY_FILTER_ALPHA), connected(false) { + alpha(COMPLEMENTARY_FILTER_ALPHA), connected(false), wire(&Wire) { // Initialize data structure memset(&data, 0, sizeof(IMU_Data)); } @@ -21,11 +35,27 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { #endif // Initialize I2C - Wire.begin(sda_pin, scl_pin, i2c_freq); + wire = &Wire; + wire->begin(sda_pin, scl_pin, i2c_freq); + wire->setTimeOut(50); + delay(20); - // Try to initialize MPU6050 - if (!mpu.begin(IMU_I2C_ADDRESS, &Wire)) { - last_error = "MPU6050 not found at 0x68. Check wiring!"; + // Probe device first so wiring / bus-speed failures are visible in logs. + wire->beginTransmission(IMU_I2C_ADDRESS); + uint8_t i2c_error = wire->endTransmission(); + if (i2c_error != 0) { + last_error = "I2C probe failed for MPU6050 at 0x68"; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] ERROR: %s (Wire err=%u, SDA=%u, SCL=%u, %luHz)\n", + last_error.c_str(), i2c_error, sda_pin, scl_pin, (unsigned long)i2c_freq); + #endif + connected = false; + return false; + } + + uint8_t who_am_i = 0; + if (!readRegister(MPU6050_REG_WHO_AM_I, who_am_i)) { + last_error = "MPU6050 WHO_AM_I read failed"; #ifdef DEBUG_SERIAL_ENABLED Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); #endif @@ -33,19 +63,33 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { return false; } + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] WHO_AM_I = 0x%02X\n", who_am_i); + #endif + + if (who_am_i != MPU6050_DEVICE_ID) { + #ifdef DEBUG_SERIAL_ENABLED + Serial.println("[IMU] WARNING: Unexpected WHO_AM_I, continuing with MPU6050-compatible init"); + #endif + } + #ifdef DEBUG_SERIAL_ENABLED Serial.printf("[IMU] MPU6050 initialized at 0x%02X\n", IMU_I2C_ADDRESS); #endif - // Configure MPU6050 settings - // Accelerometer range: ±2g (sufficient for static measurement) - mpu.setAccelerometerRange(MPU6050_RANGE_2_G); - - // Gyroscope range: ±250 deg/s (low range for better resolution) - mpu.setGyroRange(MPU6050_RANGE_250_DEG); - - // Filter bandwidth: 21Hz (balance noise reduction and responsiveness) - mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); + // Wake sensor and configure ranges / filtering. + if (!writeRegister(MPU6050_REG_PWR_MGMT_1, 0x01) || + !writeRegister(MPU6050_REG_SMPLRT_DIV, 0x07) || + !writeRegister(MPU6050_REG_CONFIG, 0x04) || + !writeRegister(MPU6050_REG_GYRO_CONFIG, 0x00) || + !writeRegister(MPU6050_REG_ACCEL_CONFIG, 0x00)) { + last_error = "MPU6050 register configuration failed"; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); + #endif + connected = false; + return false; + } // Wait for IMU to stabilize delay(100); @@ -60,11 +104,16 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { int valid_samples = 0; for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) { - sensors_event_t accel, gyro, temp; - if (mpu.getEvent(&accel, &gyro, &temp)) { + uint8_t buffer[14]; + if (readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) { + int16_t raw_ax = (buffer[0] << 8) | buffer[1]; + int16_t raw_ay = (buffer[2] << 8) | buffer[3]; + int16_t raw_az = (buffer[4] << 8) | buffer[5]; + float ax = (raw_ax / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float ay = (raw_ay / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float az = (raw_az / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; float pitch_raw, roll_raw; - calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z, - pitch_raw, roll_raw); + calculateAccelAngles(ax, ay, az, pitch_raw, roll_raw); pitch_sum += pitch_raw; roll_sum += roll_raw; valid_samples++; @@ -72,15 +121,22 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { delay(10); // 100Hz sampling } - if (valid_samples > 0) { - pitch_offset = pitch_sum / valid_samples; - roll_offset = roll_sum / valid_samples; + if (valid_samples == 0) { + last_error = "MPU6050 data read failed during calibration"; #ifdef DEBUG_SERIAL_ENABLED - Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n", - pitch_offset, roll_offset); + Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); #endif + connected = false; + return false; } + pitch_offset = pitch_sum / valid_samples; + roll_offset = roll_sum / valid_samples; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n", + pitch_offset, roll_offset); + #endif + connected = true; last_update_us = micros(); return true; @@ -91,15 +147,29 @@ bool IMU_Driver::update() { return false; } - // Get sensor events - sensors_event_t accel, gyro, temp; - if (!mpu.getEvent(&accel, &gyro, &temp)) { + uint8_t buffer[14]; + if (!readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) { #ifdef DEBUG_SERIAL_ENABLED Serial.println("[IMU] ERROR: Failed to read sensor data"); #endif return false; } + int16_t raw_ax = (buffer[0] << 8) | buffer[1]; + int16_t raw_ay = (buffer[2] << 8) | buffer[3]; + int16_t raw_az = (buffer[4] << 8) | buffer[5]; + int16_t raw_temp = (buffer[6] << 8) | buffer[7]; + int16_t raw_gx = (buffer[8] << 8) | buffer[9]; + int16_t raw_gy = (buffer[10] << 8) | buffer[11]; + int16_t raw_gz = (buffer[12] << 8) | buffer[13]; + + float ax = (raw_ax / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float ay = (raw_ay / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float az = (raw_az / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float gx_deg_s = raw_gx / MPU6050_GYRO_LSB_PER_DEG_S; + float gy_deg_s = raw_gy / MPU6050_GYRO_LSB_PER_DEG_S; + float gz_deg_s = raw_gz / MPU6050_GYRO_LSB_PER_DEG_S; + // Calculate time delta (dt) in seconds uint32_t now_us = micros(); float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds @@ -111,22 +181,21 @@ bool IMU_Driver::update() { } // Store raw sensor data - data.accel_x = accel.acceleration.x; - data.accel_y = accel.acceleration.y; - data.accel_z = accel.acceleration.z; - data.gyro_x = gyro.gyro.x; - data.gyro_y = gyro.gyro.y; - data.gyro_z = gyro.gyro.z; - data.temperature = temp.temperature; + data.accel_x = ax; + data.accel_y = ay; + data.accel_z = az; + data.gyro_x = gx_deg_s * M_PI / 180.0; + data.gyro_y = gy_deg_s * M_PI / 180.0; + data.gyro_z = gz_deg_s * M_PI / 180.0; + data.temperature = (raw_temp / 340.0f) + 36.53f; data.timestamp = millis(); // Calculate pitch and roll from accelerometer (gravity vector) float accel_pitch, accel_roll; - calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z, - accel_pitch, accel_roll); + calculateAccelAngles(ax, ay, az, accel_pitch, accel_roll); // Apply complementary filter (fuse gyro + accel) - applyComplementaryFilter(accel_pitch, accel_roll, gyro.gyro.x, gyro.gyro.y, dt); + applyComplementaryFilter(accel_pitch, accel_roll, data.gyro_x, data.gyro_y, dt); // Apply calibration offsets data.pitch = constrainAngle(filtered_pitch - pitch_offset); @@ -253,3 +322,35 @@ float IMU_Driver::constrainAngle(float angle) { while (angle < -180.0) angle += 360.0; return angle; } + +bool IMU_Driver::writeRegister(uint8_t reg, uint8_t value) { + wire->beginTransmission(IMU_I2C_ADDRESS); + wire->write(reg); + wire->write(value); + return wire->endTransmission() == 0; +} + +bool IMU_Driver::readRegister(uint8_t reg, uint8_t &value) { + if (!readRegisters(reg, &value, 1)) { + return false; + } + return true; +} + +bool IMU_Driver::readRegisters(uint8_t reg, uint8_t *buffer, size_t len) { + wire->beginTransmission(IMU_I2C_ADDRESS); + wire->write(reg); + if (wire->endTransmission(false) != 0) { + return false; + } + + size_t received = wire->requestFrom((uint8_t)IMU_I2C_ADDRESS, (uint8_t)len, (uint8_t)true); + if (received != len) { + return false; + } + + for (size_t i = 0; i < len; i++) { + buffer[i] = wire->read(); + } + return true; +} diff --git a/firmware/master/src/imu_driver.h b/firmware/master/src/imu_driver.h index f87802c..5865374 100644 --- a/firmware/master/src/imu_driver.h +++ b/firmware/master/src/imu_driver.h @@ -8,8 +8,6 @@ #include #include -#include -#include // IMU data structure struct IMU_Data { @@ -60,8 +58,8 @@ public: String getLastError() const; private: - // Adafruit MPU6050 driver instance - Adafruit_MPU6050 mpu; + // Active I2C bus for the IMU + TwoWire *wire; // Current IMU data IMU_Data data; @@ -93,6 +91,11 @@ private: // Constrain angle to -180 to +180 range float constrainAngle(float angle); + + // Low-level MPU6050 register access + bool writeRegister(uint8_t reg, uint8_t value); + bool readRegister(uint8_t reg, uint8_t &value); + bool readRegisters(uint8_t reg, uint8_t *buffer, size_t len); }; #endif // IMU_DRIVER_H diff --git a/firmware/master/src/main.cpp b/firmware/master/src/main.cpp index 84d04f8..5aea312 100644 --- a/firmware/master/src/main.cpp +++ b/firmware/master/src/main.cpp @@ -23,6 +23,8 @@ IMU_Driver imu; ESPNowMaster espnow; CalibrationManager calibration; WebServerManager* webserver = nullptr; +uint8_t master_battery_percent = 0; +float master_battery_voltage = 0.0f; // ======================================== // WiFi AP Setup @@ -75,7 +77,12 @@ bool setupWiFiAP() { // Battery Monitoring (Master) // ======================================== -uint8_t readBatteryPercent() { +bool readBatteryState(float &battery_voltage, uint8_t &battery_percent) { + #if !BATTERY_MONITOR_ENABLED || BATTERY_ADC_PIN < 0 + battery_voltage = 0.0f; + battery_percent = 0; + return false; + #else // Read battery voltage via ADC int adc_value = analogRead(BATTERY_ADC_PIN); @@ -83,7 +90,7 @@ uint8_t readBatteryPercent() { float voltage_at_adc = (adc_value / 4095.0) * 3.3; // Multiply by voltage divider ratio (2:1) - float battery_voltage = voltage_at_adc * BATTERY_VOLTAGE_DIVIDER; + battery_voltage = voltage_at_adc * BATTERY_VOLTAGE_DIVIDER; // Convert to percentage (LiPo: 3.0V = 0%, 4.2V = 100%) float percent = ((battery_voltage - BATTERY_VOLTAGE_MIN) / @@ -93,7 +100,9 @@ uint8_t readBatteryPercent() { if (percent < 0.0) percent = 0.0; if (percent > 100.0) percent = 100.0; - return (uint8_t)percent; + battery_percent = (uint8_t)percent; + return true; + #endif } // ======================================== @@ -119,8 +128,10 @@ void setup() { digitalWrite(STATUS_LED_PIN, LOW); #endif - // Initialize battery ADC + // Initialize battery ADC only if a voltage divider is present. + #if BATTERY_MONITOR_ENABLED && BATTERY_ADC_PIN >= 0 pinMode(BATTERY_ADC_PIN, INPUT); + #endif // ======================================== // Step 1: Setup WiFi AP @@ -237,7 +248,7 @@ void setup() { Serial.println("[Setup] Initializing Web Server..."); #endif - webserver = new WebServerManager(&espnow, &calibration, &imu); + webserver = new WebServerManager(&espnow, &calibration, &imu, &master_battery_percent, &master_battery_voltage); if (!webserver->begin()) { #ifdef DEBUG_SERIAL_ENABLED @@ -277,8 +288,6 @@ void loop() { static uint32_t last_espnow_update_ms = 0; static uint32_t last_battery_read_ms = 0; static uint32_t last_stats_print_ms = 0; - static uint8_t battery_percent = 100; - uint32_t now = millis(); // ======================================== @@ -312,13 +321,10 @@ void loop() { if (now - last_battery_read_ms >= 1000) { // 1000ms = 1Hz last_battery_read_ms = now; - // Read battery percentage - battery_percent = readBatteryPercent(); - - // Check for low battery - if (battery_percent <= BATTERY_WARNING_PERCENT) { + if (readBatteryState(master_battery_voltage, master_battery_percent) && + master_battery_percent <= BATTERY_WARNING_PERCENT) { #ifdef DEBUG_SERIAL_ENABLED - Serial.printf("[Battery] WARNING: Low battery (%d%%)\n", battery_percent); + Serial.printf("[Battery] WARNING: Low battery (%d%%)\n", master_battery_percent); #endif // Flash LED to warn user @@ -358,7 +364,11 @@ void loop() { Serial.println("Master Node Status Report"); Serial.println("========================================"); Serial.printf("Uptime: %lu seconds\n", now / 1000); - Serial.printf("Battery: %d%%\n", battery_percent); + #if BATTERY_MONITOR_ENABLED + Serial.printf("Battery: %d%% (%.2fV)\n", master_battery_percent, master_battery_voltage); + #else + Serial.println("Battery: not connected"); + #endif Serial.println("----------------------------------------"); Serial.printf("WiFi: %d clients connected\n", wifi_clients); Serial.printf("WiFi: http://%s\n", WIFI_AP_IP.toString().c_str()); diff --git a/firmware/master/src/web_server.cpp b/firmware/master/src/web_server.cpp index afa2d81..3eaa269 100644 --- a/firmware/master/src/web_server.cpp +++ b/firmware/master/src/web_server.cpp @@ -5,10 +5,14 @@ #include "web_server.h" #include "config.h" #include +#include #include -WebServerManager::WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu) - : espnow(espnow), calibration(calibration), master_imu(imu), server(nullptr), pair_count(0) { +WebServerManager::WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu, + const uint8_t* master_battery_percent, const float* master_battery_voltage) + : espnow(espnow), calibration(calibration), master_imu(imu), server(nullptr), + master_battery_percent(master_battery_percent), master_battery_voltage(master_battery_voltage), + pair_count(0) { } bool WebServerManager::begin() { @@ -16,6 +20,14 @@ bool WebServerManager::begin() { Serial.println("[WebServer] Initializing HTTP server..."); #endif + if (!SPIFFS.begin(true)) { + last_error = "SPIFFS mount failed"; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[WebServer] ERROR: %s\n", last_error.c_str()); + #endif + return false; + } + // Create server instance server = new AsyncWebServer(HTTP_SERVER_PORT); @@ -45,11 +57,13 @@ bool WebServerManager::begin() { this->handleGetStatus(request); }); - // GET / - Serve web UI (placeholder for now) + // GET / - Serve web UI server->on("/", HTTP_GET, [this](AsyncWebServerRequest *request) { this->handleRoot(request); }); + server->serveStatic("/", SPIFFS, "/").setDefaultFile("index.html"); + // 404 handler server->onNotFound([this](AsyncWebServerRequest *request) { this->handleNotFound(request); @@ -93,15 +107,19 @@ void WebServerManager::handleGetNodes(AsyncWebServerRequest *request) { JsonObject master_node = nodes_array.createNestedObject(); master_node["node_id"] = 1; master_node["label"] = "Master"; + master_node["device_type"] = DEVICE_TYPE_IMU; + master_node["device_type_label"] = deviceTypeToString(DEVICE_TYPE_IMU); float pitch, roll, yaw; master_imu->getAngles(pitch, roll, yaw); master_node["pitch"] = pitch; master_node["roll"] = roll; master_node["yaw"] = yaw; - - master_node["battery_percent"] = 85; // TODO: Implement Master battery monitoring - master_node["battery_voltage"] = 3.9; + master_node["battery_available"] = BATTERY_MONITOR_ENABLED; + if (BATTERY_MONITOR_ENABLED && master_battery_percent && master_battery_voltage) { + master_node["battery_percent"] = *master_battery_percent; + master_node["battery_voltage"] = *master_battery_voltage; + } master_node["rssi"] = 0; master_node["is_connected"] = true; master_node["last_update_ms"] = millis(); @@ -116,9 +134,15 @@ void WebServerManager::handleGetNodes(AsyncWebServerRequest *request) { JsonObject node_obj = nodes_array.createNestedObject(); node_obj["node_id"] = nodes[i].node_id; node_obj["label"] = nodes[i].label; + node_obj["device_type"] = nodes[i].device_type; + node_obj["device_type_label"] = deviceTypeToString(nodes[i].device_type); node_obj["pitch"] = nodes[i].pitch; node_obj["roll"] = nodes[i].roll; node_obj["yaw"] = nodes[i].yaw; + node_obj["front_weight_g"] = nodes[i].front_weight_g; + node_obj["rear_weight_g"] = nodes[i].rear_weight_g; + node_obj["cog_position_mm"] = nodes[i].cog_position_mm; + node_obj["battery_available"] = true; node_obj["battery_percent"] = nodes[i].battery_percent; node_obj["battery_voltage"] = nodes[i].battery_voltage; node_obj["rssi"] = nodes[i].rssi; @@ -158,7 +182,7 @@ void WebServerManager::handleGetDifferential(AsyncWebServerRequest *request) { // (we'll handle this below) } else { node1 = espnow->getNode(node1_id); - if (!node1 || !node1->is_connected) { + if (!node1 || !node1->is_connected || node1->device_type == DEVICE_TYPE_COG_SCALE) { request->send(404, "application/json", "{\"error\":\"Node 1 not found or disconnected\"}"); return; } @@ -169,7 +193,7 @@ void WebServerManager::handleGetDifferential(AsyncWebServerRequest *request) { // Use Master IMU directly } else { node2 = espnow->getNode(node2_id); - if (!node2 || !node2->is_connected) { + if (!node2 || !node2->is_connected || node2->device_type == DEVICE_TYPE_COG_SCALE) { request->send(404, "application/json", "{\"error\":\"Node 2 not found or disconnected\"}"); return; } @@ -323,8 +347,11 @@ void WebServerManager::handleGetStatus(AsyncWebServerRequest *request) { // Build JSON response DynamicJsonDocument doc(1024); - doc["master_battery_percent"] = 75; // TODO: Implement Master battery monitoring - doc["master_battery_voltage"] = 3.85; + doc["master_battery_available"] = BATTERY_MONITOR_ENABLED; + if (BATTERY_MONITOR_ENABLED && master_battery_percent && master_battery_voltage) { + doc["master_battery_percent"] = *master_battery_percent; + doc["master_battery_voltage"] = *master_battery_voltage; + } doc["wifi_clients_connected"] = WiFi.softAPgetStationNum(); doc["wifi_channel"] = WIFI_CHANNEL; doc["uptime_seconds"] = millis() / 1000; @@ -344,31 +371,7 @@ void WebServerManager::handleRoot(AsyncWebServerRequest *request) { #ifdef DEBUG_HTTP_REQUESTS Serial.println("[WebServer] GET /"); #endif - - // Serve web UI (placeholder - will be replaced with actual index.html) - String html = R"( - - - - SkyLogic AeroAlign - - - - -

SkyLogic AeroAlign

-

Web UI placeholder - Full React interface will be implemented in next task

-

API Endpoints:

- - - -)"; - - request->send(200, "text/html", html); + request->send(SPIFFS, "/index.html", "text/html"); } void WebServerManager::handleNotFound(AsyncWebServerRequest *request) { diff --git a/firmware/master/src/web_server.h b/firmware/master/src/web_server.h index 87beda2..4cda190 100644 --- a/firmware/master/src/web_server.h +++ b/firmware/master/src/web_server.h @@ -20,7 +20,8 @@ class WebServerManager { public: // Constructor - WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu); + WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu, + const uint8_t* master_battery_percent, const float* master_battery_voltage); // Initialize web server bool begin(); @@ -36,6 +37,8 @@ private: ESPNowMaster* espnow; CalibrationManager* calibration; IMU_Driver* master_imu; + const uint8_t* master_battery_percent; + const float* master_battery_voltage; // Last error message String last_error; diff --git a/firmware/slave/platformio.ini b/firmware/slave/platformio.ini index 36c0ea0..a1f34d6 100644 --- a/firmware/slave/platformio.ini +++ b/firmware/slave/platformio.ini @@ -27,8 +27,6 @@ build_flags = ; Library dependencies lib_deps = Wire ; I2C for IMU - adafruit/Adafruit MPU6050@^2.2.4 ; MPU6050 IMU driver - adafruit/Adafruit BNO055@^1.6.0 ; BNO055 IMU driver (optional) ; Partition scheme (minimal, no web server) board_build.partitions = min_spiffs.csv @@ -59,8 +57,6 @@ build_flags = ; Library dependencies (same as C3) lib_deps = Wire - adafruit/Adafruit MPU6050@^2.2.4 - adafruit/Adafruit BNO055@^1.6.0 ; Partition scheme board_build.partitions = min_spiffs.csv @@ -77,47 +73,69 @@ upload_speed = 921600 [env:slave1] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x02 [env:slave2] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x03 [env:slave3] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x04 [env:slave4] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x05 [env:slave5] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x06 [env:slave6] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x07 [env:slave7] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x08 [env:slave8] extends = env:esp32-c3 build_flags = - ${env:esp32-c3.build_flags} + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 -D NODE_ID=0x09 + +[env:slave1-s3] +extends = env:esp32-s3 +build_flags = + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 + -D NODE_ID=0x02 + +[env:slave2-s3] +extends = env:esp32-s3 +build_flags = + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CORE_DEBUG_LEVEL=3 + -D NODE_ID=0x03 diff --git a/firmware/slave/src/config.cpp b/firmware/slave/src/config.cpp new file mode 100644 index 0000000..259f3c3 --- /dev/null +++ b/firmware/slave/src/config.cpp @@ -0,0 +1,3 @@ +#include + +uint8_t master_mac[6] = {0x24, 0x58, 0x7C, 0xDE, 0x81, 0x90}; diff --git a/firmware/slave/src/config.h b/firmware/slave/src/config.h index bf8b339..c4f1679 100644 --- a/firmware/slave/src/config.h +++ b/firmware/slave/src/config.h @@ -17,15 +17,13 @@ // ======================================== // Master node MAC address -// **IMPORTANT**: Replace this with your Master's actual MAC address +// **IMPORTANT**: Set this to your Master's actual MAC address in config.cpp // To find Master MAC: // 1. Flash Master firmware // 2. Connect Master to USB, open serial monitor (115200 baud) // 3. Master prints MAC at boot: "Master MAC: 24:6F:28:12:34:56" -// 4. Copy MAC into this array, reflash Slave -// -// Format: {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF} -uint8_t master_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // REPLACE WITH ACTUAL MAC +// 4. Copy MAC into firmware/slave/src/config.cpp, then reflash Slave +extern uint8_t master_mac[6]; // Slave node ID (unique identifier for this Slave) // Default: 0x02 (first Slave) @@ -43,26 +41,40 @@ uint8_t master_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // REPLACE WITH A // 100ms = 10Hz update rate (balances latency and power consumption) #define ESPNOW_SEND_INTERVAL_MS 100 -// ESP-NOW packet size (15 bytes: node_id + pitch + roll + yaw + battery + checksum) -#define ESPNOW_PACKET_SIZE 15 +// ESP-NOW packet size (16 bytes: node_id + device_type + pitch + roll + yaw + battery + checksum) +#define ESPNOW_PACKET_SIZE 16 // ======================================== -// GPIO Pin Definitions (ESP32-C3) +// GPIO Pin Definitions // ======================================== -// I2C pins for IMU (MPU6050/BNO055) +#if defined(CONFIG_IDF_TARGET_ESP32S3) + +// ESP32-S3 defaults +#define IMU_I2C_SDA 4 // GPIO4 (SDA) +#define IMU_I2C_SCL 5 // GPIO5 (SCL) +#define IMU_I2C_FREQ 100000 // 100kHz for better signal margin on jumper wires +#define BATTERY_ADC_PIN 1 // GPIO1 (ADC1), avoids GPIO0 boot strap +#define BATTERY_MONITOR_ENABLED 1 +#define STATUS_LED_PIN -1 // Board-dependent on S3 modules, disabled by default +#define HARDWARE_MODEL "ESP32-S3" + +#else + #define IMU_I2C_SDA 4 // GPIO4 (SDA) #define IMU_I2C_SCL 5 // GPIO5 (SCL) #define IMU_I2C_FREQ 400000 // 400kHz (fast mode) +#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0) +#define BATTERY_MONITOR_ENABLED 1 +#define STATUS_LED_PIN 10 // GPIO10 (built-in LED on some boards) +#define HARDWARE_MODEL "ESP32-C3" + +#endif // Battery monitoring (ADC) -// Voltage divider: LiPo+ -> 10kΩ -> GPIO0 -> 10kΩ -> GND -#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0) +// Voltage divider: LiPo+ -> 10kΩ -> BATTERY_ADC_PIN -> 10kΩ -> GND #define BATTERY_VOLTAGE_DIVIDER 2.0 // 10kΩ + 10kΩ = 2:1 ratio -// Status LED (optional) -#define STATUS_LED_PIN 10 // GPIO10 (built-in LED on some boards) - // Power control (optional, for deep sleep) #define POWER_ENABLE_PIN -1 // Not used (always on) @@ -101,9 +113,6 @@ uint8_t master_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // REPLACE WITH A // Firmware version #define FIRMWARE_VERSION "1.0.0" -// Hardware model -#define HARDWARE_MODEL "ESP32-C3" - // System name #define SYSTEM_NAME "SkyLogic AeroAlign Slave" diff --git a/firmware/slave/src/espnow_slave.cpp b/firmware/slave/src/espnow_slave.cpp index 36205b0..5dd9aeb 100644 --- a/firmware/slave/src/espnow_slave.cpp +++ b/firmware/slave/src/espnow_slave.cpp @@ -77,31 +77,45 @@ bool ESPNowSlave::begin() { } bool ESPNowSlave::sendData(float pitch, float roll, float yaw, uint8_t battery) { + ESPNowPacket packet; + packet.node_id = node_id; + packet.device_type = DEVICE_TYPE_IMU; + packet.pitch = pitch; + packet.roll = roll; + packet.yaw = yaw; + packet.battery = battery; + packet.checksum = calculateChecksum((uint8_t*)&packet, sizeof(packet) - 1); + return sendPacket(packet); +} + +bool ESPNowSlave::sendScaleData(float front_weight_g, float rear_weight_g, float cog_position_mm, uint8_t battery) { + ESPNowPacket packet; + packet.node_id = node_id; + packet.device_type = DEVICE_TYPE_COG_SCALE; + packet.pitch = front_weight_g; + packet.roll = rear_weight_g; + packet.yaw = cog_position_mm; + packet.battery = battery; + packet.checksum = calculateChecksum((uint8_t*)&packet, sizeof(packet) - 1); + return sendPacket(packet); +} + +bool ESPNowSlave::sendPacket(const ESPNowPacket &packet) { if (!paired) { last_error = "Not paired with Master"; return false; } - // Build packet - ESPNowPacket packet; - packet.node_id = node_id; - packet.pitch = pitch; - packet.roll = roll; - packet.yaw = yaw; - packet.battery = battery; - - // Calculate checksum (XOR of bytes 0-13) - packet.checksum = calculateChecksum((uint8_t*)&packet, sizeof(packet) - 1); - // Send packet - esp_err_t result = esp_now_send(master_mac, (uint8_t*)&packet, sizeof(packet)); + esp_err_t result = esp_now_send(master_mac, (const uint8_t*)&packet, sizeof(packet)); if (result == ESP_OK) { total_packets_sent++; #ifdef DEBUG_ESPNOW_PACKETS - Serial.printf("[ESP-NOW] TX: pitch=%.2f° roll=%.2f° battery=%d%% checksum=0x%02X\n", - pitch, roll, battery, packet.checksum); + Serial.printf("[ESP-NOW] TX (%s): a=%.2f b=%.2f c=%.2f battery=%d%% checksum=0x%02X\n", + deviceTypeToString(static_cast(packet.device_type)), + packet.pitch, packet.roll, packet.yaw, packet.battery, packet.checksum); #endif return true; diff --git a/firmware/slave/src/espnow_slave.h b/firmware/slave/src/espnow_slave.h index 93a5488..425b1dc 100644 --- a/firmware/slave/src/espnow_slave.h +++ b/firmware/slave/src/espnow_slave.h @@ -11,17 +11,7 @@ #include #include #include - -// ESP-NOW packet structure (must match Master's packet format) -// Total: 15 bytes -struct __attribute__((packed)) ESPNowPacket { - uint8_t node_id; // Sender node ID (0x02-0x09) - float pitch; // Pitch angle (degrees) - float roll; // Roll angle (degrees) - float yaw; // Yaw angle (degrees, unused) - uint8_t battery; // Battery percentage (0-100) - uint8_t checksum; // XOR checksum of bytes 0-13 -}; +#include "../../common/telemetry_protocol.h" // ESP-NOW Slave Manager class class ESPNowSlave { @@ -35,6 +25,9 @@ public: // Send sensor data packet to Master bool sendData(float pitch, float roll, float yaw, uint8_t battery); + // Send CoG scale packet to Master + bool sendScaleData(float front_weight_g, float rear_weight_g, float cog_position_mm, uint8_t battery); + // Get transmission statistics void getStatistics(uint32_t &total_sent, uint32_t &total_failed, float &success_rate); @@ -70,6 +63,8 @@ private: // Calculate XOR checksum uint8_t calculateChecksum(const uint8_t *data, int len); + + bool sendPacket(const ESPNowPacket &packet); }; #endif // ESPNOW_SLAVE_H diff --git a/firmware/slave/src/imu_driver.cpp b/firmware/slave/src/imu_driver.cpp index 1bcae7b..824b6df 100644 --- a/firmware/slave/src/imu_driver.cpp +++ b/firmware/slave/src/imu_driver.cpp @@ -7,10 +7,24 @@ #include "config.h" #include +namespace { +constexpr uint8_t MPU6050_REG_SMPLRT_DIV = 0x19; +constexpr uint8_t MPU6050_REG_CONFIG = 0x1A; +constexpr uint8_t MPU6050_REG_GYRO_CONFIG = 0x1B; +constexpr uint8_t MPU6050_REG_ACCEL_CONFIG = 0x1C; +constexpr uint8_t MPU6050_REG_PWR_MGMT_1 = 0x6B; +constexpr uint8_t MPU6050_REG_WHO_AM_I = 0x75; +constexpr uint8_t MPU6050_REG_ACCEL_XOUT_H = 0x3B; +constexpr uint8_t MPU6050_DEVICE_ID = 0x68; +constexpr float MPU6050_ACCEL_LSB_PER_G = 16384.0f; +constexpr float MPU6050_GYRO_LSB_PER_DEG_S = 131.0f; +constexpr float GRAVITY_M_S2 = 9.80665f; +} + IMU_Driver::IMU_Driver() : pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0), filtered_pitch(0.0), filtered_roll(0.0), last_update_us(0), - alpha(COMPLEMENTARY_FILTER_ALPHA), connected(false) { + alpha(COMPLEMENTARY_FILTER_ALPHA), connected(false), wire(&Wire) { // Initialize data structure memset(&data, 0, sizeof(IMU_Data)); } @@ -21,31 +35,46 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { #endif // Initialize I2C - Wire.begin(sda_pin, scl_pin, i2c_freq); + wire = &Wire; + wire->begin(sda_pin, scl_pin, i2c_freq); + wire->setTimeOut(50); + delay(20); - // Try to initialize MPU6050 - if (!mpu.begin(IMU_I2C_ADDRESS, &Wire)) { - last_error = "MPU6050 not found at 0x68. Check wiring!"; + wire->beginTransmission(IMU_I2C_ADDRESS); + uint8_t i2c_error = wire->endTransmission(); + if (i2c_error != 0) { + last_error = "I2C probe failed for MPU6050 at 0x68"; #ifdef DEBUG_SERIAL_ENABLED - Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); + Serial.printf("[IMU] ERROR: %s (Wire err=%u, SDA=%u, SCL=%u, %luHz)\n", + last_error.c_str(), i2c_error, sda_pin, scl_pin, (unsigned long)i2c_freq); #endif connected = false; return false; } #ifdef DEBUG_SERIAL_ENABLED - Serial.printf("[IMU] MPU6050 initialized at 0x%02X\n", IMU_I2C_ADDRESS); + uint8_t who_am_i = 0; + if (!readRegister(MPU6050_REG_WHO_AM_I, who_am_i)) { + last_error = "MPU6050 WHO_AM_I read failed"; + Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); + connected = false; + return false; + } + Serial.printf("[IMU] WHO_AM_I = 0x%02X\n", who_am_i); #endif - // Configure MPU6050 settings - // Accelerometer range: ±2g (sufficient for static measurement) - mpu.setAccelerometerRange(MPU6050_RANGE_2_G); - - // Gyroscope range: ±250 deg/s (low range for better resolution) - mpu.setGyroRange(MPU6050_RANGE_250_DEG); - - // Filter bandwidth: 21Hz (balance noise reduction and responsiveness) - mpu.setFilterBandwidth(MPU6050_BAND_21_HZ); + if (!writeRegister(MPU6050_REG_PWR_MGMT_1, 0x01) || + !writeRegister(MPU6050_REG_SMPLRT_DIV, 0x07) || + !writeRegister(MPU6050_REG_CONFIG, 0x04) || + !writeRegister(MPU6050_REG_GYRO_CONFIG, 0x00) || + !writeRegister(MPU6050_REG_ACCEL_CONFIG, 0x00)) { + last_error = "MPU6050 register configuration failed"; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); + #endif + connected = false; + return false; + } // Wait for IMU to stabilize delay(100); @@ -60,11 +89,16 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { int valid_samples = 0; for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) { - sensors_event_t accel, gyro, temp; - if (mpu.getEvent(&accel, &gyro, &temp)) { + uint8_t buffer[14]; + if (readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) { + int16_t raw_ax = (buffer[0] << 8) | buffer[1]; + int16_t raw_ay = (buffer[2] << 8) | buffer[3]; + int16_t raw_az = (buffer[4] << 8) | buffer[5]; + float ax = (raw_ax / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float ay = (raw_ay / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float az = (raw_az / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; float pitch_raw, roll_raw; - calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z, - pitch_raw, roll_raw); + calculateAccelAngles(ax, ay, az, pitch_raw, roll_raw); pitch_sum += pitch_raw; roll_sum += roll_raw; valid_samples++; @@ -72,15 +106,22 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) { delay(10); // 100Hz sampling } - if (valid_samples > 0) { - pitch_offset = pitch_sum / valid_samples; - roll_offset = roll_sum / valid_samples; + if (valid_samples == 0) { + last_error = "MPU6050 data read failed during calibration"; #ifdef DEBUG_SERIAL_ENABLED - Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n", - pitch_offset, roll_offset); + Serial.printf("[IMU] ERROR: %s\n", last_error.c_str()); #endif + connected = false; + return false; } + pitch_offset = pitch_sum / valid_samples; + roll_offset = roll_sum / valid_samples; + #ifdef DEBUG_SERIAL_ENABLED + Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n", + pitch_offset, roll_offset); + #endif + connected = true; last_update_us = micros(); return true; @@ -91,15 +132,29 @@ bool IMU_Driver::update() { return false; } - // Get sensor events - sensors_event_t accel, gyro, temp; - if (!mpu.getEvent(&accel, &gyro, &temp)) { + uint8_t buffer[14]; + if (!readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) { #ifdef DEBUG_SERIAL_ENABLED Serial.println("[IMU] ERROR: Failed to read sensor data"); #endif return false; } + int16_t raw_ax = (buffer[0] << 8) | buffer[1]; + int16_t raw_ay = (buffer[2] << 8) | buffer[3]; + int16_t raw_az = (buffer[4] << 8) | buffer[5]; + int16_t raw_temp = (buffer[6] << 8) | buffer[7]; + int16_t raw_gx = (buffer[8] << 8) | buffer[9]; + int16_t raw_gy = (buffer[10] << 8) | buffer[11]; + int16_t raw_gz = (buffer[12] << 8) | buffer[13]; + + float ax = (raw_ax / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float ay = (raw_ay / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float az = (raw_az / MPU6050_ACCEL_LSB_PER_G) * GRAVITY_M_S2; + float gx_deg_s = raw_gx / MPU6050_GYRO_LSB_PER_DEG_S; + float gy_deg_s = raw_gy / MPU6050_GYRO_LSB_PER_DEG_S; + float gz_deg_s = raw_gz / MPU6050_GYRO_LSB_PER_DEG_S; + // Calculate time delta (dt) in seconds uint32_t now_us = micros(); float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds @@ -111,22 +166,21 @@ bool IMU_Driver::update() { } // Store raw sensor data - data.accel_x = accel.acceleration.x; - data.accel_y = accel.acceleration.y; - data.accel_z = accel.acceleration.z; - data.gyro_x = gyro.gyro.x; - data.gyro_y = gyro.gyro.y; - data.gyro_z = gyro.gyro.z; - data.temperature = temp.temperature; + data.accel_x = ax; + data.accel_y = ay; + data.accel_z = az; + data.gyro_x = gx_deg_s * M_PI / 180.0; + data.gyro_y = gy_deg_s * M_PI / 180.0; + data.gyro_z = gz_deg_s * M_PI / 180.0; + data.temperature = (raw_temp / 340.0f) + 36.53f; data.timestamp = millis(); // Calculate pitch and roll from accelerometer (gravity vector) float accel_pitch, accel_roll; - calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z, - accel_pitch, accel_roll); + calculateAccelAngles(ax, ay, az, accel_pitch, accel_roll); // Apply complementary filter (fuse gyro + accel) - applyComplementaryFilter(accel_pitch, accel_roll, gyro.gyro.x, gyro.gyro.y, dt); + applyComplementaryFilter(accel_pitch, accel_roll, data.gyro_x, data.gyro_y, dt); // Apply calibration offsets data.pitch = constrainAngle(filtered_pitch - pitch_offset); @@ -253,3 +307,39 @@ float IMU_Driver::constrainAngle(float angle) { while (angle < -180.0) angle += 360.0; return angle; } + +bool IMU_Driver::writeRegister(uint8_t reg, uint8_t value) { + wire->beginTransmission(IMU_I2C_ADDRESS); + wire->write(reg); + wire->write(value); + return wire->endTransmission() == 0; +} + +bool IMU_Driver::readRegister(uint8_t reg, uint8_t &value) { + if (!readRegisters(reg, &value, 1)) { + return false; + } + return true; +} + +bool IMU_Driver::readRegisters(uint8_t reg, uint8_t *buffer, size_t len) { + wire->beginTransmission(IMU_I2C_ADDRESS); + wire->write(reg); + if (wire->endTransmission(false) != 0) { + return false; + } + + size_t received = wire->requestFrom((int)IMU_I2C_ADDRESS, (int)len, (int)true); + if (received != len) { + return false; + } + + for (size_t i = 0; i < len; i++) { + if (!wire->available()) { + return false; + } + buffer[i] = wire->read(); + } + + return true; +} diff --git a/firmware/slave/src/imu_driver.h b/firmware/slave/src/imu_driver.h index f87802c..5865374 100644 --- a/firmware/slave/src/imu_driver.h +++ b/firmware/slave/src/imu_driver.h @@ -8,8 +8,6 @@ #include #include -#include -#include // IMU data structure struct IMU_Data { @@ -60,8 +58,8 @@ public: String getLastError() const; private: - // Adafruit MPU6050 driver instance - Adafruit_MPU6050 mpu; + // Active I2C bus for the IMU + TwoWire *wire; // Current IMU data IMU_Data data; @@ -93,6 +91,11 @@ private: // Constrain angle to -180 to +180 range float constrainAngle(float angle); + + // Low-level MPU6050 register access + bool writeRegister(uint8_t reg, uint8_t value); + bool readRegister(uint8_t reg, uint8_t &value); + bool readRegisters(uint8_t reg, uint8_t *buffer, size_t len); }; #endif // IMU_DRIVER_H diff --git a/firmware/slave/src/main.cpp b/firmware/slave/src/main.cpp index 2fe6c0e..cfee095 100644 --- a/firmware/slave/src/main.cpp +++ b/firmware/slave/src/main.cpp @@ -107,7 +107,7 @@ void setup() { Serial.printf("[Setup] Master MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", master_mac[0], master_mac[1], master_mac[2], master_mac[3], master_mac[4], master_mac[5]); - Serial.println("[Setup] **IMPORTANT**: Replace master_mac in config.h with your Master's MAC address!"); + Serial.println("[Setup] Master MAC source: firmware/slave/src/config.cpp"); #endif espnow = new ESPNowSlave(NODE_ID, master_mac); @@ -115,7 +115,7 @@ void setup() { if (!espnow->begin()) { #ifdef DEBUG_SERIAL_ENABLED Serial.printf("[Setup] ERROR: ESP-NOW initialization failed: %s\n", espnow->getLastError().c_str()); - Serial.println("[Setup] Check Master MAC address in config.h"); + Serial.println("[Setup] Check Master MAC address in config.cpp"); Serial.println("[Setup] HALTED - Cannot proceed without ESP-NOW"); #endif diff --git a/hardware/cad/README.md b/hardware/cad/README.md index 0de34d8..7a2cf03 100644 --- a/hardware/cad/README.md +++ b/hardware/cad/README.md @@ -1,14 +1,19 @@ # SkyLogic AeroAlign - 3D Printable Parts -**Status**: Placeholder documentation for Phase 1 design -**Design Tool**: FreeCAD 0.20+ (open-source parametric CAD) -**Export Format**: STL for 3D printing +**Status**: active documentation for AeroAlign plus planned CoG fixtures +**Design Tool**: FreeCAD 0.20+ +**Export Format**: STL --- ## Overview -This directory will contain all 3D printable parts for the SkyLogic AeroAlign wireless RC telemetry system. Parts are designed to fit within 200mm × 200mm × 200mm build volume (compatible with Ender 3, Prusa Mini, Bambu Lab P1P). +This directory covers both halves of the project: + +- AeroAlign IMU housings and clips +- future CoG scale fixtures and support cradles + +All parts should stay printable on common 200mm-class FDM printers. --- @@ -47,7 +52,7 @@ This directory will contain all 3D printable parts for the SkyLogic AeroAlign wi - For 8mm thick control surfaces (large rudders, thicker wings) - Reinforced jaw design -### Multi-Sensor Expansion (Phase 8) +### Multi-Sensor Expansion / Future AeroAlign 6. **wing_surface_mount_adjustable.stl** - 3-point contact clip for wing root attachment @@ -76,6 +81,24 @@ This directory will contain all 3D printable parts for the SkyLogic AeroAlign wi - Variant of sensor_housing_top.stl with magnet pockets - 2× N52 neodymium magnets (10mm diameter × 2mm thick) +### CoG Scale Fixtures (Planned) + +12. **cog_baseplate.stl** + - Flat base aligning front and rear supports + - Carries the support spacing reference + +13. **cog_support_front.stl** + - Front cradle above load cell #1 + - Should include anti-slip contact pad area + +14. **cog_support_rear.stl** + - Rear cradle above load cell #2 + - Matching geometry for repeatable support spacing + +15. **cog_hx711_mount.stl** + - Protected electronics bracket for HX711 boards + - Cable strain relief recommended + --- ## Print Settings @@ -155,13 +178,15 @@ This directory will contain all 3D printable parts for the SkyLogic AeroAlign wi ## Testing and Validation -### Phase 2 Validation Tasks +### Validation Tasks - [ ] T058: Print sensor housing on Ender 3 (verify fit and tolerance) - [ ] T059: Test print on Prusa Mini and Bambu Lab P1P (cross-printer compatibility) - [ ] T060: Create assembly guide with photos - [ ] T061: Document print settings for all STL files - [ ] T062: Weigh assembled nodes (verify <25g per node) +- [ ] Validate CoG support rigidity under expected aircraft weights +- [ ] Validate repeatable support spacing for CoG calculation ### Durability Testing @@ -219,9 +244,9 @@ Under the following terms: ## Roadmap -**Phase 1 (Current)**: Design sensor housing and basic clips -**Phase 2**: Validate print quality on 3 printer brands -**Phase 8**: Design specialized mounts for 8-sensor expansion +**Current**: IMU housing and clips +**Next**: CoG baseplate and load-cell support fixtures +**Later**: integrated mixed-tool workshop kit --- diff --git a/hardware/schematics/bom.csv b/hardware/schematics/bom.csv index 40d92fb..9f28920 100644 --- a/hardware/schematics/bom.csv +++ b/hardware/schematics/bom.csv @@ -1,51 +1,19 @@ -Component,Description,Quantity per Node,Amazon ASIN (US),AliExpress Link,Unit Price (USD),Total Price (2 nodes),Notes,Alternatives -ESP32-C3 DevKit,ESP32-C3 development board (RISC-V 160MHz WiFi/BLE),1,B09FK6F3JH,https://s.click.aliexpress.com/e/_DFKZXXX,$6.50,$13.00,"USB-C flashing ESP32-C3-DevKitM-1 or similar","ESP32-S3 (B0B6FF8K2M $12) for more power" -MPU6050 IMU,6-axis IMU (gyro + accel I2C),1,B08F7PZHVT,https://s.click.aliexpress.com/e/_DEYYYY,$4.50,$9.00,"GY-521 module with voltage regulator","BNO055 (B08M3P1KQZ $12) for better accuracy" -LiPo Battery 1S,250-400mAh 1S LiPo battery with JST connector,1,B0BKP6Y3XZ,https://s.click.aliexpress.com/e/_DFZZZZZ,$8.00,$16.00,"400mAh for Master 250mAh for Slave","500mAh (B08R3KZZZZ $9) for extended runtime" -TP4056 Charger,USB-C LiPo charging module with protection,1,B09KGGZZZZ,https://s.click.aliexpress.com/e/_DKZZZZZ,$1.50,$3.00,"Type-C USB includes overcharge/discharge protection","Micro-USB version (B07KZZZZ $1.20)" -HT7333 LDO,3.3V LDO voltage regulator (250mA),1,B07P6RZZZZ,https://s.click.aliexpress.com/e/_DLZZZZ,$0.80,$1.60,"SOT-89 package","AMS1117-3.3 (B01GZZZZ $0.50) if using through-hole" -Neodymium Magnets,N52 10mm diameter × 2mm thick magnets (optional magnetic mount),2,B08LZZZZ,https://s.click.aliexpress.com/e/_DMZZZZ,$5.00,$10.00,"For magnetic quick-mount sensor housing","Adhesive Velcro strips (B07KZZZZ $4) alternative" -3M VHB Tape,Double-sided adhesive tape for magnetic mount base,1 roll,B01MZZZZ,https://s.click.aliexpress.com/e/_DNZZZZ,$8.00,$8.00,"5m roll lasts for 50+ sensors","Gorilla tape (B06XZZZZ $6) cheaper alternative" -Rubber Pads,Silicone anti-slip pads for clips (6mm diameter),6,B07TZZZZ,https://s.click.aliexpress.com/e/_DOZZZ,$3.00,$6.00,"Self-adhesive prevent surface scratches","EVA foam pads (B08KZZZZ $2.50)" -M2 Screws,M2×6mm screws for housing assembly,4,B01MZZZZ (assortment),https://s.click.aliexpress.com/e/_DPZZZZ,$0.10,$0.40,"Stainless steel kit (500pcs)","M3 (B07ZZZZ) if using larger standoffs" -JST Connector,JST-PH 2.0mm 2-pin connector for battery,1,B07QZZZZ,https://s.click.aliexpress.com/e/_DQZZZZ,$0.30,$0.60,"Male + female pair comes with most LiPos","XH2.54 (B08ZZZZ $0.40) if battery uses different connector" -22AWG Wire,Silicone wire for battery/charging connections,0.5m,B07RZZZZ,https://s.click.aliexpress.com/e/_DRZZZZ,$0.50,$0.50,"Red + black stranded","24AWG (B06ZZZZ $0.40) also acceptable" -Heat Shrink Tubing,Heat shrink tubing assortment,5cm,B08SZZZZ (assortment),https://s.click.aliexpress.com/e/_DSZZZZ,$0.10,$0.10,"3mm diameter for wire insulation","Electrical tape (B07ZZZZ $2) if no heat gun" -10kΩ Resistor,10kΩ resistor for voltage divider (battery ADC),2,B08FZZZZ (assortment),https://s.click.aliexpress.com/e/_DTZZZZ,$0.05,$0.10,"1/4W through-hole carbon film","Resistor kit (B016ZZZZ $8) for 1000pcs" -USB-C Cable,USB-C to USB-A cable for charging/flashing,1,B0BYZZZZ,https://s.click.aliexpress.com/e/_DUZZZZ,$4.00,$4.00,"1m length sufficient","USB-C to USB-C (B09ZZZZ $5) for newer laptops" -,,,,,,, -,,,,TOTAL (2 Sensors):,,$72.30, -,,,,Master Node Only:,,$36.15, -,,,,Slave Node Only:,,$36.15, -,,,,,, -,,,,4-Sensor System:,,$144.60, -,,,,6-Sensor System:,,$216.90, -,,,,8-Sensor System:,,$289.20, -,,,,,,, -Optional Upgrades,,,,,,, -BNO055 IMU,9-axis IMU with sensor fusion (I2C),1,B08M3P1KQZ,https://s.click.aliexpress.com/e/_DVZZZZ,$12.00,$24.00,"±0.1° accuracy vs MPU6050 ±0.5°","MPU9250 (B07ZZZZ $8) if 9-axis needed" -ESP32-S3 DevKit,ESP32-S3 dual-core 240MHz (8MB flash),1,B0B6FF8K2M,https://s.click.aliexpress.com/e/_DWZZZZ,$12.00,$24.00,"Faster web server response","ESP32-C3 sufficient for most users" -500mAh LiPo,Larger battery for 6+ hour runtime,1,B08R3KZZZZ,https://s.click.aliexpress.com/e/_DXZZZZ,$9.00,$18.00,"Extends Master runtime to 6h","400mAh (default) provides 4h" -,,,,,,, -3D Printing Materials,,,,,,, -PLA Filament,PLA filament for sensor housing (1kg),0.02kg,B07PZZZZ,https://s.click.aliexpress.com/e/_DYZZZZ,$20.00,$0.40,"~20g per node Black recommended","PETG (B08ZZZZ $25) for heat resistance" -PETG Filament,PETG filament for flexible clips (1kg),0.01kg,B08ZZZZZ,https://s.click.aliexpress.com/e/_DAZZZZ,$25.00,$0.25,"~10g for 6 clips","TPU (B09ZZZZ $30) for maximum flexibility" -,,,,,,, -Tools Required (One-Time Purchase),,,,,,, -Soldering Iron,Temperature-controlled soldering station,1,B08RZZZZ,https://s.click.aliexpress.com/e/_DBZZZZ,$25.00,$25.00,"Hakko FX-888D or similar","Basic iron (B06ZZZZ $15) acceptable" -Solder Wire,60/40 tin-lead solder (0.8mm),1 roll,B07SZZZZ,https://s.click.aliexpress.com/e/_DCZZZZ,$8.00,$8.00,"Lead-free (B08ZZZZ $10) for EU compliance", -Heat Gun,Heat gun for heat shrink tubing,1,B08TZZZZ,https://s.click.aliexpress.com/e/_DDZZZZ,$12.00,$12.00,"Or use lighter carefully","Mini heat gun (B07ZZZZ $8)" -Wire Strippers,Wire stripper/cutter tool,1,B08UZZZZ,https://s.click.aliexpress.com/e/_DEZZZZ,$10.00,$10.00,"Automatic recommended","Manual (B06ZZZZ $6)" -3D Printer,FDM 3D printer (200mm build volume),1,See notes,,,$200-500,"Ender 3 ($200) Prusa Mini ($400) Bambu P1P ($500)","Print service (3D Hubs) $10-20 per set" -,,,,,,, -TOTAL BOM (2-Sensor System):,,,,,$72.30,,"Competitive: GliderThrow $600 (8 sensors) SkyRC $80 (2 sensors)" -TOTAL BOM (8-Sensor System):,,,,,$289.20,,"Our 8-sensor: $289 vs GliderThrow $600 (52% savings)" -,,,,,,, -Notes:,,,,,,, -- ASIN codes are placeholders (B0XXXXXX format) - verify current Amazon listings,,,,,,, -- Prices fluctuate ±20% based on seller and shipping,,,,,,, -- AliExpress links typically 30-50% cheaper but 2-4 week shipping,,,,,,, -- Total includes all components for 2 complete sensor nodes (Master + Slave),,,,,,, -- 3D printing materials cost negligible (~$0.65 per system),,,,,,, -- Tools are one-time purchase shared across projects,,,,,,, -- Multi-sensor systems (4/6/8 nodes) use same components multiplied,,,,,,, +Category,Component,Description,Typical Qty,Used In,Notes +Core MCU,ESP32-C3 DevKit,Low-cost main board,1 per node,"Master, IMU Slave, CoG Scale","Default low-cost choice" +Core MCU,ESP32-S3 DevKit,Higher headroom alternative,1 per node,"Master, IMU Slave, CoG Scale","Useful for USB and future UI/diagnostics" +IMU,MPU6050,GY-521 style 6-axis IMU,1 per IMU node,"Master, IMU Slave","Current robust driver implemented" +Power,LiPo 1S,Portable battery pack,1 per portable node,"Master, IMU Slave, CoG Scale","Capacity depends on runtime target" +Power,TP4056 USB-C,LiPo charger/protection board,1 per portable node,"Master, IMU Slave, CoG Scale","Optional for bench-powered scale jig" +Power,3.3V regulator,Clean 3.3V supply,1 per portable node,"Master, IMU Slave, CoG Scale","Avoid noisy supply rails" +Battery ADC,10k resistor,Voltage divider top resistor,1 per monitored node,"Master, IMU Slave","Use only where ADC monitoring is actually wired" +Battery ADC,10k resistor,Voltage divider bottom resistor,1 per monitored node,"Master, IMU Slave","Master S3 currently optional" +CoG Sensor,HX711,24-bit load cell ADC,2 per CoG scale,"CoG Scale","One per support recommended" +CoG Sensor,Load cell,Single-point or suitable existing load cell,2 per CoG scale,"CoG Scale","Choose range for aircraft mass" +Assembly,JST-PH 2.0,Battery connector,1 per battery node,"Master, IMU Slave, CoG Scale","Often already fitted to LiPo" +Assembly,Wire,Signal and power wiring,as needed,"All","Keep I2C runs short" +Assembly,Heat shrink,Insulation and strain relief,as needed,"All","Recommended for portable nodes" +Mechanical,Sensor housing,3D printed IMU enclosure,1 per IMU node,"Master, IMU Slave","Existing AeroAlign style" +Mechanical,Control surface clip,3D printed clip,1 per IMU node,"Master, IMU Slave","3 mm / 5 mm / 8 mm variants" +Mechanical,Scale support cradle,3D printed support fixture,2 per CoG scale,"CoG Scale","To be designed" +Mechanical,Scale base,3D printed baseplate,1 per CoG scale,"CoG Scale","To be designed" +Reference,Known calibration mass,Weight for HX711 calibration,1 set,"CoG Scale","Needed for repeatable scale factors" diff --git a/hardware/schematics/cog_scale_wiring.md b/hardware/schematics/cog_scale_wiring.md new file mode 100644 index 0000000..8a9f1a1 --- /dev/null +++ b/hardware/schematics/cog_scale_wiring.md @@ -0,0 +1,131 @@ +# SkyLogic AeroAlign - CoG Scale Wiring + +**Version**: 0.1.0 +**Date**: 2026-03-11 +**Scope**: planned CoG scale node that shares the same Master and ESP-NOW fabric + +## Overview + +The CoG extension is based on two supports: + +- front support load cell +- rear support load cell + +The recommended first implementation uses: + +- `1x ESP32-C3` or `1x ESP32-S3` +- `2x HX711` +- `2x single-point load cells` or your existing suitable load cells + +One HX711 per support keeps calibration and diagnostics simple. + +## Functional Model + +The CoG node reports three live values to the Master: + +- `front_weight_g` +- `rear_weight_g` +- `cog_position_mm` + +These values already fit the shared telemetry packet used by the current Master firmware. + +## Recommended Electronics + +| Part | Qty | Notes | +|------|-----|-------| +| ESP32-C3 or ESP32-S3 | 1 | same ecosystem as AeroAlign | +| HX711 amplifier | 2 | one per support | +| Load cell | 2 | typically 3 kg to 10 kg depending on model size | +| LiPo or bench supply | 1 | portable or fixed jig | +| TP4056 + regulator | optional | only for portable scale | + +## Wiring Strategy + +### HX711 #1: Front support + +| HX711 pin | Connection | +|-----------|------------| +| `VCC` | `3V3` | +| `GND` | `GND` | +| `DT` | `GPIO6` | +| `SCK` | `GPIO7` | + +### HX711 #2: Rear support + +| HX711 pin | Connection | +|-----------|------------| +| `VCC` | `3V3` | +| `GND` | `GND` | +| `DT` | `GPIO8` | +| `SCK` | `GPIO9` | + +These GPIOs are recommendations for the future CoG firmware. They are not yet hard-coded in the repo. + +## Load Cell Wiring + +Most 4-wire load cells expose: + +| Wire | Meaning | +|------|---------| +| Red | `E+` | +| Black | `E-` | +| Green | `A+` | +| White | `A-` | + +Connect each load cell directly to one HX711. + +### Important + +Wire colors are not universal. Validate your load cells with the supplier datasheet or a multimeter before soldering. + +## Mechanical Layout + +``` +aircraft + | + +--> front support --> load cell #1 --> HX711 #1 + | + +--> rear support --> load cell #2 --> HX711 #2 +``` + +The support spacing `L` must be known and entered into the system profile. + +## CoG Formula + +With front support at `x = 0` and rear support at `x = L`: + +`x_cog_from_front_support = rear_weight / (front_weight + rear_weight) * L` + +If you measure relative to the wing leading edge: + +`x_cog_from_leading_edge = support_offset_from_leading_edge + x_cog_from_front_support` + +## Calibration Plan + +Each support needs: + +1. zero / tare +2. scale factor from known weight + +Recommended workflow: + +1. tare both supports empty +2. place known weight on front support only +3. save front factor +4. repeat for rear support +5. validate with known total weight centered between supports + +## Current Repo Status + +- shared protocol support exists in [telemetry_protocol.h](/Users/florianklaner/Github/AeroAlign/firmware/common/telemetry_protocol.h) +- Master can already display CoG-style nodes in the existing UI +- dedicated HX711 firmware is not implemented yet + +## Next Firmware Target + +Create a new node type, likely `firmware/cog_slave`, with: + +- HX711 reading +- tare and scale calibration +- CoG computation +- ESP-NOW transmission using `DEVICE_TYPE_COG_SCALE` diff --git a/hardware/schematics/sensor_node_wiring.md b/hardware/schematics/sensor_node_wiring.md index 64b3b5f..6940e0c 100644 --- a/hardware/schematics/sensor_node_wiring.md +++ b/hardware/schematics/sensor_node_wiring.md @@ -1,360 +1,140 @@ -# SkyLogic AeroAlign - Sensor Node Wiring Diagram +# SkyLogic AeroAlign - IMU Node Wiring -**Version**: 1.0.0 -**Date**: 2026-01-22 -**Target Hardware**: ESP32-C3/ESP32-S3 + MPU6050 + LiPo + TP4056 - ---- +**Version**: 2.0.0 +**Date**: 2026-03-11 +**Scope**: Master and IMU slave nodes for the combined AeroAlign + CoG platform ## Overview -This document describes the complete wiring schematic for both Master and Slave sensor nodes. Both nodes use identical wiring (Master additionally runs WiFi AP + web server in firmware). +This document covers the current wiring for the angle-measurement nodes: ---- +- `Master`: ESP32-C3 or ESP32-S3, WiFi AP, web UI, ESP-NOW receiver, local IMU +- `IMU Slave`: ESP32-C3 or ESP32-S3, remote MPU6050 node over ESP-NOW -## Component List (Per Node) +The Master and IMU Slave share the same MPU6050 wiring. The main difference is firmware role. +CoG-specific hardware is documented separately in [cog_scale_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/cog_scale_wiring.md). -| Component | Part Number | Quantity | Function | -|-----------|-------------|----------|----------| -| ESP32-C3 DevKit | ESP32-C3-DevKitM-1 | 1 | Microcontroller | -| MPU6050 IMU | GY-521 module | 1 | 6-axis motion sensor | -| LiPo Battery | 1S 3.7V 250-400mAh | 1 | Power source | -| TP4056 Charger | USB-C variant | 1 | Battery charging | -| HT7333 LDO | 3.3V 250mA | 1 | Voltage regulator | -| 10kΩ Resistor | 1/4W carbon film | 2 | Voltage divider for battery ADC | -| JST Connector | 2-pin PH2.0 | 1 | Battery connector | -| USB-C Cable | 1m | 1 | Charging/flashing | +## Supported ESP32 Pin Maps ---- +### ESP32-C3 -## Power Supply Wiring +| Signal | GPIO | Notes | +|--------|------|-------| +| `SDA` | `GPIO4` | MPU6050 I2C data | +| `SCL` | `GPIO5` | MPU6050 I2C clock | +| `BATTERY_ADC` | `GPIO0` | Battery divider midpoint | +| `STATUS_LED` | `GPIO10` | Optional | + +### ESP32-S3 + +| Signal | GPIO | Notes | +|--------|------|-------| +| `SDA` | `GPIO4` | MPU6050 I2C data | +| `SCL` | `GPIO5` | MPU6050 I2C clock | +| `BATTERY_ADC` | `GPIO1` | Only if divider is actually fitted | +| `STATUS_LED` | disabled | Board-dependent, firmware keeps it off by default | + +## Power Topology ``` -LiPo Battery (3.7V nominal, 4.2V max, 3.0V min) - │ - ├─► [+] TP4056 IN+ (Battery charging input) - │ TP4056 IN- [GND] - │ TP4056 USB-C (for charging only) - │ - └─► [+] HT7333 VIN (3.0V - 4.2V input) - HT7333 GND [GND] - HT7333 VOUT [3.3V] ─► ESP32-C3 3.3V pin - MPU6050 VCC +LiPo 1S + | + +--> TP4056 BAT+/BAT- + | + +--> LDO VIN + | + +--> 3.3V rail --> ESP32 3V3 + --> MPU6050 VCC ``` -**Notes**: -- TP4056 charges LiPo when USB-C connected (red LED: charging, blue LED: full) -- HT7333 regulates LiPo voltage to stable 3.3V for ESP32 and IMU -- ESP32-C3 DevKit has built-in USB-C for programming (separate from TP4056 charging USB-C) +### Notes ---- +- A dedicated 3.3V regulator is still recommended for clean IMU behavior. +- The Master S3 configuration currently assumes battery monitoring may be absent. +- If no ADC divider is wired on the Master, leave `BATTERY_MONITOR_ENABLED` disabled in [config.h](/Users/florianklaner/Github/AeroAlign/firmware/master/src/config.h). -## ESP32-C3 to MPU6050 (I2C) Wiring +## MPU6050 Wiring + +| ESP32 | MPU6050 | Notes | +|-------|---------|-------| +| `GPIO4` | `SDA` | I2C | +| `GPIO5` | `SCL` | I2C | +| `3V3` | `VCC` | Use 3.3V | +| `GND` | `GND` | Common ground | +| `GND` | `AD0` | Sets address `0x68` | + +### Bus Settings + +- ESP32-C3 default: `400 kHz` +- ESP32-S3 default: `100 kHz` + +The lower S3 bus speed is intentional and matches the current robust MPU6050 access code. + +## Battery Divider + +Use the divider only on nodes that really need local battery reporting. ``` -ESP32-C3 MPU6050 (GY-521) ---------- ---------------- -GPIO4 (SDA) ───────► SDA (I2C Data) -GPIO5 (SCL) ───────► SCL (I2C Clock) -3.3V ───────► VCC -GND ───────► GND - INT (not connected) - AD0 (GND for 0x68 address) +LiPo+ + | + [10k] + | + +----> BATTERY_ADC + | + [10k] + | + GND ``` -**I2C Configuration**: -- **Address**: 0x68 (default, AD0 pulled low) -- **Frequency**: 400kHz (fast mode) -- **Pull-ups**: Internal ESP32 pull-ups enabled (no external resistors needed) +### Current firmware assumptions -**Alternative**: BNO055 IMU (for ±0.1° accuracy upgrade) -``` -ESP32-C3 BNO055 ---------- ------ -GPIO4 (SDA) ───────► SDA -GPIO5 (SCL) ───────► SCL -3.3V ───────► VIN -GND ───────► GND - PS0 (GND for I2C mode) - PS1 (3.3V) -``` +- IMU slaves: divider enabled in firmware +- Master C3: divider enabled +- Master S3: divider disabled by default until the ADC path is actually wired ---- +## Master Node Wiring Summary -## Battery Monitoring (Voltage Divider) +| Block | Required | Notes | +|-------|----------|-------| +| ESP32-C3 or ESP32-S3 | yes | `master` firmware | +| MPU6050 | yes | local reference IMU | +| LiPo + charger + regulator | yes | portable operation | +| Battery divider | optional | hidden in UI if absent | -``` -LiPo+ (3.0V - 4.2V) - │ - ▼ - 10kΩ Resistor (R1) - │ - ├────► ESP32-C3 GPIO0 (ADC1_CH0) - │ - ▼ - 10kΩ Resistor (R2) - │ - └────► GND +## IMU Slave Wiring Summary -Output Voltage = (LiPo Voltage) / 2 -ESP32 ADC reads 0-1650mV (half of LiPo voltage) -``` +| Block | Required | Notes | +|-------|----------|-------| +| ESP32-C3 or ESP32-S3 | yes | `slave` firmware | +| MPU6050 | yes | same robust driver as Master | +| LiPo + charger + regulator | yes | remote node | +| Battery divider | recommended | transmitted to Master | -**Calculation**: -```cpp -float adc_value = analogRead(GPIO0); // 0-4095 (12-bit) -float voltage_at_adc = (adc_value / 4095.0) * 3.3; // Convert to volts -float battery_voltage = voltage_at_adc * 2.0; // Multiply by voltage divider ratio -uint8_t battery_percent = ((battery_voltage - 3.0) / (4.2 - 3.0)) * 100.0; -``` +## Bring-Up Checklist -**Important**: -- R1 and R2 must be equal (10kΩ each) for 2:1 division -- ESP32-C3 ADC max input: 3.3V (do NOT exceed) -- LiPo max voltage 4.2V / 2 = 2.1V (safe margin) - ---- - -## Status LED (Optional) - -``` -ESP32-C3 GPIO10 ───► [+] LED [-] ───► 220Ω Resistor ───► GND -``` - -**LED Indicators**: -- **Solid Blue**: WiFi AP active (Master only) -- **Slow Blink (1Hz)**: Normal operation, connected -- **Fast Blink (5Hz)**: Searching for Master (Slave only) -- **Red Blink (3×)**: Low battery (<20%) - ---- - -## Complete Schematic (ASCII Art) - -``` - +──────────────────────────────────────+ - │ LiPo Battery (1S 3.7V) │ - │ 250-400mAh │ - +──────────────────────────────────────+ - │ │ - │+ │- - │ │ - ┌────────┴────────┐ │ - │ TP4056 Charger │ │ -USB-C Charge ───────┤ (USB-C input) │ │ - │ OUT+ OUT-│ │ - └─────┬────────────┴─────────┤ - │+ -│ - │ │ - ┌─────┴──────────────────────┴─────┐ - │ HT7333 LDO Regulator │ - │ VIN GND VOUT │ - └──────────────┬───────────┬───────┘ - │ │ 3.3V - │ │ - ┌─────────────┴───────────┴────────┐ - │ ESP32-C3 DevKit │ - │ │ - USB-C Flash ────┤ USB-C │ - │ │ - │ GPIO4 (SDA) ──────┐ │ - │ GPIO5 (SCL) ──────┤ │ - │ GPIO0 (ADC) ──────┤ │ - │ GPIO10 (LED) ─────┤ │ - │ 3.3V ─────────────┤ │ - │ GND ──────────────┤ │ - └────────────────────┼─────────────┘ - │ - │ - ┌────────────────────┴─────────────┐ - │ MPU6050 IMU (GY-521) │ - │ │ - │ SDA ◄──────────────────────────┤ - │ SCL ◄──────────────────────────┤ - │ VCC ◄───── 3.3V ───────────────┤ - │ GND ◄───── GND ────────────────┤ - │ INT (not connected) │ - │ AD0 ◄───── GND (0x68 address) │ - └──────────────────────────────────┘ - - Battery Monitor (Voltage Divider) - - LiPo+ ──┬── 10kΩ ──┬── 10kΩ ── GND - │ │ - │ └──► GPIO0 (ADC) - │ - (Read half voltage) -``` - ---- - -## Pin Assignment Summary - -### ESP32-C3 GPIO Mapping - -| GPIO Pin | Function | Connection | Notes | -|----------|----------|------------|-------| -| GPIO0 | ADC1_CH0 | Battery voltage divider midpoint | Read battery level | -| GPIO4 | I2C SDA | MPU6050 SDA | IMU data line | -| GPIO5 | I2C SCL | MPU6050 SCL | IMU clock line | -| GPIO10 | Digital Out | Status LED (optional) | Connection indicator | -| 3.3V | Power | HT7333 VOUT, MPU6050 VCC | Regulated power rail | -| GND | Ground | Common ground | All components share | -| USB-C | USB Serial | Flashing/debugging | Built-in on DevKit | - -### MPU6050 (GY-521) Pinout - -| Pin | Function | Connection | Notes | -|-----|----------|------------|-------| -| VCC | Power | ESP32 3.3V | 3.3V or 5V compatible | -| GND | Ground | Common GND | - | -| SDA | I2C Data | GPIO4 | Pull-up enabled internally | -| SCL | I2C Clock | GPIO5 | Pull-up enabled internally | -| INT | Interrupt | Not used | Future: motion detection | -| AD0 | Address Select | GND | Sets I2C address to 0x68 | - ---- - -## Assembly Instructions - -### Step 1: Solder HT7333 LDO - -1. **Identify pins**: HT7333 (SOT-89 package) - ``` - ┌───────┐ - │ HT7333│ - └┬─┬─┬──┘ - │ │ └── VOUT (3.3V output) - │ └──── GND - └────── VIN (3.0V-6.0V input) - ``` - -2. Solder to TP4056 OUT+ (VIN) and OUT- (GND) -3. Connect VOUT to ESP32-C3 3.3V rail - -### Step 2: Connect Battery - -1. Solder JST connector to LiPo (red = +, black = -) -2. Connect to TP4056 BAT+ and BAT- -3. **Important**: Check polarity before connecting! - -### Step 3: Wire I2C to MPU6050 - -1. Solder 4 wires (SDA, SCL, VCC, GND) from ESP32-C3 to MPU6050 -2. Use 22AWG silicone wire (flexible, ~10cm length) -3. Test continuity with multimeter - -### Step 4: Install Voltage Divider - -1. Solder two 10kΩ resistors in series -2. Connect high end to LiPo+ (or TP4056 OUT+) -3. Connect midpoint to ESP32-C3 GPIO0 -4. Connect low end to GND -5. Cover with heat shrink tubing - -### Step 5: Test Power Supply - -1. **Without ESP32 connected**: Measure HT7333 VOUT with multimeter -2. Expected: 3.25V-3.35V (3.3V ±50mV) -3. If correct, connect ESP32-C3 3.3V pin - -### Step 6: Flash Firmware - -1. Connect ESP32-C3 USB-C to computer -2. Flash Master or Slave firmware via PlatformIO -3. Open serial monitor (115200 baud) -4. Verify IMU initialization: "MPU6050 initialized (0x68)" - -### Step 7: Calibrate IMU - -1. Place sensor on flat, level surface -2. Power on, wait 10 seconds for stabilization -3. Web UI (Master) or serial output (Slave) should show ~0.0° pitch/roll - ---- +1. Verify `3.3V` rail before plugging the ESP32 in. +2. Confirm MPU6050 address `0x68` with `AD0` tied low. +3. Keep SDA/SCL leads short on S3 builds. +4. On the Slave, set the Master MAC in [config.cpp](/Users/florianklaner/Github/AeroAlign/firmware/slave/src/config.cpp). +5. Build and flash: + - `cd firmware/master && pio run` + - `cd firmware/slave && pio run -e esp32-s3` ## Troubleshooting -### Problem: ESP32 won't power on +### No MPU6050 detected -**Check**: -- LiPo voltage (should be 3.7V-4.2V) -- HT7333 VOUT (should be 3.3V) -- TP4056 protection (may shut down if overcharged/over-discharged) +- Check `GPIO4` and `GPIO5` +- Check `AD0 -> GND` +- Reduce wire length +- Prefer the S3 default `100 kHz` -**Solution**: Charge LiPo via TP4056 USB-C +### Battery percentage missing on Master -### Problem: I2C not detected (MPU6050 not found) +- Expected on S3 unless the divider is fitted and `BATTERY_MONITOR_ENABLED` is enabled +- The web UI now hides the metric when unavailable -**Check**: -- Wiring: SDA to GPIO4, SCL to GPIO5 -- I2C address: Run I2C scanner (should show 0x68) -- Pull-ups: Enable internal pull-ups in firmware +### Slave does not show up -**Solution**: Verify connections, re-solder if cold joints - -### Problem: Battery percentage reads 0% or 255% - -**Check**: -- Voltage divider wiring (two 10kΩ resistors in series) -- ADC pin (GPIO0) connection to midpoint -- ADC code calibration (3.3V ADC reference) - -**Solution**: Measure voltage at GPIO0 with multimeter (should be LiPo voltage / 2) - -### Problem: IMU angles drift over time - -**Check**: -- Complementary filter alpha (should be 0.98) -- IMU temperature (MPU6050 sensitive to >15°C delta from calibration) -- Vibration isolation (sensor should be firmly mounted) - -**Solution**: Recalibrate at operating temperature, increase filter alpha to 0.99 - ---- - -## Safety Warnings - -⚠️ **LiPo Battery Safety**: -- Never short-circuit LiPo terminals (fire/explosion risk) -- Do not charge above 4.2V (TP4056 prevents this) -- Do not discharge below 3.0V (TP4056 prevents this) -- Store at 50-60% charge (3.7V-3.8V) if unused >1 week -- Dispose of damaged/swollen batteries at hazardous waste facility - -⚠️ **Soldering Safety**: -- Use 300-350°C iron temperature (too hot damages components) -- Solder in ventilated area (avoid flux fumes) -- Do not touch soldering iron tip (obvious, but critical) - -⚠️ **ESD Protection**: -- ESP32-C3 and MPU6050 are ESD-sensitive -- Touch grounded metal before handling (discharge static) -- Avoid working on carpets or synthetic fabrics - ---- - -## Bill of Materials (Per Node) - -See `hardware/schematics/bom.csv` for complete BOM with Amazon ASINs and pricing. - -**Quick Summary**: -- ESP32-C3 DevKit: $6.50 -- MPU6050 IMU: $4.50 -- LiPo Battery (400mAh): $8.00 -- TP4056 Charger: $1.50 -- HT7333 LDO: $0.80 -- Resistors, wire, connectors: $2.00 -- **Total per node**: ~$23.30 - ---- - -## Revision History - -| Version | Date | Changes | -|---------|------|---------| -| 1.0.0 | 2026-01-22 | Initial wiring diagram for MVP | - ---- - -*SkyLogic AeroAlign - Precision Grounded.* +- Recheck Master MAC in [config.cpp](/Users/florianklaner/Github/AeroAlign/firmware/slave/src/config.cpp) +- Ensure Master AP channel and ESP-NOW channel match