normal commit

This commit is contained in:
admin 2025-09-04 01:43:17 -07:00
parent 90ef654c80
commit 12b5b25081
51 changed files with 1578 additions and 14454 deletions

View File

@ -357,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]
});
@ -369,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(){

View File

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

View File

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

View File

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

View File

@ -1,508 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 15px;
}
.status-indicator-ble {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.btn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
}
button {
flex: 1;
max-width: 130px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background 0.3s ease;
}
button:disabled {
background-color: #ccc;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
textarea {
width: 100%;
height: 300px;
font-size: 14px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px;
}
h1 {
font-size: 20px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">Device: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-wifi"></span>
<label id="status-wifi-client">Wifi Client: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-internet"></span>
<label id="status-internet">Internet: ...</label>
</div>
<div class="status-container">
<label id="status-current-version">Curr Version: ...</label>
</div>
<div class="status-container">
<label id="status-new-version">New Version: ...</label>
</div>
<!-- Buttons -->
<div class="btn-container">
<button id="bleConnectBtn">Connect</button>
<button id="checkStatusBtn" disabled>Check Status</button>
</div>
<!-- Log Area -->
<textarea id="logArea" readonly></textarea>
<div class="btn-container">
<button id="checkVersionBtn" disabled>Check Version</button>
<button id="startUpgradeBtn" disabled>Start Update</button>
</div>
<!-- Wi-Fi Input Fields -->
<div class="input-container">
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
<div style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPassword" style="width: auto;">
<label for="showPassword">Show Password</label>
</div>
</div>
<!-- Added margin-top above this button -->
<div class="btn-container wifi">
<button id="wifiConnectBtn" disabled>Connect Wifi</button>
</div>
<script>
// Constants
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
let bleDevice = null;
let bleCharacteristic1 = null;
let bleCharacteristic2 = null;
let bleConnected = false;
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
let updatePacket = {
wifiConnected: false,
wifiOnline: false,
wifiIP: [0, 0, 0, 0],
currVersion: [0, 0, 0],
newVersion: [0, 0, 0]
};
// Log messages to the textarea
function logMessage(message) {
const logArea = document.getElementById('logArea');
logArea.value += message + '\n';
logArea.scrollTop = logArea.scrollHeight;
}
// Function to scan for BLE devices
async function scanForDevices() {
logMessage('Scanning for BLE devices...');
try {
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [BLE_SERVICE_UUID]
});
if (device) {
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
} else {
logMessage('No devices found.');
}
} catch (error) {
logMessage(`Scan failed: ${error.message}`);
}
}
// Function to connect to the BLE server
async function connectToBle() {
try {
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ name: BLE_SERVER_NAME }],
optionalServices: [BLE_SERVICE_UUID]
});
//logMessage(`Connecting to ${bleDevice.name}`);
const server = await bleDevice.gatt.connect();
//await server.setPreferredMtu(247); // Request larger MTU size
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
// Subscribe to notifications
//await bleCharacteristic1.startNotifications();
// Add event listener for incoming notifications
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
//logMessage('Getting characteristic...');
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
// Subscribe to notifications
await bleCharacteristic2.startNotifications();
// Add event listener for incoming notifications
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
const value = event.target.value;
const decoder = new TextDecoder();
const decodedValue = decoder.decode(value);
logMessage('--> ' + decodedValue);
});
bleConnected = true;
document.getElementById('bleConnectBtn').disabled = true;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
document.getElementById('status-ble-connection').textContent = 'Device: Connected';
document.getElementById('wifiConnectBtn').disabled = false;
document.getElementById('checkStatusBtn').disabled = false;
logMessage(`Connected to ${bleDevice.name}`);
await readPacket();
processUpdatePacket(updatePacket);
} catch (error) {
if (error.message.includes("cancelled")) {
logMessage("Connection cancelled by user.");
} else {
logMessage(`Connection failed: ${error.message}`);
}
}
}
async function sendPacket(packetMsg) {
if (!bleCharacteristic1) {
console.log("Cannot send packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
const encoder = new TextEncoder();
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
//console.log("Request sent successfully");
return;
} catch (error) {
console.error(`Failed to send packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to send request.");
}
}
}
}
async function readPacket() {
if (!bleCharacteristic1) {
console.log("Cannot read packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
const value = await bleCharacteristic1.readValue();
const data = new Uint8Array(value.buffer);
if (data.length === 12) {
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.wifiOnline = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
//processUpdatePacket(updatePacket);
return;
}
console.log("Invalid packet length");
return;
} catch (error) {
console.error(`Failed to read packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to read packet.");
}
}
}
}
// Process update packet
function processUpdatePacket(packet) {
// Process the packet data
//console.log("Processing update packet:", packet);
if(packet.wifiConnected === true) {
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
}
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
}
if(packet.wifiOnline === true) {
document.getElementById('status-internet').textContent = 'Online';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
document.getElementById('checkVersionBtn').disabled = false;
} else {
document.getElementById('status-internet').textContent = 'Offline';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
document.getElementById('checkVersionBtn').disabled = true;
}
if (packet.currVersion[0] > 0) {
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
} else {
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
}
if (packet.newVersion[0] > 0) {
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
document.getElementById('checkVersionBtn').disabled = true;
if(packet.wifiOnline && packet.newVersion[0] > packet.currVersion[0] ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] > packet.currVersion[1]) ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] === packet.currVersion[1] && packet.newVersion[2] > packet.currVersion[2])) {
//enable start upgrade button
logMessage("New Version Available:");
document.getElementById('startUpgradeBtn').disabled = false;
} else {
//disable start upgrade button
document.getElementById('startUpgradeBtn').disabled = true;
logMessage("New Version: Not Available");
}
} else {
document.getElementById('status-new-version').textContent = 'New Version: ...';
}
}
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
function handleChar1Notifications(event) {
const data = new Uint8Array(event.data);
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
console.log("Invalid packet length");
return;
}
// Update existing updatePacket object instead of creating new one
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.internetAvailable = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
processUpdatePacket(updatePacket);
}
document.getElementById('showPassword').addEventListener('change', function() {
const passwordInput = document.getElementById('wifipassword');
passwordInput.type = this.checked ? 'text' : 'password';
});
// Event listeners for buttons
document.getElementById('bleConnectBtn').addEventListener('click', connectToBle);
document.getElementById('checkStatusBtn').addEventListener('click', async () => {
if (bleCharacteristic1) {
await readPacket();
processUpdatePacket(updatePacket);
} else {
logMessage('BLE device not connected.');
}
});
document.getElementById('checkVersionBtn').addEventListener('click', async () => {
await sendPacket('version-check');
// loop and monitor the the updatePacket.newVersion
await new Promise(resolve => setTimeout(resolve, 2000));
success = false;
for (let i = 0; i < 20; i++) {
await readPacket();
if (updatePacket.newVersion[0] > 0) {
success = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if(success) {
processUpdatePacket(updatePacket);
logMessage("New Version: Available");
} else {
logMessage("New Version: Not Available");
}
});
document.getElementById('wifiConnectBtn').addEventListener('click', async () => {
const ssid = document.getElementById('wifissid').value;
const password = document.getElementById('wifipassword').value;
if (ssid && password) {
// Send credentials to the device
jsonString = ' {"ssid":"' + ssid + '","pass":"' + password + '"} ';
await sendPacket('wifi-connect' + jsonString);
await readPacket();
processUpdatePacket(updatePacket);
} else {
alert('Please enter both SSID and password.');
}
});
document.getElementById('startUpgradeBtn').addEventListener('click', async () => {
try {
await sendPacket('upgrade-start');
logMessage("Upgrade Starting... Please wait.");
} catch (error) {
logMessage(`Error starting upgrade: ${error.message}`);
}
});
// Initial call to process the update packet with defaults
processUpdatePacket(updatePacket);
</script>
</body>
</html>

View File

@ -1,547 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Firmware Update</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 5px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 4px;
}
.status-indicator-ble {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.btn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
}
button {
flex: 1;
max-width: 130px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background 0.3s ease;
}
button:disabled {
background-color: #ccc;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
textarea {
width: 100%;
height: 300px;
font-size: 14px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px;
}
h1 {
font-size: 20px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>ATA Firmware Update</h1>
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">BLE Status: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-wifi"></span>
<label id="status-wifi-client">Wifi Client: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-internet"></span>
<label id="status-internet">Internet: ...</label>
</div>
<div class="status-container">
<label id="status-current-version">Curr Version: ...</label>
</div>
<div class="status-container">
<label id="status-new-version">New Version: ...</label>
</div>
<div class="btn-container">
<label id="ble-device-name">Device Name:</label>
</div>
<div class="btn-container">
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
</div>
<!-- Buttons -->
<div class="btn-container">
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
</div>
<!-- Log Area -->
<textarea id="logArea" readonly></textarea>
<div class="btn-container">
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
</div>
<!-- Wi-Fi Input Fields -->
<div class="input-container">
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
<div style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
<label for="showPassword">Show Password</label>
</div>
</div>
<!-- Added margin-top above this button -->
<div class="btn-container wifi">
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
</div>
<script>
(function(){
'use strict';
// Constants
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
let bleDevice = null;
let bleCharacteristic1 = null;
let bleCharacteristic2 = null;
let bleConnected = false;
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
const updatePacket = {
wifiConnected: false,
wifiOnline: false,
wifiIP: [0, 0, 0, 0],
currVersion: [0, 0, 0],
newVersion: [0, 0, 0]
};
// Function to automatically start the connection process when the page loads
function autoStart() {
document.getElementById('input-DeviceName').value = BLE_SERVER_NAME;
}
// Call autoStart when the page loads
window.addEventListener('DOMContentLoaded', autoStart);
function compareVersions(a, b){
for(let i=0;i<3;i++){ if(a[i] > b[i]) return 1; if(a[i] < b[i]) return -1; }
return 0;
}
// Log messages to the textarea
function logMessage(message) {
const logArea = document.getElementById('logArea');
logArea.value += message + '\n';
logArea.scrollTop = logArea.scrollHeight;
}
// Function to scan for BLE devices
async function scanForDevices() {
logMessage('Scanning for BLE devices...');
try {
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [BLE_SERVICE_UUID]
});
if (device) {
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
} else {
logMessage('No devices found.');
}
} catch (error) {
logMessage(`Scan failed: ${error.message}`);
}
}
// Function to connect to the BLE server
async function connectToBle() {
if(!navigator.bluetooth){
logMessage('Web Bluetooth not supported in this browser.');
return;
}
try {
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ name: document.getElementById('input-DeviceName').value }],
optionalServices: [BLE_SERVICE_UUID]
});
//logMessage(`Connecting to ${bleDevice.name}`);
const server = await bleDevice.gatt.connect();
//await server.setPreferredMtu(247); // Request larger MTU size
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
// Subscribe to notifications
//await bleCharacteristic1.startNotifications();
// Add event listener for incoming notifications
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
//logMessage('Getting characteristic...');
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
// Subscribe to notifications
await bleCharacteristic2.startNotifications();
// Add event listener for incoming notifications
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
const value = event.target.value;
const decoder = new TextDecoder();
const decodedValue = decoder.decode(value);
logMessage('--> ' + decodedValue);
});
bleConnected = true;
// Auto-reconnect / state reset handler
bleDevice.addEventListener('gattserverdisconnected', handleDisconnect);
document.getElementById('bleConnectBtn').disabled = true;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
document.getElementById('status-ble-connection').textContent ="BLE Status: Connected";
document.getElementById('wifiConnectBtn').disabled = false;
document.getElementById('checkStatusBtn').disabled = false;
logMessage(`Connected to ${bleDevice.name}`);
await readPacket();
processUpdatePacket(updatePacket);
} catch (error) {
if (error.message.includes("cancelled")) {
logMessage("Connection cancelled by user.");
} else {
logMessage(`Connection failed: ${error.message}`);
}
}
}
async function wifiConnect() {
const ssid = document.getElementById('wifissid').value;
const password = document.getElementById('wifipassword').value;
if (ssid && password) {
// Send credentials to the device (retain original spacing format expected by firmware)
const jsonString = ' {"ssid":"' + ssid.trim() + '","pass":"' + password + '"} ';
await sendPacket('wifi-connect' + jsonString);
await readPacket();
processUpdatePacket(updatePacket);
} else {
alert('Please enter both SSID and password.');
}
}
async function checkStatus() {
if (bleCharacteristic1) {
await readPacket();
processUpdatePacket(updatePacket);
} else {
logMessage('BLE device not connected.');
}
}
async function checkVersion() {
await sendPacket('version-check');
// loop and monitor the the updatePacket.newVersion
await new Promise(resolve => setTimeout(resolve, 2000));
let success = false;
for (let i = 0; i < 20; i++) {
await readPacket();
if (updatePacket.newVersion[0] > 0) {
success = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if(success) {
processUpdatePacket(updatePacket);
logMessage("New Version: Available");
} else {
logMessage("New Version: Not Available");
}
}
async function startUpgrade() {
try {
await sendPacket('upgrade-start');
logMessage("Upgrade Starting... Please wait.");
} catch (error) {
logMessage(`Error starting upgrade: ${error.message}`);
}
}
function handleDisconnect(){
bleConnected = false;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'gray';
document.getElementById('status-ble-connection').textContent = 'BLE Status: Disconnected';
document.getElementById('bleConnectBtn').disabled = false;
document.getElementById('checkStatusBtn').disabled = true;
document.getElementById('wifiConnectBtn').disabled = true;
logMessage('BLE disconnected');
}
async function sendPacket(packetMsg) {
if (!bleCharacteristic1) {
console.log("Cannot send packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
const encoder = new TextEncoder();
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
//console.log("Request sent successfully");
return;
} catch (error) {
console.error(`Failed to send packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to send request.");
}
}
}
}
async function readPacket() {
if (!bleCharacteristic1) {
console.log("Cannot read packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
const value = await bleCharacteristic1.readValue();
const data = new Uint8Array(value.buffer);
if (data.length === 12) {
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.wifiOnline = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
//processUpdatePacket(updatePacket);
return;
}
console.log("Invalid packet length");
return;
} catch (error) {
console.error(`Failed to read packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to read packet.");
}
}
}
}
// Process update packet
function processUpdatePacket(packet) {
// Process the packet data
//console.log("Processing update packet:", packet);
if(packet.wifiConnected === true) {
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
}
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
}
if(packet.wifiOnline === true) {
document.getElementById('status-internet').textContent = 'Online';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
document.getElementById('checkVersionBtn').disabled = false;
} else {
document.getElementById('status-internet').textContent = 'Offline';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
document.getElementById('checkVersionBtn').disabled = true;
}
if (packet.currVersion[0] > 0) {
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
} else {
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
}
if (packet.newVersion[0] > 0) {
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
document.getElementById('checkVersionBtn').disabled = true;
logMessage("Latest Update is: " + packet.newVersion.join('.'));
if(packet.wifiOnline && compareVersions(packet.newVersion, packet.currVersion) > 0) {
//enable start upgrade button
logMessage("New Version: Available");
document.getElementById('startUpgradeBtn').disabled = false;
} else {
//disable start upgrade button
document.getElementById('startUpgradeBtn').disabled = true;
logMessage("New Version: Not Available");
}
} else {
document.getElementById('status-new-version').textContent = 'New Version: ...';
}
}
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
function handleChar1Notifications(event) {
const data = new Uint8Array(event.data);
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
console.log("Invalid packet length");
return;
}
// Update existing updatePacket object instead of creating new one
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.wifiOnline = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
processUpdatePacket(updatePacket);
}
// Event listeners for buttons
function togglePasswordVisibility() {
const passwordInput = document.getElementById('wifipassword');
passwordInput.type = this.checked ? 'text' : 'password';
}
// Initial call to process the update packet with defaults
processUpdatePacket(updatePacket);
})();
</script>
</body>
</html>

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,472 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ATA Light Stick Reg</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
text-align: center;
}
h1 {
font-size: 22px;
margin-bottom: 20px;
}
.status-container {
display: flex;
align-items: center;
justify-content: left;
margin-bottom: 15px;
}
.status-indicator-ble {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-wifi {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.status-indicator-internet {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: gray;
margin-right: 10px;
}
.btn-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
/* Adds space above the WiFi Connect button */
.btn-container.wifi {
margin-top: 20px;
}
button {
flex: 1;
max-width: 130px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background 0.3s ease;
}
button:disabled {
background-color: #ccc;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
textarea {
width: 97%;
height: 100px;
font-size: 14px;
padding: 10px;
border-radius: 5px;
border: 1px solid #ccc;
resize: none;
}
.input-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
input {
width: 90%;
max-width: 300px;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
text-align: center;
}
input::placeholder {
text-align: center;
}
@media (max-width: 480px) {
body {
padding: 15px;
}
h1 {
font-size: 20px;
}
button {
font-size: 14px;
padding: 8px;
}
input, textarea {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>ATA Flash-Stick Link/Registration</h1>
<!-- Status Indicators -->
<div class="status-container">
<span class="status-indicator-ble"></span>
<label id="status-ble-connection">Master: ...</label>
</div>
<div class="status-container">
<span class="status-indicator-registration"></span>
<label id="status-registration">Registration: ...</label>
</div>
<div class="btn-container">
<button id="bleConnectBtn">Connect</button>
<button id="bleDisconnectBtn">Disconnect</button>
<button id="bleSaveBtn">Save</button>
</div>
<textarea id="logArea" readonly></textarea>
<script>
// Constants
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
let bleDevice = null;
let bleCharacteristic1 = null;
let bleCharacteristic2 = null;
let bleConnected = false;
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
let updatePacket = {
name: "",
};
// Log messages to the textarea
function logMessage(message) {
const logArea = document.getElementById('logArea');
logArea.value += message + '\n';
logArea.scrollTop = logArea.scrollHeight;
}
// Function to scan for BLE devices
async function scanForDevices() {
logMessage('Scanning for BLE devices...');
try {
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [BLE_SERVICE_UUID]
});
if (device) {
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
} else {
logMessage('No devices found.');
}
} catch (error) {
logMessage(`Scan failed: ${error.message}`);
}
}
// Function to connect to the BLE server
async function connectToBle() {
try {
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ name: BLE_SERVER_NAME }],
optionalServices: [BLE_SERVICE_UUID]
});
//logMessage(`Connecting to ${bleDevice.name}`);
const server = await bleDevice.gatt.connect();
//await server.setPreferredMtu(247); // Request larger MTU size
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
// Subscribe to notifications
//await bleCharacteristic1.startNotifications();
// Add event listener for incoming notifications
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
//logMessage('Getting characteristic...');
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
// Subscribe to notifications
await bleCharacteristic2.startNotifications();
// Add event listener for incoming notifications
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
const value = event.target.value;
const decoder = new TextDecoder();
const decodedValue = decoder.decode(value);
logMessage('--> ' + decodedValue);
});
bleConnected = true;
document.getElementById('bleConnectBtn').disabled = true;
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
document.getElementById('status-ble-connection').textContent = 'Device: Connected';
document.getElementById('wifiConnectBtn').disabled = false;
document.getElementById('checkStatusBtn').disabled = false;
logMessage(`Connected to ${bleDevice.name}`);
await readPacket();
processUpdatePacket(updatePacket);
} catch (error) {
if (error.message.includes("cancelled")) {
logMessage("Connection cancelled by user.");
} else {
logMessage(`Connection failed: ${error.message}`);
}
}
}
async function sendPacket(packetMsg) {
if (!bleCharacteristic1) {
console.log("Cannot send packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
const encoder = new TextEncoder();
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
//console.log("Request sent successfully");
return;
} catch (error) {
console.error(`Failed to send packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to send request.");
}
}
}
}
async function readPacket() {
if (!bleCharacteristic1) {
console.log("Cannot read packet: Not connected to BLE server.");
return;
}
const maxRetries = 3;
const retryDelay = 1000; // 1 second
let attempt = 0;
while (attempt < maxRetries) {
try {
const value = await bleCharacteristic1.readValue();
const data = new Uint8Array(value.buffer);
if (data.length === 12) {
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.wifiOnline = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
//processUpdatePacket(updatePacket);
return;
}
console.log("Invalid packet length");
return;
} catch (error) {
console.error(`Failed to read packet: ${error.message}`);
attempt++;
if (attempt < maxRetries) {
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
} else {
console.error("Max retries reached. Failed to read packet.");
}
}
}
}
// Process update packet
function processUpdatePacket(packet) {
// Process the packet data
//console.log("Processing update packet:", packet);
if(packet.wifiConnected === true) {
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
}
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
} else {
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
}
if(packet.wifiOnline === true) {
document.getElementById('status-internet').textContent = 'Online';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
document.getElementById('checkVersionBtn').disabled = false;
} else {
document.getElementById('status-internet').textContent = 'Offline';
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
document.getElementById('checkVersionBtn').disabled = true;
}
if (packet.currVersion[0] > 0) {
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
} else {
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
}
if (packet.newVersion[0] > 0) {
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
document.getElementById('checkVersionBtn').disabled = true;
if(packet.wifiOnline && packet.newVersion[0] > packet.currVersion[0] ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] > packet.currVersion[1]) ||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] === packet.currVersion[1] && packet.newVersion[2] > packet.currVersion[2])) {
//enable start upgrade button
logMessage("New Version Available:");
document.getElementById('startUpgradeBtn').disabled = false;
} else {
//disable start upgrade button
document.getElementById('startUpgradeBtn').disabled = true;
logMessage("New Version: Not Available");
}
} else {
document.getElementById('status-new-version').textContent = 'New Version: ...';
}
}
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
function handleChar1Notifications(event) {
const data = new Uint8Array(event.data);
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
console.log("Invalid packet length");
return;
}
// Update existing updatePacket object instead of creating new one
updatePacket.wifiConnected = data[0] !== 0;
updatePacket.internetAvailable = data[1] !== 0;
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
updatePacket.currVersion = [data[6], data[7], data[8]];
updatePacket.newVersion = [data[9], data[10], data[11]];
processUpdatePacket(updatePacket);
}
document.getElementById('showPassword').addEventListener('change', function() {
const passwordInput = document.getElementById('wifipassword');
passwordInput.type = this.checked ? 'text' : 'password';
});
// Event listeners for buttons
document.getElementById('bleConnectBtn').addEventListener('click', connectToBle);
document.getElementById('checkStatusBtn').addEventListener('click', async () => {
if (bleCharacteristic1) {
await readPacket();
processUpdatePacket(updatePacket);
} else {
logMessage('BLE device not connected.');
}
});
document.getElementById('checkVersionBtn').addEventListener('click', async () => {
await sendPacket('version-check');
// loop and monitor the the updatePacket.newVersion
await new Promise(resolve => setTimeout(resolve, 2000));
success = false;
for (let i = 0; i < 20; i++) {
await readPacket();
if (updatePacket.newVersion[0] > 0) {
success = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if(success) {
processUpdatePacket(updatePacket);
logMessage("New Version: Available");
} else {
logMessage("New Version: Not Available");
}
});
document.getElementById('wifiConnectBtn').addEventListener('click', async () => {
const ssid = document.getElementById('wifissid').value;
const password = document.getElementById('wifipassword').value;
if (ssid && password) {
// Send credentials to the device
jsonString = ' {"ssid":"' + ssid + '","pass":"' + password + '"} ';
await sendPacket('wifi-connect' + jsonString);
await readPacket();
processUpdatePacket(updatePacket);
} else {
alert('Please enter both SSID and password.');
}
});
document.getElementById('startUpgradeBtn').addEventListener('click', async () => {
try {
await sendPacket('upgrade-start');
logMessage("Upgrade Starting... Please wait.");
} catch (error) {
logMessage(`Error starting upgrade: ${error.message}`);
}
});
// Initial call to process the update packet with defaults
processUpdatePacket(updatePacket);
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@ -63,6 +63,8 @@ const COLOR_PACK single_colorPacks[] PROGMEM = {
// Dashes
const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } };
const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } };
@ -73,7 +75,7 @@ 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 DashesColorPacks[] PROGMEM = {
const COLOR_PACK dashes_ColorPacks[] PROGMEM = {
colorPack_RedBlack,
colorPack_OrangeBlack,
colorPack_YellowBlack,

View File

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

28
include/version.h Normal file
View File

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

View File

@ -387,63 +387,28 @@ void Lights_Control_Task(void *parameters){
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
break;
}
case 9: // Sec
loadColorPack(colorPack, colorPack_USA);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
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 10:
loadColorPack(colorPack, colorPack_MEXICO);
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80);
}
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 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);
case 16: case 17: case 18: case 19: case 20: {
//loadColorPack(colorPack, colorPack_RAINBOW);
int idx = AnimEvent.AnimationIndex - 16;
}
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 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;
@ -469,17 +434,7 @@ void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack) {
else palette[i] = CRGB::White;
}
}
/*
void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) {
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 palette[i] = CRGB::White;
}
}
*/
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) {
memcpy_P(&dest, &src, sizeof(COLOR_PACK));

View File

@ -8,6 +8,7 @@
#include "BLE_UpdateService.h"
#include <HTTPClient.h>
#include <Update.h>
#include <cstring>
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
@ -298,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
@ -470,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());
@ -518,7 +527,7 @@ void versionCheckTask(void* parameter){
if(updateUrl == ""){
loadUpdateJson();
}
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
if(!updater.checkManifest()){
ESP_LOGE(TAG, "Version check: manifest fetch failed");
} else {

View File

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

View File

@ -4,6 +4,7 @@
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(

View File

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