Compare commits
No commits in common. "d5dcf6c0fe4bf04bba7be8a60384a6b018f47fe2" and "5a34353a32781070234228cdb1deb0020a4dce71" have entirely different histories.
d5dcf6c0fe
...
5a34353a32
18
ToDo.txt
18
ToDo.txt
@ -1,10 +1,18 @@
|
||||
1 - Second Strip Animation
|
||||
a - How to synchronize the 2 Animation
|
||||
b - Set Default directions?
|
||||
|
||||
1) OTA switch to Minio
|
||||
2 - Wifi Server
|
||||
|
||||
2) Global variable ( Circular or Linear LEDS)
|
||||
3 - OTA Upgrade
|
||||
|
||||
3) Long Hold to switch
|
||||
4 - menu
|
||||
a - file manager & editor
|
||||
b - wifi credentials
|
||||
c - firmware update
|
||||
d - mode selection page
|
||||
i - booth config and reboot
|
||||
ii -
|
||||
|
||||
4) Fix white flasing at booth
|
||||
|
||||
5)
|
||||
anyting new.....
|
||||
@ -1,531 +0,0 @@
|
||||
<!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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ATA Firmware Update</h1>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="btn-container">
|
||||
<button id="bleConnectBtn">Connect</button>
|
||||
<button id="checkStatusBtn" disabled>Check Status</button>
|
||||
</div>
|
||||
|
||||
<!-- Log Area -->
|
||||
<textarea id="logArea" readonly></textarea>
|
||||
|
||||
<div class="btn-container">
|
||||
<button id="checkVersionBtn" disabled>Check Version</button>
|
||||
<button id="startUpgradeBtn" disabled>Start Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Wi-Fi Input Fields -->
|
||||
<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" style="width: auto;">
|
||||
<label for="showPassword">Show Password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Added margin-top above this button -->
|
||||
<div class="btn-container wifi">
|
||||
<button id="wifiConnectBtn" disabled>Connect Wifi</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
// Constants
|
||||
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
|
||||
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
|
||||
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
|
||||
let bleDevice = null;
|
||||
let bleCharacteristic1 = null;
|
||||
let bleCharacteristic2 = null;
|
||||
let bleConnected = false;
|
||||
|
||||
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
|
||||
|
||||
const updatePacket = {
|
||||
wifiConnected: false,
|
||||
wifiOnline: false,
|
||||
wifiIP: [0, 0, 0, 0],
|
||||
currVersion: [0, 0, 0],
|
||||
newVersion: [0, 0, 0]
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Log messages to the textarea
|
||||
function logMessage(message) {
|
||||
const logArea = document.getElementById('logArea');
|
||||
logArea.value += message + '\n';
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to scan for BLE devices
|
||||
async function scanForDevices() {
|
||||
logMessage('Scanning for BLE devices...');
|
||||
try {
|
||||
const device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: true,
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
if (device) {
|
||||
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
|
||||
} else {
|
||||
logMessage('No devices found.');
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage(`Scan failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to connect to the BLE server
|
||||
async function connectToBle() {
|
||||
if(!navigator.bluetooth){
|
||||
logMessage('Web Bluetooth not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ name: BLE_SERVER_NAME }],
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
//logMessage(`Connecting to ${bleDevice.name}`);
|
||||
const server = await bleDevice.gatt.connect();
|
||||
//await server.setPreferredMtu(247); // Request larger MTU size
|
||||
|
||||
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
||||
|
||||
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
//await bleCharacteristic1.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
|
||||
|
||||
|
||||
//logMessage('Getting characteristic...');
|
||||
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
await bleCharacteristic2.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
|
||||
const value = event.target.value;
|
||||
const decoder = new TextDecoder();
|
||||
const decodedValue = decoder.decode(value);
|
||||
logMessage('--> ' + decodedValue);
|
||||
});
|
||||
|
||||
bleConnected = true;
|
||||
// Auto-reconnect / state reset handler
|
||||
bleDevice.addEventListener('gattserverdisconnected', handleDisconnect);
|
||||
document.getElementById('bleConnectBtn').disabled = true;
|
||||
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
|
||||
document.getElementById('status-ble-connection').textContent ="BLE Status: Connected";
|
||||
document.getElementById('wifiConnectBtn').disabled = false;
|
||||
document.getElementById('checkStatusBtn').disabled = false;
|
||||
logMessage(`Connected to ${bleDevice.name}`);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes("cancelled")) {
|
||||
logMessage("Connection cancelled by user.");
|
||||
} else {
|
||||
logMessage(`Connection failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDisconnect(){
|
||||
bleConnected = false;
|
||||
document.querySelector('.status-indicator-ble').style.backgroundColor = 'gray';
|
||||
document.getElementById('status-ble-connection').textContent = 'BLE Status: Disconnected';
|
||||
document.getElementById('bleConnectBtn').disabled = false;
|
||||
document.getElementById('checkStatusBtn').disabled = true;
|
||||
document.getElementById('wifiConnectBtn').disabled = true;
|
||||
logMessage('BLE disconnected');
|
||||
}
|
||||
|
||||
async function sendPacket(packetMsg) {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot send packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
|
||||
const encoder = new TextEncoder();
|
||||
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
|
||||
//console.log("Request sent successfully");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to send packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to send request.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readPacket() {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot read packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const value = await bleCharacteristic1.readValue();
|
||||
const data = new Uint8Array(value.buffer);
|
||||
if (data.length === 12) {
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.wifiOnline = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
//processUpdatePacket(updatePacket);
|
||||
return;
|
||||
}
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to read packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to read packet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process update packet
|
||||
function processUpdatePacket(packet) {
|
||||
// Process the packet data
|
||||
//console.log("Processing update packet:", packet);
|
||||
if(packet.wifiConnected === true) {
|
||||
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
|
||||
}
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
|
||||
}
|
||||
|
||||
if(packet.wifiOnline === true) {
|
||||
document.getElementById('status-internet').textContent = 'Online';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
|
||||
document.getElementById('checkVersionBtn').disabled = false;
|
||||
|
||||
} else {
|
||||
document.getElementById('status-internet').textContent = 'Offline';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
}
|
||||
|
||||
if (packet.currVersion[0] > 0) {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
|
||||
} else {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
|
||||
}
|
||||
|
||||
if (packet.newVersion[0] > 0) {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
|
||||
if(packet.wifiOnline && compareVersions(packet.newVersion, packet.currVersion) > 0) {
|
||||
|
||||
//enable start upgrade button
|
||||
logMessage("New Version Available:");
|
||||
document.getElementById('startUpgradeBtn').disabled = false;
|
||||
} else {
|
||||
//disable start upgrade button
|
||||
document.getElementById('startUpgradeBtn').disabled = true;
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
} else {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ...';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
|
||||
function handleChar1Notifications(event) {
|
||||
const data = new Uint8Array(event.data);
|
||||
|
||||
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing updatePacket object instead of creating new one
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.wifiOnline = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
processUpdatePacket(updatePacket);
|
||||
}
|
||||
|
||||
document.getElementById('showPassword').addEventListener('change', function() {
|
||||
const passwordInput = document.getElementById('wifipassword');
|
||||
passwordInput.type = this.checked ? 'text' : 'password';
|
||||
});
|
||||
|
||||
// Event listeners for buttons
|
||||
document.getElementById('bleConnectBtn').addEventListener('click', connectToBle);
|
||||
document.getElementById('checkStatusBtn').addEventListener('click', async () => {
|
||||
if (bleCharacteristic1) {
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
logMessage('BLE device not connected.');
|
||||
}
|
||||
});
|
||||
document.getElementById('checkVersionBtn').addEventListener('click', async () => {
|
||||
await sendPacket('version-check');
|
||||
// loop and monitor the the updatePacket.newVersion
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
let success = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await readPacket();
|
||||
if (updatePacket.newVersion[0] > 0) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
if(success) {
|
||||
processUpdatePacket(updatePacket);
|
||||
logMessage("New Version: Available");
|
||||
} else {
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
|
||||
});
|
||||
document.getElementById('wifiConnectBtn').addEventListener('click', async () => {
|
||||
const ssid = document.getElementById('wifissid').value;
|
||||
const password = document.getElementById('wifipassword').value;
|
||||
if (ssid && password) {
|
||||
// Send credentials to the device (retain original spacing format expected by firmware)
|
||||
const jsonString = ' {"ssid":"' + ssid.trim() + '","pass":"' + password + '"} ';
|
||||
await sendPacket('wifi-connect' + jsonString);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
alert('Please enter both SSID and password.');
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
document.getElementById('startUpgradeBtn').addEventListener('click', async () => {
|
||||
try {
|
||||
await sendPacket('upgrade-start');
|
||||
logMessage("Upgrade Starting... Please wait.");
|
||||
} catch (error) {
|
||||
logMessage(`Error starting upgrade: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial call to process the update packet with defaults
|
||||
processUpdatePacket(updatePacket);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -104,7 +104,7 @@
|
||||
"size": 168,
|
||||
"chip": "SK6812",
|
||||
"rgb-order": "rgb",
|
||||
"shift":-5,
|
||||
"shift":-42,
|
||||
"offset": 0,
|
||||
"power-div": 0,
|
||||
"i2s-ch": 0,
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "ATALIGHTS",
|
||||
"lights-service": "FFE0",
|
||||
"lights-char": "FFE1",
|
||||
"stick-char": "FFE2",
|
||||
"upgrade-service": "abcdef01-2345-6789-1234-56789abcdef0",
|
||||
"upgrade-char1": "abcdef01-2345-6789-1234-56789abcdef1",
|
||||
"upgrade-char2": "abcdef02-2345-6789-1234-56789abcdef1"
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"boardfile": "/boards/board15.json",
|
||||
"configfile": "/booths/roamer-big.json"
|
||||
"configfile": "/booths/helio-posh.json"
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
"folder": "latest/",
|
||||
"baseurl": "https://s3-minio.boothwizard.com/boothifier/",
|
||||
"baseurl2": "https://storage.googleapis.com/boothifier/"
|
||||
"baseurl": "https://storage.googleapis.com/boothifier/",
|
||||
"folder": "latest/"
|
||||
}
|
||||
@ -1,337 +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 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 = False
|
||||
UPLOAD_MANIFEST = True
|
||||
UPLOAD_DATA = False
|
||||
|
||||
# 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')
|
||||
|
||||
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(LOCAL_ROOT_PATH / 'latest' / 'firmware.bin')
|
||||
LOCAL_MANIFEST_PATH = str(LOCAL_ROOT_PATH / 'latest' / 'update.json')
|
||||
LOCAL_DATA_DIRECTORY = str(LOCAL_ROOT_PATH / 'latest' / 'data')
|
||||
|
||||
# =============================================================================
|
||||
# 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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# Manifest
|
||||
if UPLOAD_MANIFEST:
|
||||
manifest_key = join_key(dest_prefix, 'update.json') if dest_prefix else 'update.json'
|
||||
upload_file(client, LOCAL_MANIFEST_PATH, manifest_key)
|
||||
|
||||
# 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.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -2,14 +2,8 @@
|
||||
"version": {
|
||||
"major": 1,
|
||||
"minor": 4,
|
||||
"patch": 7
|
||||
"patch": 5
|
||||
},
|
||||
"release_date": "2024-01-15",
|
||||
"description": "This is a firmware update.",
|
||||
"changelog": [
|
||||
"Fixed issue with device connectivity.",
|
||||
"Improved firmware update process."
|
||||
],
|
||||
"firmware": {
|
||||
"file": "firmware.bin",
|
||||
"md5": "b8c880418a180efb23260ee093d13e61",
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{"url":"https://s3-minio.boothwizard.com/api/v1/service-account-credentials","accessKey":"qu3aDl5mWKJjqaQPkR19","secretKey":"5A0rktd88CiwNUQAr6k6OtUuoxJLy2Xqd9E9dmyZ","api":"s3v4","path":"auto"}
|
||||
@ -8,8 +8,7 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "AppVersion.h"
|
||||
|
||||
//#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://storage.googleapis.com/boothifier/latest/"
|
||||
#define BUFFER_SIZE 4096
|
||||
|
||||
extern TaskHandle_t Update_Task_Handle;
|
||||
@ -41,8 +40,7 @@ class AppUpdater {
|
||||
public:
|
||||
Version localVersion;
|
||||
Version otaVersion;
|
||||
// Base URL (bucket) for update resources. Supports either Google Cloud Storage or MinIO (or any HTTP host)
|
||||
String baseUrl;
|
||||
const char* bucketUrl;
|
||||
const char* appName;
|
||||
const char* manifestName;
|
||||
JsonDocument jsonManifest;
|
||||
@ -71,16 +69,6 @@ class AppUpdater {
|
||||
*/
|
||||
AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName ="update.json", const char* appBin = "firmware.bin" );
|
||||
|
||||
/**
|
||||
* @brief Change the base URL after construction (e.g. switch between MinIO and GCS)
|
||||
*/
|
||||
void setBaseUrl(const String& newBaseUrl) { baseUrl = newBaseUrl; }
|
||||
|
||||
/**
|
||||
* @brief Get the currently configured base URL
|
||||
*/
|
||||
const String& getBaseUrl() const { return baseUrl; }
|
||||
|
||||
/**
|
||||
* @brief Set progress callback function
|
||||
* @param callback Function to call with progress updates
|
||||
@ -149,12 +137,6 @@ class AppUpdater {
|
||||
*/
|
||||
void updateProgress(UpdateStatus newStatus, int percentage, const char* message = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Build a full URL by combining baseUrl and a relative path. Absolute (http/https) paths pass through.
|
||||
* Ensures exactly one slash joins base and path. Leading slash on path is stripped.
|
||||
*/
|
||||
String buildUrl(const char* path) const;
|
||||
|
||||
String getLocalMD5(const char* filePath);
|
||||
};
|
||||
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
// BLE settings (loaded from JSON)
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
|
||||
// Defaults (used if JSON missing fields)
|
||||
#define DEFAULT_BT_DEVICE_NAME "ATALIGHTS"
|
||||
#define DEFAULT_BT_SERVICE "FFE0"
|
||||
#define DEFAULT_BT_SP110E_CHAR "FFE1"
|
||||
#define DEFAULT_BT_STICK_CHAR "FFE2"
|
||||
#define DEFAULT_UPGRADE_SERVICE "abcdef01-2345-6789-1234-56789abcdef0"
|
||||
#define DEFAULT_UPGRADE_CHAR1 "abcdef01-2345-6789-1234-56789abcdef1"
|
||||
#define DEFAULT_UPGRADE_CHAR2 "abcdef02-2345-6789-1234-56789abcdef1"
|
||||
|
||||
extern String BTDeviceName;
|
||||
extern String BTServiceUUID;
|
||||
extern String BTSP110ECharacteristicUUID;
|
||||
extern String BTStickCharacteristicUUID;
|
||||
extern String BTUpgradeServiceUUID;
|
||||
extern String BTUpgradeCharacteristic1UUID;
|
||||
extern String BTUpgradeCharacteristic2UUID;
|
||||
|
||||
void Load_BLE_Settings(const String &configPath);
|
||||
@ -27,11 +27,10 @@ typedef struct{
|
||||
}BOARD_PINS;
|
||||
|
||||
extern BOARD_PINS* thisBoardPins;
|
||||
// Safe status pin macros: only write when configured (>=0)
|
||||
#define setStatusPin1(state) do { if (thisBoardPins && thisBoardPins->stat[0] >= 0) digitalWrite(thisBoardPins->stat[0], state); } while(0)
|
||||
#define setStatusPin2(state) do { if (thisBoardPins && thisBoardPins->stat[1] >= 0) digitalWrite(thisBoardPins->stat[1], state); } while(0)
|
||||
#define setStatusPin1(state) digitalWrite(thisBoardPins->stat[0], state);
|
||||
#define setStatusPin2(state) digitalWrite(thisBoardPins->stat[1], state);
|
||||
|
||||
bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path);
|
||||
void Load_Board_Pins(BOARD_PINS& boardPins, String& path);
|
||||
void Init_Board_Basic(BOARD_PINS& boardPins);
|
||||
void updateFanControl(float temperature);
|
||||
void Initialize_Rear_Control(int relayIndex, int buttonIndex, int rampTime, int steps, float min, float max);
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
#include "OneButton.h"
|
||||
|
||||
extern OneButton *boardButtons[3];
|
||||
void Init_ButtonEvents(int8_t (&pin)[3]);
|
||||
|
||||
// Safely tick any initialized buttons (nullptr-aware)
|
||||
void Update_Buttons();
|
||||
#define Update_Buttons() boardButtons[1]->tick(); boardButtons[2]->tick(); boardButtons[3]->tick();
|
||||
|
||||
void Init_ButtonEvents(int8_t (&pin)[3]);
|
||||
|
||||
void btn1_click();
|
||||
void btn1_doubleClick();
|
||||
|
||||
@ -34,6 +34,6 @@ build_flags =
|
||||
-D CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=1
|
||||
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
|
||||
-D CONFIG_ARDUHAL_LOG_COLORS=1
|
||||
upload_port = COM5
|
||||
upload_port = COM11
|
||||
debug_init_break = tbreak setup
|
||||
monitor_port = COM5
|
||||
monitor_port = COM11
|
||||
|
||||
@ -19,7 +19,7 @@ TaskHandle_t Animation_Task_Handle;
|
||||
LEDSTRIP_SETTINGS ledSettings[2];
|
||||
volatile bool AnimationLooping = false;
|
||||
ANIM_EVENT prevAnimEvent = {0};
|
||||
QueueHandle_t animationQueue = xQueueCreate( 4, sizeof( ANIM_EVENT ) );
|
||||
QueueHandle_t animationQueue = xQueueCreate( 1, sizeof( ANIM_EVENT ) );
|
||||
|
||||
void Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t blu){
|
||||
|
||||
@ -51,7 +51,7 @@ void Lights_Set_Brightness(uint8_t scale){
|
||||
}
|
||||
|
||||
void Lights_Set_White(uint8_t val){
|
||||
//pwmOut[0]->setOutput(val / 2.5f);
|
||||
//pwmOut[0]->setOutput(led_status.white / 2.5f);
|
||||
}
|
||||
|
||||
void Init_Lights_Task(void){
|
||||
@ -352,12 +352,8 @@ void Lights_Control_Task(void *parameters){
|
||||
ESP_LOGD(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex);
|
||||
switch (AnimEvent.AnimationIndex) {
|
||||
case -3: // Set Pixel by index
|
||||
if (AnimEvent.data.data[7] >= 0 && AnimEvent.data.data[7] < ledSettings[0].size) {
|
||||
ledSettings[0].leds[AnimEvent.data.data[7]] = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu);
|
||||
FastLED.show();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Pixel index out of range: %d", AnimEvent.data.data[7]);
|
||||
}
|
||||
break;
|
||||
case -2: // Fill Static Color
|
||||
col = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu);
|
||||
@ -376,32 +372,29 @@ void Lights_Control_Task(void *parameters){
|
||||
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30);
|
||||
break;
|
||||
case 2:
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, ledSettings[0].shift);
|
||||
whiteTimeout = 20;
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, 0);
|
||||
break;
|
||||
case 3:
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, ledSettings[0].shift);
|
||||
whiteTimeout = 20;
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, 0);
|
||||
break;
|
||||
case 4:
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, ledSettings[0].shift);
|
||||
whiteTimeout = 20;
|
||||
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, 0);
|
||||
break;
|
||||
case 5:
|
||||
createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
|
||||
break;
|
||||
case 6:
|
||||
createFirePalette(firePalette, CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
|
||||
break;
|
||||
case 7:
|
||||
createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
|
||||
break;
|
||||
case 8:
|
||||
createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
|
||||
break;
|
||||
case 9:
|
||||
loadColorPack(colorPack, colorPack_USA);
|
||||
|
||||
@ -71,16 +71,7 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
// Call animation function
|
||||
int speedIncrease = 0;
|
||||
try {
|
||||
speedIncrease = callback(); // Call animation function
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop_Duration", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop_Duration", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
int speedIncrease = callback(); // Call animation function
|
||||
|
||||
if(!loop_active_flag) return;
|
||||
|
||||
@ -103,7 +94,11 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp
|
||||
TickType_t totalElapsed = xTaskGetTickCount() - startTicks;
|
||||
|
||||
if (totalElapsed >= durationMs) {
|
||||
// Auto-terminate the loop when duration reached
|
||||
while(loop_active_flag){
|
||||
if (ulTaskNotifyTake(pdTRUE, 50)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -126,16 +121,7 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t
|
||||
for(;;) {
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
int speedIncrease = 0;
|
||||
try {
|
||||
speedIncrease = callback(); // Call animation function
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop_Cycles", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop_Cycles", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
int speedIncrease = callback(); // Call animation function
|
||||
|
||||
if(!loop_active_flag) return;
|
||||
|
||||
@ -153,8 +139,14 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t
|
||||
// Delay and Check for termination request
|
||||
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; }
|
||||
|
||||
// Check if cycles reached and exit
|
||||
// Check if cycles reached and wait for loop_active_flag
|
||||
if (loop_cycle_count >= loop_cycles) {
|
||||
while(loop_active_flag){
|
||||
if (ulTaskNotifyTake(pdTRUE, 50)) {
|
||||
//loop_active_flag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -197,7 +189,6 @@ void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const
|
||||
|
||||
// Calculate half size for mirroring
|
||||
const int halfSize = size / 2;
|
||||
if (halfSize <= 0) return;
|
||||
|
||||
// Create heat array for half the size
|
||||
uint8_t* heat = new (std::nothrow) uint8_t[halfSize];
|
||||
@ -220,8 +211,7 @@ void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const
|
||||
|
||||
// Randomly ignite new sparks at bottom
|
||||
if(random8() < FIRE_SPARKING) {
|
||||
// ensure y is in-bounds for small strips
|
||||
y = min<int>(random8(7), halfSize - 1);
|
||||
y = random8(7);
|
||||
heat[y] = qadd8(heat[y], random8(160, 240));
|
||||
}
|
||||
|
||||
@ -392,11 +382,11 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
// Set fade factor
|
||||
uint8_t fadeFactor = shorterTail ? COMET_FADE_FACTOR2 : COMET_FADE_FACTOR1;
|
||||
|
||||
// Initialize comet positions with fixed array, evenly distributed
|
||||
// Initialize comet positions with fixed array
|
||||
int cometPositions[MAX_COMETS] = {0};
|
||||
int spacing = size / totalComets;
|
||||
for (int i = 0; i < totalComets; i++) {
|
||||
// Even distribution even when size not divisible by totalComets
|
||||
cometPositions[i] = (i * size) / totalComets;
|
||||
cometPositions[i] = i * spacing;
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
@ -424,11 +414,9 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
}
|
||||
|
||||
// Draw comet with solid color
|
||||
color = colorPack.col[i % colorPack.size];
|
||||
colorPack.col[i % colorPack.size];
|
||||
for (int j = 0; j < cometSize; j++) {
|
||||
// Tail follows the direction of movement
|
||||
pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j);
|
||||
pos = (pos % size + size) % size; // safe modulus
|
||||
pos = (cometPositions[i] - j + size) % size;
|
||||
leds[pos] += color;
|
||||
}
|
||||
}
|
||||
@ -451,7 +439,7 @@ 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) {
|
||||
if (!leds || size <= 1 || totalDurationMs <= 0) return;
|
||||
if (!leds || size <= 0 || totalDurationMs <= 0) return;
|
||||
|
||||
const int halfSize = size / 2;
|
||||
const float msPerLed = totalDurationMs / (float)halfSize;
|
||||
@ -475,7 +463,7 @@ void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCo
|
||||
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
|
||||
leds[size - 1 - pos] = fillCol; // Mirror
|
||||
}
|
||||
|
||||
// Update LEDs only when necessary
|
||||
@ -498,35 +486,35 @@ void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COL
|
||||
unsigned long startTime = millis();
|
||||
const uint32_t halfTime = timeMs / 2;
|
||||
|
||||
const uint8_t origBright = FastLED.getBrightness();
|
||||
uint8_t origBright = FastLED.getBrightness();
|
||||
FastLED.setBrightness(255);
|
||||
|
||||
uint8_t breath = MIN_BRIGHTNESS;
|
||||
uint8_t brightness = MIN_BRIGHTNESS;
|
||||
uint32_t elapsedTime;
|
||||
CRGB scaledColor, correctedColor;
|
||||
unsigned long currentTime;
|
||||
CRGB outColor;
|
||||
Animation_Loop(activeFlag, speed, [&]() -> int {
|
||||
// Elapsed time in the current breath cycle
|
||||
// Calculate elapsed time in current breath cycle
|
||||
currentTime = millis();
|
||||
elapsedTime = currentTime - startTime;
|
||||
|
||||
// Triangle wave brightness: up for half, down for half
|
||||
// Calculate brightness using a linear approach
|
||||
if (elapsedTime < halfTime) {
|
||||
breath = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten
|
||||
brightness = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten
|
||||
} else {
|
||||
breath = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim
|
||||
brightness = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim
|
||||
}
|
||||
|
||||
// Combine breath with original global brightness (rounded)
|
||||
uint16_t prod = static_cast<uint16_t>(breath) * static_cast<uint16_t>(origBright);
|
||||
uint8_t finalBright = static_cast<uint8_t>((prod + 127) / 255);
|
||||
// Scale the color directly
|
||||
scaledColor = colors.col[colorIndex];
|
||||
scaledColor.nscale8(brightness * origBright / 255);
|
||||
|
||||
// Apply perceptual scaling once with combined brightness
|
||||
outColor = colors.col[colorIndex];
|
||||
outColor.nscale8_video(finalBright);
|
||||
// Correct the color scale for vision
|
||||
correctedColor = scaledColor;
|
||||
correctedColor.nscale8_video(brightness);
|
||||
|
||||
// Fill all LEDs with scaled color
|
||||
fill_solid(leds, size, outColor);
|
||||
fill_solid(leds, size, correctedColor);
|
||||
FastLED.show();
|
||||
|
||||
if (elapsedTime >= timeMs) {
|
||||
@ -546,17 +534,21 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const
|
||||
|
||||
CRGB color1, color2;
|
||||
|
||||
// Create initial gradient: evenly distribute blends across the strip
|
||||
// For i in [0..size-1], compute position in color space [0..colors.size)
|
||||
// Create initial gradient
|
||||
int segmentLength = size / colors.size;
|
||||
for(int i = 0; i < size; i++) {
|
||||
uint32_t pos256 = (uint32_t)i * (uint32_t)colors.size * 256u / (uint32_t)size; // 8.8 fixed point
|
||||
int segment = (int)(pos256 >> 8); // 0..colors.size-1
|
||||
uint8_t blendPos = (uint8_t)(pos256 & 0xFF); // 0..255
|
||||
// Determine which color segment we're in
|
||||
int segment = i / segmentLength;
|
||||
int nextSegment = (segment + 1) % colors.size;
|
||||
|
||||
// Local copies for blending
|
||||
// Calculate blend amount within segment
|
||||
uint8_t blendPos = map(i % segmentLength, 0, segmentLength - 1, 0, 255);
|
||||
|
||||
// Create local copies for blending
|
||||
color1 = colors.col[segment];
|
||||
color2 = colors.col[nextSegment];
|
||||
|
||||
// Set initial gradient
|
||||
leds[i] = blend(color1, color2, blendPos);
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,8 @@
|
||||
#include <LittleFS.h>
|
||||
#include <memory>
|
||||
#include "global.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "jsonConstrain.h"
|
||||
#include "BLE_UpdateService.h"
|
||||
#include <HTTPClient.h>
|
||||
#include <Update.h>
|
||||
|
||||
static const char* TAG = "AppUpdater";
|
||||
TaskHandle_t Update_Task_Handle = NULL;
|
||||
@ -22,12 +20,9 @@ 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])
|
||||
: localVersion(localVersion), bucketUrl(bucket), 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());
|
||||
ESP_LOGI(TAG, "AppUpdater initialized with version %s", localVersion.toString().c_str());
|
||||
}
|
||||
|
||||
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
|
||||
@ -42,7 +37,7 @@ void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const ch
|
||||
}
|
||||
|
||||
bool AppUpdater::checkManifest() {
|
||||
String url = buildUrl(manifestName);
|
||||
String url = String(bucketUrl) + manifestName;
|
||||
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||
|
||||
// Start the HTTP client and Send GET request for manifest
|
||||
@ -95,8 +90,7 @@ bool AppUpdater::checkManifest() {
|
||||
|
||||
// Check if an update is available
|
||||
updateAvailable = false;
|
||||
// Only mark update available if remote is strictly newer than local
|
||||
if (otaVersion <= localVersion) {
|
||||
if (otaVersion < localVersion) {
|
||||
ESP_LOGI(TAG, "No updates available");
|
||||
return false;
|
||||
}else{
|
||||
@ -113,7 +107,7 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
||||
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
|
||||
// Construct full URL
|
||||
String url = buildUrl(remotePath);
|
||||
String url = String(bucketUrl) + remotePath;
|
||||
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
|
||||
|
||||
String localMd5 = getLocalMD5(localPath);
|
||||
@ -169,8 +163,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
|
||||
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
|
||||
if (contentLength > 0) {
|
||||
// Single pass with known content length
|
||||
// Single pass: Save file and calculate MD5
|
||||
while (totalRead < contentLength) {
|
||||
size_t available = stream->available();
|
||||
if (available) {
|
||||
@ -191,26 +184,6 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
}
|
||||
yield();
|
||||
}
|
||||
} else {
|
||||
// Unknown content length: read until stream ends
|
||||
for (;;) {
|
||||
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||
if (readLen == 0) {
|
||||
break;
|
||||
}
|
||||
if (file.write(downloadBuffer.get(), readLen) != readLen) {
|
||||
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||
file.close();
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
md5.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
// Progress unknown; emit periodic heartbeats at 0%
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
md5.calculate();
|
||||
@ -298,7 +271,7 @@ bool AppUpdater::updateApp() {
|
||||
|
||||
// Get the firmware MD5 hash and URL
|
||||
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
|
||||
String firmwareUrl = buildUrl(appName);
|
||||
String firmwareUrl = String(bucketUrl) + appName;
|
||||
|
||||
// Download the firmware
|
||||
HTTPClient http;
|
||||
@ -313,7 +286,7 @@ bool AppUpdater::updateApp() {
|
||||
|
||||
// Check available space
|
||||
size_t firmwareSize = http.getSize();
|
||||
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
|
||||
if (!Update.begin(firmwareSize)) {
|
||||
ESP_LOGE(TAG, "Firmware: Not enough space for update");
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
|
||||
http.end();
|
||||
@ -326,7 +299,6 @@ bool AppUpdater::updateApp() {
|
||||
|
||||
// Download and verify firmware
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
if (firmwareSize > 0) {
|
||||
size_t remaining = firmwareSize;
|
||||
while (remaining > 0) {
|
||||
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
|
||||
@ -335,6 +307,7 @@ bool AppUpdater::updateApp() {
|
||||
// Check for timeout
|
||||
if (read == 0) {
|
||||
ESP_LOGE(TAG, "Read timeout");
|
||||
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
@ -352,21 +325,6 @@ bool AppUpdater::updateApp() {
|
||||
remaining -= read;
|
||||
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
|
||||
}
|
||||
} else {
|
||||
// Unknown size: stream until end
|
||||
for (;;) {
|
||||
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||
if (read == 0) break;
|
||||
md5.add(downloadBuffer.get(), read);
|
||||
if (Update.write(downloadBuffer.get(), read) != read) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify MD5
|
||||
md5.calculate();
|
||||
@ -396,19 +354,6 @@ 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) {
|
||||
@ -465,6 +410,7 @@ void firmwareUpdateTask(void* parameter) {
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
|
||||
void startVersionCheckTask() {
|
||||
if(versionCheckTask_Handle != NULL) {
|
||||
ESP_LOGW(TAG, "Version Check Tak already running");
|
||||
@ -507,8 +453,8 @@ void loadUpdateJson(void) {
|
||||
|
||||
// Get update configuration
|
||||
JsonObject jObj = doc.as<JsonObject>();
|
||||
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://storage.googleapis.com/boothifier/");
|
||||
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());
|
||||
@ -524,7 +470,6 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
|
||||
const char* msg;
|
||||
bool isComplete = false;
|
||||
|
||||
const char* safeMsg = message ? message : "";
|
||||
switch (newStatus) {
|
||||
case AppUpdater::UpdateStatus::IDLE:
|
||||
snprintf(buffer, sizeof(buffer), "Update idle");
|
||||
@ -534,23 +479,23 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
|
||||
msg = message ? message : "";
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::DOWNLOADING:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
|
||||
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", message, percentage);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::VERIFYING:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
|
||||
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", message, percentage);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::FILE_SKIPPED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
|
||||
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", message);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::FILE_SAVED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
|
||||
snprintf(buffer, sizeof(buffer), "%s: File Saved", message);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::MD5_FAILED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
|
||||
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", message);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::COMPLETE:
|
||||
@ -559,7 +504,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
|
||||
isComplete = true;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::ERROR:
|
||||
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
|
||||
snprintf(buffer, sizeof(buffer), "Error!: %s", message);
|
||||
msg = buffer;
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -3,24 +3,14 @@
|
||||
#include "esp_log.h"
|
||||
#include "WiFi.h"
|
||||
#include "ATALights.h"
|
||||
#include "BleSettings.h"
|
||||
|
||||
static const char *tag = "BLE_SP110E";
|
||||
|
||||
//#ifndef BT_SERVICE
|
||||
// #define BT_SERVICE "FFE0"
|
||||
//#endif
|
||||
|
||||
//#ifndef BT_SP110E_CHARACTERISTIC
|
||||
// #define BT_SP110E_CHARACTERISTIC "FFE1"
|
||||
//#endif
|
||||
|
||||
#define BT_SERVICE "FFE0"
|
||||
#define BT_SP110E_CHARACTERISTIC "FFE1"
|
||||
NimBLECharacteristic *pSP110ECharacteristic = nullptr;
|
||||
|
||||
//#ifndef BT_STICK_CHARACTERISTIC
|
||||
// #define BT_STICK_CHARACTERISTIC "FFE2"
|
||||
//#endif
|
||||
|
||||
#define BT_STICK_CHARACTERISTIC "FFE2"
|
||||
NimBLECharacteristic *pStickCharacteristic = nullptr;
|
||||
|
||||
NimBLEClient* pStickClient;
|
||||
@ -65,11 +55,7 @@ class SP110ECallbacks : public NimBLECharacteristicCallbacks {
|
||||
std::string rawValue = pCharacteristic->getValue();
|
||||
const uint8_t* value = reinterpret_cast<const uint8_t*>(rawValue.data());
|
||||
size_t length = rawValue.length();
|
||||
if (length >= 3) {
|
||||
ESP_LOGI(tag, "Data received 0x%02X, 0x%02X, 0x%02X (length %zu):", value[0], value[1], value[2], length);
|
||||
} else {
|
||||
ESP_LOGI(tag, "Data received (length %zu)", length);
|
||||
}
|
||||
|
||||
sendToAllClients(value, length);
|
||||
process_BLE_SP110E_Command(value, length, pCharacteristic);
|
||||
@ -268,11 +254,11 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
|
||||
led_status.count_lsb = 20;
|
||||
|
||||
// Create BLE Service
|
||||
NimBLEService *pService = pServer->createService( BTServiceUUID.c_str() ); // Use the defined service UUID
|
||||
NimBLEService *pService = pServer->createService( BT_SERVICE );
|
||||
|
||||
// Create FFE1 Characteristic with WRITE and NOTIFY properties
|
||||
pSP110ECharacteristic = pService->createCharacteristic(
|
||||
BTSP110ECharacteristicUUID.c_str(),
|
||||
BT_SP110E_CHARACTERISTIC,
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
@ -282,7 +268,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
|
||||
|
||||
/************* Light Stick Characteristic ***************/
|
||||
pStickCharacteristic = pService->createCharacteristic(
|
||||
BTStickCharacteristicUUID.c_str(),
|
||||
BT_STICK_CHARACTERISTIC,
|
||||
NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
@ -298,7 +284,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
|
||||
|
||||
// Configure Advertising
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID( BTServiceUUID.c_str() ); // Advertise the FFE0 service UUID
|
||||
pAdvertising->addServiceUUID( BT_SERVICE ); // Advertise the FFE0 service UUID
|
||||
|
||||
const uint8_t manufacturerData[] = {0x00, 0x00, 0x38, 0x93, 0x0E, 0x12, 0xAA, 0x08}; // Example Manufacturer Data
|
||||
pAdvertising->setManufacturerData(std::string((char *)manufacturerData, sizeof(manufacturerData)));
|
||||
@ -343,13 +329,13 @@ void BLE_LightStick_Client_Task(void *parameter) {
|
||||
ESP_LOGI(tag, "Connected to the server");
|
||||
|
||||
// Get the service.
|
||||
NimBLERemoteService* pRemoteService = pStickClient->getService(BTServiceUUID.c_str());
|
||||
NimBLERemoteService* pRemoteService = pStickClient->getService(BT_SERVICE);
|
||||
if (pRemoteService == nullptr) {
|
||||
ESP_LOGE(tag, "Failed to find service UUID: %s", BTServiceUUID.c_str());
|
||||
ESP_LOGE(tag, "Failed to find service UUID: %s", BT_SERVICE);
|
||||
pStickClient->disconnect();
|
||||
} else {
|
||||
// Get the characteristic.
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(BTStickCharacteristicUUID.c_str());
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(BT_STICK_CHARACTERISTIC);
|
||||
if (pRemoteCharacteristic != nullptr && pRemoteCharacteristic->canNotify()) {
|
||||
pRemoteCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pRemoteCharacteristic,
|
||||
uint8_t* pData, size_t length, bool isNotify) {
|
||||
|
||||
@ -4,13 +4,12 @@
|
||||
#include "global.h"
|
||||
#include "AppUpgrade.h"
|
||||
#include "AppVersion.h"
|
||||
#include "BleSettings.h"
|
||||
|
||||
static const char *tag = "BLE_UpdateService";
|
||||
|
||||
//#define UPGRADE_SERVICE_UUID "abcdef01-2345-6789-1234-56789abcdef0"
|
||||
//#define UPGRADE_CHARACTERISTIC1_UUID "abcdef01-2345-6789-1234-56789abcdef1"
|
||||
//#define UPGRADE_CHARACTERISTIC2_UUID "abcdef02-2345-6789-1234-56789abcdef1"
|
||||
#define UPGRADE_SERVICE_UUID "abcdef01-2345-6789-1234-56789abcdef0"
|
||||
#define UPGRADE_CHARACTERISTIC1_UUID "abcdef01-2345-6789-1234-56789abcdef1"
|
||||
#define UPGRADE_CHARACTERISTIC2_UUID "abcdef02-2345-6789-1234-56789abcdef1"
|
||||
|
||||
NimBLEService *pUpgradeService = nullptr;
|
||||
NimBLECharacteristic *pUpgradeCharacteristic1 = nullptr;
|
||||
@ -36,36 +35,20 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
|
||||
ESP_LOGD(tag, "Upgrade Char written with value: %s", value.c_str());
|
||||
|
||||
if (value.compare(0, 12, "wifi-connect") == 0) { // Update WiFi credentials
|
||||
// Expecting: "wifi-connect:{\"ssid\":...,\"pass\":...}"
|
||||
size_t jsonStart = (value.size() > 12 && value[12] == ':') ? 13 : 12;
|
||||
if (value.size() <= jsonStart) {
|
||||
ESP_LOGW(tag, "wifi-connect command missing JSON payload");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, value.substr(jsonStart));
|
||||
if (err) {
|
||||
ESP_LOGW(tag, "JSON parse error for wifi-connect: %s", err.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
deserializeJson(doc, value.substr(13));
|
||||
JsonObject wifiJson = doc.as<JsonObject>();
|
||||
String ssid = wifiJson["ssid"].as<String>();
|
||||
String pass = wifiJson["pass"].as<String>();
|
||||
if (ssid.length() == 0) {
|
||||
ESP_LOGW(tag, "wifi-connect missing ssid");
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(tag, "WiFi connect requested: ssid='%s', pass len=%u", ssid.c_str(), (unsigned)pass.length());
|
||||
ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str());
|
||||
|
||||
bool started = StartWifiConnectTask(ssid, pass);
|
||||
if (started) {
|
||||
bool status = StartWifiConnectTask(ssid, pass);
|
||||
if(status == true){
|
||||
updatePacket.wifiStatus = WIFI_DISCONNECTED;
|
||||
updatePacket.wifiOnline = false;
|
||||
updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0;
|
||||
}else{
|
||||
ESP_LOGW(tag, "Failed to start WiFi connection task");
|
||||
ESP_LOGI(tag, "Failed to start WiFi connection task");
|
||||
}
|
||||
}
|
||||
else if (value.compare("version-check") == 0) { // Check if new version is available
|
||||
@ -92,18 +75,21 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
|
||||
updatePacket.wifiOnline = InternetAvailable;
|
||||
if(WiFi.status() == WL_CONNECTED){
|
||||
updatePacket.wifiStatus = WIFI_CONNECTED;
|
||||
IPAddress ip = WiFi.localIP();
|
||||
updatePacket.wifiIP[0] = ip[0];
|
||||
updatePacket.wifiIP[1] = ip[1];
|
||||
updatePacket.wifiIP[2] = ip[2];
|
||||
updatePacket.wifiIP[3] = ip[3];
|
||||
if(updatePacket.wifiIP[0] == 0){
|
||||
updatePacket.wifiIP[0] = WiFi.localIP()[0];
|
||||
updatePacket.wifiIP[1] = WiFi.localIP()[1];
|
||||
updatePacket.wifiIP[2] = WiFi.localIP()[2];
|
||||
updatePacket.wifiIP[3] = WiFi.localIP()[3];
|
||||
}
|
||||
}else{
|
||||
updatePacket.wifiStatus = WIFI_DISCONNECTED;
|
||||
if(updatePacket.wifiIP[0] > 0){
|
||||
updatePacket.wifiIP[0] = 0;
|
||||
updatePacket.wifiIP[1] = 0;
|
||||
updatePacket.wifiIP[2] = 0;
|
||||
updatePacket.wifiIP[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//update version
|
||||
if(otaVersion.major() != 0){
|
||||
@ -113,24 +99,19 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
|
||||
updatePacket.newVersion[2] = otaVersion.patch();
|
||||
}
|
||||
|
||||
// Only populate the control characteristic with the status packet
|
||||
if (pCharacteristic == pUpgradeCharacteristic1) {
|
||||
pCharacteristic->setValue(reinterpret_cast<uint8_t*>(&updatePacket), sizeof(updatePacket));
|
||||
ESP_LOGI(tag, "Upgrade status read");
|
||||
}
|
||||
ESP_LOGI(tag, "Upgrade Char read");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void bleUpgrade_send_message(String s){
|
||||
if(pUpgradeCharacteristic2){
|
||||
if (s.length() == 0) {
|
||||
return;
|
||||
}
|
||||
// Set value and notify only if there are subscribers to avoid unnecessary work
|
||||
pUpgradeCharacteristic2->setValue(s.c_str());
|
||||
if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
|
||||
if (s != nullptr) {
|
||||
pUpgradeCharacteristic2->setValue(s);
|
||||
pUpgradeCharacteristic2->notify();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -139,10 +120,10 @@ void bleUpgrade_send_message(String s){
|
||||
void Init_UpgradeBLEService(NimBLEServer *pServer){
|
||||
|
||||
// Create Upgrade BLE Service
|
||||
pUpgradeService= pServer->createService( BTUpgradeServiceUUID.c_str() );
|
||||
pUpgradeService= pServer->createService( UPGRADE_SERVICE_UUID );
|
||||
|
||||
pUpgradeCharacteristic1 = pUpgradeService->createCharacteristic(
|
||||
BTUpgradeCharacteristic1UUID.c_str(),
|
||||
UPGRADE_CHARACTERISTIC1_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
@ -152,7 +133,7 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
|
||||
|
||||
|
||||
pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic(
|
||||
BTUpgradeCharacteristic2UUID.c_str(),
|
||||
UPGRADE_CHARACTERISTIC2_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
@ -163,7 +144,7 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
|
||||
pUpgradeService->start();
|
||||
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
|
||||
pAdvertising->addServiceUUID( UPGRADE_SERVICE_UUID ); // Advertise service UUID
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
#include "esp_log.h"
|
||||
#include "BLE_SP110E.h"
|
||||
#include "BLE_UpdateService.h"
|
||||
#include "BleSettings.h"
|
||||
|
||||
static const char* tag = "BleServer";
|
||||
|
||||
@ -14,22 +13,18 @@ class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
// Ensure advertising remains active even after a client connects
|
||||
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (pAdvertising != nullptr) {
|
||||
if (!pAdvertising->isAdvertising()) {
|
||||
pAdvertising->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* pServer) override {
|
||||
ESP_LOGI(tag, "Client disconnected");
|
||||
// Restart advertising on disconnect to keep it active always
|
||||
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (pAdvertising != nullptr) {
|
||||
if (!pAdvertising->isAdvertising()) {
|
||||
pAdvertising->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
||||
@ -37,9 +32,8 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
||||
ESP_LOGI(tag, "Initializing BLE...");
|
||||
static bool isInitialized = false;
|
||||
if (!isInitialized) {
|
||||
NimBLEDevice::init(BTDeviceName.c_str());
|
||||
NimBLEDevice::init("ATALIGHTS");
|
||||
//NimBLEDevice::setMTU(247); // Set preferred MTU size (max 247 for BLE)
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
NimBLEServer *pServer = NimBLEDevice::createServer();
|
||||
@ -64,7 +58,7 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
||||
// Start BLE advertising
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (pAdvertising != nullptr) {
|
||||
if (!pAdvertising->isAdvertising() && !pAdvertising->start()) {
|
||||
if (!pAdvertising->start()) {
|
||||
ESP_LOGE(tag, "Failed to start advertising");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
#include "BleSettings.h"
|
||||
#include "FS.h"
|
||||
#include <LittleFS.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *tag = "ble-settings";
|
||||
|
||||
// Global variables (initialized with defaults)
|
||||
String BTDeviceName = DEFAULT_BT_DEVICE_NAME;
|
||||
String BTServiceUUID = DEFAULT_BT_SERVICE;
|
||||
String BTSP110ECharacteristicUUID = DEFAULT_BT_SP110E_CHAR;
|
||||
String BTStickCharacteristicUUID = DEFAULT_BT_STICK_CHAR;
|
||||
String BTUpgradeServiceUUID = DEFAULT_UPGRADE_SERVICE;
|
||||
String BTUpgradeCharacteristic1UUID = DEFAULT_UPGRADE_CHAR1;
|
||||
String BTUpgradeCharacteristic2UUID = DEFAULT_UPGRADE_CHAR2;
|
||||
|
||||
static String safeJsonString(JsonVariant obj, const char *key, const char *defVal) {
|
||||
if (!obj.is<JsonObject>()) return defVal;
|
||||
JsonVariant v = obj[key];
|
||||
if (!v.is<const char*>()) return defVal;
|
||||
const char *s = v.as<const char*>();
|
||||
if (!s || *s == '\0') return defVal;
|
||||
return String(s);
|
||||
}
|
||||
|
||||
void Load_BLE_Settings(const String &configPath) {
|
||||
File file = LittleFS.open(configPath, "r");
|
||||
if (!file) {
|
||||
ESP_LOGW(tag, "Config %s not found. Using defaults.", configPath.c_str());
|
||||
return; // keep defaults
|
||||
}
|
||||
|
||||
// Use a dynamic document sized to file length (clamped)
|
||||
size_t sz = file.size();
|
||||
if (sz == 0 || sz > 4096) { // protect against unrealistically large file
|
||||
ESP_LOGE(tag, "Invalid config size: %u", (unsigned)sz);
|
||||
file.close();
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc; // heuristic
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "Deserialize error: %s", error.c_str());
|
||||
return; // keep previous / defaults
|
||||
}
|
||||
|
||||
JsonObject root = doc.as<JsonObject>();
|
||||
if (root.isNull()) {
|
||||
ESP_LOGE(tag, "Empty JSON root");
|
||||
return;
|
||||
}
|
||||
|
||||
// Map expected keys
|
||||
BTDeviceName = safeJsonString(root, "name", BTDeviceName.c_str());
|
||||
BTServiceUUID = safeJsonString(root, "lights-service", BTServiceUUID.c_str());
|
||||
BTSP110ECharacteristicUUID = safeJsonString(root, "lights-char", BTSP110ECharacteristicUUID.c_str());
|
||||
BTStickCharacteristicUUID = safeJsonString(root, "stick-char", BTStickCharacteristicUUID.c_str());
|
||||
BTUpgradeServiceUUID = safeJsonString(root, "upgrade-service", BTUpgradeServiceUUID.c_str());
|
||||
BTUpgradeCharacteristic1UUID = safeJsonString(root, "upgrade-char1", BTUpgradeCharacteristic1UUID.c_str());
|
||||
BTUpgradeCharacteristic2UUID = safeJsonString(root, "upgrade-char2", BTUpgradeCharacteristic2UUID.c_str());
|
||||
|
||||
ESP_LOGI(tag, "Loaded BLE config: name=%s svc=%s char1=%s stick=%s upg_svc=%s upg1=%s upg2=%s",
|
||||
BTDeviceName.c_str(), BTServiceUUID.c_str(), BTSP110ECharacteristicUUID.c_str(),
|
||||
BTStickCharacteristicUUID.c_str(), BTUpgradeServiceUUID.c_str(),
|
||||
BTUpgradeCharacteristic1UUID.c_str(), BTUpgradeCharacteristic2UUID.c_str());
|
||||
}
|
||||
@ -3,10 +3,7 @@
|
||||
#include <esp_log.h>
|
||||
|
||||
template <typename T>
|
||||
void logConstrainedValue(const char* tag, const char* key, T) {
|
||||
// Generic fallback when no specialization provided
|
||||
ESP_LOGD(tag, "Key [%s] value set", key);
|
||||
}
|
||||
void logConstrainedValue(const char* tag, const char* key, T value);
|
||||
|
||||
template <>
|
||||
void logConstrainedValue<int>(const char* tag, const char* key, int value) {
|
||||
@ -19,10 +16,7 @@ void logConstrainedValue<float>(const char* tag, const char* key, float value) {
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void logClamping(const char* tag, const char* key, T, T, const char* condition) {
|
||||
// Generic fallback when no specialization provided
|
||||
ESP_LOGW(tag, "Key [%s] value too %s. Clamping.", key, condition);
|
||||
}
|
||||
void logClamping(const char* tag, const char* key, T value, T limit, const char* condition);
|
||||
|
||||
template <>
|
||||
void logClamping<int>(const char* tag, const char* key, int value, int limit, const char* condition) {
|
||||
@ -78,12 +72,12 @@ const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, con
|
||||
}
|
||||
|
||||
String value = jsonObject[key].as<String>();
|
||||
if (value.length() == 0) {
|
||||
if (value.isEmpty()) {
|
||||
ESP_LOGW(tag, "Key [%s] value is empty. Using default value.", key);
|
||||
return strdup(def);
|
||||
}
|
||||
|
||||
ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str());
|
||||
ESP_LOGD(tag, "Key [%s] value: %s", key, value);
|
||||
return strdup(value.c_str());
|
||||
}
|
||||
|
||||
@ -99,7 +93,7 @@ String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const
|
||||
String value = jsonObject[key].as<String>();
|
||||
|
||||
// Check if the value is empty
|
||||
if (value.length() == 0) {
|
||||
if (value.isEmpty()) {
|
||||
ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str());
|
||||
return def;
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#include "PWM_Output.h"
|
||||
#include <Arduino.h>
|
||||
#include "global.h"
|
||||
|
||||
|
||||
const char* tag = "pwmout";
|
||||
@ -57,11 +56,6 @@ void PWM_Output::setOutput(float duty){
|
||||
outDutyVal = static_cast<int>(duty * this->standardFactor);
|
||||
}
|
||||
|
||||
// Clamp to valid resolution range [0, 2^res - 1]
|
||||
int maxVal = static_cast<int>(binaryPow[this->res]);
|
||||
if (outDutyVal < 0) outDutyVal = 0;
|
||||
if (outDutyVal > maxVal) outDutyVal = maxVal;
|
||||
|
||||
ledcWrite(this->ch, outDutyVal);
|
||||
this->currOutVal = outDutyVal;
|
||||
this->currDuty = duty;
|
||||
@ -83,9 +77,8 @@ void PWM_Output::setResolution(uint8_t res){
|
||||
if(this->res < 4) this->res = 4;
|
||||
if(this->res > 16) this->res = 16;
|
||||
|
||||
// Use the clamped resolution when computing factors
|
||||
this->standardFactor = binaryPow[this->res] * 0.01f;
|
||||
this->visionFactor = binaryPow[this->res] * 0.0001f;
|
||||
this->standardFactor = binaryPow[res] * 0.01;
|
||||
this->visionFactor = binaryPow[res] * 0.0001;
|
||||
ESP_LOGD(tag, "factor=%f, vision=%f", this->standardFactor, this->visionFactor);
|
||||
}
|
||||
|
||||
|
||||
@ -3,14 +3,6 @@
|
||||
|
||||
#define MAX_HUE 360.0
|
||||
|
||||
// Normalize a hue into [0, MAX_HUE)
|
||||
static inline float wrapHue(float h)
|
||||
{
|
||||
while (h >= MAX_HUE) h -= MAX_HUE;
|
||||
while (h < 0.0f) h += MAX_HUE;
|
||||
return h;
|
||||
}
|
||||
|
||||
|
||||
/************************* CLASS - HUE PALLET DISPENSER ************************/
|
||||
|
||||
@ -19,51 +11,59 @@ HUE_PALLET_DISPENSER::HUE_PALLET_DISPENSER(void){
|
||||
}
|
||||
|
||||
void HUE_PALLET_DISPENSER::Initialize(float hue, float range, int colSteps){
|
||||
// Clamp and normalize inputs
|
||||
this->range = constrain(range, 0.0f, (float)MAX_HUE);
|
||||
this->hueSteps = (colSteps < 0) ? 0 : colSteps;
|
||||
this->range = range;
|
||||
this->hueSteps = colSteps;
|
||||
|
||||
this->startHue = hue - this->range / 2;
|
||||
if(this->startHue < 0.0){
|
||||
this->startHue += MAX_HUE;
|
||||
}else if(this->startHue > MAX_HUE){
|
||||
this->startHue -= MAX_HUE;
|
||||
}
|
||||
|
||||
this->startHue = wrapHue(hue - (this->range * 0.5f));
|
||||
this->hueIndex = 0;
|
||||
|
||||
if(this->hueSteps <= 1){
|
||||
this->hueIncrement = 0.0f;
|
||||
this->currHue = wrapHue(hue);
|
||||
this->hueIncrement = 0.0;
|
||||
this->currHue = hue;
|
||||
}else{
|
||||
this->hueIncrement = (this->range / (this->hueSteps - 1));
|
||||
this->currHue = this->startHue;
|
||||
this->hueIncrement = this->range / (this->hueSteps - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int HUE_PALLET_DISPENSER::GetNextPalletHue(void){
|
||||
if(this->hueSteps > 1){
|
||||
this->currHue = wrapHue(this->startHue + (this->hueIndex * this->hueIncrement));
|
||||
this->hueIndex = (this->hueIndex + 1) % this->hueSteps;
|
||||
this->currHue = this->startHue + this->hueIndex * this->hueIncrement;
|
||||
if(this->currHue < 0){
|
||||
this->currHue += MAX_HUE;
|
||||
}else if(this->currHue > MAX_HUE){
|
||||
this->currHue -= MAX_HUE;
|
||||
}
|
||||
|
||||
// Convert to integer hue in [0, 359]
|
||||
int out = (int)(this->currHue + 0.5f);
|
||||
if (out >= (int)MAX_HUE) out -= (int)MAX_HUE;
|
||||
if (out < 0) out = 0; // safety
|
||||
return out;
|
||||
// TODO Remove later
|
||||
//rgbpixel_t p = HUEtoRGB(huePallet.currHue);
|
||||
//Log.traceln("<anim> index: %d, hue= %F, col: %d, %d, %d", huePallet.hueIndex, huePallet.currHue, p.red, p.grn, p.blu);
|
||||
|
||||
this->hueIndex = ++this->hueIndex % this->hueSteps;
|
||||
return round(this->currHue);
|
||||
}else{
|
||||
return round(this->currHue);
|
||||
}
|
||||
}
|
||||
|
||||
float HUE_PALLET_DISPENSER::PeekNextPalletHue(int hueOffset){
|
||||
float tempHue = wrapHue(this->startHue + ((this->hueIndex + hueOffset) * this->hueIncrement));
|
||||
float tempHue = this->startHue + (this->hueIndex + hueOffset) * this->hueIncrement;
|
||||
|
||||
if(tempHue < 0){
|
||||
tempHue += MAX_HUE;
|
||||
}else if(tempHue > MAX_HUE){
|
||||
tempHue -= MAX_HUE;
|
||||
}
|
||||
|
||||
return tempHue;
|
||||
}
|
||||
|
||||
void HUE_PALLET_DISPENSER::SetHueIndex(int hueIndex){
|
||||
if (this->hueSteps > 0) {
|
||||
int n = this->hueSteps;
|
||||
int idx = hueIndex % n;
|
||||
if (idx < 0) idx += n;
|
||||
this->hueIndex = idx;
|
||||
if (this->hueSteps > 1) {
|
||||
this->currHue = wrapHue(this->startHue + (this->hueIndex * this->hueIncrement));
|
||||
}
|
||||
} else {
|
||||
this->hueIndex = 0;
|
||||
}
|
||||
this->currHue = hueIndex;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "global.h"
|
||||
#include <WiFi.h>
|
||||
#include <Wifi.h>
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include <ArduinoJson.h>
|
||||
@ -45,6 +45,7 @@ void print_chip_info(void) {
|
||||
print_ram_info();
|
||||
}
|
||||
|
||||
|
||||
void printTaskInfo(TaskStatus_t taskStatus) {
|
||||
uint32_t ulTotalRunTime = taskStatus.ulRunTimeCounter;
|
||||
uint32_t ulStatsAsPercentage = (ulTotalRunTime * 100) / ulTotalRunTime; // Total runtime equals 100%
|
||||
@ -256,12 +257,6 @@ void Log_CPU_Load(void) {
|
||||
|
||||
// Get task stats
|
||||
task_count = uxTaskGetSystemState(task_array, task_count, &total_runtime);
|
||||
if (total_runtime == 0) {
|
||||
ESP_LOGW(tag, "Runtime stats not ready yet (total_runtime==0)");
|
||||
free(stats);
|
||||
free(task_array);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find IDLE tasks
|
||||
for (UBaseType_t i = 0; i < task_count; i++) {
|
||||
|
||||
51
src/main.cpp
51
src/main.cpp
@ -33,7 +33,6 @@
|
||||
#include "ATALights.h"
|
||||
#include "OnEveryN.h"
|
||||
#include "BLE_SP110E.h"
|
||||
#include "BleSettings.h"
|
||||
|
||||
#define FREERTOs_DIAGNOSTICS 0
|
||||
#define OLED_ENABLED 0
|
||||
@ -70,8 +69,6 @@ PWM_Output *pwmOutputs[4];
|
||||
RAMP_LIGHT *rampLight1;
|
||||
RAMP_LIGHT *rampLight2;
|
||||
|
||||
bool UpgradeMode = false;
|
||||
|
||||
void Init_ADC(void);
|
||||
float readBoardInputVoltage(void);
|
||||
void setupLogLevels(esp_log_level_t logLevel);
|
||||
@ -96,7 +93,8 @@ void setup()
|
||||
{
|
||||
// Serial Port
|
||||
Serial.begin(115200);
|
||||
while (!Serial);
|
||||
while (!Serial)
|
||||
;
|
||||
|
||||
// Initialize I2C Port for TSensor, ...
|
||||
Wire.begin(I2C_SDA1_Pin, I2C_SCL1_Pin);
|
||||
@ -125,10 +123,6 @@ void setup()
|
||||
// Load Board Pins
|
||||
Load_Board_Pins(sys_settings.boardPins, board_file_path);
|
||||
|
||||
// Set status pins off
|
||||
setStatusPin1(false);
|
||||
setStatusPin2(false);
|
||||
|
||||
// Load Booth Settings
|
||||
Load_Booth_Settings(sys_settings, booth_file_path);
|
||||
|
||||
@ -150,30 +144,24 @@ void setup()
|
||||
// Initialize Ramp Lights
|
||||
Init_Ramp_Lights(sys_settings.rampLightSettings, boardButtons, pwmOutputs);
|
||||
|
||||
// Initialize ADC
|
||||
Init_ADC();
|
||||
|
||||
// Initialize Temperature Sensor
|
||||
Init_TSensor(72);
|
||||
|
||||
Init_ADC();
|
||||
|
||||
float val = readBoardInputVoltage();
|
||||
ESP_LOGI(tag, "Input Volage = %f", val);
|
||||
|
||||
// Initialize BLE
|
||||
Load_BLE_Settings("/system/ble.json");
|
||||
if (digitalRead(sys_settings.boardPins.btn[0]) == LOW)
|
||||
{
|
||||
setStatusPin1(true);
|
||||
UpgradeMode = true;
|
||||
ESP_LOGE(tag, "Enabling BLE and Update Service");
|
||||
ESP_LOGW(tag, "Enabling BLE and Update Service");
|
||||
Init_BleServer(true, true);
|
||||
ESP_LOGW(tag, "Enabling Wifi AP and Client");
|
||||
Wifi_Init();
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(tag, "Enabling BLE, No Update Service");
|
||||
Init_BleServer(true, false); // Dont start the Upgrade service
|
||||
}
|
||||
|
||||
@ -210,14 +198,7 @@ void loop()
|
||||
// Button Scanning
|
||||
ON_EVERY_N_MILLISECONDS(10)
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (boardButtons[i] != NULL)
|
||||
{
|
||||
boardButtons[i]->tick();
|
||||
}
|
||||
}
|
||||
/*
|
||||
if (boardButtons[0] != NULL)
|
||||
{
|
||||
boardButtons[0]->tick();
|
||||
}
|
||||
@ -229,7 +210,6 @@ void loop()
|
||||
{
|
||||
boardButtons[2]->tick();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Temperature Monitor
|
||||
@ -252,10 +232,10 @@ void loop()
|
||||
}
|
||||
|
||||
// Update Tune Playing
|
||||
//if (anyrtttl::nonblocking::isPlaying())
|
||||
//{
|
||||
// anyrtttl::nonblocking::play();
|
||||
//}
|
||||
if (anyrtttl::nonblocking::isPlaying())
|
||||
{
|
||||
anyrtttl::nonblocking::play();
|
||||
}
|
||||
|
||||
// Animation TestMode Timeout
|
||||
#if LEDS_ENABLED
|
||||
@ -300,15 +280,7 @@ void loop()
|
||||
{
|
||||
static bool ledState = false;
|
||||
// digitalWrite(sys_settings.boardPins.stat[1], ledState = !ledState);
|
||||
setStatusPin2(ledState = !ledState);
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Mode Tune
|
||||
if(UpgradeMode){
|
||||
ON_EVERY_N_MILLISECONDS(5000)
|
||||
{
|
||||
Buzzer_Play_Tune(TUNE_THUMP, true, true);
|
||||
setStatusPin2(ledState = !ledState)
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,15 +292,12 @@ void loop()
|
||||
#endif
|
||||
|
||||
// Turn off white light after timeout
|
||||
ON_EVERY_N_MILLISECONDS(100)
|
||||
{
|
||||
if(whiteTimeout > 0){
|
||||
whiteTimeout--;
|
||||
if(whiteTimeout == 0){
|
||||
Lights_Set_White(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -12,29 +12,13 @@ static const char* tag = "board";
|
||||
|
||||
BOARD_PINS* thisBoardPins;
|
||||
|
||||
// Basic validator for ESP32-S3 GPIOs; rejects common reserved/USB/strap pins
|
||||
static bool isValidGpio(int pin) {
|
||||
if (pin < 0 || pin > 48) return false;
|
||||
switch (pin) {
|
||||
case 19: // USB D-
|
||||
case 20: // USB D+
|
||||
case 45: // strapping
|
||||
case 46: // strapping
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path){
|
||||
// Default initialize to -1 to avoid stale values on partial loads
|
||||
memset(&boardPins, -1, sizeof(boardPins));
|
||||
void Load_Board_Pins(BOARD_PINS& boardPins, String& path){
|
||||
thisBoardPins = &boardPins;
|
||||
File file = LittleFS.open(path);
|
||||
|
||||
if (!file) {
|
||||
ESP_LOGE(tag, "Error opening %s...", path.c_str());
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
@ -43,7 +27,7 @@ bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path){
|
||||
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "%s deserialize error!..", path.c_str());
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject boardJson = doc.as<JsonObject>();
|
||||
@ -76,28 +60,9 @@ bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path){
|
||||
boardPins.rf433tx = jsonConstrain<int>(tag, boardJson, "rf433tx", -1, 48, -1);
|
||||
boardPins.rf433rx = jsonConstrain<int>(tag, boardJson, "rf433rx", -1, 48, -1);
|
||||
|
||||
// Validate pins against reserved GPIOs
|
||||
auto clampPin = [](int v){ return isValidGpio(v) ? v : -1; };
|
||||
boardPins.rgb1 = clampPin(boardPins.rgb1);
|
||||
boardPins.rgb2 = clampPin(boardPins.rgb2);
|
||||
for (int i=0;i<3;i++) boardPins.btn[i] = clampPin(boardPins.btn[i]);
|
||||
boardPins.buzzer = clampPin(boardPins.buzzer);
|
||||
for (int i=0;i<5;i++) boardPins.touch[i] = clampPin(boardPins.touch[i]);
|
||||
boardPins.shield = clampPin(boardPins.shield);
|
||||
for (int i=0;i<4;i++) boardPins.relay[i] = clampPin(boardPins.relay[i]);
|
||||
for (int i=0;i<2;i++) boardPins.stat[i] = clampPin(boardPins.stat[i]);
|
||||
boardPins.adc1 = clampPin(boardPins.adc1);
|
||||
boardPins.oled_dc = clampPin(boardPins.oled_dc);
|
||||
boardPins.oled_rst = clampPin(boardPins.oled_rst);
|
||||
boardPins.oled_mosi = clampPin(boardPins.oled_mosi);
|
||||
boardPins.oled_sck = clampPin(boardPins.oled_sck);
|
||||
boardPins.oled_cs = clampPin(boardPins.oled_cs);
|
||||
for (int i=0;i<2;i++) boardPins.ext[i] = clampPin(boardPins.ext[i]);
|
||||
boardPins.rf433tx = clampPin(boardPins.rf433tx);
|
||||
boardPins.rf433rx = clampPin(boardPins.rf433rx);
|
||||
|
||||
// TODO Add hardware version to log
|
||||
ESP_LOGI(tag, "loaded Pins from %s", path.c_str());
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -9,28 +9,16 @@ void Init_ButtonEvents(int8_t (&pin)[3]){
|
||||
|
||||
|
||||
if(pin[0] >= 0) {
|
||||
if (boardButtons[0] == nullptr) {
|
||||
boardButtons[0] = new OneButton(pin[0], true, true);
|
||||
ESP_LOGD(tag, "Button1 Events, pin=%d", pin[0]);
|
||||
} else {
|
||||
ESP_LOGD(tag, "Button1 already initialized (pin=%d)", pin[0]);
|
||||
}
|
||||
}
|
||||
if(pin[1] >= 0) {
|
||||
if (boardButtons[1] == nullptr) {
|
||||
boardButtons[1] = new OneButton(pin[1], true, true);
|
||||
ESP_LOGD(tag, "Button2 Events, pin=%d", pin[1]);
|
||||
} else {
|
||||
ESP_LOGD(tag, "Button2 already initialized (pin=%d)", pin[1]);
|
||||
}
|
||||
}
|
||||
if(pin[2] >= 0) {
|
||||
if (boardButtons[2] == nullptr) {
|
||||
boardButtons[2] = new OneButton(pin[2], true, false);
|
||||
ESP_LOGD(tag, "Button3 Events, pin=%d", pin[2]);
|
||||
} else {
|
||||
ESP_LOGD(tag, "Button3 already initialized (pin=%d)", pin[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -74,14 +62,6 @@ void Init_ButtonEvents(int8_t (&pin)[3]){
|
||||
|
||||
}
|
||||
|
||||
void Update_Buttons() {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (boardButtons[i] != nullptr) {
|
||||
boardButtons[i]->tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void btn1_click() {
|
||||
//IncrementEventIndex();
|
||||
//Pulse_LED_Status(150);
|
||||
|
||||
@ -35,45 +35,30 @@ void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority)
|
||||
static int prev_tune = -1;
|
||||
if(buzzPin < 0) return;
|
||||
|
||||
// Range / data validation
|
||||
if (tune < 0 || tune >= TUNE_MAX_COUNT) {
|
||||
ESP_LOGW(tag, "Invalid tune index: %d", (int)tune);
|
||||
return;
|
||||
}
|
||||
const String &melody = buzzTune[tune].melody;
|
||||
if (melody.isEmpty()) {
|
||||
ESP_LOGW(tag, "Empty melody for tune %d", (int)tune);
|
||||
return;
|
||||
}
|
||||
|
||||
// Async mode: begin once, then caller should periodically call again to advance playback
|
||||
if (async) {
|
||||
bool playing = anyrtttl::nonblocking::isPlaying();
|
||||
if (hasPriority && playing) {
|
||||
if (hasPriority || !anyrtttl::nonblocking::isPlaying()) {
|
||||
if (anyrtttl::nonblocking::isPlaying()) {
|
||||
anyrtttl::nonblocking::stop();
|
||||
playing = false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Blocking mode: play full tune cycles with optional pause
|
||||
ESP_LOGD(tag, "Playing blocking tune %d cycles=%d pause=%d", (int)tune, buzzTune[tune].cycles, buzzTune[tune].pause);
|
||||
for (int c = 0; c < buzzTune[tune].cycles; ++c) {
|
||||
anyrtttl::blocking::play(buzzPin, melody.c_str());
|
||||
if (buzzTune[tune].pause > 0 && c + 1 < buzzTune[tune].cycles) {
|
||||
delay(buzzTune[tune].pause); // simple pause between cycles
|
||||
// Load nonblocking if different from previous
|
||||
if (prev_tune != tune && async) {
|
||||
anyrtttl::nonblocking::begin(buzzPin, buzzTune[tune].melody.c_str());
|
||||
}
|
||||
yield(); // allow other tasks to run
|
||||
|
||||
ESP_LOGD(tag, "Playing tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str());
|
||||
|
||||
for (int c = 0; c < buzzTune[tune].cycles; c++) {
|
||||
if (async) {
|
||||
anyrtttl::nonblocking::play();
|
||||
} else {
|
||||
anyrtttl::blocking::play(buzzPin, buzzTune[tune].melody.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
prev_tune = tune;
|
||||
} else {
|
||||
ESP_LOGD(tag, "buzzer busy");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Buzzer Beep finish
|
||||
|
||||
@ -1,77 +1,38 @@
|
||||
#include "my_tsensor.h"
|
||||
#include <Temperature_LM75_Derived.h>
|
||||
#include "global.h"
|
||||
|
||||
static const char* tag = "tsensor";
|
||||
|
||||
T_SENSOR tSensorSettings;
|
||||
TI_TMP102_Compatible *tSensor = nullptr;
|
||||
TI_TMP102_Compatible *tSensor;
|
||||
|
||||
/******************* Temperature Control ********************/
|
||||
void Init_TSensor(uint8_t addr) {
|
||||
// Initialize the temperature sensor once with the provided I2C address
|
||||
if (tSensor == nullptr) {
|
||||
//tSensor = new TI_TMP102_Compatible(72);
|
||||
tSensor = new TI_TMP102_Compatible(addr);
|
||||
ESP_LOGI(tag, "TSensor initialized at I2C addr 0x%02X", addr);
|
||||
} else {
|
||||
ESP_LOGW(tag, "TSensor already initialized; ignoring re-init request (addr 0x%02X)", addr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static inline float clampf(float v, float lo, float hi) {
|
||||
if (v < lo) return lo;
|
||||
if (v > hi) return hi;
|
||||
return v;
|
||||
}
|
||||
|
||||
void UpdateFanControl(float temperature, PWM_Output* pwmOut) {
|
||||
if (pwmOut == nullptr) {
|
||||
ESP_LOGW(tag, "UpdateFanControl called with null PWM output");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isnan(temperature) || isinf(temperature)) {
|
||||
ESP_LOGW(tag, "Invalid temperature reading: %f", temperature);
|
||||
return;
|
||||
}
|
||||
|
||||
static uint8_t FanState = 0;
|
||||
tSensorSettings.temperature = temperature; // cache last temp
|
||||
float currentDuty = pwmOut->currDuty;
|
||||
float newDuty = currentDuty;
|
||||
|
||||
// Sanitize settings locally (do not modify globals)
|
||||
float sp1 = tSensorSettings.Setpoint1;
|
||||
float sp2 = tSensorSettings.Setpoint2;
|
||||
float hyst = tSensorSettings.hyst;
|
||||
float fp1 = tSensorSettings.fanPower1;
|
||||
float fp2 = tSensorSettings.fanPower2;
|
||||
|
||||
if (hyst < 0.0f) hyst = 0.0f;
|
||||
if (sp2 < sp1) {
|
||||
// Ensure sp2 >= sp1
|
||||
float tmp = sp1; sp1 = sp2; sp2 = tmp;
|
||||
}
|
||||
const float maxDuty = pwmOut->getMaxDuty();
|
||||
fp1 = clampf(fp1, 0.0f, maxDuty);
|
||||
fp2 = clampf(fp2, 0.0f, maxDuty);
|
||||
|
||||
// Fan State Logic
|
||||
if ((FanState == 2) && (temperature < (sp2 - hyst))) {
|
||||
newDuty = fp1;
|
||||
if ((FanState == 2) && (temperature < (tSensorSettings.Setpoint2 - tSensorSettings.hyst))) {
|
||||
newDuty = tSensorSettings.fanPower1;
|
||||
FanState = 1;
|
||||
//ESP_LOGD(tag, "Dropping down to FanPower1");
|
||||
}
|
||||
else if ((FanState == 1) && (temperature < (sp1 - hyst))) {
|
||||
else if ((FanState == 1) && (temperature < (tSensorSettings.Setpoint1 - tSensorSettings.hyst))) {
|
||||
newDuty = 0;
|
||||
FanState = 0;
|
||||
//ESP_LOGD(tag, "Dropping down to FanPower0");
|
||||
}
|
||||
else if ((FanState <= 1) && (temperature > sp1)) {
|
||||
newDuty = fp1;
|
||||
if (temperature > sp2) {
|
||||
newDuty = fp2;
|
||||
else if ((FanState <= 1) && (temperature > tSensorSettings.Setpoint1)) {
|
||||
newDuty = tSensorSettings.fanPower1;
|
||||
if (temperature > tSensorSettings.Setpoint2) {
|
||||
newDuty = tSensorSettings.fanPower2;
|
||||
FanState = 2;
|
||||
//ESP_LOGD(tag, "Raising up to FanPower2");
|
||||
} //else {
|
||||
@ -82,6 +43,6 @@ TI_TMP102_Compatible *tSensor = nullptr;
|
||||
// Apply new duty cycle if changed
|
||||
if (currentDuty != newDuty) {
|
||||
pwmOut->setOutput(newDuty);
|
||||
ESP_LOGD(tag, "Board T: %.2f F, Fan -> %.2f (state=%u)", temperature, newDuty, FanState);
|
||||
ESP_LOGD(tag, "Board T: %.2f, Fan -> %.2f", temperature, newDuty);
|
||||
}
|
||||
}
|
||||
111
src/my_wifi.cpp
111
src/my_wifi.cpp
@ -20,6 +20,7 @@
|
||||
|
||||
static const char *tag = "WIFI";
|
||||
|
||||
volatile bool WifiClientConnected = false;
|
||||
volatile bool InternetAvailable;
|
||||
AsyncWebServer webServer(80);
|
||||
AsyncEventSource eventUpgradeProgress("/upgrade-progress");
|
||||
@ -124,6 +125,7 @@ bool StartWifiConnectTask(String ssid = "", String pass = "")
|
||||
if (Wifi_Task_Handle == NULL)
|
||||
{
|
||||
ESP_LOGD(tag, "Creating WiFi task");
|
||||
WifiClientConnected = false;
|
||||
xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0);
|
||||
}
|
||||
else
|
||||
@ -146,8 +148,9 @@ void Wifi_ConnectTask(void *parameter)
|
||||
static const char *tag = "Wifi_Task";
|
||||
wifi_task_running = true;
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
|
||||
if (!WifiClientConnected || client_ssid != WiFi.SSID())
|
||||
{
|
||||
WifiClientConnected = false;
|
||||
ESP_LOGD(tag, "Connecting to: %s", client_ssid.c_str());
|
||||
|
||||
// Disconnect and connect to new network
|
||||
@ -178,6 +181,7 @@ void Wifi_ConnectTask(void *parameter)
|
||||
if (WiFi.status() == WL_CONNECTED)
|
||||
{
|
||||
ESP_LOGI(tag, "Connected to %s", client_ssid.c_str());
|
||||
WifiClientConnected = true;
|
||||
WiFi.setAutoReconnect(true);
|
||||
|
||||
if (!Wifi_Save_Credentials("/system/wifi.json"))
|
||||
@ -387,7 +391,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
||||
request->send(200, "application/json", "{\"status\":\"connecting\"}"); });
|
||||
server.on("/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String jsonStr = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") +
|
||||
String jsonStr = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") +
|
||||
"\",\"ip\":\"" + WiFi.localIP().toString() + "\"}";
|
||||
request->send(200, "application/json", jsonStr); });
|
||||
server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
@ -543,7 +547,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
||||
// System and LED related handlers
|
||||
server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
@ -553,25 +557,25 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
||||
serializeJson(jsDoc, summary);
|
||||
request->send(200, "application/json", summary);
|
||||
*/
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
|
||||
// LightStik related handlers
|
||||
server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
|
||||
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
|
||||
request->send(200, "application/json", response); });
|
||||
|
||||
// Firmware Update Handlers
|
||||
@ -930,6 +934,7 @@ void onWiFiEvent(WiFiEvent_t event)
|
||||
ESP_LOGD(tag, "Connected to AP");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
WifiClientConnected = false;
|
||||
ESP_LOGD(tag, "WiFi Disconnected");
|
||||
setStatusPin1(false);
|
||||
Buzzer_Play_Tune(TUNE_DISCONNECTED);
|
||||
@ -938,12 +943,14 @@ void onWiFiEvent(WiFiEvent_t event)
|
||||
ESP_LOGD(tag, "Authentication mode of access point has changed");
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
WifiClientConnected = true;
|
||||
ESP_LOGD(tag, "My IP: %s", WiFi.localIP().toString());
|
||||
// Wifi_Start_MDNS();
|
||||
setStatusPin1(true);
|
||||
Buzzer_Play_Tune(TUNE_CONNECTED);
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
|
||||
WifiClientConnected = false;
|
||||
ESP_LOGD(tag, "Lost IP address and IP address is reset to 0");
|
||||
break;
|
||||
case ARDUINO_EVENT_WPS_ER_SUCCESS:
|
||||
@ -1029,82 +1036,38 @@ bool writeFile(fs::FS &fs, const char *path, const char *message)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize and validate path
|
||||
String finalPath(path);
|
||||
if (!finalPath.startsWith("/"))
|
||||
// Open file with error checking
|
||||
File file = fs.open(path, "w");
|
||||
if (!file)
|
||||
{
|
||||
finalPath = String("/") + finalPath;
|
||||
}
|
||||
// Prevent directory traversal
|
||||
if (finalPath.indexOf("..") >= 0)
|
||||
{
|
||||
ESP_LOGE(tag, "Rejected unsafe path: %s", finalPath.c_str());
|
||||
return false;
|
||||
}
|
||||
// Collapse duplicate slashes (optional hardening)
|
||||
while (finalPath.indexOf("//") >= 0)
|
||||
{
|
||||
finalPath.replace("//", "/");
|
||||
}
|
||||
|
||||
// Size checks
|
||||
const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB cap (aligned with readFile)
|
||||
const size_t totalSize = strlen(message);
|
||||
if (totalSize > MAX_FILE_SIZE)
|
||||
{
|
||||
ESP_LOGE(tag, "Write too large: %u bytes for %s", (unsigned)totalSize, finalPath.c_str());
|
||||
ESP_LOGE(tag, "Failed to open file: %s", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write to a temporary file first for atomicity
|
||||
String tmpPath = finalPath + ".tmp";
|
||||
File tmp = fs.open(tmpPath.c_str(), "w");
|
||||
if (!tmp)
|
||||
// Write with error handling
|
||||
try
|
||||
{
|
||||
ESP_LOGE(tag, "Failed to open temp file: %s", tmpPath.c_str());
|
||||
size_t bytesWritten = file.print(message);
|
||||
if (bytesWritten == 0)
|
||||
{
|
||||
ESP_LOGE(tag, "Failed to write to file: %s", path);
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write in a loop to ensure all bytes are written
|
||||
size_t written = 0;
|
||||
const uint8_t *buf = reinterpret_cast<const uint8_t *>(message);
|
||||
while (written < totalSize)
|
||||
{
|
||||
size_t n = tmp.write(buf + written, totalSize - written);
|
||||
if (n == 0)
|
||||
{
|
||||
ESP_LOGE(tag, "Write failed to temp file: %s at %u/%u bytes", tmpPath.c_str(), (unsigned)written, (unsigned)totalSize);
|
||||
tmp.close();
|
||||
fs.remove(tmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
written += n;
|
||||
}
|
||||
|
||||
// Flush and close temp file
|
||||
tmp.flush();
|
||||
tmp.close();
|
||||
|
||||
// Replace the target file atomically: remove existing then rename
|
||||
if (fs.exists(finalPath.c_str()))
|
||||
{
|
||||
if (!fs.remove(finalPath.c_str()))
|
||||
{
|
||||
ESP_LOGE(tag, "Failed to remove existing file: %s", finalPath.c_str());
|
||||
fs.remove(tmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!fs.rename(tmpPath.c_str(), finalPath.c_str()))
|
||||
{
|
||||
ESP_LOGE(tag, "Failed to rename %s to %s", tmpPath.c_str(), finalPath.c_str());
|
||||
fs.remove(tmpPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(tag, "Successfully wrote %u bytes to %s", (unsigned)totalSize, finalPath.c_str());
|
||||
// Ensure all data is written
|
||||
file.flush();
|
||||
file.close();
|
||||
ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path);
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
ESP_LOGE(tag, "Exception while writing file %s: %s", path, e.what());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
char *readFile(fs::FS &fs, const char *path)
|
||||
{
|
||||
|
||||
@ -313,7 +313,7 @@ void Wifi_Save_Credentials(String newSSID, String newPass)
|
||||
}
|
||||
|
||||
// Parse the JSON file
|
||||
JsonDocument doc;
|
||||
DynamicJsonDocument doc(1024);
|
||||
DeserializationError error = deserializeJson(doc, credsFile);
|
||||
if(!error){
|
||||
JsonObject cred1Json = doc.createNestedObject("cred1");
|
||||
@ -675,8 +675,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
else if(!strncmp(dataType, "anim-profile", 12)){
|
||||
if (strlen(dataType) > 12) {
|
||||
int profile_index = (dataType[12] - '0' - 1) % 8; // extract index, assuming dataType has enough characters
|
||||
//DynamicJsonDocument profJson(4 * 1024);
|
||||
JsonDocument profJson;
|
||||
DynamicJsonDocument profJson(4 * 1024);
|
||||
DeserializationError error = deserializeJson(profJson, postData, total);
|
||||
|
||||
if(!error){
|
||||
@ -712,7 +711,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
}
|
||||
}
|
||||
else if(!strcmp(dataType, "play-anim")){
|
||||
JsonDocument animData;
|
||||
DynamicJsonDocument animData(1024);
|
||||
DeserializationError error = deserializeJson(animData, postData, total);
|
||||
if(!error){
|
||||
ANIMATION_EVENT testEvent;
|
||||
@ -766,7 +765,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
getpostSuccess = true;
|
||||
}
|
||||
else if(!strcmp(dataType, "set-pixel")){
|
||||
JsonDocument js;
|
||||
DynamicJsonDocument js(1024);
|
||||
DeserializationError error = deserializeJson(js, postData, total);
|
||||
if(!error){
|
||||
ANIMATION_EVENT testEvent;
|
||||
@ -797,7 +796,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
Buzzer_Beep(50, 3000);
|
||||
}
|
||||
else if(!strcmp(dataType, "setup-save")){
|
||||
JsonDocument js;
|
||||
DynamicJsonDocument js(1024);
|
||||
DeserializationError error = deserializeJson(js, postData, total);
|
||||
if(!error){
|
||||
// If app index is different open app-events.json and update
|
||||
@ -809,7 +808,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DynamicJsonDocument doc(2048);
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
@ -847,7 +846,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
|
||||
return;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DynamicJsonDocument doc(2048);
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user