Compare commits
2 Commits
9e9c045a3f
...
12b5b25081
| Author | SHA1 | Date | |
|---|---|---|---|
| 12b5b25081 | |||
| 90ef654c80 |
22
data/system/animations.json
Normal file
22
data/system/animations.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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.');
|
||||
}
|
||||
456
firmware_update/UploadToMinio_direct.py
Normal file
456
firmware_update/UploadToMinio_direct.py
Normal 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()
|
||||
497
firmware_update/UploadToMinio_direct_V2.py
Normal file
497
firmware_update/UploadToMinio_direct_V2.py
Normal 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()
|
||||
549
firmware_update/UploadToMinio_direct_V3..py
Normal file
549
firmware_update/UploadToMinio_direct_V3..py
Normal 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()
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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 |
@ -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 |
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = ' #000000';
|
||||
} else if (hue === -1) {
|
||||
colorPatch.style.backgroundColor = 'rgb(255,255,255)';
|
||||
rgbHex.innerHTML = ' #FFFFFF';
|
||||
} else {
|
||||
colorPatch.style.backgroundColor = `hsl(${hue}, 100%, 50%)`;
|
||||
const hexColor = this.hslToRgb(hue, 100, 50).toUpperCase();
|
||||
rgbHex.innerHTML = `<span> ${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);
|
||||
|
||||
8673
firmware_update/latest/data/js/jquery-3.7.1.js
vendored
8673
firmware_update/latest/data/js/jquery-3.7.1.js
vendored
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "ATALIGHTS",
|
||||
"lights-service": "FFE0",
|
||||
"lights-char": "FFE1",
|
||||
"stick-char": "FFE2",
|
||||
"upgrade-service": "abcdef01-2345-6789-1234-56789abcdef0",
|
||||
"upgrade-char1": "abcdef01-2345-6789-1234-56789abcdef1",
|
||||
"upgrade-char2": "abcdef02-2345-6789-1234-56789abcdef1"
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
|
||||
#Setting up /system/system.json
|
||||
1 - Choose the
|
||||
modes:
|
||||
booth, roamer, stick
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
{
|
||||
"folder": "latest/",
|
||||
"baseurl": "https://s3-minio.boothwizard.com/boothifier/",
|
||||
"baseurl2": "https://storage.googleapis.com/boothifier/"
|
||||
}
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>  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>  
|
||||
|
||||
</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>
|
||||
@ -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°:</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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
<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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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.
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
0
firmware_update/other/GenUpdateToMinio.py
Normal file
0
firmware_update/other/GenUpdateToMinio.py
Normal 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):
|
||||
@ -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);
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 } };
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
28
include/version.h
Normal 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"
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "AppUpgrade.h"
|
||||
#include "AppVersion.h"
|
||||
#include "BleSettings.h"
|
||||
#include "version.h"
|
||||
|
||||
static const char *tag = "BLE_UpdateService";
|
||||
|
||||
|
||||
@ -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...");
|
||||
@ -71,4 +105,57 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) {
|
||||
} else {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
21
src/main.cpp
21
src/main.cpp
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
114
src/my_wifi.cpp
114
src/my_wifi.cpp
@ -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();
|
||||
|
||||
673
temporary/AppUpgrade orig.cpp
Normal file
673
temporary/AppUpgrade orig.cpp
Normal 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);
|
||||
}
|
||||
|
||||
*/
|
||||
Loading…
x
Reference in New Issue
Block a user