diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5916630..68bdc37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write # Required to upload assets to release + pull-requests: write # Required to create pull requests steps: - name: Checkout code @@ -41,6 +42,10 @@ jobs: - name: Build firmware run: | pio run -e esp8266 + + - name: Restore version.h + run: | + git restore include/version.h - name: Rename firmware binary run: | @@ -53,3 +58,51 @@ jobs: files: esp-smart-meter-${{ github.event.release.tag_name }}.bin env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare firmware for GitHub Pages + run: | + VERSION="${{ github.event.release.tag_name }}" + # Remove 'v' prefix if present + VERSION_NUM="${VERSION#v}" + + # Fetch the latest main branch + git fetch origin main + git checkout main + mkdir -p docs/firmware + + # Clean up old firmware files (keep only version.json and README.md) + find docs/firmware/ -name "*.bin" -type f -delete + + # Move firmware with versioned name + mv esp-smart-meter-${VERSION}.bin docs/firmware/esp-smart-meter-${VERSION}.bin + + # Create version.json with metadata pointing to versioned file + cat > docs/firmware/version.json << EOF + { + "version": "${VERSION_NUM}", + "build_timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", + "download_url": "https://aviborg.github.io/esp-smart-meter/firmware/esp-smart-meter-${VERSION}.bin", + "release_url": "https://github.com/aviborg/esp-smart-meter/releases/tag/${VERSION}" + } + EOF + + echo "Created version.json:" + cat docs/firmware/version.json + + - name: Create Pull Request with firmware update + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update firmware for release ${{ github.event.release.tag_name }}" + title: "Update firmware for release ${{ github.event.release.tag_name }}" + body: | + Automated firmware update for release ${{ github.event.release.tag_name }} + + This PR updates: + - Firmware binary: `docs/firmware/esp-smart-meter-${{ github.event.release.tag_name }}.bin` + - Version metadata: `docs/firmware/version.json` + + Old firmware files have been removed to keep the repository clean. + branch: firmware-update-${{ github.event.release.tag_name }} + delete-branch: true + base: main diff --git a/README.md b/README.md index 179471d..3753d39 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ A hard reset functions is bound to the flash button/input. If held for 5 seconds OTA functionality available, WARNING, no security features are implemented as the project assumes that the MCU is on protected local network. +**Auto-Update Feature:** The device can automatically check for new firmware releases from GitHub and update itself over-the-air. See [AUTO_UPDATE.md](docs/AUTO_UPDATE.md) for details on how to use this feature. + For first time setup and connection to your local WiFi the chip will act as a access point which can be connected to from a mobile phone. If data cannot be parsed correctly, failed data will be dumped to a log.txt file accessible through the web interface. diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/docs/AUTO_UPDATE.md b/docs/AUTO_UPDATE.md new file mode 100644 index 0000000..10cc1be --- /dev/null +++ b/docs/AUTO_UPDATE.md @@ -0,0 +1,221 @@ +# Auto-Update Feature + +## Overview + +The ESP Smart Meter now includes an automatic firmware update feature that checks for new releases from the GitHub repository and can update itself over-the-air (OTA). + +## How It Works + +### Automatic Background Checks + +The device automatically checks for new firmware releases every hour by: +1. Fetching version information from GitHub Pages (`https://aviborg.github.io/esp-smart-meter/firmware/version.json`) +2. Comparing the version with the current firmware version +3. If a newer version is available, it marks an update as available + +### GitHub Pages Hosting + +Firmware binaries are hosted on GitHub Pages for reliable OTA updates: +- **Version info**: `https://aviborg.github.io/esp-smart-meter/firmware/version.json` +- **Firmware binary**: Versioned URL from version.json (e.g., `https://aviborg.github.io/esp-smart-meter/firmware/esp-smart-meter-v1.0.0.bin`) + +**Why GitHub Pages?** +- ✅ Direct HTTPS downloads (no 302 redirects) +- ✅ Short, stable URLs that work with ESP8266 +- ✅ No time-limited signed URLs +- ✅ Reliable and maintained by GitHub +- ✅ Versioned filenames for clear history + +When you create a GitHub release, the automated workflow: +1. Builds the firmware +2. Uploads it to the release (for archival/download) +3. Creates a Pull Request to update GitHub Pages with: + - Versioned firmware: `docs/firmware/esp-smart-meter-v1.0.0.bin` + - Updated `docs/firmware/version.json` pointing to the versioned file + - Old firmware files removed automatically +4. After PR is merged, GitHub Pages serves the new firmware to ESP devices + +### Version Information + +The current firmware version is defined in `include/version.h`: +```cpp +#define FIRMWARE_VERSION "1.0.0" +``` + +### Manual Update Check + +Users can manually trigger an update check via: +- Web UI: Click the "Check for Updates" button on the main page +- API: Send a GET request to `/update/check` + +### Manual Update Trigger + +When an update is available, users can trigger the update via: +- Web UI: Click the "Update Now" button (will prompt for confirmation) +- API: Send a POST request to `/update/trigger` + +## Web Interface + +The main web interface at `http://emeter/` (or your configured hostname) displays: +- Current firmware version +- Update status (checking, up-to-date, update available, etc.) +- Latest available version (if an update exists) +- "Check for Updates" button +- "Update Now" button (only shown when update is available) + +## API Endpoints + +### GET /version.json + +Returns version information in JSON format: +```json +{ + "current_version": "1.0.0", + "latest_version": "1.1.0", + "update_available": true, + "status": "Update available: 1.1.0" +} +``` + +### GET /update/check + +Manually triggers a check for updates. Returns: +- 200: "Update check initiated" +- 500: Error message if UpdateManager is not initialized + +### POST /update/trigger + +Triggers the firmware update process. Returns: +- 200: "Update started. Device will reboot after update." +- 400: "No update available" (if no update is available) +- 500: Error message if UpdateManager is not initialized + +**Note:** After triggering an update, the device will download the new firmware and automatically reboot when complete. + +## Release Requirements + +For the auto-update feature to work, GitHub releases must: +1. Include a version tag (e.g., `v1.0.0` or `1.0.0`) +2. Include a compiled firmware binary (`.bin` file) as a release asset +3. The firmware binary should be compiled for the ESP8266 platform + +## Version Numbering + +The project uses semantic versioning (MAJOR.MINOR.PATCH): +- MAJOR: Incompatible API changes +- MINOR: Added functionality in a backwards compatible manner +- PATCH: Backwards compatible bug fixes + +## Security Considerations + +⚠️ **CRITICAL SECURITY WARNINGS:** + +### Certificate Validation + +**The current implementation disables TLS certificate validation** for both GitHub API requests and firmware downloads. This creates potential security vulnerabilities: + +1. **Man-in-the-Middle (MITM) Attacks**: Without certificate validation, an attacker on the same network could intercept the connection and: + - Serve malicious firmware updates + - Inject false version information + - Compromise the device + +2. **Current Implementation Rationale**: + - Certificate validation is disabled for simplicity and to reduce memory usage on the ESP8266 + - The design assumes the device operates on a **trusted, protected local network** + - Full certificate validation on ESP8266 can be challenging due to memory constraints + +3. **Recommended Improvements for Production**: + ```cpp + // Option 1: Certificate Fingerprint (lightweight) + client.setFingerprint("GitHub certificate SHA1 fingerprint"); + + // Option 2: Full Certificate Chain (more secure but uses more memory) + client.setCACert(github_root_ca); + + // Option 3: Certificate Pinning for GitHub API + ``` + +### Network Security + +1. **No Authentication**: The update endpoints (`/update/check`, `/update/trigger`) have no authentication + - Anyone on the local network can trigger updates + - Consider adding HTTP Basic Auth or API tokens for production + +2. **Network Isolation**: + - Deploy the device on a separate, trusted network segment + - Use firewall rules to restrict access to the device + - Consider VLANs or IoT-specific network segments + +### Firmware Integrity + +The current implementation does NOT verify: +- Firmware signatures +- Checksums or hashes +- Code signing + +**Recommended for Production**: +- Implement firmware signature verification +- Add checksum validation before applying updates +- Use signed releases with cryptographic verification + +### Additional Security Measures + +For production deployments, consider: + +1. **Update Authorization**: Add authentication to update endpoints +2. **Rate Limiting**: Prevent abuse of update check endpoints +3. **Update Windows**: Only allow updates during maintenance windows +4. **Rollback Capability**: Implement dual-partition updates with rollback +5. **Audit Logging**: Log all update attempts and their outcomes +6. **Manual Approval**: Require explicit user confirmation before updates + +## Troubleshooting + +### Update Check Fails + +If update checks fail with error code -1: +1. **Connection timeout**: The device might not be able to reach GitHub's API. Verify internet connectivity and DNS resolution. +2. **TLS/SSL handshake failure**: The ESP8266 might be experiencing issues with the HTTPS connection. The timeout and buffer settings have been configured to handle this. +3. **Memory constraints**: Ensure the device has sufficient free heap memory (at least 20KB recommended). +4. Check that GitHub API is accessible from your network +5. Review serial console output for detailed error messages + +If update checks fail with other errors: +If update checks fail with other errors: +1. Verify the device has internet connectivity +2. Check that GitHub API is accessible from your network +3. Review serial console output for error messages +4. Try increasing timeout values in UpdateManager if on slow network + +### Update Download Fails + +If firmware download fails: +1. Ensure the release includes a `.bin` file +2. Verify sufficient free space on the device +3. Check network stability + +### Device Doesn't Reboot After Update + +If the device doesn't reboot after update: +1. Manually power cycle the device +2. Check serial console for error messages +3. The update may have failed; check logs + +## Development + +### Building Releases + +When creating a new release: +1. Update the version in `include/version.h` +2. Build the firmware: `platformio run -e esp8266` +3. The firmware binary will be at `.pio/build/esp8266/firmware.bin` +4. Create a GitHub release with the version tag +5. Upload the `firmware.bin` file as a release asset + +### Testing Updates + +To test the update mechanism: +1. Build and upload firmware with version X +2. Create a GitHub release with version X+1 and a firmware binary +3. Access the web interface and check for updates +4. Trigger the update and verify it completes successfully diff --git a/docs/CREATING_RELEASES.md b/docs/CREATING_RELEASES.md new file mode 100644 index 0000000..ebfaad4 --- /dev/null +++ b/docs/CREATING_RELEASES.md @@ -0,0 +1,276 @@ +# Creating Releases with Auto-Update Support + +This guide explains how to create GitHub releases that work with the ESP Smart Meter's auto-update feature. + +## Overview + +The auto-update system uses **GitHub Pages** to host firmware binaries. When you create a release, the automated workflow: +1. Builds the firmware with the correct version +2. Uploads the binary to the GitHub release (for manual downloads) +3. Creates a Pull Request to update GitHub Pages with: + - Versioned firmware file: `docs/firmware/esp-smart-meter-v1.0.0.bin` + - Updated `docs/firmware/version.json` with version metadata +4. After PR is merged, GitHub Pages serves these files to ESP devices for OTA updates + +**Note**: Old firmware files are automatically removed to keep the repository clean. + +## Release Checklist + +Before creating a release, ensure: + +- [ ] GitHub Pages is enabled (Settings → Pages → Source: main → /docs) +- [ ] Code changes committed and pushed to main +- [ ] All tests pass (if applicable) +- [ ] Changelog/release notes prepared + +**Note**: You do NOT need to manually update `include/version.h` - the workflow does this automatically! + +## Creating a GitHub Release (Automated Build) + +### Via GitHub Web Interface (Recommended) + +1. **Navigate to Releases** + - Go to: `https://github.com/aviborg/esp-smart-meter/releases` + - Click "Draft a new release" + +2. **Choose a Tag** + - Tag version: `v1.1.0` (must start with 'v', e.g., v0.0.4, v1.0.0) + - Target: `main` branch + - Click "Create new tag" + +3. **Fill Release Information** + - Release title: `v1.1.0` or descriptive name + - Description: List changes, fixes, and new features + +4. **Publish Release** + - Choose "Set as the latest release" + - Click "Publish release" + +5. **Automated Workflow** + The GitHub Actions workflow will automatically: + - Build the firmware with the version from the tag + - Upload `esp-smart-meter-v1.1.0.bin` to the release + - Create a Pull Request with: + - Versioned firmware: `docs/firmware/esp-smart-meter-v1.1.0.bin` + - Updated `docs/firmware/version.json` + - Old firmware files removed + +6. **Merge the Pull Request** + - Review the automated PR + - Merge it to make the firmware available via GitHub Pages + - ESP devices will now detect and download the new version + +### Why Pull Requests? + +The workflow uses Pull Requests instead of direct commits because: +- ✅ Works with protected main branch +- ✅ Provides review opportunity before publishing firmware +- ✅ Creates audit trail of firmware deployments +- ✅ Can be automatically merged if desired + +### Via GitHub CLI + +```bash +# Create a release (workflow will build automatically) +gh release create v1.1.0 \ + --title "Release v1.1.0" \ + --notes "Release notes here" + +# Then merge the automated PR +gh pr merge firmware-update-v1.1.0 --auto --squash +``` + +## Manual Build (Advanced) + +## Release Naming Conventions + +### Tag Names + +The auto-update feature supports: +- `v1.0.0` (recommended) +- `1.0.0` +- `V1.0.0` + +The 'v' or 'V' prefix will be automatically stripped for version comparison. + +### Firmware File Names + +The workflow creates versioned firmware files: +- `esp-smart-meter-v1.0.0.bin` (automatically named by workflow) + +The versioned filename ensures: +- Clear version history in the repository +- Easy identification of deployed versions +- Automatic cleanup of old versions + +## Semantic Versioning + +Follow semantic versioning (MAJOR.MINOR.PATCH): + +- **MAJOR** (1.0.0 → 2.0.0): Breaking changes, incompatible API changes +- **MINOR** (1.0.0 → 1.1.0): New features, backwards compatible +- **PATCH** (1.0.0 → 1.0.1): Bug fixes, backwards compatible + +Examples: +``` +1.0.0 → 1.0.1 Bug fix release +1.0.1 → 1.1.0 New auto-update feature added +1.1.0 → 2.0.0 Major API refactor +``` + +## Release Types + +### Stable Releases + +- Use standard version tags: `v1.0.0` +- Marked as "latest release" on GitHub +- Automatically detected by all devices + +### Pre-releases (Beta/RC) + +- Tag with suffix: `v1.1.0-beta.1`, `v1.1.0-rc.1` +- Mark as "pre-release" on GitHub +- NOT automatically detected (requires code modification) + +### Development Builds + +- Not recommended for auto-update +- Use for manual testing only + +## Multi-Platform Releases (Future) + +If supporting multiple platforms (ESP8266, ESP32): + +1. **Build all variants**: + ```bash + platformio run -e esp8266 + platformio run -e esp32 + ``` + +2. **Name binaries clearly**: + - `firmware-esp8266.bin` + - `firmware-esp32.bin` + +3. **Update code to detect correct binary**: + Modify `UpdateManager::fetchLatestRelease()` to filter by platform + +## Release Notes Template + +```markdown +## What's New in v1.1.0 + +### New Features +- Auto-update functionality for OTA firmware updates +- Web UI for version management + +### Improvements +- Enhanced error handling in serial parser +- Optimized memory usage + +### Bug Fixes +- Fixed issue #123: Memory leak in JSON parser +- Fixed issue #124: WiFi reconnection bug + +### Breaking Changes +- None + +### Security +- Added security documentation for OTA updates +- Note: Certificate validation disabled (see docs/AUTO_UPDATE.md) + +## Installation +1. Download `firmware.bin` from assets below +2. Flash via OTA or USB according to installation guide +3. Or use the auto-update feature if already running v1.0.0+ + +## Upgrade Notes +- Safe to upgrade from v1.0.0 +- No configuration changes required +- Device will reboot after update + +## Known Issues +- Update check requires internet connectivity +- See docs/AUTO_UPDATE.md for security considerations +``` + +## Verification After Release + +1. **Check release is published**: + - Visit releases page + - Verify "latest" badge is shown + - Verify .bin file is attached + +2. **Test API access**: + ```bash + curl https://api.github.com/repos/aviborg/esp-smart-meter/releases/latest + ``` + Should return JSON with your new release + +3. **Test on device**: + - Navigate to web UI + - Click "Check for Updates" + - Should detect new version + - (Optional) Trigger update to verify + +## Troubleshooting + +### Release not detected by devices + +- Verify release is marked as "latest" +- Check version tag format (should be `vX.Y.Z` or `X.Y.Z`) +- Ensure .bin file is attached +- Wait a few minutes for GitHub CDN to propagate + +### Binary file too large + +- GitHub release assets have size limits (2GB per file, 2GB per release) +- ESP8266 firmware should be <1MB (typically ~300-500KB) +- If too large, review build flags and strip debug symbols + +### Wrong binary downloaded + +- Verify only one .bin file per platform in release +- Check `UpdateManager::fetchLatestRelease()` logic +- Add platform-specific filtering if needed + +## Automation (Future Enhancement) + +Consider setting up GitHub Actions to: +- Automatically build firmware on tag push +- Run tests before release +- Upload binaries to release +- Generate changelog from commits + +Example workflow structure: +```yaml +name: Release +on: + push: + tags: + - 'v*' +jobs: + build-and-release: + - Build firmware + - Run tests + - Create release + - Upload binaries +``` + +## Rollback Procedure + +If a release has issues: + +1. **Create hotfix release**: + - Fix the issue + - Increment patch version (e.g., 1.1.0 → 1.1.1) + - Release normally + +2. **Un-latest a release** (not recommended): + - Edit the problematic release + - Uncheck "Set as the latest release" + - Mark a previous release as latest + +3. **Delete release** (last resort): + - Only if not yet widely deployed + - Devices already updated will remain on that version + - Create new release with higher version number diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..527d94c --- /dev/null +++ b/docs/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,284 @@ +# Auto-Update Feature - Implementation Summary + +## Overview + +This PR adds automatic firmware update capability to the ESP Smart Meter, allowing devices to check for and install new firmware releases from GitHub automatically. + +## What's New + +### Core Functionality + +1. **Automatic Update Checks** + - Device checks for updates every hour + - Non-blocking checks integrated into main loop schedule + - Compares GitHub release versions with current firmware + +2. **Manual Update Control** + - Web UI displays current version and update status + - "Check for Updates" button for manual checks + - "Update Now" button when updates are available + - Confirmation dialog before updating + +3. **RESTful API** + - `GET /version.json` - Returns version info and update status + - `GET /update/check` - Triggers manual update check + - `POST /update/trigger` - Initiates firmware download and installation + +### Implementation Details + +**UpdateManager Class** (`src/UpdateManager.h/cpp`) +- Manages GitHub API communication +- Performs semantic version comparison +- Handles firmware download via ESP8266HTTPUpdate +- Rate-limits update checks (1 hour minimum interval) + +**Version Tracking** (`include/version.h`) +- Single source of truth for firmware version +- Simple `#define FIRMWARE_VERSION "1.0.0"` + +**Web Integration** +- Modified `AmsWebServer` to expose update endpoints +- Enhanced web UI with version display and update controls +- JavaScript functions for version checking and updates + +## Files Changed + +### Added Files +``` +include/version.h - Version definition +src/UpdateManager.h - UpdateManager class header +src/UpdateManager.cpp - UpdateManager implementation +docs/AUTO_UPDATE.md - User documentation +docs/TESTING_AUTO_UPDATE.md - Testing procedures +docs/CREATING_RELEASES.md - Release creation guide +``` + +### Modified Files +``` +src/main.cpp - Integrated UpdateManager into loop +src/web/AmsWebServer.h - Added update endpoints +src/web/AmsWebServer.cpp - Implemented update handlers +web/index.html - Added version info section +web/readdata.js - Version check JavaScript +web/styles.css - Version UI styles +README.md - Mentioned auto-update feature +``` + +## How It Works + +### Update Check Flow + +1. **Periodic Check** (every hour in main loop) + ``` + Device → GitHub API → Latest Release Info + Compare versions → Store result + ``` + +2. **Manual Check** (user clicks button) + ``` + User → Web UI → /update/check endpoint + Triggers immediate check → Updates display + ``` + +3. **Update Installation** (user confirms) + ``` + User → "Update Now" → /update/trigger endpoint + Download .bin from GitHub → Flash firmware → Reboot + ``` + +### Version Comparison + +Uses semantic versioning (MAJOR.MINOR.PATCH): +- Parses version strings: "1.2.3" → {1, 2, 3} +- Compares component-wise +- Supports "v" prefix: "v1.0.0" → "1.0.0" + +### GitHub Release Requirements + +For auto-update to work, releases must: +1. Have a version tag (e.g., `v1.0.0`) +2. Include a `.bin` firmware file as an asset +3. Be marked as "latest release" on GitHub + +## Security Considerations + +⚠️ **Important**: This implementation prioritizes functionality over security: + +1. **No Certificate Validation** + - HTTPS connections skip certificate validation + - Vulnerable to MITM attacks + - Acceptable for trusted local networks + +2. **No Authentication** + - Update endpoints are unauthenticated + - Anyone on local network can trigger updates + - Assumes deployment on secure network + +3. **No Signature Verification** + - Firmware files are not cryptographically signed + - No integrity verification beyond HTTPS + +**See `docs/AUTO_UPDATE.md` for detailed security discussion and recommendations.** + +## Testing + +### Automated Tests +- ✅ Code review completed +- ✅ CodeQL security scan passed (0 alerts) +- ⚠️ Build test skipped (network issues) + +### Manual Testing Required +1. Build and flash firmware to device +2. Create GitHub release with .bin file +3. Test update detection and installation +4. Verify device functionality after update + +**See `docs/TESTING_AUTO_UPDATE.md` for complete test plan.** + +## Usage + +### For Users + +1. **Check Version** + - Open web interface: `http://emeter/` + - Scroll to "Firmware Information" section + - View current version and status + +2. **Check for Updates** + - Click "Check for Updates" button + - Wait for status to update + +3. **Install Update** + - If update available, click "Update Now" + - Confirm the dialog + - Device will reboot automatically + +### For Developers + +1. **Update Version** + ```cpp + // include/version.h + #define FIRMWARE_VERSION "1.1.0" + ``` + +2. **Build Firmware** + ```bash + platformio run -e esp8266 + # Output: .pio/build/esp8266/firmware.bin + ``` + +3. **Create Release** + - Tag: `v1.1.0` + - Upload `firmware.bin` file + - Publish as latest release + +**See `docs/CREATING_RELEASES.md` for detailed instructions.** + +## API Reference + +### GET /version.json + +Returns version information in JSON format. + +**Response:** +```json +{ + "current_version": "1.0.0", + "latest_version": "1.1.0", + "update_available": true, + "status": "Update available: 1.1.0" +} +``` + +### GET /update/check + +Manually triggers an update check. + +**Response:** +``` +200 OK: "Update check initiated" +500 Error: "UpdateManager not initialized" +``` + +### POST /update/trigger + +Triggers firmware update and reboot. + +**Response:** +``` +200 OK: "Update started. Device will reboot after update." +400 Bad Request: "No update available" +500 Error: "UpdateManager not initialized" +``` + +## Configuration + +### Update Check Interval + +Default: 1 hour (3600000 ms) + +To change, edit `src/UpdateManager.h`: +```cpp +static const unsigned long UPDATE_CHECK_INTERVAL = 3600000; // milliseconds +``` + +### GitHub Repository + +To use with different repository, edit `src/UpdateManager.cpp`: +```cpp +const char* UpdateManager::GITHUB_API_URL = + "https://api.github.com/repos/YOUR_USER/YOUR_REPO/releases/latest"; +``` + +## Future Enhancements + +Potential improvements: +- [ ] Certificate validation/pinning +- [ ] Authentication for update endpoints +- [ ] Firmware signature verification +- [ ] Automatic update option (install without confirmation) +- [ ] Rollback capability +- [ ] Update scheduling (specific time windows) +- [ ] Multi-platform support (ESP8266/ESP32) +- [ ] Delta updates (smaller downloads) +- [ ] Update history/changelog display + +## Known Limitations + +1. **Internet Required**: Device needs internet access to check/download updates +2. **GitHub Dependency**: Relies on GitHub API availability +3. **Memory Constraints**: ESP8266 limited memory may affect large updates +4. **No Rollback**: Failed updates require manual recovery +5. **Serial Data Pause**: Update process temporarily pauses meter reading + +## Troubleshooting + +**"Check failed" status** +- Verify internet connectivity +- Check GitHub API accessibility +- Review serial console output + +**Update not detected** +- Ensure release has .bin file attached +- Verify release is marked as "latest" +- Check version tag format + +**Update download fails** +- Verify sufficient free space +- Check network stability +- Ensure .bin file is valid ESP8266 firmware + +## Support + +- Documentation: `docs/AUTO_UPDATE.md` +- Testing: `docs/TESTING_AUTO_UPDATE.md` +- Releases: `docs/CREATING_RELEASES.md` +- Issues: GitHub issue tracker + +## License + +Same as main project (see LICENSE file). + +## Contributors + +Implemented by GitHub Copilot on behalf of aviborg. diff --git a/docs/TESTING_AUTO_UPDATE.md b/docs/TESTING_AUTO_UPDATE.md new file mode 100644 index 0000000..3f7fe29 --- /dev/null +++ b/docs/TESTING_AUTO_UPDATE.md @@ -0,0 +1,189 @@ +# Testing the Auto-Update Feature + +## Prerequisites + +Before you can test the auto-update feature, you need to: + +1. **Build the firmware** + ```bash + platformio run -e esp8266 + ``` + The firmware binary will be at `.pio/build/esp8266/firmware.bin` + +2. **Upload initial firmware to the device** + - Connect via USB and upload with: `platformio run -e esp8266 --target upload` + - Or use OTA if already configured: `platformio run -e esp8266 --target upload --upload-port ` + +3. **Create a GitHub Release** + - Tag version: `v1.0.0` (or match the version in `include/version.h`) + - Upload the `firmware.bin` file as a release asset + +## Test Plan + +### 1. Verify Current Version Display + +1. Open the device web interface at `http://emeter/` (or your device's IP) +2. Scroll down to the "Firmware Information" section +3. Verify it shows: + - Current Version: 1.0.0 + - Status: "Not checked" (initially) + +### 2. Test Manual Update Check (No Update Available) + +1. Click the "Check for Updates" button +2. Wait ~2 seconds for the check to complete +3. Verify the status updates to "Up to date" or "Checking..." +4. If latest release is v1.0.0, status should show "Up to date" + +### 3. Test Update Detection + +1. **Increment version in code**: + - Edit `include/version.h`: Change `FIRMWARE_VERSION` to `"1.0.1"` + - Rebuild: `platformio run -e esp8266` + - Upload to device: `platformio run -e esp8266 --target upload` + +2. **Create new GitHub release**: + - Tag: `v1.1.0` + - Upload the new `firmware.bin` + +3. **Test detection**: + - Click "Check for Updates" + - Status should show: "Update available: 1.1.0" + - "Update Now" button should appear + +### 4. Test Manual Update Trigger + +⚠️ **Warning**: This will reboot the device + +1. Click the "Update Now" button +2. Confirm the dialog +3. Device should: + - Show "Update started" message + - Download firmware + - LED should blink during update + - Automatically reboot + - After reboot, version should show 1.1.0 + +### 5. Test Automatic Update Check + +1. Monitor serial console output +2. Wait for the device to run through its schedule states +3. Every hour (3600000 ms), you should see: + ``` + Checking for firmware updates... + ``` +4. If an update is available, it will be logged but NOT automatically applied + - User must manually trigger the update via web UI + +### 6. Test API Endpoints + +#### Version Info +```bash +curl http://emeter/version.json +``` +Expected response: +```json +{ + "current_version": "1.0.0", + "latest_version": "1.1.0", + "update_available": true, + "status": "Update available: 1.1.0" +} +``` + +#### Manual Check +```bash +curl http://emeter/update/check +``` +Expected: "Update check initiated" + +#### Trigger Update +```bash +curl -X POST http://emeter/update/trigger +``` +Expected: "Update started. Device will reboot after update." + +## Monitoring and Debugging + +### Serial Console + +Monitor the device's serial output at 115200 baud: +```bash +platformio device monitor -e esp8266 +``` + +Look for messages like: +``` +UpdateManager initialized +Current firmware version: 1.0.0 +Checking for firmware updates... +New version available: 1.1.0 +Starting firmware update... +Downloading from: https://github.com/... +``` + +### Common Issues + +**"Check failed" status** +- Device may not have internet access +- GitHub API may be unreachable +- Check serial console for detailed error messages + +**Update download fails** +- Verify the release has a .bin file attached +- Check device has sufficient free space +- Verify stable network connection + +**Device doesn't reboot after update** +- Check serial console for ESP8266httpUpdate error messages +- Binary may be corrupted or incompatible +- Try manual power cycle + +## Network Testing + +Test different network scenarios: + +1. **Behind firewall**: Verify GitHub API (api.github.com) is accessible +2. **Proxy network**: May need additional configuration +3. **Slow connection**: Update may timeout; monitor serial output + +## Security Testing + +⚠️ **Do not perform in production environment** + +1. **Test without HTTPS** (if supported): + - Verify the device properly handles protocol errors + +2. **Test with invalid URL**: + - Modify code to point to invalid URL + - Verify proper error handling + +3. **Test with corrupted firmware**: + - Upload a non-firmware file as .bin + - Verify device doesn't brick (should fail gracefully) + +## Rollback Testing + +If an update fails: +1. Device should remain on previous firmware version +2. Can manually upload working firmware via USB +3. OTA should still be available if update didn't complete + +## Performance Testing + +1. **Memory usage**: Monitor free heap during update check +2. **Update time**: Measure time from trigger to reboot +3. **Network bandwidth**: Monitor download speed +4. **Concurrent operations**: Verify meter reading continues during checks + +## Success Criteria + +- ✅ Current version displays correctly in web UI +- ✅ Manual update check works and detects new versions +- ✅ Update download and installation succeeds +- ✅ Device reboots with new firmware version +- ✅ Automatic periodic checks work without blocking +- ✅ All API endpoints respond correctly +- ✅ No memory leaks during update checks +- ✅ Serial output provides useful debugging information +- ✅ Device remains operational if update fails diff --git a/docs/WORKFLOW_CHANGES.md b/docs/WORKFLOW_CHANGES.md new file mode 100644 index 0000000..57aea99 --- /dev/null +++ b/docs/WORKFLOW_CHANGES.md @@ -0,0 +1,170 @@ +# Workflow Changes Summary + +## Overview + +The release workflow has been updated to use Pull Requests for firmware deployment instead of direct commits to the main branch. + +## Why This Change? + +### Problems with Direct Push Approach +1. ❌ Fails when main branch has moved ahead: `! [rejected] HEAD -> main (fetch first)` +2. ❌ Doesn't work with protected main branch +3. ❌ No review opportunity before deploying firmware +4. ❌ No audit trail of firmware deployments + +### Benefits of PR Approach +1. ✅ **Works with protected branches** - PRs can be configured to bypass protection rules +2. ✅ **Review opportunity** - Team can review before firmware goes live +3. ✅ **Audit trail** - Each deployment creates a PR with discussion/review +4. ✅ **No conflicts** - Workflow fetches latest main before creating changes +5. ✅ **Can auto-merge** - Enable auto-merge for automated deployments +6. ✅ **Transparent** - Easy to see what changed in each firmware update + +## What Changed + +### Workflow File (.github/workflows/release.yml) + +**Before:** +```yaml +- name: Commit and push firmware to repository + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/firmware/ + git commit -m "Update firmware for release ${{ github.event.release.tag_name }}" + git push origin HEAD:main +``` + +**After:** +```yaml +- name: Prepare firmware for GitHub Pages + run: | + # Fetch latest main to avoid conflicts + git fetch origin main + git checkout main + + # Clean up old firmware files + find docs/firmware/ -name "*.bin" -type f -delete + + # Copy firmware with versioned name + cp esp-smart-meter-${VERSION}.bin docs/firmware/esp-smart-meter-${VERSION}.bin + + # Create version.json pointing to versioned file + # ... (creates JSON with download_url) + +- name: Create Pull Request with firmware update + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "Update firmware for release ${{ github.event.release.tag_name }}" + title: "Update firmware for release ${{ github.event.release.tag_name }}" + branch: firmware-update-${{ github.event.release.tag_name }} + delete-branch: true +``` + +### Firmware Naming + +**Before:** +- `docs/firmware/latest.bin` (loses version information) + +**After:** +- `docs/firmware/esp-smart-meter-v1.0.0.bin` (versioned) +- Old versions automatically removed +- Clear history of what's deployed + +### Code Changes + +**UpdateManager.cpp/h:** +- Removed hardcoded `GITHUB_PAGES_FIRMWARE_URL` constant +- Now always reads `download_url` from version.json +- Works with any versioned filename + +## How to Use + +### Automatic Workflow (Recommended) + +1. Create a GitHub release (e.g., v0.0.5) +2. Workflow automatically: + - Builds firmware + - Uploads to release + - Creates PR with firmware update +3. Review and merge the PR (or enable auto-merge) +4. GitHub Pages serves the new firmware +5. ESP devices automatically detect and can install update + +### Enable Auto-Merge (Optional) + +To automatically merge firmware PRs: + +```bash +# Enable auto-merge for a specific PR +gh pr merge firmware-update-v0.0.5 --auto --squash + +# Or configure in repository settings: +# Settings → Branches → main → Require pull request reviews before merging +# Enable "Allow auto-merge" +``` + +### Protected Branch Configuration + +If using branch protection on main: + +1. Go to: Settings → Branches → main → Edit +2. Add exception for GitHub Actions bot: + - Under "Restrict who can push", add: `github-actions[bot]` + - Or enable "Allow specified actors to bypass required pull requests" + +## Testing the Workflow + +1. Create a test release (e.g., v0.0.5-test) +2. Watch the Actions tab for the workflow run +3. Verify a PR is created: `firmware-update-v0.0.5-test` +4. Check PR contents: + - New firmware file added + - version.json updated + - Old firmware files removed +5. Merge the PR +6. Verify GitHub Pages serves the new firmware +7. Test OTA update on ESP device + +## Troubleshooting + +### PR Creation Fails + +**Error:** `Resource not accessible by integration` + +**Solution:** Ensure workflow has proper permissions: +```yaml +permissions: + contents: write + pull-requests: write +``` + +### PR Cannot Be Merged + +**Error:** `Required status checks failed` + +**Solution:** +- Check if branch protection requires specific checks +- Add exception for bot-created PRs +- Or manually approve/merge the PR + +### Old Firmware Files Not Deleted + +**Issue:** Multiple .bin files in docs/firmware/ + +**Solution:** +- Manually delete old files +- Ensure workflow has correct cleanup command: + ```bash + find docs/firmware/ -name "*.bin" -type f -delete + ``` + +## Rollback + +If you need to roll back to direct push approach: + +1. Revert the workflow changes +2. Change back to direct push method +3. Note: Won't work with protected branches + +But PR approach is recommended for production use. diff --git a/docs/firmware/README.md b/docs/firmware/README.md new file mode 100644 index 0000000..5baa2d0 --- /dev/null +++ b/docs/firmware/README.md @@ -0,0 +1,46 @@ +# Firmware Directory + +This directory hosts firmware binaries for OTA (Over-The-Air) updates via GitHub Pages. + +## Files + +- `esp-smart-meter-v*.bin` - Versioned firmware binaries (e.g., `esp-smart-meter-v1.0.0.bin`) +- `version.json` - Version metadata pointing to the current firmware + +## How It Works + +1. When a new release is created, the GitHub Actions workflow builds the firmware +2. The workflow creates a Pull Request that: + - Removes old firmware binaries + - Adds the new versioned firmware: `esp-smart-meter-v1.0.0.bin` + - Updates `version.json` with version information and download URL +3. After the PR is merged, GitHub Pages serves the firmware +4. ESP devices fetch the download URL from `version.json` and update directly + +## OTA Update Process + +ESP devices: +1. Fetch version info from: + ``` + https://aviborg.github.io/esp-smart-meter/firmware/version.json + ``` + +2. Parse the JSON to get the download URL: + ```json + { + "version": "1.0.0", + "download_url": "https://aviborg.github.io/esp-smart-meter/firmware/esp-smart-meter-v1.0.0.bin", + ... + } + ``` + +3. Download firmware from the versioned URL + +## Benefits + +✅ Direct HTTPS download (no 302 redirects) +✅ Versioned filenames for clear history +✅ Works reliably with ESP8266HTTPUpdate +✅ No time-limited signed URLs +✅ Automatic cleanup of old versions +✅ Pull Request workflow works with protected branches diff --git a/platformio.ini b/platformio.ini index 559aeae..52f4e3a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,12 +28,12 @@ extra_scripts = scripts/makeweb.py monitor_speed = 115200 test_ignore = test_desktop -; Comment the 2 lines below if upload using USB/Serial cable. -; Normally it is only needed the first time programming the +; Uncomment the 2 lines below to use OTA upload. +; The first time programming the cable must be connected to the ; MCU or if the firmware is broken. ; After first time programming, OTA should be possible. -upload_protocol = espota -upload_port = emeter +;upload_protocol = espota +;upload_port = emeter [env:testnative] platform = native diff --git a/src/UpdateManager.cpp b/src/UpdateManager.cpp new file mode 100644 index 0000000..e84a084 --- /dev/null +++ b/src/UpdateManager.cpp @@ -0,0 +1,263 @@ +#include "UpdateManager.h" +#include +#include +#include "version.h" + +const char* UpdateManager::GITHUB_PAGES_VERSION_URL = "https://aviborg.github.io/esp-smart-meter/firmware/version.json"; + +UpdateManager::UpdateManager() + : currentVersion(FIRMWARE_VERSION) + , latestVersion("") + , downloadUrl("") + , updateAvailable(false) + , updateCheckInProgress(false) + , lastUpdateCheck(0) + , updateStatus("Not checked") +{ +} + +void UpdateManager::begin() { + Serial.println("UpdateManager initialized"); + Serial.print("Current firmware version: "); + Serial.println(currentVersion); +} + +void UpdateManager::checkForUpdates() { + unsigned long now = millis(); + + // Check if enough time has passed since last check + if (now - lastUpdateCheck < UPDATE_CHECK_INTERVAL && lastUpdateCheck != 0) { + return; + } + + // Don't start a new check if one is already in progress + if (updateCheckInProgress) { + return; + } + + updateCheckInProgress = true; + lastUpdateCheck = now; + + Serial.println("Checking for firmware updates..."); + updateStatus = "Checking..."; + + if (fetchLatestRelease()) { + if (compareVersions(latestVersion, currentVersion) > 0) { + updateAvailable = true; + updateStatus = "Update available: " + latestVersion; + Serial.print("New version available: "); + Serial.println(latestVersion); + } else { + updateAvailable = false; + updateStatus = "Up to date"; + Serial.println("Firmware is up to date"); + } + } else { + updateStatus = "Check failed"; + Serial.println("Failed to check for updates"); + } + + updateCheckInProgress = false; +} + +bool UpdateManager::fetchLatestRelease() { + WiFiClientSecure client; + // WARNING: setInsecure() disables certificate validation + // This is a security risk as it allows potential MITM attacks + // For production use, consider: + // 1. Using certificate fingerprint: client.setFingerprint(...) + // 2. Using full certificate validation: client.setCACert(...) + // 3. Using certificate pinning for GitHub Pages + // The current implementation assumes a trusted local network + client.setInsecure(); + + // Set timeout for connection (10 seconds) + client.setTimeout(10000); + + // Set buffer sizes for HTTPS connection + client.setBufferSizes(512, 512); + + HTTPClient https; + + // Set timeout for HTTP request (15 seconds) + https.setTimeout(15000); + + // Fetch version information from GitHub Pages + if (!https.begin(client, GITHUB_PAGES_VERSION_URL)) { + Serial.println("Failed to connect to GitHub Pages"); + return false; + } + + https.addHeader("User-Agent", "ESP-Smart-Meter"); + int httpCode = https.GET(); + + if (httpCode != HTTP_CODE_OK) { + Serial.printf("GitHub Pages request failed, error: %d\n", httpCode); + https.end(); + return false; + } + + String payload = https.getString(); + https.end(); + + // Parse JSON response + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + Serial.print("JSON parsing failed: "); + Serial.println(error.c_str()); + return false; + } + + // Extract version information + const char* version = doc["version"]; + if (!version) { + Serial.println("No version found in response"); + return false; + } + + latestVersion = String(version); + + // Get firmware download URL from version.json + const char* url = doc["download_url"]; + if (url) { + downloadUrl = String(url); + } else { + Serial.println("No download_url found in version.json"); + return false; + } + + Serial.print("Found firmware version: "); + Serial.println(latestVersion); + Serial.print("Download URL: "); + Serial.println(downloadUrl); + + return true; +} + +int UpdateManager::compareVersions(String latestVersion, String currentVersion) { + // Simple version comparison (major.minor.patch) + // Returns: 1 if latestVersion > currentVersion, -1 if latestVersion < currentVersion, 0 if equal + + // Special case: if current version is "dev", always consider update available + if (currentVersion == "dev") { + return 1; // latestVersion < currentVersion, so update is available + } + + // Strip pre-release suffixes (e.g., "1.0.0-alpha" -> "1.0.0") + String latestVersionClean = latestVersion; + String currentVersionClean = currentVersion; + + int dashPos1 = latestVersionClean.indexOf('-'); + if (dashPos1 > 0) { + latestVersionClean = latestVersionClean.substring(0, dashPos1); + } + + int dashPos2 = currentVersionClean.indexOf('-'); + if (dashPos2 > 0) { + currentVersionClean = currentVersionClean.substring(0, dashPos2); + } + + int latestVersionMajor = 0, latestVersionMinor = 0, latestVersionPatch = 0; + int currentVersionMajor = 0, currentVersionMinor = 0, currentVersionPatch = 0; + + // Parse versions safely + int parsed1 = sscanf(latestVersionClean.c_str(), "%d.%d.%d", &latestVersionMajor, &latestVersionMinor, &latestVersionPatch); + int parsed2 = sscanf(currentVersionClean.c_str(), "%d.%d.%d", ¤tVersionMajor, ¤tVersionMinor, ¤tVersionPatch); + + // If either version couldn't be parsed, consider them equal (no update) + if (parsed1 < 1 || parsed2 < 1) { + return 0; + } + + if (latestVersionMajor != currentVersionMajor) return (latestVersionMajor > currentVersionMajor) ? 1 : -1; + if (latestVersionMinor != currentVersionMinor) return (latestVersionMinor > currentVersionMinor) ? 1 : -1; + if (latestVersionPatch != currentVersionPatch) return (latestVersionPatch > currentVersionPatch) ? 1 : -1; + + return 0; +} + +bool UpdateManager::isUpdateAvailable() { + return updateAvailable; +} + +bool UpdateManager::performUpdate() { + if (!updateAvailable || downloadUrl.length() == 0) { + Serial.println("No update available or download URL not set"); + updateStatus = "No update available"; + return false; + } + + Serial.println("Starting firmware update..."); + Serial.print("Downloading from: "); + Serial.println(downloadUrl); + Serial.println("Performing update..."); + updateStatus = "Updating..."; + + // Give the ESP some breathing room before starting heavy operations + yield(); + delay(100); + + ESPhttpUpdate.setLedPin(LED_BUILTIN, LOW); + ESPhttpUpdate.rebootOnUpdate(false); // Don't auto-reboot, we'll handle it + + // CRITICAL FIX: GitHub Pages may negotiate HTTP/2 via ALPN during TLS handshake + // ESP8266HTTPUpdate library only supports HTTP/1.1 request/response format + // + // Using WiFiClientSecure with setInsecure() should prevent ALPN negotiation in BearSSL + // However, if server still sends HTTP/2, the library will fail + // + // WORKAROUND: The ESP8266 Core 3.0.2's BearSSL doesn't advertise ALPN when setInsecure() is used + // This should force GitHub Pages CDN to fall back to HTTP/1.1 + static WiFiClientSecure client; + + // Stop any existing connection to ensure clean state + client.stop(); + + // Configure for this update + client.setInsecure(); // Disables cert validation; BearSSL won't advertise ALPN/h2 + client.setTimeout(120000); // 2 minutes timeout for slow HTTPS downloads + client.setBufferSizes(1024, 1024); // Adequate buffers for HTTPS + + // Give ESP time to configure the client + yield(); + delay(100); + + // Use HTTPS URL as-is (GitHub Pages requires HTTPS, redirects HTTP to HTTPS) + t_httpUpdate_return ret = ESPhttpUpdate.update(client, downloadUrl, currentVersion); + + switch(ret) { + case HTTP_UPDATE_FAILED: + Serial.printf("Update failed. Error (%d): %s\n", + ESPhttpUpdate.getLastError(), + ESPhttpUpdate.getLastErrorString().c_str()); + updateStatus = "Update failed"; + return false; + + case HTTP_UPDATE_NO_UPDATES: + Serial.println("No update needed"); + updateStatus = "No update needed"; + return false; + + case HTTP_UPDATE_OK: + Serial.println("Update successful! Rebooting..."); + updateStatus = "Update successful"; + ESP.restart(); // Manual reboot after successful update + return true; + } + + return false; +} + +String UpdateManager::getCurrentVersion() { + return currentVersion; +} + +String UpdateManager::getLatestVersion() { + return latestVersion; +} + +String UpdateManager::getUpdateStatus() { + return updateStatus; +} diff --git a/src/UpdateManager.h b/src/UpdateManager.h new file mode 100644 index 0000000..d451b1b --- /dev/null +++ b/src/UpdateManager.h @@ -0,0 +1,36 @@ +#ifndef UPDATE_MANAGER_H +#define UPDATE_MANAGER_H + +#include +#include +#include +#include + +class UpdateManager { +public: + UpdateManager(); + void begin(); + void checkForUpdates(); + bool isUpdateAvailable(); + bool performUpdate(); + String getCurrentVersion(); + String getLatestVersion(); + String getUpdateStatus(); + +private: + bool fetchLatestRelease(); + int compareVersions(String v1, String v2); + + String currentVersion; + String latestVersion; + String downloadUrl; + bool updateAvailable; + bool updateCheckInProgress; + unsigned long lastUpdateCheck; + String updateStatus; + + static const unsigned long UPDATE_CHECK_INTERVAL = 3600000; // 1 hour in milliseconds + static const char* GITHUB_PAGES_VERSION_URL; // Firmware URL is read from version.json +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index de288c1..4e7ab56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,9 +8,11 @@ #include "web/AmsWebServer.h" #include "hw/chipSetup.h" #include "version.h" +#include "UpdateManager.h" AmsWebServer webServer; HanReader hanReader(&Serial); +UpdateManager updateManager; void setup() { Serial.begin(115200); @@ -34,8 +36,12 @@ void setup() { // Setup wifi and webserver wifiSetup(); webServer.setup(); + webServer.setUpdateManager(&updateManager); ArduinoOTA.begin(); webServer.setDataJson(hanReader.parseData()); + + // Initialize update manager + updateManager.begin(); // Flush serial buffer while(Serial.available()>0) Serial.read(); @@ -79,6 +85,10 @@ void loop() case 3: ArduinoOTA.handle(); break; + case 4: + // Check for firmware updates (runs every hour via UpdateManager) + updateManager.checkForUpdates(); + break; default: yield(); scheduleState = 0; diff --git a/src/web/AmsWebServer.cpp b/src/web/AmsWebServer.cpp index ebf22a8..a7c43e1 100644 --- a/src/web/AmsWebServer.cpp +++ b/src/web/AmsWebServer.cpp @@ -2,12 +2,13 @@ #include #include "AmsWebServer.h" #include "version.h" +#include "../UpdateManager.h" #include "root/index_html.h" #include "root/styles_css.h" #include "root/readdata_js.h" -AmsWebServer::AmsWebServer() { +AmsWebServer::AmsWebServer() : updateManager(nullptr) { } AmsWebServer::~AmsWebServer() { @@ -21,6 +22,8 @@ void AmsWebServer::setup() { server.on("/log.txt", HTTP_GET, std::bind(&AmsWebServer::logTxt, this)); server.on("/raw.dat", HTTP_GET, std::bind(&AmsWebServer::rawData, this)); server.on("/version.json", HTTP_GET, std::bind(&AmsWebServer::versionJson, this)); + server.on("/update/check", HTTP_GET, std::bind(&AmsWebServer::updateCheck, this)); + server.on("/update/trigger", HTTP_POST, std::bind(&AmsWebServer::updateTrigger, this)); server.begin(); // Web server start } @@ -36,6 +39,10 @@ void AmsWebServer::setRawData(String str){ rawDataStr = str; } +void AmsWebServer::setUpdateManager(UpdateManager* mgr){ + updateManager = mgr; +} + void AmsWebServer::indexHtml() { String html = String((const __FlashStringHelper*) INDEX_HTML); server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); @@ -87,10 +94,25 @@ void AmsWebServer::rawData() { } void AmsWebServer::versionJson() { - StaticJsonDocument<128> doc; + DynamicJsonDocument doc(512); + + // Add basic version info doc["version"] = FIRMWARE_VERSION; doc["build_timestamp"] = BUILD_TIMESTAMP; + // Add update manager status + if (updateManager) { + doc["current_version"] = updateManager->getCurrentVersion(); + doc["latest_version"] = updateManager->getLatestVersion(); + doc["update_available"] = updateManager->isUpdateAvailable(); + doc["status"] = updateManager->getUpdateStatus(); + } else { + doc["current_version"] = FIRMWARE_VERSION; + doc["latest_version"] = "unknown"; + doc["update_available"] = false; + doc["status"] = "UpdateManager not initialized"; + } + String jsonStr; serializeJson(doc, jsonStr); @@ -98,4 +120,31 @@ void AmsWebServer::versionJson() { server.sendHeader("Access-Control-Allow-Origin", "*"); server.setContentLength(jsonStr.length()); server.send(200, "application/json", jsonStr); +} + +void AmsWebServer::updateCheck() { + if (updateManager) { + updateManager->checkForUpdates(); + server.send(200, "text/plain", "Update check initiated"); + } else { + server.send(500, "text/plain", "UpdateManager not initialized"); + } +} + +void AmsWebServer::updateTrigger() { + if (!updateManager) { + server.send(500, "text/plain", "UpdateManager not initialized"); + return; + } + + if (!updateManager->isUpdateAvailable()) { + server.send(400, "text/plain", "No update available"); + return; + } + + server.send(200, "text/plain", "Update started. Device will reboot after update."); + + // Perform update (this will reboot the device) + // The UpdateManager will handle the timing internally + updateManager->performUpdate(); } \ No newline at end of file diff --git a/src/web/AmsWebServer.h b/src/web/AmsWebServer.h index f2ab116..09cd127 100644 --- a/src/web/AmsWebServer.h +++ b/src/web/AmsWebServer.h @@ -4,6 +4,8 @@ #include #include +class UpdateManager; // Forward declaration + class AmsWebServer { public: @@ -13,8 +15,10 @@ class AmsWebServer void loop(); void setDataJson(String str); void setRawData(String str); + void setUpdateManager(UpdateManager* mgr); private: ESP8266WebServer server; + UpdateManager* updateManager; void indexHtml(); void stylesCss(); void readdataJs(); @@ -22,6 +26,8 @@ class AmsWebServer void logTxt(); void rawData(); void versionJson(); + void updateCheck(); + void updateTrigger(); String rawDataStr; String dataJsonStr; }; diff --git a/web/index.html b/web/index.html index 5b5f9ca..4376f4b 100644 --- a/web/index.html +++ b/web/index.html @@ -6,8 +6,9 @@ - +
+
diff --git a/web/readdata.js b/web/readdata.js index 553f987..b2c902b 100644 --- a/web/readdata.js +++ b/web/readdata.js @@ -30,4 +30,66 @@ function parseJSONData(meterObject) { } } return text; +} + +async function checkVersion() { + try { + let response = await fetch('/version.json'); + let versionData = await response.json(); + displayVersionInfo(versionData); + } catch (e) { + console.log('Error fetching version:', e); + } +} + +// Initialize version check on load and set up periodic checks +function initVersionCheck() { + checkVersion(); + // Check again every 5 minutes + setInterval(checkVersion, 300000); +} + +function displayVersionInfo(data) { + let html = '

