Extend AeroAlign with mixed CoG planning and telemetry base
This commit is contained in:
+48
-340
@@ -1,360 +1,68 @@
|
|||||||
# SkyLogic AeroAlign - Implementation Status
|
# SkyLogic AeroAlign - Implementation Status
|
||||||
|
|
||||||
**Date**: 2026-01-22
|
**Date**: 2026-03-11
|
||||||
**Phase**: 4 (Differential Measurement) - COMPLETE ✅
|
**State**: AeroAlign working, CoG integration base prepared
|
||||||
**Progress**: 32/130 tasks (25%)
|
|
||||||
|
|
||||||
---
|
## 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**:
|
## Implemented
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration Files**:
|
### Firmware
|
||||||
- ✅ `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
|
|
||||||
|
|
||||||
---
|
- 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`)
|
### Documentation
|
||||||
- Complete component list with Amazon ASINs
|
|
||||||
- Pricing: $72 for 2-sensor system, $289 for 8-sensor system
|
|
||||||
- Alternative components documented
|
|
||||||
|
|
||||||
- ✅ **Wiring Diagram** (`hardware/schematics/sensor_node_wiring.md`)
|
- README rewritten for combined AeroAlign + CoG direction
|
||||||
- Complete ESP32-MPU6050-LiPo-TP4056 wiring
|
- IMU wiring brought to current C3/S3 and battery-monitoring behavior
|
||||||
- Battery monitoring circuit (voltage divider)
|
- new CoG wiring guide added
|
||||||
- Power supply design (HT7333 LDO)
|
- BOM converted from placeholder marketplace list to current component overview
|
||||||
- Assembly instructions and troubleshooting
|
- CAD readme updated to include planned CoG fixtures
|
||||||
|
|
||||||
- ✅ **3D CAD Documentation** (`hardware/cad/README.md`)
|
## Current Hardware Documentation
|
||||||
- Sensor housing specs (38mm × 28mm × 18mm)
|
|
||||||
- Control surface clips (3mm, 5mm, 8mm)
|
|
||||||
- Print settings for PLA/PETG
|
|
||||||
- Multi-sensor expansion mounts (Phase 8 ready)
|
|
||||||
|
|
||||||
#### 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`)
|
## What Works Right Now
|
||||||
- MPU6050 6-axis sensor support
|
|
||||||
- Complementary filter (α=0.98) for angle calculation
|
|
||||||
- ±0.5° accuracy (static measurement)
|
|
||||||
- Temperature monitoring
|
|
||||||
- Calibration support
|
|
||||||
|
|
||||||
- ✅ **Calibration Manager** (`firmware/master/src/calibration.cpp/h`)
|
- Master AP + web UI
|
||||||
- NVS (Non-Volatile Storage) persistence
|
- IMU node discovery over ESP-NOW
|
||||||
- Zero-point offset storage per node
|
- calibration and differential measurement for angle nodes
|
||||||
- Temperature-tagged calibration
|
- S3-safe IMU access path
|
||||||
- Load/save/clear operations
|
- optional battery reporting on Master hardware where ADC is not wired
|
||||||
|
|
||||||
- ✅ **ESP-NOW Receiver** (`firmware/master/src/espnow_master.cpp/h`)
|
## What Is Not Finished Yet
|
||||||
- Receive sensor data from Slaves (10Hz)
|
|
||||||
- Checksum validation (XOR)
|
|
||||||
- Auto-discovery (no manual pairing)
|
|
||||||
- Connection timeout detection (1000ms)
|
|
||||||
- Packet statistics tracking
|
|
||||||
|
|
||||||
- ✅ **Web Server** (`firmware/master/src/web_server.cpp/h`)
|
- dedicated `firmware/cog_slave`
|
||||||
- AsyncWebServer (non-blocking)
|
- HX711 integration
|
||||||
- REST API endpoints:
|
- tare and scale calibration persistence
|
||||||
- GET /api/nodes (sensor data)
|
- model profiles for support spacing and target CoG
|
||||||
- GET /api/differential (EWD calculation)
|
- dedicated CoG workflow in the UI
|
||||||
- POST /api/calibrate (zero sensors)
|
|
||||||
- GET /api/status (system health)
|
|
||||||
- JSON responses (ArduinoJson)
|
|
||||||
|
|
||||||
- ✅ **Master Main Loop** (`firmware/master/src/main.cpp`)
|
## Recommended Next Step
|
||||||
- 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)
|
|
||||||
|
|
||||||
#### Firmware Foundation - Slave Node
|
Implement the actual CoG node firmware next, because the protocol, Master-side data path and baseline documentation are now in place.
|
||||||
|
|
||||||
- ✅ **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. 🚀
|
|
||||||
|
|||||||
@@ -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
|
Prepared but not fully implemented yet:
|
||||||
- ✅ **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
|
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
---
|
## Architecture
|
||||||
|
|
||||||
## ⚡ 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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Master Node (WiFi AP + Web Server + ESP-NOW Receiver)
|
IMU Slave(s) ------\
|
||||||
↑ WiFi (HTTP/JSON)
|
\
|
||||||
↑
|
CoG Scale Node(s) ----> Master ESP32 ----> WiFi AP ----> Browser UI
|
||||||
Smartphone/Tablet (Web UI)
|
/
|
||||||
|
Master local IMU --/
|
||||||
Master Node
|
|
||||||
↑ ESP-NOW (2.4GHz)
|
|
||||||
↑
|
|
||||||
Slave Node(s) (0x02-0x09)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Master Node Responsibilities
|
### Device roles
|
||||||
|
|
||||||
1. **WiFi Access Point**: Hosts "SkyLogic-AeroAlign" network (192.168.4.1)
|
- `Master`
|
||||||
2. **Web Server**: Serves React UI and REST API endpoints
|
- hosts `SkyLogic-AeroAlign`
|
||||||
3. **ESP-NOW Receiver**: Accepts sensor data from Slave nodes (10Hz)
|
- receives ESP-NOW telemetry
|
||||||
4. **IMU Sensor**: Measures Master node's own pitch/roll
|
- serves the web UI and REST API
|
||||||
5. **Calibration Manager**: Stores/loads zero-point offsets (NVS)
|
- 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
|
The shared packet format is defined in [telemetry_protocol.h](/Users/florianklaner/Github/AeroAlign/firmware/common/telemetry_protocol.h).
|
||||||
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
|
|
||||||
|
|
||||||
### Data Flow
|
Device types currently defined:
|
||||||
|
|
||||||
1. Slave IMU samples at 100Hz (10ms intervals)
|
- `DEVICE_TYPE_IMU`
|
||||||
2. Slave transmits ESP-NOW packet to Master at 10Hz (100ms intervals)
|
- `DEVICE_TYPE_COG_SCALE`
|
||||||
3. Master receives packet, validates checksum, updates node data
|
- `DEVICE_TYPE_HYBRID`
|
||||||
4. Web UI polls GET /api/nodes every 100ms (10Hz)
|
|
||||||
5. React UI updates angle displays in real-time
|
|
||||||
|
|
||||||
---
|
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**:
|
## Hardware Overview
|
||||||
```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,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /api/differential?node1=1&node2=2
|
### IMU nodes
|
||||||
|
|
||||||
Calculates differential angle between two nodes (for EWD measurement).
|
Use:
|
||||||
|
|
||||||
**Response**:
|
- ESP32-C3 or ESP32-S3
|
||||||
```json
|
- MPU6050
|
||||||
{
|
- LiPo, charger and 3.3V supply
|
||||||
"node1_id": 1,
|
- optional battery divider depending on node
|
||||||
"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"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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**:
|
Planned hardware stack:
|
||||||
```json
|
|
||||||
{
|
|
||||||
"node_id": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response**:
|
- ESP32-C3 or ESP32-S3
|
||||||
```json
|
- two HX711 boards
|
||||||
{
|
- two load cells
|
||||||
"success": true,
|
- rigid support spacing and repeatable fixtures
|
||||||
"message": "Node calibrated",
|
|
||||||
"node_id": 1,
|
|
||||||
"pitch_offset": -2.35,
|
|
||||||
"roll_offset": 0.87,
|
|
||||||
"yaw_offset": 0.0,
|
|
||||||
"timestamp": 1737590400
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /api/status
|
See [cog_scale_wiring.md](/Users/florianklaner/Github/AeroAlign/hardware/schematics/cog_scale_wiring.md).
|
||||||
|
|
||||||
System health and statistics.
|
### BOM
|
||||||
|
|
||||||
**Response**:
|
The BOM is now maintained as a current component overview instead of placeholder shop links:
|
||||||
```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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
- [bom.csv](/Users/florianklaner/Github/AeroAlign/hardware/schematics/bom.csv)
|
||||||
|
|
||||||
## 🛠️ Development
|
## Firmware Layout
|
||||||
|
|
||||||
### Project Structure
|
### Existing
|
||||||
|
|
||||||
```
|
- [firmware/master](/Users/florianklaner/Github/AeroAlign/firmware/master)
|
||||||
ewd-digiflo/
|
- [firmware/slave](/Users/florianklaner/Github/AeroAlign/firmware/slave)
|
||||||
├── firmware/
|
- [firmware/common](/Users/florianklaner/Github/AeroAlign/firmware/common)
|
||||||
│ ├── 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Instructions
|
### Planned
|
||||||
|
|
||||||
**Prerequisites**:
|
- `firmware/cog_slave`
|
||||||
- [PlatformIO](https://platformio.org/) (VS Code extension or CLI)
|
- HX711 reading
|
||||||
- Python 3.8+ (for esptool.py)
|
- tare and scale calibration
|
||||||
- Git
|
- CoG computation
|
||||||
|
- ESP-NOW transmission as `DEVICE_TYPE_COG_SCALE`
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Master:
|
||||||
|
|
||||||
**Build Master Firmware**:
|
|
||||||
```bash
|
```bash
|
||||||
cd firmware/master
|
cd firmware/master
|
||||||
pio run # Compile
|
pio run
|
||||||
pio run --target upload # Flash to ESP32
|
|
||||||
pio device monitor # View serial output (115200 baud)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Build Slave Firmware**:
|
Slave:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd firmware/slave
|
cd firmware/slave
|
||||||
pio run --target upload
|
pio run -e esp32-s3
|
||||||
pio device monitor
|
pio run -e slave2-s3
|
||||||
```
|
```
|
||||||
|
|
||||||
**Build All 8 Slave Variants** (Phase 8):
|
## Current Configuration Notes
|
||||||
```bash
|
|
||||||
cd firmware/slave
|
|
||||||
./build_all_slaves.sh # Compiles slave1-slave8 with different NODE_IDs
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
### 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 |
|
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.
|
||||||
|-------|-------|--------|
|
|
||||||
| **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 |
|
|
||||||
|
|
||||||
### Next Milestones
|
## CoG Math
|
||||||
|
|
||||||
1. **Phase 5 (Multi-Node)**: Complete User Story 3 - 4-6 Sensor Support
|
With front and rear supports spaced by `L`:
|
||||||
- Implement scrollable node list in web UI
|
|
||||||
- Add node labeling/naming functionality
|
|
||||||
- Test with 4-6 physical nodes
|
|
||||||
|
|
||||||
2. **Phase 6 (Throw Gauge)**: Control surface throw measurement
|
`x_cog_from_front_support = rear_weight / (front_weight + rear_weight) * L`
|
||||||
- Distance measurement mode
|
|
||||||
- Min/max deflection tracking
|
|
||||||
|
|
||||||
3. **Phase 7 (Polish)**: Testing and refinement
|
If the front support is offset from the wing leading edge:
|
||||||
- Unit tests for all modules
|
|
||||||
- 3D printing validation
|
|
||||||
- Comprehensive documentation
|
|
||||||
|
|
||||||
---
|
`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 ✅
|
The planned operating workflow and the recommended hardware roles are documented in:
|
||||||
- All components available on Amazon
|
|
||||||
- **$72 per 2-sensor system** (vs. GliderThrow $600 for 8 sensors)
|
|
||||||
- 77% cost savings compared to competitors
|
|
||||||
|
|
||||||
### II. 3D Printing Reproducibility ✅
|
- [WORKFLOW_AND_SYSTEM_PLAN.md](/Users/florianklaner/Github/AeroAlign/docs/WORKFLOW_AND_SYSTEM_PLAN.md)
|
||||||
- All parts fit within 200mm × 200mm × 200mm build volume
|
- [AIRCRAFT_PROFILE_MODEL.md](/Users/florianklaner/Github/AeroAlign/docs/AIRCRAFT_PROFILE_MODEL.md)
|
||||||
- Support structures <30% part volume
|
|
||||||
- Print settings documented for PLA/PETG
|
|
||||||
|
|
||||||
### III. Lightweight Design ✅
|
## 3D Printed Parts
|
||||||
- Each sensor node: ~23g (target: <25g)
|
|
||||||
- Minimal impact on control surface movement
|
|
||||||
|
|
||||||
### IV. Software Simplicity (Plug-and-Play) ✅
|
The CAD readme now covers both IMU parts and planned CoG fixtures:
|
||||||
- No app store submission required (web UI)
|
|
||||||
- No cloud services or internet needed
|
|
||||||
- 3-step setup: Power on → Connect WiFi → Open browser
|
|
||||||
|
|
||||||
---
|
- [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.
|
1. Add `firmware/cog_slave` with HX711 support.
|
||||||
|
2. Add tare and calibration endpoints in the Master API.
|
||||||
**Areas Needing Help**:
|
3. Add a dedicated CoG tab in the web UI.
|
||||||
- Web UI design (React components)
|
4. Add aircraft profiles in NVS.
|
||||||
- 3D CAD designs (FreeCAD → STL)
|
5. Finalize mechanical design for the two support fixtures.
|
||||||
- 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.*
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -474,7 +474,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<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>
|
<p style="margin-top: 10px;">Open source hardware & firmware | <a href="https://github.com" class="api-link">GitHub</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,6 +562,12 @@
|
|||||||
// Update status display
|
// Update status display
|
||||||
function updateStatusDisplay() {
|
function updateStatusDisplay() {
|
||||||
const statusDiv = document.getElementById('status-bar');
|
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 = `
|
statusDiv.innerHTML = `
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">Uptime</span>
|
<span class="status-label">Uptime</span>
|
||||||
@@ -583,6 +589,7 @@
|
|||||||
<span class="status-label">Free RAM</span>
|
<span class="status-label">Free RAM</span>
|
||||||
<span class="status-value">${systemStatus.free_heap_kb} KB</span>
|
<span class="status-value">${systemStatus.free_heap_kb} KB</span>
|
||||||
</div>
|
</div>
|
||||||
|
${batteryItem}
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">Version</span>
|
<span class="status-label">Version</span>
|
||||||
<span class="status-value">${systemStatus.firmware_version}</span>
|
<span class="status-value">${systemStatus.firmware_version}</span>
|
||||||
@@ -600,37 +607,57 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodesDiv.innerHTML = nodes.map(node => {
|
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 cardClass = !node.is_connected ? 'disconnected' : (isWarning ? 'warning' : '');
|
||||||
const connClass = node.is_connected ? '' : 'disconnected';
|
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 `
|
return `
|
||||||
<div class="node-card ${cardClass}">
|
<div class="node-card ${cardClass}">
|
||||||
<div class="connection-indicator ${connClass}"></div>
|
<div class="connection-indicator ${connClass}"></div>
|
||||||
<div class="node-header">
|
<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 class="node-id">ID: ${node.node_id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="angle-display">
|
<div class="angle-display">
|
||||||
<div class="angle-value">${node.pitch.toFixed(2)}°</div>
|
<div class="angle-value">${mainValue}</div>
|
||||||
<div class="angle-label">Pitch Angle</div>
|
<div class="angle-label">${mainLabel}</div>
|
||||||
</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">
|
||||||
<div class="metric-label">Roll</div>
|
<div class="metric-label">${metricOneLabel}</div>
|
||||||
<div class="metric-value">${node.roll.toFixed(2)}°</div>
|
<div class="metric-value">${metricOneValue}</div>
|
||||||
</div>
|
</div>
|
||||||
|
${batteryMetric}
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">Battery</div>
|
<div class="metric-label">${metricTwoLabel}</div>
|
||||||
<div class="metric-value">${node.battery_percent}%</div>
|
<div class="metric-value">${metricTwoValue}</div>
|
||||||
</div>
|
|
||||||
<div class="metric">
|
|
||||||
<div class="metric-label">Signal</div>
|
|
||||||
<div class="metric-value">${node.rssi} dBm</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
${signalMetric}
|
||||||
</div>
|
</div>
|
||||||
<button class="calibrate-btn" onclick="calibrateNode(${node.node_id})" ${!node.is_connected ? 'disabled' : ''}>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -642,7 +669,7 @@
|
|||||||
const select1 = document.getElementById('node1-select');
|
const select1 = document.getElementById('node1-select');
|
||||||
const select2 = document.getElementById('node2-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>`
|
`<option value="${n.node_id}">${n.label || 'Node ' + n.node_id} (${n.node_id})</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
;
|
;
|
||||||
; Master node hosts:
|
; Master node hosts:
|
||||||
; - WiFi Access Point (SSID: SkyLogic-AeroAlign)
|
; - WiFi Access Point (SSID: SkyLogic-AeroAlign)
|
||||||
; - AsyncWebServer with React web UI
|
; - AsyncWebServer with browser-based web UI
|
||||||
; - ESP-NOW receiver for Slave node data
|
; - ESP-NOW receiver for Slave node data
|
||||||
; - MPU6050/BNO055 IMU driver
|
; - MPU6050/BNO055 IMU driver
|
||||||
;
|
;
|
||||||
@@ -55,7 +55,7 @@ monitor_filters = esp32_exception_decoder
|
|||||||
|
|
||||||
; Build flags
|
; Build flags
|
||||||
build_flags =
|
build_flags =
|
||||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
-D ARDUINO_USB_CDC_ON_BOOT=0
|
||||||
-D CORE_DEBUG_LEVEL=3
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D CONFIG_ASYNC_TCP_RUNNING_CORE=0
|
-D CONFIG_ASYNC_TCP_RUNNING_CORE=0
|
||||||
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
-D CONFIG_ASYNC_TCP_USE_WDT=0
|
||||||
|
|||||||
@@ -33,22 +33,36 @@
|
|||||||
#define WIFI_AP_SUBNET IPAddress(255, 255, 255, 0)
|
#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_SDA 4 // GPIO4 (SDA)
|
||||||
#define IMU_I2C_SCL 5 // GPIO5 (SCL)
|
#define IMU_I2C_SCL 5 // GPIO5 (SCL)
|
||||||
#define IMU_I2C_FREQ 400000 // 400kHz (fast mode)
|
#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)
|
// Battery monitoring (ADC)
|
||||||
// Voltage divider: LiPo+ -> 10kΩ -> GPIO0 -> 10kΩ -> GND
|
// Voltage divider: LiPo+ -> 10kΩ -> BATTERY_ADC_PIN -> 10kΩ -> GND
|
||||||
#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0)
|
|
||||||
#define BATTERY_VOLTAGE_DIVIDER 2.0 // 10kΩ + 10kΩ = 2:1 ratio
|
#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)
|
// Power control (optional, for deep sleep)
|
||||||
#define POWER_ENABLE_PIN -1 // Not used (always on)
|
#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
|
// If no packet received from Slave for this duration, mark as disconnected
|
||||||
#define ESPNOW_TIMEOUT_MS 1000
|
#define ESPNOW_TIMEOUT_MS 1000
|
||||||
|
|
||||||
// Expected packet size (15 bytes: node_id + pitch + roll + yaw + battery + checksum)
|
// Expected packet size (16 bytes: node_id + device_type + pitch + roll + yaw + battery + checksum)
|
||||||
#define ESPNOW_PACKET_SIZE 15
|
#define ESPNOW_PACKET_SIZE 16
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// IMU Configuration
|
// IMU Configuration
|
||||||
@@ -132,9 +146,6 @@
|
|||||||
// Firmware version
|
// Firmware version
|
||||||
#define FIRMWARE_VERSION "1.0.0"
|
#define FIRMWARE_VERSION "1.0.0"
|
||||||
|
|
||||||
// Hardware model
|
|
||||||
#define HARDWARE_MODEL "ESP32-C3"
|
|
||||||
|
|
||||||
// System name
|
// System name
|
||||||
#define SYSTEM_NAME "SkyLogic AeroAlign"
|
#define SYSTEM_NAME "SkyLogic AeroAlign"
|
||||||
|
|
||||||
|
|||||||
@@ -161,9 +161,18 @@ void ESPNowMaster::handleReceivedPacket(const uint8_t *mac, const uint8_t *data,
|
|||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
// Update node data
|
// Update node data
|
||||||
|
node->device_type = static_cast<DeviceType>(packet.device_type);
|
||||||
node->pitch = packet.pitch;
|
node->pitch = packet.pitch;
|
||||||
node->roll = packet.roll;
|
node->roll = packet.roll;
|
||||||
node->yaw = packet.yaw;
|
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->battery_percent = packet.battery;
|
||||||
node->is_connected = true;
|
node->is_connected = true;
|
||||||
node->last_update_ms = millis();
|
node->last_update_ms = millis();
|
||||||
@@ -176,8 +185,9 @@ void ESPNowMaster::handleReceivedPacket(const uint8_t *mac, const uint8_t *data,
|
|||||||
total_packets_received++;
|
total_packets_received++;
|
||||||
|
|
||||||
#ifdef DEBUG_ESPNOW_PACKETS
|
#ifdef DEBUG_ESPNOW_PACKETS
|
||||||
Serial.printf("[ESP-NOW] RX from 0x%02X: pitch=%.2f° roll=%.2f° battery=%d%% RSSI=%ddBm\n",
|
Serial.printf("[ESP-NOW] RX from 0x%02X (%s): a=%.2f b=%.2f c=%.2f battery=%d%% RSSI=%ddBm\n",
|
||||||
packet.node_id, packet.pitch, packet.roll, packet.battery, node->rssi);
|
packet.node_id, deviceTypeToString(node->device_type),
|
||||||
|
packet.pitch, packet.roll, packet.yaw, packet.battery, node->rssi);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,11 +216,15 @@ void ESPNowMaster::registerNode(uint8_t node_id, const uint8_t *mac) {
|
|||||||
if (nodes[i].node_id == 0) {
|
if (nodes[i].node_id == 0) {
|
||||||
// Initialize new node
|
// Initialize new node
|
||||||
nodes[i].node_id = node_id;
|
nodes[i].node_id = node_id;
|
||||||
|
nodes[i].device_type = DEVICE_TYPE_UNKNOWN;
|
||||||
memcpy(nodes[i].mac_address, mac, 6);
|
memcpy(nodes[i].mac_address, mac, 6);
|
||||||
snprintf(nodes[i].label, sizeof(nodes[i].label), "Sensor %d", node_id - 1);
|
snprintf(nodes[i].label, sizeof(nodes[i].label), "Sensor %d", node_id - 1);
|
||||||
nodes[i].pitch = 0.0;
|
nodes[i].pitch = 0.0;
|
||||||
nodes[i].roll = 0.0;
|
nodes[i].roll = 0.0;
|
||||||
nodes[i].yaw = 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].pitch_offset = 0.0;
|
||||||
nodes[i].roll_offset = 0.0;
|
nodes[i].roll_offset = 0.0;
|
||||||
nodes[i].yaw_offset = 0.0;
|
nodes[i].yaw_offset = 0.0;
|
||||||
|
|||||||
@@ -12,26 +12,20 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <esp_now.h>
|
#include <esp_now.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include "../../common/telemetry_protocol.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
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sensor node data structure
|
// Sensor node data structure
|
||||||
struct SensorNode {
|
struct SensorNode {
|
||||||
uint8_t node_id;
|
uint8_t node_id;
|
||||||
|
DeviceType device_type;
|
||||||
uint8_t mac_address[6];
|
uint8_t mac_address[6];
|
||||||
char label[32];
|
char label[32];
|
||||||
float pitch;
|
float pitch;
|
||||||
float roll;
|
float roll;
|
||||||
float yaw;
|
float yaw;
|
||||||
|
float front_weight_g;
|
||||||
|
float rear_weight_g;
|
||||||
|
float cog_position_mm;
|
||||||
float pitch_offset;
|
float pitch_offset;
|
||||||
float roll_offset;
|
float roll_offset;
|
||||||
float yaw_offset;
|
float yaw_offset;
|
||||||
|
|||||||
@@ -7,10 +7,24 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <math.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()
|
IMU_Driver::IMU_Driver()
|
||||||
: pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0),
|
: pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0),
|
||||||
filtered_pitch(0.0), filtered_roll(0.0), last_update_us(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
|
// Initialize data structure
|
||||||
memset(&data, 0, sizeof(IMU_Data));
|
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
|
#endif
|
||||||
|
|
||||||
// Initialize I2C
|
// 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
|
// Probe device first so wiring / bus-speed failures are visible in logs.
|
||||||
if (!mpu.begin(IMU_I2C_ADDRESS, &Wire)) {
|
wire->beginTransmission(IMU_I2C_ADDRESS);
|
||||||
last_error = "MPU6050 not found at 0x68. Check wiring!";
|
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
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
||||||
#endif
|
#endif
|
||||||
@@ -33,19 +63,33 @@ bool IMU_Driver::begin(uint8_t sda_pin, uint8_t scl_pin, uint32_t i2c_freq) {
|
|||||||
return false;
|
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
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.printf("[IMU] MPU6050 initialized at 0x%02X\n", IMU_I2C_ADDRESS);
|
Serial.printf("[IMU] MPU6050 initialized at 0x%02X\n", IMU_I2C_ADDRESS);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Configure MPU6050 settings
|
// Wake sensor and configure ranges / filtering.
|
||||||
// Accelerometer range: ±2g (sufficient for static measurement)
|
if (!writeRegister(MPU6050_REG_PWR_MGMT_1, 0x01) ||
|
||||||
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
|
!writeRegister(MPU6050_REG_SMPLRT_DIV, 0x07) ||
|
||||||
|
!writeRegister(MPU6050_REG_CONFIG, 0x04) ||
|
||||||
// Gyroscope range: ±250 deg/s (low range for better resolution)
|
!writeRegister(MPU6050_REG_GYRO_CONFIG, 0x00) ||
|
||||||
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
|
!writeRegister(MPU6050_REG_ACCEL_CONFIG, 0x00)) {
|
||||||
|
last_error = "MPU6050 register configuration failed";
|
||||||
// Filter bandwidth: 21Hz (balance noise reduction and responsiveness)
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
|
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
||||||
|
#endif
|
||||||
|
connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for IMU to stabilize
|
// Wait for IMU to stabilize
|
||||||
delay(100);
|
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;
|
int valid_samples = 0;
|
||||||
|
|
||||||
for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) {
|
for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) {
|
||||||
sensors_event_t accel, gyro, temp;
|
uint8_t buffer[14];
|
||||||
if (mpu.getEvent(&accel, &gyro, &temp)) {
|
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;
|
float pitch_raw, roll_raw;
|
||||||
calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
|
calculateAccelAngles(ax, ay, az, pitch_raw, roll_raw);
|
||||||
pitch_raw, roll_raw);
|
|
||||||
pitch_sum += pitch_raw;
|
pitch_sum += pitch_raw;
|
||||||
roll_sum += roll_raw;
|
roll_sum += roll_raw;
|
||||||
valid_samples++;
|
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
|
delay(10); // 100Hz sampling
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid_samples > 0) {
|
if (valid_samples == 0) {
|
||||||
pitch_offset = pitch_sum / valid_samples;
|
last_error = "MPU6050 data read failed during calibration";
|
||||||
roll_offset = roll_sum / valid_samples;
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n",
|
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
||||||
pitch_offset, roll_offset);
|
|
||||||
#endif
|
#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;
|
connected = true;
|
||||||
last_update_us = micros();
|
last_update_us = micros();
|
||||||
return true;
|
return true;
|
||||||
@@ -91,15 +147,29 @@ bool IMU_Driver::update() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get sensor events
|
uint8_t buffer[14];
|
||||||
sensors_event_t accel, gyro, temp;
|
if (!readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) {
|
||||||
if (!mpu.getEvent(&accel, &gyro, &temp)) {
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.println("[IMU] ERROR: Failed to read sensor data");
|
Serial.println("[IMU] ERROR: Failed to read sensor data");
|
||||||
#endif
|
#endif
|
||||||
return false;
|
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
|
// Calculate time delta (dt) in seconds
|
||||||
uint32_t now_us = micros();
|
uint32_t now_us = micros();
|
||||||
float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds
|
float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds
|
||||||
@@ -111,22 +181,21 @@ bool IMU_Driver::update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store raw sensor data
|
// Store raw sensor data
|
||||||
data.accel_x = accel.acceleration.x;
|
data.accel_x = ax;
|
||||||
data.accel_y = accel.acceleration.y;
|
data.accel_y = ay;
|
||||||
data.accel_z = accel.acceleration.z;
|
data.accel_z = az;
|
||||||
data.gyro_x = gyro.gyro.x;
|
data.gyro_x = gx_deg_s * M_PI / 180.0;
|
||||||
data.gyro_y = gyro.gyro.y;
|
data.gyro_y = gy_deg_s * M_PI / 180.0;
|
||||||
data.gyro_z = gyro.gyro.z;
|
data.gyro_z = gz_deg_s * M_PI / 180.0;
|
||||||
data.temperature = temp.temperature;
|
data.temperature = (raw_temp / 340.0f) + 36.53f;
|
||||||
data.timestamp = millis();
|
data.timestamp = millis();
|
||||||
|
|
||||||
// Calculate pitch and roll from accelerometer (gravity vector)
|
// Calculate pitch and roll from accelerometer (gravity vector)
|
||||||
float accel_pitch, accel_roll;
|
float accel_pitch, accel_roll;
|
||||||
calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
|
calculateAccelAngles(ax, ay, az, accel_pitch, accel_roll);
|
||||||
accel_pitch, accel_roll);
|
|
||||||
|
|
||||||
// Apply complementary filter (fuse gyro + accel)
|
// 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
|
// Apply calibration offsets
|
||||||
data.pitch = constrainAngle(filtered_pitch - pitch_offset);
|
data.pitch = constrainAngle(filtered_pitch - pitch_offset);
|
||||||
@@ -253,3 +322,35 @@ float IMU_Driver::constrainAngle(float angle) {
|
|||||||
while (angle < -180.0) angle += 360.0;
|
while (angle < -180.0) angle += 360.0;
|
||||||
return angle;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <Adafruit_MPU6050.h>
|
|
||||||
#include <Adafruit_Sensor.h>
|
|
||||||
|
|
||||||
// IMU data structure
|
// IMU data structure
|
||||||
struct IMU_Data {
|
struct IMU_Data {
|
||||||
@@ -60,8 +58,8 @@ public:
|
|||||||
String getLastError() const;
|
String getLastError() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Adafruit MPU6050 driver instance
|
// Active I2C bus for the IMU
|
||||||
Adafruit_MPU6050 mpu;
|
TwoWire *wire;
|
||||||
|
|
||||||
// Current IMU data
|
// Current IMU data
|
||||||
IMU_Data data;
|
IMU_Data data;
|
||||||
@@ -93,6 +91,11 @@ private:
|
|||||||
|
|
||||||
// Constrain angle to -180 to +180 range
|
// Constrain angle to -180 to +180 range
|
||||||
float constrainAngle(float angle);
|
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
|
#endif // IMU_DRIVER_H
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ IMU_Driver imu;
|
|||||||
ESPNowMaster espnow;
|
ESPNowMaster espnow;
|
||||||
CalibrationManager calibration;
|
CalibrationManager calibration;
|
||||||
WebServerManager* webserver = nullptr;
|
WebServerManager* webserver = nullptr;
|
||||||
|
uint8_t master_battery_percent = 0;
|
||||||
|
float master_battery_voltage = 0.0f;
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// WiFi AP Setup
|
// WiFi AP Setup
|
||||||
@@ -75,7 +77,12 @@ bool setupWiFiAP() {
|
|||||||
// Battery Monitoring (Master)
|
// 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
|
// Read battery voltage via ADC
|
||||||
int adc_value = analogRead(BATTERY_ADC_PIN);
|
int adc_value = analogRead(BATTERY_ADC_PIN);
|
||||||
|
|
||||||
@@ -83,7 +90,7 @@ uint8_t readBatteryPercent() {
|
|||||||
float voltage_at_adc = (adc_value / 4095.0) * 3.3;
|
float voltage_at_adc = (adc_value / 4095.0) * 3.3;
|
||||||
|
|
||||||
// Multiply by voltage divider ratio (2:1)
|
// 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%)
|
// Convert to percentage (LiPo: 3.0V = 0%, 4.2V = 100%)
|
||||||
float percent = ((battery_voltage - BATTERY_VOLTAGE_MIN) /
|
float percent = ((battery_voltage - BATTERY_VOLTAGE_MIN) /
|
||||||
@@ -93,7 +100,9 @@ uint8_t readBatteryPercent() {
|
|||||||
if (percent < 0.0) percent = 0.0;
|
if (percent < 0.0) percent = 0.0;
|
||||||
if (percent > 100.0) percent = 100.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);
|
digitalWrite(STATUS_LED_PIN, LOW);
|
||||||
#endif
|
#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);
|
pinMode(BATTERY_ADC_PIN, INPUT);
|
||||||
|
#endif
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// Step 1: Setup WiFi AP
|
// Step 1: Setup WiFi AP
|
||||||
@@ -237,7 +248,7 @@ void setup() {
|
|||||||
Serial.println("[Setup] Initializing Web Server...");
|
Serial.println("[Setup] Initializing Web Server...");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
webserver = new WebServerManager(&espnow, &calibration, &imu);
|
webserver = new WebServerManager(&espnow, &calibration, &imu, &master_battery_percent, &master_battery_voltage);
|
||||||
|
|
||||||
if (!webserver->begin()) {
|
if (!webserver->begin()) {
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
@@ -277,8 +288,6 @@ void loop() {
|
|||||||
static uint32_t last_espnow_update_ms = 0;
|
static uint32_t last_espnow_update_ms = 0;
|
||||||
static uint32_t last_battery_read_ms = 0;
|
static uint32_t last_battery_read_ms = 0;
|
||||||
static uint32_t last_stats_print_ms = 0;
|
static uint32_t last_stats_print_ms = 0;
|
||||||
static uint8_t battery_percent = 100;
|
|
||||||
|
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -312,13 +321,10 @@ void loop() {
|
|||||||
if (now - last_battery_read_ms >= 1000) { // 1000ms = 1Hz
|
if (now - last_battery_read_ms >= 1000) { // 1000ms = 1Hz
|
||||||
last_battery_read_ms = now;
|
last_battery_read_ms = now;
|
||||||
|
|
||||||
// Read battery percentage
|
if (readBatteryState(master_battery_voltage, master_battery_percent) &&
|
||||||
battery_percent = readBatteryPercent();
|
master_battery_percent <= BATTERY_WARNING_PERCENT) {
|
||||||
|
|
||||||
// Check for low battery
|
|
||||||
if (battery_percent <= BATTERY_WARNING_PERCENT) {
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#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
|
#endif
|
||||||
|
|
||||||
// Flash LED to warn user
|
// Flash LED to warn user
|
||||||
@@ -358,7 +364,11 @@ void loop() {
|
|||||||
Serial.println("Master Node Status Report");
|
Serial.println("Master Node Status Report");
|
||||||
Serial.println("========================================");
|
Serial.println("========================================");
|
||||||
Serial.printf("Uptime: %lu seconds\n", now / 1000);
|
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.println("----------------------------------------");
|
||||||
Serial.printf("WiFi: %d clients connected\n", wifi_clients);
|
Serial.printf("WiFi: %d clients connected\n", wifi_clients);
|
||||||
Serial.printf("WiFi: http://%s\n", WIFI_AP_IP.toString().c_str());
|
Serial.printf("WiFi: http://%s\n", WIFI_AP_IP.toString().c_str());
|
||||||
|
|||||||
@@ -5,10 +5,14 @@
|
|||||||
#include "web_server.h"
|
#include "web_server.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <SPIFFS.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
WebServerManager::WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu)
|
WebServerManager::WebServerManager(ESPNowMaster* espnow, CalibrationManager* calibration, IMU_Driver* imu,
|
||||||
: espnow(espnow), calibration(calibration), master_imu(imu), server(nullptr), pair_count(0) {
|
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() {
|
bool WebServerManager::begin() {
|
||||||
@@ -16,6 +20,14 @@ bool WebServerManager::begin() {
|
|||||||
Serial.println("[WebServer] Initializing HTTP server...");
|
Serial.println("[WebServer] Initializing HTTP server...");
|
||||||
#endif
|
#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
|
// Create server instance
|
||||||
server = new AsyncWebServer(HTTP_SERVER_PORT);
|
server = new AsyncWebServer(HTTP_SERVER_PORT);
|
||||||
|
|
||||||
@@ -45,11 +57,13 @@ bool WebServerManager::begin() {
|
|||||||
this->handleGetStatus(request);
|
this->handleGetStatus(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET / - Serve web UI (placeholder for now)
|
// GET / - Serve web UI
|
||||||
server->on("/", HTTP_GET, [this](AsyncWebServerRequest *request) {
|
server->on("/", HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||||
this->handleRoot(request);
|
this->handleRoot(request);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server->serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
|
||||||
|
|
||||||
// 404 handler
|
// 404 handler
|
||||||
server->onNotFound([this](AsyncWebServerRequest *request) {
|
server->onNotFound([this](AsyncWebServerRequest *request) {
|
||||||
this->handleNotFound(request);
|
this->handleNotFound(request);
|
||||||
@@ -93,15 +107,19 @@ void WebServerManager::handleGetNodes(AsyncWebServerRequest *request) {
|
|||||||
JsonObject master_node = nodes_array.createNestedObject();
|
JsonObject master_node = nodes_array.createNestedObject();
|
||||||
master_node["node_id"] = 1;
|
master_node["node_id"] = 1;
|
||||||
master_node["label"] = "Master";
|
master_node["label"] = "Master";
|
||||||
|
master_node["device_type"] = DEVICE_TYPE_IMU;
|
||||||
|
master_node["device_type_label"] = deviceTypeToString(DEVICE_TYPE_IMU);
|
||||||
|
|
||||||
float pitch, roll, yaw;
|
float pitch, roll, yaw;
|
||||||
master_imu->getAngles(pitch, roll, yaw);
|
master_imu->getAngles(pitch, roll, yaw);
|
||||||
master_node["pitch"] = pitch;
|
master_node["pitch"] = pitch;
|
||||||
master_node["roll"] = roll;
|
master_node["roll"] = roll;
|
||||||
master_node["yaw"] = yaw;
|
master_node["yaw"] = yaw;
|
||||||
|
master_node["battery_available"] = BATTERY_MONITOR_ENABLED;
|
||||||
master_node["battery_percent"] = 85; // TODO: Implement Master battery monitoring
|
if (BATTERY_MONITOR_ENABLED && master_battery_percent && master_battery_voltage) {
|
||||||
master_node["battery_voltage"] = 3.9;
|
master_node["battery_percent"] = *master_battery_percent;
|
||||||
|
master_node["battery_voltage"] = *master_battery_voltage;
|
||||||
|
}
|
||||||
master_node["rssi"] = 0;
|
master_node["rssi"] = 0;
|
||||||
master_node["is_connected"] = true;
|
master_node["is_connected"] = true;
|
||||||
master_node["last_update_ms"] = millis();
|
master_node["last_update_ms"] = millis();
|
||||||
@@ -116,9 +134,15 @@ void WebServerManager::handleGetNodes(AsyncWebServerRequest *request) {
|
|||||||
JsonObject node_obj = nodes_array.createNestedObject();
|
JsonObject node_obj = nodes_array.createNestedObject();
|
||||||
node_obj["node_id"] = nodes[i].node_id;
|
node_obj["node_id"] = nodes[i].node_id;
|
||||||
node_obj["label"] = nodes[i].label;
|
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["pitch"] = nodes[i].pitch;
|
||||||
node_obj["roll"] = nodes[i].roll;
|
node_obj["roll"] = nodes[i].roll;
|
||||||
node_obj["yaw"] = nodes[i].yaw;
|
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_percent"] = nodes[i].battery_percent;
|
||||||
node_obj["battery_voltage"] = nodes[i].battery_voltage;
|
node_obj["battery_voltage"] = nodes[i].battery_voltage;
|
||||||
node_obj["rssi"] = nodes[i].rssi;
|
node_obj["rssi"] = nodes[i].rssi;
|
||||||
@@ -158,7 +182,7 @@ void WebServerManager::handleGetDifferential(AsyncWebServerRequest *request) {
|
|||||||
// (we'll handle this below)
|
// (we'll handle this below)
|
||||||
} else {
|
} else {
|
||||||
node1 = espnow->getNode(node1_id);
|
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\"}");
|
request->send(404, "application/json", "{\"error\":\"Node 1 not found or disconnected\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,7 +193,7 @@ void WebServerManager::handleGetDifferential(AsyncWebServerRequest *request) {
|
|||||||
// Use Master IMU directly
|
// Use Master IMU directly
|
||||||
} else {
|
} else {
|
||||||
node2 = espnow->getNode(node2_id);
|
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\"}");
|
request->send(404, "application/json", "{\"error\":\"Node 2 not found or disconnected\"}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -323,8 +347,11 @@ void WebServerManager::handleGetStatus(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
// Build JSON response
|
// Build JSON response
|
||||||
DynamicJsonDocument doc(1024);
|
DynamicJsonDocument doc(1024);
|
||||||
doc["master_battery_percent"] = 75; // TODO: Implement Master battery monitoring
|
doc["master_battery_available"] = BATTERY_MONITOR_ENABLED;
|
||||||
doc["master_battery_voltage"] = 3.85;
|
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_clients_connected"] = WiFi.softAPgetStationNum();
|
||||||
doc["wifi_channel"] = WIFI_CHANNEL;
|
doc["wifi_channel"] = WIFI_CHANNEL;
|
||||||
doc["uptime_seconds"] = millis() / 1000;
|
doc["uptime_seconds"] = millis() / 1000;
|
||||||
@@ -344,31 +371,7 @@ void WebServerManager::handleRoot(AsyncWebServerRequest *request) {
|
|||||||
#ifdef DEBUG_HTTP_REQUESTS
|
#ifdef DEBUG_HTTP_REQUESTS
|
||||||
Serial.println("[WebServer] GET /");
|
Serial.println("[WebServer] GET /");
|
||||||
#endif
|
#endif
|
||||||
|
request->send(SPIFFS, "/index.html", "text/html");
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebServerManager::handleNotFound(AsyncWebServerRequest *request) {
|
void WebServerManager::handleNotFound(AsyncWebServerRequest *request) {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
class WebServerManager {
|
class WebServerManager {
|
||||||
public:
|
public:
|
||||||
// Constructor
|
// 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
|
// Initialize web server
|
||||||
bool begin();
|
bool begin();
|
||||||
@@ -36,6 +37,8 @@ private:
|
|||||||
ESPNowMaster* espnow;
|
ESPNowMaster* espnow;
|
||||||
CalibrationManager* calibration;
|
CalibrationManager* calibration;
|
||||||
IMU_Driver* master_imu;
|
IMU_Driver* master_imu;
|
||||||
|
const uint8_t* master_battery_percent;
|
||||||
|
const float* master_battery_voltage;
|
||||||
|
|
||||||
// Last error message
|
// Last error message
|
||||||
String last_error;
|
String last_error;
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ build_flags =
|
|||||||
; Library dependencies
|
; Library dependencies
|
||||||
lib_deps =
|
lib_deps =
|
||||||
Wire ; I2C for IMU
|
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)
|
; Partition scheme (minimal, no web server)
|
||||||
board_build.partitions = min_spiffs.csv
|
board_build.partitions = min_spiffs.csv
|
||||||
@@ -59,8 +57,6 @@ build_flags =
|
|||||||
; Library dependencies (same as C3)
|
; Library dependencies (same as C3)
|
||||||
lib_deps =
|
lib_deps =
|
||||||
Wire
|
Wire
|
||||||
adafruit/Adafruit MPU6050@^2.2.4
|
|
||||||
adafruit/Adafruit BNO055@^1.6.0
|
|
||||||
|
|
||||||
; Partition scheme
|
; Partition scheme
|
||||||
board_build.partitions = min_spiffs.csv
|
board_build.partitions = min_spiffs.csv
|
||||||
@@ -77,47 +73,69 @@ upload_speed = 921600
|
|||||||
[env:slave1]
|
[env:slave1]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x02
|
-D NODE_ID=0x02
|
||||||
|
|
||||||
[env:slave2]
|
[env:slave2]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x03
|
-D NODE_ID=0x03
|
||||||
|
|
||||||
[env:slave3]
|
[env:slave3]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x04
|
-D NODE_ID=0x04
|
||||||
|
|
||||||
[env:slave4]
|
[env:slave4]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x05
|
-D NODE_ID=0x05
|
||||||
|
|
||||||
[env:slave5]
|
[env:slave5]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x06
|
-D NODE_ID=0x06
|
||||||
|
|
||||||
[env:slave6]
|
[env:slave6]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x07
|
-D NODE_ID=0x07
|
||||||
|
|
||||||
[env:slave7]
|
[env:slave7]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x08
|
-D NODE_ID=0x08
|
||||||
|
|
||||||
[env:slave8]
|
[env:slave8]
|
||||||
extends = env:esp32-c3
|
extends = env:esp32-c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${env:esp32-c3.build_flags}
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-D CORE_DEBUG_LEVEL=3
|
||||||
-D NODE_ID=0x09
|
-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
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
uint8_t master_mac[6] = {0x24, 0x58, 0x7C, 0xDE, 0x81, 0x90};
|
||||||
+26
-17
@@ -17,15 +17,13 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
// Master node MAC address
|
// 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:
|
// To find Master MAC:
|
||||||
// 1. Flash Master firmware
|
// 1. Flash Master firmware
|
||||||
// 2. Connect Master to USB, open serial monitor (115200 baud)
|
// 2. Connect Master to USB, open serial monitor (115200 baud)
|
||||||
// 3. Master prints MAC at boot: "Master MAC: 24:6F:28:12:34:56"
|
// 3. Master prints MAC at boot: "Master MAC: 24:6F:28:12:34:56"
|
||||||
// 4. Copy MAC into this array, reflash Slave
|
// 4. Copy MAC into firmware/slave/src/config.cpp, then reflash Slave
|
||||||
//
|
extern uint8_t master_mac[6];
|
||||||
// Format: {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
|
||||||
uint8_t master_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // REPLACE WITH ACTUAL MAC
|
|
||||||
|
|
||||||
// Slave node ID (unique identifier for this Slave)
|
// Slave node ID (unique identifier for this Slave)
|
||||||
// Default: 0x02 (first 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)
|
// 100ms = 10Hz update rate (balances latency and power consumption)
|
||||||
#define ESPNOW_SEND_INTERVAL_MS 100
|
#define ESPNOW_SEND_INTERVAL_MS 100
|
||||||
|
|
||||||
// ESP-NOW packet size (15 bytes: node_id + pitch + roll + yaw + battery + checksum)
|
// ESP-NOW packet size (16 bytes: node_id + device_type + pitch + roll + yaw + battery + checksum)
|
||||||
#define ESPNOW_PACKET_SIZE 15
|
#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_SDA 4 // GPIO4 (SDA)
|
||||||
#define IMU_I2C_SCL 5 // GPIO5 (SCL)
|
#define IMU_I2C_SCL 5 // GPIO5 (SCL)
|
||||||
#define IMU_I2C_FREQ 400000 // 400kHz (fast mode)
|
#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)
|
// Battery monitoring (ADC)
|
||||||
// Voltage divider: LiPo+ -> 10kΩ -> GPIO0 -> 10kΩ -> GND
|
// Voltage divider: LiPo+ -> 10kΩ -> BATTERY_ADC_PIN -> 10kΩ -> GND
|
||||||
#define BATTERY_ADC_PIN 0 // GPIO0 (ADC1_CH0)
|
|
||||||
#define BATTERY_VOLTAGE_DIVIDER 2.0 // 10kΩ + 10kΩ = 2:1 ratio
|
#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)
|
// Power control (optional, for deep sleep)
|
||||||
#define POWER_ENABLE_PIN -1 // Not used (always on)
|
#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
|
// Firmware version
|
||||||
#define FIRMWARE_VERSION "1.0.0"
|
#define FIRMWARE_VERSION "1.0.0"
|
||||||
|
|
||||||
// Hardware model
|
|
||||||
#define HARDWARE_MODEL "ESP32-C3"
|
|
||||||
|
|
||||||
// System name
|
// System name
|
||||||
#define SYSTEM_NAME "SkyLogic AeroAlign Slave"
|
#define SYSTEM_NAME "SkyLogic AeroAlign Slave"
|
||||||
|
|
||||||
|
|||||||
@@ -77,31 +77,45 @@ bool ESPNowSlave::begin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ESPNowSlave::sendData(float pitch, float roll, float yaw, uint8_t battery) {
|
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) {
|
if (!paired) {
|
||||||
last_error = "Not paired with Master";
|
last_error = "Not paired with Master";
|
||||||
return false;
|
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
|
// 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) {
|
if (result == ESP_OK) {
|
||||||
total_packets_sent++;
|
total_packets_sent++;
|
||||||
|
|
||||||
#ifdef DEBUG_ESPNOW_PACKETS
|
#ifdef DEBUG_ESPNOW_PACKETS
|
||||||
Serial.printf("[ESP-NOW] TX: pitch=%.2f° roll=%.2f° battery=%d%% checksum=0x%02X\n",
|
Serial.printf("[ESP-NOW] TX (%s): a=%.2f b=%.2f c=%.2f battery=%d%% checksum=0x%02X\n",
|
||||||
pitch, roll, battery, packet.checksum);
|
deviceTypeToString(static_cast<DeviceType>(packet.device_type)),
|
||||||
|
packet.pitch, packet.roll, packet.yaw, packet.battery, packet.checksum);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -11,17 +11,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <esp_now.h>
|
#include <esp_now.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include "../../common/telemetry_protocol.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
|
|
||||||
};
|
|
||||||
|
|
||||||
// ESP-NOW Slave Manager class
|
// ESP-NOW Slave Manager class
|
||||||
class ESPNowSlave {
|
class ESPNowSlave {
|
||||||
@@ -35,6 +25,9 @@ public:
|
|||||||
// Send sensor data packet to Master
|
// Send sensor data packet to Master
|
||||||
bool sendData(float pitch, float roll, float yaw, uint8_t battery);
|
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
|
// Get transmission statistics
|
||||||
void getStatistics(uint32_t &total_sent, uint32_t &total_failed, float &success_rate);
|
void getStatistics(uint32_t &total_sent, uint32_t &total_failed, float &success_rate);
|
||||||
|
|
||||||
@@ -70,6 +63,8 @@ private:
|
|||||||
|
|
||||||
// Calculate XOR checksum
|
// Calculate XOR checksum
|
||||||
uint8_t calculateChecksum(const uint8_t *data, int len);
|
uint8_t calculateChecksum(const uint8_t *data, int len);
|
||||||
|
|
||||||
|
bool sendPacket(const ESPNowPacket &packet);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ESPNOW_SLAVE_H
|
#endif // ESPNOW_SLAVE_H
|
||||||
|
|||||||
@@ -7,10 +7,24 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <math.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()
|
IMU_Driver::IMU_Driver()
|
||||||
: pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0),
|
: pitch_offset(0.0), roll_offset(0.0), yaw_offset(0.0),
|
||||||
filtered_pitch(0.0), filtered_roll(0.0), last_update_us(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
|
// Initialize data structure
|
||||||
memset(&data, 0, sizeof(IMU_Data));
|
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
|
#endif
|
||||||
|
|
||||||
// Initialize I2C
|
// 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
|
wire->beginTransmission(IMU_I2C_ADDRESS);
|
||||||
if (!mpu.begin(IMU_I2C_ADDRESS, &Wire)) {
|
uint8_t i2c_error = wire->endTransmission();
|
||||||
last_error = "MPU6050 not found at 0x68. Check wiring!";
|
if (i2c_error != 0) {
|
||||||
|
last_error = "I2C probe failed for MPU6050 at 0x68";
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#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
|
#endif
|
||||||
connected = false;
|
connected = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#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
|
#endif
|
||||||
|
|
||||||
// Configure MPU6050 settings
|
if (!writeRegister(MPU6050_REG_PWR_MGMT_1, 0x01) ||
|
||||||
// Accelerometer range: ±2g (sufficient for static measurement)
|
!writeRegister(MPU6050_REG_SMPLRT_DIV, 0x07) ||
|
||||||
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
|
!writeRegister(MPU6050_REG_CONFIG, 0x04) ||
|
||||||
|
!writeRegister(MPU6050_REG_GYRO_CONFIG, 0x00) ||
|
||||||
// Gyroscope range: ±250 deg/s (low range for better resolution)
|
!writeRegister(MPU6050_REG_ACCEL_CONFIG, 0x00)) {
|
||||||
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
|
last_error = "MPU6050 register configuration failed";
|
||||||
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
// Filter bandwidth: 21Hz (balance noise reduction and responsiveness)
|
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
||||||
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
|
#endif
|
||||||
|
connected = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for IMU to stabilize
|
// Wait for IMU to stabilize
|
||||||
delay(100);
|
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;
|
int valid_samples = 0;
|
||||||
|
|
||||||
for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) {
|
for (int i = 0; i < IMU_CALIBRATION_SAMPLES; i++) {
|
||||||
sensors_event_t accel, gyro, temp;
|
uint8_t buffer[14];
|
||||||
if (mpu.getEvent(&accel, &gyro, &temp)) {
|
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;
|
float pitch_raw, roll_raw;
|
||||||
calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
|
calculateAccelAngles(ax, ay, az, pitch_raw, roll_raw);
|
||||||
pitch_raw, roll_raw);
|
|
||||||
pitch_sum += pitch_raw;
|
pitch_sum += pitch_raw;
|
||||||
roll_sum += roll_raw;
|
roll_sum += roll_raw;
|
||||||
valid_samples++;
|
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
|
delay(10); // 100Hz sampling
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid_samples > 0) {
|
if (valid_samples == 0) {
|
||||||
pitch_offset = pitch_sum / valid_samples;
|
last_error = "MPU6050 data read failed during calibration";
|
||||||
roll_offset = roll_sum / valid_samples;
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.printf("[IMU] Calibration complete. Offsets: pitch=%.2f°, roll=%.2f°\n",
|
Serial.printf("[IMU] ERROR: %s\n", last_error.c_str());
|
||||||
pitch_offset, roll_offset);
|
|
||||||
#endif
|
#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;
|
connected = true;
|
||||||
last_update_us = micros();
|
last_update_us = micros();
|
||||||
return true;
|
return true;
|
||||||
@@ -91,15 +132,29 @@ bool IMU_Driver::update() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get sensor events
|
uint8_t buffer[14];
|
||||||
sensors_event_t accel, gyro, temp;
|
if (!readRegisters(MPU6050_REG_ACCEL_XOUT_H, buffer, sizeof(buffer))) {
|
||||||
if (!mpu.getEvent(&accel, &gyro, &temp)) {
|
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.println("[IMU] ERROR: Failed to read sensor data");
|
Serial.println("[IMU] ERROR: Failed to read sensor data");
|
||||||
#endif
|
#endif
|
||||||
return false;
|
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
|
// Calculate time delta (dt) in seconds
|
||||||
uint32_t now_us = micros();
|
uint32_t now_us = micros();
|
||||||
float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds
|
float dt = (now_us - last_update_us) / 1000000.0; // Convert to seconds
|
||||||
@@ -111,22 +166,21 @@ bool IMU_Driver::update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store raw sensor data
|
// Store raw sensor data
|
||||||
data.accel_x = accel.acceleration.x;
|
data.accel_x = ax;
|
||||||
data.accel_y = accel.acceleration.y;
|
data.accel_y = ay;
|
||||||
data.accel_z = accel.acceleration.z;
|
data.accel_z = az;
|
||||||
data.gyro_x = gyro.gyro.x;
|
data.gyro_x = gx_deg_s * M_PI / 180.0;
|
||||||
data.gyro_y = gyro.gyro.y;
|
data.gyro_y = gy_deg_s * M_PI / 180.0;
|
||||||
data.gyro_z = gyro.gyro.z;
|
data.gyro_z = gz_deg_s * M_PI / 180.0;
|
||||||
data.temperature = temp.temperature;
|
data.temperature = (raw_temp / 340.0f) + 36.53f;
|
||||||
data.timestamp = millis();
|
data.timestamp = millis();
|
||||||
|
|
||||||
// Calculate pitch and roll from accelerometer (gravity vector)
|
// Calculate pitch and roll from accelerometer (gravity vector)
|
||||||
float accel_pitch, accel_roll;
|
float accel_pitch, accel_roll;
|
||||||
calculateAccelAngles(accel.acceleration.x, accel.acceleration.y, accel.acceleration.z,
|
calculateAccelAngles(ax, ay, az, accel_pitch, accel_roll);
|
||||||
accel_pitch, accel_roll);
|
|
||||||
|
|
||||||
// Apply complementary filter (fuse gyro + accel)
|
// 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
|
// Apply calibration offsets
|
||||||
data.pitch = constrainAngle(filtered_pitch - pitch_offset);
|
data.pitch = constrainAngle(filtered_pitch - pitch_offset);
|
||||||
@@ -253,3 +307,39 @@ float IMU_Driver::constrainAngle(float angle) {
|
|||||||
while (angle < -180.0) angle += 360.0;
|
while (angle < -180.0) angle += 360.0;
|
||||||
return angle;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <Adafruit_MPU6050.h>
|
|
||||||
#include <Adafruit_Sensor.h>
|
|
||||||
|
|
||||||
// IMU data structure
|
// IMU data structure
|
||||||
struct IMU_Data {
|
struct IMU_Data {
|
||||||
@@ -60,8 +58,8 @@ public:
|
|||||||
String getLastError() const;
|
String getLastError() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Adafruit MPU6050 driver instance
|
// Active I2C bus for the IMU
|
||||||
Adafruit_MPU6050 mpu;
|
TwoWire *wire;
|
||||||
|
|
||||||
// Current IMU data
|
// Current IMU data
|
||||||
IMU_Data data;
|
IMU_Data data;
|
||||||
@@ -93,6 +91,11 @@ private:
|
|||||||
|
|
||||||
// Constrain angle to -180 to +180 range
|
// Constrain angle to -180 to +180 range
|
||||||
float constrainAngle(float angle);
|
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
|
#endif // IMU_DRIVER_H
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ void setup() {
|
|||||||
Serial.printf("[Setup] Master MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
Serial.printf("[Setup] Master MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||||
master_mac[0], master_mac[1], master_mac[2],
|
master_mac[0], master_mac[1], master_mac[2],
|
||||||
master_mac[3], master_mac[4], master_mac[5]);
|
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
|
#endif
|
||||||
|
|
||||||
espnow = new ESPNowSlave(NODE_ID, master_mac);
|
espnow = new ESPNowSlave(NODE_ID, master_mac);
|
||||||
@@ -115,7 +115,7 @@ void setup() {
|
|||||||
if (!espnow->begin()) {
|
if (!espnow->begin()) {
|
||||||
#ifdef DEBUG_SERIAL_ENABLED
|
#ifdef DEBUG_SERIAL_ENABLED
|
||||||
Serial.printf("[Setup] ERROR: ESP-NOW initialization failed: %s\n", espnow->getLastError().c_str());
|
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");
|
Serial.println("[Setup] HALTED - Cannot proceed without ESP-NOW");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
+34
-9
@@ -1,14 +1,19 @@
|
|||||||
# SkyLogic AeroAlign - 3D Printable Parts
|
# SkyLogic AeroAlign - 3D Printable Parts
|
||||||
|
|
||||||
**Status**: Placeholder documentation for Phase 1 design
|
**Status**: active documentation for AeroAlign plus planned CoG fixtures
|
||||||
**Design Tool**: FreeCAD 0.20+ (open-source parametric CAD)
|
**Design Tool**: FreeCAD 0.20+
|
||||||
**Export Format**: STL for 3D printing
|
**Export Format**: STL
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## 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)
|
- For 8mm thick control surfaces (large rudders, thicker wings)
|
||||||
- Reinforced jaw design
|
- Reinforced jaw design
|
||||||
|
|
||||||
### Multi-Sensor Expansion (Phase 8)
|
### Multi-Sensor Expansion / Future AeroAlign
|
||||||
|
|
||||||
6. **wing_surface_mount_adjustable.stl**
|
6. **wing_surface_mount_adjustable.stl**
|
||||||
- 3-point contact clip for wing root attachment
|
- 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
|
- Variant of sensor_housing_top.stl with magnet pockets
|
||||||
- 2× N52 neodymium magnets (10mm diameter × 2mm thick)
|
- 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
|
## Print Settings
|
||||||
@@ -155,13 +178,15 @@ This directory will contain all 3D printable parts for the SkyLogic AeroAlign wi
|
|||||||
|
|
||||||
## Testing and Validation
|
## Testing and Validation
|
||||||
|
|
||||||
### Phase 2 Validation Tasks
|
### Validation Tasks
|
||||||
|
|
||||||
- [ ] T058: Print sensor housing on Ender 3 (verify fit and tolerance)
|
- [ ] T058: Print sensor housing on Ender 3 (verify fit and tolerance)
|
||||||
- [ ] T059: Test print on Prusa Mini and Bambu Lab P1P (cross-printer compatibility)
|
- [ ] T059: Test print on Prusa Mini and Bambu Lab P1P (cross-printer compatibility)
|
||||||
- [ ] T060: Create assembly guide with photos
|
- [ ] T060: Create assembly guide with photos
|
||||||
- [ ] T061: Document print settings for all STL files
|
- [ ] T061: Document print settings for all STL files
|
||||||
- [ ] T062: Weigh assembled nodes (verify <25g per node)
|
- [ ] 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
|
### Durability Testing
|
||||||
|
|
||||||
@@ -219,9 +244,9 @@ Under the following terms:
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
**Phase 1 (Current)**: Design sensor housing and basic clips
|
**Current**: IMU housing and clips
|
||||||
**Phase 2**: Validate print quality on 3 printer brands
|
**Next**: CoG baseplate and load-cell support fixtures
|
||||||
**Phase 8**: Design specialized mounts for 8-sensor expansion
|
**Later**: integrated mixed-tool workshop kit
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+19
-51
@@ -1,51 +1,19 @@
|
|||||||
Component,Description,Quantity per Node,Amazon ASIN (US),AliExpress Link,Unit Price (USD),Total Price (2 nodes),Notes,Alternatives
|
Category,Component,Description,Typical Qty,Used In,Notes
|
||||||
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"
|
Core MCU,ESP32-C3 DevKit,Low-cost main board,1 per node,"Master, IMU Slave, CoG Scale","Default low-cost choice"
|
||||||
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"
|
Core MCU,ESP32-S3 DevKit,Higher headroom alternative,1 per node,"Master, IMU Slave, CoG Scale","Useful for USB and future UI/diagnostics"
|
||||||
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"
|
IMU,MPU6050,GY-521 style 6-axis IMU,1 per IMU node,"Master, IMU Slave","Current robust driver implemented"
|
||||||
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)"
|
Power,LiPo 1S,Portable battery pack,1 per portable node,"Master, IMU Slave, CoG Scale","Capacity depends on runtime target"
|
||||||
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"
|
Power,TP4056 USB-C,LiPo charger/protection board,1 per portable node,"Master, IMU Slave, CoG Scale","Optional for bench-powered scale jig"
|
||||||
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"
|
Power,3.3V regulator,Clean 3.3V supply,1 per portable node,"Master, IMU Slave, CoG Scale","Avoid noisy supply rails"
|
||||||
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"
|
Battery ADC,10k resistor,Voltage divider top resistor,1 per monitored node,"Master, IMU Slave","Use only where ADC monitoring is actually wired"
|
||||||
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)"
|
Battery ADC,10k resistor,Voltage divider bottom resistor,1 per monitored node,"Master, IMU Slave","Master S3 currently optional"
|
||||||
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"
|
CoG Sensor,HX711,24-bit load cell ADC,2 per CoG scale,"CoG Scale","One per support recommended"
|
||||||
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"
|
CoG Sensor,Load cell,Single-point or suitable existing load cell,2 per CoG scale,"CoG Scale","Choose range for aircraft mass"
|
||||||
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"
|
Assembly,JST-PH 2.0,Battery connector,1 per battery node,"Master, IMU Slave, CoG Scale","Often already fitted to LiPo"
|
||||||
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"
|
Assembly,Wire,Signal and power wiring,as needed,"All","Keep I2C runs short"
|
||||||
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"
|
Assembly,Heat shrink,Insulation and strain relief,as needed,"All","Recommended for portable nodes"
|
||||||
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"
|
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"
|
||||||
,,,,TOTAL (2 Sensors):,,$72.30,
|
Mechanical,Scale support cradle,3D printed support fixture,2 per CoG scale,"CoG Scale","To be designed"
|
||||||
,,,,Master Node Only:,,$36.15,
|
Mechanical,Scale base,3D printed baseplate,1 per CoG scale,"CoG Scale","To be designed"
|
||||||
,,,,Slave Node Only:,,$36.15,
|
Reference,Known calibration mass,Weight for HX711 calibration,1 set,"CoG Scale","Needed for repeatable scale factors"
|
||||||
,,,,,,
|
|
||||||
,,,,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,,,,,,,
|
|
||||||
|
|||||||
|
@@ -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`
|
||||||
@@ -1,360 +1,140 @@
|
|||||||
# SkyLogic AeroAlign - Sensor Node Wiring Diagram
|
# SkyLogic AeroAlign - IMU Node Wiring
|
||||||
|
|
||||||
**Version**: 1.0.0
|
**Version**: 2.0.0
|
||||||
**Date**: 2026-01-22
|
**Date**: 2026-03-11
|
||||||
**Target Hardware**: ESP32-C3/ESP32-S3 + MPU6050 + LiPo + TP4056
|
**Scope**: Master and IMU slave nodes for the combined AeroAlign + CoG platform
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
## 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 |
|
## Supported ESP32 Pin Maps
|
||||||
|-----------|-------------|----------|----------|
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
---
|
### 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)
|
LiPo 1S
|
||||||
│
|
|
|
||||||
├─► [+] TP4056 IN+ (Battery charging input)
|
+--> TP4056 BAT+/BAT-
|
||||||
│ TP4056 IN- [GND]
|
|
|
||||||
│ TP4056 USB-C (for charging only)
|
+--> LDO VIN
|
||||||
│
|
|
|
||||||
└─► [+] HT7333 VIN (3.0V - 4.2V input)
|
+--> 3.3V rail --> ESP32 3V3
|
||||||
HT7333 GND [GND]
|
--> MPU6050 VCC
|
||||||
HT7333 VOUT [3.3V] ─► ESP32-C3 3.3V pin
|
|
||||||
MPU6050 VCC
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes**:
|
### 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)
|
|
||||||
|
|
||||||
---
|
- 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)
|
LiPo+
|
||||||
--------- ----------------
|
|
|
||||||
GPIO4 (SDA) ───────► SDA (I2C Data)
|
[10k]
|
||||||
GPIO5 (SCL) ───────► SCL (I2C Clock)
|
|
|
||||||
3.3V ───────► VCC
|
+----> BATTERY_ADC
|
||||||
GND ───────► GND
|
|
|
||||||
INT (not connected)
|
[10k]
|
||||||
AD0 (GND for 0x68 address)
|
|
|
||||||
|
GND
|
||||||
```
|
```
|
||||||
|
|
||||||
**I2C Configuration**:
|
### Current firmware assumptions
|
||||||
- **Address**: 0x68 (default, AD0 pulled low)
|
|
||||||
- **Frequency**: 400kHz (fast mode)
|
|
||||||
- **Pull-ups**: Internal ESP32 pull-ups enabled (no external resistors needed)
|
|
||||||
|
|
||||||
**Alternative**: BNO055 IMU (for ±0.1° accuracy upgrade)
|
- IMU slaves: divider enabled in firmware
|
||||||
```
|
- Master C3: divider enabled
|
||||||
ESP32-C3 BNO055
|
- Master S3: divider disabled by default until the ADC path is actually wired
|
||||||
--------- ------
|
|
||||||
GPIO4 (SDA) ───────► SDA
|
|
||||||
GPIO5 (SCL) ───────► SCL
|
|
||||||
3.3V ───────► VIN
|
|
||||||
GND ───────► GND
|
|
||||||
PS0 (GND for I2C mode)
|
|
||||||
PS1 (3.3V)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
## 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 |
|
||||||
|
|
||||||
```
|
## IMU Slave Wiring Summary
|
||||||
LiPo+ (3.0V - 4.2V)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
10kΩ Resistor (R1)
|
|
||||||
│
|
|
||||||
├────► ESP32-C3 GPIO0 (ADC1_CH0)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
10kΩ Resistor (R2)
|
|
||||||
│
|
|
||||||
└────► GND
|
|
||||||
|
|
||||||
Output Voltage = (LiPo Voltage) / 2
|
| Block | Required | Notes |
|
||||||
ESP32 ADC reads 0-1650mV (half of LiPo voltage)
|
|-------|----------|-------|
|
||||||
```
|
| 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**:
|
## Bring-Up Checklist
|
||||||
```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;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**:
|
1. Verify `3.3V` rail before plugging the ESP32 in.
|
||||||
- R1 and R2 must be equal (10kΩ each) for 2:1 division
|
2. Confirm MPU6050 address `0x68` with `AD0` tied low.
|
||||||
- ESP32-C3 ADC max input: 3.3V (do NOT exceed)
|
3. Keep SDA/SCL leads short on S3 builds.
|
||||||
- LiPo max voltage 4.2V / 2 = 2.1V (safe margin)
|
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`
|
||||||
## 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Problem: ESP32 won't power on
|
### No MPU6050 detected
|
||||||
|
|
||||||
**Check**:
|
- Check `GPIO4` and `GPIO5`
|
||||||
- LiPo voltage (should be 3.7V-4.2V)
|
- Check `AD0 -> GND`
|
||||||
- HT7333 VOUT (should be 3.3V)
|
- Reduce wire length
|
||||||
- TP4056 protection (may shut down if overcharged/over-discharged)
|
- 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**:
|
### Slave does not show up
|
||||||
- Wiring: SDA to GPIO4, SCL to GPIO5
|
|
||||||
- I2C address: Run I2C scanner (should show 0x68)
|
|
||||||
- Pull-ups: Enable internal pull-ups in firmware
|
|
||||||
|
|
||||||
**Solution**: Verify connections, re-solder if cold joints
|
- Recheck Master MAC in [config.cpp](/Users/florianklaner/Github/AeroAlign/firmware/slave/src/config.cpp)
|
||||||
|
- Ensure Master AP channel and ESP-NOW channel match
|
||||||
### 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.*
|
|
||||||
|
|||||||
Reference in New Issue
Block a user