Compare commits

...

2 Commits

Author SHA1 Message Date
12b5b25081 normal commit 2025-09-04 01:43:17 -07:00
90ef654c80 basic commit 2025-08-25 23:38:53 -07:00
61 changed files with 2756 additions and 14623 deletions

View File

@ -0,0 +1,22 @@
{
"comets":{
"size": 0.2,
"fade-factor1":64,
"max-comets":16
},
"fire":{
"cooling":66,
"sparking":62,
"brightness":255
},
"custom-color-pack1": {
"color1": "#FF0000",
"color2": "#00FF00",
"color3": "#0000FF"
},
"custom-color-pack2": {
"color1": "#FFFF00",
"color2": "#FF00FF",
"color3": "#00FFFF"
}
}

View File

@ -8,6 +8,7 @@
"wifi-ap":{
"ssid": "ATA_AP",
"append-id": true,
"user": "admin",
"pass": "12345678",
"ip": "192.168.10.1",
"gateway": "192.168.10.1",

View File

@ -133,14 +133,28 @@
font-size: 14px;
}
}
/* Tabs */
.tab-bar { display:flex; gap:8px; justify-content:center; margin:12px 0 16px; flex-wrap:wrap; }
.tab-bar button { max-width:none; flex:0 0 auto; background:#6c757d; }
.tab-bar button.active { background:#007bff; }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
</style>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Status Indicators -->
<div class="status-container">
<!-- Tab Buttons -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button>
</div>
<div id="tab-upgrade" class="tab-panel active">
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">BLE Status: ...</label>
</div>
@ -183,21 +197,22 @@
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
</div>
</div> <!-- /tab-upgrade -->
<!-- Wi-Fi Input Fields -->
<div class="input-container">
<div id="tab-wifi" class="tab-panel">
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2>
<div class="input-container">
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
<div style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
<label for="showPassword">Show Password</label>
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" 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" onclick="wifiConnect()" disabled>Connect Wifi</button>
</div>
</div>
<div class="btn-container wifi">
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
</div>
</div><!-- /tab-wifi -->
<script>
(function(){
@ -342,7 +357,7 @@
async function connectToBle(){
if(!navigator.bluetooth){ logMessage('Web Bluetooth not supported.'); return; }
try{
bleDevice = await navigator.bluetooth.requestDevice({
bleDevice = await navigator.bluetooth.requestDevice({
filters:[{ name: el.inDeviceName.value || BLE_SERVER_NAME }],
optionalServices:[BLE_SERVICE_UUID]
});
@ -354,20 +369,21 @@
bleCharacteristic2.addEventListener('characteristicvaluechanged', e => {
try{
const txt = new TextDecoder().decode(e.target.value);
console.log('--> ' + txt);
logMessage('--> ' + txt.trim());
}catch(_){ /* ignore */ }
});
bleConnected=true;
bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
logMessage('Connected to ' + connectedName);
const nameLabel = document.getElementById('ble-device-name');
if(nameLabel){ nameLabel.textContent = 'Device Name: ' + connectedName; }
await readPacket();
updateUI();
}catch(err){
logMessage( err.message.includes('cancel') ? 'Connection cancelled.' : ('Connection failed: '+err.message) );
}
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
logMessage('Connected to ' + connectedName);
const nameLabel = document.getElementById('ble-device-name');
if(nameLabel){ nameLabel.textContent = 'Device Name: ' + connectedName; }
await readPacket();
updateUI();
}catch(err){
logMessage( err.message.includes('cancel') ? 'Connection cancelled.' : ('Connection failed: '+err.message) );
}
}
function onDisconnect(){
@ -457,6 +473,17 @@
el.inDeviceName.value = BLE_SERVER_NAME;
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
// Inline onclicks already wired; ensure functions are in scope
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
btn.classList.add('active');
const id = btn.getAttribute('data-tab');
const panel = document.getElementById(id);
if(panel) panel.classList.add('active');
});
});
updateUI();
logMessage('Ready. Enter device name or use default and press Connect.');
}

View File

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

View File

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

View File

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

View File

@ -1,508 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 15px;
}
.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">Device: ...</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>
// 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 };
let updatePacket = {
wifiConnected: false,
wifiOnline: false,
wifiIP: [0, 0, 0, 0],
currVersion: [0, 0, 0],
newVersion: [0, 0, 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() {
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;
document.getElementById('bleConnectBtn').disabled = true;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
document.getElementById('status-ble-connection').textContent = 'Device: 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}`);
}
}
}
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 && packet.newVersion[0] > packet.currVersion[0] ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] > packet.currVersion[1]) ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] === packet.currVersion[1] && packet.newVersion[2] > packet.currVersion[2])) {
//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.internetAvailable = 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));
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
jsonString = ' {"ssid":"' + ssid + '","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

@ -1,547 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 5px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 4px;
}
.status-indicator-ble {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.btn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
}
button {
flex: 1;
max-width: 130px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background 0.3s ease;
}
button:disabled {
background-color: #ccc;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
textarea {
width: 100%;
height: 300px;
font-size: 14px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px;
}
h1 {
font-size: 20px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">BLE Status: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-wifi"></span>
<label id="status-wifi-client">Wifi Client: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-internet"></span>
<label id="status-internet">Internet: ...</label>
</div>
<div class="status-container">
<label id="status-current-version">Curr Version: ...</label>
</div>
<div class="status-container">
<label id="status-new-version">New Version: ...</label>
</div>
<div class="btn-container">
<label id="ble-device-name">Device Name:</label>
</div>
<div class="btn-container">
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
</div>
<!-- Buttons -->
<div class="btn-container">
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
</div>
<!-- Log Area -->
<textarea id="logArea" readonly></textarea>
<div class="btn-container">
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
</div>
<!-- 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" onclick="togglePasswordVisibility()" 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" onclick="wifiConnect()" 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 to automatically start the connection process when the page loads
function autoStart() {
document.getElementById('input-DeviceName').value = BLE_SERVER_NAME;
}
// Call autoStart when the page loads
window.addEventListener('DOMContentLoaded', autoStart);
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: document.getElementById('input-DeviceName').value }],
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}`);
}
}
}
async function wifiConnect() {
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.');
}
}
async function checkStatus() {
if (bleCharacteristic1) {
await readPacket();
processUpdatePacket(updatePacket);
} else {
logMessage('BLE device not connected.');
}
}
async function checkVersion() {
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");
}
}
async function startUpgrade() {
try {
await sendPacket('upgrade-start');
logMessage("Upgrade Starting... Please wait.");
} catch (error) {
logMessage(`Error starting upgrade: ${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;
logMessage("Latest Update is: " + packet.newVersion.join('.'));
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);
}
// Event listeners for buttons
function togglePasswordVisibility() {
const passwordInput = document.getElementById('wifipassword');
passwordInput.type = this.checked ? 'text' : 'password';
}
// Initial call to process the update packet with defaults
processUpdatePacket(updatePacket);
})();
</script>
</body>
</html>

View File

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

View File

@ -1,79 +0,0 @@
body {
/*background-color: #f7f7f7;*/
font-family: Tahoma, Arial, sans-serif;
font-size: small;
margin: 0;
display: flex;
min-height: 100vh;
}
.main-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 20px;
}
h1 {
margin-top: 0;
}
input{
cursor:pointer;
}
input[type="number"]{
width: 100px;
}
#submit {
width:120px;
}
select{
width:160px;
}
#select-path {
width:250px;
}
#select-dir {
width:90px;
}
#spacer-50 {
height: 50px;
}
#spacer-20 {
height: 20px;
}
#spacer-10 {
height: 10px;
}
table {
/*background-color: #dddddd;*/
border-collapse: collapse;
width:600px;
margin: 0 auto;
overflow: visible;
}
td, th {
/*border: 1px solid #dddddd;*/
text-align: left;
padding: 2px;
}
#first_td_th {
width:400px;
}
fieldset {
width:620px;
/*background-color: #f7f7f7;*/
border-radius: 10px;
border-color: blue;
}
#format-notice {
color: #ff0000;
}
legend {
display: flex;
justify-content: center;
background-color:white;
background-blend-mode: darken;
border-radius: 10px;
padding: 1px 8px 2px 8px;
border-style: solid;
border-width: 1.0;
}

View File