Firmware Information

'; + html += '
Current Version: ' + data.current_version + '
'; + html += '
Status: ' + data.status + '
'; + + if (data.update_available) { + html += '
New Version Available: ' + data.latest_version + '
'; + html += ''; + } + + html += ''; + + document.getElementById('version-info').innerHTML = html; +} + +async function manualCheckUpdate() { + try { + await fetch('/update/check'); + // Wait a moment for the check to complete, then refresh version info + setTimeout(async () => { + let response = await fetch('/version.json'); + let versionData = await response.json(); + displayVersionInfo(versionData); + }, 2000); + } catch (e) { + console.log('Error checking for updates:', e); + alert('Failed to check for updates'); + } +} + +async function triggerUpdate() { + if (!confirm('Are you sure you want to update the firmware? The device will reboot.')) { + return; + } + + try { + let response = await fetch('/update/trigger', { method: 'POST' }); + let message = await response.text(); + alert(message); + } catch (e) { + console.log('Error triggering update:', e); + alert('Failed to trigger update'); + } } \ No newline at end of file diff --git a/web/styles.css b/web/styles.css index 3819d6d..d630e34 100644 --- a/web/styles.css +++ b/web/styles.css @@ -32,4 +32,53 @@ body { border-style: solid; border-color: grey; border-radius: 8px; +} + +.version-container { + margin-top: 20px; + padding: 15px; + background-color: #f0f0f0; + border-width: 1px; + border-style: solid; + border-color: grey; + border-radius: 8px; +} + +.version-container h3 { + margin-top: 0; +} + +.version-field { + padding: 5px; + margin: 5px 0; +} + +.update-button { + background-color: #4CAF50; + color: white; + padding: 10px 20px; + margin: 10px 5px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.update-button:hover { + background-color: #45a049; +} + +.check-button { + background-color: #008CBA; + color: white; + padding: 10px 20px; + margin: 10px 5px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.check-button:hover { + background-color: #007399; } \ No newline at end of file