Compare commits

...

3 Commits

Author SHA1 Message Date
d5dcf6c0fe commit 8-19-25-950 2025-08-19 09:50:41 -07:00
88c2ff3def Refactor code structure and clean up unused files 2025-08-14 23:35:33 -07:00
f7950c417f Small fixes 2025-03-25 15:41:14 -07:00
33 changed files with 1646 additions and 354 deletions

View File

@ -1,18 +1,10 @@
1 - Second Strip Animation
a - How to synchronize the 2 Animation
b - Set Default directions?
2 - Wifi Server
1) OTA switch to Minio
3 - OTA Upgrade
2) Global variable ( Circular or Linear LEDS)
4 - menu
a - file manager & editor
b - wifi credentials
c - firmware update
d - mode selection page
i - booth config and reboot
ii -
3) Long Hold to switch
4) Fix white flasing at booth
anyting new.....
5)

View File

@ -0,0 +1,531 @@
<!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>

View File

@ -104,7 +104,7 @@
"size": 168,
"chip": "SK6812",
"rgb-order": "rgb",
"shift":-42,
"shift":-5,
"offset": 0,
"power-div": 0,
"i2s-ch": 0,

9
data/system/ble.json Normal file
View File

@ -0,0 +1,9 @@
{
"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"
}

View File

@ -1,4 +1,4 @@
{
"boardfile": "/boards/board15.json",
"configfile": "/booths/helio-posh.json"
"configfile": "/booths/roamer-big.json"
}

View File

@ -1,4 +1,5 @@
{
"baseurl": "https://storage.googleapis.com/boothifier/",
"folder": "latest/"
"folder": "latest/",
"baseurl": "https://s3-minio.boothwizard.com/boothifier/",
"baseurl2": "https://storage.googleapis.com/boothifier/"
}

View File

@ -0,0 +1,337 @@
#!/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()

View File

@ -2,8 +2,14 @@
"version": {
"major": 1,
"minor": 4,
"patch": 5
"patch": 7
},
"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",

View File

@ -0,0 +1 @@
{"url":"https://s3-minio.boothwizard.com/api/v1/service-account-credentials","accessKey":"qu3aDl5mWKJjqaQPkR19","secretKey":"5A0rktd88CiwNUQAr6k6OtUuoxJLy2Xqd9E9dmyZ","api":"s3v4","path":"auto"}

View File

@ -8,7 +8,8 @@
#include <ESPAsyncWebServer.h>
#include "AppVersion.h"
#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
#define BUFFER_SIZE 4096
extern TaskHandle_t Update_Task_Handle;
@ -40,7 +41,8 @@ class AppUpdater {
public:
Version localVersion;
Version otaVersion;
const char* bucketUrl;
// Base URL (bucket) for update resources. Supports either Google Cloud Storage or MinIO (or any HTTP host)
String baseUrl;
const char* appName;
const char* manifestName;
JsonDocument jsonManifest;
@ -69,6 +71,16 @@ 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
@ -137,6 +149,12 @@ 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);
};

22
include/BleSettings.h Normal file
View File

@ -0,0 +1,22 @@
// 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);

View File

@ -27,10 +27,11 @@ typedef struct{
}BOARD_PINS;
extern BOARD_PINS* thisBoardPins;
#define setStatusPin1(state) digitalWrite(thisBoardPins->stat[0], state);
#define setStatusPin2(state) digitalWrite(thisBoardPins->stat[1], state);
// 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)
void Load_Board_Pins(BOARD_PINS& boardPins, String& path);
bool Load_Board_Pins(BOARD_PINS& boardPins, const 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);

View File

@ -4,11 +4,11 @@
#include "OneButton.h"
extern OneButton *boardButtons[3];
#define Update_Buttons() boardButtons[1]->tick(); boardButtons[2]->tick(); boardButtons[3]->tick();
void Init_ButtonEvents(int8_t (&pin)[3]);
// Safely tick any initialized buttons (nullptr-aware)
void Update_Buttons();
void btn1_click();
void btn1_doubleClick();
void btn1_LongPressStart();

View File

@ -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 = COM11
upload_port = COM5
debug_init_break = tbreak setup
monitor_port = COM11
monitor_port = COM5

View File