@ -1,72 +0,0 @@
.navbar {
width: 100%;
background-color: black;
border-bottom: 2px solid white;
display: flex;
justify-content: flex-start;
align-items: center;
position: sticky;
top: 0;
z-index: 1000;
}
.navbar-left {
padding: 0 10px;
}
.navbar ul {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
position: relative; /* Ensure relative positioning for the submenu */
}
.navbar ul li {
float: left;
position: relative; /* Ensure relative positioning for the submenu */
}
.navbar ul li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
border-right: 1px solid white;
}
.navbar ul li a:hover {
background-color: grey;
}
.navbar ul li a.disabled {
color: grey;
pointer-events: none;
}
.navbar ul .submenu {
display: none;
position: absolute;
top: 100%;
left: 0;
background-color: black;
list-style-type: none;
margin: 0;
padding: 0;
border-top: 2px solid white;
z-index: 1000;
}
.navbar ul .submenu li {
float: none;
border-right: none;
}
.navbar ul .submenu li a {
padding: 10px 16px;
border-bottom: 1px solid white;
}
.navbar ul .submenu li a:hover {
background-color: grey;
}
.navbar ul li:hover > .submenu {
display: block;
}
.nav-image {
height: 40px;
width: auto;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,472 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Light Stick Reg</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 15px;
}
.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: 97%;
height: 100px;
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 Flash-Stick Link/Registration</h1>
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">Master: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-registration"></span>
<label id="status-registration">Registration: ...</label>
</div>
<div class="btn-container">
<button id="bleConnectBtn">Connect</button>
<button id="bleDisconnectBtn">Disconnect</button>
<button id="bleSaveBtn">Save</button>
</div>
<textarea id="logArea" readonly></textarea>
<script>
// 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 };
let updatePacket = {
name: "",
};
// 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() {
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;
document.getElementById('bleConnectBtn').disabled = true;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
document.getElementById('status-ble-connection').textContent = 'Device: 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}`);
}
}
}
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 && packet.newVersion[0] > packet.currVersion[0] ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] > packet.currVersion[1]) ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] === packet.currVersion[1] && packet.newVersion[2] > packet.currVersion[2])) {
//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.internetAvailable = 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));
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
jsonString = ' {"ssid":"' + ssid + '","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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,442 +0,0 @@
class EventBox extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.outer-container {
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
border: 1px solid #ccc;
padding: 5px 20px 5px 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
width:90%;
}
.select-container {
margin-bottom: 15px;
}
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.row > * {
flex: 0 0 calc(50% - 20px);
margin-bottom: 15px;
}
.row:last-child {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
label {
display: block;
margin-bottom: 1px;
}
select, input[type="range"], input[type="color"] {
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}
select{
width: 90%;
font-size: large;
height: auto;
}
input[type="color"] {
height: 30px;
padding: 0;
width: 30px;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: left;
}
.try-button-container {
display: flex;
justify-content: right;
width: 100%;
}
button {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.center-text {
text-align: center;
font-size: larger;
font-weight: bold;
}
input[type="checkbox"] {
transform: scale(2.5);
margin-left: 20px;
}
input[type="checkbox"].css-checkbox + label.css-label {
display: inline-block;
margin-left: 30px;
}
.label-check{
margin-left: 10px;
}
</style>
<div class="outer-container">
<div class="container">
<legend class="center-text" id="center-text">Event0</legend><br>
<div class="row">
<div>
<label for="animation-list" id="list-label">Animations:</label>
<select id="animation-list">
</select>
</div>
<div>
<hue-select id="hue-selector"></hue-select>
</div>
</div>
<div class="row">
<div>
<label for="speed" id="speed-label">Speed:</label>
<input type="range" id="speed" min="1" max="10">
</div>
<div>
<label for="huerange" id="huerange-label">Hue Range:</label>
<input type="range" id="huerange" min="0" max="100">
</div>
</div>
<div class="row">
<div>
<label for="param1" id="param1-label">Param1:</label>
<input type="range" id="param1" min="0" max="50">
</div>
<div>
<label for="param2" id="param2-label">Param2:</label>
<input type="range" id="param2" min="0" max="100">
</div>
</div>
<div class="row">
<div class="checkbox-container">
<input type="checkbox" id="check1">
<label class="label-check" id="check1-label">Check1</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="check3">
<label class="label-check" id="check3-label">Check3</label>
</div>
</div>
<div class="row">
<div class="checkbox-container">
<input type="checkbox" id="check2">
<label class="label-check" id="check2-label">Check2</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="check4">
<label class="label-check" id="check4-label">50% Lum</label>
<div class="try-button-container">
<button id="try-button">Try</button>
</div>
</div>
</div>
</div>
</div>
`;
this.Title = this.shadowRoot.querySelector('#center-text');
this.hueSelect = this.shadowRoot.querySelector('#hue-selector');
this.AnimationList = this.shadowRoot.getElementById('animation-list');
this.Speed = this.shadowRoot.getElementById('speed');
this.HueRange = this.shadowRoot.querySelector('#huerange');
this.Param1 = this.shadowRoot.querySelector('#param1');
this.Param2 = this.shadowRoot.querySelector('#param2');
this.Check1 = this.shadowRoot.querySelector('#check1');
this.Check2 = this.shadowRoot.querySelector('#check2');
this.Check3 = this.shadowRoot.querySelector('#check3');
this.Check4 = this.shadowRoot.querySelector('#check4');
this.AnimationListLabel = this.shadowRoot.querySelector('#list-label');
this.SpeedLabel = this.shadowRoot.querySelector('#speed-label');
this.HueRangeLabel = this.shadowRoot.querySelector('#huerange-label');
this.Param1Label = this.shadowRoot.getElementById('param1-label');
this.Param2Label = this.shadowRoot.getElementById('param2-label');
this.Check1Label = this.shadowRoot.getElementById('check1-label');
this.Check2Label = this.shadowRoot.getElementById('check2-label');
this.Check3Label = this.shadowRoot.getElementById('check3-label');
this.Check4Label = this.shadowRoot.getElementById('check4-label');
this.AnimationPropsJson;
this.SpeedCaption = "";
this.HueRangeCaption = "";
this.Param1Caption = "";
this.Param2Caption = "";
this.Check1Caption = "";
this.Check2Caption = "";
this.Check3Caption = "";
this.setSpeedCaption(`Speed: `);
this.Speed.min = 0;
this.Speed.max = 100;
this.setHueRangeCaption(`Hue Range: `);
this.HueRange.min = 0;
this.HueRange.max = 360;
this.setParam1Caption(`Param1: `);
this.Param1.min = 0;
this.Param1.max = 100;
this.setParam2Caption(`Param2: `);
this.Param2.min = 0;
this.Param2.max = 100;
this.setCheck1Caption(`Check1`);
this.setCheck2Caption(`Check2`);
this.setCheck3Caption(`Check3`);
this.setCheck4Caption(`50% Lum`);
this.Index = 0;
this.Speed.addEventListener('input', () => { this.updateSpeedLabel(); });
this.HueRange.addEventListener('input', () => { this.updateHueRangeLabel(); });
this.Param1.addEventListener('input', () => { this.updateParam1Label(); });
this.Param2.addEventListener('input', () => { this.updateParam2Label(); });
this.TryButton = this.shadowRoot.querySelector('#try-button');
this.TryButton.addEventListener('click', () => {
this.handleTryButtonClick();
});
this.AnimationList.addEventListener('change', this.handleAnimationListChange.bind(this));
}
setIndex(i){ this.Index = i; }
getIndex(){ return this.Index; }
updateSpeedLabel(){
if(!this.Speed.hidden){
this.SpeedLabel.innerHTML = this.SpeedCaption + this.getSpeedValue();
}
}
updateHueRangeLabel(){
if(!this.HueRange.hidden){
this.HueRangeLabel.innerHTML = this.HueRangeCaption + this.getHueRangeValue();
}
}
updateParam1Label(){
if(!this.Param1.hidden){
this.Param1Label.innerHTML = this.Param1Caption + this.getParam1Value();
}
}
updateParam2Label(){
if(!this.Param2.hidden){
this.Param2Label.innerHTML = this.Param2Caption + this.getParam2Value();
}
}
setTitle(text){
this.Title.textContent = text;
}
addOptionToList(value, text){
const optionElement = document.createElement('option');
optionElement.value = value;
optionElement.textContent = text;
this.AnimationList.appendChild(optionElement);
}
setHidden(hidden){ this.hidden = hidden; }
getHidden(){ return this.hidden; }
setHueValue(value){ this.hueSelect.setHue(value); }
getHueValue(){ return this.hueSelect.getSelectedHue(); }
setSpeedValue(value){
this.Speed.value = value;
this.updateSpeedLabel();
}
setSpeedCaption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.SpeedCaption = caption;
this.SpeedLabel.innerHTML = caption;
this.Speed.hidden = hid;
this.SpeedLabel.hidden = hid;
}
getSpeedValue(){ return this.Speed.value; }
setHueRangeValue(value){
this.HueRange.value = value;
this.updateHueRangeLabel()
}
setHueRangeCaption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.HueRangeCaption = caption;
this.HueRangeLabel.innerHTML = caption;
this.HueRange.hidden = hid;
this.HueRangeLabel.hidden = hid;
}
getHueRangeValue(){ return this.HueRange.value; }
setParam1Value(value){
this.Param1.value = value;
this.updateParam1Label()
}
setParam1Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Param1Caption = caption;
this.Param1Label.innerHTML = caption;
this.Param1.hidden = hid;
this.Param1Label.hidden = hid;
}
getParam1Value(){ return this.Param1.value; }
setParam2Value(value){
this.Param2.value = value;
this.updateParam2Label()
}
setParam2Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Param2Caption = caption;
this.Param2Label.innerHTML = caption;
this.Param2.hidden = hid;
this.Param2Label.hidden = hid;
}
getParam2Value(){ return this.Param2.value; }
setCheck1Value(value){ this.Check1.checked = value; }
setCheck1Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Check1Label.innerHTML = caption;
this.Check1.hidden = hid;
this.Check1Label.hidden = hid;
}
getCheck1Value(){ return this.Check1.checked; }
setCheck2Value(value){ this.Check2.checked = value; }
setCheck2Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Check2Label.innerHTML = caption;
this.Check2.hidden = hid;
this.Check2Label.hidden = hid;
}
getCheck2Value(){ return this.Check2.checked; }
setCheck3Value(value){ this.Check3.checked = value; }
setCheck3Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Check3Label.innerHTML = caption;
this.Check3.hidden = hid;
this.Check3Label.hidden = hid;
}
getCheck3Value(){ return this.Check3.checked; }
setCheck4Value(value){ this.Check4.checked = value; }
setCheck4Caption(caption){
let hid = false;
if(caption.trim() === ""){
hid = true;
}
this.Check4Label.innerHTML = caption;
this.Check4.hidden = hid;
this.Check4Label.hidden = hid;
}
getCheck4Value(){ return this.Check4.checked; }
setAnimationIndex(index){
this.AnimationList.selectedIndex = index;
const changeEvent = new Event('change');
this.AnimationList.dispatchEvent(changeEvent);
}
getAnimationIndex(){
return this.AnimationList.selectedIndex;
}
handleTryButtonClick() {
const eventIndexValue = this.getIndex();
const animIndexValue = this.getAnimationIndex();
const hueValue = this.getHueValue();
const speedValue = this.getSpeedValue();
const colorRangeValue = this.getHueRangeValue();
const param1Value = this.getParam1Value();
const param2Value = this.getParam2Value();
const check1Value = this.getCheck1Value();
const check2Value = this.getCheck2Value();
const check3Value = this.getCheck3Value();
const check4Value = this.getCheck4Value();
this.dispatchEvent(new CustomEvent('tryClick', {
detail: {
eventIndex: eventIndexValue,
animIndex: animIndexValue,
hue: hueValue,
speed: speedValue,
colorRange: colorRangeValue,
param1: param1Value,
param2: param2Value,
check1: check1Value,
check2: check2Value,
check3: check3Value,
check4: check4Value
}
}));
}
setAnimationCaptions(propsJson){
this.AnimationPropsJson = propsJson;
//add options to list
let x = 0;
this.AnimationPropsJson.forEach(props => {
this.addOptionToList(x, props.name);
x++;
});
}
handleAnimationListChange(){
const selectedIndex = this.getAnimationIndex();
const selectedProps = this.AnimationPropsJson[selectedIndex];
this.updateControlProps(selectedProps);
}
updateControlProps(props){
this.setSpeedCaption(props.speed);
let s = props['hue-range'];
this.setHueRangeCaption(s || "");
this.setParam1Caption(props.param1 || "");
this.setParam2Caption(props.param2 || "");
this.setCheck1Caption(props.check1 || "");
this.setCheck2Caption(props.check2 || "");
this.setCheck3Caption(props.check3 || "");
this.setCheck4Caption(props.check4 || "");
this.updateSpeedLabel();
this.updateHueRangeLabel();
this.updateParam1Label();
this.updateParam2Label();
}
}
customElements.define('event-box', EventBox);

View File

@ -1,64 +0,0 @@
async function uploadFile(event) {
event.preventDefault();
const file = fileInput.files[0];
if (!file) {
alert("Please select a file.");
return;
}
// Existing file validation code...
const formData = new FormData();
formData.append('file', file);
// Create a custom reader function
const reader = file.stream().getReader();
const totalLength = file.size;
let uploaded = 0;
// Create a readable stream and use the custom reader function
const uploadStream = new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
uploaded += value.length;
let percentUploaded = (uploaded / totalLength * 100).toFixed(2);
// Update progress bar and label
progressBar.value = percentUploaded;
progressLabel.innerHTML = `${percentUploaded}%`;
controller.enqueue(value);
}
controller.close();
},
});
// Create a new Request with the readable stream as body
const req = new Request('/update', {
method: 'POST',
body: uploadStream,
headers: {
// Add any relevant headers here
},
});
try {
const response = await fetch(req);
if (response.ok) {
alert('Upload completed!');
progressLabel.innerHTML = "Completed!";
} else {
alert('An error occurred during the upload.');
progressLabel.innerHTML = "Error!";
}
} catch (error) {
console.error('Upload failed:', error);
}
}

View File

@ -1,244 +0,0 @@
class HueSelect extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.color-option {
display: flex;
align-items: center;
cursor: pointer;
}
.color-patch {
width: 30px;
height: 30px;
margin-top: 18px;
margin-right: 10px;
border: 1px solid #000;
}
.hue-label {
margin-top: 18px;
width: 90px;
text-align: left;
}
.label-container {
display: flex;
align-items: center;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f9f9f9;
min-width: 180px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
max-height: 300px;
overflow-y: auto;
}
.dropdown-content .color-option {
padding: 8px 12px;
}
.dropdown:hover .dropdown-content {
display: block;
}
</style>
<div class="dropdown">
<div class="color-option" id="selectedColor">
<span class="color-patch" style="background-color: hsl(0, 100%, 50%)"></span>
<div class="label-container">
<label class="hue-label" id="hue-label">0</label>
</div>
</div>
<div class="dropdown-content" id="colorPicker">
<!-- The options will be added dynamically using JavaScript -->
</div>
</div>
`;
this.currentHue = 0;
this.colorList;
this.hueLabel = this.shadowRoot.getElementById('hue-label');
const selectedColorDiv = this.shadowRoot.getElementById('selectedColor');
const colorPatch = selectedColorDiv.querySelector('.color-patch');
selectedColorDiv.addEventListener('click', () => {
const dropdownContent = this.shadowRoot.querySelector('.dropdown-content');
dropdownContent.style.display = dropdownContent.style.display === 'block' ? 'none' : 'block';
});
this.createColorOptions();
this.setHue(0);
}
generateColors() {
const colors = [];
for (let hue = 0; hue <= 360; hue += 10) {
if (hue == 360) { hue = 359; }
colors.push(hue);
}
colors.push(-1);
colors.push(-2);
return colors;
}
createColorOption(hue) {
const colorOption = document.createElement('div');
colorOption.classList.add('color-option');
const colorPatch = document.createElement('span');
colorPatch.classList.add('color-patch');
const colorText = document.createElement('span');
colorText.classList.add('color-text');
colorText.textContent = hue;
const rgbHex = document.createElement('span');
rgbHex.classList.add('rgb-hex');
if (hue === -2) {
colorPatch.style.backgroundColor = 'rgb(0,0,0)';
rgbHex.innerHTML = '&nbsp; #000000';
} else if (hue === -1) {
colorPatch.style.backgroundColor = 'rgb(255,255,255)';
rgbHex.innerHTML = '&nbsp; #FFFFFF';
} else {
colorPatch.style.backgroundColor = `hsl(${hue}, 100%, 50%)`;
const hexColor = this.hslToRgb(hue, 100, 50).toUpperCase();
rgbHex.innerHTML = `<span>&nbsp; ${hexColor}</span>`;
}
colorOption.appendChild(colorPatch);
colorOption.appendChild(colorText);
colorOption.appendChild(rgbHex);
colorOption.addEventListener('click', () => this.handleColorSelection(hue));
return colorOption;
}
createColorOptions() {
const dropdownContent = this.shadowRoot.querySelector('.dropdown-content');
this.colorList = this.generateColors();
this.colorList.forEach(hue => {
const colorOption = this.createColorOption(hue);
dropdownContent.appendChild(colorOption);
});
}
// Function to convert HSL to RGB
hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = (x) => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
handleColorSelection(hue) {
this.currentHue = hue;
const selectedColorDiv = this.shadowRoot.getElementById('selectedColor');
const colorPatch = selectedColorDiv.querySelector('.color-patch');
// Update the color patch and hue label
const rgb = this.getRGBfromHue(hue);
colorPatch.style.backgroundColor = rgb;
this.setHueLabel(hue);
this.hideDropdown();
this.dispatchEvent(new CustomEvent('change', { detail: { hue, rgb } }));
}
// Method to get the hue value of the selected item
getSelectedHue() {
return this.currentHue;
}
getSelectedRGB() {
const selectedColorText = this.shadowRoot.getElementById('selectedColor').textContent.trim();
return parseFloat(selectedColorText);
}
getRGBfromHue(hue){
if(hue == -1){
return '#FFFFFF';
}else if(hue == -2){
return '#000000';
}else{
return this.hslToRgb(hue, 100, 50);;
}
}
// Method to get the RGB value of the selected item
getSelectedRgb() {
const selectedHue = this.getSelectedHue();
return getRGBfromHue(selectedHue);
}
setHue(hue) {
// Round the input hue to the nearest 10th
let roundedHue = hue;
if(hue === -1 || hue === -2){
roundedHue = hue;
}else{
roundedHue = Math.round(hue / 10) * 10;
if (roundedHue >= 360) {
roundedHue = 359;
}else if (roundedHue < 0) {
roundedHue = 0;
}
}
this.currentHue = roundedHue;
this.colorList.forEach(colorVal => {
if (colorVal === roundedHue) {
this.handleColorSelection(roundedHue);
}
});
this.hideDropdown();
}
hideDropdown() {
const dropdownContent = this.shadowRoot.querySelector('.dropdown-content');
dropdownContent.style.display = 'none';
}
setHueLabel(value){
this.hueLabel.textContent = "Hue: " + value;
}
}
customElements.define('hue-select', HueSelect);

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"name": "ATALIGHTS",
"lights-service": "FFE0",
"lights-char": "FFE1",
"stick-char": "FFE2",
"upgrade-service": "abcdef01-2345-6789-1234-56789abcdef0",
"upgrade-char1": "abcdef01-2345-6789-1234-56789abcdef1",
"upgrade-char2": "abcdef02-2345-6789-1234-56789abcdef1"
}

View File

@ -1,5 +0,0 @@
#Setting up /system/system.json
1 - Choose the
modes:
booth, roamer, stick

View File

@ -1,70 +0,0 @@
{
"tunes":
[
{
"cycles": 1,
"pause": 0,
"tune": "Boot:d=8,o=5,b=200:g,g,c6,e6,g6"
},
{
"cycles": 1,
"pause": 0,
"tune": "Shutdown:d=8,o=5,b=200:g6,e6,c6,g"
},
{
"cycles": 1,
"pause": 0,
"tune": "Connected:d=16,o=5,b=240:c,e,g,c6"
},
{
"cycles": 1,
"pause": 0,
"tune": "Disconnected:d=16,o=5,b=160:g,f,e,d,c"
},
{
"cycles": 1,
"pause": 0,
"tune": "Done:d=16,o=5,b=240:g,c6,e"
},
{
"cycles": 1,
"pause":0,
"tune": "Warning:d=16,o=6,b=300:e6,d6,e6,d6"
},
{
"cycles": 1,
"pause": 0,
"tune": "Error:d=16,o=4,b=100:f,f"
},
{
"cycles": 1,
"pause": 0,
"tune": "Blip:d=16,o=6,b=220:c6,g6"
},
{
"cycles": 1,
"pause": 0,
"tune": "Thump:d=8,o=4,b=180:c,c"
},
{
"cycles": 1,
"pause": 0,
"tune": "Ack:d=16,o=5,b=200:c,e,g"
},
{
"cycles": 1,
"pause": 0,
"tune": "Waiting:d=16,o=5,b=112:b"
},
{
"cycles": 1,
"pause": 0,
"tune": "LowBeep:d=4,o=5,b=240:c"
},
{
"cycles": 1,
"pause": 0,
"tune": "HighBeep:d=4,o=5,b=240:b"
}
]
}

View File

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

View File

@ -1,169 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Printio</title>
<link href="/css/nav.css" rel="stylesheet">
<style>
h1 {
margin-bottom: 20px;
}
label, select, #media-sizes, button {
margin: 5px 0;
text-align: center;
}
select {
padding: 3px;
font-size: 1.1em;
}
#media-sizes ul {
list-style-type: none;
padding: 0;
}
#media-sizes li {
display: flex;
align-items: center;
margin-bottom: 5px;
}
#media-sizes input[type="checkbox"] {
transform: scale(1.5);
margin-right: 12px;
}
#media-sizes span {
font-size: 1.2em;
}
button {
background-color: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 1.2em;
cursor: pointer;
}
button:hover {
background-color: darkblue;
}
p {
display: block;
margin-top: 1em;
margin-bottom: 1em;
margin-left: 0;
margin-right: 0;
}
.license-section {
margin-top: 30px;
text-align: center;
}
.license-status {
font-size: 1.1em;
margin-bottom: 15px;
}
.license-input {
margin-top: 10px;
}
.license-input input {
padding: 10px;
width: 200px;
font-size: 1.1em;
margin-right: 10px;
}
.license-input button {
background-color: #4CAF50;
padding: 10px 20px;
font-size: 1.1em;
cursor: pointer;
border-radius: 5px;
border: none;
}
.license-input button:hover {
background-color: #45a049;
}
/* New Styles for Layout */
.content-wrapper {
display: flex;
}
.left-column {
width: 280px;
padding-right: 20px;
box-sizing: border-box;
}
.right-column {
width: 70%;
}
.info-box {
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.license-box {
padding: 15px;
border: 1px solid #ccc;
border-radius: 5px;
}
#p {
margin-left: 10px;
}
</style>
</head>
<body>
<div id="navbar"></div>
<div class="content-wrapper">
<div class="left-column">
<div class="info-box">
<h3>System Info</h3>
<p>Version: {{FIRM_VER}}
<p>CPU: {{info.cpu}}</p>
<p>CPU T: {{info.cpu_t}}</p>
<p>Disk Size: {{FS_TOTAL_BYTES}}</p>
<p>Disk Used: {{FS_USED_BYTES}}</p>
<p>RAM Size: {{RAM_TOTAL_BYTES}}</p>
<p>RAM Used: {{RAM_USED_BYTES}}</p>
</div>
</div>
<div class="right-column">
<h1>About ATA Boothifier</h1>
<p>
ATA Boothifier is photobooth control board designed to work with ATA's line of photobooths and beyond.
</p>
<p>
Warning: Unauthorized reproduction or distribution of this product
is strictly prohibited and may result in severe civil and criminal
penalties.
</p>
</div>
</div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
</body>
</html>

View File

