basic commit
This commit is contained in:
parent
9e9c045a3f
commit
90ef654c80
22
data/system/animations.json
Normal file
22
data/system/animations.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"comets":{
|
||||||
|
"size": 0.2,
|
||||||
|
"fade-factor1":64,
|
||||||
|
"max-comets":16
|
||||||
|
},
|
||||||
|
"fire":{
|
||||||
|
"cooling":66,
|
||||||
|
"sparking":62,
|
||||||
|
"brightness":255
|
||||||
|
},
|
||||||
|
"custom-color-pack1": {
|
||||||
|
"color1": "#FF0000",
|
||||||
|
"color2": "#00FF00",
|
||||||
|
"color3": "#0000FF"
|
||||||
|
},
|
||||||
|
"custom-color-pack2": {
|
||||||
|
"color1": "#FFFF00",
|
||||||
|
"color2": "#FF00FF",
|
||||||
|
"color3": "#00FFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@
|
|||||||
"wifi-ap":{
|
"wifi-ap":{
|
||||||
"ssid": "ATA_AP",
|
"ssid": "ATA_AP",
|
||||||
"append-id": true,
|
"append-id": true,
|
||||||
|
"user": "admin",
|
||||||
"pass": "12345678",
|
"pass": "12345678",
|
||||||
"ip": "192.168.10.1",
|
"ip": "192.168.10.1",
|
||||||
"gateway": "192.168.10.1",
|
"gateway": "192.168.10.1",
|
||||||
|
|||||||
@ -133,14 +133,28 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tab-bar { display:flex; gap:8px; justify-content:center; margin:12px 0 16px; flex-wrap:wrap; }
|
||||||
|
.tab-bar button { max-width:none; flex:0 0 auto; background:#6c757d; }
|
||||||
|
.tab-bar button.active { background:#007bff; }
|
||||||
|
.tab-panel { display:none; }
|
||||||
|
.tab-panel.active { display:block; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>ATA Firmware Update</h1>
|
<h1>ATA Firmware Update</h1>
|
||||||
|
|
||||||
<!-- Status Indicators -->
|
<!-- Tab Buttons -->
|
||||||
<div class="status-container">
|
<div class="tab-bar">
|
||||||
|
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-upgrade" class="tab-panel active">
|
||||||
|
<!-- Status Indicators -->
|
||||||
|
<div class="status-container">
|
||||||
<span class="status-indicator-ble"></span>
|
<span class="status-indicator-ble"></span>
|
||||||
<label id="status-ble-connection">BLE Status: ...</label>
|
<label id="status-ble-connection">BLE Status: ...</label>
|
||||||
</div>
|
</div>
|
||||||
@ -183,21 +197,22 @@
|
|||||||
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
|
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
|
||||||
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
|
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div> <!-- /tab-upgrade -->
|
||||||
|
|
||||||
<!-- Wi-Fi Input Fields -->
|
<div id="tab-wifi" class="tab-panel">
|
||||||
<div class="input-container">
|
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2>
|
||||||
|
<div class="input-container">
|
||||||
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
||||||
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
||||||
<div style="display: flex; align-items: center; gap: 5px;">
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
|
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
|
||||||
<label for="showPassword">Show Password</label>
|
<label for="showPassword">Show Password</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="btn-container wifi">
|
||||||
<!-- Added margin-top above this button -->
|
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
|
||||||
<div class="btn-container wifi">
|
</div>
|
||||||
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
|
</div><!-- /tab-wifi -->
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
@ -457,6 +472,17 @@
|
|||||||
el.inDeviceName.value = BLE_SERVER_NAME;
|
el.inDeviceName.value = BLE_SERVER_NAME;
|
||||||
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
|
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
|
||||||
// Inline onclicks already wired; ensure functions are in scope
|
// Inline onclicks already wired; ensure functions are in scope
|
||||||
|
// Tab switching
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const id = btn.getAttribute('data-tab');
|
||||||
|
const panel = document.getElementById(id);
|
||||||
|
if(panel) panel.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
updateUI();
|
updateUI();
|
||||||
logMessage('Ready. Enter device name or use default and press Connect.');
|
logMessage('Ready. Enter device name or use default and press Connect.');
|
||||||
}
|
}
|
||||||
@ -60,7 +60,8 @@ void Lights_Set_ON(void);
|
|||||||
void Lights_Set_OFF(void);
|
void Lights_Set_OFF(void);
|
||||||
void Lights_Set_Brightness(uint8_t scale);
|
void Lights_Set_Brightness(uint8_t scale);
|
||||||
void Lights_Set_White(uint8_t val);
|
void Lights_Set_White(uint8_t val);
|
||||||
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3);
|
//void createFirePalette(CRGBPalette16& palette, COLOR_PACK& colorPack);
|
||||||
|
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack);
|
||||||
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src);
|
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,15 @@
|
|||||||
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
|
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
|
||||||
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
|
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
|
||||||
#define BUFFER_SIZE 4096
|
#define BUFFER_SIZE 4096
|
||||||
|
// Maximum allowed manifest size (bytes) to protect memory
|
||||||
|
#define MAX_MANIFEST_SIZE (64 * 1024)
|
||||||
|
|
||||||
|
// Number of HTTP retry attempts for transient failures
|
||||||
|
#define HTTP_RETRY_COUNT 3
|
||||||
|
#define HTTP_RETRY_DELAY_MS 500
|
||||||
|
|
||||||
|
// Allow external cancellation
|
||||||
|
extern volatile bool g_UpdateCancelFlag;
|
||||||
|
|
||||||
extern TaskHandle_t Update_Task_Handle;
|
extern TaskHandle_t Update_Task_Handle;
|
||||||
|
|
||||||
@ -186,7 +195,6 @@ void sendUpdateMessage(const char* message, bool complete, int progress);
|
|||||||
|
|
||||||
void handleUpdateProgress(AsyncWebServerRequest *request);
|
void handleUpdateProgress(AsyncWebServerRequest *request);
|
||||||
|
|
||||||
bool checkManifest(Version& remoteVersion);
|
|
||||||
|
|
||||||
void startVersionCheckTask();
|
void startVersionCheckTask();
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,21 @@ typedef struct {
|
|||||||
CRGB col[8];
|
CRGB col[8];
|
||||||
} COLOR_PACK;
|
} COLOR_PACK;
|
||||||
|
|
||||||
|
|
||||||
|
const COLOR_PACK colorPack_FireRed PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };
|
||||||
|
const COLOR_PACK colorPack_FireGreen PROGMEM = { 4, { CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen, CRGB::Black } };
|
||||||
|
const COLOR_PACK colorPack_FireBlue PROGMEM = { 4, { CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue, CRGB::Black } };
|
||||||
|
const COLOR_PACK colorPack_FireViolet PROGMEM = { 4, { CRGB::Purple, CRGB::Blue, CRGB::Violet, CRGB::Black } };
|
||||||
|
|
||||||
|
// Fire (compacted: single PROGMEM array, removes duplicate size constants)
|
||||||
|
const COLOR_PACK fireColorPacks[] PROGMEM = {
|
||||||
|
colorPack_FireRed,
|
||||||
|
colorPack_FireGreen,
|
||||||
|
colorPack_FireBlue,
|
||||||
|
colorPack_FireViolet
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Sectors
|
// Sectors
|
||||||
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } };
|
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } };
|
||||||
const COLOR_PACK colorPack_USA PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } };
|
const COLOR_PACK colorPack_USA PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } };
|
||||||
@ -15,6 +30,16 @@ const COLOR_PACK colorPack_MEXICO PROGMEM = { 3, { CRGB::Green, CRGB::White, CRG
|
|||||||
const COLOR_PACK colorPack_CANADA PROGMEM = { 2, { CRGB::Red, CRGB::White } };
|
const COLOR_PACK colorPack_CANADA PROGMEM = { 2, { CRGB::Red, CRGB::White } };
|
||||||
const COLOR_PACK colorPack_GERMANY PROGMEM = { 3, { CRGB::Black, CRGB::Red, CRGB::Yellow } };
|
const COLOR_PACK colorPack_GERMANY PROGMEM = { 3, { CRGB::Black, CRGB::Red, CRGB::Yellow } };
|
||||||
|
|
||||||
|
const COLOR_PACK combo_colorPacks[] PROGMEM = {
|
||||||
|
colorPack_RAINBOW,
|
||||||
|
colorPack_USA,
|
||||||
|
colorPack_MEXICO,
|
||||||
|
colorPack_CANADA,
|
||||||
|
colorPack_GERMANY
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Single Colors
|
// Single Colors
|
||||||
const COLOR_PACK colorPack_Single_Red PROGMEM = { 1, { CRGB::Red } };
|
const COLOR_PACK colorPack_Single_Red PROGMEM = { 1, { CRGB::Red } };
|
||||||
const COLOR_PACK colorPack_Single_Orange PROGMEM = { 1, { CRGB::OrangeRed } };
|
const COLOR_PACK colorPack_Single_Orange PROGMEM = { 1, { CRGB::OrangeRed } };
|
||||||
@ -25,6 +50,19 @@ const COLOR_PACK colorPack_Single_Viloet PROGMEM = { 1, { CRGB::DarkViolet } };
|
|||||||
const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } };
|
const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } };
|
||||||
const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } };
|
const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } };
|
||||||
|
|
||||||
|
const COLOR_PACK single_colorPacks[] PROGMEM = {
|
||||||
|
colorPack_Single_Red,
|
||||||
|
colorPack_Single_Orange,
|
||||||
|
colorPack_Single_Yellow,
|
||||||
|
colorPack_Single_Green,
|
||||||
|
colorPack_Single_Blue,
|
||||||
|
colorPack_Single_Viloet,
|
||||||
|
colorPack_Single_Magenta,
|
||||||
|
colorPack_Single_White
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Dashes
|
// Dashes
|
||||||
const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } };
|
const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } };
|
||||||
const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } };
|
const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } };
|
||||||
@ -35,8 +73,20 @@ const COLOR_PACK colorPack_IndigoBlack PROGMEM = { 2, { CRGB::Indigo, CRGB::Blac
|
|||||||
const COLOR_PACK colorPack_VioletBlack PROGMEM = { 2, { CRGB::MediumVioletRed, CRGB::Black } };
|
const COLOR_PACK colorPack_VioletBlack PROGMEM = { 2, { CRGB::MediumVioletRed, CRGB::Black } };
|
||||||
const COLOR_PACK colorPack_WhiteBlack PROGMEM = { 2, { CRGB::White, CRGB::Black } };
|
const COLOR_PACK colorPack_WhiteBlack PROGMEM = { 2, { CRGB::White, CRGB::Black } };
|
||||||
|
|
||||||
|
const COLOR_PACK DashesColorPacks[] PROGMEM = {
|
||||||
|
colorPack_RedBlack,
|
||||||
|
colorPack_OrangeBlack,
|
||||||
|
colorPack_YellowBlack,
|
||||||
|
colorPack_GreenBlack,
|
||||||
|
colorPack_BlueBlack,
|
||||||
|
colorPack_IndigoBlack,
|
||||||
|
colorPack_VioletBlack,
|
||||||
|
colorPack_WhiteBlack
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const COLOR_PACK fire PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -375,35 +375,19 @@ void Lights_Control_Task(void *parameters){
|
|||||||
case 1:
|
case 1:
|
||||||
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30);
|
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2: case 3: case 4: { // Timed Fill Animations
|
||||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, ledSettings[0].shift);
|
int timeDuration = (AnimEvent.AnimationIndex-1) * 1000;
|
||||||
|
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
|
||||||
whiteTimeout = 20;
|
whiteTimeout = 20;
|
||||||
break;
|
break;
|
||||||
case 3:
|
}
|
||||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, ledSettings[0].shift);
|
case 5: case 6: case 7: case 8: {// Fire Animations
|
||||||
whiteTimeout = 20;
|
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 5]; // copy const pack to mutable
|
||||||
break;
|
createFirePalette(firePalette, fp);
|
||||||
case 4:
|
|
||||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, ledSettings[0].shift);
|
|
||||||
whiteTimeout = 20;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange);
|
|
||||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||||
break;
|
break;
|
||||||
case 6:
|
}
|
||||||
createFirePalette(firePalette, CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen);
|
case 9: // Sec
|
||||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
|
||||||
break;
|
|
||||||
case 7:
|
|
||||||
createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue);
|
|
||||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
|
||||||
break;
|
|
||||||
case 8:
|
|
||||||
createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet);
|
|
||||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
|
||||||
break;
|
|
||||||
case 9:
|
|
||||||
loadColorPack(colorPack, colorPack_USA);
|
loadColorPack(colorPack, colorPack_USA);
|
||||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
|
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
|
||||||
break;
|
break;
|
||||||
@ -430,11 +414,14 @@ void Lights_Control_Task(void *parameters){
|
|||||||
case 15:
|
case 15:
|
||||||
loadColorPack(colorPack, colorPack_VioletBlack);
|
loadColorPack(colorPack, colorPack_VioletBlack);
|
||||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
|
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
|
||||||
|
case 16: case 17: case 18: case 19: case 20: {
|
||||||
|
//loadColorPack(colorPack, colorPack_RAINBOW);
|
||||||
|
int idx = AnimEvent.AnimationIndex - 16;
|
||||||
|
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, combo_colorPacks[idx], 80, RANDOM_DECAY, true, 1);
|
||||||
break;
|
break;
|
||||||
case 16:
|
}
|
||||||
loadColorPack(colorPack, colorPack_RAINBOW);
|
/*
|
||||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
|
/*
|
||||||
break;
|
|
||||||
case 17:
|
case 17:
|
||||||
loadColorPack(colorPack, colorPack_USA);
|
loadColorPack(colorPack, colorPack_USA);
|
||||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, LINEAR_DECAY, true, 1);
|
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, LINEAR_DECAY, true, 1);
|
||||||
@ -451,6 +438,7 @@ void Lights_Control_Task(void *parameters){
|
|||||||
loadColorPack(colorPack, colorPack_RAINBOW);
|
loadColorPack(colorPack, colorPack_RAINBOW);
|
||||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
|
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
|
||||||
break;
|
break;
|
||||||
|
*/
|
||||||
case 21:
|
case 21:
|
||||||
loadColorPack(colorPack, colorPack_RAINBOW);
|
loadColorPack(colorPack, colorPack_RAINBOW);
|
||||||
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 7000, 90);
|
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 7000, 90);
|
||||||
@ -472,6 +460,16 @@ void Lights_Control_Task(void *parameters){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack) {
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
if (i < 3) palette[i] = CRGB::Black;
|
||||||
|
else if (i < 7) palette[i] = colorPack.col[0];
|
||||||
|
else if (i < 10) palette[i] = colorPack.col[1];
|
||||||
|
else if (i < 15) palette[i] = colorPack.col[2];
|
||||||
|
else palette[i] = CRGB::White;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) {
|
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) {
|
||||||
for (uint8_t i = 0; i < 16; i++) {
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
if (i < 3) palette[i] = CRGB::Black;
|
if (i < 3) palette[i] = CRGB::Black;
|
||||||
@ -481,6 +479,7 @@ void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB co
|
|||||||
else palette[i] = CRGB::White;
|
else palette[i] = CRGB::White;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) {
|
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) {
|
||||||
memcpy_P(&dest, &src, sizeof(COLOR_PACK));
|
memcpy_P(&dest, &src, sizeof(COLOR_PACK));
|
||||||
|
|||||||
@ -574,6 +574,63 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Anim_ColorWipe(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) {
|
||||||
|
if (!leds || size <= 0 || colors.size <= 0) return;
|
||||||
|
|
||||||
|
int currentIndex = 0;
|
||||||
|
Animation_Loop(activeFlag, speed, [&]() -> int {
|
||||||
|
// Wipe color across the strip
|
||||||
|
fill_solid(leds, size, colors.col[currentIndex]);
|
||||||
|
FastLED.show();
|
||||||
|
|
||||||
|
// Move to the next color
|
||||||
|
currentIndex = (currentIndex + 1) % colors.size;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anim_Sparkle(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t sparkleChance) {
|
||||||
|
if (!leds || size <= 0 || colors.size <= 0) return;
|
||||||
|
|
||||||
|
Animation_Loop(activeFlag, speed, [&]() -> int {
|
||||||
|
// Randomly light up LEDs with colors from the color pack
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
if (getRandomValue(100) < sparkleChance) {
|
||||||
|
int colorIndex = getRandomValue(colors.size);
|
||||||
|
leds[i] = colors.col[colorIndex];
|
||||||
|
} else {
|
||||||
|
leds[i] = CRGB::Black; // Turn off LED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FastLED.show();
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anim_TheaterChase(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t spacing) {
|
||||||
|
if (!leds || size <= 0 || colors.size <= 0 || spacing == 0) return;
|
||||||
|
|
||||||
|
int colorIndex = 0;
|
||||||
|
Animation_Loop(activeFlag, speed, [&]() -> int {
|
||||||
|
// Clear all LEDs
|
||||||
|
fill_solid(leds, size, CRGB::Black);
|
||||||
|
|
||||||
|
// Light up every 'spacing' LED with the current color
|
||||||
|
for (int i = 0; i < size; i += spacing) {
|
||||||
|
leds[i] = colors.col[colorIndex];
|
||||||
|
}
|
||||||
|
FastLED.show();
|
||||||
|
|
||||||
|
// Move to the next color
|
||||||
|
colorIndex = (colorIndex + 1) % colors.size;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
uint32_t getRandomValue(uint32_t maxValue) {
|
uint32_t getRandomValue(uint32_t maxValue) {
|
||||||
return esp_random() % maxValue;
|
return esp_random() % maxValue;
|
||||||
}
|
}
|
||||||
@ -12,6 +12,7 @@
|
|||||||
static const char* TAG = "AppUpdater";
|
static const char* TAG = "AppUpdater";
|
||||||
TaskHandle_t Update_Task_Handle = NULL;
|
TaskHandle_t Update_Task_Handle = NULL;
|
||||||
TaskHandle_t versionCheckTask_Handle = NULL;
|
TaskHandle_t versionCheckTask_Handle = NULL;
|
||||||
|
volatile bool g_UpdateCancelFlag = false; // cancellation flag
|
||||||
|
|
||||||
// Queue handle for firmware update messages
|
// Queue handle for firmware update messages
|
||||||
//QueueHandle_t updateMsgQueue = NULL;
|
//QueueHandle_t updateMsgQueue = NULL;
|
||||||
@ -45,19 +46,30 @@ bool AppUpdater::checkManifest() {
|
|||||||
String url = buildUrl(manifestName);
|
String url = buildUrl(manifestName);
|
||||||
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||||
|
|
||||||
// Start the HTTP client and Send GET request for manifest
|
String payload;
|
||||||
HTTPClient http;
|
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
|
||||||
http.begin(url);
|
if(g_UpdateCancelFlag) return false;
|
||||||
int httpCode = http.GET();
|
HTTPClient http;
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
http.begin(url);
|
||||||
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
int httpCode = http.GET();
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
payload = http.getString();
|
||||||
|
http.end();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Manifest GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
http.end();
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
if(payload.isEmpty()){
|
||||||
|
ESP_LOGE(TAG, "Failed to fetch manifest after retries");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the response
|
if(payload.length() > MAX_MANIFEST_SIZE){
|
||||||
String payload = http.getString();
|
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
|
||||||
http.end();
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse JSON
|
// Parse JSON
|
||||||
DeserializationError error = deserializeJson(jsonManifest, payload);
|
DeserializationError error = deserializeJson(jsonManifest, payload);
|
||||||
@ -116,9 +128,13 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
|||||||
String url = buildUrl(remotePath);
|
String url = buildUrl(remotePath);
|
||||||
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
|
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
|
||||||
|
|
||||||
String localMd5 = getLocalMD5(localPath);
|
// Quick skip: if exists and size & MD5 match
|
||||||
|
bool skip = false;
|
||||||
if (localMd5.equals(expectedMd5)) {
|
if(fileSystem.exists(localPath)){
|
||||||
|
String localMd5 = getLocalMD5(localPath);
|
||||||
|
if(localMd5.equals(expectedMd5)) skip = true;
|
||||||
|
}
|
||||||
|
if(skip){
|
||||||
ESP_LOGI(TAG, "File already up to date: %s", localPath);
|
ESP_LOGI(TAG, "File already up to date: %s", localPath);
|
||||||
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
|
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
|
||||||
return true;
|
return true;
|
||||||
@ -126,12 +142,19 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
|||||||
|
|
||||||
// Start the download
|
// Start the download
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(url);
|
int httpCode = -1;
|
||||||
int httpCode = http.GET();
|
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
|
||||||
|
if(g_UpdateCancelFlag) return false;
|
||||||
|
http.begin(url);
|
||||||
|
httpCode = http.GET();
|
||||||
|
if(httpCode == HTTP_CODE_OK) break;
|
||||||
|
ESP_LOGW(TAG, "File GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
ESP_LOGE(TAG, "Download failed: %d", httpCode);
|
ESP_LOGE(TAG, "Download failed: %d", httpCode);
|
||||||
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
|
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
|
||||||
http.end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +195,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
|||||||
if (contentLength > 0) {
|
if (contentLength > 0) {
|
||||||
// Single pass with known content length
|
// Single pass with known content length
|
||||||
while (totalRead < contentLength) {
|
while (totalRead < contentLength) {
|
||||||
|
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
|
||||||
size_t available = stream->available();
|
size_t available = stream->available();
|
||||||
if (available) {
|
if (available) {
|
||||||
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
|
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
|
||||||
@ -187,13 +211,14 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
|||||||
|
|
||||||
md5.add(downloadBuffer.get(), readLen);
|
md5.add(downloadBuffer.get(), readLen);
|
||||||
totalRead += readLen;
|
totalRead += readLen;
|
||||||
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
|
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
|
||||||
}
|
}
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unknown content length: read until stream ends
|
// Unknown content length: read until stream ends
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
|
||||||
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||||
if (readLen == 0) {
|
if (readLen == 0) {
|
||||||
break;
|
break;
|
||||||
@ -207,7 +232,10 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
|||||||
md5.add(downloadBuffer.get(), readLen);
|
md5.add(downloadBuffer.get(), readLen);
|
||||||
totalRead += readLen;
|
totalRead += readLen;
|
||||||
// Progress unknown; emit periodic heartbeats at 0%
|
// Progress unknown; emit periodic heartbeats at 0%
|
||||||
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
// For unknown size, send heartbeats every ~16KB
|
||||||
|
if((totalRead & 0x3FFF) == 0){
|
||||||
|
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||||
|
}
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,12 +245,15 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
|||||||
String calculatedMd5 = md5.toString();
|
String calculatedMd5 = md5.toString();
|
||||||
|
|
||||||
// Verify MD5 hash
|
// Verify MD5 hash
|
||||||
|
updateProgress(UpdateStatus::VERIFYING, 90, localPath);
|
||||||
if (!calculatedMd5.equals(expectedMd5)) {
|
if (!calculatedMd5.equals(expectedMd5)) {
|
||||||
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
|
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
|
||||||
fileSystem.remove(tempPath.c_str());
|
fileSystem.remove(tempPath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProgress(UpdateStatus::VERIFYING, 95, localPath);
|
||||||
|
|
||||||
// Replace original file with verified temp file
|
// Replace original file with verified temp file
|
||||||
if (fileSystem.exists(localPath)) {
|
if (fileSystem.exists(localPath)) {
|
||||||
fileSystem.remove(localPath);
|
fileSystem.remove(localPath);
|
||||||
@ -233,6 +264,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProgress(UpdateStatus::VERIFYING, 100, localPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,12 +334,19 @@ bool AppUpdater::updateApp() {
|
|||||||
|
|
||||||
// Download the firmware
|
// Download the firmware
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(firmwareUrl);
|
int httpCode = -1;
|
||||||
int httpCode = http.GET();
|
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
|
||||||
|
if(g_UpdateCancelFlag) return false;
|
||||||
|
http.begin(firmwareUrl);
|
||||||
|
httpCode = http.GET();
|
||||||
|
if(httpCode == HTTP_CODE_OK) break;
|
||||||
|
ESP_LOGW(TAG, "Firmware GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
||||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
||||||
http.end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,6 +368,7 @@ bool AppUpdater::updateApp() {
|
|||||||
if (firmwareSize > 0) {
|
if (firmwareSize > 0) {
|
||||||
size_t remaining = firmwareSize;
|
size_t remaining = firmwareSize;
|
||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
|
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
|
||||||
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
|
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
|
||||||
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
|
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
|
||||||
|
|
||||||
@ -355,6 +395,7 @@ bool AppUpdater::updateApp() {
|
|||||||
} else {
|
} else {
|
||||||
// Unknown size: stream until end
|
// Unknown size: stream until end
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
|
||||||
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||||
if (read == 0) break;
|
if (read == 0) break;
|
||||||
md5.add(downloadBuffer.get(), read);
|
md5.add(downloadBuffer.get(), read);
|
||||||
@ -371,6 +412,7 @@ bool AppUpdater::updateApp() {
|
|||||||
// Verify MD5
|
// Verify MD5
|
||||||
md5.calculate();
|
md5.calculate();
|
||||||
String calculatedMd5 = md5.toString();
|
String calculatedMd5 = md5.toString();
|
||||||
|
updateProgress(UpdateStatus::VERIFYING, 95, "firmware");
|
||||||
if (!calculatedMd5.equals(expectedMd5)) {
|
if (!calculatedMd5.equals(expectedMd5)) {
|
||||||
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
|
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
|
||||||
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
|
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
|
||||||
@ -388,7 +430,7 @@ bool AppUpdater::updateApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
|
updateProgress(UpdateStatus::COMPLETE, 100, "Firmware: Complete");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +501,6 @@ void firmwareUpdateTask(void* parameter) {
|
|||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||||
}
|
}
|
||||||
end:
|
|
||||||
delete updater;
|
delete updater;
|
||||||
Update_Task_Handle = NULL;
|
Update_Task_Handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
@ -474,15 +515,16 @@ void startVersionCheckTask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void versionCheckTask(void* parameter){
|
void versionCheckTask(void* parameter){
|
||||||
|
|
||||||
if(updateUrl == ""){
|
if(updateUrl == ""){
|
||||||
loadUpdateJson();
|
loadUpdateJson();
|
||||||
}
|
}
|
||||||
|
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
|
||||||
if(checkManifest(otaVersion) == false){
|
if(!updater.checkManifest()){
|
||||||
ESP_LOGE(TAG, "Error checking manifest");
|
ESP_LOGE(TAG, "Version check: manifest fetch failed");
|
||||||
|
} else {
|
||||||
|
otaVersion = updater.otaVersion; // capture remote
|
||||||
|
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
versionCheckTask_Handle = NULL;
|
versionCheckTask_Handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
@ -589,51 +631,7 @@ void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
|
|||||||
bleUpgrade_send_message(message);
|
bleUpgrade_send_message(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool checkManifest(Version& remoteVersion) {
|
// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
|
||||||
const char* TAG = "manifestCheck";
|
|
||||||
String url = updateUrl + "update.json";
|
|
||||||
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
|
||||||
|
|
||||||
// Start the HTTP client and send GET request for manifest
|
|
||||||
HTTPClient http;
|
|
||||||
http.begin(url);
|
|
||||||
int httpCode = http.GET();
|
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
|
||||||
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
|
||||||
http.end();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the response
|
|
||||||
String payload = http.getString();
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
//ESP_LOGD(TAG, "%s", payload.c_str());
|
|
||||||
|
|
||||||
// Parse JSON
|
|
||||||
JsonDocument jsonManifest;
|
|
||||||
DeserializationError error = deserializeJson(jsonManifest, payload);
|
|
||||||
if (error) {
|
|
||||||
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for version section
|
|
||||||
JsonObject jsonVersion = jsonManifest["version"];
|
|
||||||
|
|
||||||
if (jsonVersion.isNull()) {
|
|
||||||
ESP_LOGE(TAG, "No version section in manifest");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the remote version
|
|
||||||
byte major = jsonVersion["major"] | 0;
|
|
||||||
byte minor = jsonVersion["minor"] | 0;
|
|
||||||
byte patch = jsonVersion["patch"] | 0;
|
|
||||||
remoteVersion = {major, minor, patch};
|
|
||||||
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
void setup() {
|
void setup() {
|
||||||
|
|||||||
@ -60,6 +60,7 @@ uint8_t calculateChecksum(const uint8_t bArr[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Class for handling characteristic events
|
// Class for handling characteristic events
|
||||||
|
/*
|
||||||
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
|
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
|
||||||
void onWrite(NimBLECharacteristic *pCharacteristic) override {
|
void onWrite(NimBLECharacteristic *pCharacteristic) override {
|
||||||
std::string rawValue = pCharacteristic->getValue();
|
std::string rawValue = pCharacteristic->getValue();
|
||||||
@ -75,6 +76,54 @@ class SP110ECallbacks : public NimBLECharacteristicCallbacks {
|
|||||||
process_BLE_SP110E_Command(value, length, pCharacteristic);
|
process_BLE_SP110E_Command(value, length, pCharacteristic);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Class for handling characteristic events
|
||||||
|
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
|
||||||
|
public:
|
||||||
|
void onWrite(NimBLECharacteristic* pCharacteristic) override {
|
||||||
|
if (!pCharacteristic) return;
|
||||||
|
|
||||||
|
std::string raw = pCharacteristic->getValue(); // NimBLE copies internally
|
||||||
|
size_t len = raw.size();
|
||||||
|
if (len == 0) {
|
||||||
|
ESP_LOGW(tag, "Write received with zero-length payload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* data = reinterpret_cast<const uint8_t*>(raw.data());
|
||||||
|
|
||||||
|
// Log up to first 16 bytes to avoid log spam
|
||||||
|
//logBytes(data, len);
|
||||||
|
|
||||||
|
// Clamp length passed to command processor (its param is uint8_t)
|
||||||
|
uint8_t procLen = static_cast<uint8_t>(len > 255 ? 255 : len);
|
||||||
|
|
||||||
|
// Forward to any subscribed mirror clients first (raw payload)
|
||||||
|
sendToAllClients(data, procLen);
|
||||||
|
|
||||||
|
// Process command (may generate response/notify on same characteristic)
|
||||||
|
process_BLE_SP110E_Command(data, procLen, pCharacteristic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void logBytes(const uint8_t* data, size_t len) {
|
||||||
|
if (!data) return;
|
||||||
|
char buf[3 * 16 + 1];
|
||||||
|
size_t toPrint = len > 16 ? 16 : len;
|
||||||
|
char* p = buf;
|
||||||
|
for (size_t i = 0; i < toPrint; ++i) {
|
||||||
|
sprintf(p, "%02X ", data[i]);
|
||||||
|
p += 3;
|
||||||
|
}
|
||||||
|
*p = 0;
|
||||||
|
if (len > toPrint) {
|
||||||
|
ESP_LOGI(tag, "Data (%zu bytes): %s...", len, buf);
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(tag, "Data (%zu bytes): %s", len, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class LightStickCallbacks : public NimBLECharacteristicCallbacks {
|
class LightStickCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
void onRead(NimBLECharacteristic *pCharacteristic) override {
|
void onRead(NimBLECharacteristic *pCharacteristic) override {
|
||||||
@ -83,7 +132,34 @@ class LightStickCallbacks : public NimBLECharacteristicCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to send data to all connected clients in chunks based on MTU
|
||||||
|
void sendToAllClients(const uint8_t* data, size_t len) {
|
||||||
|
if (!pStickCharacteristic || !data || len == 0) return;
|
||||||
|
|
||||||
|
// Skip if no subscribed clients (if API available)
|
||||||
|
#if defined(NIMBLE_INCLUDED) || true
|
||||||
|
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
|
||||||
|
if (pStickCharacteristic->getSubscribedCount() == 0) return;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Determine a safe chunk size based on (last) negotiated MTU; fallback to 20.
|
||||||
|
uint16_t mtu = NimBLEDevice::getMTU(); // Typically 23 default -> 20 payload
|
||||||
|
size_t maxChunk = (mtu > 3) ? (mtu - 3) : 20;
|
||||||
|
if (maxChunk == 0) maxChunk = 20;
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < len) {
|
||||||
|
size_t chunk = len - offset;
|
||||||
|
if (chunk > maxChunk) chunk = maxChunk;
|
||||||
|
pStickCharacteristic->setValue(data + offset, chunk);
|
||||||
|
// notify() returns void in this NimBLE version, so just call it without checking a return value
|
||||||
|
pStickCharacteristic->notify();
|
||||||
|
offset += chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
void sendToAllClients(const uint8_t *data, size_t len) {
|
void sendToAllClients(const uint8_t *data, size_t len) {
|
||||||
// Check if the characteristic is valid and has subscribed clients
|
// Check if the characteristic is valid and has subscribed clients
|
||||||
if (pStickCharacteristic != nullptr) {
|
if (pStickCharacteristic != nullptr) {
|
||||||
@ -92,6 +168,7 @@ void sendToAllClients(const uint8_t *data, size_t len) {
|
|||||||
pStickCharacteristic->notify();
|
pStickCharacteristic->notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
|
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
|
||||||
@ -115,6 +192,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
|
|||||||
//ESP_LOGI(tag, "Lights OFF");
|
//ESP_LOGI(tag, "Lights OFF");
|
||||||
break;
|
break;
|
||||||
case SET_STATIC_COLOR:
|
case SET_STATIC_COLOR:
|
||||||
|
if(len < 7) {
|
||||||
|
ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
led_status.red = val[1];
|
led_status.red = val[1];
|
||||||
led_status.green = val[2];
|
led_status.green = val[2];
|
||||||
led_status.blue = val[0];
|
led_status.blue = val[0];
|
||||||
@ -122,6 +203,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
|
|||||||
//ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
|
//ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
|
||||||
break;
|
break;
|
||||||
case SET_BRIGHT:
|
case SET_BRIGHT:
|
||||||
|
if(len < 5) {
|
||||||
|
ESP_LOGW(tag, "SET_BRIGHT command requires 1 parameter (brightness)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
led_status.bright = val[0];
|
led_status.bright = val[0];
|
||||||
Lights_Set_Brightness(val[0]);
|
Lights_Set_Brightness(val[0]);
|
||||||
//ESP_LOGI(tag, "Bright set to %d", led_status.bright);
|
//ESP_LOGI(tag, "Bright set to %d", led_status.bright);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ static const char* tag = "BleServer";
|
|||||||
|
|
||||||
|
|
||||||
// Class for handling server events
|
// Class for handling server events
|
||||||
|
/*
|
||||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
void onConnect(NimBLEServer* pServer) override {
|
void onConnect(NimBLEServer* pServer) override {
|
||||||
ESP_LOGI(tag, "Client connected");
|
ESP_LOGI(tag, "Client connected");
|
||||||
@ -31,7 +32,40 @@ class ServerCallbacks : public NimBLEServerCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
public:
|
||||||
|
void onConnect(NimBLEServer* /*pServer*/) override {
|
||||||
|
ESP_LOGI(tag, "Client connected");
|
||||||
|
ensureAdvertising("onConnect");
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect(NimBLEServer* /*pServer*/) override {
|
||||||
|
ESP_LOGI(tag, "Client disconnected");
|
||||||
|
ensureAdvertising("onDisconnect");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureAdvertising(const char* reason) {
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
if (!adv) {
|
||||||
|
ESP_LOGE(tag, "[%s] Advertising object unavailable", reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (adv->isAdvertising()) {
|
||||||
|
ESP_LOGD(tag, "[%s] Advertising already running", reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (adv->start()) {
|
||||||
|
ESP_LOGI(tag, "[%s] Advertising (re)started", reason);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(tag, "[%s] Failed to start advertising", reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
||||||
|
|
||||||
ESP_LOGI(tag, "Initializing BLE...");
|
ESP_LOGI(tag, "Initializing BLE...");
|
||||||
@ -71,4 +105,57 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
|||||||
} else {
|
} else {
|
||||||
ESP_LOGE(tag, "Failed to get advertising object");
|
ESP_LOGE(tag, "Failed to get advertising object");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
void Init_BleServer(bool isSP110EActive, bool isUpgradeActive) {
|
||||||
|
ESP_LOGI(tag, "Initializing BLE...");
|
||||||
|
|
||||||
|
// Initialize BLE device only once
|
||||||
|
static bool deviceInitialized = false;
|
||||||
|
if (!deviceInitialized) {
|
||||||
|
NimBLEDevice::init(BTDeviceName.c_str());
|
||||||
|
deviceInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create server only once
|
||||||
|
static NimBLEServer* pServer = nullptr;
|
||||||
|
if (!pServer) {
|
||||||
|
pServer = NimBLEDevice::createServer();
|
||||||
|
if (!pServer) {
|
||||||
|
ESP_LOGE(tag, "Failed to create BLE server");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static ServerCallbacks serverCallbacks;
|
||||||
|
pServer->setCallbacks(&serverCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add services only once (no removal logic if later flags become false)
|
||||||
|
static bool sp110eAdded = false;
|
||||||
|
if (isSP110EActive && !sp110eAdded) {
|
||||||
|
Init_BLE_SP110E(pServer);
|
||||||
|
sp110eAdded = true;
|
||||||
|
ESP_LOGI(tag, "SP110E service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool upgradeAdded = false;
|
||||||
|
if (isUpgradeActive && !upgradeAdded) {
|
||||||
|
Init_UpgradeBLEService(pServer);
|
||||||
|
upgradeAdded = true;
|
||||||
|
ESP_LOGI(tag, "Upgrade service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start / ensure advertising
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
if (!adv) {
|
||||||
|
ESP_LOGE(tag, "Failed to get advertising object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!adv->isAdvertising()) {
|
||||||
|
if (!adv->start()) {
|
||||||
|
ESP_LOGE(tag, "Failed to start advertising");
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(tag, "Advertising started");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
#include "ColorPalettes.h"
|
#include "ColorPalettes.h"
|
||||||
|
|
||||||
|
|
||||||
|
extern const COLOR_PACK combo_colorPacks[] PROGMEM;
|
||||||
|
extern const COLOR_PACK single_colorPacks[] PROGMEM;
|
||||||
|
extern const COLOR_PACK fireColorPacks[] PROGMEM;
|
||||||
|
|
||||||
void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette) {
|
void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette) {
|
||||||
customPalette = CRGBPalette16(
|
customPalette = CRGBPalette16(
|
||||||
CRGB::Red, CRGB::Yellow, CRGB::Violet,
|
CRGB::Red, CRGB::Yellow, CRGB::Violet,
|
||||||
|
|||||||
21
src/main.cpp
21
src/main.cpp
@ -222,19 +222,6 @@ void loop()
|
|||||||
boardButtons[i]->tick();
|
boardButtons[i]->tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
{
|
|
||||||
boardButtons[0]->tick();
|
|
||||||
}
|
|
||||||
if (boardButtons[1] != NULL)
|
|
||||||
{
|
|
||||||
boardButtons[1]->tick();
|
|
||||||
}
|
|
||||||
if (boardButtons[2] != NULL)
|
|
||||||
{
|
|
||||||
boardButtons[2]->tick();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature Monitor
|
// Temperature Monitor
|
||||||
@ -246,7 +233,7 @@ void loop()
|
|||||||
if (sys_settings.tSensorSettings.enabled)
|
if (sys_settings.tSensorSettings.enabled)
|
||||||
{
|
{
|
||||||
boardTemperature = tSensor->readTemperatureF();
|
boardTemperature = tSensor->readTemperatureF();
|
||||||
// ESP_LOGD(tag, "Board T: %F", boardTemperature);
|
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fan Control
|
// Fan Control
|
||||||
@ -327,10 +314,12 @@ void loop()
|
|||||||
// Turn off white light after timeout
|
// Turn off white light after timeout
|
||||||
ON_EVERY_N_MILLISECONDS(100)
|
ON_EVERY_N_MILLISECONDS(100)
|
||||||
{
|
{
|
||||||
if(whiteTimeout > 0){
|
// Only decrement if timeout is active
|
||||||
|
if (whiteTimeout > 0) {
|
||||||
whiteTimeout--;
|
whiteTimeout--;
|
||||||
if(whiteTimeout == 0){
|
if (whiteTimeout == 0) {
|
||||||
Lights_Set_White(0);
|
Lights_Set_White(0);
|
||||||
|
ESP_LOGD(tag, "White light timeout triggered");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/my_wifi.cpp
114
src/my_wifi.cpp
@ -44,7 +44,7 @@ String dirDropdownOptions((char *)0);
|
|||||||
String savePath((char *)0); // needed for storing file when editing a file
|
String savePath((char *)0); // needed for storing file when editing a file
|
||||||
String savePathInput((char *)0);
|
String savePathInput((char *)0);
|
||||||
const char *http_username = "admin";
|
const char *http_username = "admin";
|
||||||
const char *http_password = "admin";
|
const char *http_password = "12345678";
|
||||||
const char *param_delete_path = "delete-path";
|
const char *param_delete_path = "delete-path";
|
||||||
const char *param_edit_path = "edit-path";
|
const char *param_edit_path = "edit-path";
|
||||||
const char *param_dir_pad = "dir-path";
|
const char *param_dir_pad = "dir-path";
|
||||||
@ -109,6 +109,53 @@ void Wifi_Init()
|
|||||||
// Wifi_Scan_for_Networks();
|
// Wifi_Scan_for_Networks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Wifi_Load_Settings(String path)
|
||||||
|
{
|
||||||
|
// Load WiFi settings
|
||||||
|
File file = LittleFS.open(path, "r");
|
||||||
|
if (!file)
|
||||||
|
{
|
||||||
|
ESP_LOGE(tag, "Error opening %s", path.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject wifiJson = doc.as<JsonObject>();
|
||||||
|
if (wifiJson.isNull())
|
||||||
|
{
|
||||||
|
ESP_LOGE(tag, "%s is empty", path.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load AP settings
|
||||||
|
JsonObject apJson = wifiJson["wifi-ap"];
|
||||||
|
if (!apJson.isNull())
|
||||||
|
{
|
||||||
|
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
|
||||||
|
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
|
||||||
|
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
|
||||||
|
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
|
||||||
|
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Client settings
|
||||||
|
JsonObject clientJson = wifiJson["wifi-client"];
|
||||||
|
if (!apJson.isNull())
|
||||||
|
{
|
||||||
|
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
|
||||||
|
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool StartWifiConnectTask(String ssid = "", String pass = "")
|
bool StartWifiConnectTask(String ssid = "", String pass = "")
|
||||||
{
|
{
|
||||||
if (ssid.isEmpty() || pass.length() < 8)
|
if (ssid.isEmpty() || pass.length() < 8)
|
||||||
@ -257,53 +304,6 @@ bool Wifi_Save_Credentials(String path)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Wifi_Load_Settings(String path)
|
|
||||||
{
|
|
||||||
// Load WiFi settings
|
|
||||||
File file = LittleFS.open(path, "r");
|
|
||||||
if (!file)
|
|
||||||
{
|
|
||||||
ESP_LOGE(tag, "Error opening %s", path.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonDocument doc;
|
|
||||||
DeserializationError error = deserializeJson(doc, file);
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
{
|
|
||||||
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject wifiJson = doc.as<JsonObject>();
|
|
||||||
if (wifiJson.isNull())
|
|
||||||
{
|
|
||||||
ESP_LOGE(tag, "%s is empty", path.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load AP settings
|
|
||||||
JsonObject apJson = wifiJson["wifi-ap"];
|
|
||||||
if (!apJson.isNull())
|
|
||||||
{
|
|
||||||
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
|
|
||||||
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
|
|
||||||
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
|
|
||||||
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
|
|
||||||
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Client settings
|
|
||||||
JsonObject clientJson = wifiJson["wifi-client"];
|
|
||||||
if (!apJson.isNull())
|
|
||||||
{
|
|
||||||
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
|
|
||||||
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Wifi_Scan_for_Networks()
|
void Wifi_Scan_for_Networks()
|
||||||
{
|
{
|
||||||
// Start a scan for available networks
|
// Start a scan for available networks
|
||||||
@ -577,11 +577,19 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
|||||||
// Firmware Update Handlers
|
// Firmware Update Handlers
|
||||||
server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
//String newVersion;
|
// Ensure updateUrl is loaded (function resides in AppUpgrade.cpp)
|
||||||
loadUpdateJson();
|
loadUpdateJson();
|
||||||
//bool avai = checkManifest(FIRMWARE_VERSION, newVersion);
|
// Pass nullptr bucket to use internally loaded default + subsequently set base via setBaseUrl if needed
|
||||||
checkManifest(otaVersion);
|
AppUpdater updater(LittleFS, localVersion, nullptr, "update.json", "firmware.bin");
|
||||||
bool avail = otaVersion > localVersion;
|
// If a dynamic URL was loaded, override base
|
||||||
|
extern String updateUrl; // declared in AppUpgrade.cpp
|
||||||
|
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
|
||||||
|
if(!updater.checkManifest()){
|
||||||
|
ESP_LOGE(tag, "Manifest check failed via /upgrade/check");
|
||||||
|
} else {
|
||||||
|
otaVersion = updater.otaVersion;
|
||||||
|
}
|
||||||
|
bool avail = otaVersion > localVersion;
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["currentVersion"] = localVersion.toString();
|
doc["currentVersion"] = localVersion.toString();
|
||||||
|
|||||||
673
temporary/AppUpgrade orig.cpp
Normal file
673
temporary/AppUpgrade orig.cpp
Normal file
@ -0,0 +1,673 @@
|
|||||||
|
#include "AppUpgrade.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <MD5Builder.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <memory>
|
||||||
|
#include "global.h"
|
||||||
|
#include "JsonConstrain.h"
|
||||||
|
#include "BLE_UpdateService.h"
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <Update.h>
|
||||||
|
|
||||||
|
static const char* TAG = "AppUpdater";
|
||||||
|
TaskHandle_t Update_Task_Handle = NULL;
|
||||||
|
TaskHandle_t versionCheckTask_Handle = NULL;
|
||||||
|
|
||||||
|
// Queue handle for firmware update messages
|
||||||
|
//QueueHandle_t updateMsgQueue = NULL;
|
||||||
|
|
||||||
|
String updateUrl = "";
|
||||||
|
|
||||||
|
Version otaVersion;
|
||||||
|
|
||||||
|
|
||||||
|
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
|
||||||
|
: localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
|
||||||
|
{
|
||||||
|
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
|
||||||
|
// Ensure baseUrl ends with a single '/'
|
||||||
|
if(!baseUrl.endsWith("/")) baseUrl += "/";
|
||||||
|
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
|
||||||
|
progressCb = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
|
||||||
|
status = newStatus;
|
||||||
|
if (progressCb) {
|
||||||
|
progressCb(status, percentage, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::checkManifest() {
|
||||||
|
String url = buildUrl(manifestName);
|
||||||
|
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||||
|
|
||||||
|
// Start the HTTP client and Send GET request for manifest
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response
|
||||||
|
String payload = http.getString();
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
DeserializationError error = deserializeJson(jsonManifest, payload);
|
||||||
|
ESP_LOGD(TAG, "Manifest deserialized");
|
||||||
|
if (error) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for files section
|
||||||
|
jsonFilesArray = jsonManifest["files"];
|
||||||
|
if (jsonFilesArray.isNull()) {
|
||||||
|
ESP_LOGE(TAG, "No files section in manifest");
|
||||||
|
return false;
|
||||||
|
}else{
|
||||||
|
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for version section
|
||||||
|
JsonObject jsonVersion = jsonManifest["version"];
|
||||||
|
ESP_LOGD(TAG, "Version section found");
|
||||||
|
if (jsonVersion.isNull()) {
|
||||||
|
ESP_LOGE(TAG, "No version section in manifest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the remote version
|
||||||
|
byte major = jsonVersion["major"] | 0;
|
||||||
|
byte minor = jsonVersion["minor"] | 0;
|
||||||
|
byte patch = jsonVersion["patch"] | 0;
|
||||||
|
otaVersion = {major, minor, patch};
|
||||||
|
|
||||||
|
//Version localVersion;
|
||||||
|
//::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch);
|
||||||
|
|
||||||
|
// Check if an update is available
|
||||||
|
updateAvailable = false;
|
||||||
|
// Only mark update available if remote is strictly newer than local
|
||||||
|
if (otaVersion <= localVersion) {
|
||||||
|
ESP_LOGI(TAG, "No updates available");
|
||||||
|
return false;
|
||||||
|
}else{
|
||||||
|
updateAvailable = true;
|
||||||
|
ESP_LOGD(TAG, "Update available");
|
||||||
|
}
|
||||||
|
|
||||||
|
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
|
||||||
|
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||||
|
|
||||||
|
// Construct full URL
|
||||||
|
String url = buildUrl(remotePath);
|
||||||
|
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
|
||||||
|
|
||||||
|
String localMd5 = getLocalMD5(localPath);
|
||||||
|
|
||||||
|
if (localMd5.equals(expectedMd5)) {
|
||||||
|
ESP_LOGI(TAG, "File already up to date: %s", localPath);
|
||||||
|
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the download
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "Download failed: %d", httpCode);
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the stream and content length
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
size_t contentLength = http.getSize();
|
||||||
|
|
||||||
|
// Verify and save the file
|
||||||
|
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
|
||||||
|
http.end();
|
||||||
|
if(!success){
|
||||||
|
updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed");
|
||||||
|
}else{
|
||||||
|
updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
|
||||||
|
{
|
||||||
|
MD5Builder md5;
|
||||||
|
md5.begin();
|
||||||
|
size_t totalRead = 0;
|
||||||
|
|
||||||
|
// Create temporary filename
|
||||||
|
String tempPath = String(localPath) + ".tmp";
|
||||||
|
|
||||||
|
// Open temporary file for writing
|
||||||
|
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
|
||||||
|
if (!file) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open temporary file for writing");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||||
|
|
||||||
|
if (contentLength > 0) {
|
||||||
|
// Single pass with known content length
|
||||||
|
while (totalRead < contentLength) {
|
||||||
|
size_t available = stream->available();
|
||||||
|
if (available) {
|
||||||
|
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
|
||||||
|
|
||||||
|
// Write to temp file and update MD5
|
||||||
|
if (file.write(downloadBuffer.get(), readLen) != readLen) {
|
||||||
|
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
fileSystem.remove(tempPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
md5.add(downloadBuffer.get(), readLen);
|
||||||
|
totalRead += readLen;
|
||||||
|
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
|
||||||
|
}
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown content length: read until stream ends
|
||||||
|
for (;;) {
|
||||||
|
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||||
|
if (readLen == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (file.write(downloadBuffer.get(), readLen) != readLen) {
|
||||||
|
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||||
|
file.close();
|
||||||
|
fileSystem.remove(tempPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
md5.add(downloadBuffer.get(), readLen);
|
||||||
|
totalRead += readLen;
|
||||||
|
// Progress unknown; emit periodic heartbeats at 0%
|
||||||
|
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
md5.calculate();
|
||||||
|
String calculatedMd5 = md5.toString();
|
||||||
|
|
||||||
|
// Verify MD5 hash
|
||||||
|
if (!calculatedMd5.equals(expectedMd5)) {
|
||||||
|
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
|
||||||
|
fileSystem.remove(tempPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace original file with verified temp file
|
||||||
|
if (fileSystem.exists(localPath)) {
|
||||||
|
fileSystem.remove(localPath);
|
||||||
|
}
|
||||||
|
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to rename temporary file");
|
||||||
|
fileSystem.remove(tempPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String AppUpdater::getLocalMD5(const char* filePath){
|
||||||
|
File file = fileSystem.open(filePath, "r");
|
||||||
|
if(!file){
|
||||||
|
ESP_LOGE(TAG, "Error opening %s...", filePath);
|
||||||
|
return String();
|
||||||
|
}
|
||||||
|
|
||||||
|
MD5Builder md5Builder;
|
||||||
|
md5Builder.begin();
|
||||||
|
size_t fileSize = file.size();
|
||||||
|
size_t totalRead = 0;
|
||||||
|
size_t readLen = 0;
|
||||||
|
while (totalRead < fileSize) {
|
||||||
|
readLen = file.readBytes(reinterpret_cast<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
|
||||||
|
md5Builder.add(downloadBuffer.get(), readLen);
|
||||||
|
totalRead += readLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
md5Builder.calculate();
|
||||||
|
file.close();
|
||||||
|
return md5Builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::updateFilesArray() {
|
||||||
|
int successCount = 0;
|
||||||
|
int totalFiles = jsonFilesArray.size();
|
||||||
|
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
|
||||||
|
|
||||||
|
// Iterate over each file entry in the manifest
|
||||||
|
for (JsonObject file : jsonFilesArray) {
|
||||||
|
const char* remotePath = file["remote"];
|
||||||
|
const char* localPath = file["local"];
|
||||||
|
const char* expectedMd5 = file["md5"];
|
||||||
|
|
||||||
|
// Skip invalid entries
|
||||||
|
if (!remotePath || !localPath || !expectedMd5) {
|
||||||
|
ESP_LOGE(TAG, "Invalid file entry in manifest");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to update the file
|
||||||
|
if (updateFile(remotePath, localPath, expectedMd5)) {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
|
||||||
|
return successCount == totalFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::updateApp() {
|
||||||
|
updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update");
|
||||||
|
|
||||||
|
// Check for firmware section in manifest
|
||||||
|
if (!jsonManifest["firmware"].is<JsonObject>() || !jsonManifest["firmware"]["md5"].is<const char*>()) {
|
||||||
|
ESP_LOGE(TAG, "Invalid firmware section in manifest");
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the firmware MD5 hash and URL
|
||||||
|
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
|
||||||
|
String firmwareUrl = buildUrl(appName);
|
||||||
|
|
||||||
|
// Download the firmware
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(firmwareUrl);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check available space
|
||||||
|
size_t firmwareSize = http.getSize();
|
||||||
|
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
|
||||||
|
ESP_LOGE(TAG, "Firmware: Not enough space for update");
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up MD5 checking
|
||||||
|
MD5Builder md5;
|
||||||
|
md5.begin();
|
||||||
|
|
||||||
|
// Download and verify firmware
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
if (firmwareSize > 0) {
|
||||||
|
size_t remaining = firmwareSize;
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
|
||||||
|
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
|
||||||
|
|
||||||
|
// Check for timeout
|
||||||
|
if (read == 0) {
|
||||||
|
ESP_LOGE(TAG, "Read timeout");
|
||||||
|
Update.abort();
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update MD5 and write firmware
|
||||||
|
md5.add(downloadBuffer.get(), read);
|
||||||
|
if (Update.write(downloadBuffer.get(), read) != read) {
|
||||||
|
ESP_LOGE(TAG, "Write failed");
|
||||||
|
Update.abort();
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= read;
|
||||||
|
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown size: stream until end
|
||||||
|
for (;;) {
|
||||||
|
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||||
|
if (read == 0) break;
|
||||||
|
md5.add(downloadBuffer.get(), read);
|
||||||
|
if (Update.write(downloadBuffer.get(), read) != read) {
|
||||||
|
ESP_LOGE(TAG, "Write failed");
|
||||||
|
Update.abort();
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify MD5
|
||||||
|
md5.calculate();
|
||||||
|
String calculatedMd5 = md5.toString();
|
||||||
|
if (!calculatedMd5.equals(expectedMd5)) {
|
||||||
|
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
|
||||||
|
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
|
||||||
|
Update.abort();
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish update
|
||||||
|
if (!Update.end()) {
|
||||||
|
ESP_LOGE(TAG, "Update end failed");
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::IsUpdateAvailable(){
|
||||||
|
return updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
String AppUpdater::buildUrl(const char* path) const {
|
||||||
|
if(!path || !*path) return baseUrl; // just base
|
||||||
|
String p(path);
|
||||||
|
// If already absolute URL, pass through
|
||||||
|
if(p.startsWith("http://") || p.startsWith("https://")) return p;
|
||||||
|
// Strip leading slashes to avoid double
|
||||||
|
while(p.startsWith("/")) p.remove(0,1);
|
||||||
|
// Ensure baseUrl has single trailing slash
|
||||||
|
String b = baseUrl;
|
||||||
|
if(!b.endsWith("/")) b += "/";
|
||||||
|
return b + p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AsyncEventSource* eventProgress = nullptr;
|
||||||
|
void startFirmwareUpdateTask(AsyncEventSource* evProg) {
|
||||||
|
eventProgress = evProg;
|
||||||
|
if(Update_Task_Handle) {
|
||||||
|
ESP_LOGW(TAG, "Firmware update task already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void firmwareUpdateTask(void* parameter) {
|
||||||
|
static const char* TAG = "UpdateTask";
|
||||||
|
AppUpdater* updater = nullptr;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadUpdateJson();
|
||||||
|
|
||||||
|
// Initialize updater
|
||||||
|
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
|
||||||
|
updater->setProgressCallback(updateProgress);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
|
||||||
|
|
||||||
|
// Check and perform updates
|
||||||
|
if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); }
|
||||||
|
|
||||||
|
if (updater->IsUpdateAvailable()) {
|
||||||
|
ESP_LOGI(TAG, "Update available, updating files...");
|
||||||
|
|
||||||
|
if (!updater->updateFilesArray()) {
|
||||||
|
throw std::runtime_error("Failed to update files");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Updating firmware...");
|
||||||
|
if (!updater->updateApp()) {
|
||||||
|
throw std::runtime_error("Failed to update firmware");
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Update successful, restarting...");
|
||||||
|
|
||||||
|
sendUpdateMessage("Restarting ", true, 100);
|
||||||
|
vTaskDelay(2000);
|
||||||
|
|
||||||
|
ESP.restart();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||||
|
}
|
||||||
|
end:
|
||||||
|
delete updater;
|
||||||
|
Update_Task_Handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startVersionCheckTask() {
|
||||||
|
if(versionCheckTask_Handle != NULL) {
|
||||||
|
ESP_LOGW(TAG, "Version Check Tak already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void versionCheckTask(void* parameter){
|
||||||
|
|
||||||
|
if(updateUrl == ""){
|
||||||
|
loadUpdateJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(checkManifest(otaVersion) == false){
|
||||||
|
ESP_LOGE(TAG, "Error checking manifest");
|
||||||
|
}
|
||||||
|
|
||||||
|
versionCheckTask_Handle = NULL;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadUpdateJson(void) {
|
||||||
|
try {
|
||||||
|
ESP_LOGD(TAG, "loadUpdateJaon function...");
|
||||||
|
if(updateUrl == "") {
|
||||||
|
String updateJsonPath = "/system/update.json";
|
||||||
|
|
||||||
|
// Read and parse update.json
|
||||||
|
File file = LittleFS.open(updateJsonPath);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to open update.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (error) { throw std::runtime_error("Failed to parse update.json"); }
|
||||||
|
|
||||||
|
// Get update configuration
|
||||||
|
JsonObject jObj = doc.as<JsonObject>();
|
||||||
|
String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
|
||||||
|
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
|
||||||
|
updateUrl = baseUrl + folderName;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
|
||||||
|
|
||||||
|
char buffer[128];
|
||||||
|
const char* msg;
|
||||||
|
bool isComplete = false;
|
||||||
|
|
||||||
|
const char* safeMsg = message ? message : "";
|
||||||
|
switch (newStatus) {
|
||||||
|
case AppUpdater::UpdateStatus::IDLE:
|
||||||
|
snprintf(buffer, sizeof(buffer), "Update idle");
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::MESSAGE:
|
||||||
|
msg = message ? message : "";
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::DOWNLOADING:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::VERIFYING:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::FILE_SKIPPED:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::FILE_SAVED:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::MD5_FAILED:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::COMPLETE:
|
||||||
|
snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!");
|
||||||
|
msg = buffer;
|
||||||
|
isComplete = true;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::ERROR:
|
||||||
|
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "%s", msg);
|
||||||
|
sendUpdateMessage(msg, isComplete, percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
|
||||||
|
if(eventProgress && eventProgress->count() > 0) {
|
||||||
|
JsonDocument jsonDoc;
|
||||||
|
jsonDoc["message"] = message;
|
||||||
|
jsonDoc["complete"] = complete;
|
||||||
|
jsonDoc["progress"] = progress;
|
||||||
|
String strMessage;
|
||||||
|
serializeJson(jsonDoc, strMessage);
|
||||||
|
eventProgress->send(strMessage.c_str(), "update", millis());
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
ESP_LOGW(TAG, "No clients connected to event source");
|
||||||
|
}
|
||||||
|
|
||||||
|
bleUpgrade_send_message(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkManifest(Version& remoteVersion) {
|
||||||
|
const char* TAG = "manifestCheck";
|
||||||
|
String url = updateUrl + "update.json";
|
||||||
|
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||||
|
|
||||||
|
// Start the HTTP client and send GET request for manifest
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response
|
||||||
|
String payload = http.getString();
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
//ESP_LOGD(TAG, "%s", payload.c_str());
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
JsonDocument jsonManifest;
|
||||||
|
DeserializationError error = deserializeJson(jsonManifest, payload);
|
||||||
|
if (error) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for version section
|
||||||
|
JsonObject jsonVersion = jsonManifest["version"];
|
||||||
|
|
||||||
|
if (jsonVersion.isNull()) {
|
||||||
|
ESP_LOGE(TAG, "No version section in manifest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the remote version
|
||||||
|
byte major = jsonVersion["major"] | 0;
|
||||||
|
byte minor = jsonVersion["minor"] | 0;
|
||||||
|
byte patch = jsonVersion["patch"] | 0;
|
||||||
|
remoteVersion = {major, minor, patch};
|
||||||
|
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
// Initialize WiFi connection first
|
||||||
|
// ... WiFi connection code ...
|
||||||
|
|
||||||
|
// Initialize filesystem
|
||||||
|
if(!LittleFS.begin()) {
|
||||||
|
Serial.println("LittleFS Mount Failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create updater instance with:
|
||||||
|
// - Current version: "1.0.0"
|
||||||
|
// - Update server URL: "https://my-update-server.com/"
|
||||||
|
// - Filesystem: LittleFS
|
||||||
|
AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
|
||||||
|
|
||||||
|
// Set progress callback
|
||||||
|
updater.setProgressCallback([](int progress) {
|
||||||
|
Serial.printf("Update progress: %d%%\n", progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check and update firmware
|
||||||
|
if (updater.checkAndUpdate()) {
|
||||||
|
Serial.println("Update successful! Rebooting...");
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update specific files from manifest
|
||||||
|
int updatedFiles = updater.updateFilesFromManifest("test_update.json");
|
||||||
|
Serial.printf("Updated %d files\n", updatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
Loading…
x
Reference in New Issue
Block a user