Extend AeroAlign with mixed CoG planning and telemetry base

This commit is contained in:
2026-03-11 23:14:33 +01:00
parent 538c3081bf
commit 56890272a0
28 changed files with 1631 additions and 1332 deletions
+48 -340
View File
@@ -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.
+108 -375
View File
@@ -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.
+61
View File
@@ -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.
+268
View File
@@ -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.
+281
View File
@@ -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.
+46
View File
@@ -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 <Arduino.h>
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
+43 -16
View File
@@ -474,7 +474,7 @@
</div>
<div class="footer">
<p>SkyLogic AeroAlign v1.0.0 | Phase 4: Differential Measurement Complete</p>
<p>SkyLogic AeroAlign v1.0.0 | AeroAlign active, CoG integration base in progress</p>
<p style="margin-top: 10px;">Open source hardware & firmware | <a href="https://github.com" class="api-link">GitHub</a></p>
</div>
</div>
@@ -562,6 +562,12 @@
// Update status display
function updateStatusDisplay() {
const statusDiv = document.getElementById('status-bar');
const batteryItem = systemStatus.master_battery_available ? `
<div class="status-item">
<span class="status-label">Master Battery</span>
<span class="status-value">${systemStatus.master_battery_percent}%</span>
</div>
` : '';
statusDiv.innerHTML = `
<div class="status-item">
<span class="status-label">Uptime</span>
@@ -583,6 +589,7 @@
<span class="status-label">Free RAM</span>
<span class="status-value">${systemStatus.free_heap_kb} KB</span>
</div>
${batteryItem}
<div class="status-item">
<span class="status-label">Version</span>
<span class="status-value">${systemStatus.firmware_version}</span>
@@ -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 ? `
<div class="metric">
<div class="metric-label">Battery</div>
<div class="metric-value">${node.battery_percent}%</div>
</div>
` : '';
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 ? '' : `
<div class="metric">
<div class="metric-label">Signal</div>
<div class="metric-value">${node.rssi} dBm</div>
</div>
`;
return `
<div class="node-card ${cardClass}">
<div class="connection-indicator ${connClass}"></div>
<div class="node-header">
<div class="node-label">${node.label || 'Sensor ' + node.node_id}</div>
<div class="node-label">${cardTitle}</div>
<div class="node-id">ID: ${node.node_id}</div>
</div>
<div class="angle-display">
<div class="angle-value">${node.pitch.toFixed(2)}°</div>
<div class="angle-label">Pitch Angle</div>
<div class="angle-value">${mainValue}</div>
<div class="angle-label">${mainLabel}</div>
</div>
<div class="node-metrics">
<div class="node-metrics" style="grid-template-columns: repeat(${isScaleNode ? metricColumns : metricColumns}, 1fr);">
<div class="metric">
<div class="metric-label">Roll</div>
<div class="metric-value">${node.roll.toFixed(2)}°</div>
<div class="metric-label">${metricOneLabel}</div>
<div class="metric-value">${metricOneValue}</div>
</div>
${batteryMetric}
<div class="metric">
<div class="metric-label">Battery</div>
<div class="metric-value">${node.battery_percent}%</div>
</div>
<div class="metric">
<div class="metric-label">Signal</div>
<div class="metric-value">${node.rssi} dBm</div>
<div class="metric-label">${metricTwoLabel}</div>
<div class="metric-value">${metricTwoValue}</div>
</div>
${signalMetric}
</div>
<button class="calibrate-btn" onclick="calibrateNode(${node.node_id})" ${!node.is_connected ? 'disabled' : ''}>
${!node.is_connected ? 'Disconnected' : '⚙ Calibrate (Zero)'}
${!node.is_connected ? 'Disconnected' : (isScaleNode ? '⚙ Tare / Calibrate' : '⚙ Calibrate (Zero)')}
</button>
</div>
`;
@@ -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 =>
`<option value="${n.node_id}">${n.label || 'Node ' + n.node_id} (${n.node_id})</option>`
).join('');
+2 -2
View File
@@ -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
+23 -12
View File
@@ -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"
+16 -2
View File
@@ -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<DeviceType>(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;
+5 -11
View File
@@ -12,26 +12,20 @@
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>
// 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;
+137 -36
View File
@@ -7,10 +7,24 @@
#include "config.h"
#include <math.h>
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;
}
+7 -4
View File
@@ -8,8 +8,6 @@
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// 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
+24 -14
View File
@@ -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());
+38 -35
View File
@@ -5,10 +5,14 @@
#include "web_server.h"
#include "config.h"
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <math.h>
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"(
<!DOCTYPE html>
<html>
<head>
<title>SkyLogic AeroAlign</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>SkyLogic AeroAlign</h1>
<p>Web UI placeholder - Full React interface will be implemented in next task</p>
<p>API Endpoints:</p>
<ul>
<li><a href="/api/nodes">/api/nodes</a></li>
<li><a href="/api/status">/api/status</a></li>
<li>/api/differential?node1=1&node2=2</li>
<li>POST /api/calibrate</li>
</ul>
</body>
</html>
)";
request->send(200, "text/html", html);
request->send(SPIFFS, "/index.html", "text/html");
}
void WebServerManager::handleNotFound(AsyncWebServerRequest *request) {
+4 -1
View File
@@ -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;
+30 -12
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#include <stdint.h>
uint8_t master_mac[6] = {0x24, 0x58, 0x7C, 0xDE, 0x81, 0x90};
+26 -17
View File
@@ -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"
+28 -14
View File
@@ -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<DeviceType>(packet.device_type)),
packet.pitch, packet.roll, packet.yaw, packet.battery, packet.checksum);
#endif
return true;
+6 -11
View File
@@ -11,17 +11,7 @@
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>
// 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
+128 -38
View File
@@ -7,10 +7,24 @@
#include "config.h"
#include <math.h>
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;
}
+7 -4
View File
@@ -8,8 +8,6 @@
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// 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
+2 -2
View File
@@ -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
+34 -9
View File
@@ -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
---
+19 -51
View File
@@ -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"
1 Category Component Quantity per Node Description Amazon ASIN (US) Typical Qty AliExpress Link Used In Unit Price (USD) Notes Total Price (2 nodes) Alternatives
2 Core MCU ESP32-C3 DevKit 1 ESP32-C3 development board (RISC-V 160MHz WiFi/BLE) Low-cost main board B09FK6F3JH 1 per node https://s.click.aliexpress.com/e/_DFKZXXX Master, IMU Slave, CoG Scale $6.50 USB-C flashing ESP32-C3-DevKitM-1 or similar Default low-cost choice $13.00 ESP32-S3 (B0B6FF8K2M $12) for more power
3 Core MCU MPU6050 IMU ESP32-S3 DevKit 1 6-axis IMU (gyro + accel I2C) Higher headroom alternative B08F7PZHVT 1 per node https://s.click.aliexpress.com/e/_DEYYYY Master, IMU Slave, CoG Scale $4.50 GY-521 module with voltage regulator Useful for USB and future UI/diagnostics $9.00 BNO055 (B08M3P1KQZ $12) for better accuracy
4 IMU LiPo Battery 1S MPU6050 1 250-400mAh 1S LiPo battery with JST connector GY-521 style 6-axis IMU B0BKP6Y3XZ 1 per IMU node https://s.click.aliexpress.com/e/_DFZZZZZ Master, IMU Slave $8.00 400mAh for Master 250mAh for Slave Current robust driver implemented $16.00 500mAh (B08R3KZZZZ $9) for extended runtime
5 Power TP4056 Charger LiPo 1S 1 USB-C LiPo charging module with protection Portable battery pack B09KGGZZZZ 1 per portable node https://s.click.aliexpress.com/e/_DKZZZZZ Master, IMU Slave, CoG Scale $1.50 Type-C USB includes overcharge/discharge protection Capacity depends on runtime target $3.00 Micro-USB version (B07KZZZZ $1.20)
6 Power HT7333 LDO TP4056 USB-C 1 3.3V LDO voltage regulator (250mA) LiPo charger/protection board B07P6RZZZZ 1 per portable node https://s.click.aliexpress.com/e/_DLZZZZ Master, IMU Slave, CoG Scale $0.80 SOT-89 package Optional for bench-powered scale jig $1.60 AMS1117-3.3 (B01GZZZZ $0.50) if using through-hole
7 Power Neodymium Magnets 3.3V regulator 2 N52 10mm diameter × 2mm thick magnets (optional magnetic mount) Clean 3.3V supply B08LZZZZ 1 per portable node https://s.click.aliexpress.com/e/_DMZZZZ Master, IMU Slave, CoG Scale $5.00 For magnetic quick-mount sensor housing Avoid noisy supply rails $10.00 Adhesive Velcro strips (B07KZZZZ $4) alternative
8 Battery ADC 3M VHB Tape 10k resistor 1 roll Double-sided adhesive tape for magnetic mount base Voltage divider top resistor B01MZZZZ 1 per monitored node https://s.click.aliexpress.com/e/_DNZZZZ Master, IMU Slave $8.00 5m roll lasts for 50+ sensors Use only where ADC monitoring is actually wired $8.00 Gorilla tape (B06XZZZZ $6) cheaper alternative
9 Battery ADC Rubber Pads 10k resistor 6 Silicone anti-slip pads for clips (6mm diameter) Voltage divider bottom resistor B07TZZZZ 1 per monitored node https://s.click.aliexpress.com/e/_DOZZZ Master, IMU Slave $3.00 Self-adhesive prevent surface scratches Master S3 currently optional $6.00 EVA foam pads (B08KZZZZ $2.50)
10 CoG Sensor M2 Screws HX711 4 M2×6mm screws for housing assembly 24-bit load cell ADC B01MZZZZ (assortment) 2 per CoG scale https://s.click.aliexpress.com/e/_DPZZZZ CoG Scale $0.10 Stainless steel kit (500pcs) One per support recommended $0.40 M3 (B07ZZZZ) if using larger standoffs
11 CoG Sensor JST Connector Load cell 1 JST-PH 2.0mm 2-pin connector for battery Single-point or suitable existing load cell B07QZZZZ 2 per CoG scale https://s.click.aliexpress.com/e/_DQZZZZ CoG Scale $0.30 Male + female pair comes with most LiPos Choose range for aircraft mass $0.60 XH2.54 (B08ZZZZ $0.40) if battery uses different connector
12 Assembly 22AWG Wire JST-PH 2.0 0.5m Silicone wire for battery/charging connections Battery connector B07RZZZZ 1 per battery node https://s.click.aliexpress.com/e/_DRZZZZ Master, IMU Slave, CoG Scale $0.50 Red + black stranded Often already fitted to LiPo $0.50 24AWG (B06ZZZZ $0.40) also acceptable
13 Assembly Heat Shrink Tubing Wire 5cm Heat shrink tubing assortment Signal and power wiring B08SZZZZ (assortment) as needed https://s.click.aliexpress.com/e/_DSZZZZ All $0.10 3mm diameter for wire insulation Keep I2C runs short $0.10 Electrical tape (B07ZZZZ $2) if no heat gun
14 Assembly 10kΩ Resistor Heat shrink 2 10kΩ resistor for voltage divider (battery ADC) Insulation and strain relief B08FZZZZ (assortment) as needed https://s.click.aliexpress.com/e/_DTZZZZ All $0.05 1/4W through-hole carbon film Recommended for portable nodes $0.10 Resistor kit (B016ZZZZ $8) for 1000pcs
15 Mechanical USB-C Cable Sensor housing 1 USB-C to USB-A cable for charging/flashing 3D printed IMU enclosure B0BYZZZZ 1 per IMU node https://s.click.aliexpress.com/e/_DUZZZZ Master, IMU Slave $4.00 1m length sufficient Existing AeroAlign style $4.00 USB-C to USB-C (B09ZZZZ $5) for newer laptops
16 Mechanical Control surface clip 3D printed clip 1 per IMU node Master, IMU Slave 3 mm / 5 mm / 8 mm variants
17 Mechanical Scale support cradle 3D printed support fixture 2 per CoG scale TOTAL (2 Sensors): CoG Scale To be designed $72.30
18 Mechanical Scale base 3D printed baseplate 1 per CoG scale Master Node Only: CoG Scale To be designed $36.15
19 Reference Known calibration mass Weight for HX711 calibration 1 set Slave Node Only: CoG Scale Needed for repeatable scale factors $36.15
4-Sensor System: $144.60
6-Sensor System: $216.90
8-Sensor System: $289.20
Optional Upgrades
BNO055 IMU 1 9-axis IMU with sensor fusion (I2C) B08M3P1KQZ https://s.click.aliexpress.com/e/_DVZZZZ $12.00 ±0.1° accuracy vs MPU6050 ±0.5° $24.00 MPU9250 (B07ZZZZ $8) if 9-axis needed
ESP32-S3 DevKit 1 ESP32-S3 dual-core 240MHz (8MB flash) B0B6FF8K2M https://s.click.aliexpress.com/e/_DWZZZZ $12.00 Faster web server response $24.00 ESP32-C3 sufficient for most users
500mAh LiPo 1 Larger battery for 6+ hour runtime B08R3KZZZZ https://s.click.aliexpress.com/e/_DXZZZZ $9.00 Extends Master runtime to 6h $18.00 400mAh (default) provides 4h
3D Printing Materials
PLA Filament 0.02kg PLA filament for sensor housing (1kg) B07PZZZZ https://s.click.aliexpress.com/e/_DYZZZZ $20.00 ~20g per node Black recommended $0.40 PETG (B08ZZZZ $25) for heat resistance
PETG Filament 0.01kg PETG filament for flexible clips (1kg) B08ZZZZZ https://s.click.aliexpress.com/e/_DAZZZZ $25.00 ~10g for 6 clips $0.25 TPU (B09ZZZZ $30) for maximum flexibility
Tools Required (One-Time Purchase)
Soldering Iron 1 Temperature-controlled soldering station B08RZZZZ https://s.click.aliexpress.com/e/_DBZZZZ $25.00 Hakko FX-888D or similar $25.00 Basic iron (B06ZZZZ $15) acceptable
Solder Wire 1 roll 60/40 tin-lead solder (0.8mm) B07SZZZZ https://s.click.aliexpress.com/e/_DCZZZZ $8.00 Lead-free (B08ZZZZ $10) for EU compliance $8.00
Heat Gun 1 Heat gun for heat shrink tubing B08TZZZZ https://s.click.aliexpress.com/e/_DDZZZZ $12.00 Or use lighter carefully $12.00 Mini heat gun (B07ZZZZ $8)
Wire Strippers 1 Wire stripper/cutter tool B08UZZZZ https://s.click.aliexpress.com/e/_DEZZZZ $10.00 Automatic recommended $10.00 Manual (B06ZZZZ $6)
3D Printer 1 FDM 3D printer (200mm build volume) See notes Ender 3 ($200) Prusa Mini ($400) Bambu P1P ($500) $200-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
+131
View File
@@ -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`
+106 -326
View File
@@ -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