@ -1,191 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Edit file</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<style>
h1{
text-align: center;
margin: 0;
margin-bottom: 10px;
}
body {
font-family: Arial, sans-serif;
padding: 0;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
width: 100%;
max-width: auto;
min-width: 400px;
margin: 0 auto;
}
.outer-container {
justify-content: center;
align-items: center;
padding: 20px;
padding-top: 0;
}
.container {
border: 1px solid #ccc;
padding: 10px; /* Adjust padding */
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
width: 90%;
min-width: 400px;
height: calc(100vh - 120px); /* Adjust for navbar and margins */
margin: auto; /* Center horizontally */
margin-bottom: 20px;
}
.center-text {
text-align: center;
font-size: larger;
font-weight: bold;
margin-bottom: 10;
}
input[type="text"] {
width: 100%;
box-sizing: border-box;
}
.row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: nowrap;
min-width: 400px;
}
.row > * {
margin-bottom: 8px;
}
.row label {
flex: 0 0 auto;
white-space: nowrap;
}
.row div {
display: flex;
flex: 1;
align-items: center;
gap: 10px;
}
input[type="number"], select {
border: 1px solid #ccc;
border-radius: 4px;
width: 50%;
}
button {
background-color: #007bff;
color: #fff;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 20px 5px 10px;
}
textarea {
width: 100%; /* Use full width */
height: calc(100vh - 200px); /* Use vh unit for height */
box-sizing: border-box;
border: 1.5px solid gray;
resize: vertical; /* Allow vertical resizing */
word-wrap: none;
}
</style>
</head>
<body>
<div id="navbar"></div>
<h1>Edit file</h2>
<div class="outer-container">
<div class="container">
<form name="edit-file" action="/files/save" onsubmit="return validateForm()">
<div>
<textarea name="edit-textarea" id="edit-textarea" wrap="off" ></textarea>
</div>
<div class="row">
<div>
<label>File Name:</label>
<input type="text" id="save-path" value="{{SAVE_PATH_INPUT}}">
</div>
</div>
<div class="row">
<div>
<button type="submit" id="submit-edit" >Save</button>
<button id="cancel" onclick="window.location.href='/files';">Cancel</button>
</div>
</div>
</form>
</div>
</div>
<script>
// Load navbar
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
window.onload=loadEditFile();
function loadEditFile(){
var savePath = document.getElementById('save-path').value;
if( savePath != "/new.txt"){ // skip if new.txt
fetch(savePath)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch file');
}
return response.text();
})
.then(fileContents => {
// Put the file contents into a textarea element
document.getElementById('edit-textarea').value = fileContents;
document.getElementById('save-path').disabled = true;
})
.catch(error => {
console.error(error);
});
}
}
function validateForm()
{
var allowedExtensions = "{{ALLOWED_EXTENSIONS_EDIT}}";
var inputMessage = document.getElementById('save-path').value;
var dotIndex = inputMessage.lastIndexOf(".")+1;
var inputMessageExtension = inputMessage.substring(dotIndex);
var extIndex = allowedExtensions.indexOf(inputMessageExtension);
var isSlash = inputMessage.substring(0,1);
if(inputMessage == "")
{
alert("Enter the file name! \ne.g.: /new.txt");
return false;
}
if(isSlash != "/")
{
alert("The slash at the beginning of the file is missing!");
return false;
}
if(dotIndex == 0)
{
alert("The extension is missing at the end of the file!");
return false;
}
if(inputMessageExtension == "")
{
alert("The extension is missing at the end of the file!");
return false;
}
if(extIndex == -1)
{
alert("Extension not supported!");
return false;
}
}
</script>
</body>
</html>

View File

@ -1,151 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Edit file</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Tahoma, Arial, sans-serif;
font-size: small;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
h1 {
margin-top: 0;
}
#submit {
width:100px;
}
button{
width: 100px;
}
#spacer-50 {
height: 20px;
}
#spacer-20 {
height: 10px;
}
fieldset {
width:700px;
border-radius: 10px;
background-color: lightgray;
}
td, th {
text-align: center;
padding: 1px;
}
legend{
background-color:white;
background-blend-mode: darken;
border-radius: 5px;
padding: 1px 6px 2px 6px;
border-style:solid;
border-width: 1.0;
}
textarea {
width: 700px;
height: 500px;
box-sizing: border-box;
border: 1.5px solid #000000;
border-radius: 8px;
resize: none;
word-wrap: none;
}
</style>
</head>
<body>
<div id="navbar"></div>
<h2>Edit file</h2>
<fieldset>
<legend>Editing file: {{SAVE_PATH_INPUT}}</legend>
<div id="spacer-20"></div>
<table><tr><td colspan="2">
<form name="edit-file" action="/save" onsubmit="return validateForm()">
<textarea name="edit-textarea" id="edit-textarea" wrap="off" ></textarea>
<div id="spacer-20"></div>
</td></tr><tr><td>
{{SAVE_PATH_INPUT}}
<button type="submit" id="submit-edit" >Save</button>
</form>
</td><td>
<button id="submit" onclick="window.location.href='/files';">Cancel</button>
</td></tr></table>
<div id="spacer-50"></div>
</fieldset>
<iframe style="display:none" name="self-page"></iframe>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
window.onload=loadEditFile();
function loadEditFile(){
var savePath = document.getElementById('save-path').value;
if( savePath != "/new.txt"){ // skip if new.txt
fetch(savePath)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch file');
}
return response.text();
})
.then(fileContents => {
// Put the file contents into a textarea element
document.getElementById('edit-textarea').value = fileContents;
})
.catch(error => {
console.error(error);
});
}
}
function validateForm()
{
var allowedExtensions = "{{ALLOWED_EXTENSIONS_EDIT}}";
var inputMessage = document.getElementById('save-path').value;
var dotIndex = inputMessage.lastIndexOf(".")+1;
var inputMessageExtension = inputMessage.substring(dotIndex);
var extIndex = allowedExtensions.indexOf(inputMessageExtension);
var isSlash = inputMessage.substring(0,1);
if(inputMessage == "")
{
alert("Enter the file name! \ne.g.: /new.txt");
return false;
}
if(isSlash != "/")
{
alert("The slash at the beginning of the file is missing!");
return false;
}
if(dotIndex == 0)
{
alert("The extension is missing at the end of the file!");
return false;
}
if(inputMessageExtension == "")
{
alert("The extension is missing at the end of the file!");
return false;
}
if(extIndex == -1)
{
alert("Extension not supported!");
return false;
}
}
</script>
</body>
</html>

View File

@ -1,33 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Update Failed</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<style>
body {
background-color: #f7f7f7;
}
#spacer-50 {
height: 50px;
}
</style>
</head>
<body>
<div id="navbar"></div>
<center>
<h2>The update has failed.</h2>
<div id="spacer-50"></div>
<button onclick="window.location.href='/files';">to homepage</button>
</center>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
</body>
</html>

View File

@ -1,276 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>File Manager</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/global-style.css" rel="stylesheet">
<link href="/css/nav.css" rel="stylesheet">
<style>
#file-row:nth-child(odd) {
background-color: #e8e8e8;
}
#first_td_th {
width:300px;
}
progress{
width: 290px;
}
#submit-edit, #submit-upload, #submit-delete, #submit-update-local{
width:120px;
}
#dir-path{
width: 75px;
}
fieldset {
width: 100%;
max-width: 600px;
}
table {
width: 100%;
max-width: 600px;
border-collapse: collapse;
margin: 1rem 0;
/*width:380px; */
}
</style>
<body>
<div id="navbar"></div>
<div class="main-container">
<h1>File Manager</h1>
<fieldset>
<legend>File list</legend>
<div id="spacer-20"></div>
<p>&emsp;&emsp;Total: {{FS_TOTAL_BYTES}}, Used: {{FS_USED_BYTES}}, Available: {{FS_FREE_BYTES}}</p>
<div id="spacer-20"></div>
<table><tr><th id="first_td_th">Listing:</th> </th><th>Size:</th></tr>
{{LISTED_FILES}}
</table>
<div id="spacer-20"></div>
</fieldset>
<div id="spacer-20"></div>
<fieldset>
<legend>File upload</legend>
<div id="spacer-20"></div>
<form action="/upload" method="POST" enctype="multipart/form-data">
<table>
<tr>
<td id="first_td_th">
<label for="dir-path">Dir: </label><br>
<select name="dir-path" id="dir-path">
{{DIR_LIST}}
</select>&emsp;&emsp;
</td>
<td><input type="submit" id="submit-upload" value="File upload!" onclick="return validateFormUpload()"></td>
</tr>
<tr>
<td>
<div id="spacer-20"></div>
</td>
</tr>
<tr>
<td>
<input type="file" id="upload-file" name="upload-file">
</td>
</tr>
</table>
</form>
</fieldset>
<div id="spacer-20"></div>
<fieldset>
<legend>Edit file</legend>
<div id="spacer-20"></div>
<form action="/files/edit" method="GET">
<table><tr>
<td id="first_td_th">
<select name="edit-path" id="edit-path">
<option value="choose">Select file to edit</option>
<option value="new">New text file</option>
{{EDIT-DEL_FILES}}
</select></td>
<td><input type="submit" id="submit-edit" value="Edit" onclick="return validateFormEdit()"></td>
</tr></table>
</form>
</fieldset>
<div id="spacer-20"></div>
<fieldset>
<legend>Delete file</legend>
<div id="spacer-20"></div>
<form action="/files/delete" method="GET">
<table><tr>
<td id="first_td_th">
<select name="delete-path" id="select-files">
<option value="choose">Select file to delete</option>
{{EDIT-DEL_FILES}}
</select></td>
<td><input type="submit" id="submit-delete" value="Delete" onclick="return validateFormDelete()"></td>
</tr></table>
</form>
</fieldset>
<div id="spacer-20"></div>
<!--<fieldset>
<legend>Format File System</legend>
<div id="spacer-20"></div>
<form action="/format" method="POST" target="self-page">
<table><tr>
<td id="first_td_th">
<p id="format-notice">Pressing the 'Format' button will immediately delete all data from File System!</p></td>
<td><input type="submit" id="submit-format" value="Format" onclick="return confirmFormat()"></td>
</tr></table>
</form>
<div id="spacer-20"></div>
</fieldset>-->
<!--<iframe style="display:none" name="self-page"></iframe>-->
</div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
function validateFormEdit()
{
var allowedExtensions = "{{ALLOWED_EXTENSIONS_EDIT}}";
var editSelectValue = document.getElementById('edit-path').value;
var dotIndex = editSelectValue.lastIndexOf(".")+1;
var editSelectValueExtension = editSelectValue.substring(dotIndex);
var extIndex = allowedExtensions.indexOf(editSelectValueExtension);
if(editSelectValue == "new"){
return true;
}
if(editSelectValue == "choose" ){
alert("You have not chosen a file!");
return false;
}
if(extIndex == -1){
alert("Editing of this file type is not supported!");
return false;
}
}
function validateFormDelete()
{
var deleteSelectValue = document.getElementById('delete-path').value;
if(deleteSelectValue == "choose" || !deleteSelectValue.indexOf(".")){
alert("You have not chosen a file!");
return false;
}
}
function confirmFormat()
{
var text = 'Pressing the "OK" button immediately deletes all data from SPIFFS and restarts ESP32!';
if (confirm(text) == true) {
return true;
}
else{
return false;
}
}
function validateFormUpload()
{
var inputElement = document.getElementById('upload-file');
var files = inputElement.files;
if(files.length==0){
alert("You have not chosen a file!");
return false;
}
}
const fileInput = document.getElementById('update-file');
const progressBar = document.getElementById('firm-progress');
const progressLabel = document.getElementById('lbl-firm-progress');
const submitButton = document.getElementById('submit-update-local');
submitButton.addEventListener('click', uploadFile);
function uploadFile(event) {
event.preventDefault(); // Prevent the default form submission
const file = fileInput.files[0];
const url = '/update'; // Replace with the actual upload URL
// File Checks
//*********************************************************
// Check file extension
if (!file.name.toLowerCase().endsWith('.bin')) {
alert('Please select a file with the ".bin" extension.');
return;
}
// Check filename prefix
if (!file.name.toLowerCase().startsWith('fwata') && !file.name.toLowerCase().startsWith('lfsata')) {
alert('Please select a file with a filename starting with "fwata" or "lfsata".');
return;
}
// Check file size
const maxSizeBytes = 2.7 * 1024 * 1024; // 2.75Mb in bytes
if (file.size > maxSizeBytes) {
alert('Please select a file with a size not exceeding 2.7Mb.');
return;
}
//*********************************************************
const formData = new FormData();
formData.append('file-size', file.size); // Include the file size as a parameter
formData.append('update-file', file);
let s = "file-size: " + file.size;
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progressPercent = Math.round((event.loaded * 100) / event.total);
progressBar.value = progressPercent;
progressLabel.innerHTML =progressBar.value + "%";
}
});
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
console.log("received status: %d", xhr.status);
if (xhr.status === 200) {
// Upload completed successfully
alert('Upload completed!');
progressLabel.innerHTML = "Completed!";
} else if (xhr.status === 500) {
// Request was aborted (server-side)
alert('Upload aborted by the server.');
progressLabel.innerHTML = "Aborted!";
} else {
// Handle other error cases
alert('An error occurred during the upload.');
progressLabel.innerHTML = "Error!";
}
submitButton.disabled = false;
progressBar.value = 0;
progressLabel.innerHTML = "";
}
};
xhr.open('POST', url, true);
xhr.send(formData);
submitButton.disabled = true;
}
</script>
</body>
</html>

View File

@ -1,303 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>System Summary</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<style>
h1{
text-align: center;
margin: 0;
margin-bottom: 0;
font-size: larger;
}
body {
font-family: Arial, sans-serif;
padding: 0;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
width: 100%;
max-width: 600px;
min-width: 400px;
margin: 0 auto;
}
.outer-container {
justify-content: center;
align-items: center;
padding: 4 20px 4 20px
}
.container {
border: 1px solid #ccc;
padding: 5px 20px 10px 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
width:90%;
margin-bottom: 20px;
}
.center-text {
text-align: center;
font-size: medium;
font-weight: bold;
margin-bottom: 10;
}
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
align-items: flex-end;
}
.row > * {
flex: 1;
width: 0 1 calc(52% - 20px);
margin-bottom: 8px;
}
.row > *:last-child {
flex: 0 1 calc(48% - 10px); /* 40% width with 10px gap */
}
input[type="number"], select {
border: 1px solid #ccc;
border-radius: 4px;
width: 50%;
}
button {
background-color: #007bff;
color: #fff;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: auto;
}
.lbldata{
font-style:italic;
font-weight: bold;
color: purple;
}
</style>
</head>
<body>
<div id="navbar"></div>
<h1 name="h1element">System Summary</h1>
<div class="outer-container">
<div class="container">
<legend class="center-text">Booth Overview</legend>
<div class="row">
<div>
<label>App:</label>
<label class="lbldata">{{APP_NAME}}</label>
</div>
<div>
<label>OLED:</label>
<label class="lbldata">{{OLED}}</label>
</div>
</div>
<div class="row">
<div>
<label>RGB Ch1:</label>
<label class="lbldata">{{STRIP1}}</label>
</div>
<div>
<label>RGB Ch2:</label>
<label class="lbldata">{{STRIP2}}</label>
</div>
</div>
<div class="row">
<div>
<label>Front Light:</label>
<label class="lbldata">{{FRONT_LIGHT}}</label>
</div>
<div>
<label>Rear Light:</label>
<label class="lbldata">{{REAR_LIGHT}}</label>
</div>
</div>
</div>
<div class="container">
<legend class="center-text">System Information</legend>
<div class="row">
<div>
<label>Firmware Ver:</label>
<label class="lbldata">{{FIRMWARE}}</label>
</div>
</div>
<div class="row">
<div>
<label>Booth T&deg;:</label>
<label class="lbldata">{{BOOTH_T}}</label>
</div>
<div>
<label>Setpoint:</label>
<label class="lbldata">{{SETPOINT}}</label>
</div>
</div>
<div class="row">
<div>
<label>Flash Size:</label>
<label class="lbldata">{{FLASH_SIZE}}</label>
</div>
<div>
<label>Flash Free:</label>
<label class="lbldata">{{FLASH_FREE}}</label>
</div>
</div>
<div class="row">
<div>
<label>Heap Size:</label>
<label class="lbldata">{{HEAP_SIZE}}</label>
</div>
<div>
<label>Heap Free:</label>
<label class="lbldata">{{HEAP_FREE}}</label>
</div>
</div>
<div class="row">
<div>
</div>
<div>
<label>CPU Freq:</label>
<label class="lbldata">{{CPU_FREQ}}</label>
</div>
</div>
</div>
<div class="container">
<legend class="center-text">Network / WiFi</legend>
<div class="row">
<div>
<label>IP:</label>
<label class="lbldata">{{IP}}</label>
</div>
<div>
<label>MAC:</label>
<label class="lbldata">{{MAC}}</label>
</div>
</div>
<div class="row">
<div>
<label>SSID:</label>
<label class="lbldata">{{SSID}}</label>
</div>
<div>
<label>RSSi:</label>
<label class="lbldata">{{RSSI}}</label>
</div>
</div>
<div class="row">
<div>
<label>Ch:</label>
<label class="lbldata">{{WIFI_CH}}</label>
</div>
<div>
<label>Encryp:</label>
<label class="lbldata">{{ENCRYP}}</label>
</div>
</div>
<div class="row">
<div>
<label>AP SSID:</label>
<label class="lbldata">{{AP_SSID}}</label>
</div>
<div>
<label>AP Clients:</label>
<label class="lbldata">{{AP_CLIENTS}}</label>
</div>
</div>
<div class="row">
<div>
<label>AP MAC:</label>
<label class="lbldata">{{AP_MAC}}</label>
</div>
<div>
<label>---</label>
<label class="lbldata"></label>
</div>
</div>
</div>
<div class="container">
<legend class="center-text">Bluetoth LE</legend>
<div class="row">
<div>
<label>Active:</label>
<label class="lbldata">{{BLE}}</label>
</div>
</div>
<div class="row">
<div>
<label>SSID:</label>
<label class="lbldata">{{BLE_SSID}}</label>
</div>
<div>
<label>Clients:</label>
<label class="lbldata">{{BLE_CLIENTS}}</label>
</div>
</div>
</div>
<div class="container">
<legend class="center-text">Luma Stiks</legend>
<div class="row">
<div>
<label>Stik1:</label>
<label class="lbldata">{{STIK1}}</label>
</div>
<div>
<label>Stik2:</label>
<label class="lbldata">{{STIK2}}</label>
</div>
</div>
<div class="row">
<div>
<label>Stik3:</label>
<label class="lbldata">{{STIK3}}</label>
</div>
<div>
<label>Stik4:</label>
<label class="lbldata">{{STIK4}}</label>
</div>
</div>
<div class="row">
<div>
<label>Stik5:</label>
<label class="lbldata">{{STIK5}}</label>
</div>
<div>
<label>Stik6:</label>
<label class="lbldata">{{STIK6}}</label>
</div>
</div>
</div>
</div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link href="/css/nav.css" rel="stylesheet">
<title>ESP32 AP</title>
</head>
<body>
<div id="navbar"></div>
<h1>Welcome to ESP32 AP</h1>
<p>This is a simple web server running on ESP32 in Access Point mode.</p>
<div id="status"></div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
// Fetch status
fetch('/api/status')
.then(response => response.json())
.then(data => {
document.getElementById('status').innerHTML =
`Status: ${data.status}`;
});
</script>
</body>
</html>