@ -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( 1, sizeof( ANIM_EVENT ) );
QueueHandle_t animationQueue = xQueueCreate( 4, 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(led_status.white / 2.5f);
//pwmOut[0]->setOutput(val / 2.5f);
}
void Init_Lights_Task(void){
@ -352,8 +352,12 @@ 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);
@ -372,29 +376,32 @@ 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, 0);
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 3:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, 0);
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 4:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, 0);
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 5:
createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 6:
createFirePalette(firePalette, CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 7:
createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 8:
createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
case 9:
loadColorPack(colorPack, colorPack_USA);

View File

@ -71,7 +71,16 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp
xLastWakeTime = xTaskGetTickCount();
// Call animation function
int speedIncrease = callback(); // 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;
}
if(!loop_active_flag) return;
@ -94,11 +103,7 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp
TickType_t totalElapsed = xTaskGetTickCount() - startTicks;
if (totalElapsed >= durationMs) {
while(loop_active_flag){
if (ulTaskNotifyTake(pdTRUE, 50)) {
return;
}
}
// Auto-terminate the loop when duration reached
break;
}
}
@ -121,7 +126,16 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t
for(;;) {
xLastWakeTime = xTaskGetTickCount();
int speedIncrease = callback(); // Call animation function
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;
}
if(!loop_active_flag) return;
@ -139,14 +153,8 @@ 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 wait for loop_active_flag
// Check if cycles reached and exit
if (loop_cycle_count >= loop_cycles) {
while(loop_active_flag){
if (ulTaskNotifyTake(pdTRUE, 50)) {
//loop_active_flag = false;
break;
}
}
break;
}
@ -189,6 +197,7 @@ 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];
@ -211,7 +220,8 @@ void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const
// Randomly ignite new sparks at bottom
if(random8() < FIRE_SPARKING) {
y = random8(7);
// ensure y is in-bounds for small strips
y = min<int>(random8(7), halfSize - 1);
heat[y] = qadd8(heat[y], random8(160, 240));
}
@ -382,11 +392,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
// Initialize comet positions with fixed array, evenly distributed
int cometPositions[MAX_COMETS] = {0};
int spacing = size / totalComets;
for (int i = 0; i < totalComets; i++) {
cometPositions[i] = i * spacing;
// Even distribution even when size not divisible by totalComets
cometPositions[i] = (i * size) / totalComets;
}
// Animation loop
@ -414,9 +424,11 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
}
// Draw comet with solid color
colorPack.col[i % colorPack.size];
color = colorPack.col[i % colorPack.size];
for (int j = 0; j < cometSize; j++) {
pos = (cometPositions[i] - j + size) % size;
// Tail follows the direction of movement
pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j);
pos = (pos % size + size) % size; // safe modulus
leds[pos] += color;
}
}
@ -439,7 +451,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 <= 0 || totalDurationMs <= 0) return;
if (!leds || size <= 1 || totalDurationMs <= 0) return;
const int halfSize = size / 2;
const float msPerLed = totalDurationMs / (float)halfSize;
@ -463,7 +475,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 - pos] = fillCol; // Mirror
leds[(size - 1 - i + shift + size) % size] = fillCol; // Correct mirroring calculation
}
// Update LEDs only when necessary
@ -486,35 +498,35 @@ void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COL
unsigned long startTime = millis();
const uint32_t halfTime = timeMs / 2;
uint8_t origBright = FastLED.getBrightness();
const uint8_t origBright = FastLED.getBrightness();
FastLED.setBrightness(255);
uint8_t brightness = MIN_BRIGHTNESS;
uint8_t breath = MIN_BRIGHTNESS;
uint32_t elapsedTime;
CRGB scaledColor, correctedColor;
unsigned long currentTime;
CRGB outColor;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Calculate elapsed time in current breath cycle
// Elapsed time in the current breath cycle
currentTime = millis();
elapsedTime = currentTime - startTime;
// Calculate brightness using a linear approach
// Triangle wave brightness: up for half, down for half
if (elapsedTime < halfTime) {
brightness = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten
breath = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten
} else {
brightness = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim
breath = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim
}
// Scale the color directly
scaledColor = colors.col[colorIndex];
scaledColor.nscale8(brightness * origBright / 255);
// 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);
// Correct the color scale for vision
correctedColor = scaledColor;
correctedColor.nscale8_video(brightness);
// Apply perceptual scaling once with combined brightness
outColor = colors.col[colorIndex];
outColor.nscale8_video(finalBright);
// Fill all LEDs with scaled color
fill_solid(leds, size, correctedColor);
fill_solid(leds, size, outColor);
FastLED.show();
if (elapsedTime >= timeMs) {
@ -534,21 +546,17 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const
CRGB color1, color2;
// Create initial gradient
int segmentLength = size / colors.size;
// Create initial gradient: evenly distribute blends across the strip
// For i in [0..size-1], compute position in color space [0..colors.size)
for (int i = 0; i < size; i++) {
// Determine which color segment we're in
int segment = i / segmentLength;
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
int nextSegment = (segment + 1) % colors.size;
// Calculate blend amount within segment
uint8_t blendPos = map(i % segmentLength, 0, segmentLength - 1, 0, 255);
// Create local copies for blending
// Local copies for blending
color1 = colors.col[segment];
color2 = colors.col[nextSegment];
// Set initial gradient
leds[i] = blend(color1, color2, blendPos);
}

View File

@ -4,8 +4,10 @@
#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;
@ -20,9 +22,12 @@ Version otaVersion;
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
: localVersion(localVersion), bucketUrl(bucket), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
: localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
{
ESP_LOGI(TAG, "AppUpdater initialized with version %s", localVersion.toString().c_str());
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
// Ensure baseUrl ends with a single '/'
if(!baseUrl.endsWith("/")) baseUrl += "/";
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
}
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
@ -37,7 +42,7 @@ void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const ch
}
bool AppUpdater::checkManifest() {
String url = String(bucketUrl) + manifestName;
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and Send GET request for manifest
@ -90,7 +95,8 @@ bool AppUpdater::checkManifest() {
// Check if an update is available
updateAvailable = false;
if (otaVersion < localVersion) {
// Only mark update available if remote is strictly newer than local
if (otaVersion <= localVersion) {
ESP_LOGI(TAG, "No updates available");
return false;
}else{
@ -107,7 +113,7 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
// Construct full URL
String url = String(bucketUrl) + remotePath;
String url = buildUrl(remotePath);
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
String localMd5 = getLocalMD5(localPath);
@ -163,7 +169,8 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
// Single pass: Save file and calculate MD5
if (contentLength > 0) {
// Single pass with known content length
while (totalRead < contentLength) {
size_t available = stream->available();
if (available) {
@ -184,6 +191,26 @@ 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();
@ -271,7 +298,7 @@ bool AppUpdater::updateApp() {
// Get the firmware MD5 hash and URL
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
String firmwareUrl = String(bucketUrl) + appName;
String firmwareUrl = buildUrl(appName);
// Download the firmware
HTTPClient http;
@ -286,7 +313,7 @@ bool AppUpdater::updateApp() {
// Check available space
size_t firmwareSize = http.getSize();
if (!Update.begin(firmwareSize)) {
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
ESP_LOGE(TAG, "Firmware: Not enough space for update");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
http.end();
@ -299,6 +326,7 @@ 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));
@ -307,7 +335,6 @@ bool AppUpdater::updateApp() {
// Check for timeout
if (read == 0) {
ESP_LOGE(TAG, "Read timeout");
Update.abort();
http.end();
return false;
@ -325,6 +352,21 @@ 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();
@ -354,6 +396,19 @@ 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) {
@ -410,7 +465,6 @@ void firmwareUpdateTask(void* parameter) {
vTaskDelete(NULL);
}
void startVersionCheckTask() {
if(versionCheckTask_Handle != NULL) {
ESP_LOGW(TAG, "Version Check Tak already running");
@ -453,8 +507,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());
@ -470,6 +524,7 @@ 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");
@ -479,23 +534,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%%", message, percentage);
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::VERIFYING:
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", message, percentage);
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SKIPPED:
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", message);
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SAVED:
snprintf(buffer, sizeof(buffer), "%s: File Saved", message);
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::MD5_FAILED:
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", message);
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::COMPLETE:
@ -504,7 +559,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
isComplete = true;
break;
case AppUpdater::UpdateStatus::ERROR:
snprintf(buffer, sizeof(buffer), "Error!: %s", message);
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
msg = buffer;
break;
default:

View File

@ -3,14 +3,24 @@
#include "esp_log.h"
#include "WiFi.h"
#include "ATALights.h"
#include "BleSettings.h"
static const char *tag = "BLE_SP110E";
#define BT_SERVICE "FFE0"
#define BT_SP110E_CHARACTERISTIC "FFE1"
//#ifndef BT_SERVICE
// #define BT_SERVICE "FFE0"
//#endif
//#ifndef BT_SP110E_CHARACTERISTIC
// #define BT_SP110E_CHARACTERISTIC "FFE1"
//#endif
NimBLECharacteristic *pSP110ECharacteristic = nullptr;
#define BT_STICK_CHARACTERISTIC "FFE2"
//#ifndef BT_STICK_CHARACTERISTIC
// #define BT_STICK_CHARACTERISTIC "FFE2"
//#endif
NimBLECharacteristic *pStickCharacteristic = nullptr;
NimBLEClient* pStickClient;
@ -55,7 +65,11 @@ 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);
@ -254,11 +268,11 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
led_status.count_lsb = 20;
// Create BLE Service
NimBLEService *pService = pServer->createService( BT_SERVICE );
NimBLEService *pService = pServer->createService( BTServiceUUID.c_str() ); // Use the defined service UUID
// Create FFE1 Characteristic with WRITE and NOTIFY properties
pSP110ECharacteristic = pService->createCharacteristic(
BT_SP110E_CHARACTERISTIC,
BTSP110ECharacteristicUUID.c_str(),
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
);
@ -268,7 +282,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
/************* Light Stick Characteristic ***************/
pStickCharacteristic = pService->createCharacteristic(
BT_STICK_CHARACTERISTIC,
BTStickCharacteristicUUID.c_str(),
NIMBLE_PROPERTY::NOTIFY
);
@ -284,7 +298,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
// Configure Advertising
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID( BT_SERVICE ); // Advertise the FFE0 service UUID
pAdvertising->addServiceUUID( BTServiceUUID.c_str() ); // 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)));
@ -329,13 +343,13 @@ void BLE_LightStick_Client_Task(void *parameter) {
ESP_LOGI(tag, "Connected to the server");
// Get the service.
NimBLERemoteService* pRemoteService = pStickClient->getService(BT_SERVICE);
NimBLERemoteService* pRemoteService = pStickClient->getService(BTServiceUUID.c_str());
if (pRemoteService == nullptr) {
ESP_LOGE(tag, "Failed to find service UUID: %s", BT_SERVICE);
ESP_LOGE(tag, "Failed to find service UUID: %s", BTServiceUUID.c_str());
pStickClient->disconnect();
} else {
// Get the characteristic.
pRemoteCharacteristic = pRemoteService->getCharacteristic(BT_STICK_CHARACTERISTIC);
pRemoteCharacteristic = pRemoteService->getCharacteristic(BTStickCharacteristicUUID.c_str());
if (pRemoteCharacteristic != nullptr && pRemoteCharacteristic->canNotify()) {
pRemoteCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pRemoteCharacteristic,
uint8_t* pData, size_t length, bool isNotify) {

View File

@ -4,12 +4,13 @@
#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;
@ -35,20 +36,36 @@ 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;
deserializeJson(doc, value.substr(13));
DeserializationError err = deserializeJson(doc, value.substr(jsonStart));
if (err) {
ESP_LOGW(tag, "JSON parse error for wifi-connect: %s", err.c_str());
return;
}
JsonObject wifiJson = doc.as<JsonObject>();
String ssid = wifiJson["ssid"].as<String>();
String pass = wifiJson["pass"].as<String>();
ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str());
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());
bool status = StartWifiConnectTask(ssid, pass);
if(status == true){
bool started = StartWifiConnectTask(ssid, pass);
if (started) {
updatePacket.wifiStatus = WIFI_DISCONNECTED;
updatePacket.wifiOnline = false;
updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0;
} else {
ESP_LOGI(tag, "Failed to start WiFi connection task");
ESP_LOGW(tag, "Failed to start WiFi connection task");
}
}
else if (value.compare("version-check") == 0) { // Check if new version is available
@ -75,21 +92,18 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
updatePacket.wifiOnline = InternetAvailable;
if (WiFi.status() == WL_CONNECTED) {
updatePacket.wifiStatus = WIFI_CONNECTED;
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];
}
IPAddress ip = WiFi.localIP();
updatePacket.wifiIP[0] = ip[0];
updatePacket.wifiIP[1] = ip[1];
updatePacket.wifiIP[2] = ip[2];
updatePacket.wifiIP[3] = ip[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){
@ -99,19 +113,24 @@ 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 Char read");
ESP_LOGI(tag, "Upgrade status read");
}
}
};
void bleUpgrade_send_message(String s){
if(pUpgradeCharacteristic2){
if (s != nullptr) {
pUpgradeCharacteristic2->setValue(s);
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) {
pUpgradeCharacteristic2->notify();
} else {
ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message");
}
}
}
@ -120,10 +139,10 @@ void bleUpgrade_send_message(String s){
void Init_UpgradeBLEService(NimBLEServer *pServer){
// Create Upgrade BLE Service
pUpgradeService= pServer->createService( UPGRADE_SERVICE_UUID );
pUpgradeService= pServer->createService( BTUpgradeServiceUUID.c_str() );
pUpgradeCharacteristic1 = pUpgradeService->createCharacteristic(
UPGRADE_CHARACTERISTIC1_UUID,
BTUpgradeCharacteristic1UUID.c_str(),
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
);
@ -133,7 +152,7 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic(
UPGRADE_CHARACTERISTIC2_UUID,
BTUpgradeCharacteristic2UUID.c_str(),
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
@ -144,7 +163,7 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
pUpgradeService->start();
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID( UPGRADE_SERVICE_UUID ); // Advertise service UUID
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
}

View File

@ -2,6 +2,7 @@
#include "esp_log.h"
#include "BLE_SP110E.h"
#include "BLE_UpdateService.h"
#include "BleSettings.h"
static const char* tag = "BleServer";
@ -13,18 +14,22 @@ 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) {
@ -32,8 +37,9 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
static bool isInitialized = false;
if (!isInitialized) {
NimBLEDevice::init("ATALIGHTS");
NimBLEDevice::init(BTDeviceName.c_str());
//NimBLEDevice::setMTU(247); // Set preferred MTU size (max 247 for BLE)
isInitialized = true;
}
NimBLEServer *pServer = NimBLEDevice::createServer();
@ -58,7 +64,7 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
// Start BLE advertising
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
if (pAdvertising != nullptr) {
if (!pAdvertising->start()) {
if (!pAdvertising->isAdvertising() && !pAdvertising->start()) {
ESP_LOGE(tag, "Failed to start advertising");
return;
}

69
src/BleSettings.cpp Normal file
View File

@ -0,0 +1,69 @@
#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());
}

View File

@ -3,7 +3,10 @@
#include <esp_log.h>
template <typename T>
void logConstrainedValue(const char* tag, const char* key, T value);
void logConstrainedValue(const char* tag, const char* key, T) {
// Generic fallback when no specialization provided
ESP_LOGD(tag, "Key [%s] value set", key);
}
template <>
void logConstrainedValue<int>(const char* tag, const char* key, int value) {
@ -16,7 +19,10 @@ void logConstrainedValue<float>(const char* tag, const char* key, float value) {
}
template <typename T>
void logClamping(const char* tag, const char* key, T value, T limit, const char* condition);
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);
}
template <>
void logClamping<int>(const char* tag, const char* key, int value, int limit, const char* condition) {
@ -72,12 +78,12 @@ const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, con
}
String value = jsonObject[key].as<String>();
if (value.isEmpty()) {
if (value.length() == 0) {
ESP_LOGW(tag, "Key [%s] value is empty. Using default value.", key);
return strdup(def);
}
ESP_LOGD(tag, "Key [%s] value: %s", key, value);
ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str());
return strdup(value.c_str());
}
@ -93,7 +99,7 @@ String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const
String value = jsonObject[key].as<String>();
// Check if the value is empty
if (value.isEmpty()) {
if (value.length() == 0) {
ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str());
return def;
}

View File

@ -1,5 +1,6 @@
#include "PWM_Output.h"
#include <Arduino.h>
#include "global.h"
const char* tag = "pwmout";
@ -56,6 +57,11 @@ 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;
@ -77,8 +83,9 @@ void PWM_Output::setResolution(uint8_t res){
if(this->res < 4) this->res = 4;
if(this->res > 16) this->res = 16;
this->standardFactor = binaryPow[res] * 0.01;
this->visionFactor = binaryPow[res] * 0.0001;
// Use the clamped resolution when computing factors
this->standardFactor = binaryPow[this->res] * 0.01f;
this->visionFactor = binaryPow[this->res] * 0.0001f;
ESP_LOGD(tag, "factor=%f, vision=%f", this->standardFactor, this->visionFactor);
}

View File

@ -3,6 +3,14 @@
#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 ************************/
@ -11,59 +19,51 @@ HUE_PALLET_DISPENSER::HUE_PALLET_DISPENSER(void){
}
void HUE_PALLET_DISPENSER::Initialize(float hue, float range, int 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;
}
// Clamp and normalize inputs
this->range = constrain(range, 0.0f, (float)MAX_HUE);
this->hueSteps = (colSteps < 0) ? 0 : colSteps;
this->startHue = wrapHue(hue - (this->range * 0.5f));
this->hueIndex = 0;
if (this->hueSteps <= 1) {
this->hueIncrement = 0.0;
this->currHue = hue;
this->hueIncrement = 0.0f;
this->currHue = wrapHue(hue);
} else {
this->hueIncrement = this->range / (this->hueSteps - 1);
this->hueIncrement = (this->range / (this->hueSteps - 1));
this->currHue = this->startHue;
}
}
int HUE_PALLET_DISPENSER::GetNextPalletHue(void){
if (this->hueSteps > 1) {
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;
this->currHue = wrapHue(this->startHue + (this->hueIndex * this->hueIncrement));
this->hueIndex = (this->hueIndex + 1) % this->hueSteps;
}
// 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);
}
// 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;
}
float HUE_PALLET_DISPENSER::PeekNextPalletHue(int hueOffset){
float tempHue = this->startHue + (this->hueIndex + hueOffset) * this->hueIncrement;
if(tempHue < 0){
tempHue += MAX_HUE;
}else if(tempHue > MAX_HUE){
tempHue -= MAX_HUE;
}
float tempHue = wrapHue(this->startHue + ((this->hueIndex + hueOffset) * this->hueIncrement));
return tempHue;
}
void HUE_PALLET_DISPENSER::SetHueIndex(int hueIndex){
this->currHue = 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;
}
}

View File

@ -1,5 +1,5 @@
#include "global.h"
#include <Wifi.h>
#include <WiFi.h>
#include <FS.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
@ -45,7 +45,6 @@ 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%
@ -257,6 +256,12 @@ 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++) {

View File

@ -33,6 +33,7 @@
#include "ATALights.h"
#include "OnEveryN.h"
#include "BLE_SP110E.h"
#include "BleSettings.h"
#define FREERTOs_DIAGNOSTICS 0
#define OLED_ENABLED 0
@ -69,6 +70,8 @@ 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);
@ -93,8 +96,7 @@ void setup()
{
// Serial Port
Serial.begin(115200);
while (!Serial)
;
while (!Serial);
// Initialize I2C Port for TSensor, ...
Wire.begin(I2C_SDA1_Pin, I2C_SCL1_Pin);
@ -123,6 +125,10 @@ 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);
@ -144,24 +150,30 @@ 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)
{
ESP_LOGW(tag, "Enabling BLE and Update Service");
setStatusPin1(true);
UpgradeMode = true;
ESP_LOGE(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
}
@ -198,7 +210,14 @@ void loop()
// Button Scanning
ON_EVERY_N_MILLISECONDS(10)
{
if (boardButtons[0] != NULL)
for (int i = 0; i < 3; i++)
{
if (boardButtons[i] != NULL)
{
boardButtons[i]->tick();
}
}
/*
{
boardButtons[0]->tick();
}
@ -210,6 +229,7 @@ void loop()
{
boardButtons[2]->tick();
}
*/
}
// Temperature Monitor
@ -232,10 +252,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
@ -280,7 +300,15 @@ void loop()
{
static bool ledState = false;
// digitalWrite(sys_settings.boardPins.stat[1], ledState = !ledState);
setStatusPin2(ledState = !ledState)
setStatusPin2(ledState = !ledState);
}
}
// Upgrade Mode Tune
if(UpgradeMode){
ON_EVERY_N_MILLISECONDS(5000)
{
Buzzer_Play_Tune(TUNE_THUMP, true, true);
}
}
@ -292,12 +320,15 @@ 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);
}
}
}
}

