Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
914752cccd Simplify bootloader version detection to use rollback capability only
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 21:28:38 +00:00
copilot-swe-agent[bot]
81ef4b5a8c Fix bootloader description reading with proper ESP-IDF implementation
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 21:13:58 +00:00
copilot-swe-agent[bot]
717e4d02f5 Improve bootloader description reading with better search and enhanced debugging
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 20:52:07 +00:00
Will Tatam
4d06700d77 Version can only be fetched for ESP32 and path needs to not be path inside existing /json 2025-09-03 21:28:48 +01:00
copilot-swe-agent[bot]
8f06cdaaac Remove WLED_DISABLE_OTA guard from /json/bootloader endpoint
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 20:19:43 +00:00
copilot-swe-agent[bot]
3b1b3578f0 Fix ESP32 build error by removing unnecessary esp_app_desc.h include
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 19:25:42 +00:00
copilot-swe-agent[bot]
956c1f5e6d Remove fallback bootloader detection logic as requested
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 19:12:37 +00:00
copilot-swe-agent[bot]
a4b9da6142 Implement proper bootloader description reading with esp_ota_get_bootloader_description()
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 19:03:27 +00:00
copilot-swe-agent[bot]
bf88f29ed8 Fix bootloader version detection to read actual flash memory
- Replace ESP-IDF version heuristics with direct flash memory reading
- Read bootloader binary from standard location (0x1000) on ESP32
- Use esp_flash_read() to access actual bootloader data from flash
- Still maintain fallback to ESP-IDF heuristics if flash read fails
- Add proper error handling and debug logging
- Addresses feedback that previous implementation only checked platform capabilities

Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-03 18:54:59 +00:00
copilot-swe-agent[bot]
64d0fabdd4 Add bootloader info endpoint, documentation, and improve error messages
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-02 08:14:24 +00:00
copilot-swe-agent[bot]
6e4db40382 Implement bootloader version checking for OTA updates
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
2025-09-02 08:09:20 +00:00
copilot-swe-agent[bot]
822660ed43 Initial plan 2025-09-02 07:56:56 +00:00
55 changed files with 2279 additions and 476 deletions

View File

@@ -8,7 +8,7 @@ Always reference these instructions first and fallback to search or bash command
### Initial Setup
- Install Node.js 20+ (specified in `.nvmrc`): Check your version with `node --version`
- Install dependencies: `npm ci` (takes ~5 seconds)
- Install dependencies: `npm install` (takes ~5 seconds)
- Install PlatformIO for hardware builds: `pip install -r requirements.txt` (takes ~60 seconds)
### Build and Test Workflow
@@ -135,4 +135,4 @@ The GitHub Actions workflow:
4. Compiles firmware for multiple hardware targets
5. Uploads build artifacts
Match this workflow in your local development to ensure CI success.
Match this workflow in your local development to ensure CI success.

View File

@@ -27,8 +27,6 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
- name: Update Nightly Release
uses: andelf/nightly-release@main
env:

View File

@@ -24,9 +24,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
sinceTag: v0.15.0
maxIssues: 500
# Exclude issues that were closed without resolution from changelog
exclude-labels: 'stale,wontfix,duplicate,invalid'
maxIssues: 500
- name: Create draft release
uses: softprops/action-gh-release@v1
with:

View File