View File

@ -1,527 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<script src="event-box.js"></script>
<script src="hue-select.js"></script>
<style>
h1{
text-align: center;
margin: 0;
margin-bottom: 0;
font-size: larger;
}
body {
font-family: Arial, sans-serif;
padding: 0;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
width: 100%;
max-width: 700px;
min-width: 350px;
margin: 0 auto;
}
.outer-container {
justify-content: center;
align-items: center;
padding: 4 20px 20 20px
}
.container {
border: 1px solid #ccc;
padding: 5px 20px 5px 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
width:90%;
}
.center-text {
text-align: center;
font-size: larger;
font-weight: bold;
margin-bottom: 10;
}
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
}
.row > * {
flex: 0 0 calc(50% - 20px);
margin-bottom: 15px;
}
.row:last-child {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
input[type="range"] {
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
}
select{
width: 50%;
}
button {
background-color: #007bff;
color: #fff;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
#set-countdown-button {
margin-left: 30px; /* Adjust top margin if necessary */
}
</style>
</head>
<body>
<div id="navbar"></div>
<div class="outer-container">
<h1 name="h1element">Lights Configuration</h1>
<div class="container">
<legend class="center-text" id="center-text">Countdown ( Constant Light )</legend>
<div class="row">
<div>
<label id="constLightMin-label" for="constlightMin">Light Min:</label>
<input type="range" name="constLightMin" id="constlightMin" min="0" max="99" value="25" step="1" onchange="updateLabel('Light Min: ', this)">
</div>
<div>
<label id="constLightMax-label" for="constlightMax">Light Max:</label>
<input type="range" name="constLightMax" id="constlightMax" min="1" max="100" value="90" step="1" onchange="updateLabel('Light Max: ', this)">
</div>
</div>
<div class="row">
<div>
<label for="Holdtime">Hold time(ms):</label><br>
<input type="number" name="holdTime" id="holdtime" min="0" max="5000" value="500">
</div>
<div>
<label for="Ramptime">Ramp dn(ms):</label><br>
<input type="number" name="rampTime" id="Ramptime" min="0" max="5000" value="500">
</div>
</div>
<div class="row">
<br>
</div>
<div class="row">
<div>
<label>Active Profile:</label>
<select name= "selActiveAnimProfiles" id="selActivedAnimProfiles" style="width:150px">
<option>(1)</option>
<option>(2)</option>
<option>(3)</option>
<option>(4)</option>
<option>(5)</option>
<option>(6)</option>
<option>(7)</option>
<option>(8)</option>
</select>
</div>
<div>
<button id="testCommon" onclick="SaveProfileCommonToServer(true)">Try</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button id="saveCommon" onclick="SaveProfileCommonToServer(false)">Save</button>
</div>
</div>
</div>
</div>
<div class="outer-container">
<div class="container">
<legend class="center-text" id="center-text">Saved Animation Profiles</legend>
<div class="row">
<div>
<label for="selSavedAnimProfiles">Profiles:</label>
<select name= "selSavedAnimProfiles" id="selSaveddAnimProfiles" style="width:150px" onchange="OnSavedAnimProfilesChanged(this)">
<option>(1)</option>
<option>(2)</option>
<option>(3)</option>
<option>(4)</option>
<option>(5)</option>
<option>(6)</option>
<option>(7)</option>
<option>(8)</option>
</select>
</div>
<div>
<label for="profileName">Rename:</label>
<input type="text" name="inputProfileName" id="profileName" style="width:140px">
<button id="saveProfile" onclick="SaveProfileToServer()">Save</button>
</div>
</div>
</div>
</div>
<div id="events-container"></div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
window.onload = function() { getProfilesAndEvents(); };
// Initialize Names
const EVENTCOUNT = 12;
const PROFILECOUNT = 8;
var profileCommonJson;
var profileJson = [PROFILECOUNT];
var eventsJson;
var animListJson;
const inputProfileName = document.getElementsByName('inputProfileName')[0];
const selSavedAnimProfiles = document.getElementsByName('selSavedAnimProfiles')[0];
const selActiveAnimProfiles = document.getElementsByName('selActiveAnimProfiles')[0];
const constLightMin = document.getElementsByName('constLightMin')[0];
const constLightMax = document.getElementsByName('constLightMax')[0];
const holdTime = document.getElementsByName('holdTime')[0];
const rampTime = document.getElementsByName('rampTime')[0];
var lastAnimProfileIndex = 0;
var h1element = document.getElementsByName("h1element")[0];
var eventBox = [EVENTCOUNT];
function createEventForms(count){
const eventContainer = document.getElementById('events-container');
for(let i = 0; i < count; i++ ){
const div = document.createElement('div');
eventBox[i] = document.createElement('event-box');
eventBox[i].id = `event${i}`;
eventBox[i].setHidden(true);
eventBox[i].setIndex(i);
eventBox[i].addEventListener('tryClick', handleEventTryClick);
div.appendChild(eventBox[i]);
eventContainer.appendChild(div);
}
}
// Get profiles
// fetch anim-profile-common.json and anim-profile(1-8), events and anim-list
function getProfilesAndEvents() {
try {
fetch_file('cfg/anim-profile-common.json').then(profilesResult => {
profileCommonJson = profilesResult.data;
let profilePromises = [];
for (let i = 1; i <= PROFILECOUNT; i++) {
profilePromises.push(fetch_file(`cfg/anim-profile${i}.json`));
}
Promise.all(profilePromises).then(results => {
profileJson = results.map(result => result.data);
fetch_file('cfg/app-events.json').then(eventsResult => {
eventsJson = eventsResult.data.apps[eventsResult.data.index];
fetch_file('cfg/anim-list.json').then(animListResult => {
animListJson = animListResult.data;
// Title for page
h1element.textContent = eventsJson.name;
// Event Boxes
createEventForms(EVENTCOUNT);
NameAndHideEvents(animListJson);
// Rest
FillProfilesList();
selSavedAnimProfiles.selectedIndex = profileCommonJson['profile-index'];
lastAnimProfileIndex = selSavedAnimProfiles.selectedIndex;
setProfile(selSavedAnimProfiles.selectedIndex);
selActiveAnimProfiles.selectedIndex = profileCommonJson['profile-index'];
});
});
});
}).catch(error => {
console.error(error);
});
} catch (error) {
console.error(error);
}
}
// update the form with animation data when profile list item selected
function setProfile(index){
var changeEvent = new Event("change");
// update common
inputProfileName.value = "";
constLightMin.value = profileCommonJson.countdown.min;
constLightMin.dispatchEvent(changeEvent);
constLightMax.value = profileCommonJson.countdown.max;
constLightMax.dispatchEvent(changeEvent);
holdTime.value = profileCommonJson.countdown.hold;
rampTime.value = profileCommonJson.countdown.ramp;
// Update Events
for (let i = 0; i < EVENTCOUNT; i++) {
if(!eventBox[i].getHidden()){
let thisEvent = profileJson[index].events[i];
eventBox[i].setAnimationIndex(thisEvent.anim);
eventBox[i].setHueValue(thisEvent.hue);
eventBox[i].setSpeedValue(thisEvent.speed);
eventBox[i].setHueRangeValue(thisEvent["hue-range"]);
eventBox[i].setParam1Value(thisEvent.param1);
eventBox[i].setParam2Value(thisEvent.param2);
eventBox[i].setCheck1Value(thisEvent.check1);
eventBox[i].setCheck2Value(thisEvent.check2);
eventBox[i].setCheck3Value(thisEvent.check3);
eventBox[i].setCheck4Value(thisEvent.check4);
}
}
}
// When new profile is selected update
function OnSavedAnimProfilesChanged( event ){
console.log('Anim Profile Selected index:', event.selectedIndex);
console.log('Anim Profile Updated index:', lastAnimProfileIndex);
updateSingleProfileJson(lastAnimProfileIndex); // save current setting in ram but not in server yet
setProfile(event.selectedIndex);
lastAnimProfileIndex = event.selectedIndex;
}
//Update the anim-profilesX.json obj with updated values
function updateSingleProfileJson(index){
if(inputProfileName.value != ""){
profileJson[index].name = inputProfileName.value;
selSavedAnimProfiles.options[index].text = '(' + (index + 1) + ') ' + inputProfileName.value;
selSavedAnimProfiles.options[index].value = inputProfileName.value;
}
// Update Events
for (let i = 0; i < EVENTCOUNT; i++) {
profileJson[index].events[i].anim = eventBox[i].getAnimationIndex();
profileJson[index].events[i].hue = eventBox[i].getHueValue();
profileJson[index].events[i].speed = eventBox[i].getSpeedValue();
profileJson[index].events[i]["hue-range"] = eventBox[i].getHueRangeValue();
profileJson[index].events[i].param1 = eventBox[i].getParam1Value();
profileJson[index].events[i].param2 = eventBox[i].getParam2Value();
profileJson[index].events[i].check1 = eventBox[i].getCheck1Value();
profileJson[index].events[i].check2 = eventBox[i].getCheck2Value();
profileJson[index].events[i].check3 = eventBox[i].getCheck3Value();
profileJson[index].events[i].check4 = eventBox[i].getCheck4Value();
}
}
// save profileX.Json
function SaveProfileToServer(){
updateSingleProfileJson(selSavedAnimProfiles.selectedIndex);
const params = new URLSearchParams();
let i = selSavedAnimProfiles.selectedIndex + 1;
params.append('type', `anim-profile${i}`);
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' } ,
body: JSON.stringify(profileJson[selSavedAnimProfiles.selectedIndex]) // convert to string
})
.then(response => {
if (response.ok) { console.log('Request successful');}
else { throw new Error('Request failed');}
})
.catch(error => { console.error(error);});
}
// save profileCommonJson
function SaveProfileCommonToServer(TestOnly){
profileCommonJson.countdown.min = constLightMin.value;
profileCommonJson.countdown.max = constLightMax.value;
profileCommonJson.countdown.hold = holdTime.value;
profileCommonJson.countdown.ramp = rampTime.value;
profileCommonJson["profile-index"] = selActiveAnimProfiles.selectedIndex; // Set at active profile
const params = new URLSearchParams();
params.append('type', TestOnly ? 'set-countdown' : 'anim-profile-common');
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/json' } ,
body: JSON.stringify(profileCommonJson) // convert to string
})
.then(response => {
if (response.ok) { console.log('Request successful');}
else { throw new Error('Request failed');}
})
.catch(error => { console.error(error);});
}
//
function fetch_file(filename){
return fetch(filename)
.then(response => {
if (response.ok) { return response.json(); }
else { throw new Error('fetching ' + fileName + ' failed'); }
})
.then(data => { return{data:data}; })
}
//
function fetch_json_file(fileName){
const params = new URLSearchParams();
params.append('type', fileName);
const url = '/get?' + params.toString();
return fetch(url, {
method: 'GET'
})
.then(response => {
if (response.ok) { return response.json(); }
else { throw new Error('fetching ' + fileName + ' failed'); }
})
.then(data => { return{data:data}; })
}
// Rename legends and or hide unused events property boxes
function NameAndHideEvents(props){
for(let i = 0; i < EVENTCOUNT; i++){
if(eventsJson.events[i] === ''){
break;
}
eventBox[i].setTitle(eventsJson.events[i]);
eventBox[i].setHidden(false);
}
// load captions for white fills box
eventBox[0].setAnimationCaptions(props.whitefills);
// load captions for the rest of the eventBox
for(let i = 1; i < EVENTCOUNT; i++){
eventBox[i].setAnimationCaptions(props.animations);
}
}
// Fill Profiles List
function FillProfilesList(){
for(let i = 0; i < PROFILECOUNT; i++){
selSavedAnimProfiles.options[i].text = '(' + (i + 1) + ') ' + profileJson[i].name;
selSavedAnimProfiles.options[i].value = profileJson[i].name;
selActiveAnimProfiles.options[i].text = selSavedAnimProfiles.options[i].text;
selActiveAnimProfiles.options[i].value = selSavedAnimProfiles.options[i].value;
}
}
// Fill White Animation drop down lists
function FillWhitefilList(whiteJson){
for(let x = 0; x < whiteJson.length; x++){
eventBox[0].addOptionToList(x, whiteJson[x])
}
}
// Fill Animations drop down lists
function FillAnimationsList(animJson){
for(let x = 0; x < animJson.length; x++){
if(animJson[x].name == ""){break;}// stop if blank
for(let i = 1; i < EVENTCOUNT; i++){
eventBox[i].addOptionToList(i, animJson[x].name);
}
}
}
// for const light props
function updateLabel(labelText, slider) {
let label = document.getElementById(slider.name + "-label");
label.innerHTML = labelText + slider.value;
}
// send anim event to test out
function postPlayAnim(i){
let tempAnimProps = {};
tempAnimProps.event = eventIndex;
tempAnimProps.anim = eventBox[i].getAnimationIndex();
tempAnimProps.hue = eventBox[i].getHueValue();
tempAnimProps.speed = eventBox[i].getSpeedValue();
tempAnimProps["hue-range"] = eventBox[i].getHueRangeValue();
tempAnimProps.param1 = eventBox[i].getParam1Value();
tempAnimProps.param2 = eventBox[i].getParam2Value();
tempAnimProps.check1 = eventBox[i].getCheck1Value();
tempAnimProps.check2 = eventBox[i].getCheck2Value();
tempAnimProps.check3 = eventBox[i].getCheck3Value();
tempAnimProps.check4 = eventBox[i].getCheck4Value();
const params = new URLSearchParams();
params.append('type', 'play-anim');
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain'} ,
body: JSON.stringify(tempAnimProps)
})
.then(response => {
if (!response.ok) { throw new Error('Request failed'); }
})
.catch(error => { console.error(error); });
}
// send anim event to test out
function postTestCountdown(){
let tempCountdown = {};
tempCountdown.min = constLightMin.value;
tempCountdown.max = constLightMax.value;
tempCountdown.hold = holdTime.value;
tempCountdown.ramp = rampTime.value;
tempCountdown.active_profile = selSavedAnimProfiles.selectedIndex;
const params = new URLSearchParams();
params.append('type', 'set-countdown');
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain'} ,
body: JSON.stringify(tempCountdown)
})
.then(response => {
if (!response.ok) { throw new Error('Request failed'); }
})
.catch(error => { console.error(error); });
}
// Post Play Anim
function handleEventTryClick(event) {
let tempAnimProps = {};
tempAnimProps.index = event.detail.eventIndex;
tempAnimProps.anim = event.detail.animIndex;
tempAnimProps.hue = event.detail.hue;
tempAnimProps.speed = event.detail.speed;
tempAnimProps["hue-range"] = event.detail.colorRange;
tempAnimProps.param1 = event.detail.param1;
tempAnimProps.param2 = event.detail.param2;
tempAnimProps.check1= event.detail.check1;
tempAnimProps.check2 = event.detail.check2;
tempAnimProps.check3 = event.detail.check3;
tempAnimProps.check4 = event.detail.check4;
//console.log(tempAnimProps);
const params = new URLSearchParams();
params.append('type', 'play-anim');
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain'} ,
body: JSON.stringify(tempAnimProps)
})
.then(response => {
if (!response.ok) { throw new Error('Request failed'); }
})
.catch(error => { console.error(error); });
};
</script>
</body>
</html>

