commit 8-19-25-950

This commit is contained in:
admin 2025-08-19 09:50:41 -07:00
parent 88c2ff3def
commit d5dcf6c0fe
19 changed files with 1144 additions and 88 deletions

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>

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,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

@ -41,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;
@ -68,7 +69,17 @@ class AppUpdater {
* @param bucket Base URL for updates
* @param fs Filesystem reference
*/
AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName ="update.json", const char* appBin = "firmware.bin" );
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
@ -104,7 +115,7 @@ class AppUpdater {
* @param manifestPath Path to manifest file
* @return Manifest content as a json document
*/
bool checkManifest(void);
bool checkManifest(void);
bool updateApp(void);
@ -128,15 +139,21 @@ class AppUpdater {
* @param expectedMd5 Expected MD5 hash
* @return true if successful
*/
bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength,
const char* localPath, const char* expectedMd5);
bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength,
const char* localPath, const char* expectedMd5);
/**
* @brief Update progress callback
* @param percentage Progress percentage
* @param newStatus Current status
*/
void updateProgress(UpdateStatus newStatus, int percentage, const char* message = nullptr);
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

@ -22,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)) {
@ -39,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
@ -110,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);
@ -295,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;
@ -393,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) {
@ -491,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());

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;
@ -258,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
);
@ -272,7 +282,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
/************* Light Stick Characteristic ***************/
pStickCharacteristic = pService->createCharacteristic(
BT_STICK_CHARACTERISTIC,
BTStickCharacteristicUUID.c_str(),
NIMBLE_PROPERTY::NOTIFY
);
@ -288,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)));
@ -333,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;
@ -42,7 +43,7 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
return;
}
DynamicJsonDocument doc(512);
JsonDocument doc;
DeserializationError err = deserializeJson(doc, value.substr(jsonStart));
if (err) {
ESP_LOGW(tag, "JSON parse error for wifi-connect: %s", err.c_str());
@ -138,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
);
@ -151,7 +152,7 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic(
UPGRADE_CHARACTERISTIC2_UUID,
BTUpgradeCharacteristic2UUID.c_str(),
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
);
@ -162,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";
@ -36,8 +37,7 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
static bool isInitialized = false;
if (!isInitialized) {
//NimBLEDevice::init("ATALIGHTS");
NimBLEDevice::init("SP110E-ATA");
NimBLEDevice::init(BTDeviceName.c_str());
//NimBLEDevice::setMTU(247); // Set preferred MTU size (max 247 for BLE)
isInitialized = true;
}

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

@ -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%

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);
@ -122,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);
@ -143,17 +150,22 @@ 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");
Init_BleServer(true, true);
ESP_LOGW(tag, "Enabling Wifi AP and Client");
@ -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
@ -284,6 +304,14 @@ void loop()
}
}
// Upgrade Mode Tune
if(UpgradeMode){
ON_EVERY_N_MILLISECONDS(5000)
{
Buzzer_Play_Tune(TUNE_THUMP, true, true);
}
}
#if FREERTOs_DIAGNOSTICS
ON_EVERY_N_MILLISECONDS(60000)
{

View File

@ -33,32 +33,47 @@ void Init_Buzzer(int8_t pin, const char* configFile)
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();
}
if (buzzPin < 0) 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++) {
if (async) {
anyrtttl::nonblocking::play();
} else {
anyrtttl::blocking::play(buzzPin, buzzTune[tune].melody.c_str());
}
}
prev_tune = tune;
} else {
ESP_LOGD(tag, "buzzer busy");
// 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) {
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
}
yield(); // allow other tasks to run
}
prev_tune = tune;
}
// TODO Buzzer Beep finish

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:

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();