View File

@ -12,13 +12,29 @@ static const char* tag = "board";
BOARD_PINS* thisBoardPins;
void Load_Board_Pins(BOARD_PINS& boardPins, String& path){
// 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));
thisBoardPins = &boardPins;
File file = LittleFS.open(path);
if (!file) {
ESP_LOGE(tag, "Error opening %s...", path.c_str());
return;
return false;
}
JsonDocument doc;
@ -27,7 +43,7 @@ void Load_Board_Pins(BOARD_PINS& boardPins, String& path){
if (error) {
ESP_LOGE(tag, "%s deserialize error!..", path.c_str());
return;
return false;
}
JsonObject boardJson = doc.as<JsonObject>();
@ -60,9 +76,28 @@ void Load_Board_Pins(BOARD_PINS& boardPins, String& path){
boardPins.rf433tx = jsonConstrain<int>(tag, boardJson, "rf433tx", -1, 48, -1);
boardPins.rf433rx = jsonConstrain<int>(tag, boardJson, "rf433rx", -1, 48, -1);
// TODO Add hardware version to log
ESP_LOGI(tag, "loaded Pins from %s", path.c_str());
// 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);
ESP_LOGI(tag, "loaded Pins from %s", path.c_str());
return true;
}

View File

@ -9,16 +9,28 @@ 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]);
}
}
/*
@ -62,6 +74,14 @@ 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);

View File

@ -35,30 +35,45 @@ void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority)
static int prev_tune = -1;
if (buzzPin < 0) return;
if (hasPriority || !anyrtttl::nonblocking::isPlaying()) {
if (anyrtttl::nonblocking::isPlaying()) {
anyrtttl::nonblocking::stop();
// 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;
}
// Load nonblocking if different from previous
if (prev_tune != tune && async) {
anyrtttl::nonblocking::begin(buzzPin, buzzTune[tune].melody.c_str());
}
ESP_LOGD(tag, "Playing tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str());
for (int c = 0; c < buzzTune[tune].cycles; c++) {
// Async mode: begin once, then caller should periodically call again to advance playback
if (async) {
anyrtttl::nonblocking::play();
} else {
anyrtttl::blocking::play(buzzPin, buzzTune[tune].melody.c_str());
bool playing = anyrtttl::nonblocking::isPlaying();
if (hasPriority && playing) {
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;
}
prev_tune = tune;
} else {
ESP_LOGD(tag, "buzzer busy");
// 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
}
yield(); // allow other tasks to run
}
prev_tune = tune;
}
// TODO Buzzer Beep finish

View File

@ -1,38 +1,77 @@
#include "my_tsensor.h"
#include <Temperature_LM75_Derived.h>
#include "global.h"
static const char* tag = "tsensor";
T_SENSOR tSensorSettings;
TI_TMP102_Compatible *tSensor;
TI_TMP102_Compatible *tSensor = nullptr;
/******************* Temperature Control ********************/
void Init_TSensor(uint8_t addr) {
//tSensor = new TI_TMP102_Compatible(72);
// Initialize the temperature sensor once with the provided I2C address
if (tSensor == nullptr) {
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 < (tSensorSettings.Setpoint2 - tSensorSettings.hyst))) {
newDuty = tSensorSettings.fanPower1;
if ((FanState == 2) && (temperature < (sp2 - hyst))) {
newDuty = fp1;
FanState = 1;
//ESP_LOGD(tag, "Dropping down to FanPower1");
}
else if ((FanState == 1) && (temperature < (tSensorSettings.Setpoint1 - tSensorSettings.hyst))) {
else if ((FanState == 1) && (temperature < (sp1 - hyst))) {
newDuty = 0;
FanState = 0;
//ESP_LOGD(tag, "Dropping down to FanPower0");
}
else if ((FanState <= 1) && (temperature > tSensorSettings.Setpoint1)) {
newDuty = tSensorSettings.fanPower1;
if (temperature > tSensorSettings.Setpoint2) {
newDuty = tSensorSettings.fanPower2;
else if ((FanState <= 1) && (temperature > sp1)) {
newDuty = fp1;
if (temperature > sp2) {
newDuty = fp2;
FanState = 2;
//ESP_LOGD(tag, "Raising up to FanPower2");
} //else {
@ -43,6 +82,6 @@ TI_TMP102_Compatible *tSensor;
// Apply new duty cycle if changed
if (currentDuty != newDuty) {
pwmOut->setOutput(newDuty);
ESP_LOGD(tag, "Board T: %.2f, Fan -> %.2f", temperature, newDuty);
ESP_LOGD(tag, "Board T: %.2f F, Fan -> %.2f (state=%u)", temperature, newDuty, FanState);
}
}

View File

@ -20,7 +20,6 @@
static const char *tag = "WIFI";
volatile bool WifiClientConnected = false;
volatile bool InternetAvailable;
AsyncWebServer webServer(80);
AsyncEventSource eventUpgradeProgress("/upgrade-progress");
@ -125,7 +124,6 @@ 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
@ -148,9 +146,8 @@ void Wifi_ConnectTask(void *parameter)
static const char *tag = "Wifi_Task";
wifi_task_running = true;
if (!WifiClientConnected || client_ssid != WiFi.SSID())
if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
{
WifiClientConnected = false;
ESP_LOGD(tag, "Connecting to: %s", client_ssid.c_str());
// Disconnect and connect to new network
@ -181,7 +178,6 @@ 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"))
@ -391,7 +387,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(WifiClientConnected ? "Connected" : "Disconnected") +
String jsonStr = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") +
"\",\"ip\":\"" + WiFi.localIP().toString() + "\"}";
request->send(200, "application/json", jsonStr); });
server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request)
@ -547,7 +543,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
// System and LED related handlers
server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request)
{
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request)
{
@ -557,25 +553,25 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
serializeJson(jsDoc, summary);
request->send(200, "application/json", summary);
*/
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request)
{
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
// LightStik related handlers
server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request)
{
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request)
{
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request)
{
String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}";
String response = "{\"status\":\"" + String(WiFi.status() == WL_CONNECTED ? "Connected" : "Disconnected") + "\"}";
request->send(200, "application/json", response); });
// Firmware Update Handlers
@ -934,7 +930,6 @@ 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);
@ -943,14 +938,12 @@ 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:
@ -1036,38 +1029,82 @@ bool writeFile(fs::FS &fs, const char *path, const char *message)
return false;
}
// Open file with error checking
File file = fs.open(path, "w");
if (!file)
// Normalize and validate path
String finalPath(path);
if (!finalPath.startsWith("/"))
{
ESP_LOGE(tag, "Failed to open file: %s", path);
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());
return false;
}
// Write with error handling
try
// Write to a temporary file first for atomicity
String tmpPath = finalPath + ".tmp";
File tmp = fs.open(tmpPath.c_str(), "w");
if (!tmp)
{
size_t bytesWritten = file.print(message);
if (bytesWritten == 0)
{
ESP_LOGE(tag, "Failed to write to file: %s", path);
file.close();
ESP_LOGE(tag, "Failed to open temp file: %s", tmpPath.c_str());
return false;
}
// Ensure all data is written
file.flush();
file.close();
ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path);
// 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());
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)
{

View File

@ -313,7 +313,7 @@ void Wifi_Save_Credentials(String newSSID, String newPass)
}
// Parse the JSON file
DynamicJsonDocument doc(1024);
JsonDocument doc;
DeserializationError error = deserializeJson(doc, credsFile);
if(!error){
JsonObject cred1Json = doc.createNestedObject("cred1");
@ -675,7 +675,8 @@ 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);
//DynamicJsonDocument profJson(4 * 1024);
JsonDocument profJson;
DeserializationError error = deserializeJson(profJson, postData, total);
if(!error){
@ -711,7 +712,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
}
}
else if(!strcmp(dataType, "play-anim")){
DynamicJsonDocument animData(1024);
JsonDocument animData;
DeserializationError error = deserializeJson(animData, postData, total);
if(!error){
ANIMATION_EVENT testEvent;
@ -765,7 +766,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
getpostSuccess = true;
}
else if(!strcmp(dataType, "set-pixel")){
DynamicJsonDocument js(1024);
JsonDocument js;
DeserializationError error = deserializeJson(js, postData, total);
if(!error){
ANIMATION_EVENT testEvent;
@ -796,7 +797,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
Buzzer_Beep(50, 3000);
}
else if(!strcmp(dataType, "setup-save")){
DynamicJsonDocument js(1024);
JsonDocument js;
DeserializationError error = deserializeJson(js, postData, total);
if(!error){
// If app index is different open app-events.json and update
@ -808,7 +809,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
return;
}
DynamicJsonDocument doc(2048);
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
@ -846,7 +847,7 @@ void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t
return;
}
DynamicJsonDocument doc(2048);
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();