View File

@ -1,17 +0,0 @@
<nav class="navbar">
<div class="navbar-left">
<img src="/images/atalogo.png" alt="Left Image" class="nav-image">
</div>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/setup">Setup</a></li>
<li>
<a href="#"> System</a>
<ul class="submenu">
<li><a href="/www/wifi.html">Internet</a></li>
<li><a href="/www/firmware.html">Update</a></li>
<li><a href="/www/about.html">About</a></li>
</ul>
</li>
</ul>
</nav>

View File

@ -1,34 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Update Success</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<style>
body {
background-color: #f7f7f7;
}
#spacer-50 {
height: 50px;
}
</style>
</head>
<body>
<div id="navbar"></div>
<center>
<h2>The update was successful.</h2>
<div id="spacer-50"></div>
<button onclick="window.location.href='/files';">to homepage</button>
</center>
<script>
// Load navbar
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
</script>
</body>
</html>

View File

@ -1,385 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Booth Configuration Tools</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/nav.css" rel="stylesheet">
<script src="hue-select.js"></script>
<style>
h1{
text-align: center;
margin: 0;
margin-bottom: 0;
font-size: larger;
}
body {
font-family: Arial, sans-serif;
padding: 0;
justify-content: center;
align-items: center;
background-color: #f0f0f0;
width: 100%;
max-width: 600px;
min-width: 400px;
margin: 0 auto;
}
.outer-container {
justify-content: center;
align-items: center;
padding: 4 20px 4 20px
}
.container {
border: 1px solid #ccc;
padding: 5px 20px 10px 20px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
width:90%;
margin-bottom: 20px;
}
.center-text {
text-align: center;
font-size: medium;
font-weight: bold;
margin-bottom: 10;
}
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
align-items: flex-end;
}
.row > * {
flex: 1;
width: calc(50% - 20px);
margin-bottom: 8px;
}
input[type="number"], select {
border: 1px solid #ccc;
border-radius: 4px;
width: 50%;
}
button {
background-color: #007bff;
color: #fff;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: auto;
}
</style>
</head>
<body>
<div id="navbar"></div>
<h1 name="h1element">System Setup</h1>
<div class="outer-container">
<div class="container">
<legend class="center-text" id="center-text">LED Strip #1 Settings</legend>
<div class="row">
<div>
<label>Control App:</label><br>
<select id="app-names-list" style="width: 80%;"></select>
</div>
<div>
<button onclick="OnSaveAllClick()">Save All</button>
</div>
<div>
<button onclick="OnReStartClick()">ReStart</button>
</div>
</div>
</div>
<div class="container">
<legend class="center-text" id="center-text">LED Strip #1 Settings</legend>
<div class="row">
<div>
<label>Enabled</label><br>
<input type="checkbox" id="strip1-en">
</div>
<div>
<label for="inputLEDS">LED Count:</label><br>
<input type="number" id="strip1-led-count" min="1" max="200" step="1">
</div>
<div>
<label for=" ">Shift:</label><br>
<input type="number" id="strip1-shift" min="-199" max="199" step="1">
</div>
</div>
<div class="row">
<div>
<label for=" ">Offset:</label><br>
<input type="number" id="strip1-offset" min="0" max="100" step="1">
</div>
<div>
<label>RGB Order:</label><br>
<select id="strip1-rgb">
<option> RGB</option>
<option> RBG</option>
<option> GRB</option>
<option> GBR</option>
<option> BRG</option>
<option> BGR</option>
</select>
</div>
<div>
<label>Power:</label><br>
<select id="strip1-power">
<option> 100%</option>
<option> 50%</option>
<option> 25%</option>
<option> 12.5%</option>
</select>
</div>
</div>
</div>
<div class="container">
<legend class="center-text" id="center-text">Test Tool (Single LED)</legend>
<div class="row">
<div>
<input type="radio" id="test-radio-strip1" name="strip" value="0" checked>
<label>Strip #1</label><br>
</div>
<div hidden>
<input type="radio" name="strip" value="1">
<label>Strip #2</label><br>
</div>
</div>
<div class="row">
<div>
<label>Pixel Index:</label><br>
<input type="number" id="test-pixel-index" min="-199" max="199" step="1" style="width: 80px;">
</div>
<div>
<hue-select id="test-hue"></hue-select>
</div>
</div>
<div class="row">
<div>
<button onclick="OnSetPixelClick()">Set</button>
</div>
<div>
<button onclick="OnClearClick()">Clear</button>
</div>
</div>
</div>
<div class="container">
<legend class="center-text" id="center-text">Luma Stiks Found</legend>
<div class="row">
<div>
<label>Stik #1 SSID:</label><br>
<input type="checkbox" id="stick1-en">
<input id="stick1" >
</div>
<div>
<label>Stik #2 SSID:</label><br>
<input type="checkbox" id="stick2-en">
<input id="stick2" >
</div>
</div>
<div class="row">
<div>
<label>Stik #3 SSID:</label><br>
<input type="checkbox" id="stick3-en">
<input id="stick3" >
</div>
<div>
<label>Stik #4 SSID:</label><br>
<input type="checkbox" id="stick4-en">
<input id="stick4" >
</div>
</div>
<div class="row">
<div>
<label>Stik #5 SSID:</label><br>
<input type="checkbox" id="stick5-en">
<input id="stick5" >
</div>
<div>
<label>Stik #6 SSID:</label><br>
<input type="checkbox" id="stick6-en">
<input id="stick6" >
</div>
</div>
<div class="row">
<div>
<label>Stik #7 SSID:</label><br>
<input type="checkbox" id="stick7-en">
<input id="stick7" >
</div>
<div>
<label>Stik #8 SSID:</label><br>
<input type="checkbox" id="stick8-en">
<input id="stick8" >
</div>
</div>
<div class="row" >
<div>
<button id="save-stiks">Scan</button>
</div>
<div>
<button id="save-stiks">Save</button>
</div>
</div>
</div>
</div>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
window.onload = function() { OnPageLoad(); };
const AppList = document.getElementById('app-names-list');
const Strip1Enabled = document.getElementById('strip1-en');
const Strip1LedCount = document.getElementById('strip1-led-count');
const Strip1Shift = document.getElementById('strip1-shift');
const Strip1Offset = document.getElementById('strip1-offset');
const Strip1RGB = document.getElementById('strip1-rgb');
const Strip1Power = document.getElementById('strip1-power');
const TestHue = document.getElementById('test-hue');
const PixelIndex = document.getElementById('test-pixel-index');
PixelIndex.value = 0;
function OnPageLoad(){
fetch_json_file('cfg/app-events.json').then(result =>{
jsAppEvents = result.data;
fetch_json_file('cfg/led-devices.json').then(result =>{
jsLedDevices = result.data;
FillControlAppList(jsAppEvents);
FillStripValues(jsLedDevices);
});
});
}
function FillStripValues(js){
Strip1Enabled.checked = js.strip1.en;
Strip1LedCount.value = js.strip1.size;
Strip1Shift.value = js.strip1.shift;
Strip1Offset.value = js.strip1.offset;
Strip1RGB.selectedIndex = js.strip1['rgb-order'];
Strip1Power.selectedIndex = js.strip1['power-div'];
SelectRgbByText(js.strip1['rgb-order'].toLowerCase());
}
function SelectRgbByText(rgbText){
let x = 0;
if(rgbText === "rgb"){x = 0;}
else if(rgbText === "rbg"){x = 0;}
else if(rgbText === "grb"){x = 0;}
else if(rgbText === "gbr"){x = 0;}
else if(rgbText === "brg"){x = 0;}
else if(rgbText === "bgr"){x = 0;}
Strip1RGB.selectedIndex = x;
}
function FillControlAppList(js){
jsApps = js.apps;
let x = 0;
jsApps.forEach(app => {
AddOptionToList(x, app.name);
x++;
});
AppList.selectedIndex = js.index;
}
function AddOptionToList(value, text){
const optionElement = document.createElement('option');
optionElement.value = value;
optionElement.textContent = text;
AppList.appendChild(optionElement);
}
function OnSetPixelClick(){
let jsPacket = {};
jsPacket.strip = 0;
i = PixelIndex.value;
if( i < -200){
PixelIndex.value = i;
return;
}
jsPacket.index = PixelIndex.value;
jsPacket.hue = TestHue.getSelectedHue();
SendJson(jsPacket, 'set-pixel');
}
function SendJson(js, type){
const params = new URLSearchParams();
params.append('type', type);
const url = '/post?' + params.toString();
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/plain'} ,
body: JSON.stringify(js)
})
.then(response => {
if (!response.ok) { throw new Error('Request failed'); }
})
.catch(error => { console.error(error); });
}
function OnClearClick(){
let jsPacket = {};
jsPacket.strip = 0;
SendJson(jsPacket, 'clear-strip');
}
function OnSaveAllClick(){
let jsPacket = {};
jsPacket.appindex = AppList.selectedIndex;
jsPacket.en1 = Strip1Enabled.checked;
jsPacket.count1 = Strip1LedCount.value;
jsPacket.shift1 = Strip1Shift.value;
jsPacket.offset1 = Strip1Offset.value;
jsPacket.rgb1 = Strip1RGB.value.toLowerCase();
jsPacket.power1 = Strip1Power.selectedIndex;
SendJson(jsPacket, 'setup-save');
}
function OnReStartClick(){
let jsPacket = {};
jsPacket.test = "restart";
SendJson(jsPacket, 'restart');
}
function fetch_json_file(filename){
return fetch(filename)
.then(response => {
if (response.ok) { return response.json(); }
else { throw new Error('fetching ' + fileName + ' failed'); }
})
.then(data => { return{data:data}; })
}
</script>
</body>
</html>

View File