@@ -0,0 +1,82 @@
# Bootloader Compatibility Checking
As of WLED 0.16, the firmware includes bootloader version checking to prevent incompatible OTA updates that could cause boot loops.
## Background
ESP32 devices use different bootloader versions:
- **V2 Bootloaders**: Legacy bootloaders (ESP-IDF < 4.4)
- **V3 Bootloaders**: Intermediate bootloaders (ESP-IDF 4.4+)
- **V4 Bootloaders**: Modern bootloaders (ESP-IDF 5.0+) with rollback support
WLED 0.16+ requires V4 bootloaders for full compatibility and safety features.
## Checking Your Bootloader Version
### Method 1: Web Interface
Visit your WLED device at: `http://your-device-ip/json/bootloader`
This will return JSON like:
```json
{
"version": 4,
"rollback_capable": true,
"esp_idf_version": 50002
}
```
### Method 2: Serial Console
Enable debug output and look for bootloader version messages during startup.
## OTA Update Behavior
When uploading firmware via OTA:
1. **Compatible Bootloader**: Update proceeds normally
2. **Incompatible Bootloader**: Update is blocked with error message:
> "Bootloader incompatible! Please update to a newer bootloader first."
3. **No Metadata**: Update proceeds (for backward compatibility with older firmware)
## Upgrading Your Bootloader
If you have an incompatible bootloader, you have several options:
### Option 1: Serial Flash (Recommended)
Use the [WLED web installer](https://install.wled.me) to flash via USB cable. This will install the latest bootloader and firmware.
### Option 2: Staged Update
1. First update to WLED 0.15.x (which supports your current bootloader)
2. Then update to WLED 0.16+ (0.15.x may include bootloader update)
### Option 3: ESP Tool
Use esptool.py to manually flash a new bootloader (advanced users only).
## For Firmware Builders
When building custom firmware that requires V4 bootloader:
```bash
# Add bootloader requirement to your binary
python3 tools/add_bootloader_metadata.py firmware.bin 4
```
## Technical Details
- Metadata format: ASCII string `WLED_BOOTLOADER:X` where X is required version (1-9)
- Checked in first 512 bytes of uploaded firmware
- Uses ESP-IDF version and rollback capability to detect current bootloader
- Backward compatible with firmware without metadata
## Troubleshooting
**Error: "Bootloader incompatible!"**
- Use web installer to update via USB
- Or use staged update through 0.15.x
**How to check if I need an update?**
- Visit `/json/bootloader` endpoint
- If version < 4, you may need to update for future firmware
**Can I force an update?**
- Not recommended - could brick your device
- Use proper upgrade path instead

View File

@@ -1,6 +1,5 @@
Import("env")
import shutil
import os
node_ex = shutil.which("node")
# Check if Node.js is installed and present in PATH if it failed, abort the build
@@ -13,21 +12,6 @@ else:
print('\x1b[6;33;42m' + 'Installing node packages' + '\x1b[0m')
env.Execute("npm ci")
# Extract the release name from build flags
release_name = "Custom"
build_flags = env.get("BUILD_FLAGS", [])
for flag in build_flags:
if 'WLED_RELEASE_NAME=' in flag:
# Extract the release name, remove quotes and handle different formats
parts = flag.split('WLED_RELEASE_NAME=')
if len(parts) > 1:
release_name = parts[1].split()[0].strip('\"\\')
break
# Set environment variable for cdata.js to use
os.environ['WLED_RELEASE_NAME'] = release_name
print(f'Building web UI with release name: {release_name}')
# Call the bundling script
exitCode = env.Execute("npm run build")

View File

@@ -143,7 +143,6 @@ lib_deps =
makuna/NeoPixelBus @ 2.8.3
#https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta
https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2
marvinroger/AsyncMqttClient @ 0.9.0
# for I2C interface
;Wire
# ESP-NOW library

View File

@@ -63,7 +63,6 @@ See [here](https://kno.wled.ge/basics/compatible-hardware)!
Licensed under the EUPL v1.2 license
Credits [here](https://kno.wled.ge/about/contributors/)!
CORS proxy by [Corsfix](https://corsfix.com/)
Join the Discord server to discuss everything about WLED!

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
Simple script to add bootloader requirement metadata to WLED binary files.
This adds a metadata tag that the OTA handler can detect.
Usage: python add_bootloader_metadata.py <binary_file> <required_version>
Example: python add_bootloader_metadata.py firmware.bin 4
"""
import sys
import os
def add_bootloader_metadata(binary_file, required_version):
"""Add bootloader metadata to a binary file"""
if not os.path.exists(binary_file):
print(f"Error: File {binary_file} does not exist")
return False
# Validate version
try:
version = int(required_version)
if version < 1 or version > 9:
print("Error: Bootloader version must be between 1 and 9")
return False
except ValueError:
print("Error: Bootloader version must be a number")
return False
# Create metadata string
metadata = f"WLED_BOOTLOADER:{version}"
# Check if metadata already exists
try:
with open(binary_file, 'rb') as f:
content = f.read()
if metadata.encode('ascii') in content:
print(f"File already contains bootloader v{version} requirement")
return True
# Check for any bootloader metadata
if b"WLED_BOOTLOADER:" in content:
print("Warning: File already contains bootloader metadata. Adding new requirement.")
except Exception as e:
print(f"Error reading file: {e}")
return False
# Append metadata to file
try:
with open(binary_file, 'ab') as f:
f.write(metadata.encode('ascii'))
print(f"Successfully added bootloader v{version} requirement to {binary_file}")
return True
except Exception as e:
print(f"Error writing to file: {e}")
return False
def main():
if len(sys.argv) != 3:
print("Usage: python add_bootloader_metadata.py <binary_file> <required_version>")
print("Example: python add_bootloader_metadata.py firmware.bin 4")
sys.exit(1)
binary_file = sys.argv[1]
required_version = sys.argv[2]
if add_bootloader_metadata(binary_file, required_version):
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
# Bootloader Metadata Tool
This tool adds bootloader version requirements to WLED firmware binaries to prevent incompatible OTA updates.
## Usage
```bash
python3 tools/add_bootloader_metadata.py <binary_file> <required_version>
```
Example:
```bash
python3 tools/add_bootloader_metadata.py firmware.bin 4
```
## Bootloader Versions
- **Version 2**: Legacy bootloader (ESP-IDF < 4.4)
- **Version 3**: Intermediate bootloader (ESP-IDF 4.4+)
- **Version 4**: Modern bootloader (ESP-IDF 5.0+) with rollback support
## How It Works
1. The script appends a metadata tag `WLED_BOOTLOADER:X` to the binary file
2. During OTA upload, WLED checks the first 512 bytes for this metadata
3. If found, WLED compares the required version with the current bootloader
4. The update is blocked if the current bootloader is incompatible
## Metadata Format
The metadata is a simple ASCII string: `WLED_BOOTLOADER:X` where X is the required bootloader version (1-9).
This approach was chosen over filename-based detection because users often rename firmware files.
## Integration with Build Process
To automatically add metadata during builds, add this to your platformio.ini:
```ini
[env:your_env]
extra_scripts = post:add_metadata.py
```
Create `add_metadata.py`:
```python
Import("env")
import subprocess
def add_metadata(source, target, env):
firmware_path = str(target[0])
subprocess.run(["python3", "tools/add_bootloader_metadata.py", firmware_path, "4"])
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", add_metadata)
```

View File

@@ -95,11 +95,6 @@ function adoptVersionAndRepo(html) {
if (version) {
html = html.replaceAll("##VERSION##", version);
}
// Replace ##RELEASE## with the actual release name from build environment
const releaseName = process.env.WLED_RELEASE_NAME || 'Custom';
html = html.replaceAll("##RELEASE##", releaseName);
return html;
}

View File

@@ -1,5 +0,0 @@
{
"name": "udp_name_sync",
"build": { "libArchive": false },
"dependencies": {}
}

View File

@@ -1,85 +0,0 @@
#include "wled.h"
class UdpNameSync : public Usermod {
private:
bool enabled = false;
char segmentName[WLED_MAX_SEGNAME_LEN] = {0};
static constexpr uint8_t kPacketType = 200; // custom usermod packet type
static const char _name[];
static const char _enabled[];
public:
/**
* Enable/Disable the usermod
*/
inline void enable(bool value) { enabled = value; }
/**
* Get usermod enabled/disabled state
*/
inline bool isEnabled() const { return enabled; }
void setup() override {
// Enabled when this usermod is compiled, set to false if you prefer runtime opt-in
enable(true);
}
void loop() override {
if (!enabled) return;
if (!WLED_CONNECTED) return;
if (!udpConnected) return;
Segment& mainseg = strip.getMainSegment();
if (segmentName[0] == '\0' && !mainseg.name) return; //name was never set, do nothing
const char* curName = mainseg.name ? mainseg.name : "";
if (strncmp(curName, segmentName, sizeof(segmentName)) == 0) return; // same name, do nothing
IPAddress broadcastIp = uint32_t(Network.localIP()) | ~uint32_t(Network.subnetMask());
byte udpOut[WLED_MAX_SEGNAME_LEN + 2];
udpOut[0] = kPacketType; // custom usermod packet type (avoid 0..5 used by core protocols)
if (segmentName[0] != '\0' && !mainseg.name) { // name cleared
notifierUdp.beginPacket(broadcastIp, udpPort);
segmentName[0] = '\0';
DEBUG_PRINTLN(F("UdpNameSync: sending empty name"));
udpOut[1] = 0; // explicit empty string
notifierUdp.write(udpOut, 2);
notifierUdp.endPacket();
return;
}
notifierUdp.beginPacket(broadcastIp, udpPort);
DEBUG_PRINT(F("UdpNameSync: saving segment name "));
DEBUG_PRINTLN(curName);
strlcpy(segmentName, curName, sizeof(segmentName));
strlcpy((char *)&udpOut[1], segmentName, sizeof(udpOut) - 1); // leave room for header byte
size_t nameLen = strnlen((char *)&udpOut[1], sizeof(udpOut) - 1);
notifierUdp.write(udpOut, 2 + nameLen);
notifierUdp.endPacket();
DEBUG_PRINT(F("UdpNameSync: Sent segment name : "));
DEBUG_PRINTLN(segmentName);
return;
}
bool onUdpPacket(uint8_t * payload, size_t len) override {
DEBUG_PRINT(F("UdpNameSync: Received packet"));
if (!enabled) return false;
if (receiveDirect) return false;
if (len < 2) return false; // need type + at least 1 byte for name (can be 0)
if (payload[0] != kPacketType) return false;
Segment& mainseg = strip.getMainSegment();
char tmp[WLED_MAX_SEGNAME_LEN] = {0};
size_t copyLen = len - 1;
if (copyLen > sizeof(tmp) - 1) copyLen = sizeof(tmp) - 1;
memcpy(tmp, &payload[1], copyLen);
tmp[copyLen] = '\0';
mainseg.setName(tmp);
DEBUG_PRINT(F("UdpNameSync: set segment name"));
return true;
}
};
static UdpNameSync udp_name_sync;
REGISTER_USERMOD(udp_name_sync);

View File

@@ -2328,7 +2328,7 @@ uint16_t mode_colortwinkle() {
}
if (cur == prev) { //fix "stuck" pixels
col = color_add(col, col);
color_add(col, col);
SEGMENT.setPixelColor(i, col);
}
else SEGMENT.setPixelColor(i, col);
@@ -3940,7 +3940,7 @@ uint16_t mode_percent(void) {
return FRAMETIME;
}
static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@!,% of fill,,,,One color;!,!;!";
static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@,% of fill,,,,One color;!,!;!";
/*

View File

@@ -107,7 +107,6 @@ Y: <input name="P${i}Y" type="number" min="0" max="255" value="0" oninput="UI()"
Sf[`P${p}H`].value = ph;
}
}
UI(); // Update the preview after generating panels
}
function expand(o,i)

View File

@@ -28,13 +28,10 @@
<h2>WLED Software Update</h2>
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">WLED ##VERSION##</span><br>
Release: <span class="sip">##RELEASE##</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<input type='checkbox' name='skipValidation' id='skipValidation'>
<label for='skipValidation'>Ignore firmware validation</label><br>
<button type="submit">Update!</button><br>
<hr class="sml">
<button id="rev" type="button" onclick="cR()">Revert update</button><br>

View File

@@ -315,7 +315,6 @@ class Usermod {
virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe)
virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic)
virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received
virtual bool onUdpPacket(uint8_t* payload, size_t len) { return false; } //fired upon UDP packet received
virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update
virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change
virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;}
@@ -355,7 +354,6 @@ namespace UsermodManager {
#ifndef WLED_DISABLE_ESPNOW
bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len);
#endif
bool onUdpPacket(uint8_t* payload, size_t len);
void onUpdateBegin(bool);
void onStateChange(uint8_t);
Usermod* lookup(uint16_t mod_id);
@@ -468,6 +466,10 @@ void handleBootLoop(); // detect and handle bootloops
#ifndef ESP8266
void bootloopCheckOTA(); // swap boot image if bootloop is detected instead of restoring config
#endif
#ifndef WLED_DISABLE_OTA
uint32_t getBootloaderVersion(); // get current bootloader version
bool isBootloaderCompatible(uint32_t required_version); // check bootloader compatibility
#endif
// RAII guard class for the JSON Buffer lock
// Modeled after std::lock_guard
class JSONBufferGuard {

View File

@@ -1,122 +0,0 @@
#include "ota_release_check.h"
#include "wled.h"
#ifdef ESP32
#include <esp_app_format.h>
#include <esp_ota_ops.h>
#endif
// Same hash function used at compile time (must match wled_custom_desc.cpp)
static uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
if (!binaryData || !extractedRelease || dataSize < 64) {
return false;
}
// Search in first 8KB only - ESP32 .rodata.wled_desc and ESP8266 .ver_number
// sections appear early in binary. 8KB should be sufficient for metadata discovery
// while minimizing processing time for large firmware files.
const size_t search_limit = min(dataSize, (size_t)8192);
for (size_t offset = 0; offset <= search_limit - sizeof(wled_custom_desc_t); offset++) {
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + offset);
// Check for magic number
if (custom_desc->magic == WLED_CUSTOM_DESC_MAGIC) {
// Found potential match, validate version
if (custom_desc->version != WLED_CUSTOM_DESC_VERSION) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but version mismatch: %u\n"),
offset, custom_desc->version);
continue;
}
// Validate hash using same algorithm as compile-time
uint32_t expected_hash = djb2_hash(custom_desc->release_name);
if (custom_desc->crc32 != expected_hash) {
DEBUG_PRINTF_P(PSTR("Found WLED structure at offset %u but hash mismatch\n"), offset);
continue;
}
// Valid structure found
strncpy(extractedRelease, custom_desc->release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
extractedRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
#ifdef ESP32
DEBUG_PRINTF_P(PSTR("Extracted ESP32 release name from .rodata.wled_desc section at offset %u: '%s'\n"),
offset, extractedRelease);
#else
DEBUG_PRINTF_P(PSTR("Extracted ESP8266 release name from .ver_number section at offset %u: '%s'\n"),
offset, extractedRelease);
#endif
return true;
}
}
DEBUG_PRINTLN(F("No WLED custom description found in binary"));
return false;
}
bool validateReleaseCompatibility(const char* extractedRelease) {
if (!extractedRelease || strlen(extractedRelease) == 0) {
return false;
}
// Simple string comparison - releases must match exactly
bool match = strcmp(releaseString, extractedRelease) == 0;
DEBUG_PRINTF_P(PSTR("Release compatibility check: current='%s', uploaded='%s', match=%s\n"),
releaseString, extractedRelease, match ? "YES" : "NO");
return match;
}
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage) {
// Clear error message
if (errorMessage) {
errorMessage[0] = '\0';
}
// Ensure our custom description structure is referenced (prevents optimization)
const wled_custom_desc_t* local_desc = getWledCustomDesc();
(void)local_desc; // Suppress unused variable warning
// If user chose to ignore release check, allow OTA
if (skipValidation) {
DEBUG_PRINTLN(F("OTA release check bypassed by user"));
return true;
}
// Try to extract release name directly from binary data
char extractedRelease[WLED_RELEASE_NAME_MAX_LEN];
bool hasCustomDesc = extractReleaseFromCustomDesc(binaryData, dataSize, extractedRelease);
if (!hasCustomDesc) {
// No custom description - this could be a legacy binary
if (errorMessage) {
strcpy(errorMessage, "Binary has no release compatibility metadata. Check 'Ignore validation' to proceed.");
}
DEBUG_PRINTLN(F("OTA blocked: No custom description found"));
return false;
}
// Validate compatibility using extracted release name
if (!validateReleaseCompatibility(extractedRelease)) {
if (errorMessage) {
snprintf(errorMessage, 127, "Release mismatch: current='%s', uploaded='%s'. Check 'Ignore validation' to proceed.",
releaseString, extractedRelease);
}
DEBUG_PRINTF_P(PSTR("OTA blocked: Release mismatch current='%s', uploaded='%s'\n"),
releaseString, extractedRelease);
return false;
}
DEBUG_PRINTLN(F("OTA allowed: Release names match"));
return true;
}

View File

@@ -1,64 +0,0 @@
#ifndef WLED_OTA_RELEASE_CHECK_H
#define WLED_OTA_RELEASE_CHECK_H
/*
* OTA Release Compatibility Checking using ESP-IDF Custom Description Section
* Functions to extract and validate release names from uploaded binary files using embedded metadata
*/
#include <Arduino.h>
#ifdef ESP32
#include <esp_app_format.h>
#endif
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
#define WLED_CUSTOM_DESC_VERSION 1
#define WLED_RELEASE_NAME_MAX_LEN 48
/**
* WLED Custom Description Structure
* This structure is embedded in platform-specific sections at a fixed offset
* in ESP32/ESP8266 binaries, allowing extraction without modifying the binary format
*/
typedef struct {
uint32_t magic; // Magic number to identify WLED custom description
uint32_t version; // Structure version for future compatibility
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
uint32_t crc32; // CRC32 of the above fields for integrity check
} __attribute__((packed)) wled_custom_desc_t;
/**
* Extract release name from binary using ESP-IDF custom description section
* @param binaryData Pointer to binary file data
* @param dataSize Size of binary data in bytes
* @param extractedRelease Buffer to store extracted release name (should be at least WLED_RELEASE_NAME_MAX_LEN bytes)
* @return true if release name was found and extracted, false otherwise
*/
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
/**
* Validate if extracted release name matches current release
* @param extractedRelease Release name from uploaded binary
* @return true if releases match (OTA should proceed), false if they don't match
*/
bool validateReleaseCompatibility(const char* extractedRelease);
/**
* Check if OTA should be allowed based on release compatibility using custom description
* @param binaryData Pointer to binary file data (not modified)
* @param dataSize Size of binary data in bytes
* @param skipValidation If true, skip release validation
* @param errorMessage Buffer to store error message if validation fails (should be at least 128 bytes)
* @return true if OTA should proceed, false if it should be blocked
*/
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool skipValidation, char* errorMessage);
/**
* Get pointer to the embedded custom description structure
* This ensures the structure is referenced and not optimized out
* @return pointer to the custom description structure
*/
const wled_custom_desc_t* getWledCustomDesc();
#endif // WLED_OTA_RELEASE_CHECK_H

View File

@@ -0,0 +1,877 @@
#include "AsyncMqttClient.hpp"
AsyncMqttClient::AsyncMqttClient()
: _connected(false)
, _connectPacketNotEnoughSpace(false)
, _disconnectFlagged(false)
, _tlsBadFingerprint(false)
, _lastClientActivity(0)
, _lastServerActivity(0)
, _lastPingRequestTime(0)
, _host(nullptr)
, _useIp(false)
#if ASYNC_TCP_SSL_ENABLED
, _secure(false)
#endif
, _port(0)
, _keepAlive(15)
, _cleanSession(true)
, _clientId(nullptr)
, _username(nullptr)
, _password(nullptr)
, _willTopic(nullptr)
, _willPayload(nullptr)
, _willPayloadLength(0)
, _willQos(0)
, _willRetain(false)
, _parsingInformation { .bufferState = AsyncMqttClientInternals::BufferState::NONE }
, _currentParsedPacket(nullptr)
, _remainingLengthBufferPosition(0)
, _nextPacketId(1) {
_client.onConnect([](void* obj, AsyncClient* c) { (static_cast<AsyncMqttClient*>(obj))->_onConnect(c); }, this);
_client.onDisconnect([](void* obj, AsyncClient* c) { (static_cast<AsyncMqttClient*>(obj))->_onDisconnect(c); }, this);
_client.onError([](void* obj, AsyncClient* c, int8_t error) { (static_cast<AsyncMqttClient*>(obj))->_onError(c, error); }, this);
_client.onTimeout([](void* obj, AsyncClient* c, uint32_t time) { (static_cast<AsyncMqttClient*>(obj))->_onTimeout(c, time); }, this);
_client.onAck([](void* obj, AsyncClient* c, size_t len, uint32_t time) { (static_cast<AsyncMqttClient*>(obj))->_onAck(c, len, time); }, this);
_client.onData([](void* obj, AsyncClient* c, void* data, size_t len) { (static_cast<AsyncMqttClient*>(obj))->_onData(c, static_cast<char*>(data), len); }, this);
_client.onPoll([](void* obj, AsyncClient* c) { (static_cast<AsyncMqttClient*>(obj))->_onPoll(c); }, this);
#ifdef ESP32
sprintf(_generatedClientId, "esp32%06x", (uint32_t)ESP.getEfuseMac());
_xSemaphore = xSemaphoreCreateMutex();
#elif defined(ESP8266)
sprintf(_generatedClientId, "esp8266%06x", (uint32_t)ESP.getChipId());
#endif
_clientId = _generatedClientId;
setMaxTopicLength(128);
}
AsyncMqttClient::~AsyncMqttClient() {
delete _currentParsedPacket;
delete[] _parsingInformation.topicBuffer;
#ifdef ESP32
vSemaphoreDelete(_xSemaphore);
#endif
}
AsyncMqttClient& AsyncMqttClient::setKeepAlive(uint16_t keepAlive) {
_keepAlive = keepAlive;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setClientId(const char* clientId) {
_clientId = clientId;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setCleanSession(bool cleanSession) {
_cleanSession = cleanSession;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setMaxTopicLength(uint16_t maxTopicLength) {
_parsingInformation.maxTopicLength = maxTopicLength;
delete[] _parsingInformation.topicBuffer;
_parsingInformation.topicBuffer = new char[maxTopicLength + 1];
return *this;
}
AsyncMqttClient& AsyncMqttClient::setCredentials(const char* username, const char* password) {
_username = username;
_password = password;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setWill(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length) {
_willTopic = topic;
_willQos = qos;
_willRetain = retain;
_willPayload = payload;
_willPayloadLength = length;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setServer(IPAddress ip, uint16_t port) {
_useIp = true;
_ip = ip;
_port = port;
return *this;
}
AsyncMqttClient& AsyncMqttClient::setServer(const char* host, uint16_t port) {
_useIp = false;
_host = host;
_port = port;
return *this;
}
#if ASYNC_TCP_SSL_ENABLED
AsyncMqttClient& AsyncMqttClient::setSecure(bool secure) {
_secure = secure;
return *this;
}
AsyncMqttClient& AsyncMqttClient::addServerFingerprint(const uint8_t* fingerprint) {
std::array<uint8_t, SHA1_SIZE> newFingerprint;
memcpy(newFingerprint.data(), fingerprint, SHA1_SIZE);
_secureServerFingerprints.push_back(newFingerprint);
return *this;
}
#endif
AsyncMqttClient& AsyncMqttClient::onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback) {
_onConnectUserCallbacks.push_back(callback);
return *this;
}
AsyncMqttClient& AsyncMqttClient::onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback) {
_onDisconnectUserCallbacks.push_back(callback);
return *this;
}
AsyncMqttClient& AsyncMqttClient::onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback) {
_onSubscribeUserCallbacks.push_back(callback);
return *this;
}
AsyncMqttClient& AsyncMqttClient::onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback) {
_onUnsubscribeUserCallbacks.push_back(callback);
return *this;
}
AsyncMqttClient& AsyncMqttClient::onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback) {
_onMessageUserCallbacks.push_back(callback);
return *this;
}
AsyncMqttClient& AsyncMqttClient::onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback) {
_onPublishUserCallbacks.push_back(callback);
return *this;
}
void AsyncMqttClient::_freeCurrentParsedPacket() {
delete _currentParsedPacket;
_currentParsedPacket = nullptr;
}
void AsyncMqttClient::_clear() {
_lastPingRequestTime = 0;
_connected = false;
_disconnectFlagged = false;
_connectPacketNotEnoughSpace = false;
_tlsBadFingerprint = false;
_freeCurrentParsedPacket();
_pendingPubRels.clear();
_pendingPubRels.shrink_to_fit();
_toSendAcks.clear();
_toSendAcks.shrink_to_fit();
_nextPacketId = 1;
_parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE;
}
/* TCP */
void AsyncMqttClient::_onConnect(AsyncClient* client) {
(void)client;
#if ASYNC_TCP_SSL_ENABLED
if (_secure && _secureServerFingerprints.size() > 0) {
SSL* clientSsl = _client.getSSL();
bool sslFoundFingerprint = false;
for (std::array<uint8_t, SHA1_SIZE> fingerprint : _secureServerFingerprints) {
if (ssl_match_fingerprint(clientSsl, fingerprint.data()) == SSL_OK) {
sslFoundFingerprint = true;
break;
}
}
if (!sslFoundFingerprint) {
_tlsBadFingerprint = true;
_client.close(true);
return;
}
}
#endif
char fixedHeader[5];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.CONNECT;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.CONNECT_RESERVED;
uint16_t protocolNameLength = 4;
char protocolNameLengthBytes[2];
protocolNameLengthBytes[0] = protocolNameLength >> 8;
protocolNameLengthBytes[1] = protocolNameLength & 0xFF;
char protocolLevel[1];
protocolLevel[0] = 0x04;
char connectFlags[1];
connectFlags[0] = 0;
if (_cleanSession) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.CLEAN_SESSION;
if (_username != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.USERNAME;
if (_password != nullptr) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.PASSWORD;
if (_willTopic != nullptr) {
connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL;
if (_willRetain) connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_RETAIN;
switch (_willQos) {
case 0:
connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS0;
break;
case 1:
connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS1;
break;
case 2:
connectFlags[0] |= AsyncMqttClientInternals::ConnectFlag.WILL_QOS2;
break;
}
}
char keepAliveBytes[2];
keepAliveBytes[0] = _keepAlive >> 8;
keepAliveBytes[1] = _keepAlive & 0xFF;
uint16_t clientIdLength = strlen(_clientId);
char clientIdLengthBytes[2];
clientIdLengthBytes[0] = clientIdLength >> 8;
clientIdLengthBytes[1] = clientIdLength & 0xFF;
// Optional fields
uint16_t willTopicLength = 0;
char willTopicLengthBytes[2];
uint16_t willPayloadLength = _willPayloadLength;
char willPayloadLengthBytes[2];
if (_willTopic != nullptr) {
willTopicLength = strlen(_willTopic);
willTopicLengthBytes[0] = willTopicLength >> 8;
willTopicLengthBytes[1] = willTopicLength & 0xFF;
if (_willPayload != nullptr && willPayloadLength == 0) willPayloadLength = strlen(_willPayload);
willPayloadLengthBytes[0] = willPayloadLength >> 8;
willPayloadLengthBytes[1] = willPayloadLength & 0xFF;
}
uint16_t usernameLength = 0;
char usernameLengthBytes[2];
if (_username != nullptr) {
usernameLength = strlen(_username);
usernameLengthBytes[0] = usernameLength >> 8;
usernameLengthBytes[1] = usernameLength & 0xFF;
}
uint16_t passwordLength = 0;
char passwordLengthBytes[2];
if (_password != nullptr) {
passwordLength = strlen(_password);
passwordLengthBytes[0] = passwordLength >> 8;
passwordLengthBytes[1] = passwordLength & 0xFF;
}
uint32_t remainingLength = 2 + protocolNameLength + 1 + 1 + 2 + 2 + clientIdLength; // always present
if (_willTopic != nullptr) remainingLength += 2 + willTopicLength + 2 + willPayloadLength;
if (_username != nullptr) remainingLength += 2 + usernameLength;
if (_password != nullptr) remainingLength += 2 + passwordLength;
uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1);
uint32_t neededSpace = 1 + remainingLengthLength;
neededSpace += 2;
neededSpace += protocolNameLength;
neededSpace += 1;
neededSpace += 1;
neededSpace += 2;
neededSpace += 2;
neededSpace += clientIdLength;
if (_willTopic != nullptr) {
neededSpace += 2;
neededSpace += willTopicLength;
neededSpace += 2;
if (_willPayload != nullptr) neededSpace += willPayloadLength;
}
if (_username != nullptr) {
neededSpace += 2;
neededSpace += usernameLength;
}
if (_password != nullptr) {
neededSpace += 2;
neededSpace += passwordLength;
}
SEMAPHORE_TAKE();
if (_client.space() < neededSpace) {
_connectPacketNotEnoughSpace = true;
_client.close(true);
SEMAPHORE_GIVE();
return;
}
_client.add(fixedHeader, 1 + remainingLengthLength);
_client.add(protocolNameLengthBytes, 2);
_client.add("MQTT", protocolNameLength);
_client.add(protocolLevel, 1);
_client.add(connectFlags, 1);
_client.add(keepAliveBytes, 2);
_client.add(clientIdLengthBytes, 2);
_client.add(_clientId, clientIdLength);
if (_willTopic != nullptr) {
_client.add(willTopicLengthBytes, 2);
_client.add(_willTopic, willTopicLength);
_client.add(willPayloadLengthBytes, 2);
if (_willPayload != nullptr) _client.add(_willPayload, willPayloadLength);
}
if (_username != nullptr) {
_client.add(usernameLengthBytes, 2);
_client.add(_username, usernameLength);
}
if (_password != nullptr) {
_client.add(passwordLengthBytes, 2);
_client.add(_password, passwordLength);
}
_client.send();
_lastClientActivity = millis();
SEMAPHORE_GIVE();
}
void AsyncMqttClient::_onDisconnect(AsyncClient* client) {
(void)client;
if (!_disconnectFlagged) {
AsyncMqttClientDisconnectReason reason;
if (_connectPacketNotEnoughSpace) {
reason = AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE;
} else if (_tlsBadFingerprint) {
reason = AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT;
} else {
reason = AsyncMqttClientDisconnectReason::TCP_DISCONNECTED;
}
for (auto callback : _onDisconnectUserCallbacks) callback(reason);
}
_clear();
}
void AsyncMqttClient::_onError(AsyncClient* client, int8_t error) {
(void)client;
(void)error;
// _onDisconnect called anyway
}
void AsyncMqttClient::_onTimeout(AsyncClient* client, uint32_t time) {
(void)client;
(void)time;
// disconnection will be handled by ping/pong management
}
void AsyncMqttClient::_onAck(AsyncClient* client, size_t len, uint32_t time) {
(void)client;
(void)len;
(void)time;
}
void AsyncMqttClient::_onData(AsyncClient* client, char* data, size_t len) {
(void)client;
size_t currentBytePosition = 0;
char currentByte;
do {
switch (_parsingInformation.bufferState) {
case AsyncMqttClientInternals::BufferState::NONE:
currentByte = data[currentBytePosition++];
_parsingInformation.packetType = currentByte >> 4;
_parsingInformation.packetFlags = (currentByte << 4) >> 4;
_parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::REMAINING_LENGTH;
_lastServerActivity = millis();
switch (_parsingInformation.packetType) {
case AsyncMqttClientInternals::PacketType.CONNACK:
_currentParsedPacket = new AsyncMqttClientInternals::ConnAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onConnAck, this, std::placeholders::_1, std::placeholders::_2));
break;
case AsyncMqttClientInternals::PacketType.PINGRESP:
_currentParsedPacket = new AsyncMqttClientInternals::PingRespPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPingResp, this));
break;
case AsyncMqttClientInternals::PacketType.SUBACK:
_currentParsedPacket = new AsyncMqttClientInternals::SubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onSubAck, this, std::placeholders::_1, std::placeholders::_2));
break;
case AsyncMqttClientInternals::PacketType.UNSUBACK:
_currentParsedPacket = new AsyncMqttClientInternals::UnsubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onUnsubAck, this, std::placeholders::_1));
break;
case AsyncMqttClientInternals::PacketType.PUBLISH:
_currentParsedPacket = new AsyncMqttClientInternals::PublishPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6, std::placeholders::_7, std::placeholders::_8, std::placeholders::_9), std::bind(&AsyncMqttClient::_onPublish, this, std::placeholders::_1, std::placeholders::_2));
break;
case AsyncMqttClientInternals::PacketType.PUBREL:
_currentParsedPacket = new AsyncMqttClientInternals::PubRelPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRel, this, std::placeholders::_1));
break;
case AsyncMqttClientInternals::PacketType.PUBACK:
_currentParsedPacket = new AsyncMqttClientInternals::PubAckPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubAck, this, std::placeholders::_1));
break;
case AsyncMqttClientInternals::PacketType.PUBREC:
_currentParsedPacket = new AsyncMqttClientInternals::PubRecPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubRec, this, std::placeholders::_1));
break;
case AsyncMqttClientInternals::PacketType.PUBCOMP:
_currentParsedPacket = new AsyncMqttClientInternals::PubCompPacket(&_parsingInformation, std::bind(&AsyncMqttClient::_onPubComp, this, std::placeholders::_1));
break;
default:
break;
}
break;
case AsyncMqttClientInternals::BufferState::REMAINING_LENGTH:
currentByte = data[currentBytePosition++];
_remainingLengthBuffer[_remainingLengthBufferPosition++] = currentByte;
if (currentByte >> 7 == 0) {
_parsingInformation.remainingLength = AsyncMqttClientInternals::Helpers::decodeRemainingLength(_remainingLengthBuffer);
_remainingLengthBufferPosition = 0;
if (_parsingInformation.remainingLength > 0) {
_parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::VARIABLE_HEADER;
} else {
// PINGRESP is a special case where it has no variable header, so the packet ends right here
_parsingInformation.bufferState = AsyncMqttClientInternals::BufferState::NONE;
_onPingResp();
}
}
break;
case AsyncMqttClientInternals::BufferState::VARIABLE_HEADER:
_currentParsedPacket->parseVariableHeader(data, len, &currentBytePosition);
break;
case AsyncMqttClientInternals::BufferState::PAYLOAD:
_currentParsedPacket->parsePayload(data, len, &currentBytePosition);
break;
default:
currentBytePosition = len;
}
} while (currentBytePosition != len);
}
void AsyncMqttClient::_onPoll(AsyncClient* client) {
if (!_connected) return;
// if there is too much time the client has sent a ping request without a response, disconnect client to avoid half open connections
if (_lastPingRequestTime != 0 && (millis() - _lastPingRequestTime) >= (_keepAlive * 1000 * 2)) {
disconnect();
return;
// send ping to ensure the server will receive at least one message inside keepalive window
} else if (_lastPingRequestTime == 0 && (millis() - _lastClientActivity) >= (_keepAlive * 1000 * 0.7)) {
_sendPing();
// send ping to verify if the server is still there (ensure this is not a half connection)
} else if (_connected && _lastPingRequestTime == 0 && (millis() - _lastServerActivity) >= (_keepAlive * 1000 * 0.7)) {
_sendPing();
}
// handle to send ack packets
_sendAcks();
// handle disconnect
if (_disconnectFlagged) {
_sendDisconnect();
}
}
/* MQTT */
void AsyncMqttClient::_onPingResp() {
_freeCurrentParsedPacket();
_lastPingRequestTime = 0;
}
void AsyncMqttClient::_onConnAck(bool sessionPresent, uint8_t connectReturnCode) {
(void)sessionPresent;
_freeCurrentParsedPacket();
if (connectReturnCode == 0) {
_connected = true;
for (auto callback : _onConnectUserCallbacks) callback(sessionPresent);
} else {
for (auto callback : _onDisconnectUserCallbacks) callback(static_cast<AsyncMqttClientDisconnectReason>(connectReturnCode));
_disconnectFlagged = true;
}
}
void AsyncMqttClient::_onSubAck(uint16_t packetId, char status) {
_freeCurrentParsedPacket();
for (auto callback : _onSubscribeUserCallbacks) callback(packetId, status);
}
void AsyncMqttClient::_onUnsubAck(uint16_t packetId) {
_freeCurrentParsedPacket();
for (auto callback : _onUnsubscribeUserCallbacks) callback(packetId);
}
void AsyncMqttClient::_onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId) {
bool notifyPublish = true;
if (qos == 2) {
for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) {
if (pendingPubRel.packetId == packetId) {
notifyPublish = false;
break;
}
}
}
if (notifyPublish) {
AsyncMqttClientMessageProperties properties;
properties.qos = qos;
properties.dup = dup;
properties.retain = retain;
for (auto callback : _onMessageUserCallbacks) callback(topic, payload, properties, len, index, total);
}
}
void AsyncMqttClient::_onPublish(uint16_t packetId, uint8_t qos) {
AsyncMqttClientInternals::PendingAck pendingAck;
if (qos == 1) {
pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBACK;
pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBACK_RESERVED;
pendingAck.packetId = packetId;
_toSendAcks.push_back(pendingAck);
} else if (qos == 2) {
pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREC;
pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREC_RESERVED;
pendingAck.packetId = packetId;
_toSendAcks.push_back(pendingAck);
bool pubRelAwaiting = false;
for (AsyncMqttClientInternals::PendingPubRel pendingPubRel : _pendingPubRels) {
if (pendingPubRel.packetId == packetId) {
pubRelAwaiting = true;
break;
}
}
if (!pubRelAwaiting) {
AsyncMqttClientInternals::PendingPubRel pendingPubRel;
pendingPubRel.packetId = packetId;
_pendingPubRels.push_back(pendingPubRel);
}
_sendAcks();
}
_freeCurrentParsedPacket();
}
void AsyncMqttClient::_onPubRel(uint16_t packetId) {
_freeCurrentParsedPacket();
AsyncMqttClientInternals::PendingAck pendingAck;
pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBCOMP;
pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBCOMP_RESERVED;
pendingAck.packetId = packetId;
_toSendAcks.push_back(pendingAck);
for (size_t i = 0; i < _pendingPubRels.size(); i++) {
if (_pendingPubRels[i].packetId == packetId) {
_pendingPubRels.erase(_pendingPubRels.begin() + i);
_pendingPubRels.shrink_to_fit();
}
}
_sendAcks();
}
void AsyncMqttClient::_onPubAck(uint16_t packetId) {
_freeCurrentParsedPacket();
for (auto callback : _onPublishUserCallbacks) callback(packetId);
}
void AsyncMqttClient::_onPubRec(uint16_t packetId) {
_freeCurrentParsedPacket();
AsyncMqttClientInternals::PendingAck pendingAck;
pendingAck.packetType = AsyncMqttClientInternals::PacketType.PUBREL;
pendingAck.headerFlag = AsyncMqttClientInternals::HeaderFlag.PUBREL_RESERVED;
pendingAck.packetId = packetId;
_toSendAcks.push_back(pendingAck);
_sendAcks();
}
void AsyncMqttClient::_onPubComp(uint16_t packetId) {
_freeCurrentParsedPacket();
for (auto callback : _onPublishUserCallbacks) callback(packetId);
}
bool AsyncMqttClient::_sendPing() {
char fixedHeader[2];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.PINGREQ;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.PINGREQ_RESERVED;
fixedHeader[1] = 0;
size_t neededSpace = 2;
SEMAPHORE_TAKE(false);
if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return false; }
_client.add(fixedHeader, 2);
_client.send();
_lastClientActivity = millis();
_lastPingRequestTime = millis();
SEMAPHORE_GIVE();
return true;
}
void AsyncMqttClient::_sendAcks() {
uint8_t neededAckSpace = 2 + 2;
SEMAPHORE_TAKE();
for (size_t i = 0; i < _toSendAcks.size(); i++) {
if (_client.space() < neededAckSpace) break;
AsyncMqttClientInternals::PendingAck pendingAck = _toSendAcks[i];
char fixedHeader[2];
fixedHeader[0] = pendingAck.packetType;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | pendingAck.headerFlag;
fixedHeader[1] = 2;
char packetIdBytes[2];
packetIdBytes[0] = pendingAck.packetId >> 8;
packetIdBytes[1] = pendingAck.packetId & 0xFF;
_client.add(fixedHeader, 2);
_client.add(packetIdBytes, 2);
_client.send();
_toSendAcks.erase(_toSendAcks.begin() + i);
_toSendAcks.shrink_to_fit();
_lastClientActivity = millis();
}
SEMAPHORE_GIVE();
}
bool AsyncMqttClient::_sendDisconnect() {
if (!_connected) return true;
const uint8_t neededSpace = 2;
SEMAPHORE_TAKE(false);
if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return false; }
char fixedHeader[2];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.DISCONNECT;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.DISCONNECT_RESERVED;
fixedHeader[1] = 0;
_client.add(fixedHeader, 2);
_client.send();
_client.close(true);
_disconnectFlagged = false;
SEMAPHORE_GIVE();
return true;
}
uint16_t AsyncMqttClient::_getNextPacketId() {
uint16_t nextPacketId = _nextPacketId;
if (_nextPacketId == 65535) _nextPacketId = 0; // 0 is forbidden
_nextPacketId++;
return nextPacketId;
}
bool AsyncMqttClient::connected() const {
return _connected;
}
void AsyncMqttClient::connect() {
if (_connected) return;
#if ASYNC_TCP_SSL_ENABLED
if (_useIp) {
_client.connect(_ip, _port, _secure);
} else {
_client.connect(_host, _port, _secure);
}
#else
if (_useIp) {
_client.connect(_ip, _port);
} else {
_client.connect(_host, _port);
}
#endif
}
void AsyncMqttClient::disconnect(bool force) {
if (!_connected) return;
if (force) {
_client.close(true);
} else {
_disconnectFlagged = true;
_sendDisconnect();
}
}
uint16_t AsyncMqttClient::subscribe(const char* topic, uint8_t qos) {
if (!_connected) return 0;
char fixedHeader[5];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.SUBSCRIBE;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.SUBSCRIBE_RESERVED;
uint16_t topicLength = strlen(topic);
char topicLengthBytes[2];
topicLengthBytes[0] = topicLength >> 8;
topicLengthBytes[1] = topicLength & 0xFF;
char qosByte[1];
qosByte[0] = qos;
uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength + 1, fixedHeader + 1);
size_t neededSpace = 0;
neededSpace += 1 + remainingLengthLength;
neededSpace += 2;
neededSpace += 2;
neededSpace += topicLength;
neededSpace += 1;
SEMAPHORE_TAKE(0);
if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; }
uint16_t packetId = _getNextPacketId();
char packetIdBytes[2];
packetIdBytes[0] = packetId >> 8;
packetIdBytes[1] = packetId & 0xFF;
_client.add(fixedHeader, 1 + remainingLengthLength);
_client.add(packetIdBytes, 2);
_client.add(topicLengthBytes, 2);
_client.add(topic, topicLength);
_client.add(qosByte, 1);
_client.send();
_lastClientActivity = millis();
SEMAPHORE_GIVE();
return packetId;
}
uint16_t AsyncMqttClient::unsubscribe(const char* topic) {
if (!_connected) return 0;
char fixedHeader[5];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.UNSUBSCRIBE;
fixedHeader[0] = fixedHeader[0] << 4;
fixedHeader[0] = fixedHeader[0] | AsyncMqttClientInternals::HeaderFlag.UNSUBSCRIBE_RESERVED;
uint16_t topicLength = strlen(topic);
char topicLengthBytes[2];
topicLengthBytes[0] = topicLength >> 8;
topicLengthBytes[1] = topicLength & 0xFF;
uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(2 + 2 + topicLength, fixedHeader + 1);
size_t neededSpace = 0;
neededSpace += 1 + remainingLengthLength;
neededSpace += 2;
neededSpace += 2;
neededSpace += topicLength;
SEMAPHORE_TAKE(0);
if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; }
uint16_t packetId = _getNextPacketId();
char packetIdBytes[2];
packetIdBytes[0] = packetId >> 8;
packetIdBytes[1] = packetId & 0xFF;
_client.add(fixedHeader, 1 + remainingLengthLength);
_client.add(packetIdBytes, 2);
_client.add(topicLengthBytes, 2);
_client.add(topic, topicLength);
_client.send();
_lastClientActivity = millis();
SEMAPHORE_GIVE();
return packetId;
}
uint16_t AsyncMqttClient::publish(const char* topic, uint8_t qos, bool retain, const char* payload, size_t length, bool dup, uint16_t message_id) {
if (!_connected) return 0;
char fixedHeader[5];
fixedHeader[0] = AsyncMqttClientInternals::PacketType.PUBLISH;
fixedHeader[0] = fixedHeader[0] << 4;
if (dup) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_DUP;
if (retain) fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_RETAIN;
switch (qos) {
case 0:
fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS0;
break;
case 1:
fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS1;
break;
case 2:
fixedHeader[0] |= AsyncMqttClientInternals::HeaderFlag.PUBLISH_QOS2;
break;
}
uint16_t topicLength = strlen(topic);
char topicLengthBytes[2];
topicLengthBytes[0] = topicLength >> 8;
topicLengthBytes[1] = topicLength & 0xFF;
uint32_t payloadLength = length;
if (payload != nullptr && payloadLength == 0) payloadLength = strlen(payload);
uint32_t remainingLength = 2 + topicLength + payloadLength;
if (qos != 0) remainingLength += 2;
uint8_t remainingLengthLength = AsyncMqttClientInternals::Helpers::encodeRemainingLength(remainingLength, fixedHeader + 1);
size_t neededSpace = 0;
neededSpace += 1 + remainingLengthLength;
neededSpace += 2;
neededSpace += topicLength;
if (qos != 0) neededSpace += 2;
if (payload != nullptr) neededSpace += payloadLength;
SEMAPHORE_TAKE(0);
if (_client.space() < neededSpace) { SEMAPHORE_GIVE(); return 0; }
uint16_t packetId = 0;
char packetIdBytes[2];
if (qos != 0) {
if (dup && message_id > 0) {
packetId = message_id;
} else {
packetId = _getNextPacketId();
}
packetIdBytes[0] = packetId >> 8;
packetIdBytes[1] = packetId & 0xFF;
}
_client.add(fixedHeader, 1 + remainingLengthLength);
_client.add(topicLengthBytes, 2);
_client.add(topic, topicLength);
if (qos != 0) _client.add(packetIdBytes, 2);
if (payload != nullptr) _client.add(payload, payloadLength);
_client.send();
_lastClientActivity = millis();
SEMAPHORE_GIVE();
if (qos != 0) {
return packetId;
} else {
return 1;
}
}

View File

@@ -0,0 +1,6 @@
#ifndef SRC_ASYNCMQTTCLIENT_H_
#define SRC_ASYNCMQTTCLIENT_H_
#include "AsyncMqttClient.hpp"
#endif // SRC_ASYNCMQTTCLIENT_H_

View File

@@ -0,0 +1,166 @@
#pragma once
#include <functional>
#include <vector>
#include "Arduino.h"
#ifdef ESP32
#include <AsyncTCP.h>
#include <freertos/semphr.h>
#elif defined(ESP8266)
#include <ESPAsyncTCP.h>
#else
#error Platform not supported
#endif
#if ASYNC_TCP_SSL_ENABLED
#include <tcp_axtls.h>
#define SHA1_SIZE 20
#endif
#include "AsyncMqttClient/Flags.hpp"
#include "AsyncMqttClient/ParsingInformation.hpp"
#include "AsyncMqttClient/MessageProperties.hpp"
#include "AsyncMqttClient/Helpers.hpp"
#include "AsyncMqttClient/Callbacks.hpp"
#include "AsyncMqttClient/DisconnectReasons.hpp"
#include "AsyncMqttClient/Storage.hpp"
#include "AsyncMqttClient/Packets/Packet.hpp"
#include "AsyncMqttClient/Packets/ConnAckPacket.hpp"
#include "AsyncMqttClient/Packets/PingRespPacket.hpp"
#include "AsyncMqttClient/Packets/SubAckPacket.hpp"
#include "AsyncMqttClient/Packets/UnsubAckPacket.hpp"
#include "AsyncMqttClient/Packets/PublishPacket.hpp"
#include "AsyncMqttClient/Packets/PubRelPacket.hpp"
#include "AsyncMqttClient/Packets/PubAckPacket.hpp"
#include "AsyncMqttClient/Packets/PubRecPacket.hpp"
#include "AsyncMqttClient/Packets/PubCompPacket.hpp"
#if ESP32
#define SEMAPHORE_TAKE(X) if (xSemaphoreTake(_xSemaphore, 1000 / portTICK_PERIOD_MS) != pdTRUE) { return X; } // Waits max 1000ms
#define SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore);
#elif defined(ESP8266)
#define SEMAPHORE_TAKE(X) void()
#define SEMAPHORE_GIVE() void()
#endif
class AsyncMqttClient {
public:
AsyncMqttClient();
~AsyncMqttClient();
AsyncMqttClient& setKeepAlive(uint16_t keepAlive);
AsyncMqttClient& setClientId(const char* clientId);
AsyncMqttClient& setCleanSession(bool cleanSession);
AsyncMqttClient& setMaxTopicLength(uint16_t maxTopicLength);
AsyncMqttClient& setCredentials(const char* username, const char* password = nullptr);
AsyncMqttClient& setWill(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0);
AsyncMqttClient& setServer(IPAddress ip, uint16_t port);
AsyncMqttClient& setServer(const char* host, uint16_t port);
#if ASYNC_TCP_SSL_ENABLED
AsyncMqttClient& setSecure(bool secure);
AsyncMqttClient& addServerFingerprint(const uint8_t* fingerprint);
#endif
AsyncMqttClient& onConnect(AsyncMqttClientInternals::OnConnectUserCallback callback);
AsyncMqttClient& onDisconnect(AsyncMqttClientInternals::OnDisconnectUserCallback callback);
AsyncMqttClient& onSubscribe(AsyncMqttClientInternals::OnSubscribeUserCallback callback);
AsyncMqttClient& onUnsubscribe(AsyncMqttClientInternals::OnUnsubscribeUserCallback callback);
AsyncMqttClient& onMessage(AsyncMqttClientInternals::OnMessageUserCallback callback);
AsyncMqttClient& onPublish(AsyncMqttClientInternals::OnPublishUserCallback callback);
bool connected() const;
void connect();
void disconnect(bool force = false);
uint16_t subscribe(const char* topic, uint8_t qos);
uint16_t unsubscribe(const char* topic);
uint16_t publish(const char* topic, uint8_t qos, bool retain, const char* payload = nullptr, size_t length = 0, bool dup = false, uint16_t message_id = 0);
private:
AsyncClient _client;
bool _connected;
bool _connectPacketNotEnoughSpace;
bool _disconnectFlagged;
bool _tlsBadFingerprint;
uint32_t _lastClientActivity;
uint32_t _lastServerActivity;
uint32_t _lastPingRequestTime;
char _generatedClientId[13 + 1]; // esp8266abc123
IPAddress _ip;
const char* _host;
bool _useIp;
#if ASYNC_TCP_SSL_ENABLED
bool _secure;
#endif
uint16_t _port;
uint16_t _keepAlive;
bool _cleanSession;
const char* _clientId;
const char* _username;
const char* _password;
const char* _willTopic;
const char* _willPayload;
uint16_t _willPayloadLength;
uint8_t _willQos;
bool _willRetain;
#if ASYNC_TCP_SSL_ENABLED
std::vector<std::array<uint8_t, SHA1_SIZE>> _secureServerFingerprints;
#endif
std::vector<AsyncMqttClientInternals::OnConnectUserCallback> _onConnectUserCallbacks;
std::vector<AsyncMqttClientInternals::OnDisconnectUserCallback> _onDisconnectUserCallbacks;
std::vector<AsyncMqttClientInternals::OnSubscribeUserCallback> _onSubscribeUserCallbacks;
std::vector<AsyncMqttClientInternals::OnUnsubscribeUserCallback> _onUnsubscribeUserCallbacks;
std::vector<AsyncMqttClientInternals::OnMessageUserCallback> _onMessageUserCallbacks;
std::vector<AsyncMqttClientInternals::OnPublishUserCallback> _onPublishUserCallbacks;
AsyncMqttClientInternals::ParsingInformation _parsingInformation;
AsyncMqttClientInternals::Packet* _currentParsedPacket;
uint8_t _remainingLengthBufferPosition;
char _remainingLengthBuffer[4];
uint16_t _nextPacketId;
std::vector<AsyncMqttClientInternals::PendingPubRel> _pendingPubRels;
std::vector<AsyncMqttClientInternals::PendingAck> _toSendAcks;
#ifdef ESP32
SemaphoreHandle_t _xSemaphore = nullptr;
#endif
void _clear();
void _freeCurrentParsedPacket();
// TCP
void _onConnect(AsyncClient* client);
void _onDisconnect(AsyncClient* client);
static void _onError(AsyncClient* client, int8_t error);
void _onTimeout(AsyncClient* client, uint32_t time);
static void _onAck(AsyncClient* client, size_t len, uint32_t time);
void _onData(AsyncClient* client, char* data, size_t len);
void _onPoll(AsyncClient* client);
// MQTT
void _onPingResp();
void _onConnAck(bool sessionPresent, uint8_t connectReturnCode);
void _onSubAck(uint16_t packetId, char status);
void _onUnsubAck(uint16_t packetId);
void _onMessage(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId);
void _onPublish(uint16_t packetId, uint8_t qos);
void _onPubRel(uint16_t packetId);
void _onPubAck(uint16_t packetId);
void _onPubRec(uint16_t packetId);
void _onPubComp(uint16_t packetId);
bool _sendPing();
void _sendAcks();
bool _sendDisconnect();
uint16_t _getNextPacketId();
};

View File

@@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include "DisconnectReasons.hpp"
#include "MessageProperties.hpp"
namespace AsyncMqttClientInternals {
// user callbacks
typedef std::function<void(bool sessionPresent)> OnConnectUserCallback;
typedef std::function<void(AsyncMqttClientDisconnectReason reason)> OnDisconnectUserCallback;
typedef std::function<void(uint16_t packetId, uint8_t qos)> OnSubscribeUserCallback;
typedef std::function<void(uint16_t packetId)> OnUnsubscribeUserCallback;
typedef std::function<void(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total)> OnMessageUserCallback;
typedef std::function<void(uint16_t packetId)> OnPublishUserCallback;
// internal callbacks
typedef std::function<void(bool sessionPresent, uint8_t connectReturnCode)> OnConnAckInternalCallback;
typedef std::function<void()> OnPingRespInternalCallback;
typedef std::function<void(uint16_t packetId, char status)> OnSubAckInternalCallback;
typedef std::function<void(uint16_t packetId)> OnUnsubAckInternalCallback;
typedef std::function<void(char* topic, char* payload, uint8_t qos, bool dup, bool retain, size_t len, size_t index, size_t total, uint16_t packetId)> OnMessageInternalCallback;
typedef std::function<void(uint16_t packetId, uint8_t qos)> OnPublishInternalCallback;
typedef std::function<void(uint16_t packetId)> OnPubRelInternalCallback;
typedef std::function<void(uint16_t packetId)> OnPubAckInternalCallback;
typedef std::function<void(uint16_t packetId)> OnPubRecInternalCallback;
typedef std::function<void(uint16_t packetId)> OnPubCompInternalCallback;
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,15 @@
#pragma once
enum class AsyncMqttClientDisconnectReason : int8_t {
TCP_DISCONNECTED = 0,
MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1,
MQTT_IDENTIFIER_REJECTED = 2,
MQTT_SERVER_UNAVAILABLE = 3,
MQTT_MALFORMED_CREDENTIALS = 4,
MQTT_NOT_AUTHORIZED = 5,
ESP8266_NOT_ENOUGH_SPACE = 6,
TLS_BAD_FINGERPRINT = 7
};

View File

@@ -0,0 +1,57 @@
#pragma once
namespace AsyncMqttClientInternals {
constexpr struct {
const uint8_t RESERVED = 0;
const uint8_t CONNECT = 1;
const uint8_t CONNACK = 2;
const uint8_t PUBLISH = 3;
const uint8_t PUBACK = 4;
const uint8_t PUBREC = 5;
const uint8_t PUBREL = 6;
const uint8_t PUBCOMP = 7;
const uint8_t SUBSCRIBE = 8;
const uint8_t SUBACK = 9;
const uint8_t UNSUBSCRIBE = 10;
const uint8_t UNSUBACK = 11;
const uint8_t PINGREQ = 12;
const uint8_t PINGRESP = 13;
const uint8_t DISCONNECT = 14;
const uint8_t RESERVED2 = 1;
} PacketType;
constexpr struct {
const uint8_t CONNECT_RESERVED = 0x00;
const uint8_t CONNACK_RESERVED = 0x00;
const uint8_t PUBLISH_DUP = 0x08;
const uint8_t PUBLISH_QOS0 = 0x00;
const uint8_t PUBLISH_QOS1 = 0x02;
const uint8_t PUBLISH_QOS2 = 0x04;
const uint8_t PUBLISH_QOSRESERVED = 0x06;
const uint8_t PUBLISH_RETAIN = 0x01;
const uint8_t PUBACK_RESERVED = 0x00;
const uint8_t PUBREC_RESERVED = 0x00;
const uint8_t PUBREL_RESERVED = 0x02;
const uint8_t PUBCOMP_RESERVED = 0x00;
const uint8_t SUBSCRIBE_RESERVED = 0x02;
const uint8_t SUBACK_RESERVED = 0x00;
const uint8_t UNSUBSCRIBE_RESERVED = 0x02;
const uint8_t UNSUBACK_RESERVED = 0x00;
const uint8_t PINGREQ_RESERVED = 0x00;
const uint8_t PINGRESP_RESERVED = 0x00;
const uint8_t DISCONNECT_RESERVED = 0x00;
const uint8_t RESERVED2_RESERVED = 0x00;
} HeaderFlag;
constexpr struct {
const uint8_t USERNAME = 0x80;
const uint8_t PASSWORD = 0x40;
const uint8_t WILL_RETAIN = 0x20;
const uint8_t WILL_QOS0 = 0x00;
const uint8_t WILL_QOS1 = 0x08;
const uint8_t WILL_QOS2 = 0x10;
const uint8_t WILL = 0x04;
const uint8_t CLEAN_SESSION = 0x02;
const uint8_t RESERVED = 0x00;
} ConnectFlag;
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,38 @@
#pragma once
namespace AsyncMqttClientInternals {
class Helpers {
public:
static uint32_t decodeRemainingLength(char* bytes) {
uint32_t multiplier = 1;
uint32_t value = 0;
uint8_t currentByte = 0;
uint8_t encodedByte;
do {
encodedByte = bytes[currentByte++];
value += (encodedByte & 127) * multiplier;
multiplier *= 128;
} while ((encodedByte & 128) != 0);
return value;
}
static uint8_t encodeRemainingLength(uint32_t remainingLength, char* destination) {
uint8_t currentByte = 0;
uint8_t bytesNeeded = 0;
do {
uint8_t encodedByte = remainingLength % 128;
remainingLength /= 128;
if (remainingLength > 0) {
encodedByte = encodedByte | 128;
}
destination[currentByte++] = encodedByte;
bytesNeeded++;
} while (remainingLength > 0);
return bytesNeeded;
}
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,7 @@
#pragma once
struct AsyncMqttClientMessageProperties {
uint8_t qos;
bool dup;
bool retain;
};

View File

@@ -0,0 +1,30 @@
#include "ConnAckPacket.hpp"
using AsyncMqttClientInternals::ConnAckPacket;
ConnAckPacket::ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _sessionPresent(false)
, _connectReturnCode(0) {
}
ConnAckPacket::~ConnAckPacket() {
}
void ConnAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_sessionPresent = (currentByte << 7) >> 7;
} else {
_connectReturnCode = currentByte;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_sessionPresent, _connectReturnCode);
}
}
void ConnAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class ConnAckPacket : public Packet {
public:
explicit ConnAckPacket(ParsingInformation* parsingInformation, OnConnAckInternalCallback callback);
~ConnAckPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnConnAckInternalCallback _callback;
uint8_t _bytePosition;
bool _sessionPresent;
uint8_t _connectReturnCode;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,11 @@
#pragma once
namespace AsyncMqttClientInternals {
class Packet {
public:
virtual ~Packet() {}
virtual void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) = 0;
virtual void parsePayload(char* data, size_t len, size_t* currentBytePosition) = 0;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,21 @@
#include "PingRespPacket.hpp"
using AsyncMqttClientInternals::PingRespPacket;
PingRespPacket::PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback) {
}
PingRespPacket::~PingRespPacket() {
}
void PingRespPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}
void PingRespPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PingRespPacket : public Packet {
public:
explicit PingRespPacket(ParsingInformation* parsingInformation, OnPingRespInternalCallback callback);
~PingRespPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnPingRespInternalCallback _callback;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,30 @@
#include "PubAckPacket.hpp"
using AsyncMqttClientInternals::PubAckPacket;
PubAckPacket::PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
PubAckPacket::~PubAckPacket() {
}
void PubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId);
}
}
void PubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PubAckPacket : public Packet {
public:
explicit PubAckPacket(ParsingInformation* parsingInformation, OnPubAckInternalCallback callback);
~PubAckPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnPubAckInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,30 @@
#include "PubCompPacket.hpp"
using AsyncMqttClientInternals::PubCompPacket;
PubCompPacket::PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
PubCompPacket::~PubCompPacket() {
}
void PubCompPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId);
}
}
void PubCompPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PubCompPacket : public Packet {
public:
explicit PubCompPacket(ParsingInformation* parsingInformation, OnPubCompInternalCallback callback);
~PubCompPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnPubCompInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,30 @@
#include "PubRecPacket.hpp"
using AsyncMqttClientInternals::PubRecPacket;
PubRecPacket::PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
PubRecPacket::~PubRecPacket() {
}
void PubRecPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId);
}
}
void PubRecPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PubRecPacket : public Packet {
public:
explicit PubRecPacket(ParsingInformation* parsingInformation, OnPubRecInternalCallback callback);
~PubRecPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnPubRecInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,30 @@
#include "PubRelPacket.hpp"
using AsyncMqttClientInternals::PubRelPacket;
PubRelPacket::PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
PubRelPacket::~PubRelPacket() {
}
void PubRelPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId);
}
}
void PubRelPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PubRelPacket : public Packet {
public:
explicit PubRelPacket(ParsingInformation* parsingInformation, OnPubRelInternalCallback callback);
~PubRelPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnPubRelInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,91 @@
#include "PublishPacket.hpp"
using AsyncMqttClientInternals::PublishPacket;
PublishPacket::PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback)
: _parsingInformation(parsingInformation)
, _dataCallback(dataCallback)
, _completeCallback(completeCallback)
, _dup(false)
, _qos(0)
, _retain(0)
, _bytePosition(0)
, _topicLengthMsb(0)
, _topicLength(0)
, _ignore(false)
, _packetIdMsb(0)
, _packetId(0)
, _payloadLength(0)
, _payloadBytesRead(0) {
_dup = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_DUP;
_retain = _parsingInformation->packetFlags & HeaderFlag.PUBLISH_RETAIN;
char qosMasked = _parsingInformation->packetFlags & 0x06;
switch (qosMasked) {
case HeaderFlag.PUBLISH_QOS0:
_qos = 0;
break;
case HeaderFlag.PUBLISH_QOS1:
_qos = 1;
break;
case HeaderFlag.PUBLISH_QOS2:
_qos = 2;
break;
}
}
PublishPacket::~PublishPacket() {
}
void PublishPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition == 0) {
_topicLengthMsb = currentByte;
} else if (_bytePosition == 1) {
_topicLength = currentByte | _topicLengthMsb << 8;
if (_topicLength > _parsingInformation->maxTopicLength) {
_ignore = true;
} else {
_parsingInformation->topicBuffer[_topicLength] = '\0';
}
} else if (_bytePosition >= 2 && _bytePosition < 2 + _topicLength) {
// Starting from here, _ignore might be true
if (!_ignore) _parsingInformation->topicBuffer[_bytePosition - 2] = currentByte;
if (_bytePosition == 2 + _topicLength - 1 && _qos == 0) {
_preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1));
return;
}
} else if (_bytePosition == 2 + _topicLength) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_preparePayloadHandling(_parsingInformation->remainingLength - (_bytePosition + 1));
}
_bytePosition++;
}
void PublishPacket::_preparePayloadHandling(uint32_t payloadLength) {
_payloadLength = payloadLength;
if (payloadLength == 0) {
_parsingInformation->bufferState = BufferState::NONE;
if (!_ignore) {
_dataCallback(_parsingInformation->topicBuffer, nullptr, _qos, _dup, _retain, 0, 0, 0, _packetId);
_completeCallback(_packetId, _qos);
}
} else {
_parsingInformation->bufferState = BufferState::PAYLOAD;
}
}
void PublishPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
size_t remainToRead = len - (*currentBytePosition);
if (_payloadBytesRead + remainToRead > _payloadLength) remainToRead = _payloadLength - _payloadBytesRead;
if (!_ignore) _dataCallback(_parsingInformation->topicBuffer, data + (*currentBytePosition), _qos, _dup, _retain, remainToRead, _payloadBytesRead, _payloadLength, _packetId);
_payloadBytesRead += remainToRead;
(*currentBytePosition) += remainToRead;
if (_payloadBytesRead == _payloadLength) {
_parsingInformation->bufferState = BufferState::NONE;
if (!_ignore) _completeCallback(_packetId, _qos);
}
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../Flags.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class PublishPacket : public Packet {
public:
explicit PublishPacket(ParsingInformation* parsingInformation, OnMessageInternalCallback dataCallback, OnPublishInternalCallback completeCallback);
~PublishPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnMessageInternalCallback _dataCallback;
OnPublishInternalCallback _completeCallback;
void _preparePayloadHandling(uint32_t payloadLength);
bool _dup;
uint8_t _qos;
bool _retain;
uint8_t _bytePosition;
char _topicLengthMsb;
uint16_t _topicLength;
bool _ignore;
char _packetIdMsb;
uint16_t _packetId;
uint32_t _payloadLength;
uint32_t _payloadBytesRead;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,46 @@
#include "SubAckPacket.hpp"
using AsyncMqttClientInternals::SubAckPacket;
SubAckPacket::SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
SubAckPacket::~SubAckPacket() {
}
void SubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::PAYLOAD;
}
}
void SubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
char status = data[(*currentBytePosition)++];
/* switch (status) {
case 0:
Serial.println("Success QoS 0");
break;
case 1:
Serial.println("Success QoS 1");
break;
case 2:
Serial.println("Success QoS 2");
break;
case 0x80:
Serial.println("Failure");
break;
} */
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId, status);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class SubAckPacket : public Packet {
public:
explicit SubAckPacket(ParsingInformation* parsingInformation, OnSubAckInternalCallback callback);
~SubAckPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnSubAckInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,30 @@
#include "UnsubAckPacket.hpp"
using AsyncMqttClientInternals::UnsubAckPacket;
UnsubAckPacket::UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback)
: _parsingInformation(parsingInformation)
, _callback(callback)
, _bytePosition(0)
, _packetIdMsb(0)
, _packetId(0) {
}
UnsubAckPacket::~UnsubAckPacket() {
}
void UnsubAckPacket::parseVariableHeader(char* data, size_t len, size_t* currentBytePosition) {
char currentByte = data[(*currentBytePosition)++];
if (_bytePosition++ == 0) {
_packetIdMsb = currentByte;
} else {
_packetId = currentByte | _packetIdMsb << 8;
_parsingInformation->bufferState = BufferState::NONE;
_callback(_packetId);
}
}
void UnsubAckPacket::parsePayload(char* data, size_t len, size_t* currentBytePosition) {
(void)data;
(void)currentBytePosition;
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Arduino.h"
#include "Packet.hpp"
#include "../ParsingInformation.hpp"
#include "../Callbacks.hpp"
namespace AsyncMqttClientInternals {
class UnsubAckPacket : public Packet {
public:
explicit UnsubAckPacket(ParsingInformation* parsingInformation, OnUnsubAckInternalCallback callback);
~UnsubAckPacket();
void parseVariableHeader(char* data, size_t len, size_t* currentBytePosition);
void parsePayload(char* data, size_t len, size_t* currentBytePosition);
private:
ParsingInformation* _parsingInformation;
OnUnsubAckInternalCallback _callback;
uint8_t _bytePosition;
char _packetIdMsb;
uint16_t _packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,21 @@
#pragma once
namespace AsyncMqttClientInternals {
enum class BufferState : uint8_t {
NONE = 0,
REMAINING_LENGTH = 2,
VARIABLE_HEADER = 3,
PAYLOAD = 4
};
struct ParsingInformation {
BufferState bufferState;
uint16_t maxTopicLength;
char* topicBuffer;
uint8_t packetType;
uint16_t packetFlags;
uint32_t remainingLength;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,13 @@
#pragma once
namespace AsyncMqttClientInternals {
struct PendingPubRel {
uint16_t packetId;
};
struct PendingAck {
uint8_t packetType;
uint8_t headerFlag;
uint16_t packetId;
};
} // namespace AsyncMqttClientInternals

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Marvin Roger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,18 @@
Async MQTT client for ESP8266 and ESP32 (Github: https://github.com/marvinroger/async-mqtt-client)
=============================
[![Build Status](https://img.shields.io/travis/marvinroger/async-mqtt-client/master.svg?style=flat-square)](https://travis-ci.org/marvinroger/async-mqtt-client)
An Arduino for ESP8266 and ESP32 asynchronous [MQTT](http://mqtt.org/) client implementation, built on [me-no-dev/ESPAsyncTCP (ESP8266)](https://github.com/me-no-dev/ESPAsyncTCP) | [me-no-dev/AsyncTCP (ESP32)](https://github.com/me-no-dev/AsyncTCP) .
## Features
* Compliant with the 3.1.1 version of the protocol
* Fully asynchronous
* Subscribe at QoS 0, 1 and 2
* Publish at QoS 0, 1 and 2
* SSL/TLS support
* Available in the [PlatformIO registry](http://platformio.org/lib/show/346/AsyncMqttClient)
## Requirements, installation and usage
The project is documented in the [/docs folder](docs).

View File

@@ -565,82 +565,93 @@ void handleNotifications()
return;
}
if (receiveDirect) {
//TPM2.NET
if (udpIn[0] == 0x9c) {
//WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant)
//if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet
byte tpmType = udpIn[1];
if (tpmType == 0xaa) { //TPM2.NET polling, expect answer
sendTPM2Ack(); return;
}
if (tpmType != 0xda) return; //return if notTPM2.NET data
if (!receiveDirect) return;
realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET);
if (realtimeOverride) return;
tpmPacketCount++; //increment the packet count
if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet
byte packetNum = udpIn[4]; //starts with 1!
byte numPackets = udpIn[5];
unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED
unsigned totalLen = strip.getLengthTotal();
for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received
tpmPacketCount = 0;
if (useMainSegmentOnly) strip.trigger();
else strip.show();
}
return;
//TPM2.NET
if (udpIn[0] == 0x9c)
{
//WARNING: this code assumes that the final TMP2.NET payload is evenly distributed if using multiple packets (ie. frame size is constant)
//if the number of LEDs in your installation doesn't allow that, please include padding bytes at the end of the last packet
byte tpmType = udpIn[1];
if (tpmType == 0xaa) { //TPM2.NET polling, expect answer
sendTPM2Ack(); return;
}
if (tpmType != 0xda) return; //return if notTPM2.NET data
//UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw
if (udpIn[0] > 0 && udpIn[0] < 6) {
realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();
DEBUG_PRINTLN(realtimeIP);
if (packetSize < 2) return;
realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET);
if (realtimeOverride) return;
if (udpIn[1] == 0) {
realtimeTimeout = 0; // cancel realtime mode immediately
return;
} else {
realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP);
}
if (realtimeOverride) return;
tpmPacketCount++; //increment the packet count
if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet
byte packetNum = udpIn[4]; //starts with 1!
byte numPackets = udpIn[5];
unsigned totalLen = strip.getLengthTotal();
if (udpIn[0] == 1 && packetSize > 5) { //warls
for (size_t i = 2; i < packetSize -3; i += 4) {
setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0);
}
} else if (udpIn[0] == 2 && packetSize > 4) { //drgb
for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
} else if (udpIn[0] == 3 && packetSize > 6) { //drgbw
for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
}
} else if (udpIn[0] == 4 && packetSize > 7) { //dnrgb
unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
} else if (udpIn[0] == 5 && packetSize > 8) { //dnrgbw
unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
}
}
unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED
unsigned totalLen = strip.getLengthTotal();
for (size_t i = 6; i < tpmPayloadFrameSize + 4U && id < totalLen; i += 3, id++) {
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
if (tpmPacketCount == numPackets) { //reset packet count and show if all packets were received
tpmPacketCount = 0;
if (useMainSegmentOnly) strip.trigger();
else strip.show();
return;
}
return;
}
//UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw
if (udpIn[0] > 0 && udpIn[0] < 6)
{
realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP();
DEBUG_PRINTLN(realtimeIP);
if (packetSize < 2) return;
if (udpIn[1] == 0) {
realtimeTimeout = 0; // cancel realtime mode immediately
return;
} else {
realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP);
}
if (realtimeOverride) return;
unsigned totalLen = strip.getLengthTotal();
if (udpIn[0] == 1 && packetSize > 5) //warls
{
for (size_t i = 2; i < packetSize -3; i += 4)
{
setRealtimePixel(udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3], 0);
}
} else if (udpIn[0] == 2 && packetSize > 4) //drgb
{
for (size_t i = 2, id = 0; i < packetSize -2 && id < totalLen; i += 3, id++)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
} else if (udpIn[0] == 3 && packetSize > 6) //drgbw
{
for (size_t i = 2, id = 0; i < packetSize -3 && id < totalLen; i += 4, id++)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
}
} else if (udpIn[0] == 4 && packetSize > 7) //dnrgb
{
unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 3, id++)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0);
}
} else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw
{
unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00);
for (size_t i = 4; i < packetSize -2 && id < totalLen; i += 4, id++)
{
setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]);
}
}
if (useMainSegmentOnly) strip.trigger();
else strip.show();
return;
}
// API over UDP
@@ -658,8 +669,6 @@ void handleNotifications()
}
releaseJSONBufferLock();
}
UsermodManager::onUdpPacket(udpIn, packetSize);
}

View File

@@ -68,10 +68,6 @@ bool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t
return false;
}
#endif
bool UsermodManager::onUdpPacket(uint8_t* payload, size_t len) {
for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) if ((*mod)->onUdpPacket(payload, len)) return true;
return false;
}
void UsermodManager::onUpdateBegin(bool init) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onUpdateBegin(init); } // notify usermods that update is to begin
void UsermodManager::onStateChange(uint8_t mode) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->onStateChange(mode); } // notify usermods that WLED state changed

View File

@@ -10,6 +10,11 @@
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0)
#include "soc/rtc.h"
#endif
#ifndef WLED_DISABLE_OTA
#include "esp_flash.h" // for direct flash access
#include "esp_log.h" // for error handling
#endif
#endif
@@ -857,6 +862,65 @@ void handleBootLoop() {
ESP.restart(); // restart cleanly and don't wait for another crash
}
#ifndef WLED_DISABLE_OTA
#ifdef ESP32
// Get bootloader version for OTA compatibility checking
// Uses rollback capability as primary indicator since bootloader description
// structure is only available in ESP-IDF v5+ bootloaders
uint32_t getBootloaderVersion() {
static uint32_t cached_version = 0;
if (cached_version != 0) return cached_version;
DEBUG_PRINTF_P(PSTR("Determining bootloader version...\n"));
#ifndef WLED_DISABLE_OTA
bool can_rollback = Update.canRollBack();
#else
bool can_rollback = false;
#endif
DEBUG_PRINTF_P(PSTR("Rollback capability: %s\n"), can_rollback ? "YES" : "NO");
if (can_rollback) {
// Rollback capability indicates v4+ bootloader
cached_version = 4;
DEBUG_PRINTF_P(PSTR("Bootloader v4+ detected (rollback capable)\n"));
} else {
// No rollback capability - check ESP-IDF version for best guess
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
cached_version = 3;
DEBUG_PRINTF_P(PSTR("Bootloader v3 detected (ESP-IDF 4.4+)\n"));
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)
cached_version = 2;
DEBUG_PRINTF_P(PSTR("Bootloader v2 detected (ESP-IDF 4.x)\n"));
#else
cached_version = 1;
DEBUG_PRINTF_P(PSTR("Bootloader v1/legacy detected (ESP-IDF 3.x)\n"));
#endif
}
DEBUG_PRINTF_P(PSTR("getBootloaderVersion() returning: %d\n"), cached_version);
return cached_version;
}
// Check if current bootloader is compatible with given required version
bool isBootloaderCompatible(uint32_t required_version) {
uint32_t current_version = getBootloaderVersion();
bool compatible = current_version >= required_version;
DEBUG_PRINTF_P(PSTR("Bootloader compatibility check: current=%d, required=%d, compatible=%s\n"),
current_version, required_version, compatible ? "YES" : "NO");
return compatible;
}
#else
// ESP8266 compatibility functions - always assume compatible for now
uint32_t getBootloaderVersion() { return 1; }
bool isBootloaderCompatible(uint32_t required_version) { return true; }
#endif
#endif
/*
* Fixed point integer based Perlin noise functions by @dedehai
* Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness

View File

@@ -106,6 +106,10 @@
#include <LittleFS.h>
#endif
#include "esp_task_wdt.h"
#ifndef WLED_DISABLE_OTA
#include "esp_ota_ops.h"
#endif
#ifndef WLED_DISABLE_ESPNOW
#include <esp_now.h>
@@ -155,7 +159,7 @@
#include "src/dependencies/e131/ESPAsyncE131.h"
#ifndef WLED_DISABLE_MQTT
#include <AsyncMqttClient.h>
#include "src/dependencies/async-mqtt-client/AsyncMqttClient.h"
#endif
#define ARDUINOJSON_DECODE_UNICODE 0

View File

@@ -1,31 +0,0 @@
#include "ota_release_check.h"
#include "wled.h"
// Simple compile-time hash function for release name validation
constexpr uint32_t djb2_hash(const char* str) {
uint32_t hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) + *str++;
}
return hash;
}
// Single structure definition for both platforms
#ifdef ESP32
const wled_custom_desc_t __attribute__((section(".rodata.wled_desc"))) wled_custom_description = {
#elif defined(ESP8266)
const wled_custom_desc_t __attribute__((section(".ver_number"))) wled_custom_description = {
#endif
WLED_CUSTOM_DESC_MAGIC, // magic
WLED_CUSTOM_DESC_VERSION, // version
WLED_RELEASE_NAME, // release_name
djb2_hash(WLED_RELEASE_NAME) // crc32 - computed at compile time
};
// Single reference to ensure it's not optimized away
const wled_custom_desc_t* __attribute__((used)) wled_custom_desc_ref = &wled_custom_description;
// Function to ensure the structure is referenced by code
const wled_custom_desc_t* getWledCustomDesc() {
return &wled_custom_description;
}

View File

@@ -6,7 +6,6 @@
#else
#include <Update.h>
#endif
#include "ota_release_check.h"
#endif
#include "html_ui.h"
#include "html_settings.h"
@@ -389,6 +388,28 @@ void initServer()
createEditHandler(correctPIN);
// Bootloader info endpoint for troubleshooting
server.on("/bootloader", HTTP_GET, [](AsyncWebServerRequest *request){
AsyncJsonResponse *response = new AsyncJsonResponse(128);
JsonObject root = response->getRoot();
#ifdef ESP32
root[F("version")] = getBootloaderVersion();
#ifndef WLED_DISABLE_OTA
root[F("rollback_capable")] = Update.canRollBack();
#else
root[F("rollback_capable")] = false;
#endif
root[F("esp_idf_version")] = ESP_IDF_VERSION;
#else
root[F("rollback_capable")] = false;
root[F("platform")] = F("ESP8266");
#endif
response->setLength();
request->send(response);
});
static const char _update[] PROGMEM = "/update";
#ifndef WLED_DISABLE_OTA
//init ota page
@@ -425,81 +446,67 @@ void initServer()
return;
}
if (!correctPIN || otaLock) return;
// Static variable to track release check status across chunks
static bool releaseCheckPassed = false;
if(!index){
DEBUG_PRINTLN(F("OTA Update Start"));
// Check if user wants to ignore release check
bool skipValidation = request->hasParam("skipValidation", true);
// Validate OTA release compatibility using the first chunk data directly
char errorMessage[128];
releaseCheckPassed = shouldAllowOTA(data, len, skipValidation, errorMessage);
if (!releaseCheckPassed) {
DEBUG_PRINTF_P(PSTR("OTA blocked: %s\n"), errorMessage);
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), errorMessage);
return;
#ifndef WLED_DISABLE_OTA
// Check for bootloader compatibility metadata in first chunk
if (len >= 32) {
// Look for metadata header: "WLED_BOOTLOADER:X" where X is required version
const char* metadata_prefix = "WLED_BOOTLOADER:";
size_t prefix_len = strlen(metadata_prefix);
// Search for metadata in first 512 bytes or available data, whichever is smaller
size_t search_len = (len > 512) ? 512 : len;
for (size_t i = 0; i <= search_len - prefix_len - 1; i++) {
if (memcmp(data + i, metadata_prefix, prefix_len) == 0) {
// Found metadata header, extract required version
char version_char = data[i + prefix_len];
if (version_char >= '1' && version_char <= '9') {
uint32_t required_version = version_char - '0';
DEBUG_PRINTF_P(PSTR("OTA file requires bootloader v%d\n"), required_version);
if (!isBootloaderCompatible(required_version)) {
DEBUG_PRINTF_P(PSTR("Bootloader incompatible! Current: v%d, Required: v%d\n"),
getBootloaderVersion(), required_version);
request->send(400, FPSTR(CONTENT_TYPE_PLAIN),
F("Bootloader incompatible! This firmware requires bootloader v4+. "
"Please update via USB using install.wled.me first, or use WLED 0.15.x."));
return;
}
DEBUG_PRINTLN(F("Bootloader compatibility check passed"));
break;
}
}
}
}
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
#endif
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().disableWatchdog();
#endif
UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init)
lastEditTime = millis(); // make sure PIN does not lock during update
// Start the actual OTA update
strip.suspend();
backupConfig(); // backup current config in case the update ends badly
strip.resetSegments(); // free as much memory as you can
#ifdef ESP8266
Update.runAsync(true);
#endif
// Begin update with the firmware size from content length
size_t updateSize = request->contentLength() > 0 ? request->contentLength() : ((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
if (!Update.begin(updateSize)) {
DEBUG_PRINTF_P(PSTR("OTA Failed to begin: %s\n"), Update.getErrorString().c_str());
Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
}
if(!Update.hasError()) Update.write(data, len);
if(isFinal){
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
strip.resume();
UsermodManager::onUpdateBegin(false);
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
#ifdef ESP32
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.errorString());
#else
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), String("Update.begin failed: ") + Update.getErrorString());
#endif
return;
}
}
// Write chunk data to OTA update (only if release check passed)
if (releaseCheckPassed && !Update.hasError()) {
if (Update.write(data, len) != len) {
DEBUG_PRINTF_P(PSTR("OTA write failed on chunk %zu: %s\n"), index, Update.getErrorString().c_str());
}
}
if(isFinal){
DEBUG_PRINTLN(F("OTA Update End"));
if (releaseCheckPassed) {
if(Update.end(true)){
DEBUG_PRINTLN(F("Update Success"));
} else {
DEBUG_PRINTLN(F("Update Failed"));
strip.resume();
UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init)
#if WLED_WATCHDOG_TIMEOUT > 0
WLED::instance().enableWatchdog();
#endif
}
}
}
});