commit 9-7-25

This commit is contained in:
admin 2025-09-07 23:38:56 -07:00
parent 12b5b25081
commit 084de5cd44
36 changed files with 3926 additions and 1961 deletions

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": false,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -55,22 +55,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": false,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": false,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -56,22 +56,12 @@
{ {
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
}, },
{ {
"en": true, "en": true,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1
"min": 5.0,
"max": 100.0,
"step": 1.5,
"skip-count": 5,
"vision": true
} }
], ],
"oled": { "oled": {

View File

@ -58,17 +58,17 @@
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0,
"min": 5.0, "min": 0.0,
"max": 100.0, "max": 100.0,
"step": 1.5, "step": 1.5,
"skip-count": 5, "skip-count": 5,
"vision": true "vision": true
}, },
{ {
"en": true, "en": false,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1,
"min": 5.0, "min": 0.0,
"max": 100.0, "max": 100.0,
"step": 1.5, "step": 1.5,
"skip-count": 5, "skip-count": 5,
@ -106,6 +106,7 @@
"rgb-order": "rgb", "rgb-order": "rgb",
"shift":-5, "shift":-5,
"offset": 0, "offset": 0,
"bright": 200,
"power-div": 0, "power-div": 0,
"i2s-ch": 0, "i2s-ch": 0,
"core": 1 "core": 1
@ -117,6 +118,7 @@
"rgb-order": "rgb", "rgb-order": "rgb",
"shift":-27, "shift":-27,
"offset": 0, "offset": 0,
"bright": 255,
"power-div": 0, "power-div": 0,
"i2s-ch": 0, "i2s-ch": 0,
"core": 1 "core": 1

View File

@ -58,17 +58,17 @@
"en": true, "en": true,
"relay-index": 0, "relay-index": 0,
"button-index": 0, "button-index": 0,
"min": 5.0, "min": 0.0,
"max": 100.0, "max": 100.0,
"step": 1.5, "step": 1.5,
"skip-count": 5, "skip-count": 5,
"vision": true "vision": true
}, },
{ {
"en": true, "en": false,
"relay-index": 1, "relay-index": 1,
"button-index": 1, "button-index": 1,
"min": 5.0, "min": 0.0,
"max": 100.0, "max": 100.0,
"step": 1.5, "step": 1.5,
"skip-count": 5, "skip-count": 5,

View File

@ -5,242 +5,409 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title> <title>ATA Firmware Update</title>
<style> <style>
/* --- CSS Variables for Easy Theming --- */
:root {
--color-bg: #f8f9fa;
--color-surface: #ffffff;
--color-text: #212529;
--color-text-muted: #6c757d;
--color-primary: #007bff;
--color-primary-hover: #0056b3;
--color-disabled: #e9ecef;
--color-disabled-text: #6c757d;
--color-border: #dee2e6;
--color-success: #28a745;
--color-warning: #ffc107;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--border-radius: 8px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
--transition-speed: 0.3s;
}
/* --- Base & Layout --- */
* {
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: var(--font-family);
margin: 0; margin: 0;
padding: 20px; padding: 20px;
background-color: #f4f4f4; background-color: var(--color-bg);
text-align: center; color: var(--color-text);
display: grid;
place-items: center;
min-height: 100vh;
} }
main {
width: 100%;
max-width: 500px;
background: var(--color-surface);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 24px 32px;
}
/* --- Typography --- */
h1 { h1 {
font-size: 22px; font-size: 24px;
margin-bottom: 5px; text-align: center;
margin-bottom: 16px;
color: var(--color-text);
} }
.status-container { h2 {
font-size: 20px;
margin: 0 0 16px;
border-bottom: 1px solid var(--color-border);
padding-bottom: 8px;
}
label {
font-weight: 500;
}
/* --- Tabs --- */
.tab-bar {
display: flex;
gap: 8px;
margin-bottom: 24px;
border-bottom: 1px solid var(--color-border);
}
.tab-bar button {
flex: 1;
padding: 10px 15px;
font-size: 16px;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
background-color: transparent;
color: var(--color-text-muted);
transition: all var(--transition-speed) ease;
border-radius: 0;
font-weight: 600;
}
.tab-bar button:hover {
color: var(--color-primary);
}
.tab-bar button.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* --- Status Indicators --- */
.status-grid {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 10px 15px;
margin-bottom: 20px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: var(--color-text-muted);
transition: background-color var(--transition-speed) ease;
}
.status-indicator.is-success { background-color: var(--color-success); }
.status-indicator.is-warning { background-color: var(--color-warning); }
.status-label {
font-size: 15px;
color: var(--color-text);
}
/* --- Form Elements & Buttons --- */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 14px;
color: var(--color-text-muted);
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
font-size: 16px;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
/* Radio button styling */
input[type="radio"] {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--color-primary);
}
.password-toggle {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: left; gap: 8px;
margin-bottom: 4px; font-size: 14px;
margin-top: 8px;
} }
.status-indicator-ble { .password-toggle input {
width: 20px; width: auto;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
} }
.btn-container { .btn-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 10px; gap: 12px;
margin-bottom: 10px; margin: 20px 0;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
} }
button { button {
flex: 1; flex: 1;
max-width: 130px; min-width: 140px;
padding: 10px; padding: 12px 20px;
font-size: 16px; font-size: 16px;
font-weight: 600;
border: none; border: none;
border-radius: 5px; border-radius: var(--border-radius);
cursor: pointer; cursor: pointer;
background-color: #007bff; background-color: var(--color-primary);
color: white; color: white;
transition: background 0.3s ease; transition: background-color var(--transition-speed) ease, transform var(--transition-speed) ease;
}
button:disabled {
background-color: #ccc;
} }
button:hover:not(:disabled) { button:hover:not(:disabled) {
background-color: #0056b3; background-color: var(--color-primary-hover);
transform: translateY(-2px);
} }
textarea { button:disabled {
background-color: var(--color-disabled);
color: var(--color-disabled-text);
cursor: not-allowed;
}
/* --- Log Area --- */
textarea#logArea {
width: 100%; width: 100%;
height: 300px; height: 250px;
font-size: 14px; font-family: "SF Mono", "Consolas", "Menlo", monospace;
padding: 10px; font-size: 13px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px; padding: 15px;
border-radius: var(--border-radius);
border: 1px solid var(--color-border);
background-color: var(--color-bg);
resize: vertical;
line-height: 1.5;
} }
/* --- Responsive --- */
@media (max-width: 540px) {
body {
padding: 10px;
}
main {
padding: 16px 20px;
}
h1 { h1 {
font-size: 20px; font-size: 22px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
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>
<main>
<h1>ATA Firmware Update</h1> <h1>ATA Firmware Update</h1>
<!-- Tab Buttons -->
<div class="tab-bar"> <div class="tab-bar">
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button> <button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button> <button class="tab-btn" data-tab="tab-wifi">WiFi Settings</button>
<button class="tab-btn" data-tab="tab-info">Info</button>
</div> </div>
<div id="tab-upgrade" class="tab-panel active"> <div id="tab-upgrade" class="tab-panel active">
<!-- Status Indicators --> <div class="status-grid">
<div class="status-container"> <div class="status-indicator" id="indicator-ble"></div>
<span class="status-indicator-ble"></span> <div class="status-label" id="status-ble-connection">BLE Status: ...</div>
<label id="status-ble-connection">BLE Status: ...</label> <div class="status-indicator" id="indicator-wifi"></div>
<div class="status-label" id="status-wifi-client">WiFi Client: ...</div>
<div class="status-indicator" id="indicator-internet"></div>
<div class="status-label" id="status-internet">Internet: ...</div>
<span></span> <div class="status-label" id="status-current-version">Current Version: ...</div>
<span></span> <div class="status-label" id="status-new-version">New Version: ...</div>
</div> </div>
<div class="status-container"> <div class="form-group">
<span class="status-indicator-wifi"></span> <label for="input-DeviceName" id="ble-device-name-label">Device Name</label>
<label id="status-wifi-client">Wifi Client: ...</label> <input type="text" id="input-DeviceName" required>
</div> </div>
<div class="status-container">
<span class="status-indicator-internet"></span>
<label id="status-internet">Internet: ...</label>
</div>
<div class="status-container">
<label id="status-current-version">Curr Version: ...</label>
</div>
<div class="status-container">
<label id="status-new-version">New Version: ...</label>
</div>
<div class="btn-container">
<label id="ble-device-name">Device Name:</label>
</div>
<div class="btn-container">
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
</div>
<!-- Buttons -->
<div class="btn-container"> <div class="btn-container">
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button> <button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button> <button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
</div> </div>
<!-- Log Area -->
<textarea id="logArea" readonly></textarea> <textarea id="logArea" readonly></textarea>
<!-- Update Mode Selection -->
<div class="form-group">
<label>Update Mode:</label>
<div style="display: flex; gap: 15px; margin-top: 8px; flex-wrap: wrap;">
<label style="display: flex; align-items: center; gap: 6px; font-weight: normal; cursor: pointer;">
<input type="radio" name="updateMode" value="both" checked>
Update Both
</label>
<label style="display: flex; align-items: center; gap: 6px; font-weight: normal; cursor: pointer;">
<input type="radio" name="updateMode" value="files">
Files Only
</label>
<label style="display: flex; align-items: center; gap: 6px; font-weight: normal; cursor: pointer;">
<input type="radio" name="updateMode" value="firmware">
Firmware Only
</label>
</div>
</div>
<div class="btn-container"> <div class="btn-container">
<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 --> </div>
<div id="tab-wifi" class="tab-panel"> <div id="tab-wifi" class="tab-panel">
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2> <h2>WiFi Connection</h2>
<div class="input-container"> <div class="form-group">
<label for="wifissid">WiFi Name (SSID)</label>
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required> <input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
</div>
<div class="form-group">
<label for="wifipassword">WiFi Password</label>
<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 class="password-toggle">
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;"> <input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()">
<label for="showPassword">Show Password</label> <label for="showPassword">Show Password</label>
</div> </div>
</div> </div>
<div class="btn-container wifi"> <div class="btn-container">
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button> <button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect to WiFi</button>
</div> </div>
</div><!-- /tab-wifi --> </div>
<div id="tab-info" class="tab-panel">
<h2>Instructions</h2>
<textarea readonly style="width: 100%; height: 640px; font-family: var(--font-family); font-size: 14px; padding: 15px; border-radius: var(--border-radius); border: 1px solid var(--color-border); background-color: var(--color-bg); resize: vertical; line-height: 1.6;">ATA Firmware Update Tool - Instructions
OVERVIEW:
This tool allows you to update your ATA Boothifier device firmware and configuration files over Bluetooth Low Energy (BLE) and WiFi. You can choose to update files only, firmware only, or both.
GETTING STARTED:
1. SET DEVICE/BOARD IN UPGRADE MODE
• hold button 1 and click the RST button on the board.
- If device is in upgrade mode buzzer will beep every 5 secs.
- LED1 (L1) will be solid.
- RGB LEDS will be set to solid red.
2. CONNECT TO DEVICE
• Make sure your device is powered on and in upgrade mode
• Click "Connect" button to scan for and connect to your device
• The device name should appear as "ATALIGHTS" by default
• Wait for BLE Status to show "Connected"
3. CHECK STATUS
• When connected it will update status automatically:
- WiFi connection status
- Internet connectivity status
- Current firmware version
4. CONNECT TO WIFI (if needed)
• If WiFi is not connected, go to "WiFi Settings" tab
• Enter your WiFi network name (SSID) and password
• Click "Connect to WiFi"
• Return to "Upgrade" tab once connected
5. CHECK FOR UPDATES
• Click "Check Version" to see if new firmware is available
• This requires internet connection through WiFi
• New version will be displayed if available
6. CHOOSE UPDATE MODE
• Update Both: Updates files and firmware (requires restart)
• Files Only: Updates configuration/web files only (no restart)
• Firmware Only: Updates firmware only (requires restart)
7. START UPDATE
• Click "Start Update" to begin the update process
• Monitor progress in the log area
• DO NOT disconnect or power off during update
• Device will restart automatically if firmware was updated
TROUBLESHOOTING:
• BLE Connection Issues:
- Ensure device is in upgrade mode
- Try refreshing the page and reconnecting
- Make sure no other devices are connected to the ATA device
• WiFi Connection Issues:
- Double-check SSID and password
- Ensure WiFi network is available and has internet access
- Some networks may block the device
• Update Issues:
- Ensure stable internet connection
- Try "Files Only" update if firmware update fails
- Check log messages for specific error details
IMPORTANT NOTES:
• Always ensure stable power during firmware updates
• Device will restart after firmware updates
• Files-only updates do not require restart
• Log area shows detailed progress and error information
• You can copy text from the log area for troubleshooting</textarea>
</div>
</main>
<script> <script>
(function(){ (function(){
'use strict'; 'use strict';
/* ================= Constants & Packet Layout ================= */ /* ================= Constants & Packet Layout ================= */
const BLE_SERVER_NAME = "ATALIGHTS"; // Keep hardcoded per instruction (ignore external JSON) const BLE_SERVER_NAME = "ATALIGHTS";
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0";
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Control / status const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1";
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Logs / events const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1";
// Packet layout (mirrors firmware struct updateStatus)
// byte 0 : wifiStatus (enum)
// byte 1 : wifiOnline (bool)
// bytes2-5: wifiIP
// bytes6-8: currVersion (major,minor,patch)
// bytes9-11: newVersion (major,minor,patch)
// bytes12-31: wifiSSID (20 bytes, null padded)
const PACKET_LEN = 32; const PACKET_LEN = 32;
const OFF_WIFI_STATUS = 0; const OFF_WIFI_STATUS = 0, OFF_WIFI_ONLINE = 1, OFF_WIFI_IP = 2;
const OFF_WIFI_ONLINE = 1; const OFF_CURR_VER = 6, OFF_NEW_VER = 9, OFF_WIFI_SSID = 12;
const OFF_WIFI_IP = 2;
const OFF_CURR_VER = 6;
const OFF_NEW_VER = 9;
const OFF_WIFI_SSID = 12;
const WIFI_STAT = { DISCONNECTED:0, BAD_CREDS:1, NO_AP:2, CONNECTED:3 }; const WIFI_STAT = { DISCONNECTED:0, BAD_CREDS:1, NO_AP:2, CONNECTED:3 };
const WIFI_STAT_TEXT = ["Disconnected","Bad Creds","No AP","Connected"]; const WIFI_STAT_TEXT = ["Disconnected", "Bad Credentials", "AP Not Found", "Connected"];
const MAX_LOG_LINES = 400; const MAX_LOG_LINES = 400;
/* ================= State ================= */ /* ================= State ================= */
@ -256,16 +423,18 @@
}; };
/* ================= Cached DOM ================= */ /* ================= Cached DOM ================= */
// Cache DOM elements for performance and easier access
const el = {}; const el = {};
function cacheDom() { function cacheDom() {
el.bleIndicator = document.querySelector('.status-indicator-ble'); el.bleIndicator = document.getElementById('indicator-ble');
el.wifiIndicator = document.querySelector('.status-indicator-wifi'); el.wifiIndicator = document.getElementById('indicator-wifi');
el.internetIndicator = document.querySelector('.status-indicator-internet'); el.internetIndicator = document.getElementById('indicator-internet');
el.lblBle = document.getElementById('status-ble-connection'); el.lblBle = document.getElementById('status-ble-connection');
el.lblWifi = document.getElementById('status-wifi-client'); el.lblWifi = document.getElementById('status-wifi-client');
el.lblInternet = document.getElementById('status-internet'); el.lblInternet = document.getElementById('status-internet');
el.lblCurrVer = document.getElementById('status-current-version'); el.lblCurrVer = document.getElementById('status-current-version');
el.lblNewVer = document.getElementById('status-new-version'); el.lblNewVer = document.getElementById('status-new-version');
el.lblDeviceName = document.getElementById('ble-device-name-label');
el.inDeviceName = document.getElementById('input-DeviceName'); el.inDeviceName = document.getElementById('input-DeviceName');
el.inSsid = document.getElementById('wifissid'); el.inSsid = document.getElementById('wifissid');
el.inPass = document.getElementById('wifipassword'); el.inPass = document.getElementById('wifipassword');
@ -279,35 +448,56 @@
} }
/* ================= Utilities ================= */ /* ================= Utilities ================= */
function logMessage(msg){ const logMessage = (msg, overwriteLastLine = false) => {
const lines = el.logArea.value.trim().length ? el.logArea.value.split(/\n/) : []; // Append message to log area, managing max lines and scrolling
lines.push(msg); if (!el.logArea) return;
let lines = el.logArea.value.length ? el.logArea.value.replace(/\r/g, '').split('\n') : [];
if (lines.length && lines[lines.length - 1] === '') lines.pop(); // remove trailing empty from previous newline
const entry = `-> ${msg}`;
// Handle line management
if (overwriteLastLine && lines.length > 0) {
// Replace the last line for progress updates
lines[lines.length - 1] = entry;
} else {
// Add a new line
lines.push(entry);
}
// Trim log if too long
if (lines.length > MAX_LOG_LINES) { if (lines.length > MAX_LOG_LINES) {
lines.splice(0, lines.length - MAX_LOG_LINES); lines = lines.slice(lines.length - MAX_LOG_LINES);
} }
el.logArea.value = lines.join('\n') + '\n';
el.logArea.value = lines.join('\n');
el.logArea.scrollTop = el.logArea.scrollHeight; el.logArea.scrollTop = el.logArea.scrollHeight;
} };
function compareVersions(a,b){ // Compare version arrays [major, minor, patch]
for(let i=0;i<3;i++){ if(a[i]>b[i]) return 1; if(a[i]<b[i]) return -1; } // Returns 1 if a > b, -1 if a < b, 0
const compareVersions = (a, b) => {
for(let i = 0; i < 3; i++) {
if(a[i] > b[i]) return 1;
if(a[i] < b[i]) return -1;
}
return 0; return 0;
} };
function colorIndicator(elm, color){ if(elm) elm.style.backgroundColor = color; } const ipToString = (ip) => ip.join('.');
function ipToString(ip){ return ip.join('.'); } const setIndicatorStatus = (elm, status) => {
if (elm) elm.className = `status-indicator ${status}`;
};
/* ================= Packet Handling ================= */ /* ================= Packet Handling ================= */
function parsePacket(data) { function parsePacket(data) {
if(data.length !== PACKET_LEN) return false; if(data.length !== PACKET_LEN) return false;
state.wifiStatus = data[OFF_WIFI_STATUS]; state.wifiStatus = data[OFF_WIFI_STATUS];
if(state.wifiStatus > WIFI_STAT.CONNECTED) state.wifiStatus = WIFI_STAT.DISCONNECTED; // clamp if(state.wifiStatus > WIFI_STAT.CONNECTED) state.wifiStatus = WIFI_STAT.DISCONNECTED;
state.wifiOnline = !!data[OFF_WIFI_ONLINE]; state.wifiOnline = !!data[OFF_WIFI_ONLINE];
state.wifiIP = Array.from(data.slice(OFF_WIFI_IP, OFF_WIFI_IP + 4)); state.wifiIP = Array.from(data.slice(OFF_WIFI_IP, OFF_WIFI_IP + 4));
state.currVersion = Array.from(data.slice(OFF_CURR_VER, OFF_CURR_VER + 3)); state.currVersion = Array.from(data.slice(OFF_CURR_VER, OFF_CURR_VER + 3));
state.newVersion = Array.from(data.slice(OFF_NEW_VER, OFF_NEW_VER + 3)); state.newVersion = Array.from(data.slice(OFF_NEW_VER, OFF_NEW_VER + 3));
// Extract SSID (stop at first 0)
let rawSsidBytes = data.slice(OFF_WIFI_SSID, OFF_WIFI_SSID + 20); let rawSsidBytes = data.slice(OFF_WIFI_SSID, OFF_WIFI_SSID + 20);
let zeroIndex = rawSsidBytes.indexOf(0); let zeroIndex = rawSsidBytes.indexOf(0);
if(zeroIndex >= 0) rawSsidBytes = rawSsidBytes.slice(0, zeroIndex); if(zeroIndex >= 0) rawSsidBytes = rawSsidBytes.slice(0, zeroIndex);
@ -318,31 +508,27 @@
function updateUI() { function updateUI() {
// BLE // BLE
el.lblBle.textContent = 'BLE Status: ' + (bleConnected ? 'Connected' : 'Disconnected'); el.lblBle.textContent = 'BLE Status: ' + (bleConnected ? 'Connected' : 'Disconnected');
colorIndicator(el.bleIndicator, bleConnected ? 'green' : 'gray'); setIndicatorStatus(el.bleIndicator, bleConnected ? 'is-success' : '');
// WiFi client // WiFi client
const statText = WIFI_STAT_TEXT[state.wifiStatus] || 'Unknown'; const statText = WIFI_STAT_TEXT[state.wifiStatus] || 'Unknown';
if(state.wifiStatus === WIFI_STAT.CONNECTED) { if(state.wifiStatus === WIFI_STAT.CONNECTED) {
const ssidPart = state.wifiSSID ? ' SSID: '+state.wifiSSID : ''; const ssidPart = state.wifiSSID ? ` to "${state.wifiSSID}"` : '';
el.lblWifi.textContent = 'Wifi Client: ' + statText + (state.wifiIP[0] ? ' ('+ipToString(state.wifiIP)+')':'' ) + ssidPart; const ipPart = state.wifiIP[0] ? ` (${ipToString(state.wifiIP)})` : '';
colorIndicator(el.wifiIndicator, 'green'); el.lblWifi.textContent = 'WiFi Client: ' + statText + ssidPart + ipPart;
} else if(state.wifiStatus === WIFI_STAT.BAD_CREDS){ setIndicatorStatus(el.wifiIndicator, 'is-success');
el.lblWifi.textContent = 'Wifi Client: Bad Credentials';
colorIndicator(el.wifiIndicator, 'orange');
} else if(state.wifiStatus === WIFI_STAT.NO_AP){
el.lblWifi.textContent = 'Wifi Client: AP Not Found';
colorIndicator(el.wifiIndicator, 'orange');
} else { } else {
el.lblWifi.textContent = 'Wifi Client: ' + statText; el.lblWifi.textContent = 'WiFi Client: ' + statText;
colorIndicator(el.wifiIndicator, 'gray'); const statusClass = (state.wifiStatus === WIFI_STAT.BAD_CREDS || state.wifiStatus === WIFI_STAT.NO_AP) ? 'is-warning' : '';
setIndicatorStatus(el.wifiIndicator, statusClass);
} }
// Internet // Internet
el.lblInternet.textContent = state.wifiOnline ? 'Online' : 'Offline'; el.lblInternet.textContent = 'Internet: ' + (state.wifiOnline ? 'Online' : 'Offline');
colorIndicator(el.internetIndicator, state.wifiOnline ? 'green' : 'gray'); setIndicatorStatus(el.internetIndicator, state.wifiOnline ? 'is-success' : '');
// Versions // Versions
el.lblCurrVer.textContent = state.currVersion[0] ? 'Curr Version: ' + state.currVersion.join('.') : 'Curr Version: ...'; el.lblCurrVer.textContent = state.currVersion[0] ? 'Current Version: ' + state.currVersion.join('.') : 'Current Version: ...';
el.lblNewVer.textContent = state.newVersion[0] ? 'New Version: ' + state.newVersion.join('.') : 'New Version: ...'; el.lblNewVer.textContent = state.newVersion[0] ? 'New Version: ' + state.newVersion.join('.') : 'New Version: ...';
// Buttons // Buttons
@ -366,13 +552,60 @@
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID); bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID); bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
await bleCharacteristic2.startNotifications(); await bleCharacteristic2.startNotifications();
let prevProgressFound = false;
bleCharacteristic2.addEventListener('characteristicvaluechanged', e => { bleCharacteristic2.addEventListener('characteristicvaluechanged', e => {
try{ try{
const txt = new TextDecoder().decode(e.target.value); //const view = e.target.value; // DataView
console.log('--> ' + txt); //const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
logMessage('--> ' + txt.trim());
}catch(_){ /* ignore */ } // Debug info
//let debugInfo = `Received ${bytes.length} bytes: `;
//for (let i = 0; i < bytes.length; i++) {
// debugInfo += bytes[i].toString(16).padStart(2, '0') + ' ';
// }
//console.log(debugInfo);
// Try to decode as text
let txt = '';
try {
//txt = new TextDecoder().decode(bytes);
txt = new TextDecoder().decode(e.target.value);
// Remove null terminators and trim
const nullIdx = txt.indexOf('\0');
if (nullIdx !== -1) txt = txt.slice(0, nullIdx);
txt = txt.trim();
} catch (decodeErr) {
console.error('Text decode error:', decodeErr);
// Fallback to showing hex if text decode fails
txt = '[Binary data]';
}
// Show both raw and processed data in console
//console.log('--> Raw bytes:', bytes);
//console.log('--> As text:', txt);
//logMessage(`--> (${bytes.length} bytes) ${txt}`);
// progress messages handling variable captured from outer scope
if(prevProgressFound && txt.includes('progress')){
logMessage(`${txt}`, true); // overwrite last line for progress updates
}
else{
logMessage(`${txt}`);
prevProgressFound = false; // reset if current message isn't progress
}
if(txt.includes('progress')){
prevProgressFound = true;
}
} catch(err) {
console.error('Processing error', err);
logMessage('--> Error processing message: ' + err.message);
}
}); });
bleConnected=true; bleConnected=true;
bleDevice.addEventListener('gattserverdisconnected', onDisconnect); bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device'; const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
@ -387,9 +620,14 @@
} }
function onDisconnect() { function onDisconnect() {
bleConnected=false; updateUI(); logMessage('BLE disconnected'); bleConnected = false;
el.lblDeviceName.textContent = 'Device Name';
updateUI();
logMessage('BLE disconnected');
} }
const delay = ms => new Promise(r => setTimeout(r, ms));
async function sendPacket(msg) { async function sendPacket(msg) {
if(!bleCharacteristic1) return; if(!bleCharacteristic1) return;
const enc = new TextEncoder(); const enc = new TextEncoder();
@ -406,7 +644,8 @@
try { try {
const val = await bleCharacteristic1.readValue(); const val = await bleCharacteristic1.readValue();
const data = new Uint8Array(val.buffer); const data = new Uint8Array(val.buffer);
if(parsePacket(data)) return true; else { logMessage('Packet parse failed (len='+data.length+')'); return false; } if(parsePacket(data)) return true;
else { logMessage('Packet parse failed (len=' + data.length + ')'); return false; }
} catch(e) { if(attempt === 2) logMessage('Read failed: '+e.message); else await delay(1000); } } catch(e) { if(attempt === 2) logMessage('Read failed: '+e.message); else await delay(1000); }
} }
return false; return false;
@ -418,25 +657,19 @@
const pw = el.inPass.value; const pw = el.inPass.value;
if(!ssid || !pw) { alert('Enter SSID & password'); return; } if(!ssid || !pw) { alert('Enter SSID & password'); return; }
logMessage('Sending WiFi credentials...'); logMessage('Sending WiFi credentials...');
el.btnWifiConnect.disabled = true; // prevent multiple submissions while polling el.btnWifiConnect.disabled = true;
await sendPacket('wifi-connect {"ssid":"'+ssid+'","pass":"'+pw+'"} '); await sendPacket(`wifi-connect {"ssid":"${ssid}","pass":"${pw}"} `);
// Poll for status for up to 15s
const start = Date.now(); const start = Date.now();
while(Date.now() - start < 15000) { while(Date.now() - start < 15000) {
await delay(1000); await delay(1000);
await readPacket(); await readPacket();
updateUI(); updateUI();
if(state.wifiStatus === WIFI_STAT.CONNECTED){ if(state.wifiStatus === WIFI_STAT.CONNECTED) { logMessage(`WiFi Connected: ${ipToString(state.wifiIP)}`); break; }
logMessage('WiFi Connected: '+ipToString(state.wifiIP));
break;
}
if(state.wifiStatus === WIFI_STAT.BAD_CREDS) { logMessage('WiFi Error: Bad Credentials'); break; } if(state.wifiStatus === WIFI_STAT.BAD_CREDS) { logMessage('WiFi Error: Bad Credentials'); break; }
if(state.wifiStatus === WIFI_STAT.NO_AP) { logMessage('WiFi Error: AP Not Found'); break; } if(state.wifiStatus === WIFI_STAT.NO_AP) { logMessage('WiFi Error: AP Not Found'); break; }
} }
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ logMessage('WiFi connect attempt finished with status: '+WIFI_STAT_TEXT[state.wifiStatus]); } if(state.wifiStatus !== WIFI_STAT.CONNECTED) logMessage(`WiFi connect attempt finished with status: ${WIFI_STAT_TEXT[state.wifiStatus]}`);
if(!bleConnected) return; // keep disabled if BLE disconnected during process if(bleConnected && state.wifiStatus !== WIFI_STAT.CONNECTED) { el.btnWifiConnect.disabled = false; }
// Re-enable for retry unless connected
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ el.btnWifiConnect.disabled = false; }
} }
async function checkStatus() { if(await readPacket()) updateUI(); } async function checkStatus() { if(await readPacket()) updateUI(); }
@ -449,7 +682,14 @@
while(Date.now() - start < 15000) { while(Date.now() - start < 15000) {
await delay(750); await delay(750);
await readPacket(); await readPacket();
if(state.newVersion[0]){ logMessage('Latest version: '+state.newVersion.join('.')); break; } if(state.newVersion[0]) {
if(compareVersions(state.newVersion, state.currVersion) > 0) {
logMessage('New version available: ' + state.newVersion.join('.'));
} else {
logMessage('Newer version not available.');
}
break;
}
} }
if(!state.newVersion[0]) logMessage('No new version info received'); if(!state.newVersion[0]) logMessage('No new version info received');
updateUI(); updateUI();
@ -457,22 +697,43 @@
async function startUpgrade() { async function startUpgrade() {
if(el.btnStartUpgrade.disabled) return; if(el.btnStartUpgrade.disabled) return;
logMessage('Starting upgrade...');
el.btnStartUpgrade.disabled = true; // Get selected update mode
await sendPacket('upgrade-start'); const selectedMode = document.querySelector('input[name="updateMode"]:checked')?.value || 'both';
// Progress will arrive via characteristic2 logs
// Determine packet to send based on selection
let packet;
let modeText;
switch(selectedMode) {
case 'files':
packet = 'upgrade-start-files-only';
modeText = 'files only';
break;
case 'firmware':
packet = 'upgrade-start-firmware-only';
modeText = 'firmware only';
break;
case 'both':
default:
packet = 'upgrade-start';
modeText = 'both files and firmware';
break;
} }
/* ================= Helpers ================= */ logMessage(`Starting upgrade (${modeText})...`);
const delay = ms => new Promise(r=>setTimeout(r,ms)); el.btnStartUpgrade.disabled = true;
function togglePasswordVisibility(){ el.inPass.type = el.chkShowPass.checked ? 'text' : 'password'; } await sendPacket(packet);
}
/* ================= Initialization ================= */
function togglePasswordVisibility() {
el.inPass.type = el.chkShowPass.checked ? 'text' : 'password';
}
function init() { function init() {
cacheDom(); cacheDom();
el.inDeviceName.value = BLE_SERVER_NAME; el.inDeviceName.value = BLE_SERVER_NAME;
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
// Inline onclicks already wired; ensure functions are in scope
// Tab switching // Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => { document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@ -485,10 +746,9 @@
}); });
}); });
updateUI(); updateUI();
logMessage('Ready. Enter device name or use default and press Connect.'); logMessage('Press Connect -> Check Version -> Start Update');
} }
// Expose functions for inline handlers
window.connectToBle = connectToBle; window.connectToBle = connectToBle;
window.checkStatus = checkStatus; window.checkStatus = checkStatus;
window.checkVersion = checkVersion; window.checkVersion = checkVersion;
@ -501,3 +761,4 @@
</script> </script>
</body> </body>
</html> </html>
```

View File

@ -1,456 +0,0 @@
#!/usr/bin/env python3
"""Upload firmware, manifest, and data assets to a MinIO (S3-compatible) bucket.
Features preserved from original GCS script:
- Optional backup (copies existing objects under destination prefix to timestamped folder under backups/)
- Upload firmware.bin, update.json, and recursively mirror a data directory
- Cache-Control set to disable caching on clients
Switches from google.cloud.storage to boto3 (S3 API) for MinIO compatibility.
"""
import os
import sys
import datetime
import hashlib
import json
from pathlib import Path
try:
import boto3
from botocore.exceptions import ClientError
from botocore.config import Config
except ImportError:
print("ERROR: boto3 is required. Install with: pip install boto3")
sys.exit(1)
# =============================================================================
# CONFIGURATION CONSTANTS (edit as needed or supply via environment variables)
# =============================================================================
CREATE_BACKUP = False
UPLOAD_FIRMWARE = True
UPDATE_MANIFEST = True
UPLOAD_DATA = True
DIR_SKIP_LIST = [
"system",
"booths"
]
FIlES_SKIP_LIST = [
]
# Bucket / endpoint configuration
BUCKET_NAME = os.getenv('MINIO_BUCKET', 'boothifier')
DESTINATION_DIR = os.getenv('MINIO_DEST_PREFIX', 'latest') # prefix inside bucket
BACKUPS_DIR = os.getenv('MINIO_BACKUPS_PREFIX', 'backups')
PROJECT_ROOT_PATH = Path(__file__).parent.parent.resolve()
LOCAL_ROOT_PATH = Path(__file__).parent.resolve()
# Optional service account style JSON key (generated by MinIO Console). Expected fields:
# {"url":"https://minio.example.com/api/v1/service-account-credentials","accessKey":"...","secretKey":"...","api":"s3v4","path":"auto"}
MINIO_KEY_FILE = LOCAL_ROOT_PATH / 'minio-boothifier-key.json'
# Defaults before loading file / env
_json_access = None
_json_secret = None
_json_url = None
def _load_json_key():
global _json_access, _json_secret, _json_url
try:
if MINIO_KEY_FILE.is_file():
with open(MINIO_KEY_FILE, 'r', encoding='utf-8') as fh:
data = json.load(fh)
_json_access = data.get('accessKey') or None
_json_secret = data.get('secretKey') or None
_json_url = data.get('url') or None
except Exception as e:
print(f"WARN: Failed to load MinIO key file '{MINIO_KEY_FILE.name}': {e}")
_load_json_key()
def _derive_endpoint(url_value: str) -> str:
if not url_value:
return 'https://s3-minio.boothwizard.com'
# Remove known API suffix if present (/api/...)
# e.g. https://s3-minio.boothwizard.com/api/v1/service-account-credentials -> https://s3-minio.boothwizard.com
parts = url_value.split('/api/')
return parts[0] if parts else url_value
# MinIO credentials with precedence: ENV > JSON file > fallback
MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') or _derive_endpoint(_json_url)
MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') or _json_access or 'CHANGE_ME_ACCESS'
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY') or _json_secret or 'CHANGE_ME_SECRET'
MINIO_REGION = os.getenv('MINIO_REGION', 'us-east-1') # MinIO ignores but boto3 wants some value
# Addressing / SSL options
MINIO_ADDRESSING = os.getenv('MINIO_ADDRESSING_STYLE', 'path').lower() # 'path' or 'virtual'
MINIO_VERIFY_SSL = os.getenv('MINIO_TLS_VERIFY', '1') not in ('0','false','no')
MINIO_DEBUG = os.getenv('MINIO_DEBUG', '0') in ('1','true','yes')
MINIO_ALLOW_VARIANTS = os.getenv('MINIO_ALLOW_ENDPOINT_VARIANTS', '0') in ('1','true','yes') # normally false with nginx redirect
LOCAL_FIRMWARE_PATH = str(PROJECT_ROOT_PATH / '.pio' / 'build' / 'esp32s3dev' / 'firmware.bin')
LOCAL_DATA_DIRECTORY = str(PROJECT_ROOT_PATH / 'data')
MANIFEST_LOCAL_PATH = str(LOCAL_ROOT_PATH / 'manifest-local.json') # source of version/description/changelog
MANIFEST_FILENAME = os.getenv('MANIFEST_FILENAME', 'manifest.json') # destination manifest name
# =============================================================================
# HELPERS
# =============================================================================
def s3_client():
"""Create an S3 client pointed at MinIO endpoint, forcing path-style unless overridden, with short timeouts."""
addressing = 'path' if MINIO_ADDRESSING not in ('virtual','auto') else 'virtual'
cfg = Config(
s3={'addressing_style': addressing},
signature_version='s3v4',
connect_timeout=3,
read_timeout=5,
retries={'max_attempts': 2}
)
if MINIO_DEBUG:
masked_key = (MINIO_ACCESS_KEY[:3] + '...' + MINIO_ACCESS_KEY[-3:]) if MINIO_ACCESS_KEY else 'None'
print(f"[DEBUG] Creating client: endpoint={MINIO_ENDPOINT} addressing={addressing} verifySSL={MINIO_VERIFY_SSL} region={MINIO_REGION} accessKey={masked_key}")
return boto3.client(
's3',
endpoint_url=MINIO_ENDPOINT,
aws_access_key_id=MINIO_ACCESS_KEY,
aws_secret_access_key=MINIO_SECRET_KEY,
region_name=MINIO_REGION,
verify=MINIO_VERIFY_SSL,
config=cfg,
)
def _endpoint_variants(base: str):
"""Return endpoint variants only if explicitly allowed; otherwise just the base (nginx handles forwarding)."""
if not MINIO_ALLOW_VARIANTS:
return [base]
# Fallback to previous expanded logic if variants are enabled
try:
variants = []
if not base:
return variants
base = base.rstrip('/')
proto_sep = '://'
if proto_sep in base:
scheme, rest = base.split(proto_sep,1)
else:
scheme, rest = 'https', base
host_port = rest
if ':' in host_port:
host, port = host_port.split(':',1)
else:
host, port = host_port, ''
variants.append(f"{scheme}://{host_port}")
common_ports = ['9000','443','80']
for p in common_ports:
if port != p:
variants.append(f"{scheme}://{host}:{p}")
alt_scheme = 'http' if scheme == 'https' else 'https'
variants.append(f"{alt_scheme}://{host_port}")
for p in common_ports:
if port != p:
variants.append(f"{alt_scheme}://{host}:{p}")
seen = set()
uniq = []
for v in variants:
if v not in seen:
uniq.append(v)
seen.add(v)
return uniq
except Exception:
return [base]
def create_validated_client():
"""Validate (or create) client using only provided endpoint unless variants enabled."""
global MINIO_ENDPOINT
primary = MINIO_ENDPOINT
variants = _endpoint_variants(primary) or [primary]
errors = []
probe_bucket = BUCKET_NAME # we will head the target bucket directly
for candidate in variants:
saved = MINIO_ENDPOINT
MINIO_ENDPOINT = candidate
if MINIO_DEBUG:
print(f"[DEBUG] Probing endpoint candidate: {candidate}")
try:
c = s3_client()
try:
c.head_bucket(Bucket=probe_bucket)
if MINIO_DEBUG:
print(f"[DEBUG] head_bucket succeeded on {candidate} for '{probe_bucket}'.")
return c
except ClientError as e:
msg = str(e)
# Acceptable if bucket not found (we can create later)
if any(code in msg for code in ('404', 'NoSuchBucket', 'NotFound')):
if MINIO_DEBUG:
print(f"[DEBUG] Bucket not found on {candidate} (expected if first deploy). Using this endpoint.")
return c
if 'API Requests must be made to API port' in msg:
errors.append(f"{candidate}: wrong port (console endpoint)")
else:
errors.append(f"{candidate}: {msg}")
MINIO_ENDPOINT = saved
except Exception as ex:
errors.append(f"{candidate}: {ex}")
MINIO_ENDPOINT = saved
continue
print("ERROR: Could not validate any endpoint candidate.")
for e in errors:
print(' - ' + e)
print("Provide correct API endpoint (e.g. https://host:9000) via MINIO_ENDPOINT env var.")
sys.exit(3)
def list_objects(client, prefix: str):
"""Generator yielding object keys under a prefix (non-recursive listing with pagination)."""
kwargs = {'Bucket': BUCKET_NAME, 'Prefix': prefix}
while True:
resp = client.list_objects_v2(**kwargs)
for obj in resp.get('Contents', []):
yield obj['Key']
if not resp.get('IsTruncated'):
break
kwargs['ContinuationToken'] = resp['NextContinuationToken']
def normalize_prefix(p: str) -> str:
p = p.strip('/')
return p
def join_key(*parts: str) -> str:
parts_clean = [p.strip('/') for p in parts if p is not None and p != '']
return '/'.join(parts_clean)
def backup_existing_files(client, destination_prefix: str, backups_prefix: str, backup_folder: str):
if not destination_prefix:
prefix = ''
else:
prefix = destination_prefix + '/'
print(f"Scanning existing objects under '{prefix}' for backup...")
for key in list_objects(client, prefix):
if backups_prefix and key.startswith(backups_prefix + '/'): # Skip prior backups
continue
# relative path within destination
relative = key[len(prefix):] if prefix and key.startswith(prefix) else key
backup_key = join_key(backups_prefix, backup_folder, relative)
print(f"Backup copy: {key} -> {backup_key}")
client.copy_object(
Bucket=BUCKET_NAME,
CopySource={'Bucket': BUCKET_NAME, 'Key': key},
Key=backup_key,
MetadataDirective='COPY'
)
def upload_file(client, local_path: str, key: str, cache_control: str = 'private, max-age=0, no-transform'):
if not os.path.isfile(local_path):
print(f"WARN: File missing, skipping: {local_path}")
return
print(f"Upload: {local_path} -> s3://{BUCKET_NAME}/{key}")
extra_args = { 'CacheControl': cache_control }
client.upload_file(local_path, BUCKET_NAME, key, ExtraArgs=extra_args)
def upload_directory(client, local_directory: str, destination_prefix: str):
if not os.path.isdir(local_directory):
print(f"WARN: Data directory missing: {local_directory}")
return
for root, _, files in os.walk(local_directory):
for fname in files:
full = os.path.join(root, fname)
rel = os.path.relpath(full, local_directory).replace('\\','/') # force forward slashes for S3 keys
key = join_key(destination_prefix, rel)
upload_file(client, full, key)
def ensure_bucket(client):
"""Ensure bucket exists; provide diagnostics if HeadBucket returns 400/other errors."""
try:
client.head_bucket(Bucket=BUCKET_NAME)
if MINIO_DEBUG:
print(f"[DEBUG] Bucket '{BUCKET_NAME}' exists.")
return
except ClientError as e:
code = e.response.get('Error', {}).get('Code')
status = e.response.get('ResponseMetadata', {}).get('HTTPStatusCode')
print(f"HeadBucket failed (code={code}, status={status}).")
# List buckets for diagnostics
try:
resp = client.list_buckets()
bucket_names = [b['Name'] for b in resp.get('Buckets', [])]
print(f"Available buckets: {bucket_names or 'None'}")
except Exception as le:
print(f"WARN: list_buckets failed: {le}")
if code in ('404', 'NoSuchBucket', 'NotFound'):
print(f"Bucket '{BUCKET_NAME}' not found. Attempting to create...")
try:
client.create_bucket(Bucket=BUCKET_NAME)
print(f"Created bucket '{BUCKET_NAME}'.")
return
except ClientError as ce:
print(f"ERROR: Cannot create bucket: {ce}")
sys.exit(2)
if status == 400:
print("HINTS: \n - Verify endpoint URL (MINIO_ENDPOINT).\n - Ensure no trailing slash in endpoint.\n - Check that TLS verify matches server cert (set MINIO_TLS_VERIFY=0 to test).\n - Confirm bucket name is correct and DNS compatible.\n - Credentials may lack permission: verify access key policies.")
# Retry once forcing path style if not already
if MINIO_ADDRESSING != 'path':
print("Retrying with path-style addressing...")
os.environ['MINIO_ADDRESSING_STYLE'] = 'path'
new_client = s3_client()
try:
new_client.head_bucket(Bucket=BUCKET_NAME)
print("Second attempt succeeded with path-style addressing.")
return
except ClientError as e2:
print(f"Second HeadBucket attempt failed: {e2}")
print(f"ERROR: head_bucket ultimately failed: {e}")
sys.exit(2)
# =============================================================================
# MAIN
# =============================================================================
def main():
dest_prefix = normalize_prefix(DESTINATION_DIR)
backups_prefix = normalize_prefix(BACKUPS_DIR) if BACKUPS_DIR else ''
client = create_validated_client()
if MINIO_DEBUG:
print("[DEBUG] Starting ensure_bucket phase...")
ensure_bucket(client)
# Create backup if needed
if CREATE_BACKUP:
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
backup_folder = f"backup_{ts}"
print(f"Creating backup under '{backups_prefix}/{backup_folder}' from prefix '{dest_prefix}'")
backup_existing_files(client, dest_prefix, backups_prefix, backup_folder)
print("Backup complete.")
else:
print("Backup creation skipped.")
# Firmware
if UPLOAD_FIRMWARE:
firmware_key = join_key(dest_prefix, 'firmware.bin') if dest_prefix else 'firmware.bin'
upload_file(client, LOCAL_FIRMWARE_PATH, firmware_key)
print("Firmware upload complete.")
else:
print("Firmware upload skipped.")
# Data directory
if UPLOAD_DATA:
data_prefix = join_key(dest_prefix, 'data') if dest_prefix else 'data'
upload_directory(client, LOCAL_DATA_DIRECTORY, data_prefix)
print("All uploads complete.")
else:
print("Data upload skipped.")
# Manifest
if UPDATE_MANIFEST:
manifest_key = join_key(dest_prefix, MANIFEST_FILENAME) if dest_prefix else MANIFEST_FILENAME
try:
manifest_doc = build_and_write_manifest(client, dest_prefix)
upload_manifest_json(client, manifest_doc, manifest_key)
print(f"Manifest upload complete: s3://{BUCKET_NAME}/{manifest_key}")
except Exception as e:
print(f"ERROR: Manifest generation/upload failed: {e}")
sys.exit(4)
else:
print("Manifest upload skipped.")
# ================= Manifest Support =================
def md5_hex(path: str, chunk_size: int = 65536) -> str:
h = hashlib.md5()
with open(path, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
def collect_data_files(data_root: str):
files = []
if not os.path.isdir(data_root):
return files
for root, _, filenames in os.walk(data_root):
for fname in filenames:
full = os.path.join(root, fname)
rel = os.path.relpath(full, data_root).replace('\\','/')
entry = {
'path': f"data/{rel}",
'md5': md5_hex(full),
'size': os.path.getsize(full)
}
files.append(entry)
files.sort(key=lambda x: x['path'])
return files
def read_local_manifest(local_path: str):
if not os.path.isfile(local_path):
raise FileNotFoundError(f"manifest-local file not found: {local_path}")
with open(local_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
# Basic validation
if 'version' not in data:
raise ValueError('manifest-local missing version section')
ver = data['version']
for k in ('major','minor','patch'):
if k not in ver:
raise ValueError(f"manifest-local version missing '{k}'")
data.setdefault('description', '')
data.setdefault('changelog', [])
if not isinstance(data['changelog'], list):
raise ValueError('changelog must be an array')
return data
def build_and_write_manifest(client, dest_prefix: str):
# Read local manifest-local.json
base_info = read_local_manifest(MANIFEST_LOCAL_PATH)
# Timestamp
now = datetime.datetime.now()
release_date = now.strftime('%Y-%m-%d')
release_time = now.strftime('%H:%M:%S')
# Firmware info
fw_path_local = LOCAL_FIRMWARE_PATH
if not os.path.isfile(fw_path_local):
raise FileNotFoundError(f"Firmware file not found: {fw_path_local}")
fw_md5 = md5_hex(fw_path_local)
fw_size = os.path.getsize(fw_path_local)
# Data files
data_files = collect_data_files(LOCAL_DATA_DIRECTORY)
manifest = {
'version': {
'major': int(base_info['version']['major']),
'minor': int(base_info['version']['minor']),
'patch': int(base_info['version']['patch'])
},
'release_date': release_date,
'release_time': release_time,
'description': base_info.get('description',''),
'changelog': base_info.get('changelog', []),
'firmware': {
'path': 'firmware.bin',
'md5': fw_md5,
'size': fw_size
},
'files': data_files
}
return manifest
def upload_manifest_json(client, manifest_obj: dict, key: str):
body = json.dumps(manifest_obj, indent=4).encode('utf-8')
client.put_object(
Bucket=BUCKET_NAME,
Key=key,
Body=body,
ContentType='application/json',
CacheControl='private, max-age=0, no-transform'
)
if __name__ == '__main__':
main()

View File

@ -1,497 +0,0 @@
#!/usr/bin/env python3
"""Upload firmware, manifest, and data assets to a MinIO (S3-compatible) bucket.
Features preserved from original GCS script:
- Optional backup (copies existing objects under destination prefix to timestamped folder under backups/)
- Upload firmware.bin, update.json, and recursively mirror a data directory
- Cache-Control set to disable caching on clients
Switches from google.cloud.storage to boto3 (S3 API) for MinIO compatibility.
"""
import os
import sys
import datetime
import hashlib
import json
from pathlib import Path
try:
import boto3
from botocore.exceptions import ClientError
from botocore.config import Config
except ImportError:
print("ERROR: boto3 is required. Install with: pip install boto3")
sys.exit(1)
# =============================================================================
# CONFIGURATION CONSTANTS (edit as needed or supply via environment variables)
# =============================================================================
CREATE_BACKUP = False
UPLOAD_FIRMWARE = True
UPDATE_MANIFEST = True
UPLOAD_DATA = True
DIR_SKIP_LIST = [
"data/system/**/*",
"data/booths/**/*"
]
FILES_SKIP_LIST = [
# Add base filenames to skip regardless of directory, e.g. "readme.txt"
]
# Bucket / endpoint configuration
BUCKET_NAME = os.getenv('MINIO_BUCKET', 'boothifier')
DESTINATION_DIR = os.getenv('MINIO_DEST_PREFIX', 'latest') # prefix inside bucket
BACKUPS_DIR = os.getenv('MINIO_BACKUPS_PREFIX', 'backups')
PROJECT_ROOT_PATH = Path(__file__).parent.parent.resolve()
LOCAL_ROOT_PATH = Path(__file__).parent.resolve()
# Optional service account style JSON key (generated by MinIO Console). Expected fields:
# {"url":"https://minio.example.com/api/v1/service-account-credentials","accessKey":"...","secretKey":"...","api":"s3v4","path":"auto"}
MINIO_KEY_FILE = LOCAL_ROOT_PATH / 'minio-boothifier-key.json'
# Defaults before loading file / env
_json_access = None
_json_secret = None
_json_url = None
def _load_json_key():
global _json_access, _json_secret, _json_url
try:
if MINIO_KEY_FILE.is_file():
with open(MINIO_KEY_FILE, 'r', encoding='utf-8') as fh:
data = json.load(fh)
_json_access = data.get('accessKey') or None
_json_secret = data.get('secretKey') or None
_json_url = data.get('url') or None
except Exception as e:
print(f"WARN: Failed to load MinIO key file '{MINIO_KEY_FILE.name}': {e}")
_load_json_key()
def _derive_endpoint(url_value: str) -> str:
if not url_value:
return 'https://s3-minio.boothwizard.com'
# Remove known API suffix if present (/api/...)
# e.g. https://s3-minio.boothwizard.com/api/v1/service-account-credentials -> https://s3-minio.boothwizard.com
parts = url_value.split('/api/')
return parts[0] if parts else url_value
# MinIO credentials with precedence: ENV > JSON file > fallback
MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT') or _derive_endpoint(_json_url)
MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY') or _json_access or 'CHANGE_ME_ACCESS'
MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY') or _json_secret or 'CHANGE_ME_SECRET'
MINIO_REGION = os.getenv('MINIO_REGION', 'us-east-1') # MinIO ignores but boto3 wants some value
# Addressing / SSL options
MINIO_ADDRESSING = os.getenv('MINIO_ADDRESSING_STYLE', 'path').lower() # 'path' or 'virtual'
MINIO_VERIFY_SSL = os.getenv('MINIO_TLS_VERIFY', '1') not in ('0','false','no')
MINIO_DEBUG = os.getenv('MINIO_DEBUG', '0') in ('1','true','yes')
MINIO_ALLOW_VARIANTS = os.getenv('MINIO_ALLOW_ENDPOINT_VARIANTS', '0') in ('1','true','yes') # normally false with nginx redirect
LOCAL_FIRMWARE_PATH = str(PROJECT_ROOT_PATH / '.pio' / 'build' / 'esp32s3dev' / 'firmware.bin')
LOCAL_DATA_DIRECTORY = str(PROJECT_ROOT_PATH / 'data')
MANIFEST_LOCAL_PATH = str(LOCAL_ROOT_PATH / 'manifest-local.json') # source of version/description/changelog
MANIFEST_FILENAME = os.getenv('MANIFEST_FILENAME', 'manifest.json') # destination manifest name
# =============================================================================
# HELPERS
# =============================================================================
def s3_client():
"""Create an S3 client pointed at MinIO endpoint, forcing path-style unless overridden, with short timeouts."""
addressing = 'path' if MINIO_ADDRESSING not in ('virtual','auto') else 'virtual'
cfg = Config(
s3={'addressing_style': addressing},
signature_version='s3v4',
connect_timeout=3,
read_timeout=5,
retries={'max_attempts': 2}
)
if MINIO_DEBUG:
masked_key = (MINIO_ACCESS_KEY[:3] + '...' + MINIO_ACCESS_KEY[-3:]) if MINIO_ACCESS_KEY else 'None'
print(f"[DEBUG] Creating client: endpoint={MINIO_ENDPOINT} addressing={addressing} verifySSL={MINIO_VERIFY_SSL} region={MINIO_REGION} accessKey={masked_key}")
return boto3.client(
's3',
endpoint_url=MINIO_ENDPOINT,
aws_access_key_id=MINIO_ACCESS_KEY,
aws_secret_access_key=MINIO_SECRET_KEY,
region_name=MINIO_REGION,
verify=MINIO_VERIFY_SSL,
config=cfg,
)
def _endpoint_variants(base: str):
"""Return endpoint variants only if explicitly allowed; otherwise just the base (nginx handles forwarding)."""
if not MINIO_ALLOW_VARIANTS:
return [base]
# Fallback to previous expanded logic if variants are enabled
try:
variants = []
if not base:
return variants
base = base.rstrip('/')
proto_sep = '://'
if proto_sep in base:
scheme, rest = base.split(proto_sep,1)
else:
scheme, rest = 'https', base
host_port = rest
if ':' in host_port:
host, port = host_port.split(':',1)
else:
host, port = host_port, ''
variants.append(f"{scheme}://{host_port}")
common_ports = ['9000','443','80']
for p in common_ports:
if port != p:
variants.append(f"{scheme}://{host}:{p}")
alt_scheme = 'http' if scheme == 'https' else 'https'
variants.append(f"{alt_scheme}://{host_port}")
for p in common_ports:
if port != p:
variants.append(f"{alt_scheme}://{host}:{p}")
seen = set()
uniq = []
for v in variants:
if v not in seen:
uniq.append(v)
seen.add(v)
return uniq
except Exception:
return [base]
def create_validated_client():
"""Validate (or create) client using only provided endpoint unless variants enabled."""
global MINIO_ENDPOINT
primary = MINIO_ENDPOINT
variants = _endpoint_variants(primary) or [primary]
errors = []
probe_bucket = BUCKET_NAME # we will head the target bucket directly
for candidate in variants:
saved = MINIO_ENDPOINT
MINIO_ENDPOINT = candidate
if MINIO_DEBUG:
print(f"[DEBUG] Probing endpoint candidate: {candidate}")
try:
c = s3_client()
try:
c.head_bucket(Bucket=probe_bucket)
if MINIO_DEBUG:
print(f"[DEBUG] head_bucket succeeded on {candidate} for '{probe_bucket}'.")
return c
except ClientError as e:
msg = str(e)
# Acceptable if bucket not found (we can create later)
if any(code in msg for code in ('404', 'NoSuchBucket', 'NotFound')):
if MINIO_DEBUG:
print(f"[DEBUG] Bucket not found on {candidate} (expected if first deploy). Using this endpoint.")
return c
if 'API Requests must be made to API port' in msg:
errors.append(f"{candidate}: wrong port (console endpoint)")
else:
errors.append(f"{candidate}: {msg}")
MINIO_ENDPOINT = saved
except Exception as ex:
errors.append(f"{candidate}: {ex}")
MINIO_ENDPOINT = saved
continue
print("ERROR: Could not validate any endpoint candidate.")
for e in errors:
print(' - ' + e)
print("Provide correct API endpoint (e.g. https://host:9000) via MINIO_ENDPOINT env var.")
sys.exit(3)
def list_objects(client, prefix: str):
"""Generator yielding object keys under a prefix (non-recursive listing with pagination)."""
kwargs = {'Bucket': BUCKET_NAME, 'Prefix': prefix}
while True:
resp = client.list_objects_v2(**kwargs)
for obj in resp.get('Contents', []):
yield obj['Key']
if not resp.get('IsTruncated'):
break
kwargs['ContinuationToken'] = resp['NextContinuationToken']
def normalize_prefix(p: str) -> str:
p = p.strip('/')
return p
def join_key(*parts: str) -> str:
parts_clean = [p.strip('/') for p in parts if p is not None and p != '']
return '/'.join(parts_clean)
def backup_existing_files(client, destination_prefix: str, backups_prefix: str, backup_folder: str):
if not destination_prefix:
prefix = ''
else:
prefix = destination_prefix + '/'
print(f"Scanning existing objects under '{prefix}' for backup...")
for key in list_objects(client, prefix):
if backups_prefix and key.startswith(backups_prefix + '/'): # Skip prior backups
continue
# relative path within destination
relative = key[len(prefix):] if prefix and key.startswith(prefix) else key
backup_key = join_key(backups_prefix, backup_folder, relative)
print(f"Backup copy: {key} -> {backup_key}")
client.copy_object(
Bucket=BUCKET_NAME,
CopySource={'Bucket': BUCKET_NAME, 'Key': key},
Key=backup_key,
MetadataDirective='COPY'
)
def upload_file(client, local_path: str, key: str, cache_control: str = 'private, max-age=0, no-transform'):
if not os.path.isfile(local_path):
print(f"WARN: File missing, skipping: {local_path}")
return
print(f"Upload: {local_path} -> s3://{BUCKET_NAME}/{key}")
extra_args = { 'CacheControl': cache_control }
client.upload_file(local_path, BUCKET_NAME, key, ExtraArgs=extra_args)
def upload_directory(client, local_directory: str, destination_prefix: str):
if not os.path.isdir(local_directory):
print(f"WARN: Data directory missing: {local_directory}")
return
skip_dirs = set(DIR_SKIP_LIST)
skip_files = set(FILES_SKIP_LIST)
for root, dirs, files in os.walk(local_directory):
rel_dir = os.path.relpath(root, local_directory).replace('\\','/')
if rel_dir == '.':
rel_dir = ''
# Prune directories in-place if their TOP-LEVEL relative segment matches skip list
pruned = []
for d in list(dirs):
seg = d # immediate subdir name
if seg in skip_dirs:
if MINIO_DEBUG:
print(f"[DEBUG] Skipping directory subtree: {os.path.join(root,d)}")
dirs.remove(d)
pruned.append(d)
for fname in files:
if fname in skip_files:
if MINIO_DEBUG:
print(f"[DEBUG] Skipping file by name: {os.path.join(root,fname)}")
continue
full = os.path.join(root, fname)
rel = os.path.relpath(full, local_directory).replace('\\','/') # force forward slashes for S3 keys
# If top-level directory of this file is in skip list, skip (covers deeper nested finds if any slipped through)
top_level = rel.split('/',1)[0]
if top_level in skip_dirs:
if MINIO_DEBUG:
print(f"[DEBUG] Skipping file in skipped dir: {rel}")
continue
key = join_key(destination_prefix, rel)
upload_file(client, full, key)
def ensure_bucket(client):
"""Ensure bucket exists; provide diagnostics if HeadBucket returns 400/other errors."""
try:
client.head_bucket(Bucket=BUCKET_NAME)
if MINIO_DEBUG:
print(f"[DEBUG] Bucket '{BUCKET_NAME}' exists.")
return
except ClientError as e:
code = e.response.get('Error', {}).get('Code')
status = e.response.get('ResponseMetadata', {}).get('HTTPStatusCode')
print(f"HeadBucket failed (code={code}, status={status}).")
# List buckets for diagnostics
try:
resp = client.list_buckets()
bucket_names = [b['Name'] for b in resp.get('Buckets', [])]
print(f"Available buckets: {bucket_names or 'None'}")
except Exception as le:
print(f"WARN: list_buckets failed: {le}")
if code in ('404', 'NoSuchBucket', 'NotFound'):
print(f"Bucket '{BUCKET_NAME}' not found. Attempting to create...")
try:
client.create_bucket(Bucket=BUCKET_NAME)
print(f"Created bucket '{BUCKET_NAME}'.")
return
except ClientError as ce:
print(f"ERROR: Cannot create bucket: {ce}")
sys.exit(2)
if status == 400:
print("HINTS: \n - Verify endpoint URL (MINIO_ENDPOINT).\n - Ensure no trailing slash in endpoint.\n - Check that TLS verify matches server cert (set MINIO_TLS_VERIFY=0 to test).\n - Confirm bucket name is correct and DNS compatible.\n - Credentials may lack permission: verify access key policies.")
# Retry once forcing path style if not already
if MINIO_ADDRESSING != 'path':
print("Retrying with path-style addressing...")
os.environ['MINIO_ADDRESSING_STYLE'] = 'path'
new_client = s3_client()
try:
new_client.head_bucket(Bucket=BUCKET_NAME)
print("Second attempt succeeded with path-style addressing.")
return
except ClientError as e2:
print(f"Second HeadBucket attempt failed: {e2}")
print(f"ERROR: head_bucket ultimately failed: {e}")
sys.exit(2)
# =============================================================================
# MAIN
# =============================================================================
def main():
dest_prefix = normalize_prefix(DESTINATION_DIR)
backups_prefix = normalize_prefix(BACKUPS_DIR) if BACKUPS_DIR else ''
client = create_validated_client()
if MINIO_DEBUG:
print("[DEBUG] Starting ensure_bucket phase...")
ensure_bucket(client)
# Create backup if needed
if CREATE_BACKUP:
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
backup_folder = f"backup_{ts}"
print(f"Creating backup under '{backups_prefix}/{backup_folder}' from prefix '{dest_prefix}'")
backup_existing_files(client, dest_prefix, backups_prefix, backup_folder)
print("Backup complete.")
else:
print("Backup creation skipped.")
# Firmware
if UPLOAD_FIRMWARE:
firmware_key = join_key(dest_prefix, 'firmware.bin') if dest_prefix else 'firmware.bin'
upload_file(client, LOCAL_FIRMWARE_PATH, firmware_key)
print("Firmware upload complete.")
else:
print("Firmware upload skipped.")
# Data directory
if UPLOAD_DATA:
data_prefix = join_key(dest_prefix, 'data') if dest_prefix else 'data'
upload_directory(client, LOCAL_DATA_DIRECTORY, data_prefix)
print("All uploads complete.")
else:
print("Data upload skipped.")
# Manifest
if UPDATE_MANIFEST:
manifest_key = join_key(dest_prefix, MANIFEST_FILENAME) if dest_prefix else MANIFEST_FILENAME
try:
manifest_doc = build_and_write_manifest(client, dest_prefix)
upload_manifest_json(client, manifest_doc, manifest_key)
print(f"Manifest upload complete: s3://{BUCKET_NAME}/{manifest_key}")
except Exception as e:
print(f"ERROR: Manifest generation/upload failed: {e}")
sys.exit(4)
else:
print("Manifest upload skipped.")
# ================= Manifest Support =================
def md5_hex(path: str, chunk_size: int = 65536) -> str:
h = hashlib.md5()
with open(path, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
def collect_data_files(data_root: str):
files = []
if not os.path.isdir(data_root):
return files
skip_dirs = set(DIR_SKIP_LIST)
skip_files = set(FILES_SKIP_LIST)
for root, dirs, filenames in os.walk(data_root):
# Prune dirs
for d in list(dirs):
if d in skip_dirs:
dirs.remove(d)
if MINIO_DEBUG:
print(f"[DEBUG] (manifest) Pruned dir: {os.path.join(root,d)}")
for fname in filenames:
if fname in skip_files:
if MINIO_DEBUG:
print(f"[DEBUG] (manifest) Skipped file: {os.path.join(root,fname)}")
continue
full = os.path.join(root, fname)
rel = os.path.relpath(full, data_root).replace('\\','/')
top_level = rel.split('/',1)[0]
if top_level in skip_dirs:
if MINIO_DEBUG:
print(f"[DEBUG] (manifest) Skipped by top-level dir: {rel}")
continue
entry = {
'path': f"data/{rel}",
'md5': md5_hex(full),
'size': os.path.getsize(full)
}
files.append(entry)
files.sort(key=lambda x: x['path'])
return files
def read_local_manifest(local_path: str):
if not os.path.isfile(local_path):
raise FileNotFoundError(f"manifest-local file not found: {local_path}")
with open(local_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
# Basic validation
if 'version' not in data:
raise ValueError('manifest-local missing version section')
ver = data['version']
for k in ('major','minor','patch'):
if k not in ver:
raise ValueError(f"manifest-local version missing '{k}'")
data.setdefault('description', '')
data.setdefault('changelog', [])
if not isinstance(data['changelog'], list):
raise ValueError('changelog must be an array')
return data
def build_and_write_manifest(client, dest_prefix: str):
# Read local manifest-local.json
base_info = read_local_manifest(MANIFEST_LOCAL_PATH)
# Timestamp
now = datetime.datetime.now()
release_date = now.strftime('%Y-%m-%d')
release_time = now.strftime('%H:%M:%S')
# Firmware info
fw_path_local = LOCAL_FIRMWARE_PATH
if not os.path.isfile(fw_path_local):
raise FileNotFoundError(f"Firmware file not found: {fw_path_local}")
fw_md5 = md5_hex(fw_path_local)
fw_size = os.path.getsize(fw_path_local)
# Data files
data_files = collect_data_files(LOCAL_DATA_DIRECTORY)
manifest = {
'version': {
'major': int(base_info['version']['major']),
'minor': int(base_info['version']['minor']),
'patch': int(base_info['version']['patch'])
},
'release_date': release_date,
'release_time': release_time,
'description': base_info.get('description',''),
'changelog': base_info.get('changelog', []),
'firmware': {
'path': 'firmware.bin',
'md5': fw_md5,
'size': fw_size
},
'files': data_files
}
return manifest
def upload_manifest_json(client, manifest_obj: dict, key: str):
body = json.dumps(manifest_obj, indent=4).encode('utf-8')
client.put_object(
Bucket=BUCKET_NAME,
Key=key,
Body=body,
ContentType='application/json',
CacheControl='private, max-age=0, no-transform'
)
if __name__ == '__main__':
main()

View File

@ -34,8 +34,8 @@ UPDATE_MANIFEST = True
UPLOAD_DATA = True UPLOAD_DATA = True
DIR_SKIP_LIST = [ DIR_SKIP_LIST = [
"data/system/**/*", "system", # Just directory names, not paths
"data/booths/**/*" "booths"
] ]
FILES_SKIP_LIST = [ FILES_SKIP_LIST = [

View File

@ -10,17 +10,20 @@
//#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 2048 // Reduced from 4096 to use less memory
// Maximum allowed manifest size (bytes) to protect memory // Maximum allowed manifest size (bytes) to protect memory
#define MAX_MANIFEST_SIZE (64 * 1024) #define MAX_MANIFEST_SIZE (64 * 1024)
// Number of HTTP retry attempts for transient failures // Number of HTTP retry attempts for transient failures
#define HTTP_RETRY_COUNT 3 #define HTTP_RETRY_COUNT 5 // Increased from 3
#define HTTP_RETRY_DELAY_MS 500 #define HTTP_RETRY_DELAY_MS 1000 // Increased from 500
// Allow external cancellation // Allow external cancellation
extern volatile bool g_UpdateCancelFlag; extern volatile bool g_UpdateCancelFlag;
// Global update mode setting
extern UpdateMode g_UpdateMode;
extern TaskHandle_t Update_Task_Handle; extern TaskHandle_t Update_Task_Handle;
/** /**
@ -32,6 +35,15 @@ extern TaskHandle_t Update_Task_Handle;
extern Version otaVersion; extern Version otaVersion;
/**
* @brief Update mode enumeration
*/
enum class UpdateMode {
UPDATE_FILES_ONLY, ///< Update files only, skip firmware
UPDATE_FIRMWARE_ONLY, ///< Update firmware only, skip files
UPDATE_BOTH ///< Update both files and firmware (default)
};
/** /**
* @brief File information structure * @brief File information structure
*/ */
@ -90,6 +102,16 @@ class AppUpdater {
*/ */
const String& getBaseUrl() const { return baseUrl; } const String& getBaseUrl() const { return baseUrl; }
/**
* @brief Set update mode (files only, firmware only, or both)
*/
void setUpdateMode(UpdateMode mode);
/**
* @brief Get current update mode
*/
UpdateMode getUpdateMode() const;
/** /**
* @brief Set progress callback function * @brief Set progress callback function
* @param callback Function to call with progress updates * @param callback Function to call with progress updates
@ -119,12 +141,25 @@ class AppUpdater {
*/ */
bool updateFile(const char* remotePath, const char* localPath, const char* expectedMd5); bool updateFile(const char* remotePath, const char* localPath, const char* expectedMd5);
/**
* @brief Results from checkManifest
*/
enum class ManifestCheckResult {
ERROR_FETCH_FAILED, ///< Failed to fetch manifest
ERROR_TOO_LARGE, ///< Manifest too large
ERROR_PARSE_FAILED, ///< Failed to parse manifest JSON
ERROR_NO_FILES_SECTION, ///< No files section in manifest
ERROR_NO_VERSION, ///< No version section in manifest
VERSION_CURRENT, ///< Current version is same or newer
UPDATE_AVAILABLE ///< New version available
};
/** /**
* @brief Get manifest content * @brief Get manifest content
* @param manifestPath Path to manifest file * @param manifestPath Path to manifest file
* @return Manifest content as a json document * @return Result indicating success, failure, or version status
*/ */
bool checkManifest(void); ManifestCheckResult checkManifest(void);
bool updateApp(void); bool updateApp(void);
@ -137,6 +172,7 @@ class AppUpdater {
UpdateStatus status; UpdateStatus status;
std::unique_ptr<uint8_t[]> downloadBuffer; std::unique_ptr<uint8_t[]> downloadBuffer;
bool updateAvailable = false; bool updateAvailable = false;
UpdateMode updateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware
@ -149,7 +185,7 @@ class AppUpdater {
* @return true if successful * @return true if successful
*/ */
bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength, bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength,
const char* localPath, const char* expectedMd5); const char* localPath, const char* remotePath, const char* expectedMd5);
/** /**
* @brief Update progress callback * @brief Update progress callback
@ -199,3 +235,10 @@ void handleUpdateProgress(AsyncWebServerRequest *request);
void startVersionCheckTask(); void startVersionCheckTask();
void versionCheckTask(void* parameter); void versionCheckTask(void* parameter);
// Convenience functions for setting update mode
void setGlobalUpdateMode(UpdateMode mode);
UpdateMode getGlobalUpdateMode();
void setUpdateModeFilesOnly();
void setUpdateModeFirmwareOnly();
void setUpdateModeBoth();

View File

@ -27,11 +27,33 @@ private:
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y) #define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
#define UNIQUE_NAME(base) CONCATENATE(base, __LINE__) #define UNIQUE_NAME(base) CONCATENATE(base, __LINE__)
// Macro for ON_EVERY_N_MILLISECONDS // Macro for ON_EVERY_N_MILLISECONDS (constant N via template)
#define ON_EVERY_N_MILLISECONDS(N) \ #define ON_EVERY_N_MILLISECONDS(N) \
static OnEveryN<N> UNIQUE_NAME(__on_everyN_); \ static OnEveryN<N> UNIQUE_NAME(__on_everyN_); \
if (UNIQUE_NAME(__on_everyN_).ready()) if (UNIQUE_NAME(__on_everyN_).ready())
// Runtime-configurable variant (interval can be a variable/expression)
class OnEveryMsVariable {
public:
OnEveryMsVariable() : lastTime(0) {}
bool ready(unsigned long interval) {
if (interval == 0) return false; // ignore 0 to avoid busy looping
unsigned long now = millis();
if (now - lastTime >= interval) {
lastTime = now;
return true;
}
return false;
}
private:
unsigned long lastTime;
};
// Macro for variable interval in milliseconds
#define ON_EVERY_MILLISECONDS(VAR_INTERVAL) \
static OnEveryVariable UNIQUE_NAME(__on_everyVar_); \
if (UNIQUE_NAME(__on_everyVar_).ready(VAR_INTERVAL))
// Macro for ON_EVERY_N_SECONDS // Macro for ON_EVERY_N_SECONDS
#define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000) #define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000)

326
include/RtttlPlayer.h Normal file
View File

@ -0,0 +1,326 @@
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/semphr.h>
// Tiny, efficient RTTTL player for ESP32 LEDC (non-blocking, priority-aware)
class RtttlPlayer {
public:
// pin: GPIO to output tone, channel: LEDC channel [0..15]
// timer: LEDC timer index [0..3], resolutionBits typically 10-13
RtttlPlayer(uint8_t pin, uint8_t channel, uint8_t timer=0, uint8_t resolutionBits=10,
uint16_t queueDepth=4)
: _pin(pin), _ch(channel), _timer(timer), _resBits(resolutionBits) {
// LEDC init
ledcSetup(_ch, /*freq*/ 1000, _resBits);
ledcAttachPin(_pin, _ch);
ledcWrite(_ch, 0); // duty 0 initially
// Sync primitives
_queue = xQueueCreate(queueDepth, sizeof(PlayItem));
_mtx = xSemaphoreCreateMutex();
_stopFlag = false;
_playing = false;
_curPrio = 0;
_preempt.item = nullptr; // Initialize preempt slot
// Playback task
xTaskCreatePinnedToCore(_taskThunk, "rtttl_task", 2048, this, /*prio*/ 1, &_taskHandle, 1);
}
~RtttlPlayer() {
if (_taskHandle) vTaskDelete(_taskHandle);
if (_queue) vQueueDelete(_queue);
if (_mtx) vSemaphoreDelete(_mtx);
}
// Non-blocking request to play a tune at 'priority'.
// Copies the RTTTL string internally (heap); returns false if queue full or alloc fails.
bool play(const char* rtttl, uint8_t priority) {
if (!rtttl) return false;
const size_t len = strnlen(rtttl, 512); // Reduce memory cap for efficiency
char* buf = (char*)malloc(len + 1);
if (!buf) return false;
memcpy(buf, rtttl, len);
buf[len] = '\0';
PlayItem item{buf, priority};
// Fast path: preempt if strictly higher priority than current
xSemaphoreTake(_mtx, portMAX_DELAY);
const bool shouldPreempt = _playing && (priority > _curPrio);
if (shouldPreempt) {
// Put new item into the front by sending to a small high-prio queue slot
// Approach: set preempt slot; playback loop will pick it ASAP.
if (_preempt.item) {
// Drop older preempt request to avoid leaks, keep newest
free((void*)_preempt.item->tune);
delete _preempt.item;
}
_preempt.item = new PlayItem(item); // copy
xSemaphoreGive(_mtx);
// we own original 'buf' no longer (copied into new PlayItem); free the temporary
free(buf);
// Signal stop to current note so task can switch between notes (cheap, cooperative)
_stopFlag = true;
return true;
}
xSemaphoreGive(_mtx);
// Otherwise enqueue and return (will play after current/earlier)
if (xQueueSend(_queue, &item, 0) == pdTRUE) {
return true;
} else {
free(buf);
return false;
}
}
// Stop playback immediately and flush queue (non-blocking).
void stopAll() {
xSemaphoreTake(_mtx, portMAX_DELAY);
_stopFlag = true;
// Drain queue
PlayItem tmp;
while (xQueueReceive(_queue, &tmp, 0) == pdTRUE) {
if (tmp.tune) free((void*)tmp.tune);
}
// Drop any pending preempt
if (_preempt.item) {
free((void*)_preempt.item->tune);
delete _preempt.item;
_preempt.item = nullptr;
}
xSemaphoreGive(_mtx);
}
private:
struct PlayItem {
const char* tune; // heap-allocated copy
uint8_t priority;
};
struct PreemptSlot {
PlayItem* item = nullptr; // one-slot "front of line"
};
// ===== LEDC helpers =====
inline void toneOn(uint32_t freq) {
if (freq == 0) { // pause
ledcWrite(_ch, 0);
return;
}
// Update LEDC frequency efficiently
// Use ledcSetup to set frequency (works on all Arduino-ESP32 versions)
ledcSetup(_ch, freq, _resBits);
// Duty: ~50% for square-like tone
const uint32_t dutyMax = (1U << _resBits) - 1U;
ledcWrite(_ch, dutyMax / 2);
}
inline void toneOff() {
ledcWrite(_ch, 0);
}
// ===== Playback task =====
static void _taskThunk(void* arg) {
((RtttlPlayer*)arg)->_taskLoop();
}
void _taskLoop() {
for (;;) {
// First check preempt slot, then queue
PlayItem item{};
if (_takePreempt(item) || xQueueReceive(_queue, &item, portMAX_DELAY) == pdTRUE) {
_playing = true;
_curPrio = item.priority;
_stopFlag = false;
_playOne(item.tune);
// cleanup
free((void*)item.tune);
_playing = false;
_curPrio = 0;
}
}
}
bool _takePreempt(PlayItem& out) {
bool got = false;
xSemaphoreTake(_mtx, portMAX_DELAY);
if (_preempt.item) {
out = *_preempt.item;
delete _preempt.item;
_preempt.item = nullptr;
got = true;
}
xSemaphoreGive(_mtx);
return got;
}
// ===== RTTTL parsing & playback =====
struct Defaults { uint16_t dur = 4; uint8_t oct = 5; uint16_t bpm = 63; };
// Note frequencies for octave 4 (rounded). Others are scaled by powers of two.
// Index by semitone: C, C#, D, D#, E, F, F#, G, G#, A, A#, B
static constexpr uint16_t baseA4 = 440;
// Well derive semitone freq using integer math: f = 440 * 2^((n)/12)
// To avoid floating point in the loop, we precompute a small LUT for octave 4.
static constexpr uint16_t LUT4[12] = {
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494
};
static uint32_t freqFor(uint8_t noteIndex, int8_t octave) {
// noteIndex 0..11; octave typical 3..7
// Get octave 4 freq and scale by 2^(oct-4)
uint32_t f = LUT4[noteIndex];
if (octave > 4) f <<= (octave - 4);
else if (octave < 4) f >>= (4 - octave);
return f;
}
// Returns next token start or nullptr when finished
static const char* skipSpaces(const char* p) {
while (p && *p && (*p == ' ' || *p == ',')) ++p;
return p;
}
void _playOne(const char* rtttl) {
if (!rtttl) return;
Defaults def;
// Format: name:d=4,o=5,b=120: note[,note...]
const char* p = strchr(rtttl, ':');
if (!p) return;
const char* p2 = strchr(p + 1, ':');
if (!p2) return;
// Parse defaults between first and second ':'
parseDefaults(p + 1, p2, def);
// Whole note duration in ms
uint32_t wholenote = (60000UL / def.bpm) * 4;
// Notes section
p = p2 + 1;
p = skipSpaces(p);
while (p && *p) {
// Cooperative preemption between notes
if (_checkPreempt()) break;
uint16_t duration = 0;
// 1) Optional duration number (e.g., 16)
while (*p >= '0' && *p <= '9') {
duration = duration * 10 + (*p - '0');
++p;
}
if (duration == 0) duration = def.dur;
// 2) Note letter or pause
bool pause = false;
uint8_t noteIndex = 0xFF;
switch (tolower(*p)) {
case 'c': noteIndex = 0; break;
case 'd': noteIndex = 2; break;
case 'e': noteIndex = 4; break;
case 'f': noteIndex = 5; break;
case 'g': noteIndex = 7; break;
case 'a': noteIndex = 9; break;
case 'b': noteIndex = 11; break;
case 'p': pause = true; break;
default: break;
}
if (*p) ++p;
// 3) Optional sharp '#'
if (!pause && *p == '#') {
if (noteIndex != 0xFF) noteIndex++;
++p;
}
// 4) Optional dotted note '.'
bool dotted = false;
if (*p == '.') { dotted = true; ++p; }
// 5) Optional octave number
int8_t octave = def.oct;
if (*p >= '4' && *p <= '7') { octave = (*p - '0'); ++p; }
// Compute duration in ms
uint32_t noteDur = wholenote / duration;
if (dotted) noteDur += noteDur / 2;
// Play the note
if (pause || noteIndex == 0xFF) {
toneOff();
vTaskDelay(pdMS_TO_TICKS(noteDur));
} else {
const uint32_t f = freqFor(noteIndex % 12, octave);
toneOn(f);
// Shorten a little to add a tiny gap (staccato for clarity & queue responsiveness)
uint32_t onMs = (noteDur >= 4) ? (noteDur - 2) : noteDur;
uint32_t offMs = noteDur - onMs;
vTaskDelay(pdMS_TO_TICKS(onMs));
toneOff();
if (offMs) vTaskDelay(pdMS_TO_TICKS(offMs));
}
p = skipSpaces(p);
// Optional trailing comma already handled by skipSpaces
}
toneOff();
}
void parseDefaults(const char* beg, const char* end, Defaults& def) {
const char* p = beg;
while (p < end && *p) {
// key=value (d,o,b)
char key = tolower(*p);
const char* eq = (const char*)memchr(p, '=', end - p);
if (!eq) break;
const char* val = eq + 1;
const char* nxt = (const char*)memchr(val, ',', end - val);
if (!nxt) nxt = end;
uint32_t v = 0;
for (const char* t = val; t < nxt; ++t) {
if (*t >= '0' && *t <= '9') v = v * 10 + (*t - '0');
}
if (key == 'd' && v) def.dur = v;
else if (key == 'o' && v) def.oct = (uint8_t)v;
else if (key == 'b' && v) def.bpm = v;
p = (nxt < end) ? (nxt + 1) : end;
}
// Clamp sane ranges
if (def.dur == 0) def.dur = 4;
if (def.oct < 3 || def.oct > 7) def.oct = 5;
if (def.bpm < 20) def.bpm = 20;
if (def.bpm > 400) def.bpm = 400;
}
bool _checkPreempt() {
if (!_stopFlag) return false;
// Grab preempt immediately if present, otherwise just stop current tune
PlayItem nxt{};
if (_takePreempt(nxt)) {
// Play preempted tune immediately (no recursion - direct call)
_curPrio = nxt.priority;
_stopFlag = false;
_playOne(nxt.tune);
free((void*)nxt.tune);
}
// Indicate current tune should finish early
return true;
}
// ===== members =====
uint8_t _pin, _ch, _timer, _resBits;
TaskHandle_t _taskHandle = nullptr;
QueueHandle_t _queue = nullptr;
SemaphoreHandle_t _mtx = nullptr;
volatile bool _stopFlag;
volatile bool _playing;
volatile uint8_t _curPrio;
PreemptSlot _preempt;
};

View File

@ -21,21 +21,19 @@ typedef enum {
//Tunes //Tunes
typedef struct { typedef struct {
bool enabled;
int cycles; int cycles;
int pause; int pause;
String melody; String melody;
}BUZZ_TUNE; }BUZZ_TUNE;
#define TUNE_MAX_COUNT 12 #define TUNE_MAX_COUNT 14
extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT]; extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
void Init_Buzzer(int8_t, const char* configPath); void Init_Buzzer(int8_t pin, const char* configPath, int8_t channel = -1);
void Buzzer_Beep(int, int freq=1000);
void Buzzer_Load_Tunes(const char* tunesPath); void Buzzer_Load_Tunes(const char* tunesPath);
void Buzzer_Play_Tune(TUNE_TYPE, bool async=true, bool priority=true); void Buzzer_Play_Tune(TUNE_TYPE tune, int priority=1);

View File

@ -1,6 +1,6 @@
#define FIRMWARE_VERSION_MAJOR 1 #define FIRMWARE_VERSION_MAJOR 1
#define FIRMWARE_VERSION_MINOR 4 #define FIRMWARE_VERSION_MINOR 4
#define FIRMWARE_VERSION_PATCH 9 #define FIRMWARE_VERSION_PATCH 19
#define FIRMWARE_DESCRIPTION \ #define FIRMWARE_DESCRIPTION \

View File

@ -8,6 +8,7 @@
#include <functional> #include <functional>
#include "system.h" #include "system.h"
#include "ColorPalettes.h" #include "ColorPalettes.h"
#include "global.h"
//#include <crgb.h> //#include <crgb.h>
#define FASTLED_CORE 0 #define FASTLED_CORE 0
@ -65,10 +66,37 @@ void Init_Lights_Task(void){
Init_Strip(ledSettings[1].leds, ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip, ledSettings[1].bright); Init_Strip(ledSettings[1].leds, ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip, ledSettings[1].bright);
ESP_LOGD(tag, "Initializing Strip2: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip); ESP_LOGD(tag, "Initializing Strip2: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip);
xTaskCreatePinnedToCore(Lights_Control_Task, "Lights_Task", 1024*8, NULL, 1, &Animation_Task_Handle, FASTLED_CORE); xTaskCreatePinnedToCore(Lights_Control_Task, "Lights_Task", 1024*6, NULL, 1, &Animation_Task_Handle, FASTLED_CORE);
ESP_LOGI(tag, "Lights Task Created..."); ESP_LOGI(tag, "Lights Task Created...");
} }
/*
void Init_Ramp_Lights_Task(void){
xTaskCreatePinnedToCore(Ramp_Lights_Control_Task, "Ramp_Lights_Task", 1024*1, NULL, 1, &Animation_Task_Handle, (FASTLED_CORE ? 1 : 0));
ESP_LOGI(tag, "Ramp Lights Task Created...");
}
void RampUpLights(int level)
{
}
void Ramp_Lights_Control_Task(void *parameters)
{
static *OutputPWM* pwmOut = NULL;
pwmOut = pwmOut[sys_settings.rampLightSettings[0].pwmOutIndex];
while(1){
sys_settings.rampLightSettings[rampIndex].pwmOutIndex
while()
vTaskDelay(100 / portTICK_PERIOD_MS);
vTaskSuspend(NULL);
}
}
*/
void Animation_Loop_Exit(void){ void Animation_Loop_Exit(void){
if( Animation_Task_Handle ){ if( Animation_Task_Handle ){

View File

@ -4,6 +4,12 @@
#include <FastLED.h> #include <FastLED.h>
#include "ColorPalettes.h" #include "ColorPalettes.h"
#include "esp_system.h" #include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include <functional>
#include "PWM_Output.h"
typedef struct{ typedef struct{
@ -449,6 +455,67 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
} }
} }
/*
void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0, PWM_Output* pwmOutput = nullptr) {
if (!leds || size <= 1 || totalDurationMs <= 0) return;
const int halfSize = size / 2;
const float msPerLed = totalDurationMs / (float)halfSize;
unsigned long startTime = millis();
// Define the point at which PWM begins ramping (75% of fill time)
const float pwmStartPoint = 0.75f;
const int pwmStartLeds = halfSize * pwmStartPoint;
// Initialize PWM output to 0 if provided
if (pwmOutput) {
pwmOutput->setOutput(0.0f);
}
fill_solid(leds, size, baseCol);
int prevLedsToLight = 0;
unsigned long currentTime;
unsigned long elapsedTime;
int ledsToLight, pos;
Animation_Loop(activeFlag, 90, [&]() -> int {
currentTime = millis();
elapsedTime = currentTime - startTime;
// Calculate how many LEDs should be lit based on elapsed time
ledsToLight = (elapsedTime / msPerLed);
if (ledsToLight > halfSize) ledsToLight = halfSize;
// Fill LEDs up to current position
for (int i = 0; i < ledsToLight; i++) {
pos = (i + shift + size) % size;
leds[pos] = fillCol;
leds[(size - 1 - i + shift + size) % size] = fillCol; // Correct mirroring calculation
}
// Handle PWM output ramp starting at 75% of fill time
if (pwmOutput && ledsToLight >= pwmStartLeds) {
// Calculate PWM value as percentage of remaining fill time
// Map from [pwmStartLeds, halfSize] to [0, 100]
float pwmValue = map(ledsToLight, pwmStartLeds, halfSize, 0, 100);
// Ensure pwmValue is in range [0, 100]
pwmValue = constrain(pwmValue, 0.0f, 100.0f);
// Set the PWM output
pwmOutput->setOutput(pwmValue);
}
// Update LEDs only when necessary
if(prevLedsToLight < ledsToLight){
FastLED.show();
}
prevLedsToLight = ledsToLight;
// Return 1 when complete
return (ledsToLight >= halfSize) ? 1 : 0;
});
}
*/
void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0) { void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0) {
if (!leds || size <= 1 || totalDurationMs <= 0) return; if (!leds || size <= 1 || totalDurationMs <= 0) return;

View File

@ -3,39 +3,56 @@
#include <MD5Builder.h> #include <MD5Builder.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <memory> #include <memory>
#include <algorithm>
#include "global.h" #include "global.h"
#include "JsonConstrain.h" #include "JsonConstrain.h"
#include "BLE_UpdateService.h" #include "BLE_UpdateService.h"
#include <HTTPClient.h> #include <HTTPClient.h>
#include <Update.h> #include <Update.h>
#include <cstring> #include <cstring>
#include <esp_task_wdt.h>
#include <esp_ota_ops.h>
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 volatile bool g_UpdateCancelFlag = false; // cancellation flag
UpdateMode g_UpdateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware
// Queue handle for firmware update messages
//QueueHandle_t updateMsgQueue = NULL;
String updateUrl = ""; String updateUrl = "";
Version otaVersion; Version otaVersion;
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin) 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]) : localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs)
{ {
// Use dynamic buffer size based on available memory - much more conservative now
size_t available_heap = ESP.getFreeHeap();
size_t buffer_size = std::min<size_t>(BUFFER_SIZE, available_heap / 16); // Use at most 1/16 of free heap
// Ensure minimum viable size
if (buffer_size < 1024) buffer_size = 1024; // Absolute minimum is 1KB
downloadBuffer.reset(new uint8_t[buffer_size]);
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL); baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
// Ensure baseUrl ends with a single '/' // Ensure baseUrl ends with a single '/'
if(!baseUrl.endsWith("/")) baseUrl += "/"; if(!baseUrl.endsWith("/")) baseUrl += "/";
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str()); ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s, buffer=%u bytes",
localVersion.toString().c_str(), baseUrl.c_str(), buffer_size);
} }
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) { void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
progressCb = callback; progressCb = callback;
} }
void AppUpdater::setUpdateMode(UpdateMode mode) {
updateMode = mode;
g_UpdateMode = mode; // Sync with global mode for firmware update task
}
UpdateMode AppUpdater::getUpdateMode() const {
return updateMode;
}
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) { void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
status = newStatus; status = newStatus;
if (progressCb) { if (progressCb) {
@ -43,15 +60,17 @@ void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const ch
} }
} }
bool AppUpdater::checkManifest() { AppUpdater::ManifestCheckResult 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());
String payload; String payload;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){ for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false; if(g_UpdateCancelFlag) return ManifestCheckResult::ERROR_FETCH_FAILED;
HTTPClient http; HTTPClient http;
http.begin(url); http.begin(url);
http.setTimeout(10); // 10 second timeout
http.setConnectTimeout(10000); // 10 second connect timeout
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) { if (httpCode == HTTP_CODE_OK) {
payload = http.getString(); payload = http.getString();
@ -64,12 +83,12 @@ bool AppUpdater::checkManifest() {
} }
if(payload.isEmpty()){ if(payload.isEmpty()){
ESP_LOGE(TAG, "Failed to fetch manifest after retries"); ESP_LOGE(TAG, "Failed to fetch manifest after retries");
return false; return ManifestCheckResult::ERROR_FETCH_FAILED;
} }
if(payload.length() > MAX_MANIFEST_SIZE){ if(payload.length() > MAX_MANIFEST_SIZE){
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length()); ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
return false; return ManifestCheckResult::ERROR_TOO_LARGE;
} }
// Parse JSON // Parse JSON
@ -77,14 +96,14 @@ bool AppUpdater::checkManifest() {
ESP_LOGD(TAG, "Manifest deserialized"); ESP_LOGD(TAG, "Manifest deserialized");
if (error) { if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str()); ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false; return ManifestCheckResult::ERROR_PARSE_FAILED;
} }
// Check for files section // Check for files section
jsonFilesArray = jsonManifest["files"]; jsonFilesArray = jsonManifest["files"];
if (jsonFilesArray.isNull()) { if (jsonFilesArray.isNull()) {
ESP_LOGE(TAG, "No files section in manifest"); ESP_LOGE(TAG, "No files section in manifest");
return false; return ManifestCheckResult::ERROR_NO_FILES_SECTION;
}else{ }else{
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size()); ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
} }
@ -94,7 +113,7 @@ bool AppUpdater::checkManifest() {
ESP_LOGD(TAG, "Version section found"); ESP_LOGD(TAG, "Version section found");
if (jsonVersion.isNull()) { if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest"); ESP_LOGE(TAG, "No version section in manifest");
return false; return ManifestCheckResult::ERROR_NO_VERSION;
} }
// Get the remote version // Get the remote version
@ -110,16 +129,18 @@ bool AppUpdater::checkManifest() {
updateAvailable = false; updateAvailable = false;
// Only mark update available if remote is strictly newer than local // Only mark update available if remote is strictly newer than local
if (otaVersion <= localVersion) { if (otaVersion <= localVersion) {
ESP_LOGI(TAG, "No updates available"); ESP_LOGI(TAG, "No updates available: remote=%s, local=%s",
return false; otaVersion.toString().c_str(), localVersion.toString().c_str());
return ManifestCheckResult::VERSION_CURRENT;
}else{ }else{
updateAvailable = true; updateAvailable = true;
ESP_LOGD(TAG, "Update available"); ESP_LOGI(TAG, "Update available: remote=%s, local=%s",
otaVersion.toString().c_str(), localVersion.toString().c_str());
} }
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str()); //ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
return true; return ManifestCheckResult::UPDATE_AVAILABLE;
} }
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) { bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
@ -133,7 +154,10 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
bool skip = false; bool skip = false;
if(fileSystem.exists(localPath)){ if(fileSystem.exists(localPath)){
String localMd5 = getLocalMD5(localPath); String localMd5 = getLocalMD5(localPath);
ESP_LOGI(TAG, "Local file exists: %s, MD5: %s, Expected: %s", localPath, localMd5.c_str(), expectedMd5);
if(localMd5.equals(expectedMd5)) skip = true; if(localMd5.equals(expectedMd5)) skip = true;
} else {
ESP_LOGI(TAG, "Local file does not exist: %s", localPath);
} }
if(skip){ if(skip){
ESP_LOGI(TAG, "File already up to date: %s", localPath); ESP_LOGI(TAG, "File already up to date: %s", localPath);
@ -141,6 +165,8 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
return true; return true;
} }
ESP_LOGI(TAG, "Need to download file: %s (local MD5 mismatch or file missing)", localPath);
// Start the download // Start the download
HTTPClient http; HTTPClient http;
int httpCode = -1; int httpCode = -1;
@ -154,8 +180,9 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS)); 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 for %s: HTTP code %d", localPath, httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed"); updateProgress(UpdateStatus::ERROR, 0, String(String("Download failed: ") + localPath).c_str());
http.end();
return false; return false;
} }
@ -164,10 +191,20 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
size_t contentLength = http.getSize(); size_t contentLength = http.getSize();
// Verify and save the file // Verify and save the file
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5); bool success = verifyAndSaveFile(stream, contentLength, localPath, remotePath, expectedMd5);
http.end(); http.end();
if(!success){ if(!success){
updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed"); String errMsg = String(localPath) + " MD5 failed";
updateProgress(UpdateStatus::ERROR, 0, errMsg.c_str());
// For HTML/CSS/JS files, we might have saved them anyway (see verifyAndSaveFile logic)
if (fileSystem.exists(localPath) &&
(String(localPath).endsWith(".html") ||
String(localPath).endsWith(".css") ||
String(localPath).endsWith(".js"))) {
ESP_LOGW(TAG, "Using %s despite MD5 mismatch (non-critical file)", localPath);
// Return false to indicate verification failure but file is still usable
}
}else{ }else{
updateProgress(UpdateStatus::FILE_SAVED, 100, localPath); updateProgress(UpdateStatus::FILE_SAVED, 100, localPath);
} }
@ -175,19 +212,59 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
return success; return success;
} }
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5) bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* remotePath, const char* expectedMd5)
{ {
const int MAX_RETRIES = 2; // Maximum number of retries for MD5 failure
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
if (retry > 0) {
ESP_LOGW(TAG, "Retrying download of %s (attempt %d/%d)", localPath, retry, MAX_RETRIES);
// Need to re-fetch the file for retry - use the REMOTE path, not local path
String url = buildUrl(remotePath);
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Retry download failed: %d", httpCode);
http.end();
continue; // Try next retry if available
}
stream = http.getStreamPtr();
contentLength = http.getSize();
}
MD5Builder md5; MD5Builder md5;
md5.begin(); md5.begin();
size_t totalRead = 0; size_t totalRead = 0;
// Create temporary filename // Create temporary filename - try root directory first to avoid path issues
String tempPath = String(localPath) + ".tmp"; String baseName = String(localPath);
baseName.replace("/", "_");
baseName.replace("\\", "_");
String tempPath = "/temp_" + baseName + ".download";
// Open temporary file for writing // Clean up any existing temp file first
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE); if (fileSystem.exists(tempPath.c_str())) {
ESP_LOGW(TAG, "Removing existing temp file: %s", tempPath.c_str());
fileSystem.remove(tempPath.c_str());
}
ESP_LOGI(TAG, "Using temp file path: %s for target: %s", tempPath.c_str(), localPath);
// Open temporary file for writing (LittleFS will create directories automatically)
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE, true); // true = create if not exists
if (!file) { if (!file) {
ESP_LOGE(TAG, "Failed to open temporary file for writing"); ESP_LOGE(TAG, "Failed to open temporary file for writing: %s", tempPath.c_str());
// Try to diagnose the issue
ESP_LOGE(TAG, "LittleFS info - Used: %u bytes, Total: %u bytes",
LittleFS.usedBytes(), LittleFS.totalBytes());
// Check if we're out of space
if (LittleFS.usedBytes() >= LittleFS.totalBytes() * 0.95) {
ESP_LOGE(TAG, "LittleFS nearly full - may not have space for temp file");
}
return false; return false;
} }
@ -247,20 +324,99 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
// Verify MD5 hash // Verify MD5 hash
updateProgress(UpdateStatus::VERIFYING, 90, localPath); updateProgress(UpdateStatus::VERIFYING, 90, localPath);
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath); ESP_LOGI(TAG, "MD5 verification for %s: Expected='%s', Calculated='%s'",
localPath, expectedMd5, calculatedMd5.c_str());
// Compare MD5 case-insensitively (in case there are case differences)
String expectedMd5Lower = String(expectedMd5);
expectedMd5Lower.toLowerCase();
String calculatedMd5Lower = calculatedMd5;
calculatedMd5Lower.toLowerCase();
if (!calculatedMd5Lower.equals(expectedMd5Lower)) {
ESP_LOGE(TAG, "MD5 mismatch for %s (attempt %d/%d). Expected: %s, Got: %s",
localPath, retry+1, MAX_RETRIES+1, expectedMd5, calculatedMd5.c_str());
ESP_LOGE(TAG, "Length comparison - Expected: %d chars, Got: %d chars",
strlen(expectedMd5), calculatedMd5.length());
fileSystem.remove(tempPath.c_str()); fileSystem.remove(tempPath.c_str());
if (retry < MAX_RETRIES) {
// Will retry in next loop iteration
continue;
}
// Special case for certain file types - allow them to be used even with MD5 mismatch
// This is a fallback option for non-critical files like HTML pages
bool isNonCriticalFile = false;
if (String(localPath).endsWith(".html") ||
String(localPath).endsWith(".css") ||
String(localPath).endsWith(".js")) {
isNonCriticalFile = true;
}
if (isNonCriticalFile) {
ESP_LOGW(TAG, "Using file %s despite MD5 mismatch (non-critical file)", localPath);
// We'll still keep this file but report it as a verification failure
// Ensure target directory exists before rename
String dirPath = String(localPath);
int lastSlash = dirPath.lastIndexOf('/');
if (lastSlash > 0) {
dirPath = dirPath.substring(0, lastSlash);
if (!fileSystem.exists(dirPath.c_str())) {
ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str());
String dummyFile = dirPath + "/.dummy";
File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true);
if (dummy) {
dummy.print("temp");
dummy.close();
fileSystem.remove(dummyFile.c_str());
}
}
}
// Rename the temp file to the final location
if (fileSystem.exists(localPath)) {
fileSystem.remove(localPath);
}
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
ESP_LOGE(TAG, "Failed to rename temporary file for non-critical use: %s -> %s",
tempPath.c_str(), localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
// Return false to indicate verification failure, but the file will still be used
return false;
}
return false; return false;
} }
updateProgress(UpdateStatus::VERIFYING, 95, localPath); updateProgress(UpdateStatus::VERIFYING, 95, localPath);
// Ensure target directory exists before rename
String dirPath = String(localPath);
int lastSlash = dirPath.lastIndexOf('/');
if (lastSlash > 0) {
dirPath = dirPath.substring(0, lastSlash);
if (!fileSystem.exists(dirPath.c_str())) {
ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str());
String dummyFile = dirPath + "/.dummy";
File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true);
if (dummy) {
dummy.print("temp");
dummy.close();
fileSystem.remove(dummyFile.c_str());
}
}
}
// 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);
} }
if (!fileSystem.rename(tempPath.c_str(), localPath)) { if (!fileSystem.rename(tempPath.c_str(), localPath)) {
ESP_LOGE(TAG, "Failed to rename temporary file"); ESP_LOGE(TAG, "Failed to rename temporary file: %s -> %s", tempPath.c_str(), localPath);
fileSystem.remove(tempPath.c_str()); fileSystem.remove(tempPath.c_str());
return false; return false;
} }
@ -269,6 +425,9 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
return true; return true;
} }
return false; // All retries failed
}
String AppUpdater::getLocalMD5(const char* filePath){ String AppUpdater::getLocalMD5(const char* filePath){
File file = fileSystem.open(filePath, "r"); File file = fileSystem.open(filePath, "r");
if(!file){ if(!file){
@ -295,6 +454,7 @@ String AppUpdater::getLocalMD5(const char* filePath){
bool AppUpdater::updateFilesArray() { bool AppUpdater::updateFilesArray() {
int successCount = 0; int successCount = 0;
int totalFiles = jsonFilesArray.size(); int totalFiles = jsonFilesArray.size();
int failedCount = 0;
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles); ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
// Iterate over each file entry in the manifest // Iterate over each file entry in the manifest
@ -320,11 +480,19 @@ bool AppUpdater::updateFilesArray() {
// Attempt to update the file // Attempt to update the file
if (updateFile(remotePath, localPath, expectedMd5)) { if (updateFile(remotePath, localPath, expectedMd5)) {
successCount++; successCount++;
} else {
failedCount++;
ESP_LOGE(TAG, "Failed to update file: %s", localPath);
// Continue with remaining files instead of stopping
} }
} }
ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles); ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated, %d failed",
return successCount == totalFiles; successCount, totalFiles, failedCount);
// Consider the update successful if most files updated correctly
// Allow up to 20% of files to fail before considering the entire update failed
return (failedCount <= totalFiles * 0.2);
} }
bool AppUpdater::updateApp() { bool AppUpdater::updateApp() {
@ -341,17 +509,53 @@ bool AppUpdater::updateApp() {
const char* expectedMd5 = jsonManifest["firmware"]["md5"]; const char* expectedMd5 = jsonManifest["firmware"]["md5"];
String firmwareUrl = buildUrl(appName); String firmwareUrl = buildUrl(appName);
// Download the firmware // First, try a HEAD request to verify server connectivity and file availability
HTTPClient httpHead;
bool fileExists = false;
httpHead.begin(firmwareUrl);
httpHead.setTimeout(10);
int headCode = httpHead.sendRequest("HEAD");
if (headCode == HTTP_CODE_OK) {
ESP_LOGI(TAG, "Firmware file exists on server, size: %d bytes", httpHead.getSize());
fileExists = true;
} else {
ESP_LOGW(TAG, "HEAD request failed: %d, proceeding with direct download attempt", headCode);
}
httpHead.end();
// Download the firmware with progressive timeouts
HTTPClient http; HTTPClient http;
int httpCode = -1; int httpCode = -1;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){ for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false; if(g_UpdateCancelFlag) return false;
// Progressive timeout increase with each attempt
int timeout = 30 + (attempt * 15); // Start with 30s, add 15s each retry (30, 45, 60, 75, 90)
int connectTimeout = 15000 + (attempt * 5000); // Start with 15s, add 5s each retry
http.begin(firmwareUrl); http.begin(firmwareUrl);
http.setTimeout(timeout);
http.setConnectTimeout(connectTimeout);
ESP_LOGI(TAG, "Downloading firmware from %s (attempt %d/%d, timeout=%ds)",
firmwareUrl.c_str(), attempt+1, HTTP_RETRY_COUNT, timeout);
updateProgress(UpdateStatus::DOWNLOADING, 0,
String("Firmware download attempt " + String(attempt+1) + "/" +
String(HTTP_RETRY_COUNT)).c_str());
httpCode = http.GET(); httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) break; if(httpCode == HTTP_CODE_OK) break;
ESP_LOGW(TAG, "Firmware GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode); ESP_LOGW(TAG, "Firmware 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));
// Exponential backoff - start with HTTP_RETRY_DELAY_MS and double each time
int delayMs = HTTP_RETRY_DELAY_MS * (1 << attempt); // 1000, 2000, 4000, 8000, 16000 ms
if(attempt+1 < HTTP_RETRY_COUNT) {
ESP_LOGI(TAG, "Waiting %d ms before next attempt", delayMs);
vTaskDelay(pdMS_TO_TICKS(delayMs));
}
} }
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);
@ -361,6 +565,46 @@ bool AppUpdater::updateApp() {
// Check available space // Check available space
size_t firmwareSize = http.getSize(); size_t firmwareSize = http.getSize();
// Pre-validation checks
if (firmwareSize < 1024*100) { // Sanity check - 100KB minimum expected
ESP_LOGE(TAG, "Firmware size too small (%d bytes), possible corruption", firmwareSize);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware size");
http.end();
return false;
}
// Verify OTA partitions
const esp_partition_t* running = esp_ota_get_running_partition();
const esp_partition_t* update = esp_ota_get_next_update_partition(NULL);
if (!running || !update) {
ESP_LOGE(TAG, "Failed to get partitions");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Failed to get partitions");
http.end();
return false;
}
ESP_LOGI(TAG, "Running partition: %s, update target: %s",
running->label, update->label);
// Check for sufficient free memory - much more lenient now with chunked downloads
size_t freeHeap = ESP.getFreeHeap();
size_t minRequiredHeap = 40 * 1024; // Only require 40KB minimum
ESP_LOGI(TAG, "Free heap: %d bytes, firmware size: %d bytes", freeHeap, firmwareSize);
if (freeHeap < minRequiredHeap) {
ESP_LOGE(TAG, "Critically low memory: %d bytes (minimum required: %d bytes)",
freeHeap, minRequiredHeap);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Critically low memory");
http.end();
return false;
}
// Check if we can roll back (just for logging, not a blocker)
if (!Update.canRollBack()) {
ESP_LOGW(TAG, "No valid firmware to roll back to");
}
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) { if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
ESP_LOGE(TAG, "Firmware: Not enough space for update"); ESP_LOGE(TAG, "Firmware: Not enough space for update");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update"); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
@ -372,26 +616,79 @@ bool AppUpdater::updateApp() {
MD5Builder md5; MD5Builder md5;
md5.begin(); md5.begin();
// Download and verify firmware // Prepare for download with improved resilience
WiFiClient* stream = http.getStreamPtr(); WiFiClient* stream = http.getStreamPtr();
unsigned long lastProgressTime = millis(); // Track time since last progress
unsigned long connectStartTime = millis(); // Track total download time
unsigned long lastWatchdogKick = millis(); // Track watchdog kicks
// Calculate smaller buffer size based on heap
size_t bufferSize = std::min(size_t(4096), ESP.getFreeHeap() / 16); // Use at most 1/16 of free heap, max 4KB
if (bufferSize < 1024) bufferSize = 1024; // Minimum 1KB
ESP_LOGI(TAG, "Using download buffer size: %u bytes (free heap: %u bytes)",
bufferSize, ESP.getFreeHeap());
// Create a buffer just for this operation if needed
std::unique_ptr<uint8_t[]> localBuffer;
uint8_t* downloadBuf = nullptr;
if (bufferSize <= BUFFER_SIZE) {
// Use the existing downloadBuffer
downloadBuf = downloadBuffer.get();
} else {
// Create a larger buffer for this specific download
localBuffer.reset(new uint8_t[bufferSize]);
downloadBuf = localBuffer.get();
}
updateProgress(UpdateStatus::DOWNLOADING, 0, "Firmware download started");
// Download and verify firmware
if (firmwareSize > 0) { if (firmwareSize > 0) {
size_t remaining = firmwareSize; size_t remaining = firmwareSize;
while (remaining > 0) { size_t totalReceived = 0;
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; } size_t failedReads = 0; // Count consecutive read failures
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE)); const size_t MAX_FAILED_READS = 10; // Allow up to 10 consecutive failures
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
// Check for timeout while (remaining > 0 && failedReads < MAX_FAILED_READS) {
if (read == 0) { // Check for cancellation
ESP_LOGE(TAG, "Read timeout"); if(g_UpdateCancelFlag) {
ESP_LOGE(TAG, "Update cancelled by user");
Update.abort(); Update.abort();
http.end(); http.end();
return false; return false;
} }
// Check WiFi status every 5 seconds
if (millis() - lastWatchdogKick > 5000) {
if (WiFi.status() != WL_CONNECTED) {
ESP_LOGE(TAG, "WiFi disconnected during download");
Update.abort();
http.end();
return false;
}
// Reset watchdog to prevent timeout
esp_task_wdt_reset();
lastWatchdogKick = millis();
}
// Calculate optimum chunk size to balance speed vs reliability
size_t chunk = std::min(remaining, bufferSize);
size_t available = stream->available();
if (available > 0) {
// Read what's available (up to our chunk size)
size_t read = stream->readBytes(downloadBuf, std::min(available, chunk));
if (read > 0) {
// Reset failure counter on successful read
failedReads = 0;
// Update MD5 and write firmware // Update MD5 and write firmware
md5.add(downloadBuffer.get(), read); md5.add(downloadBuf, read);
if (Update.write(downloadBuffer.get(), read) != read) { if (Update.write(downloadBuf, read) != read) {
ESP_LOGE(TAG, "Write failed"); ESP_LOGE(TAG, "Write failed");
Update.abort(); Update.abort();
http.end(); http.end();
@ -399,22 +696,107 @@ bool AppUpdater::updateApp() {
} }
remaining -= read; remaining -= read;
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware"); totalReceived += read;
lastProgressTime = millis(); // Reset progress timer
// Update progress every ~2% or at least every 2 seconds
static int lastPercent = 0;
int percent = (totalReceived * 100) / firmwareSize;
if (percent != lastPercent || (millis() - lastProgressTime > 2000)) {
updateProgress(UpdateStatus::DOWNLOADING, percent, "Firmware");
lastPercent = percent;
} }
} else { } else {
// Unknown size: stream until end // Handle zero bytes read despite data being available
for (;;) { failedReads++;
ESP_LOGW(TAG, "Read returned 0 bytes despite data available (failure %d/%d)",
failedReads, MAX_FAILED_READS);
delay(100); // Short delay before retry
}
} else {
// No data currently available
if (millis() - lastProgressTime > 60000) {
// No progress for 60 seconds - too long
ESP_LOGE(TAG, "Download timed out - no progress for 60 seconds");
Update.abort();
http.end();
return false;
}
// Send periodic progress updates to keep client informed
if (millis() - lastWatchdogKick > 5000) {
int percent = (totalReceived * 100) / firmwareSize;
updateProgress(UpdateStatus::DOWNLOADING, percent,
String("Firmware: " + String(percent) + "% - waiting for data...").c_str());
}
delay(100); // Short delay to prevent CPU hogging
}
}
// Check if we failed due to too many consecutive read failures
if (failedReads >= MAX_FAILED_READS) {
ESP_LOGE(TAG, "Too many consecutive read failures");
Update.abort();
http.end();
return false;
}
} else {
// Unknown size: stream until end (less common case)
ESP_LOGW(TAG, "Firmware size unknown, streaming until end");
size_t totalReceived = 0;
size_t emptyReads = 0;
const size_t MAX_EMPTY_READS = 20; // Allow up to 20 empty reads before considering done
while (emptyReads < MAX_EMPTY_READS) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; } if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (read == 0) break; // Reset watchdog periodically
md5.add(downloadBuffer.get(), read); if (millis() - lastWatchdogKick > 5000) {
if (Update.write(downloadBuffer.get(), read) != read) { esp_task_wdt_reset();
lastWatchdogKick = millis();
}
size_t available = stream->available();
if (available > 0) {
size_t read = stream->readBytes(downloadBuf, std::min(available, bufferSize));
if (read > 0) {
emptyReads = 0; // Reset empty read counter
md5.add(downloadBuf, read);
if (Update.write(downloadBuf, read) != read) {
ESP_LOGE(TAG, "Write failed"); ESP_LOGE(TAG, "Write failed");
Update.abort(); Update.abort();
http.end(); http.end();
return false; return false;
} }
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware"); totalReceived += read;
lastProgressTime = millis();
// Just update with received byte count since we don't know total
updateProgress(UpdateStatus::DOWNLOADING, 0,
String("Firmware: " + String(totalReceived / 1024) + "KB received").c_str());
} else {
emptyReads++;
delay(100);
}
} else {
// No data available
if (totalReceived > 0 && millis() - lastProgressTime > 30000) {
// If we've received data but nothing for 30s, probably done
ESP_LOGI(TAG, "No data for 30s after receiving %d bytes, assuming download complete", totalReceived);
break;
}
emptyReads++;
delay(100);
}
}
if (totalReceived < 100*1024) { // Less than 100KB received
ESP_LOGE(TAG, "Downloaded firmware too small (%d bytes)", totalReceived);
Update.abort();
http.end();
return false;
} }
} }
@ -432,8 +814,16 @@ bool AppUpdater::updateApp() {
// Finish update // Finish update
if (!Update.end()) { if (!Update.end()) {
ESP_LOGE(TAG, "Update end failed"); ESP_LOGE(TAG, "Update end failed: %d", Update.getError());
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed"); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
// Try to roll back if possible
if (Update.hasError() && Update.canRollBack()) {
ESP_LOGI(TAG, "Rolling back to previous version");
Update.rollBack();
// Don't restart here, let the main task do it
}
http.end(); http.end();
return false; return false;
} }
@ -468,49 +858,131 @@ void startFirmwareUpdateTask(AsyncEventSource* evProg) {
ESP_LOGW(TAG, "Firmware update task already running"); ESP_LOGW(TAG, "Firmware update task already running");
return; return;
} }
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle); // Create task with higher priority (3) and optimized stack size
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*6, NULL, 3, &Update_Task_Handle);
} }
void firmwareUpdateTask(void* parameter) { void firmwareUpdateTask(void* parameter) {
static const char* TAG = "UpdateTask"; static const char* TAG = "UpdateTask";
AppUpdater* updater = nullptr;
// Initialize watchdog timer for the update task
esp_task_wdt_init(60, true); // 60 second timeout, panic on timeout
esp_task_wdt_add(NULL); // Add current task to watchdog
try { try {
loadUpdateJson(); loadUpdateJson();
// Initialize updater esp_task_wdt_reset(); // Reset watchdog timer after JSON loading
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
// Initialize updater with smart pointer
std::unique_ptr<AppUpdater> updater(new AppUpdater(
LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin"));
updater->setProgressCallback(updateProgress); updater->setProgressCallback(updateProgress);
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str()); ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
// Check and perform updates // Check and perform updates
if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); } auto manifestResult = updater->checkManifest();
if (manifestResult != AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE) {
// Handle different error cases
std::string errorMsg;
switch (manifestResult) {
case AppUpdater::ManifestCheckResult::ERROR_FETCH_FAILED:
errorMsg = "Failed to fetch manifest";
break;
case AppUpdater::ManifestCheckResult::ERROR_TOO_LARGE:
errorMsg = "Manifest file too large";
break;
case AppUpdater::ManifestCheckResult::ERROR_PARSE_FAILED:
errorMsg = "Failed to parse manifest";
break;
case AppUpdater::ManifestCheckResult::ERROR_NO_FILES_SECTION:
errorMsg = "Manifest missing files section";
break;
case AppUpdater::ManifestCheckResult::ERROR_NO_VERSION:
errorMsg = "Manifest missing version section";
break;
case AppUpdater::ManifestCheckResult::VERSION_CURRENT:
errorMsg = "Current version is up to date";
// This is not actually an error
ESP_LOGI(TAG, "No update needed: %s", errorMsg.c_str());
updateProgress(AppUpdater::UpdateStatus::MESSAGE, 0, errorMsg.c_str());
// Don't throw, just exit gracefully
break;
default:
errorMsg = "Unknown manifest check error";
}
if (manifestResult != AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
ESP_LOGE(TAG, "Manifest check failed: %s", errorMsg.c_str());
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, errorMsg.c_str());
}
}
if (updater->IsUpdateAvailable()) { if (updater->IsUpdateAvailable()) {
ESP_LOGI(TAG, "Update available, updating files..."); bool filesUpdated = true;
bool firmwareUpdated = true;
if (!updater->updateFilesArray()) { // Update files based on update mode
throw std::runtime_error("Failed to update files"); if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
ESP_LOGI(TAG, "Update mode includes files, updating files...");
filesUpdated = updater->updateFilesArray();
if (!filesUpdated) {
ESP_LOGW(TAG, "Some files failed to update");
if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY) {
ESP_LOGE(TAG, "Files-only update failed");
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update files");
// Skip to cleanup since this is files-only mode and it failed
goto cleanup;
} else {
ESP_LOGW(TAG, "File update failed, but continuing with firmware update");
}
}
} else {
ESP_LOGI(TAG, "Skipping file updates (mode: firmware only)");
} }
ESP_LOGI(TAG, "Updating firmware..."); // Update firmware based on update mode
if (!updater->updateApp()) { if (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
throw std::runtime_error("Failed to update firmware"); ESP_LOGI(TAG, "Update mode includes firmware, updating firmware...");
firmwareUpdated = updater->updateApp();
if (!firmwareUpdated) {
ESP_LOGE(TAG, "Failed to update firmware");
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update firmware");
// Skip to cleanup since firmware update failed
goto cleanup;
}
} else {
ESP_LOGI(TAG, "Skipping firmware update (mode: files only)");
} }
ESP_LOGI(TAG, "Update successful, restarting...");
// Determine if we need to restart
bool needsRestart = (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) && firmwareUpdated;
if (needsRestart) {
ESP_LOGI(TAG, "Firmware update successful, restarting...");
sendUpdateMessage("Restarting ", true, 100); sendUpdateMessage("Restarting ", true, 100);
vTaskDelay(2000); vTaskDelay(2000);
ESP.restart(); ESP.restart();
} else {
ESP_LOGI(TAG, "Update completed successfully (no restart required)");
updateProgress(AppUpdater::UpdateStatus::COMPLETE, 100, "Update completed successfully");
}
} }
cleanup:
} catch (const std::exception& e) { } catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what()); ESP_LOGE(TAG, "Update failed with exception: %s", e.what());
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, e.what());
} catch (...) {
ESP_LOGE(TAG, "Update failed with unknown exception");
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Unknown error during update");
} }
delete updater;
// Clean up watchdog before exit
esp_task_wdt_delete(NULL);
Update_Task_Handle = NULL; Update_Task_Handle = NULL;
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@ -520,7 +992,8 @@ void startVersionCheckTask() {
ESP_LOGW(TAG, "Version Check Tak already running"); ESP_LOGW(TAG, "Version Check Tak already running");
return; return;
} }
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle); // Create task with higher priority (3) and optimized stack size
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*6, NULL, 3, &versionCheckTask_Handle);
} }
void versionCheckTask(void* parameter){ void versionCheckTask(void* parameter){
@ -528,12 +1001,17 @@ void versionCheckTask(void* parameter){
loadUpdateJson(); loadUpdateJson();
} }
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin"); AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
if(!updater.checkManifest()){
ESP_LOGE(TAG, "Version check: manifest fetch failed"); auto manifestResult = updater.checkManifest();
} else {
if (manifestResult == AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE ||
manifestResult == AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
otaVersion = updater.otaVersion; // capture remote otaVersion = updater.otaVersion; // capture remote
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str()); ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
} else {
ESP_LOGE(TAG, "Version check: manifest check failed with code %d", static_cast<int>(manifestResult));
} }
versionCheckTask_Handle = NULL; versionCheckTask_Handle = NULL;
vTaskDelete(NULL); vTaskDelete(NULL);
} }
@ -593,7 +1071,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
msg = buffer; msg = buffer;
break; break;
case AppUpdater::UpdateStatus::FILE_SKIPPED: case AppUpdater::UpdateStatus::FILE_SKIPPED:
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg); snprintf(buffer, sizeof(buffer), "%s: File Skipped, up to date", safeMsg);
msg = buffer; msg = buffer;
break; break;
case AppUpdater::UpdateStatus::FILE_SAVED: case AppUpdater::UpdateStatus::FILE_SAVED:
@ -624,7 +1102,9 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
} }
void sendUpdateMessage(const char* message, bool complete, int progress = -1) { void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
if(eventProgress && eventProgress->count() > 0) { if(eventProgress && eventProgress->count() > 0) {
// This is for the web client and not the BLE client
JsonDocument jsonDoc; JsonDocument jsonDoc;
jsonDoc["message"] = message; jsonDoc["message"] = message;
jsonDoc["complete"] = complete; jsonDoc["complete"] = complete;
@ -640,41 +1120,27 @@ void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
bleUpgrade_send_message(message); bleUpgrade_send_message(message);
} }
// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead) // Convenience functions for setting update mode
void setGlobalUpdateMode(UpdateMode mode) {
/* g_UpdateMode = mode;
void setup() { ESP_LOGI(TAG, "Global update mode set to: %s",
Serial.begin(115200); mode == UpdateMode::UPDATE_FILES_ONLY ? "UPDATE_FILES_ONLY" :
mode == UpdateMode::UPDATE_FIRMWARE_ONLY ? "UPDATE_FIRMWARE_ONLY" : "UPDATE_BOTH");
// Initialize WiFi connection first
// ... WiFi connection code ...
// Initialize filesystem
if(!LittleFS.begin()) {
Serial.println("LittleFS Mount Failed");
return;
} }
// Create updater instance with: UpdateMode getGlobalUpdateMode() {
// - Current version: "1.0.0" return g_UpdateMode;
// - 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 void setUpdateModeFilesOnly() {
int updatedFiles = updater.updateFilesFromManifest("test_update.json"); setGlobalUpdateMode(UpdateMode::UPDATE_FILES_ONLY);
Serial.printf("Updated %d files\n", updatedFiles); }
void setUpdateModeFirmwareOnly() {
setGlobalUpdateMode(UpdateMode::UPDATE_FIRMWARE_ONLY);
}
void setUpdateModeBoth() {
setGlobalUpdateMode(UpdateMode::UPDATE_BOTH);
} }
*/

View File

@ -4,6 +4,7 @@
#include "WiFi.h" #include "WiFi.h"
#include "ATALights.h" #include "ATALights.h"
#include "BleSettings.h" #include "BleSettings.h"
#include <esp_task_wdt.h>
static const char *tag = "BLE_SP110E"; static const char *tag = "BLE_SP110E";
@ -134,12 +135,23 @@ class LightStickCallbacks : public NimBLECharacteristicCallbacks {
// Function to send data to all connected clients in chunks based on MTU // Function to send data to all connected clients in chunks based on MTU
void sendToAllClients(const uint8_t* data, size_t len) { void sendToAllClients(const uint8_t* data, size_t len) {
if (!pStickCharacteristic || !data || len == 0) return; if (!pStickCharacteristic) {
ESP_LOGW(tag, "Cannot send to clients: pStickCharacteristic is null");
return;
}
if (!data || len == 0) {
ESP_LOGW(tag, "Cannot send to clients: data is null or length is 0");
return;
}
// Skip if no subscribed clients (if API available) // Skip if no subscribed clients (if API available)
#if defined(NIMBLE_INCLUDED) || true #if defined(NIMBLE_INCLUDED) || true
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL #ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
if (pStickCharacteristic->getSubscribedCount() == 0) return; if (pStickCharacteristic->getSubscribedCount() == 0) {
ESP_LOGD(tag, "No clients subscribed, skipping notification");
return;
}
#endif #endif
#endif #endif
@ -152,9 +164,19 @@ void sendToAllClients(const uint8_t* data, size_t len) {
while (offset < len) { while (offset < len) {
size_t chunk = len - offset; size_t chunk = len - offset;
if (chunk > maxChunk) chunk = maxChunk; if (chunk > maxChunk) chunk = maxChunk;
try {
pStickCharacteristic->setValue(data + offset, chunk); pStickCharacteristic->setValue(data + offset, chunk);
// notify() returns void in this NimBLE version, so just call it without checking a return value // notify() returns void in this NimBLE version, but wrap in try/catch for robustness
pStickCharacteristic->notify(); pStickCharacteristic->notify();
ESP_LOGD(tag, "Sent %zu bytes to clients", chunk);
} catch (const std::exception& e) {
ESP_LOGE(tag, "Exception during notification: %s", e.what());
// Consider adding a delay or recovery mechanism here
break;
}
offset += chunk; offset += chunk;
} }
} }
@ -172,8 +194,16 @@ void sendToAllClients(const uint8_t *data, size_t len) {
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) {
if (!val) {
ESP_LOGE(tag, "Null command data received");
return;
}
if (len < 4) {
ESP_LOGW(tag, "Command too short: %d bytes, expected at least 4", len);
return;
}
if (len >= 4) {
uint8_t command = val[3]; uint8_t command = val[3];
ESP_LOGI(tag, "Command received: 0x%02X", command); ESP_LOGI(tag, "Command received: 0x%02X", command);
@ -262,10 +292,15 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
break; break;
} }
} }
}
void Init_BLE_SP110E(NimBLEServer* pServer) { void Init_BLE_SP110E(NimBLEServer* pServer) {
if (!pServer) {
ESP_LOGE(tag, "Invalid BLE server pointer");
return;
}
led_status.speed = 10; led_status.speed = 10;
led_status.bright = 50; led_status.bright = 50;
led_status.ic_model = 0; led_status.ic_model = 0;
@ -325,7 +360,14 @@ void Init_BLE_LightStick_Client(){
ESP_LOGW(tag, "Light Stick Client Task already running"); ESP_LOGW(tag, "Light Stick Client Task already running");
return; return;
} }
xTaskCreate(BLE_LightStick_Client_Task, "VersionCheckTask", 1024*8, NULL, 1, &LightStick_Client_Task_Handle);
BaseType_t result = xTaskCreate(BLE_LightStick_Client_Task, "LightStickTask", 1024*6, NULL, 1, &LightStick_Client_Task_Handle);
if (result != pdPASS) {
ESP_LOGE(tag, "Failed to create Light Stick client task, error: %d", result);
LightStick_Client_Task_Handle = NULL;
} else {
ESP_LOGI(tag, "Light Stick client task created successfully");
}
} }
// Task for the BLE LightStick client // Task for the BLE LightStick client
@ -334,7 +376,13 @@ void BLE_LightStick_Client_Task(void *parameter) {
static const char *tag = "BLE_LightStick_Client_Task"; static const char *tag = "BLE_LightStick_Client_Task";
ESP_LOGI(tag, "BLE LightStick Client Task started"); ESP_LOGI(tag, "BLE LightStick Client Task started");
// Register task with watchdog
esp_task_wdt_add(NULL);
while (true) { while (true) {
// Reset watchdog timer
esp_task_wdt_reset();
// Only try to connect if we're not already connected and a device is set. // Only try to connect if we're not already connected and a device is set.
if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) { if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) {
// Create a new client instance if needed. // Create a new client instance if needed.
@ -371,11 +419,20 @@ void BLE_LightStick_Client_Task(void *parameter) {
ESP_LOGE(tag, "Failed to connect to the server"); ESP_LOGE(tag, "Failed to connect to the server");
// Delete the client instance so that a new one is created next time. // Delete the client instance so that a new one is created next time.
if (pStickClient != nullptr) { if (pStickClient != nullptr) {
try {
NimBLEDevice::deleteClient(pStickClient); NimBLEDevice::deleteClient(pStickClient);
pStickClient = nullptr; pStickClient = nullptr;
} catch (const std::exception& e) {
ESP_LOGE(tag, "Exception deleting client: %s", e.what());
} }
// Wait before retrying. }
vTaskDelay(pdMS_TO_TICKS(5000)); // Implement exponential backoff for connection retries
static uint16_t retryDelay = 2000; // Start with 2 seconds
retryDelay = (retryDelay * 3) / 2; // Increase by 50% each time
if (retryDelay > 30000) retryDelay = 30000; // Cap at 30 seconds
ESP_LOGI(tag, "Will retry in %d ms", retryDelay);
vTaskDelay(pdMS_TO_TICKS(retryDelay));
continue; continue;
} }
} }
@ -390,6 +447,12 @@ void BLE_LightStick_Client_Task(void *parameter) {
vTaskDelay(pdMS_TO_TICKS(5000)); vTaskDelay(pdMS_TO_TICKS(5000));
} }
} }
// Task should never exit, but in case it does:
ESP_LOGI(tag, "BLE LightStick Client Task ending");
esp_task_wdt_delete(NULL);
LightStick_Client_Task_Handle = NULL;
vTaskDelete(NULL);
} }

View File

@ -80,6 +80,17 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
} }
else if (value.compare("upgrade-start") == 0) { // Start OTA update else if (value.compare("upgrade-start") == 0) { // Start OTA update
ESP_LOGI(tag, "Start OTA update command received"); ESP_LOGI(tag, "Start OTA update command received");
setUpdateModeBoth();
startFirmwareUpdateTask(nullptr); // start the task
}
else if (value.compare("upgrade-start-files-only") == 0) { // Start OTA update
ESP_LOGI(tag, "Start OTA update files-only command received");
setUpdateModeFilesOnly();
startFirmwareUpdateTask(nullptr); // start the task
}
else if (value.compare("upgrade-start-firmware-only") == 0) { // Start OTA update
ESP_LOGI(tag, "Start OTA update firmware-only command received");
setUpdateModeFirmwareOnly();
startFirmwareUpdateTask(nullptr); // start the task startFirmwareUpdateTask(nullptr); // start the task
} }
else if (value.compare("rename-device") == 0) { // Start renaming device else if (value.compare("rename-device") == 0) { // Start renaming device
@ -132,14 +143,66 @@ void bleUpgrade_send_message(String s){
if (s.length() == 0) { if (s.length() == 0) {
return; return;
} }
// Set value and notify only if there are subscribers to avoid unnecessary work
pUpgradeCharacteristic2->setValue(s.c_str()); // Log message details before sending
ESP_LOGI(tag, "Sending BLE message, length=%d bytes", s.length());
if (s.length() < 100) {
ESP_LOGI(tag, "Message content: '%s'", s.c_str());
}
// For testing - ensure string is null-terminated properly
String paddedString = s;
// Explicitly set using raw bytes with explicit length
const char* raw = paddedString.c_str();
size_t rawLen = paddedString.length();
// Explicitly handle null-termination ourselves
std::string stdStr(raw, rawLen);
pUpgradeCharacteristic2->setValue(stdStr);
// Log the value that was actually set
std::string valueAfterSet = pUpgradeCharacteristic2->getValue();
ESP_LOGI(tag, "Value after set: length=%d bytes", valueAfterSet.length());
if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
pUpgradeCharacteristic2->notify();
ESP_LOGI(tag, "Notification sent");
} else {
ESP_LOGW(tag, "No subscribers for notification");
}
}
}
/*
void bleUpgrade_send_message(String s) {
if(pUpgradeCharacteristic2) {
if (s.length() == 0) {
return;
}
// OPTION 1: Sanitize non-printable characters
String sanitized = "";
for (size_t i = 0; i < s.length(); i++) {
char c = s[i];
// Only keep printable ASCII characters and common whitespace
if ((c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t') {
sanitized += c;
} else {
// Replace non-printable with hexadecimal representation or skip
sanitized += String("[0x") + String(c, HEX) + "]";
// OR just: continue; // to skip unprintable chars
}
}
// Set value and notify only if there are subscribers
pUpgradeCharacteristic2->setValue(sanitized.c_str());
if (pUpgradeCharacteristic2->getSubscribedCount() > 0) { if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
pUpgradeCharacteristic2->notify(); pUpgradeCharacteristic2->notify();
} }
} }
} }
*/
void Init_UpgradeBLEService(NimBLEServer *pServer){ void Init_UpgradeBLEService(NimBLEServer *pServer){
@ -170,6 +233,5 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
} }

View File

@ -3,6 +3,7 @@
#include "BLE_SP110E.h" #include "BLE_SP110E.h"
#include "BLE_UpdateService.h" #include "BLE_UpdateService.h"
#include "BleSettings.h" #include "BleSettings.h"
#include "my_buzzer.h"
static const char* tag = "BleServer"; static const char* tag = "BleServer";
@ -39,11 +40,13 @@ public:
void onConnect(NimBLEServer* /*pServer*/) override { void onConnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client connected"); ESP_LOGI(tag, "Client connected");
ensureAdvertising("onConnect"); ensureAdvertising("onConnect");
Buzzer_Play_Tune(TUNE_CONNECTED);
} }
void onDisconnect(NimBLEServer* /*pServer*/) override { void onDisconnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client disconnected"); ESP_LOGI(tag, "Client disconnected");
ensureAdvertising("onDisconnect"); ensureAdvertising("onDisconnect");
Buzzer_Play_Tune(TUNE_DISCONNECTED, 1);
} }
private: private:

View File

@ -89,26 +89,55 @@ const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, con
String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def) { String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def) {
// Check if the key exists and is not null // Check if the key exists using the recommended approach
if (!jsonObject[key].is<String>()) { if (!jsonObject[key] || jsonObject[key].isNull()) {
ESP_LOGW(tag, "Key [%s] not found or null. Using default value [%s].", key, def.c_str()); ESP_LOGW(tag, "Key [%s] not found/null", key);
return def; return def;
} }
// Extract the value as a String // Handle different types to avoid unnecessary String conversions
String value = jsonObject[key].as<String>(); if (jsonObject[key].is<const char*>()) {
const char* charValue = jsonObject[key].as<const char*>();
// Check if the value is empty if (!charValue || *charValue == '\0') {
if (value.length() == 0) { ESP_LOGW(tag, "Key [%s] empty", key);
ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str());
return def; return def;
} }
ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str()); // Create String object for return value (only once)
String value(charValue);
// Process string only if needed
const size_t MAX_STRING_LENGTH = 1024;
if (value.length() > MAX_STRING_LENGTH) {
value = value.substring(0, MAX_STRING_LENGTH);
}
// Minimal logging
ESP_LOGD(tag, "Key [%s] value set", key);
return value; return value;
} }
// For non-char types, use standard String conversion
String value = jsonObject[key].as<String>();
// Check for empty string
if (value.length() == 0) {
ESP_LOGW(tag, "Key [%s] empty", key);
return def;
}
// Apply length constraint
const size_t MAX_STRING_LENGTH = 1024;
if (value.length() > MAX_STRING_LENGTH) {
value = value.substring(0, MAX_STRING_LENGTH);
}
// Minimal logging to reduce memory usage
ESP_LOGD(tag, "Key [%s] value set", key);
return value;
}
/*
bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) { bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
// Check if the key exists and is of type boolean // Check if the key exists and is of type boolean
if (!jsonObject[key].is<bool>()) { if (!jsonObject[key].is<bool>()) {
@ -121,6 +150,50 @@ bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char
ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false"); ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
return value; return value;
} }
*/
bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
// Check if the key exists using the recommended approach
if (!jsonObject[key] || jsonObject[key].isNull()) {
ESP_LOGW(tag, "Key [%s] not found or null. Using default [%s]", key, def ? "true" : "false");
return def;
}
// Direct boolean check first (fastest path, no conversion)
if (jsonObject[key].is<bool>()) {
bool value = jsonObject[key].as<bool>();
ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
return value;
}
// Numeric conversion (no heap allocation)
if (jsonObject[key].is<int>() || jsonObject[key].is<float>()) {
bool value = (jsonObject[key].as<int>() != 0);
ESP_LOGD(tag, "Key [%s] numeric converted to: %s", key, value ? "true" : "false");
return value;
}
// String conversion - minimal processing
if (jsonObject[key].is<const char*>()) {
const char* str = jsonObject[key].as<const char*>();
// Fast direct comparison with common values (no heap allocation)
if (strcmp(str, "true") == 0 || strcmp(str, "1") == 0 ||
strcmp(str, "yes") == 0 || strcmp(str, "on") == 0) {
return true;
}
if (strcmp(str, "false") == 0 || strcmp(str, "0") == 0 ||
strcmp(str, "no") == 0 || strcmp(str, "off") == 0) {
return false;
}
ESP_LOGW(tag, "Key [%s] string couldn't convert to bool, using default [%s]",
key, def ? "true" : "false");
} else {
ESP_LOGW(tag, "Key [%s] type not supported for bool conversion", key);
}
return def;
}
// Explicit instantiations // Explicit instantiations

View File

@ -144,7 +144,8 @@ void setup()
Init_Board_Basic(sys_settings.boardPins); Init_Board_Basic(sys_settings.boardPins);
// Load tunes.json and initialize // Load tunes.json and initialize
Init_Buzzer(sys_settings.boardPins.buzzer, "/system/tunes.json"); // Reserve channel 7 for buzzer (highest channel number to avoid conflicts)
Init_Buzzer(sys_settings.boardPins.buzzer, "/system/tunes.json", 7);
// Initialize PWM Outputs // Initialize PWM Outputs
Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings); Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings);
@ -171,10 +172,13 @@ void setup()
{ {
setStatusPin1(true); setStatusPin1(true);
UpgradeMode = true; UpgradeMode = true;
ESP_LOGW(tag, "Upgrade Mode Triggered");
ESP_LOGW(tag, "Enabling BLE and Update Service"); ESP_LOGW(tag, "Enabling BLE and Update Service");
Init_BleServer(true, true); Init_BleServer(true, true);
ESP_LOGW(tag, "Enabling Wifi AP and Client"); ESP_LOGW(tag, "Enabling Wifi AP and Client");
Wifi_Init(); Wifi_Init();
//Buzzer_Play_Tune(TUNE_UPGRADE_MODE);
} }
else else
{ {
@ -197,7 +201,7 @@ void setup()
Init_Lights_Task(); Init_Lights_Task();
#endif #endif
Buzzer_Play_Tune(TUNE_BOOT, true, true); Buzzer_Play_Tune(TUNE_BOOT);
// TODO... Test if this is still necessary need to configure pin 0 for some reason // TODO... Test if this is still necessary need to configure pin 0 for some reason
// pinMode(0, INPUT); // button0/boot pin // pinMode(0, INPUT); // button0/boot pin
@ -225,29 +229,21 @@ void loop()
} }
// Temperature Monitor // Temperature Monitor
ON_EVERY_N_MILLISECONDS(5000) static OnEveryMsVariable temperatureMonitorTimer;
if (sys_settings.tSensorSettings.enabled)
{
if (temperatureMonitorTimer.ready(sys_settings.tSensorSettings.intervalMs))
{ {
static float boardTemperature; static float boardTemperature;
// Read temperature if the sensor is enabled
if (sys_settings.tSensorSettings.enabled)
{
boardTemperature = tSensor->readTemperatureF(); boardTemperature = tSensor->readTemperatureF();
// ESP_LOGI(tag, "Board T: %F", boardTemperature); // ESP_LOGI(tag, "Board T: %F", boardTemperature);
}
// Fan Control // Fan Control
if (sys_settings.tSensorSettings.enabled)
{
UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]); UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]);
} }
} }
// Update Tune Playing
//if (anyrtttl::nonblocking::isPlaying())
//{
// anyrtttl::nonblocking::play();
//}
// Animation TestMode Timeout // Animation TestMode Timeout
#if LEDS_ENABLED #if LEDS_ENABLED
@ -274,7 +270,7 @@ void loop()
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
#if BUZZER_ENABLED #if BUZZER_ENABLED
Buzzer_Play_Tune(TUNE_BEEP, false); // blocking Buzzer_Play_Tune(TUNE_LOWEEP); // blocking
#endif #endif
vTaskDelay(200); vTaskDelay(200);
} }
@ -296,11 +292,12 @@ void loop()
} }
} }
// Upgrade Mode Tune // Upgrade Mode Hearbeat tune
if(UpgradeMode){ if(UpgradeMode){
ON_EVERY_N_MILLISECONDS(5000) ON_EVERY_N_MILLISECONDS(5000)
{ {
Buzzer_Play_Tune(TUNE_ACK, true, true); Buzzer_Play_Tune(TUNE_LOWBEEP);
//ESP_LOGI(tag, "Upgrade Mode Heartbeat");
} }
} }
@ -479,9 +476,6 @@ void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath)
sys_settings.rampLightSettings[rampIndex].vision = jsonConstrainBool(tag, obj, "vision", true); sys_settings.rampLightSettings[rampIndex].vision = jsonConstrainBool(tag, obj, "vision", true);
sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain<int>(tag, obj, "relay-index", 0, 1, 0); sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain<int>(tag, obj, "relay-index", 0, 1, 0);
sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0); sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0);
sys_settings.rampLightSettings[rampIndex].min = jsonConstrain<float>(tag, obj, "min", 0.0, 100.0, 0.0);
sys_settings.rampLightSettings[rampIndex].max = jsonConstrain<float>(tag, obj, "max", 5.0, 100.0, 100.0);
sys_settings.rampLightSettings[rampIndex].step = jsonConstrain<float>(tag, obj, "step", 0.01, 100.0, 1.5);
rampIndex++; rampIndex++;
} }
ESP_LOGI(tag, "Loaded Ramp Lights settings..."); ESP_LOGI(tag, "Loaded Ramp Lights settings...");

View File

@ -1,6 +1,8 @@
#include "my_buttons.h" #include "my_buttons.h"
#include "global.h" #include "global.h"
#include "BLE_UpdateService.h" #include "BLE_UpdateService.h"
#include "esp_log.h"
#include "AppUpgrade.h"
static const char* tag = "button"; static const char* tag = "button";
OneButton *boardButtons[3]; OneButton *boardButtons[3];
@ -111,6 +113,7 @@ void btn2_click() {
//Pulse_LED_Status(150); //Pulse_LED_Status(150);
//Buzzer_Beep(150); //Buzzer_Beep(150);
// send packet // send packet
sendUpdateMessage("testing....", false, -1);
ESP_LOGD(tag, "btn2 1x"); ESP_LOGD(tag, "btn2 1x");
} }

View File

@ -7,102 +7,93 @@
#include <pitches.h> #include <pitches.h>
#include "JsonConstrain.h" #include "JsonConstrain.h"
#include "global.h"
#include "RtttlPlayer.h"
#include "esp_log.h"
const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g"; const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g";
// serial debugging enabled
//#define ANY_RTTTL_INFO
static const char* tag = "buzzer"; static const char* tag = "buzzer";
// Define static constexpr member from RtttlPlayer class
constexpr uint16_t RtttlPlayer::LUT4[12];
RtttlPlayer *player;
BUZZ_TUNE buzzTune[TUNE_MAX_COUNT]; BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
int8_t buzzPin; int8_t buzzPin;
int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
void Init_Buzzer(int8_t pin, const char* configFile, int8_t channel)
void Init_Buzzer(int8_t pin, const char* configFile)
{ {
buzzPin = pin; buzzPin = pin;
if(buzzPin >= 0){ if(buzzPin >= 0){
pinMode(buzzPin, OUTPUT);
player = new RtttlPlayer(pin, channel);
buzzerChannel = channel;
ESP_LOGI(tag, "Buzzer hardware initialized on pin %d using LEDC channel %d", buzzPin, buzzerChannel);
} }
Buzzer_Load_Tunes(configFile); // Load Tunes Buzzer_Load_Tunes(configFile); // Load Tunes
ESP_LOGI(tag, "Buzzer initialized.."); ESP_LOGI(tag, "Buzzer initialized on pin %d, channel %d", buzzPin, buzzerChannel);
} }
void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority) void Buzzer_Play_Tune(TUNE_TYPE tune, int priority)
{ {
static int prev_tune = -1; if(buzzPin < 0 || !player) return;
if (buzzPin < 0) return;
// Range / data validation
if(tune < 0 || tune >= TUNE_MAX_COUNT){ if(tune < 0 || tune >= TUNE_MAX_COUNT){
ESP_LOGW(tag, "Invalid tune index: %d", (int)tune); ESP_LOGW(tag, "Invalid tune index %d", tune);
return;
}
const String &melody = buzzTune[tune].melody;
if (melody.isEmpty()) {
ESP_LOGW(tag, "Empty melody for tune %d", (int)tune);
return; return;
} }
// Async mode: begin once, then caller should periodically call again to advance playback int cycles = buzzTune[tune].cycles;
if (async) { int pause = buzzTune[tune].pause;
bool playing = anyrtttl::nonblocking::isPlaying(); String melody = buzzTune[tune].melody;
if (hasPriority && playing) {
anyrtttl::nonblocking::stop(); if(melody.length() == 0){
playing = false; ESP_LOGW(tag, "Tune %d has empty melody, skipping playback", tune);
}
if (!playing || prev_tune != tune) {
// (Re)start tune
anyrtttl::nonblocking::begin(buzzPin, melody.c_str());
prev_tune = tune;
ESP_LOGD(tag, "Started async tune %d (%s)", (int)tune, melody.c_str());
}
// Advance playback one tick
anyrtttl::nonblocking::play();
return; return;
} }
// Blocking mode: play full tune cycles with optional pause // Play the tune the specified number of cycles
ESP_LOGD(tag, "Playing blocking tune %d cycles=%d pause=%d", (int)tune, buzzTune[tune].cycles, buzzTune[tune].pause); for(int i = 0; i < cycles; i++){
for (int c = 0; c < buzzTune[tune].cycles; ++c) { bool played = player->play(melody.c_str(), priority ? 2 : 1); // Use priority level
anyrtttl::blocking::play(buzzPin, melody.c_str()); if(!played){
if (buzzTune[tune].pause > 0 && c + 1 < buzzTune[tune].cycles) { ESP_LOGW(tag, "Failed to play tune %d (cycle %d)", tune, i+1);
delay(buzzTune[tune].pause); // simple pause between cycles return;
}
yield(); // allow other tasks to run
}
prev_tune = tune;
} }
// TODO Buzzer Beep finish if(pause > 0 && i < cycles - 1){
void Buzzer_Beep(int mSecs, int freq) delay(pause);
{
/*
ledcAttachPin(buzzPin, buzzerCh);
ledcSetup(buzzerCh, 2000, 8);
ledcWrite(buzzerCh, 125);
vTaskDelay(mSecs);
ledcWrite(buzzerCh, 0);
*/
} }
}
// TODO Reduce tunes to load () }
// Optimized tune loading - minimal memory allocation
void Buzzer_Load_Tunes(const char* tunesPath){ void Buzzer_Load_Tunes(const char* tunesPath){
ESP_LOGI(tag, "Loading tunes from: %s", tunesPath);
File file = LittleFS.open(tunesPath); File file = LittleFS.open(tunesPath);
if (!file) { if (!file) {
ESP_LOGE(tag, "Error opening %s...", tunesPath); ESP_LOGW(tag, "Could not open %s, using default tune", tunesPath);
// Set default tune only at index 0
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune at index 0: %s", DEFAULT_MELODY);
return; return;
} }
// Use smaller JSON document for memory efficiency
JsonDocument doc; JsonDocument doc;
DeserializationError error = deserializeJson(doc, file); DeserializationError error = deserializeJson(doc, file);
file.close(); file.close();
if(error){ if(error){
ESP_LOGE(tag, "%s deserialize error!..", tunesPath); ESP_LOGE(tag, "JSON parse error: %s", error.c_str());
// Set default tune on error
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune due to JSON error: %s", DEFAULT_MELODY);
return; return;
} }
@ -114,11 +105,18 @@ void Buzzer_Load_Tunes(const char* tunesPath){
buzzTune[tuneIndex].cycles = jsonConstrain<int>(tag, obj, "cycles", 1, 100, 1); buzzTune[tuneIndex].cycles = jsonConstrain<int>(tag, obj, "cycles", 1, 100, 1);
buzzTune[tuneIndex].pause = jsonConstrain<int>(tag, obj, "pause", 0, 100, 0); buzzTune[tuneIndex].pause = jsonConstrain<int>(tag, obj, "pause", 0, 100, 0);
buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY); buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
ESP_LOGD(tag, "tune %d : %s", tuneIndex, buzzTune[tuneIndex].melody.c_str()); ESP_LOGI(tag, "Loaded tune %d: cycles=%d, pause=%d, melody=%.40s...",
tuneIndex, buzzTune[tuneIndex].cycles, buzzTune[tuneIndex].pause,
buzzTune[tuneIndex].melody.c_str());
tuneIndex++; tuneIndex++;
} }
ESP_LOGI(tag, "Loaded tunes..."); ESP_LOGI(tag, "Successfully loaded %d tunes", tuneIndex);
} else { } else {
ESP_LOGE(tag, "Error!, %s key: tunes not found..", tunesPath); ESP_LOGW(tag, "No 'tunes' array found in JSON");
// Set default tune if no tunes array found
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune: %s", DEFAULT_MELODY);
} }
} }

View File

@ -164,6 +164,13 @@ bool StartWifiConnectTask(String ssid = "", String pass = "")
return false; return false;
} }
// Create mutex if it doesn't exist
if (wifiMutex == nullptr) {
wifiMutex = xSemaphoreCreateMutex();
}
// Take mutex with timeout
if (xSemaphoreTake(wifiMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (!wifi_task_running) if (!wifi_task_running)
{ {
client_ssid = ssid; client_ssid = ssid;
@ -171,19 +178,23 @@ bool StartWifiConnectTask(String ssid = "", String pass = "")
if (Wifi_Task_Handle == NULL) if (Wifi_Task_Handle == NULL)
{ {
ESP_LOGI(tag, "Creating WiFi task"); ESP_LOGI(tag, "Creating WiFi task");
xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0); xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 6, NULL, 1, &Wifi_Task_Handle, 0);
xSemaphoreGive(wifiMutex);
return true;
} }
else else
{ {
ESP_LOGI(tag, "WiFi task already running"); ESP_LOGI(tag, "WiFi task already running");
} }
return true;
} }
else else
{ {
ESP_LOGE(tag, "Task already running"); ESP_LOGE(tag, "Task already running");
} }
xSemaphoreGive(wifiMutex);
} else {
ESP_LOGE(tag, "Failed to acquire mutex - WiFi operation in progress");
}
return false; return false;
} }
@ -193,6 +204,9 @@ void Wifi_ConnectTask(void *parameter)
static const char *tag = "Wifi_Task"; static const char *tag = "Wifi_Task";
wifi_task_running = true; wifi_task_running = true;
// Register task with watchdog to prevent system hangs
esp_task_wdt_add(NULL);
if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID()) if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
{ {
ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str()); ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str());
@ -206,6 +220,9 @@ void Wifi_ConnectTask(void *parameter)
uint8_t attempts = 0; uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS) while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS)
{ {
// Reset watchdog timer to prevent timeouts during connection attempts
esp_task_wdt_reset();
switch (WiFi.status()) switch (WiFi.status())
{ {
case WL_NO_SSID_AVAIL: case WL_NO_SSID_AVAIL:
@ -242,6 +259,9 @@ void Wifi_ConnectTask(void *parameter)
ESP_LOGI(tag, "Wifi Task ended"); ESP_LOGI(tag, "Wifi Task ended");
// Unregister from watchdog before deletion
esp_task_wdt_delete(NULL);
Wifi_Task_Handle = NULL; Wifi_Task_Handle = NULL;
wifi_task_running = false; wifi_task_running = false;
vTaskDelete(NULL); vTaskDelete(NULL);
@ -249,17 +269,25 @@ void Wifi_ConnectTask(void *parameter)
void Wifi_Check_Internet() void Wifi_Check_Internet()
{ {
// Check for internet connection // Check for internet connection with multiple fallback servers
const char *host = "8.8.8.8"; // Google DNS server const char *hosts[] = {"8.8.8.8", "1.1.1.1", "208.67.222.222"}; // Google DNS, Cloudflare DNS, OpenDNS
if (Ping.ping(host, 1)) const int num_hosts = sizeof(hosts) / sizeof(hosts[0]);
{
InternetAvailable = true;
ESP_LOGI(tag, "Internet connection verified");
}
else
{
InternetAvailable = false; InternetAvailable = false;
ESP_LOGW(tag, "No internet connection");
// Try pinging each host
for (int i = 0; i < num_hosts; i++) {
if (Ping.ping(hosts[i], 1)) {
InternetAvailable = true;
ESP_LOGI(tag, "Internet connection verified via %s", hosts[i]);
break;
}
// Small delay between ping attempts
vTaskDelay(pdMS_TO_TICKS(100));
}
if (!InternetAvailable) {
ESP_LOGW(tag, "No internet connection after trying multiple DNS servers");
} }
} }
@ -304,38 +332,122 @@ bool Wifi_Save_Credentials(String path)
return true; return true;
} }
/**
* Scans for available WiFi networks and stores the results in JSON format
*
* Updates scanStatus global: 0=none, 1=scanning, 2=complete, -1=error
* Sets scanInProgress flag during operation
* Populates networkList with JSON formatted scan results
*/
void Wifi_Scan_for_Networks() void Wifi_Scan_for_Networks()
{ {
// Start a scan for available networks static const char* tag = "WiFiScan";
WiFi.scanNetworks(false, false); const uint32_t SCAN_TIMEOUT_MS = 15000; // 15 second timeout for scan
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING)
{ // Protect against concurrent scans
vTaskDelay(100); // Wait for scan to complete if (scanInProgress) {
ESP_LOGW(tag, "WiFi scan already in progress");
return;
} }
// Use mutex for thread safety if available
bool useMutex = (wifiMutex != nullptr);
if (useMutex && xSemaphoreTake(wifiMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
ESP_LOGE(tag, "Failed to acquire mutex - WiFi operation in progress");
return;
}
scanInProgress = true;
scanStatus = 1; // Scanning
ESP_LOGI(tag, "Starting WiFi network scan");
// Start scan (async=false, show_hidden=false)
WiFi.scanNetworks(false, false);
// Wait for scan with timeout
uint32_t startTime = millis();
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING)
{
// Check for timeout
if (millis() - startTime > SCAN_TIMEOUT_MS) {
ESP_LOGE(tag, "WiFi scan timeout after %u ms", SCAN_TIMEOUT_MS);
scanInProgress = false;
scanStatus = -1; // Error
if (useMutex) xSemaphoreGive(wifiMutex);
return;
}
// Reset watchdog if needed
#ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0
esp_task_wdt_reset();
#endif
vTaskDelay(pdMS_TO_TICKS(100)); // Wait for scan to complete
}
// Get scan results
networkCount = WiFi.scanComplete(); networkCount = WiFi.scanComplete();
if (networkCount >= 0) if (networkCount >= 0)
{ {
ESP_LOGI(tag, "WiFi scan complete, found %d networks", networkCount);
scanStatus = 2; // Complete
// Create JSON document with appropriate capacity
JsonDocument doc; JsonDocument doc;
doc.clear();
JsonArray networks = doc["networks"].to<JsonArray>(); JsonArray networks = doc["networks"].to<JsonArray>();
for (int i = 0; i < networkCount; i++) for (int i = 0; i < networkCount; i++)
{ {
auto network = networks.add<JsonObject>(); auto network = networks.add<JsonObject>();
// Basic network info
network["ssid"] = WiFi.SSID(i); network["ssid"] = WiFi.SSID(i);
network["rssi"] = WiFi.RSSI(i); network["rssi"] = WiFi.RSSI(i);
network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN; network["channel"] = WiFi.channel(i);
// Security details
wifi_auth_mode_t encType = WiFi.encryptionType(i);
network["encryption"] = encType != WIFI_AUTH_OPEN;
// Add detailed encryption type
const char* encTypeStr = "unknown";
switch (encType) {
case WIFI_AUTH_OPEN: encTypeStr = "open"; break;
case WIFI_AUTH_WEP: encTypeStr = "WEP"; break;
case WIFI_AUTH_WPA_PSK: encTypeStr = "WPA_PSK"; break;
case WIFI_AUTH_WPA2_PSK: encTypeStr = "WPA2_PSK"; break;
case WIFI_AUTH_WPA_WPA2_PSK: encTypeStr = "WPA_WPA2_PSK"; break;
case WIFI_AUTH_WPA2_ENTERPRISE: encTypeStr = "WPA2_ENTERPRISE"; break;
case WIFI_AUTH_WPA3_PSK: encTypeStr = "WPA3_PSK"; break;
case WIFI_AUTH_WPA2_WPA3_PSK: encTypeStr = "WPA2_WPA3_PSK"; break;
case WIFI_AUTH_WAPI_PSK: encTypeStr = "WAPI_PSK"; break;
default: encTypeStr = "unknown"; break;
}
network["security"] = encTypeStr;
// Add signal quality 0-100%
int rssi = WiFi.RSSI(i);
int rssiLimited = rssi < -100 ? -100 : (rssi > -50 ? -50 : rssi);
int quality = ((rssiLimited + 100) * 2); // Convert -100..-50 to 0..100
network["quality"] = quality;
} }
String jsonString; // Serialize to the global variable
serializeJson(doc, jsonString); networkList.clear();
networkList = jsonString; serializeJson(doc, networkList);
// Clean up scan results from memory
WiFi.scanDelete(); WiFi.scanDelete();
} }
else else
{ {
ESP_LOGE(tag, "WiFi scan failed"); ESP_LOGE(tag, "WiFi scan failed with error code: %d", networkCount);
scanStatus = -1; // Error
} }
scanInProgress = false;
if (useMutex) xSemaphoreGive(wifiMutex);
} }
void Setup_WebServer_Handlers(AsyncWebServer &server) void Setup_WebServer_Handlers(AsyncWebServer &server)
@ -374,7 +486,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
String pass = request->getParam("pass", false, false)->value(); String pass = request->getParam("pass", false, false)->value();
// Validate credentials // Validate credentials
if (client_ssid.length() < 1 || client_pass.length() < 8) { if (ssid.length() < 1 || pass.length() < 8) {
ESP_LOGE(tag, "Invalid credentials"); ESP_LOGE(tag, "Invalid credentials");
request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}"); request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}");
return; return;
@ -584,11 +696,10 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
// If a dynamic URL was loaded, override base // If a dynamic URL was loaded, override base
extern String updateUrl; // declared in AppUpgrade.cpp extern String updateUrl; // declared in AppUpgrade.cpp
if(updateUrl.length()) updater.setBaseUrl(updateUrl); if(updateUrl.length()) updater.setBaseUrl(updateUrl);
if(!updater.checkManifest()){ // checkManifest() does not return a bool; capture its result (type-dependent) instead of using it in a boolean expression
ESP_LOGE(tag, "Manifest check failed via /upgrade/check"); auto manifestResult = updater.checkManifest();
} else { // TODO: inspect manifestResult for success/failure once its API is known
otaVersion = updater.otaVersion; otaVersion = updater.otaVersion;
}
bool avail = otaVersion > localVersion; bool avail = otaVersion > localVersion;
JsonDocument doc; JsonDocument doc;
@ -682,7 +793,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{ {
static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit static const size_t MAX_UPLOAD_SIZE = 1024 * 512; // 512KB limit
if (!index) if (!index)
{ {

View File

@ -0,0 +1,716 @@
#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>
#include <cstring>
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
volatile bool g_UpdateCancelFlag = false; // cancellation flag
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);
}
}
AppUpdater::ManifestCheckResult AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
String payload;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return ManifestCheckResult::ERROR_FETCH_FAILED;
HTTPClient http;
http.begin(url);
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();
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 ManifestCheckResult::ERROR_FETCH_FAILED;
}
if(payload.length() > MAX_MANIFEST_SIZE){
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
return ManifestCheckResult::ERROR_TOO_LARGE;
}
// 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 ManifestCheckResult::ERROR_PARSE_FAILED;
}
// Check for files section
jsonFilesArray = jsonManifest["files"];
if (jsonFilesArray.isNull()) {
ESP_LOGE(TAG, "No files section in manifest");
return ManifestCheckResult::ERROR_NO_FILES_SECTION;
}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 ManifestCheckResult::ERROR_NO_VERSION;
}
// 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: remote=%s, local=%s",
otaVersion.toString().c_str(), localVersion.toString().c_str());
return ManifestCheckResult::VERSION_CURRENT;
}else{
updateAvailable = true;
ESP_LOGI(TAG, "Update available: remote=%s, local=%s",
otaVersion.toString().c_str(), localVersion.toString().c_str());
}
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
return ManifestCheckResult::UPDATE_AVAILABLE;
}
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);
// Quick skip: if exists and size & MD5 match
bool skip = false;
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);
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
return true;
}
// Start the download
HTTPClient http;
int httpCode = -1;
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) {
ESP_LOGE(TAG, "Download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
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){
String errMsg = String(localPath) + " MD5 failed";
updateProgress( UpdateStatus::ERROR, 0, errMsg.c_str() );
}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) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
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 * 80) / contentLength , localPath);
}
yield();
}
} else {
// Unknown content length: read until stream ends
for (;;) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
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%
// For unknown size, send heartbeats every ~16KB
if((totalRead & 0x3FFF) == 0){
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
}
yield();
}
}
file.close();
md5.calculate();
String calculatedMd5 = md5.toString();
// Verify MD5 hash
updateProgress(UpdateStatus::VERIFYING, 90, localPath);
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
updateProgress(UpdateStatus::VERIFYING, 95, localPath);
// 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;
}
updateProgress(UpdateStatus::VERIFYING, 100, localPath);
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["path"];
const char* localPath = remotePath;
// If path begins with "data/" or "/data/" strip only the "data" portion, retaining the leading slash
if (localPath) {
if (strncmp(localPath, "data/", 5) == 0) {
localPath += 4; // points to '/'
} else if (strncmp(localPath, "/data/", 6) == 0) {
localPath += 5; // points to '/'
}
}
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;
int httpCode = -1;
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) {
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
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) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
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 (;;) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
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();
updateProgress(UpdateStatus::VERIFYING, 95, "firmware");
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, 100, "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(), "manifest.json", "firmware.bin");
updater->setProgressCallback(updateProgress);
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
// Check and perform updates
auto manifestResult = updater->checkManifest();
if (manifestResult != AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE) {
// Handle different error cases
std::string errorMsg;
switch (manifestResult) {
case AppUpdater::ManifestCheckResult::ERROR_FETCH_FAILED:
errorMsg = "Failed to fetch manifest";
break;
case AppUpdater::ManifestCheckResult::ERROR_TOO_LARGE:
errorMsg = "Manifest file too large";
break;
case AppUpdater::ManifestCheckResult::ERROR_PARSE_FAILED:
errorMsg = "Failed to parse manifest";
break;
case AppUpdater::ManifestCheckResult::ERROR_NO_FILES_SECTION:
errorMsg = "Manifest missing files section";
break;
case AppUpdater::ManifestCheckResult::ERROR_NO_VERSION:
errorMsg = "Manifest missing version section";
break;
case AppUpdater::ManifestCheckResult::VERSION_CURRENT:
errorMsg = "Current version is up to date";
// This is not actually an error
ESP_LOGI(TAG, "No update needed: %s", errorMsg.c_str());
throw std::runtime_error(errorMsg);
break;
default:
errorMsg = "Unknown manifest check error";
}
throw std::runtime_error(errorMsg);
}
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());
}
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();
}
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
auto manifestResult = updater.checkManifest();
if (manifestResult == AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE ||
manifestResult == AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
otaVersion = updater.otaVersion; // capture remote
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
} else {
ESP_LOGE(TAG, "Version check: manifest check failed with code %d", static_cast<int>(manifestResult));
}
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: File Skipped, 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) {
// This is for the web client and not the BLE client
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);
}
// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
/*
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);
}
*/

View File

@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 5px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 4px;
}
.status-indicator-ble {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.btn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
}
button {
flex: 1;
max-width: 130px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background 0.3s ease;
}
button:disabled {
background-color: #ccc;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
textarea {
width: 100%;
height: 300px;
font-size: 14px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px;
}
h1 {
font-size: 20px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
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>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Tab Buttons -->
<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>
<label id="status-ble-connection">BLE Status: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-wifi"></span>
<label id="status-wifi-client">Wifi Client: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-internet"></span>
<label id="status-internet">Internet: ...</label>
</div>
<div class="status-container">
<label id="status-current-version">Curr Version: ...</label>
</div>
<div class="status-container">
<label id="status-new-version">New Version: ...</label>
</div>
<div class="btn-container">
<label id="ble-device-name">Device Name:</label>
</div>
<div class="btn-container">
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
</div>
<!-- Buttons -->
<div class="btn-container">
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
</div>
<!-- Log Area -->
<textarea id="logArea" readonly></textarea>
<div class="btn-container">
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
</div>
</div> <!-- /tab-upgrade -->
<div id="tab-wifi" class="tab-panel">
<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="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
<div style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
<label for="showPassword">Show Password</label>
</div>
</div>
<div class="btn-container wifi">
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
</div>
</div><!-- /tab-wifi -->
<script>
(function(){
'use strict';
/* ================= Constants & Packet Layout ================= */
const BLE_SERVER_NAME = "ATALIGHTS"; // Keep hardcoded per instruction (ignore external JSON)
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0";
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Control / status
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Logs / events
// Packet layout (mirrors firmware struct updateStatus)
// byte 0 : wifiStatus (enum)
// byte 1 : wifiOnline (bool)
// bytes2-5: wifiIP
// bytes6-8: currVersion (major,minor,patch)
// bytes9-11: newVersion (major,minor,patch)
// bytes12-31: wifiSSID (20 bytes, null padded)
const PACKET_LEN = 32;
const OFF_WIFI_STATUS = 0;
const OFF_WIFI_ONLINE = 1;
const OFF_WIFI_IP = 2;
const OFF_CURR_VER = 6;
const OFF_NEW_VER = 9;
const OFF_WIFI_SSID = 12;
const WIFI_STAT = { DISCONNECTED:0, BAD_CREDS:1, NO_AP:2, CONNECTED:3 };
const WIFI_STAT_TEXT = ["Disconnected","Bad Creds","No AP","Connected"];
const MAX_LOG_LINES = 400;
/* ================= State ================= */
let bleDevice=null, bleCharacteristic1=null, bleCharacteristic2=null;
let bleConnected=false;
const state = {
wifiStatus: WIFI_STAT.DISCONNECTED,
wifiOnline:false,
wifiIP:[0,0,0,0],
currVersion:[0,0,0],
newVersion:[0,0,0],
wifiSSID:""
};
/* ================= Cached DOM ================= */
const el = {};
function cacheDom(){
el.bleIndicator = document.querySelector('.status-indicator-ble');
el.wifiIndicator = document.querySelector('.status-indicator-wifi');
el.internetIndicator = document.querySelector('.status-indicator-internet');
el.lblBle = document.getElementById('status-ble-connection');
el.lblWifi = document.getElementById('status-wifi-client');
el.lblInternet = document.getElementById('status-internet');
el.lblCurrVer = document.getElementById('status-current-version');
el.lblNewVer = document.getElementById('status-new-version');
el.inDeviceName = document.getElementById('input-DeviceName');
el.inSsid = document.getElementById('wifissid');
el.inPass = document.getElementById('wifipassword');
el.chkShowPass = document.getElementById('showPassword');
el.btnBleConnect = document.getElementById('bleConnectBtn');
el.btnCheckStatus = document.getElementById('checkStatusBtn');
el.btnCheckVersion = document.getElementById('checkVersionBtn');
el.btnStartUpgrade = document.getElementById('startUpgradeBtn');
el.btnWifiConnect = document.getElementById('wifiConnectBtn');
el.logArea = document.getElementById('logArea');
}
/* ================= Utilities ================= */
function logMessage(msg){
const lines = el.logArea.value.trim().length ? el.logArea.value.split(/\n/) : [];
lines.push(msg);
if(lines.length > MAX_LOG_LINES){
lines.splice(0, lines.length - MAX_LOG_LINES);
}
el.logArea.value = lines.join('\n') + '\n';
el.logArea.scrollTop = el.logArea.scrollHeight;
}
function compareVersions(a,b){
for(let i=0;i<3;i++){ if(a[i]>b[i]) return 1; if(a[i]<b[i]) return -1; }
return 0;
}
function colorIndicator(elm, color){ if(elm) elm.style.backgroundColor = color; }
function ipToString(ip){ return ip.join('.'); }
/* ================= Packet Handling ================= */
function parsePacket(data){
if(data.length !== PACKET_LEN) return false;
state.wifiStatus = data[OFF_WIFI_STATUS];
if(state.wifiStatus > WIFI_STAT.CONNECTED) state.wifiStatus = WIFI_STAT.DISCONNECTED; // clamp
state.wifiOnline = !!data[OFF_WIFI_ONLINE];
state.wifiIP = Array.from(data.slice(OFF_WIFI_IP, OFF_WIFI_IP+4));
state.currVersion = Array.from(data.slice(OFF_CURR_VER, OFF_CURR_VER+3));
state.newVersion = Array.from(data.slice(OFF_NEW_VER, OFF_NEW_VER+3));
// Extract SSID (stop at first 0)
let rawSsidBytes = data.slice(OFF_WIFI_SSID, OFF_WIFI_SSID+20);
let zeroIndex = rawSsidBytes.indexOf(0);
if(zeroIndex >= 0) rawSsidBytes = rawSsidBytes.slice(0, zeroIndex);
state.wifiSSID = rawSsidBytes.length ? String.fromCharCode(...rawSsidBytes) : "";
return true;
}
function updateUI(){
// BLE
el.lblBle.textContent = 'BLE Status: ' + (bleConnected ? 'Connected' : 'Disconnected');
colorIndicator(el.bleIndicator, bleConnected ? 'green' : 'gray');
// WiFi client
const statText = WIFI_STAT_TEXT[state.wifiStatus] || 'Unknown';
if(state.wifiStatus === WIFI_STAT.CONNECTED){
const ssidPart = state.wifiSSID ? ' SSID: '+state.wifiSSID : '';
el.lblWifi.textContent = 'Wifi Client: ' + statText + (state.wifiIP[0] ? ' ('+ipToString(state.wifiIP)+')':'' ) + ssidPart;
colorIndicator(el.wifiIndicator, 'green');
} else if(state.wifiStatus === WIFI_STAT.BAD_CREDS){
el.lblWifi.textContent = 'Wifi Client: Bad Credentials';
colorIndicator(el.wifiIndicator, 'orange');
} else if(state.wifiStatus === WIFI_STAT.NO_AP){
el.lblWifi.textContent = 'Wifi Client: AP Not Found';
colorIndicator(el.wifiIndicator, 'orange');
} else {
el.lblWifi.textContent = 'Wifi Client: ' + statText;
colorIndicator(el.wifiIndicator, 'gray');
}
// Internet
el.lblInternet.textContent = state.wifiOnline ? 'Online' : 'Offline';
colorIndicator(el.internetIndicator, state.wifiOnline ? 'green' : 'gray');
// Versions
el.lblCurrVer.textContent = state.currVersion[0] ? 'Curr Version: ' + state.currVersion.join('.') : 'Curr Version: ...';
el.lblNewVer.textContent = state.newVersion[0] ? 'New Version: ' + state.newVersion.join('.') : 'New Version: ...';
// Buttons
el.btnCheckStatus.disabled = !bleConnected;
el.btnWifiConnect.disabled = !bleConnected;
el.btnCheckVersion.disabled = !(bleConnected && state.wifiOnline);
const newerAvail = state.newVersion[0] && state.wifiOnline && compareVersions(state.newVersion, state.currVersion) > 0;
el.btnStartUpgrade.disabled = !newerAvail;
}
/* ================= BLE Operations ================= */
async function connectToBle(){
if(!navigator.bluetooth){ logMessage('Web Bluetooth not supported.'); return; }
try{
bleDevice = await navigator.bluetooth.requestDevice({
filters:[{ name: el.inDeviceName.value || BLE_SERVER_NAME }],
optionalServices:[BLE_SERVICE_UUID]
});
const server = await bleDevice.gatt.connect();
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
await bleCharacteristic2.startNotifications();
bleCharacteristic2.addEventListener('characteristicvaluechanged', e => {
try{
const view = e.target.value; // DataView
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
// Debug info
let debugInfo = `Received ${bytes.length} bytes: `;
for (let i = 0; i < bytes.length; i++) {
debugInfo += bytes[i].toString(16).padStart(2, '0') + ' ';
}
console.log(debugInfo);
// Try to decode as text
let txt = '';
try {
txt = new TextDecoder().decode(bytes);
// Remove null terminators and trim
const nullIdx = txt.indexOf('\0');
if (nullIdx !== -1) txt = txt.slice(0, nullIdx);
txt = txt.trim();
} catch (decodeErr) {
console.error('Text decode error:', decodeErr);
// Fallback to showing hex if text decode fails
txt = '[Binary data: ' + debugInfo + ']';
}
// Show both raw and processed data in console
console.log('--> Raw bytes:', bytes);
console.log('--> As text:', txt);
// Log message with length info for debugging
logMessage(`--> (${bytes.length} bytes) ${txt}`);
} catch(err) {
console.error('Processing error', err);
logMessage('--> Error processing message: ' + err.message);
}
});
bleConnected=true;
bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
logMessage('Connected to ' + connectedName);
const nameLabel = document.getElementById('ble-device-name');
if(nameLabel){ nameLabel.textContent = 'Device Name: ' + connectedName; }
await readPacket();
updateUI();
}catch(err){
logMessage( err.message.includes('cancel') ? 'Connection cancelled.' : ('Connection failed: '+err.message) );
}
}
function onDisconnect(){
bleConnected=false; updateUI(); logMessage('BLE disconnected');
}
async function sendPacket(msg){
if(!bleCharacteristic1) return;
const enc = new TextEncoder();
for(let attempt=0; attempt<3; attempt++){
try{ await bleCharacteristic1.writeValueWithResponse(enc.encode(msg)); return true; }
catch(e){ if(attempt===2) logMessage('Send failed: '+e.message); else await delay(1000); }
}
return false;
}
async function readPacket(){
if(!bleCharacteristic1) return false;
for(let attempt=0; attempt<3; attempt++){
try{
const val = await bleCharacteristic1.readValue();
const data = new Uint8Array(val.buffer);
if(parsePacket(data)) return true; else { logMessage('Packet parse failed (len='+data.length+')'); return false; }
}catch(e){ if(attempt===2) logMessage('Read failed: '+e.message); else await delay(1000); }
}
return false;
}
/* ================= Actions ================= */
async function wifiConnect(){
const ssid = el.inSsid.value.trim();
const pw = el.inPass.value;
if(!ssid || !pw){ alert('Enter SSID & password'); return; }
logMessage('Sending WiFi credentials...');
el.btnWifiConnect.disabled = true; // prevent multiple submissions while polling
await sendPacket('wifi-connect {"ssid":"'+ssid+'","pass":"'+pw+'"} ');
// Poll for status for up to 15s
const start = Date.now();
while(Date.now()-start < 15000){
await delay(1000);
await readPacket();
updateUI();
if(state.wifiStatus === WIFI_STAT.CONNECTED){
logMessage('WiFi Connected: '+ipToString(state.wifiIP));
break;
}
if(state.wifiStatus === WIFI_STAT.BAD_CREDS){ logMessage('WiFi Error: Bad Credentials'); break; }
if(state.wifiStatus === WIFI_STAT.NO_AP){ logMessage('WiFi Error: AP Not Found'); break; }
}
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ logMessage('WiFi connect attempt finished with status: '+WIFI_STAT_TEXT[state.wifiStatus]); }
if(!bleConnected) return; // keep disabled if BLE disconnected during process
// Re-enable for retry unless connected
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ el.btnWifiConnect.disabled = false; }
}
async function checkStatus(){ if(await readPacket()) updateUI(); }
async function checkVersion(){
el.btnCheckVersion.disabled = true;
logMessage('Checking for new version...');
await sendPacket('version-check');
const start = Date.now();
while(Date.now()-start < 15000){
await delay(750);
await readPacket();
if(state.newVersion[0]){ logMessage('Latest version: '+state.newVersion.join('.')); break; }
}
if(!state.newVersion[0]) logMessage('No new version info received');
updateUI();
}
async function startUpgrade(){
if(el.btnStartUpgrade.disabled) return;
logMessage('Starting upgrade...');
el.btnStartUpgrade.disabled = true;
await sendPacket('upgrade-start');
// Progress will arrive via characteristic2 logs
}
/* ================= Helpers ================= */
const delay = ms => new Promise(r=>setTimeout(r,ms));
function togglePasswordVisibility(){ el.inPass.type = el.chkShowPass.checked ? 'text' : 'password'; }
function init(){
cacheDom();
el.inDeviceName.value = BLE_SERVER_NAME;
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
// 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();
logMessage('Ready. Enter device name or use default and press Connect.');
}
// Expose functions for inline handlers
window.connectToBle = connectToBle;
window.checkStatus = checkStatus;
window.checkVersion = checkVersion;
window.startUpgrade = startUpgrade;
window.wifiConnect = wifiConnect;
window.togglePasswordVisibility = togglePasswordVisibility;
window.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>

249
temporary/my_buzzer.cpp Normal file
View File

@ -0,0 +1,249 @@
#include "my_buzzer.h"
#include <FS.h>
#include <LittleFS.h>
#include <anyrtttl.h>
#include <binrtttl.h>
#include <pitches.h>
#include "JsonConstrain.h"
#include "global.h"
const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g";
// serial debugging enabled
//#define ANY_RTTTL_INFO
static const char* tag = "buzzer";
BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
int8_t buzzPin;
int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
// File-scope state for tune management (minimal overhead)
static volatile int prev_tune = -1;
static volatile bool buzzer_busy = false;
// Optimized tone functions - minimal overhead, no logging
void buzzerTone(uint8_t pin, unsigned int frequency, unsigned long duration) {
if (buzzerChannel >= 0 && frequency > 0) {
ledcWriteTone(buzzerChannel, frequency);
}
}
void buzzerNoTone(uint8_t pin) {
if (buzzerChannel >= 0) {
ledcWrite(buzzerChannel, 0);
}
}
void Init_Buzzer(int8_t pin, const char* configFile, int8_t channel)
{
buzzPin = pin;
if(buzzPin >= 0){
pinMode(buzzPin, OUTPUT);
// If channel is not provided, find an unused one
if (channel < 0) {
buzzerChannel = findUnusedLedcChannel();
if (buzzerChannel < 0) {
ESP_LOGE(tag, "No available LEDC channel for buzzer");
return;
}
} else {
// Use the provided channel and mark it as used
extern bool markLedcChannelUsed(int ch); // Function from global.cpp
if (markLedcChannelUsed(channel)) {
buzzerChannel = channel;
} else {
ESP_LOGE(tag, "Requested channel %d is already in use, finding alternative", channel);
buzzerChannel = findUnusedLedcChannel();
if (buzzerChannel < 0) {
ESP_LOGE(tag, "No available LEDC channel for buzzer");
return;
}
}
}
// Set up the channel for the buzzer with proper audio frequency range
ledcSetup(buzzerChannel, 2000, 10); // 2000 Hz base, 10-bit resolution for better frequency range
ledcAttachPin(buzzPin, buzzerChannel);
// Test the channel is working
ESP_LOGI(tag, "Testing buzzer channel %d...", buzzerChannel);
ledcWriteTone(buzzerChannel, 1000); // Test tone
delay(100);
ledcWrite(buzzerChannel, 0); // Stop test tone
// Set custom tone functions for anyrtttl
anyrtttl::setToneFunction(buzzerTone);
anyrtttl::setNoToneFunction(buzzerNoTone);
ESP_LOGI(tag, "Buzzer hardware initialized on pin %d using LEDC channel %d", buzzPin, buzzerChannel);
}
Buzzer_Load_Tunes(configFile); // Load Tunes
ESP_LOGI(tag, "Buzzer initialized on pin %d, channel %d", buzzPin, buzzerChannel);
}
int8_t Buzzer_Get_Channel() {
return buzzerChannel;
}
void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority)
{
// Fast path checks - minimal overhead
if (buzzPin < 0 || buzzerChannel < 0) {
ESP_LOGW(tag, "Buzzer not initialized - pin:%d, channel:%d", buzzPin, buzzerChannel);
return;
}
if (tune < 0 || tune >= TUNE_MAX_COUNT) {
ESP_LOGW(tag, "Invalid tune index: %d (max: %d)", (int)tune, TUNE_MAX_COUNT);
return;
}
// Direct reference to avoid String copying
const String& melody = buzzTune[tune].melody;
if (melody.isEmpty()) {
ESP_LOGW(tag, "Empty melody for tune %d", (int)tune);
return;
}
ESP_LOGI(tag, "Playing tune %d: %s (async=%d, priority=%d)", (int)tune, melody.c_str(), async, hasPriority);
// Simple atomic check for thread safety without mutex overhead
if (buzzer_busy && !hasPriority) {
ESP_LOGD(tag, "Buzzer busy, skipping tune %d", (int)tune);
return;
}
// Async mode: minimal state management
if (async) {
bool playing = anyrtttl::nonblocking::isPlaying();
if (hasPriority && playing) {
ESP_LOGD(tag, "Stopping current tune for priority tune %d", (int)tune);
anyrtttl::nonblocking::stop();
playing = false;
}
if (!playing || prev_tune != tune) {
ESP_LOGI(tag, "Starting async tune %d", (int)tune);
anyrtttl::nonblocking::begin(buzzPin, melody.c_str());
prev_tune = tune;
}
anyrtttl::nonblocking::play();
return;
}
// Blocking mode: minimal cycles with yield for multitasking
ESP_LOGI(tag, "Playing blocking tune %d, cycles=%d", (int)tune, buzzTune[tune].cycles);
buzzer_busy = true;
const int cycles = buzzTune[tune].cycles;
const int pause_ms = buzzTune[tune].pause;
for (int c = 0; c < cycles; ++c) {
anyrtttl::blocking::play(buzzPin, melody.c_str());
if (pause_ms > 0 && c + 1 < cycles) {
delay(pause_ms);
}
yield(); // Allow other tasks to run
}
prev_tune = tune;
buzzer_busy = false;
ESP_LOGI(tag, "Finished playing tune %d", (int)tune);
}
// Optimized beep function - minimal overhead
void Buzzer_Beep(int mSecs, int freq)
{
if (buzzPin < 0 || buzzerChannel < 0) return;
ledcWriteTone(buzzerChannel, freq);
delay(mSecs);
ledcWrite(buzzerChannel, 0);
}
// Test function to verify buzzer functionality
void Buzzer_Test() {
if (buzzPin < 0 || buzzerChannel < 0) {
ESP_LOGE(tag, "Cannot test buzzer - not initialized");
return;
}
ESP_LOGI(tag, "Testing buzzer...");
// Test direct LEDC control
ESP_LOGI(tag, "Test 1: Direct LEDC tones");
for (int freq = 500; freq <= 2000; freq += 500) {
ESP_LOGI(tag, "Playing %d Hz", freq);
ledcWriteTone(buzzerChannel, freq);
delay(200);
ledcWrite(buzzerChannel, 0);
delay(100);
}
// Test custom tone functions
ESP_LOGI(tag, "Test 2: Custom tone functions");
buzzerTone(buzzPin, 1000, 500);
delay(500);
buzzerNoTone(buzzPin);
// Test anyrtttl with a simple melody
ESP_LOGI(tag, "Test 3: anyrtttl blocking play");
anyrtttl::blocking::play(buzzPin, DEFAULT_MELODY);
ESP_LOGI(tag, "Buzzer test complete");
}
// Optimized tune loading - minimal memory allocation
void Buzzer_Load_Tunes(const char* tunesPath){
ESP_LOGI(tag, "Loading tunes from: %s", tunesPath);
File file = LittleFS.open(tunesPath);
if (!file) {
ESP_LOGW(tag, "Could not open %s, using default tune", tunesPath);
// Set default tune only at index 0
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune at index 0: %s", DEFAULT_MELODY);
return;
}
// Use smaller JSON document for memory efficiency
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if(error){
ESP_LOGE(tag, "JSON parse error: %s", error.c_str());
// Set default tune on error
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune due to JSON error: %s", DEFAULT_MELODY);
return;
}
JsonArray tuneJsonArray = doc["tunes"];
if(!tuneJsonArray.isNull()){
int tuneIndex = 0;
for(JsonObject obj : tuneJsonArray){
if(tuneIndex >= TUNE_MAX_COUNT) break;
buzzTune[tuneIndex].cycles = jsonConstrain<int>(tag, obj, "cycles", 1, 100, 1);
buzzTune[tuneIndex].pause = jsonConstrain<int>(tag, obj, "pause", 0, 100, 0);
buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
ESP_LOGI(tag, "Loaded tune %d: cycles=%d, pause=%d, melody=%.40s...",
tuneIndex, buzzTune[tuneIndex].cycles, buzzTune[tuneIndex].pause,
buzzTune[tuneIndex].melody.c_str());
tuneIndex++;
}
ESP_LOGI(tag, "Successfully loaded %d tunes", tuneIndex);
} else {
ESP_LOGW(tag, "No 'tunes' array found in JSON");
// Set default tune if no tunes array found
buzzTune[0].cycles = 1;
buzzTune[0].pause = 0;
buzzTune[0].melody = DEFAULT_MELODY;
ESP_LOGI(tag, "Loaded default tune: %s", DEFAULT_MELODY);
}
}

View File

@ -183,7 +183,7 @@ void Wifi_Init() {
ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str()); ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str());
// Start the WiFi task // Start the WiFi task
xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*4, NULL, 1, &Wifi_Task_Handle, 0); xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*6, NULL, 1, &Wifi_Task_Handle, 0);
} }
void Wifi_Load_Settings(String path){ void Wifi_Load_Settings(String path){