@ -1,234 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/css/nav.css">
<title>Firmware Upgrade</title>
<style>
.status-circle {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.connected { background-color: #4CAF50; }
.disconnected { background-color: #f44336; }
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.version-info {
margin: 20px 0;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.progress-box {
height: calc(100vh - 500px);
min-height: 200px;
margin: 20px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
overflow-y: auto;
font-family: monospace;
}
button {
background-color: #2196F3;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div id="navbar"></div>
<div class="container">
<h1>Firmware Upgrade</h1>
<div>
<span class="status-circle disconnected" id="status-indicator"></span>
<span id="connection-status">Disconnected</span>
</div>
<div class="version-info">
<p>Current Version: <span id="current-version">-</span></p>
<p>Latest Version: <span id="latest-version">-</span></p>
</div>
<div>
<button id="check-upgrades" onclick="checkUpdates()">Check for Upgrades</button>
<button id="start-upgrade" onclick="startUpdate()" disabled>Start upgrade</button>
</div>
<div class="progress-box" id="progress-log"></div>
</div>
<script>
// Load the navigation bar from an external HTML file and insert it into the page
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
// Flag to track if a firmware upgrade is available
let updateAvailable = false;
// Updates the UI to show connection status
// Changes the color and text of the status indicator
function updateStatus(connected) {
const indicator = document.getElementById('status-indicator');
const status = document.getElementById('connection-status');
indicator.className = `status-circle ${connected ? 'connected' : 'disconnected'}`;
status.textContent = connected ? 'Connected' : 'Disconnected';
}
// Adds a message to the progress log box
// Auto-scrolls to the bottom to show latest messages
function log(message) {
const logBox = document.getElementById('progress-log');
logBox.innerHTML += `${message}<br>`;
logBox.scrollTop = logBox.scrollHeight;
}
// Checks for firmware updates by calling the server API
// Updates the UI with version information and enables/disables upgrade button
async function checkUpdates() {
const checkButton = document.getElementById('check-upgrades');
checkButton.disabled = true; // Disable button while checking
try {
const response = await fetch('/upgrade/check');
const data = await response.json();
// Upgrade version information in the UI
document.getElementById('current-version').textContent = data.currentVersion;
document.getElementById('latest-version').textContent = data.latestVersion;
// Enable/disable upgrade button based on availability
updateAvailable = data.updateAvailable;
document.getElementById('start-upgrade').disabled = !updateAvailable;
log(updateAvailable ? 'Upgrade available!' : 'No upgrades available');
} catch (error) {
log('Error checking for upgrades: ' + error);
} finally {
checkButton.disabled = false; // Re-enable button when done
}
}
// Initiates the firmware upgrade process
// Uses Websocket to receive progress updates
async function startUpdate() {
const startButton = document.getElementById('start-upgrade');
const checkButton = document.getElementById('check-upgrades');
let retryCount = 0;
const maxRetries = 3;
// Disable buttons during the update
startButton.disabled = true;
checkButton.disabled = true;
try {
// Start the upgrade process with a timeout
log('Starting update...');
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), 10000)
);
const response = await Promise.race([
fetch('/upgrade/start', {
method: 'POST',
credentials: 'same-origin'
}),
timeout
]);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
log('Update initiated successfully.');
// Create SSE connection
const eventSource = new EventSource('/upgrade-progress');
log('Connecting to update server...');
eventSource.onopen = () => {
console.log("EventSource connected");
log('Update connection established.');
};
eventSource.addEventListener('update', (event) => {
try {
console.log("Received message:", event.data);
const data = JSON.parse(event.data);
log(data.message); // Log the update message
// Handle completion
if (data.complete) {
eventSource.close();
log('Upgrade complete! Rebooting...');
//setTimeout(() => window.location.reload(), 5000);
}
} catch (error) {
console.error("Message parsing error:", error);
log(`Error processing update message: ${error.message}`);
}
});
eventSource.onerror = (event) => {
const errorDetails = event.error ? `Error: ${event.error}` : event.status ? `Status: ${event.status}` : 'Unknown error';
log('Connection error occurred' + errorDetails);
if (retryCount++ >= maxRetries || eventSource.readyState === EventSource.CLOSED) {
log('Max retries reached. Please refresh the page to retry.');
eventSource.close();
startButton.disabled = false;
checkButton.disabled = false;
} else {
log(`Attempting to reconnect... (${retryCount}/${maxRetries})`);
}
};
} catch (error) {
console.error("Update error:", error);
log(`Update error: ${error.message}`);
} finally {
startButton.disabled = false;
checkButton.disabled = false;
}
}
// Poll server every 5 seconds to check if device is still connected
// Updates the status indicator accordingly
setInterval(async () => {
try {
const response = await fetch('/api/status');
updateStatus(response.ok);
} catch {
updateStatus(false);
}
}, 5000);
</script>
</body>
</html>

View File

@ -1,159 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/css/nav.css">
<title>Wifi Access</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f2f2f2;
margin: 0;
padding: 0;
}
h1 {
margin: 30px 0 20px;
text-align: center;
}
form {
background-color: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
margin: auto;
max-width: 500px;
}
input[type="text"], input[type="password"], select {
padding: 12px 20px;
margin: 8px 0;
box-sizing: border-box;
border: none;
border-radius: 4px;
background-color: #f2f2f2;
font-size: 16px;
width: 100%;
}
button {
background-color: #2d2dfa;
color: #fff;
padding: 12px 20px;
margin: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
width: 100%;
}
button:hover {
background-color: #45a049;
}
img{
display: block;
margin-left: auto;
margin-right:auto;
height: auto;
width: auto;
max-width: 150px;
filter: drop-shadow(2px 2px 2px #666666);
}
</style>
</head>
<body>
<div id="navbar"></div>
<h1>Wifi Access</h1>
<form id="wifi-credentials-form">
<div style="text-align: center; margin-bottom: 15px;">
<span style="display: inline-block; width: 25px; height: 25px; border-radius: 50%; background-color: gray; margin-right: 10px; vertical-align: middle;"></span>
<label id="status-label">Status: Not Connected</label>
</div>
<label for="ssid">SSID:</label>
<select id="ssid" name="ssid" required>
<option value="">Select a network...</option>
</select>
<label for="password">Password:</label>
<input type="text" id="password" name="password" placeholder="Password" required>
<button type="button" onclick="postConnectToAP(event)">Connect</button>
</form>
<script>
fetch('/www/navbar.html')
.then(response => response.text())
.then(data => {
document.getElementById('navbar').innerHTML = data;
});
const form = document.querySelector('#wifi-credentials-form');
window.onload = getWifiNetworks();
function onStart(){
getConnectionStatus();
getWifiNetworks();
}
async function getWifiNetworks() {
try {
const response = await fetch('/wifi/scans', { method: 'GET' });
const data = await response.json();
const ssidSelect = document.getElementById('ssid');
ssidSelect.innerHTML = '<option value="">Select a network...</option>';
if (data.networks && Array.isArray(data.networks)) {
data.networks.forEach(network => {
const option = document.createElement('option');
option.value = network.ssid;
option.textContent = `${network.ssid} (${network.rssi}dBm)`;
ssidSelect.appendChild(option);
});
} else {
console.error('Network data is not in expected format');
}
} catch (error) {
console.error('WiFi scan failed:', error);
}
}
async function postConnectToAP(event) {
event.preventDefault();
const connectButton = document.querySelector('button[onclick="postConnectToAP(event)"]');
connectButton.disabled = true;
try {
const params = new URLSearchParams({
ssid: document.getElementById('ssid').value,
pass: document.getElementById('password').value
});
await fetch(`/wifi/connect?${params.toString()}`, {
method: 'POST',
headers: { 'Accept': 'application/json' }
});
document.getElementById('status-label').textContent = `Status: Disconnected`;
const circle = document.querySelector('span');
circle.style.backgroundColor = 'gray';
} catch (error) {
console.error('Connection failed:', error);
} finally {
connectButton.disabled = false;
}
}
async function getConnectionStatus() {
try {
const data = await fetch('/wifi/status', { method: 'GET'}).then(res => res.json());
document.getElementById('status-label').textContent = `Status: ${data.status}`;
const circle = document.querySelector('span');
circle.style.backgroundColor = data.status === 'Connected' ? '#45a049' : 'gray';
} catch (error) {
console.error('Failed to get connection status:', error);
}
}
// Run getConnectionStatus every second
setInterval(getConnectionStatus, 10000);
</script>
</body>
</html>

Binary file not shown.

View File

@ -1,201 +0,0 @@
{
"version": {
"major": 1,
"minor": 4,
"patch": 8
},
"release_date": "2025-08-20",
"release_time": "08:57:43",
"description": "This is a firmware update.",
"changelog": [
"...",
"..."
],
"firmware": {
"file": "firmware.bin",
"md5": "fcec6e659842cc0333880b701a3e2634",
"size": 1401712
},
"files": [
{
"remote": "data/ata-boothifier-upgrade.html",
"local": "/ata-boothifier-upgrade.html",
"md5": "92074ec24fd467545272eb9e837d644c",
"size": 16980
},
{
"remote": "data/ata-boothifier-upgradeV2.html",
"local": "/ata-boothifier-upgradeV2.html",
"md5": "484648993370e4b6aff13124bd1c55c1",
"size": 18178
},
{
"remote": "data/ata-boothifier-upgradeV3.html",
"local": "/ata-boothifier-upgradeV3.html",
"md5": "79426145db379ac15ccc742245a31ecb",
"size": 17233
},
{
"remote": "data/favicon.ico",
"local": "/favicon.ico",
"md5": "ba4c4e3bf5e5db2bbfc56a52f3657d79",
"size": 1150
},
{
"remote": "data/flashstik-reg.html",
"local": "/flashstik-reg.html",
"md5": "394fdfe3cd3fb89a8ddb3d6edf2d2da4",
"size": 15651
},
{
"remote": "data/css/global-style.css",
"local": "/css/global-style.css",
"md5": "217a9cca8b4eae2d28fa8bc5a0f6db09",
"size": 1239
},
{
"remote": "data/css/nav.css",
"local": "/css/nav.css",
"md5": "e653a75433056f28e3f410f567b8622c",
"size": 1538
},
{
"remote": "data/images/atalogo.png",
"local": "/images/atalogo.png",
"md5": "5a18c88a4ea80c8d8d0ad52b2fdbbadc",
"size": 16690
},
{
"remote": "data/images/favicon-32x32.png",
"local": "/images/favicon-32x32.png",
"md5": "d80cf74ace3a8be487a5158bca48f9b6",
"size": 2428
},
{
"remote": "data/js/event-box.js",
"local": "/js/event-box.js",
"md5": "553f26707686038e275227a97eb96659",
"size": 12341
},
{
"remote": "data/js/fwUoload.js",
"local": "/js/fwUoload.js",
"md5": "d4a734cce529c3adf831ea46259f9c2d",
"size": 1524
},
{
"remote": "data/js/hue-select.js",
"local": "/js/hue-select.js",
"md5": "0a58f1a339af5c54aecfc5ce1aac7168",
"size": 6367
},
{
"remote": "data/js/jquery-3.7.1.js",
"local": "/js/jquery-3.7.1.js",
"md5": "fdb81281d3773a7462998fdddbe6f5bf",
"size": 196885
},
{
"remote": "data/system/ble.json",
"local": "/system/ble.json",
"md5": "faebefd5b50046a01d4a8aa27f8504d0",
"size": 307
},
{
"remote": "data/system/readme.txt",
"local": "/system/readme.txt",
"md5": "198c92a2cc06b3effce3b5ba313cc6a0",
"size": 86
},
{
"remote": "data/system/tunes.json",
"local": "/system/tunes.json",
"md5": "814999e88296bee179cef5d394f3f696",
"size": 1149
},
{
"remote": "data/system/update.json",
"local": "/system/update.json",
"md5": "01cc6a1935601085df49308cab646673",
"size": 156
},
{
"remote": "data/www/about.html",
"local": "/www/about.html",
"md5": "23f0c991c5c67e7eefe6c24aa50b59fb",
"size": 4222
},
{
"remote": "data/www/edit.html",
"local": "/www/edit.html",
"md5": "fed238dcf87d3797b08ed371330ea800",
"size": 5546
},
{
"remote": "data/www/edit_old.html",
"local": "/www/edit_old.html",
"md5": "9aafba533ac77352d30841cdc34db0c4",
"size": 4240
},
{
"remote": "data/www/failed.html",
"local": "/www/failed.html",
"md5": "a24025d56bef1cd2ed5375f6e8853dde",
"size": 828
},
{
"remote": "data/www/files.html",
"local": "/www/files.html",
"md5": "52d82d4d23b038929691cd0fb20e8ee0",
"size": 9369
},
{
"remote": "data/www/home.html",
"local": "/www/home.html",
"md5": "767fd73b733d8e37dc79252fcd20b610",
"size": 7822
},
{
"remote": "data/www/index.html",
"local": "/www/index.html",
"md5": "af690aaa4dec02691ffdb2765c837370",
"size": 806
},
{
"remote": "data/www/lights.html",
"local": "/www/lights.html",
"md5": "272f8dc423923bb5026837240f654efe",
"size": 19056
},
{
"remote": "data/www/navbar.html",
"local": "/www/navbar.html",
"md5": "89af40b990e297e41e116105b04b66ee",
"size": 533
},
{
"remote": "data/www/ok.html",
"local": "/www/ok.html",
"md5": "db42bd2ff52293c3b4d6ef62fb4e3a42",
"size": 865
},
{
"remote": "data/www/setup.html",
"local": "/www/setup.html",
"md5": "19e1f419844852800757ccd36bb7892a",
"size": 11349
},
{
"remote": "data/www/upgrade.html",
"local": "/www/upgrade.html",
"md5": "f4a3efb67be66b5214d905e605984c93",
"size": 9080
},
{
"remote": "data/www/wifi.html",
"local": "/www/wifi.html",
"md5": "c6e55946a02cb0ff28689dd10d265987",
"size": 5320
}
]
}

View File

@ -73,7 +73,6 @@ def update_json_file(json_array: List[dict], folder_path: str) -> None:
json_array.clear()
json_array.extend(file_array)
def perform_data_copy(src_path: str, dest_path: str, skip_dirs: Optional[Iterable[str]] = None, skip_files: Optional[Iterable[str]] = None) -> bool:
# Check if the source folder exists
if not os.path.isdir(src_path):

View File

@ -60,7 +60,8 @@ void Lights_Set_ON(void);
void Lights_Set_OFF(void);
void Lights_Set_Brightness(uint8_t scale);
void Lights_Set_White(uint8_t val);
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3);
//void createFirePalette(CRGBPalette16& palette, COLOR_PACK& colorPack);
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack);
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src);

View File

@ -11,6 +11,15 @@
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
#define BUFFER_SIZE 4096
// Maximum allowed manifest size (bytes) to protect memory
#define MAX_MANIFEST_SIZE (64 * 1024)
// Number of HTTP retry attempts for transient failures
#define HTTP_RETRY_COUNT 3
#define HTTP_RETRY_DELAY_MS 500
// Allow external cancellation
extern volatile bool g_UpdateCancelFlag;
extern TaskHandle_t Update_Task_Handle;
@ -186,7 +195,6 @@ void sendUpdateMessage(const char* message, bool complete, int progress);
void handleUpdateProgress(AsyncWebServerRequest *request);
bool checkManifest(Version& remoteVersion);
void startVersionCheckTask();

View File

@ -8,6 +8,21 @@ typedef struct {
CRGB col[8];
} COLOR_PACK;
const COLOR_PACK colorPack_FireRed PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };
const COLOR_PACK colorPack_FireGreen PROGMEM = { 4, { CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen, CRGB::Black } };
const COLOR_PACK colorPack_FireBlue PROGMEM = { 4, { CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue, CRGB::Black } };
const COLOR_PACK colorPack_FireViolet PROGMEM = { 4, { CRGB::Purple, CRGB::Blue, CRGB::Violet, CRGB::Black } };
// Fire (compacted: single PROGMEM array, removes duplicate size constants)
const COLOR_PACK fireColorPacks[] PROGMEM = {
colorPack_FireRed,
colorPack_FireGreen,
colorPack_FireBlue,
colorPack_FireViolet
};
// Sectors
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } };
const COLOR_PACK colorPack_USA PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } };
@ -15,6 +30,16 @@ const COLOR_PACK colorPack_MEXICO PROGMEM = { 3, { CRGB::Green, CRGB::White, CRG
const COLOR_PACK colorPack_CANADA PROGMEM = { 2, { CRGB::Red, CRGB::White } };
const COLOR_PACK colorPack_GERMANY PROGMEM = { 3, { CRGB::Black, CRGB::Red, CRGB::Yellow } };
const COLOR_PACK combo_colorPacks[] PROGMEM = {
colorPack_RAINBOW,
colorPack_USA,
colorPack_MEXICO,
colorPack_CANADA,
colorPack_GERMANY
};
// Single Colors
const COLOR_PACK colorPack_Single_Red PROGMEM = { 1, { CRGB::Red } };
const COLOR_PACK colorPack_Single_Orange PROGMEM = { 1, { CRGB::OrangeRed } };
@ -25,6 +50,21 @@ const COLOR_PACK colorPack_Single_Viloet PROGMEM = { 1, { CRGB::DarkViolet } };
const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } };
const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } };
const COLOR_PACK single_colorPacks[] PROGMEM = {
colorPack_Single_Red,
colorPack_Single_Orange,
colorPack_Single_Yellow,
colorPack_Single_Green,
colorPack_Single_Blue,
colorPack_Single_Viloet,
colorPack_Single_Magenta,
colorPack_Single_White
};
// Dashes
const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } };
const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } };
@ -35,8 +75,20 @@ const COLOR_PACK colorPack_IndigoBlack PROGMEM = { 2, { CRGB::Indigo, CRGB::Blac
const COLOR_PACK colorPack_VioletBlack PROGMEM = { 2, { CRGB::MediumVioletRed, CRGB::Black } };
const COLOR_PACK colorPack_WhiteBlack PROGMEM = { 2, { CRGB::White, CRGB::Black } };
const COLOR_PACK dashes_ColorPacks[] PROGMEM = {
colorPack_RedBlack,
colorPack_OrangeBlack,
colorPack_YellowBlack,
colorPack_GreenBlack,
colorPack_BlueBlack,
colorPack_IndigoBlack,
colorPack_VioletBlack,
colorPack_WhiteBlack
};
const COLOR_PACK fire PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } };

View File

@ -5,9 +5,9 @@
#include "appVersion.h"
//#define FIRMWARE_VERSION "1.0.0"
#define FIRMWARE_VERSION_MAJOR 1
#define FIRMWARE_VERSION_MINOR 4
#define FIRMWARE_VERSION_PATCH 4
//#define FIRMWARE_VERSION_MAJOR 1
//#define FIRMWARE_VERSION_MINOR 4
//#define FIRMWARE_VERSION_PATCH 4
extern Version localVersion;

28
include/version.h Normal file
View File

@ -0,0 +1,28 @@
#define FIRMWARE_VERSION_MAJOR 1
#define FIRMWARE_VERSION_MINOR 4
#define FIRMWARE_VERSION_PATCH 9
#define FIRMWARE_DESCRIPTION \
"Boothifier firmware\n" \
"Designed for modular booth control\n" \
"Supports headless operation and OTA updates\n" \
"Optimized for ARM-based systems\n"
// Changelog format:
// Each version starts with @version <version>
// Each change is prefixed with -
// Use \n for line breaks and \ at the end of each line to continue the macro
#define FIRMWARE_CHANGELOG \
"@version 1.4.3\n" \
"- Improved performance on ARM\n" \
"- Fixed memory leak\n" \
"- Updated UI theme\n" \
"@version 1.4.2\n" \
"- Added headless mode\n" \
"- Improved connection stability\n" \
"@version 1.4.1\n" \
"- Fixed UI scaling on ARM\n"

View File

@ -375,87 +375,40 @@ void Lights_Control_Task(void *parameters){
case 1:
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30);
break;
case 2:
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, ledSettings[0].shift);
case 2: case 3: case 4: { // Timed Fill Animations
int timeDuration = (AnimEvent.AnimationIndex-1) * 1000;
Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 3:
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, ledSettings[0].shift);
whiteTimeout = 20;
break;
case 5:
createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange);
}
case 5: case 6: case 7: case 8: {// Fire Animations
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 5]; // copy const pack to mutable
createFirePalette(firePalette, fp);
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, ledSettings[0].shift);
}
case 9: case 10: case 11: case 12: case 13: {//
COLOR_PACK ccp = combo_colorPacks[AnimEvent.AnimationIndex - 9]; // copy const pack to mutable
//loadColorPack(colorPack, colorPack_USA);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, ccp, 1, 80);
break;
case 7:
createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
}
case 14: case 15: case 16: case 17: case 18: {
COLOR_PACK dcp = dashes_ColorPacks[AnimEvent.AnimationIndex - 14]; // copy const pack to mutable
//loadColorPack(colorPack, colorPack_RedBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, dcp, 4, 80);
break;
case 8:
createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet);
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
}
case 19: case 20: case 21: case 22: case 23: {
int idx = AnimEvent.AnimationIndex - 19;
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, combo_colorPacks[idx], 80, RANDOM_DECAY, true, 1);
break;
case 9:
loadColorPack(colorPack, colorPack_USA);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
break;
case 10:
loadColorPack(colorPack, colorPack_MEXICO);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
break;
case 11:
loadColorPack(colorPack, colorPack_RedBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
break;
case 12:
loadColorPack(colorPack, colorPack_YellowBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
break;
case 13:
loadColorPack(colorPack, colorPack_GreenBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
break;
case 14:
loadColorPack(colorPack, colorPack_BlueBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
break;
case 15:
loadColorPack(colorPack, colorPack_VioletBlack);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80);
break;
case 16:
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
break;
case 17:
loadColorPack(colorPack, colorPack_USA);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, LINEAR_DECAY, true, 1);
break;
case 18:
loadColorPack(colorPack, colorPack_USA);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, false, 1);
break;
case 19:
loadColorPack(colorPack, colorPack_USA);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 2);
break;
case 20:
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1);
break;
case 21:
}
case 24:
loadColorPack(colorPack, colorPack_RAINBOW);
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 7000, 90);
break;
case 22:
case 25:
loadColorPack(colorPack, colorPack_USA);
Anim_GradientRotate(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 50);
break;
@ -472,16 +425,17 @@ void Lights_Control_Task(void *parameters){
}
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) {
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack) {
for (uint8_t i = 0; i < 16; i++) {
if (i < 3) palette[i] = CRGB::Black;
else if (i < 7) palette[i] = color1;
else if (i < 10) palette[i] = color2;
else if (i < 15) palette[i] = color3;
else if (i < 7) palette[i] = colorPack.col[0];
else if (i < 10) palette[i] = colorPack.col[1];
else if (i < 15) palette[i] = colorPack.col[2];
else palette[i] = CRGB::White;
}
}
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) {
memcpy_P(&dest, &src, sizeof(COLOR_PACK));
}

View File

@ -574,6 +574,63 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const
}
void Anim_ColorWipe(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) {
if (!leds || size <= 0 || colors.size <= 0) return;
int currentIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Wipe color across the strip
fill_solid(leds, size, colors.col[currentIndex]);
FastLED.show();
// Move to the next color
currentIndex = (currentIndex + 1) % colors.size;
return 0;
});
}
void Anim_Sparkle(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t sparkleChance) {
if (!leds || size <= 0 || colors.size <= 0) return;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Randomly light up LEDs with colors from the color pack
for (int i = 0; i < size; i++) {
if (getRandomValue(100) < sparkleChance) {
int colorIndex = getRandomValue(colors.size);
leds[i] = colors.col[colorIndex];
} else {
leds[i] = CRGB::Black; // Turn off LED
}
}
FastLED.show();
return 0;
});
}
void Anim_TheaterChase(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t spacing) {
if (!leds || size <= 0 || colors.size <= 0 || spacing == 0) return;
int colorIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Clear all LEDs
fill_solid(leds, size, CRGB::Black);
// Light up every 'spacing' LED with the current color
for (int i = 0; i < size; i += spacing) {
leds[i] = colors.col[colorIndex];
}
FastLED.show();
// Move to the next color
colorIndex = (colorIndex + 1) % colors.size;
return 0;
});
}
uint32_t getRandomValue(uint32_t maxValue) {
return esp_random() % maxValue;
}

View File

@ -8,10 +8,12 @@
#include "BLE_UpdateService.h"
#include <HTTPClient.h>
#include <Update.h>
#include <cstring>
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
volatile bool g_UpdateCancelFlag = false; // cancellation flag
// Queue handle for firmware update messages
//QueueHandle_t updateMsgQueue = NULL;
@ -45,19 +47,30 @@ bool AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and Send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
String payload;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
payload = http.getString();
http.end();
break;
}
ESP_LOGW(TAG, "Manifest GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if(payload.isEmpty()){
ESP_LOGE(TAG, "Failed to fetch manifest after retries");
return false;
}
// Read the response
String payload = http.getString();
http.end();
if(payload.length() > MAX_MANIFEST_SIZE){
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
return false;
}
// Parse JSON
DeserializationError error = deserializeJson(jsonManifest, payload);
@ -116,9 +129,13 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
String url = buildUrl(remotePath);
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
String localMd5 = getLocalMD5(localPath);
if (localMd5.equals(expectedMd5)) {
// Quick skip: if exists and size & MD5 match
bool skip = false;
if(fileSystem.exists(localPath)){
String localMd5 = getLocalMD5(localPath);
if(localMd5.equals(expectedMd5)) skip = true;
}
if(skip){
ESP_LOGI(TAG, "File already up to date: %s", localPath);
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
return true;
@ -126,12 +143,19 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
// Start the download
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
int httpCode = -1;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
http.begin(url);
httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) break;
ESP_LOGW(TAG, "File GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
http.end();
return false;
}
@ -172,6 +196,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
if (contentLength > 0) {
// Single pass with known content length
while (totalRead < contentLength) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
size_t available = stream->available();
if (available) {
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
@ -187,13 +212,14 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
}
yield();
}
} else {
// Unknown content length: read until stream ends
for (;;) {
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (readLen == 0) {
break;
@ -207,7 +233,10 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
// Progress unknown; emit periodic heartbeats at 0%
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
// For unknown size, send heartbeats every ~16KB
if((totalRead & 0x3FFF) == 0){
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
}
yield();
}
}
@ -217,12 +246,15 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
String calculatedMd5 = md5.toString();
// Verify MD5 hash
updateProgress(UpdateStatus::VERIFYING, 90, localPath);
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
updateProgress(UpdateStatus::VERIFYING, 95, localPath);
// Replace original file with verified temp file
if (fileSystem.exists(localPath)) {
fileSystem.remove(localPath);
@ -233,6 +265,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
return false;
}
updateProgress(UpdateStatus::VERIFYING, 100, localPath);
return true;
}
@ -266,8 +299,16 @@ bool AppUpdater::updateFilesArray() {
// Iterate over each file entry in the manifest
for (JsonObject file : jsonFilesArray) {
const char* remotePath = file["remote"];
const char* localPath = file["local"];
const char* remotePath = file["path"];
const char* localPath = remotePath;
// If path begins with "data/" or "/data/" strip only the "data" portion, retaining the leading slash
if (localPath) {
if (strncmp(localPath, "data/", 5) == 0) {
localPath += 4; // points to '/'
} else if (strncmp(localPath, "/data/", 6) == 0) {
localPath += 5; // points to '/'
}
}
const char* expectedMd5 = file["md5"];
// Skip invalid entries
@ -302,12 +343,19 @@ bool AppUpdater::updateApp() {
// Download the firmware
HTTPClient http;
http.begin(firmwareUrl);
int httpCode = http.GET();
int httpCode = -1;
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
if(g_UpdateCancelFlag) return false;
http.begin(firmwareUrl);
httpCode = http.GET();
if(httpCode == HTTP_CODE_OK) break;
ESP_LOGW(TAG, "Firmware GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
http.end();
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
http.end();
return false;
}
@ -329,6 +377,7 @@ bool AppUpdater::updateApp() {
if (firmwareSize > 0) {
size_t remaining = firmwareSize;
while (remaining > 0) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
@ -355,6 +404,7 @@ bool AppUpdater::updateApp() {
} else {
// Unknown size: stream until end
for (;;) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (read == 0) break;
md5.add(downloadBuffer.get(), read);
@ -371,6 +421,7 @@ bool AppUpdater::updateApp() {
// Verify MD5
md5.calculate();
String calculatedMd5 = md5.toString();
updateProgress(UpdateStatus::VERIFYING, 95, "firmware");
if (!calculatedMd5.equals(expectedMd5)) {
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
@ -388,7 +439,7 @@ bool AppUpdater::updateApp() {
}
http.end();
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
updateProgress(UpdateStatus::COMPLETE, 100, "Firmware: Complete");
return true;
}
@ -428,7 +479,7 @@ void firmwareUpdateTask(void* parameter) {
loadUpdateJson();
// Initialize updater
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
updater->setProgressCallback(updateProgress);
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
@ -459,7 +510,6 @@ void firmwareUpdateTask(void* parameter) {
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
end:
delete updater;
Update_Task_Handle = NULL;
vTaskDelete(NULL);
@ -474,15 +524,16 @@ void startVersionCheckTask() {
}
void versionCheckTask(void* parameter){
if(updateUrl == ""){
loadUpdateJson();
}
if(checkManifest(otaVersion) == false){
ESP_LOGE(TAG, "Error checking manifest");
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
if(!updater.checkManifest()){
ESP_LOGE(TAG, "Version check: manifest fetch failed");
} else {
otaVersion = updater.otaVersion; // capture remote
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
}
versionCheckTask_Handle = NULL;
vTaskDelete(NULL);
}
@ -589,51 +640,7 @@ void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
bleUpgrade_send_message(message);
}
bool checkManifest(Version& remoteVersion) {
const char* TAG = "manifestCheck";
String url = updateUrl + "update.json";
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
//ESP_LOGD(TAG, "%s", payload.c_str());
// Parse JSON
JsonDocument jsonManifest;
DeserializationError error = deserializeJson(jsonManifest, payload);
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
remoteVersion = {major, minor, patch};
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
return true;
}
// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
/*
void setup() {

View File

@ -60,6 +60,7 @@ uint8_t calculateChecksum(const uint8_t bArr[]) {
}
// Class for handling characteristic events
/*
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic *pCharacteristic) override {
std::string rawValue = pCharacteristic->getValue();
@ -75,6 +76,54 @@ class SP110ECallbacks : public NimBLECharacteristicCallbacks {
process_BLE_SP110E_Command(value, length, pCharacteristic);
}
};
*/
// Class for handling characteristic events
class SP110ECallbacks : public NimBLECharacteristicCallbacks {
public:
void onWrite(NimBLECharacteristic* pCharacteristic) override {
if (!pCharacteristic) return;
std::string raw = pCharacteristic->getValue(); // NimBLE copies internally
size_t len = raw.size();
if (len == 0) {
ESP_LOGW(tag, "Write received with zero-length payload");
return;
}
const uint8_t* data = reinterpret_cast<const uint8_t*>(raw.data());
// Log up to first 16 bytes to avoid log spam
//logBytes(data, len);
// Clamp length passed to command processor (its param is uint8_t)
uint8_t procLen = static_cast<uint8_t>(len > 255 ? 255 : len);
// Forward to any subscribed mirror clients first (raw payload)
sendToAllClients(data, procLen);
// Process command (may generate response/notify on same characteristic)
process_BLE_SP110E_Command(data, procLen, pCharacteristic);
}
private:
static void logBytes(const uint8_t* data, size_t len) {
if (!data) return;
char buf[3 * 16 + 1];
size_t toPrint = len > 16 ? 16 : len;
char* p = buf;
for (size_t i = 0; i < toPrint; ++i) {
sprintf(p, "%02X ", data[i]);
p += 3;
}
*p = 0;
if (len > toPrint) {
ESP_LOGI(tag, "Data (%zu bytes): %s...", len, buf);
} else {
ESP_LOGI(tag, "Data (%zu bytes): %s", len, buf);
}
}
};
class LightStickCallbacks : public NimBLECharacteristicCallbacks {
void onRead(NimBLECharacteristic *pCharacteristic) override {
@ -83,7 +132,34 @@ class LightStickCallbacks : public NimBLECharacteristicCallbacks {
}
};
// Function to send data to all connected clients in chunks based on MTU
void sendToAllClients(const uint8_t* data, size_t len) {
if (!pStickCharacteristic || !data || len == 0) return;
// Skip if no subscribed clients (if API available)
#if defined(NIMBLE_INCLUDED) || true
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
if (pStickCharacteristic->getSubscribedCount() == 0) return;
#endif
#endif
// Determine a safe chunk size based on (last) negotiated MTU; fallback to 20.
uint16_t mtu = NimBLEDevice::getMTU(); // Typically 23 default -> 20 payload
size_t maxChunk = (mtu > 3) ? (mtu - 3) : 20;
if (maxChunk == 0) maxChunk = 20;
size_t offset = 0;
while (offset < len) {
size_t chunk = len - offset;
if (chunk > maxChunk) chunk = maxChunk;
pStickCharacteristic->setValue(data + offset, chunk);
// notify() returns void in this NimBLE version, so just call it without checking a return value
pStickCharacteristic->notify();
offset += chunk;
}
}
/*
void sendToAllClients(const uint8_t *data, size_t len) {
// Check if the characteristic is valid and has subscribed clients
if (pStickCharacteristic != nullptr) {
@ -92,6 +168,7 @@ void sendToAllClients(const uint8_t *data, size_t len) {
pStickCharacteristic->notify();
}
}
*/
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
@ -115,6 +192,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
//ESP_LOGI(tag, "Lights OFF");
break;
case SET_STATIC_COLOR:
if(len < 7) {
ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
break;
}
led_status.red = val[1];
led_status.green = val[2];
led_status.blue = val[0];
@ -122,6 +203,10 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
//ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
break;
case SET_BRIGHT:
if(len < 5) {
ESP_LOGW(tag, "SET_BRIGHT command requires 1 parameter (brightness)");
break;
}
led_status.bright = val[0];
Lights_Set_Brightness(val[0]);
//ESP_LOGI(tag, "Bright set to %d", led_status.bright);

View File

@ -5,6 +5,7 @@
#include "AppUpgrade.h"
#include "AppVersion.h"
#include "BleSettings.h"
#include "version.h"
static const char *tag = "BLE_UpdateService";

View File

@ -8,6 +8,7 @@ static const char* tag = "BleServer";
// Class for handling server events
/*
class ServerCallbacks : public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) override {
ESP_LOGI(tag, "Client connected");
@ -31,7 +32,40 @@ class ServerCallbacks : public NimBLEServerCallbacks {
}
}
};
*/
class ServerCallbacks : public NimBLEServerCallbacks {
public:
void onConnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client connected");
ensureAdvertising("onConnect");
}
void onDisconnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client disconnected");
ensureAdvertising("onDisconnect");
}
private:
void ensureAdvertising(const char* reason) {
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
if (!adv) {
ESP_LOGE(tag, "[%s] Advertising object unavailable", reason);
return;
}
if (adv->isAdvertising()) {
ESP_LOGD(tag, "[%s] Advertising already running", reason);
return;
}
if (adv->start()) {
ESP_LOGI(tag, "[%s] Advertising (re)started", reason);
} else {
ESP_LOGE(tag, "[%s] Failed to start advertising", reason);
}
}
};
/*
void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
@ -72,3 +106,56 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGE(tag, "Failed to get advertising object");
}
}
*/
void Init_BleServer(bool isSP110EActive, bool isUpgradeActive) {
ESP_LOGI(tag, "Initializing BLE...");
// Initialize BLE device only once
static bool deviceInitialized = false;
if (!deviceInitialized) {
NimBLEDevice::init(BTDeviceName.c_str());
deviceInitialized = true;
}
// Create server only once
static NimBLEServer* pServer = nullptr;
if (!pServer) {
pServer = NimBLEDevice::createServer();
if (!pServer) {
ESP_LOGE(tag, "Failed to create BLE server");
return;
}
static ServerCallbacks serverCallbacks;
pServer->setCallbacks(&serverCallbacks);
}
// Add services only once (no removal logic if later flags become false)
static bool sp110eAdded = false;
if (isSP110EActive && !sp110eAdded) {
Init_BLE_SP110E(pServer);
sp110eAdded = true;
ESP_LOGI(tag, "SP110E service initialized");
}
static bool upgradeAdded = false;
if (isUpgradeActive && !upgradeAdded) {
Init_UpgradeBLEService(pServer);
upgradeAdded = true;
ESP_LOGI(tag, "Upgrade service initialized");
}
// Start / ensure advertising
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
if (!adv) {
ESP_LOGE(tag, "Failed to get advertising object");
return;
}
if (!adv->isAdvertising()) {
if (!adv->start()) {
ESP_LOGE(tag, "Failed to start advertising");
} else {
ESP_LOGI(tag, "Advertising started");
}
}
}

View File

@ -1,5 +1,11 @@
#include "ColorPalettes.h"
extern const COLOR_PACK combo_colorPacks[] PROGMEM;
extern const COLOR_PACK single_colorPacks[] PROGMEM;
extern const COLOR_PACK fireColorPacks[] PROGMEM;
extern const COLOR_PACK dashes_ColorPacks[] PROGMEM;
void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette) {
customPalette = CRGBPalette16(
CRGB::Red, CRGB::Yellow, CRGB::Violet,

View File

@ -6,6 +6,7 @@
#include "JsonConstrain.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "version.h"
static const char* tag = "global";
const char* appName;

View File

@ -222,19 +222,6 @@ void loop()
boardButtons[i]->tick();
}
}
/*
{
boardButtons[0]->tick();
}
if (boardButtons[1] != NULL)
{
boardButtons[1]->tick();
}
if (boardButtons[2] != NULL)
{
boardButtons[2]->tick();
}
*/
}
// Temperature Monitor
@ -246,7 +233,7 @@ void loop()
if (sys_settings.tSensorSettings.enabled)
{
boardTemperature = tSensor->readTemperatureF();
// ESP_LOGD(tag, "Board T: %F", boardTemperature);
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
}
// Fan Control
@ -327,10 +314,12 @@ void loop()
// Turn off white light after timeout
ON_EVERY_N_MILLISECONDS(100)
{
if(whiteTimeout > 0){
// Only decrement if timeout is active
if (whiteTimeout > 0) {
whiteTimeout--;
if(whiteTimeout == 0){
if (whiteTimeout == 0) {
Lights_Set_White(0);
ESP_LOGD(tag, "White light timeout triggered");
}
}
}

View File

@ -44,7 +44,7 @@ String dirDropdownOptions((char *)0);
String savePath((char *)0); // needed for storing file when editing a file
String savePathInput((char *)0);
const char *http_username = "admin";
const char *http_password = "admin";
const char *http_password = "12345678";
const char *param_delete_path = "delete-path";
const char *param_edit_path = "edit-path";
const char *param_dir_pad = "dir-path";
@ -109,6 +109,53 @@ void Wifi_Init()
// Wifi_Scan_for_Networks();
}
void Wifi_Load_Settings(String path)
{
// Load WiFi settings
File file = LittleFS.open(path, "r");
if (!file)
{
ESP_LOGE(tag, "Error opening %s", path.c_str());
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error)
{
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
return;
}
JsonObject wifiJson = doc.as<JsonObject>();
if (wifiJson.isNull())
{
ESP_LOGE(tag, "%s is empty", path.c_str());
return;
}
// Load AP settings
JsonObject apJson = wifiJson["wifi-ap"];
if (!apJson.isNull())
{
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
}
// Load Client settings
JsonObject clientJson = wifiJson["wifi-client"];
if (!apJson.isNull())
{
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
}
}
bool StartWifiConnectTask(String ssid = "", String pass = "")
{
if (ssid.isEmpty() || pass.length() < 8)
@ -257,53 +304,6 @@ bool Wifi_Save_Credentials(String path)
return true;
}
void Wifi_Load_Settings(String path)
{
// Load WiFi settings
File file = LittleFS.open(path, "r");
if (!file)
{
ESP_LOGE(tag, "Error opening %s", path.c_str());
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error)
{
ESP_LOGE(tag, "Failed to deserialize %s", path.c_str());
return;
}
JsonObject wifiJson = doc.as<JsonObject>();
if (wifiJson.isNull())
{
ESP_LOGE(tag, "%s is empty", path.c_str());
return;
}
// Load AP settings
JsonObject apJson = wifiJson["wifi-ap"];
if (!apJson.isNull())
{
ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP");
ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678");
local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1"));
gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1"));
subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0"));
}
// Load Client settings
JsonObject clientJson = wifiJson["wifi-client"];
if (!apJson.isNull())
{
client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none");
client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678");
}
}
void Wifi_Scan_for_Networks()
{
// Start a scan for available networks
@ -577,11 +577,19 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
// Firmware Update Handlers
server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request)
{
//String newVersion;
loadUpdateJson();
//bool avai = checkManifest(FIRMWARE_VERSION, newVersion);
checkManifest(otaVersion);
bool avail = otaVersion > localVersion;
// Ensure updateUrl is loaded (function resides in AppUpgrade.cpp)
loadUpdateJson();
// Pass nullptr bucket to use internally loaded default + subsequently set base via setBaseUrl if needed
AppUpdater updater(LittleFS, localVersion, nullptr, "update.json", "firmware.bin");
// If a dynamic URL was loaded, override base
extern String updateUrl; // declared in AppUpgrade.cpp
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
if(!updater.checkManifest()){
ESP_LOGE(tag, "Manifest check failed via /upgrade/check");
} else {
otaVersion = updater.otaVersion;
}
bool avail = otaVersion > localVersion;
JsonDocument doc;
doc["currentVersion"] = localVersion.toString();

View File

@ -0,0 +1,673 @@
#include "AppUpgrade.h"
#include "esp_log.h"
#include <MD5Builder.h>
#include <LittleFS.h>
#include <memory>
#include "global.h"
#include "JsonConstrain.h"
#include "BLE_UpdateService.h"
#include <HTTPClient.h>
#include <Update.h>
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
// Queue handle for firmware update messages
//QueueHandle_t updateMsgQueue = NULL;
String updateUrl = "";
Version otaVersion;
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
: localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
{
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
// Ensure baseUrl ends with a single '/'
if(!baseUrl.endsWith("/")) baseUrl += "/";
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
}
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
progressCb = callback;
}
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
status = newStatus;
if (progressCb) {
progressCb(status, percentage, message);
}
}
bool AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and Send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
// Parse JSON
DeserializationError error = deserializeJson(jsonManifest, payload);
ESP_LOGD(TAG, "Manifest deserialized");
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for files section
jsonFilesArray = jsonManifest["files"];
if (jsonFilesArray.isNull()) {
ESP_LOGE(TAG, "No files section in manifest");
return false;
}else{
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
ESP_LOGD(TAG, "Version section found");
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
otaVersion = {major, minor, patch};
//Version localVersion;
//::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch);
// Check if an update is available
updateAvailable = false;
// Only mark update available if remote is strictly newer than local
if (otaVersion <= localVersion) {
ESP_LOGI(TAG, "No updates available");
return false;
}else{
updateAvailable = true;
ESP_LOGD(TAG, "Update available");
}
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
return true;
}
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
// Construct full URL
String url = buildUrl(remotePath);
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
String localMd5 = getLocalMD5(localPath);
if (localMd5.equals(expectedMd5)) {
ESP_LOGI(TAG, "File already up to date: %s", localPath);
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
return true;
}
// Start the download
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
http.end();
return false;
}
// Get the stream and content length
WiFiClient* stream = http.getStreamPtr();
size_t contentLength = http.getSize();
// Verify and save the file
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
http.end();
if(!success){
updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed");
}else{
updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
}
return success;
}
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
{
MD5Builder md5;
md5.begin();
size_t totalRead = 0;
// Create temporary filename
String tempPath = String(localPath) + ".tmp";
// Open temporary file for writing
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
if (!file) {
ESP_LOGE(TAG, "Failed to open temporary file for writing");
return false;
}
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
if (contentLength > 0) {
// Single pass with known content length
while (totalRead < contentLength) {
size_t available = stream->available();
if (available) {
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
// Write to temp file and update MD5
if (file.write(downloadBuffer.get(), readLen) != readLen) {
ESP_LOGE(TAG, "Failed to write to temporary file");
file.close();
fileSystem.remove(tempPath.c_str());
return false;
}
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
}
yield();
}
} else {
// Unknown content length: read until stream ends
for (;;) {
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (readLen == 0) {
break;
}
if (file.write(downloadBuffer.get(), readLen) != readLen) {
ESP_LOGE(TAG, "Failed to write to temporary file");
file.close();
fileSystem.remove(tempPath.c_str());
return false;
}
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
// Progress unknown; emit periodic heartbeats at 0%
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
yield();
}
}
file.close();
md5.calculate();
String calculatedMd5 = md5.toString();
// Verify MD5 hash
if (!calculatedMd5.equals(expectedMd5)) {
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
fileSystem.remove(tempPath.c_str());
return false;
}
// Replace original file with verified temp file
if (fileSystem.exists(localPath)) {
fileSystem.remove(localPath);
}
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
ESP_LOGE(TAG, "Failed to rename temporary file");
fileSystem.remove(tempPath.c_str());
return false;
}
return true;
}
String AppUpdater::getLocalMD5(const char* filePath){
File file = fileSystem.open(filePath, "r");
if(!file){
ESP_LOGE(TAG, "Error opening %s...", filePath);
return String();
}
MD5Builder md5Builder;
md5Builder.begin();
size_t fileSize = file.size();
size_t totalRead = 0;
size_t readLen = 0;
while (totalRead < fileSize) {
readLen = file.readBytes(reinterpret_cast<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
md5Builder.add(downloadBuffer.get(), readLen);
totalRead += readLen;
}
md5Builder.calculate();
file.close();
return md5Builder.toString();
}
bool AppUpdater::updateFilesArray() {
int successCount = 0;
int totalFiles = jsonFilesArray.size();
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
// Iterate over each file entry in the manifest
for (JsonObject file : jsonFilesArray) {
const char* remotePath = file["remote"];
const char* localPath = file["local"];
const char* expectedMd5 = file["md5"];
// Skip invalid entries
if (!remotePath || !localPath || !expectedMd5) {
ESP_LOGE(TAG, "Invalid file entry in manifest");
continue;
}
// Attempt to update the file
if (updateFile(remotePath, localPath, expectedMd5)) {
successCount++;
}
}
ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
return successCount == totalFiles;
}
bool AppUpdater::updateApp() {
updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update");
// Check for firmware section in manifest
if (!jsonManifest["firmware"].is<JsonObject>() || !jsonManifest["firmware"]["md5"].is<const char*>()) {
ESP_LOGE(TAG, "Invalid firmware section in manifest");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
return false;
}
// Get the firmware MD5 hash and URL
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
String firmwareUrl = buildUrl(appName);
// Download the firmware
HTTPClient http;
http.begin(firmwareUrl);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
http.end();
return false;
}
// Check available space
size_t firmwareSize = http.getSize();
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
ESP_LOGE(TAG, "Firmware: Not enough space for update");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
http.end();
return false;
}
// Set up MD5 checking
MD5Builder md5;
md5.begin();
// Download and verify firmware
WiFiClient* stream = http.getStreamPtr();
if (firmwareSize > 0) {
size_t remaining = firmwareSize;
while (remaining > 0) {
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
// Check for timeout
if (read == 0) {
ESP_LOGE(TAG, "Read timeout");
Update.abort();
http.end();
return false;
}
// Update MD5 and write firmware
md5.add(downloadBuffer.get(), read);
if (Update.write(downloadBuffer.get(), read) != read) {
ESP_LOGE(TAG, "Write failed");
Update.abort();
http.end();
return false;
}
remaining -= read;
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
}
} else {
// Unknown size: stream until end
for (;;) {
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
if (read == 0) break;
md5.add(downloadBuffer.get(), read);
if (Update.write(downloadBuffer.get(), read) != read) {
ESP_LOGE(TAG, "Write failed");
Update.abort();
http.end();
return false;
}
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
}
}
// Verify MD5
md5.calculate();
String calculatedMd5 = md5.toString();
if (!calculatedMd5.equals(expectedMd5)) {
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
Update.abort();
http.end();
return false;
}
// Finish update
if (!Update.end()) {
ESP_LOGE(TAG, "Update end failed");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
http.end();
return false;
}
http.end();
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
return true;
}
bool AppUpdater::IsUpdateAvailable(){
return updateAvailable;
}
String AppUpdater::buildUrl(const char* path) const {
if(!path || !*path) return baseUrl; // just base
String p(path);
// If already absolute URL, pass through
if(p.startsWith("http://") || p.startsWith("https://")) return p;
// Strip leading slashes to avoid double
while(p.startsWith("/")) p.remove(0,1);
// Ensure baseUrl has single trailing slash
String b = baseUrl;
if(!b.endsWith("/")) b += "/";
return b + p;
}
AsyncEventSource* eventProgress = nullptr;
void startFirmwareUpdateTask(AsyncEventSource* evProg) {
eventProgress = evProg;
if(Update_Task_Handle) {
ESP_LOGW(TAG, "Firmware update task already running");
return;
}
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
}
void firmwareUpdateTask(void* parameter) {
static const char* TAG = "UpdateTask";
AppUpdater* updater = nullptr;
try {
loadUpdateJson();
// Initialize updater
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
updater->setProgressCallback(updateProgress);
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
// Check and perform updates
if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); }
if (updater->IsUpdateAvailable()) {
ESP_LOGI(TAG, "Update available, updating files...");
if (!updater->updateFilesArray()) {
throw std::runtime_error("Failed to update files");
}
ESP_LOGI(TAG, "Updating firmware...");
if (!updater->updateApp()) {
throw std::runtime_error("Failed to update firmware");
}
ESP_LOGI(TAG, "Update successful, restarting...");
sendUpdateMessage("Restarting ", true, 100);
vTaskDelay(2000);
ESP.restart();
}
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
end:
delete updater;
Update_Task_Handle = NULL;
vTaskDelete(NULL);
}
void startVersionCheckTask() {
if(versionCheckTask_Handle != NULL) {
ESP_LOGW(TAG, "Version Check Tak already running");
return;
}
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
}
void versionCheckTask(void* parameter){
if(updateUrl == ""){
loadUpdateJson();
}
if(checkManifest(otaVersion) == false){
ESP_LOGE(TAG, "Error checking manifest");
}
versionCheckTask_Handle = NULL;
vTaskDelete(NULL);
}
void loadUpdateJson(void) {
try {
ESP_LOGD(TAG, "loadUpdateJaon function...");
if(updateUrl == "") {
String updateJsonPath = "/system/update.json";
// Read and parse update.json
File file = LittleFS.open(updateJsonPath);
if (!file) {
throw std::runtime_error("Failed to open update.json");
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) { throw std::runtime_error("Failed to parse update.json"); }
// Get update configuration
JsonObject jObj = doc.as<JsonObject>();
String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
updateUrl = baseUrl + folderName;
ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
}
} catch (const std::exception& e) {
ESP_LOGE(TAG, "Update failed: %s", e.what());
}
}
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
char buffer[128];
const char* msg;
bool isComplete = false;
const char* safeMsg = message ? message : "";
switch (newStatus) {
case AppUpdater::UpdateStatus::IDLE:
snprintf(buffer, sizeof(buffer), "Update idle");
msg = buffer;
break;
case AppUpdater::UpdateStatus::MESSAGE:
msg = message ? message : "";
break;
case AppUpdater::UpdateStatus::DOWNLOADING:
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::VERIFYING:
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SKIPPED:
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SAVED:
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::MD5_FAILED:
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::COMPLETE:
snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!");
msg = buffer;
isComplete = true;
break;
case AppUpdater::UpdateStatus::ERROR:
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
msg = buffer;
break;
default:
snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus);
msg = buffer;
break;
}
ESP_LOGI(TAG, "%s", msg);
sendUpdateMessage(msg, isComplete, percentage);
}
void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
if(eventProgress && eventProgress->count() > 0) {
JsonDocument jsonDoc;
jsonDoc["message"] = message;
jsonDoc["complete"] = complete;
jsonDoc["progress"] = progress;
String strMessage;
serializeJson(jsonDoc, strMessage);
eventProgress->send(strMessage.c_str(), "update", millis());
}
else{
ESP_LOGW(TAG, "No clients connected to event source");
}
bleUpgrade_send_message(message);
}
bool checkManifest(Version& remoteVersion) {
const char* TAG = "manifestCheck";
String url = updateUrl + "update.json";
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
// Start the HTTP client and send GET request for manifest
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
http.end();
return false;
}
// Read the response
String payload = http.getString();
http.end();
//ESP_LOGD(TAG, "%s", payload.c_str());
// Parse JSON
JsonDocument jsonManifest;
DeserializationError error = deserializeJson(jsonManifest, payload);
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
return false;
}
// Check for version section
JsonObject jsonVersion = jsonManifest["version"];
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
return false;
}
// Get the remote version
byte major = jsonVersion["major"] | 0;
byte minor = jsonVersion["minor"] | 0;
byte patch = jsonVersion["patch"] | 0;
remoteVersion = {major, minor, patch};
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
return true;
}
/*
void setup() {
Serial.begin(115200);
// Initialize WiFi connection first
// ... WiFi connection code ...
// Initialize filesystem
if(!LittleFS.begin()) {
Serial.println("LittleFS Mount Failed");
return;
}
// Create updater instance with:
// - Current version: "1.0.0"
// - Update server URL: "https://my-update-server.com/"
// - Filesystem: LittleFS
AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
// Set progress callback
updater.setProgressCallback([](int progress) {
Serial.printf("Update progress: %d%%\n", progress);
});
// Check and update firmware
if (updater.checkAndUpdate()) {
Serial.println("Update successful! Rebooting...");
ESP.restart();
}
// Update specific files from manifest
int updatedFiles = updater.updateFilesFromManifest("test_update.json");
Serial.printf("Updated %d files\n", updatedFiles);
}
*/