-
WiFi Connection
-
-
+ function init() {
+ cacheDom();
+ el.inDeviceName.value = BLE_SERVER_NAME;
+ // Tab switching
+ document.querySelectorAll('.tab-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
+ btn.classList.add('active');
+ const id = btn.getAttribute('data-tab');
+ const panel = document.getElementById(id);
+ if(panel) panel.classList.add('active');
+ });
+ });
+ updateUI();
+ logMessage('Press Connect -> Check Version -> Start Update');
+ }
+
+ window.connectToBle = connectToBle;
+ window.checkStatus = checkStatus;
+ window.checkVersion = checkVersion;
+ window.startUpgrade = startUpgrade;
+ window.wifiConnect = wifiConnect;
+ window.togglePasswordVisibility = togglePasswordVisibility;
+
+ window.addEventListener('DOMContentLoaded', init);
+ })();
+
+```
\ No newline at end of file
diff --git a/firmware_update/UploadToMinio_direct.py b/firmware_update/UploadToMinio_direct.py
deleted file mode 100644
index b4f76e4..0000000
--- a/firmware_update/UploadToMinio_direct.py
+++ /dev/null
@@ -1,456 +0,0 @@
-#!/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()
diff --git a/firmware_update/UploadToMinio_direct_V2.py b/firmware_update/UploadToMinio_direct_V2.py
deleted file mode 100644
index acd2219..0000000
--- a/firmware_update/UploadToMinio_direct_V2.py
+++ /dev/null
@@ -1,497 +0,0 @@
-#!/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()
diff --git a/firmware_update/UploadToMinio_direct_V3..py b/firmware_update/UploadToMinio_direct_V3..py
index 04f8c9b..b46618e 100644
--- a/firmware_update/UploadToMinio_direct_V3..py
+++ b/firmware_update/UploadToMinio_direct_V3..py
@@ -34,8 +34,8 @@ UPDATE_MANIFEST = True
UPLOAD_DATA = True
DIR_SKIP_LIST = [
- "data/system/**/*",
- "data/booths/**/*"
+ "system", # Just directory names, not paths
+ "booths"
]
FILES_SKIP_LIST = [
diff --git a/include/AppUpgrade.h b/include/AppUpgrade.h
index 73d29cf..d50c97b 100644
--- a/include/AppUpgrade.h
+++ b/include/AppUpgrade.h
@@ -10,17 +10,20 @@
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
-#define BUFFER_SIZE 4096
+#define BUFFER_SIZE 2048 // Reduced from 4096 to use less memory
// Maximum allowed manifest size (bytes) to protect memory
#define MAX_MANIFEST_SIZE (64 * 1024)
// Number of HTTP retry attempts for transient failures
-#define HTTP_RETRY_COUNT 3
-#define HTTP_RETRY_DELAY_MS 500
+#define HTTP_RETRY_COUNT 5 // Increased from 3
+#define HTTP_RETRY_DELAY_MS 1000 // Increased from 500
// Allow external cancellation
extern volatile bool g_UpdateCancelFlag;
+// Global update mode setting
+extern UpdateMode g_UpdateMode;
+
extern TaskHandle_t Update_Task_Handle;
/**
@@ -32,6 +35,15 @@ extern TaskHandle_t Update_Task_Handle;
extern Version otaVersion;
+/**
+ * @brief Update mode enumeration
+ */
+enum class UpdateMode {
+ UPDATE_FILES_ONLY, ///< Update files only, skip firmware
+ UPDATE_FIRMWARE_ONLY, ///< Update firmware only, skip files
+ UPDATE_BOTH ///< Update both files and firmware (default)
+};
+
/**
* @brief File information structure
*/
@@ -90,6 +102,16 @@ class AppUpdater {
*/
const String& getBaseUrl() const { return baseUrl; }
+ /**
+ * @brief Set update mode (files only, firmware only, or both)
+ */
+ void setUpdateMode(UpdateMode mode);
+
+ /**
+ * @brief Get current update mode
+ */
+ UpdateMode getUpdateMode() const;
+
/**
* @brief Set progress callback function
* @param callback Function to call with progress updates
@@ -119,12 +141,25 @@ class AppUpdater {
*/
bool updateFile(const char* remotePath, const char* localPath, const char* expectedMd5);
+ /**
+ * @brief Results from checkManifest
+ */
+ enum class ManifestCheckResult {
+ ERROR_FETCH_FAILED, ///< Failed to fetch manifest
+ ERROR_TOO_LARGE, ///< Manifest too large
+ ERROR_PARSE_FAILED, ///< Failed to parse manifest JSON
+ ERROR_NO_FILES_SECTION, ///< No files section in manifest
+ ERROR_NO_VERSION, ///< No version section in manifest
+ VERSION_CURRENT, ///< Current version is same or newer
+ UPDATE_AVAILABLE ///< New version available
+ };
+
/**
* @brief Get manifest content
* @param manifestPath Path to manifest file
- * @return Manifest content as a json document
+ * @return Result indicating success, failure, or version status
*/
- bool checkManifest(void);
+ ManifestCheckResult checkManifest(void);
bool updateApp(void);
@@ -137,6 +172,7 @@ class AppUpdater {
UpdateStatus status;
std::unique_ptr
downloadBuffer;
bool updateAvailable = false;
+ UpdateMode updateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware
@@ -149,7 +185,7 @@ class AppUpdater {
* @return true if successful
*/
bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength,
- const char* localPath, const char* expectedMd5);
+ const char* localPath, const char* remotePath, const char* expectedMd5);
/**
* @brief Update progress callback
@@ -198,4 +234,11 @@ void handleUpdateProgress(AsyncWebServerRequest *request);
void startVersionCheckTask();
-void versionCheckTask(void* parameter);
\ No newline at end of file
+void versionCheckTask(void* parameter);
+
+// Convenience functions for setting update mode
+void setGlobalUpdateMode(UpdateMode mode);
+UpdateMode getGlobalUpdateMode();
+void setUpdateModeFilesOnly();
+void setUpdateModeFirmwareOnly();
+void setUpdateModeBoth();
\ No newline at end of file
diff --git a/include/OnEveryN.h b/include/OnEveryN.h
index a70f602..3b1808f 100644
--- a/include/OnEveryN.h
+++ b/include/OnEveryN.h
@@ -27,11 +27,33 @@ private:
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
#define UNIQUE_NAME(base) CONCATENATE(base, __LINE__)
-// Macro for ON_EVERY_N_MILLISECONDS
+// Macro for ON_EVERY_N_MILLISECONDS (constant N via template)
#define ON_EVERY_N_MILLISECONDS(N) \
static OnEveryN UNIQUE_NAME(__on_everyN_); \
if (UNIQUE_NAME(__on_everyN_).ready())
+// Runtime-configurable variant (interval can be a variable/expression)
+class OnEveryMsVariable {
+public:
+ OnEveryMsVariable() : lastTime(0) {}
+ bool ready(unsigned long interval) {
+ if (interval == 0) return false; // ignore 0 to avoid busy looping
+ unsigned long now = millis();
+ if (now - lastTime >= interval) {
+ lastTime = now;
+ return true;
+ }
+ return false;
+ }
+private:
+ unsigned long lastTime;
+};
+
+// Macro for variable interval in milliseconds
+#define ON_EVERY_MILLISECONDS(VAR_INTERVAL) \
+ static OnEveryVariable UNIQUE_NAME(__on_everyVar_); \
+ if (UNIQUE_NAME(__on_everyVar_).ready(VAR_INTERVAL))
+
// Macro for ON_EVERY_N_SECONDS
#define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000)
diff --git a/include/RtttlPlayer.h b/include/RtttlPlayer.h
new file mode 100644
index 0000000..f3dec56
--- /dev/null
+++ b/include/RtttlPlayer.h
@@ -0,0 +1,326 @@
+#pragma once
+#include
+#include
+#include
+#include
+
+// Tiny, efficient RTTTL player for ESP32 LEDC (non-blocking, priority-aware)
+class RtttlPlayer {
+public:
+ // pin: GPIO to output tone, channel: LEDC channel [0..15]
+ // timer: LEDC timer index [0..3], resolutionBits typically 10-13
+ RtttlPlayer(uint8_t pin, uint8_t channel, uint8_t timer=0, uint8_t resolutionBits=10,
+ uint16_t queueDepth=4)
+ : _pin(pin), _ch(channel), _timer(timer), _resBits(resolutionBits) {
+
+ // LEDC init
+ ledcSetup(_ch, /*freq*/ 1000, _resBits);
+ ledcAttachPin(_pin, _ch);
+ ledcWrite(_ch, 0); // duty 0 initially
+
+ // Sync primitives
+ _queue = xQueueCreate(queueDepth, sizeof(PlayItem));
+ _mtx = xSemaphoreCreateMutex();
+ _stopFlag = false;
+ _playing = false;
+ _curPrio = 0;
+ _preempt.item = nullptr; // Initialize preempt slot
+
+ // Playback task
+ xTaskCreatePinnedToCore(_taskThunk, "rtttl_task", 2048, this, /*prio*/ 1, &_taskHandle, 1);
+ }
+
+ ~RtttlPlayer() {
+ if (_taskHandle) vTaskDelete(_taskHandle);
+ if (_queue) vQueueDelete(_queue);
+ if (_mtx) vSemaphoreDelete(_mtx);
+ }
+
+ // Non-blocking request to play a tune at 'priority'.
+ // Copies the RTTTL string internally (heap); returns false if queue full or alloc fails.
+ bool play(const char* rtttl, uint8_t priority) {
+ if (!rtttl) return false;
+
+ const size_t len = strnlen(rtttl, 512); // Reduce memory cap for efficiency
+ char* buf = (char*)malloc(len + 1);
+ if (!buf) return false;
+ memcpy(buf, rtttl, len);
+ buf[len] = '\0';
+
+ PlayItem item{buf, priority};
+
+ // Fast path: preempt if strictly higher priority than current
+ xSemaphoreTake(_mtx, portMAX_DELAY);
+ const bool shouldPreempt = _playing && (priority > _curPrio);
+ if (shouldPreempt) {
+ // Put new item into the front by sending to a small high-prio queue slot
+ // Approach: set preempt slot; playback loop will pick it ASAP.
+ if (_preempt.item) {
+ // Drop older preempt request to avoid leaks, keep newest
+ free((void*)_preempt.item->tune);
+ delete _preempt.item;
+ }
+ _preempt.item = new PlayItem(item); // copy
+ xSemaphoreGive(_mtx);
+ // we own original 'buf' no longer (copied into new PlayItem); free the temporary
+ free(buf);
+ // Signal stop to current note so task can switch between notes (cheap, cooperative)
+ _stopFlag = true;
+ return true;
+ }
+ xSemaphoreGive(_mtx);
+
+ // Otherwise enqueue and return (will play after current/earlier)
+ if (xQueueSend(_queue, &item, 0) == pdTRUE) {
+ return true;
+ } else {
+ free(buf);
+ return false;
+ }
+ }
+
+ // Stop playback immediately and flush queue (non-blocking).
+ void stopAll() {
+ xSemaphoreTake(_mtx, portMAX_DELAY);
+ _stopFlag = true;
+ // Drain queue
+ PlayItem tmp;
+ while (xQueueReceive(_queue, &tmp, 0) == pdTRUE) {
+ if (tmp.tune) free((void*)tmp.tune);
+ }
+ // Drop any pending preempt
+ if (_preempt.item) {
+ free((void*)_preempt.item->tune);
+ delete _preempt.item;
+ _preempt.item = nullptr;
+ }
+ xSemaphoreGive(_mtx);
+ }
+
+private:
+ struct PlayItem {
+ const char* tune; // heap-allocated copy
+ uint8_t priority;
+ };
+ struct PreemptSlot {
+ PlayItem* item = nullptr; // one-slot "front of line"
+ };
+
+ // ===== LEDC helpers =====
+ inline void toneOn(uint32_t freq) {
+ if (freq == 0) { // pause
+ ledcWrite(_ch, 0);
+ return;
+ }
+ // Update LEDC frequency efficiently
+ // Use ledcSetup to set frequency (works on all Arduino-ESP32 versions)
+ ledcSetup(_ch, freq, _resBits);
+ // Duty: ~50% for square-like tone
+ const uint32_t dutyMax = (1U << _resBits) - 1U;
+ ledcWrite(_ch, dutyMax / 2);
+ }
+ inline void toneOff() {
+ ledcWrite(_ch, 0);
+ }
+
+ // ===== Playback task =====
+ static void _taskThunk(void* arg) {
+ ((RtttlPlayer*)arg)->_taskLoop();
+ }
+
+ void _taskLoop() {
+ for (;;) {
+ // First check preempt slot, then queue
+ PlayItem item{};
+ if (_takePreempt(item) || xQueueReceive(_queue, &item, portMAX_DELAY) == pdTRUE) {
+ _playing = true;
+ _curPrio = item.priority;
+ _stopFlag = false;
+ _playOne(item.tune);
+ // cleanup
+ free((void*)item.tune);
+ _playing = false;
+ _curPrio = 0;
+ }
+ }
+ }
+
+ bool _takePreempt(PlayItem& out) {
+ bool got = false;
+ xSemaphoreTake(_mtx, portMAX_DELAY);
+ if (_preempt.item) {
+ out = *_preempt.item;
+ delete _preempt.item;
+ _preempt.item = nullptr;
+ got = true;
+ }
+ xSemaphoreGive(_mtx);
+ return got;
+ }
+
+ // ===== RTTTL parsing & playback =====
+ struct Defaults { uint16_t dur = 4; uint8_t oct = 5; uint16_t bpm = 63; };
+
+ // Note frequencies for octave 4 (rounded). Others are scaled by powers of two.
+ // Index by semitone: C, C#, D, D#, E, F, F#, G, G#, A, A#, B
+ static constexpr uint16_t baseA4 = 440;
+ // We’ll derive semitone freq using integer math: f = 440 * 2^((n)/12)
+ // To avoid floating point in the loop, we precompute a small LUT for octave 4.
+ static constexpr uint16_t LUT4[12] = {
+ 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494
+ };
+
+ static uint32_t freqFor(uint8_t noteIndex, int8_t octave) {
+ // noteIndex 0..11; octave typical 3..7
+ // Get octave 4 freq and scale by 2^(oct-4)
+ uint32_t f = LUT4[noteIndex];
+ if (octave > 4) f <<= (octave - 4);
+ else if (octave < 4) f >>= (4 - octave);
+ return f;
+ }
+
+ // Returns next token start or nullptr when finished
+ static const char* skipSpaces(const char* p) {
+ while (p && *p && (*p == ' ' || *p == ',')) ++p;
+ return p;
+ }
+
+ void _playOne(const char* rtttl) {
+ if (!rtttl) return;
+ Defaults def;
+ // Format: name:d=4,o=5,b=120: note[,note...]
+ const char* p = strchr(rtttl, ':');
+ if (!p) return;
+ const char* p2 = strchr(p + 1, ':');
+ if (!p2) return;
+
+ // Parse defaults between first and second ':'
+ parseDefaults(p + 1, p2, def);
+
+ // Whole note duration in ms
+ uint32_t wholenote = (60000UL / def.bpm) * 4;
+
+ // Notes section
+ p = p2 + 1;
+ p = skipSpaces(p);
+ while (p && *p) {
+ // Cooperative preemption between notes
+ if (_checkPreempt()) break;
+
+ uint16_t duration = 0;
+ // 1) Optional duration number (e.g., 16)
+ while (*p >= '0' && *p <= '9') {
+ duration = duration * 10 + (*p - '0');
+ ++p;
+ }
+ if (duration == 0) duration = def.dur;
+
+ // 2) Note letter or pause
+ bool pause = false;
+ uint8_t noteIndex = 0xFF;
+ switch (tolower(*p)) {
+ case 'c': noteIndex = 0; break;
+ case 'd': noteIndex = 2; break;
+ case 'e': noteIndex = 4; break;
+ case 'f': noteIndex = 5; break;
+ case 'g': noteIndex = 7; break;
+ case 'a': noteIndex = 9; break;
+ case 'b': noteIndex = 11; break;
+ case 'p': pause = true; break;
+ default: break;
+ }
+ if (*p) ++p;
+
+ // 3) Optional sharp '#'
+ if (!pause && *p == '#') {
+ if (noteIndex != 0xFF) noteIndex++;
+ ++p;
+ }
+
+ // 4) Optional dotted note '.'
+ bool dotted = false;
+ if (*p == '.') { dotted = true; ++p; }
+
+ // 5) Optional octave number
+ int8_t octave = def.oct;
+ if (*p >= '4' && *p <= '7') { octave = (*p - '0'); ++p; }
+
+ // Compute duration in ms
+ uint32_t noteDur = wholenote / duration;
+ if (dotted) noteDur += noteDur / 2;
+
+ // Play the note
+ if (pause || noteIndex == 0xFF) {
+ toneOff();
+ vTaskDelay(pdMS_TO_TICKS(noteDur));
+ } else {
+ const uint32_t f = freqFor(noteIndex % 12, octave);
+ toneOn(f);
+ // Shorten a little to add a tiny gap (staccato for clarity & queue responsiveness)
+ uint32_t onMs = (noteDur >= 4) ? (noteDur - 2) : noteDur;
+ uint32_t offMs = noteDur - onMs;
+ vTaskDelay(pdMS_TO_TICKS(onMs));
+ toneOff();
+ if (offMs) vTaskDelay(pdMS_TO_TICKS(offMs));
+ }
+
+ p = skipSpaces(p);
+ // Optional trailing comma already handled by skipSpaces
+ }
+
+ toneOff();
+ }
+
+ void parseDefaults(const char* beg, const char* end, Defaults& def) {
+ const char* p = beg;
+ while (p < end && *p) {
+ // key=value (d,o,b)
+ char key = tolower(*p);
+ const char* eq = (const char*)memchr(p, '=', end - p);
+ if (!eq) break;
+ const char* val = eq + 1;
+ const char* nxt = (const char*)memchr(val, ',', end - val);
+ if (!nxt) nxt = end;
+
+ uint32_t v = 0;
+ for (const char* t = val; t < nxt; ++t) {
+ if (*t >= '0' && *t <= '9') v = v * 10 + (*t - '0');
+ }
+ if (key == 'd' && v) def.dur = v;
+ else if (key == 'o' && v) def.oct = (uint8_t)v;
+ else if (key == 'b' && v) def.bpm = v;
+
+ p = (nxt < end) ? (nxt + 1) : end;
+ }
+ // Clamp sane ranges
+ if (def.dur == 0) def.dur = 4;
+ if (def.oct < 3 || def.oct > 7) def.oct = 5;
+ if (def.bpm < 20) def.bpm = 20;
+ if (def.bpm > 400) def.bpm = 400;
+ }
+
+ bool _checkPreempt() {
+ if (!_stopFlag) return false;
+ // Grab preempt immediately if present, otherwise just stop current tune
+ PlayItem nxt{};
+ if (_takePreempt(nxt)) {
+ // Play preempted tune immediately (no recursion - direct call)
+ _curPrio = nxt.priority;
+ _stopFlag = false;
+ _playOne(nxt.tune);
+ free((void*)nxt.tune);
+ }
+ // Indicate current tune should finish early
+ return true;
+ }
+
+ // ===== members =====
+ uint8_t _pin, _ch, _timer, _resBits;
+ TaskHandle_t _taskHandle = nullptr;
+ QueueHandle_t _queue = nullptr;
+ SemaphoreHandle_t _mtx = nullptr;
+ volatile bool _stopFlag;
+ volatile bool _playing;
+ volatile uint8_t _curPrio;
+ PreemptSlot _preempt;
+};
diff --git a/include/my_buzzer.h b/include/my_buzzer.h
index 69aeae4..4c1579f 100644
--- a/include/my_buzzer.h
+++ b/include/my_buzzer.h
@@ -21,21 +21,19 @@ typedef enum {
//Tunes
typedef struct {
- bool enabled;
int cycles;
int pause;
String melody;
}BUZZ_TUNE;
-#define TUNE_MAX_COUNT 12
+#define TUNE_MAX_COUNT 14
extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
-void Init_Buzzer(int8_t, const char* configPath);
-
-void Buzzer_Beep(int, int freq=1000);
+void Init_Buzzer(int8_t pin, const char* configPath, int8_t channel = -1);
void Buzzer_Load_Tunes(const char* tunesPath);
-void Buzzer_Play_Tune(TUNE_TYPE, bool async=true, bool priority=true);
+void Buzzer_Play_Tune(TUNE_TYPE tune, int priority=1);
+
diff --git a/include/version.h b/include/version.h
index ee6e40a..68f405a 100644
--- a/include/version.h
+++ b/include/version.h
@@ -1,6 +1,6 @@
#define FIRMWARE_VERSION_MAJOR 1
#define FIRMWARE_VERSION_MINOR 4
-#define FIRMWARE_VERSION_PATCH 9
+#define FIRMWARE_VERSION_PATCH 19
#define FIRMWARE_DESCRIPTION \
diff --git a/src/ATALights.cpp b/src/ATALights.cpp
index 65332f7..5f5c250 100644
--- a/src/ATALights.cpp
+++ b/src/ATALights.cpp
@@ -8,6 +8,7 @@
#include
#include "system.h"
#include "ColorPalettes.h"
+#include "global.h"
//#include
#define FASTLED_CORE 0
@@ -65,10 +66,37 @@ void Init_Lights_Task(void){
Init_Strip(ledSettings[1].leds, ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip, ledSettings[1].bright);
ESP_LOGD(tag, "Initializing Strip2: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip);
- xTaskCreatePinnedToCore(Lights_Control_Task, "Lights_Task", 1024*8, NULL, 1, &Animation_Task_Handle, FASTLED_CORE);
+ xTaskCreatePinnedToCore(Lights_Control_Task, "Lights_Task", 1024*6, NULL, 1, &Animation_Task_Handle, FASTLED_CORE);
ESP_LOGI(tag, "Lights Task Created...");
}
+/*
+void Init_Ramp_Lights_Task(void){
+
+ xTaskCreatePinnedToCore(Ramp_Lights_Control_Task, "Ramp_Lights_Task", 1024*1, NULL, 1, &Animation_Task_Handle, (FASTLED_CORE ? 1 : 0));
+ ESP_LOGI(tag, "Ramp Lights Task Created...");
+}
+
+void RampUpLights(int level)
+{
+
+}
+
+void Ramp_Lights_Control_Task(void *parameters)
+{
+ static *OutputPWM* pwmOut = NULL;
+ pwmOut = pwmOut[sys_settings.rampLightSettings[0].pwmOutIndex];
+
+ while(1){
+ sys_settings.rampLightSettings[rampIndex].pwmOutIndex
+ while()
+
+ vTaskDelay(100 / portTICK_PERIOD_MS);
+ vTaskSuspend(NULL);
+ }
+
+}
+*/
void Animation_Loop_Exit(void){
if( Animation_Task_Handle ){
diff --git a/src/Animations.cpp b/src/Animations.cpp
index 2c74569..01d2157 100644
--- a/src/Animations.cpp
+++ b/src/Animations.cpp
@@ -4,6 +4,12 @@
#include
#include "ColorPalettes.h"
#include "esp_system.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_log.h"
+#include
+#include "PWM_Output.h"
typedef struct{
@@ -449,6 +455,67 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
}
}
+/*
+void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0, PWM_Output* pwmOutput = nullptr) {
+ if (!leds || size <= 1 || totalDurationMs <= 0) return;
+
+ const int halfSize = size / 2;
+ const float msPerLed = totalDurationMs / (float)halfSize;
+ unsigned long startTime = millis();
+
+ // Define the point at which PWM begins ramping (75% of fill time)
+ const float pwmStartPoint = 0.75f;
+ const int pwmStartLeds = halfSize * pwmStartPoint;
+
+ // Initialize PWM output to 0 if provided
+ if (pwmOutput) {
+ pwmOutput->setOutput(0.0f);
+ }
+
+ fill_solid(leds, size, baseCol);
+
+ int prevLedsToLight = 0;
+ unsigned long currentTime;
+ unsigned long elapsedTime;
+ int ledsToLight, pos;
+ Animation_Loop(activeFlag, 90, [&]() -> int {
+ currentTime = millis();
+ elapsedTime = currentTime - startTime;
+
+ // Calculate how many LEDs should be lit based on elapsed time
+ ledsToLight = (elapsedTime / msPerLed);
+ if (ledsToLight > halfSize) ledsToLight = halfSize;
+
+ // Fill LEDs up to current position
+ for (int i = 0; i < ledsToLight; i++) {
+ pos = (i + shift + size) % size;
+ leds[pos] = fillCol;
+ leds[(size - 1 - i + shift + size) % size] = fillCol; // Correct mirroring calculation
+ }
+
+ // Handle PWM output ramp starting at 75% of fill time
+ if (pwmOutput && ledsToLight >= pwmStartLeds) {
+ // Calculate PWM value as percentage of remaining fill time
+ // Map from [pwmStartLeds, halfSize] to [0, 100]
+ float pwmValue = map(ledsToLight, pwmStartLeds, halfSize, 0, 100);
+ // Ensure pwmValue is in range [0, 100]
+ pwmValue = constrain(pwmValue, 0.0f, 100.0f);
+ // Set the PWM output
+ pwmOutput->setOutput(pwmValue);
+ }
+
+ // Update LEDs only when necessary
+ if(prevLedsToLight < ledsToLight){
+ FastLED.show();
+ }
+ prevLedsToLight = ledsToLight;
+
+ // Return 1 when complete
+ return (ledsToLight >= halfSize) ? 1 : 0;
+ });
+}
+*/
+
void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0) {
if (!leds || size <= 1 || totalDurationMs <= 0) return;
diff --git a/src/AppUpgrade.cpp b/src/AppUpgrade.cpp
index 65816f5..ca843c0 100644
--- a/src/AppUpgrade.cpp
+++ b/src/AppUpgrade.cpp
@@ -3,39 +3,56 @@
#include
#include
#include
+#include
#include "global.h"
#include "JsonConstrain.h"
#include "BLE_UpdateService.h"
#include
#include
#include
+#include
+#include
static const char* TAG = "AppUpdater";
TaskHandle_t Update_Task_Handle = NULL;
TaskHandle_t versionCheckTask_Handle = NULL;
volatile bool g_UpdateCancelFlag = false; // cancellation flag
-
-// Queue handle for firmware update messages
-//QueueHandle_t updateMsgQueue = NULL;
-
+UpdateMode g_UpdateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware
String updateUrl = "";
-
Version otaVersion;
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
- : localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
+ : localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs)
{
+ // Use dynamic buffer size based on available memory - much more conservative now
+ size_t available_heap = ESP.getFreeHeap();
+ size_t buffer_size = std::min(BUFFER_SIZE, available_heap / 16); // Use at most 1/16 of free heap
+ // Ensure minimum viable size
+ if (buffer_size < 1024) buffer_size = 1024; // Absolute minimum is 1KB
+
+ downloadBuffer.reset(new uint8_t[buffer_size]);
+
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
// Ensure baseUrl ends with a single '/'
if(!baseUrl.endsWith("/")) baseUrl += "/";
- ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
+ ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s, buffer=%u bytes",
+ localVersion.toString().c_str(), baseUrl.c_str(), buffer_size);
}
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
progressCb = callback;
}
+void AppUpdater::setUpdateMode(UpdateMode mode) {
+ updateMode = mode;
+ g_UpdateMode = mode; // Sync with global mode for firmware update task
+}
+
+UpdateMode AppUpdater::getUpdateMode() const {
+ return updateMode;
+}
+
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
status = newStatus;
if (progressCb) {
@@ -43,15 +60,17 @@ void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const ch
}
}
-bool AppUpdater::checkManifest() {
+AppUpdater::ManifestCheckResult AppUpdater::checkManifest() {
String url = buildUrl(manifestName);
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
String payload;
for(int attempt=0; attempt MAX_MANIFEST_SIZE){
ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
- return false;
+ return ManifestCheckResult::ERROR_TOO_LARGE;
}
// Parse JSON
@@ -77,14 +96,14 @@ bool AppUpdater::checkManifest() {
ESP_LOGD(TAG, "Manifest deserialized");
if (error) {
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
- return false;
+ return ManifestCheckResult::ERROR_PARSE_FAILED;
}
// Check for files section
jsonFilesArray = jsonManifest["files"];
if (jsonFilesArray.isNull()) {
ESP_LOGE(TAG, "No files section in manifest");
- return false;
+ return ManifestCheckResult::ERROR_NO_FILES_SECTION;
}else{
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
}
@@ -94,7 +113,7 @@ bool AppUpdater::checkManifest() {
ESP_LOGD(TAG, "Version section found");
if (jsonVersion.isNull()) {
ESP_LOGE(TAG, "No version section in manifest");
- return false;
+ return ManifestCheckResult::ERROR_NO_VERSION;
}
// Get the remote version
@@ -110,16 +129,18 @@ bool AppUpdater::checkManifest() {
updateAvailable = false;
// Only mark update available if remote is strictly newer than local
if (otaVersion <= localVersion) {
- ESP_LOGI(TAG, "No updates available");
- return false;
+ ESP_LOGI(TAG, "No updates available: remote=%s, local=%s",
+ otaVersion.toString().c_str(), localVersion.toString().c_str());
+ return ManifestCheckResult::VERSION_CURRENT;
}else{
updateAvailable = true;
- ESP_LOGD(TAG, "Update available");
+ ESP_LOGI(TAG, "Update available: remote=%s, local=%s",
+ otaVersion.toString().c_str(), localVersion.toString().c_str());
}
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
- return true;
+ return ManifestCheckResult::UPDATE_AVAILABLE;
}
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
@@ -133,7 +154,10 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
bool skip = false;
if(fileSystem.exists(localPath)){
String localMd5 = getLocalMD5(localPath);
+ ESP_LOGI(TAG, "Local file exists: %s, MD5: %s, Expected: %s", localPath, localMd5.c_str(), expectedMd5);
if(localMd5.equals(expectedMd5)) skip = true;
+ } else {
+ ESP_LOGI(TAG, "Local file does not exist: %s", localPath);
}
if(skip){
ESP_LOGI(TAG, "File already up to date: %s", localPath);
@@ -141,6 +165,8 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
return true;
}
+ ESP_LOGI(TAG, "Need to download file: %s (local MD5 mismatch or file missing)", localPath);
+
// Start the download
HTTPClient http;
int httpCode = -1;
@@ -154,8 +180,9 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
}
if (httpCode != HTTP_CODE_OK) {
- ESP_LOGE(TAG, "Download failed: %d", httpCode);
- updateProgress(UpdateStatus::ERROR, 0, "Download failed");
+ ESP_LOGE(TAG, "Download failed for %s: HTTP code %d", localPath, httpCode);
+ updateProgress(UpdateStatus::ERROR, 0, String(String("Download failed: ") + localPath).c_str());
+ http.end();
return false;
}
@@ -164,109 +191,241 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
size_t contentLength = http.getSize();
// Verify and save the file
- bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
+ bool success = verifyAndSaveFile(stream, contentLength, localPath, remotePath, expectedMd5);
http.end();
if(!success){
- updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed");
+ String errMsg = String(localPath) + " MD5 failed";
+ updateProgress(UpdateStatus::ERROR, 0, errMsg.c_str());
+
+ // For HTML/CSS/JS files, we might have saved them anyway (see verifyAndSaveFile logic)
+ if (fileSystem.exists(localPath) &&
+ (String(localPath).endsWith(".html") ||
+ String(localPath).endsWith(".css") ||
+ String(localPath).endsWith(".js"))) {
+ ESP_LOGW(TAG, "Using %s despite MD5 mismatch (non-critical file)", localPath);
+ // Return false to indicate verification failure but file is still usable
+ }
}else{
- updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
+ updateProgress(UpdateStatus::FILE_SAVED, 100, localPath);
}
return success;
}
-bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
+bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* remotePath, const char* expectedMd5)
{
- MD5Builder md5;
- md5.begin();
- size_t totalRead = 0;
+ const int MAX_RETRIES = 2; // Maximum number of retries for MD5 failure
- // Create temporary filename
- String tempPath = String(localPath) + ".tmp";
+ for (int retry = 0; retry <= MAX_RETRIES; retry++) {
+ if (retry > 0) {
+ ESP_LOGW(TAG, "Retrying download of %s (attempt %d/%d)", localPath, retry, MAX_RETRIES);
+ // Need to re-fetch the file for retry - use the REMOTE path, not local path
+ String url = buildUrl(remotePath);
+ HTTPClient http;
+ http.begin(url);
+ int httpCode = http.GET();
+ if (httpCode != HTTP_CODE_OK) {
+ ESP_LOGE(TAG, "Retry download failed: %d", httpCode);
+ http.end();
+ continue; // Try next retry if available
+ }
+ stream = http.getStreamPtr();
+ contentLength = http.getSize();
+ }
+
+ MD5Builder md5;
+ md5.begin();
+ size_t totalRead = 0;
+
+ // Create temporary filename - try root directory first to avoid path issues
+ String baseName = String(localPath);
+ baseName.replace("/", "_");
+ baseName.replace("\\", "_");
+ String tempPath = "/temp_" + baseName + ".download";
+
+ // Clean up any existing temp file first
+ if (fileSystem.exists(tempPath.c_str())) {
+ ESP_LOGW(TAG, "Removing existing temp file: %s", tempPath.c_str());
+ fileSystem.remove(tempPath.c_str());
+ }
+
+ ESP_LOGI(TAG, "Using temp file path: %s for target: %s", tempPath.c_str(), localPath);
+
+ // Open temporary file for writing (LittleFS will create directories automatically)
+ File file = fileSystem.open(tempPath.c_str(), FILE_WRITE, true); // true = create if not exists
+ if (!file) {
+ ESP_LOGE(TAG, "Failed to open temporary file for writing: %s", tempPath.c_str());
+
+ // Try to diagnose the issue
+ ESP_LOGE(TAG, "LittleFS info - Used: %u bytes, Total: %u bytes",
+ LittleFS.usedBytes(), LittleFS.totalBytes());
+
+ // Check if we're out of space
+ if (LittleFS.usedBytes() >= LittleFS.totalBytes() * 0.95) {
+ ESP_LOGE(TAG, "LittleFS nearly full - may not have space for temp file");
+ }
+
+ return false;
+ }
- // Open temporary file for writing
- File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
- if (!file) {
- ESP_LOGE(TAG, "Failed to open temporary file for writing");
- return false;
- }
-
- //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
-
- if (contentLength > 0) {
- // Single pass with known content length
- while (totalRead < contentLength) {
- if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
- size_t available = stream->available();
- if (available) {
- size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
-
- // Write to temp file and update MD5
+ //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
+
+ if (contentLength > 0) {
+ // Single pass with known content length
+ while (totalRead < contentLength) {
+ if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
+ size_t available = stream->available();
+ if (available) {
+ size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
+
+ // Write to temp file and update MD5
+ if (file.write(downloadBuffer.get(), readLen) != readLen) {
+ ESP_LOGE(TAG, "Failed to write to temporary file");
+
+ file.close();
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+
+ md5.add(downloadBuffer.get(), readLen);
+ totalRead += readLen;
+ updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
+ }
+ yield();
+ }
+ } else {
+ // Unknown content length: read until stream ends
+ for (;;) {
+ if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
+ size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
+ if (readLen == 0) {
+ break;
+ }
if (file.write(downloadBuffer.get(), readLen) != readLen) {
ESP_LOGE(TAG, "Failed to write to temporary file");
-
file.close();
fileSystem.remove(tempPath.c_str());
return false;
}
-
md5.add(downloadBuffer.get(), readLen);
totalRead += readLen;
- updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
+ // Progress unknown; emit periodic heartbeats at 0%
+ // For unknown size, send heartbeats every ~16KB
+ if((totalRead & 0x3FFF) == 0){
+ updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
+ }
+ yield();
}
- yield();
}
- } else {
- // Unknown content length: read until stream ends
- for (;;) {
- if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
- size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
- if (readLen == 0) {
- break;
+
+ file.close();
+ md5.calculate();
+ String calculatedMd5 = md5.toString();
+
+ // Verify MD5 hash
+ updateProgress(UpdateStatus::VERIFYING, 90, localPath);
+
+ ESP_LOGI(TAG, "MD5 verification for %s: Expected='%s', Calculated='%s'",
+ localPath, expectedMd5, calculatedMd5.c_str());
+
+ // Compare MD5 case-insensitively (in case there are case differences)
+ String expectedMd5Lower = String(expectedMd5);
+ expectedMd5Lower.toLowerCase();
+ String calculatedMd5Lower = calculatedMd5;
+ calculatedMd5Lower.toLowerCase();
+
+ if (!calculatedMd5Lower.equals(expectedMd5Lower)) {
+ ESP_LOGE(TAG, "MD5 mismatch for %s (attempt %d/%d). Expected: %s, Got: %s",
+ localPath, retry+1, MAX_RETRIES+1, expectedMd5, calculatedMd5.c_str());
+ ESP_LOGE(TAG, "Length comparison - Expected: %d chars, Got: %d chars",
+ strlen(expectedMd5), calculatedMd5.length());
+ fileSystem.remove(tempPath.c_str());
+
+ if (retry < MAX_RETRIES) {
+ // Will retry in next loop iteration
+ continue;
}
- if (file.write(downloadBuffer.get(), readLen) != readLen) {
- ESP_LOGE(TAG, "Failed to write to temporary file");
- file.close();
- fileSystem.remove(tempPath.c_str());
+
+ // Special case for certain file types - allow them to be used even with MD5 mismatch
+ // This is a fallback option for non-critical files like HTML pages
+ bool isNonCriticalFile = false;
+ if (String(localPath).endsWith(".html") ||
+ String(localPath).endsWith(".css") ||
+ String(localPath).endsWith(".js")) {
+ isNonCriticalFile = true;
+ }
+
+ if (isNonCriticalFile) {
+ ESP_LOGW(TAG, "Using file %s despite MD5 mismatch (non-critical file)", localPath);
+ // We'll still keep this file but report it as a verification failure
+ // Ensure target directory exists before rename
+ String dirPath = String(localPath);
+ int lastSlash = dirPath.lastIndexOf('/');
+ if (lastSlash > 0) {
+ dirPath = dirPath.substring(0, lastSlash);
+ if (!fileSystem.exists(dirPath.c_str())) {
+ ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str());
+ String dummyFile = dirPath + "/.dummy";
+ File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true);
+ if (dummy) {
+ dummy.print("temp");
+ dummy.close();
+ fileSystem.remove(dummyFile.c_str());
+ }
+ }
+ }
+
+ // Rename the temp file to the final location
+ if (fileSystem.exists(localPath)) {
+ fileSystem.remove(localPath);
+ }
+ if (!fileSystem.rename(tempPath.c_str(), localPath)) {
+ ESP_LOGE(TAG, "Failed to rename temporary file for non-critical use: %s -> %s",
+ tempPath.c_str(), localPath);
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+ // Return false to indicate verification failure, but the file will still be used
return false;
}
- md5.add(downloadBuffer.get(), readLen);
- totalRead += readLen;
- // Progress unknown; emit periodic heartbeats at 0%
- // For unknown size, send heartbeats every ~16KB
- if((totalRead & 0x3FFF) == 0){
- updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
- }
- yield();
+
+ return false;
}
+
+ updateProgress(UpdateStatus::VERIFYING, 95, localPath);
+
+ // Ensure target directory exists before rename
+ String dirPath = String(localPath);
+ int lastSlash = dirPath.lastIndexOf('/');
+ if (lastSlash > 0) {
+ dirPath = dirPath.substring(0, lastSlash);
+ if (!fileSystem.exists(dirPath.c_str())) {
+ ESP_LOGI(TAG, "Creating target directory: %s", dirPath.c_str());
+ String dummyFile = dirPath + "/.dummy";
+ File dummy = fileSystem.open(dummyFile.c_str(), FILE_WRITE, true);
+ if (dummy) {
+ dummy.print("temp");
+ dummy.close();
+ fileSystem.remove(dummyFile.c_str());
+ }
+ }
+ }
+
+ // Replace original file with verified temp file
+ if (fileSystem.exists(localPath)) {
+ fileSystem.remove(localPath);
+ }
+ if (!fileSystem.rename(tempPath.c_str(), localPath)) {
+ ESP_LOGE(TAG, "Failed to rename temporary file: %s -> %s", tempPath.c_str(), localPath);
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+
+ updateProgress(UpdateStatus::VERIFYING, 100, localPath);
+ return true;
}
-
- file.close();
- md5.calculate();
- String calculatedMd5 = md5.toString();
-
- // Verify MD5 hash
- updateProgress(UpdateStatus::VERIFYING, 90, localPath);
- if (!calculatedMd5.equals(expectedMd5)) {
- //ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
- fileSystem.remove(tempPath.c_str());
- return false;
- }
-
- updateProgress(UpdateStatus::VERIFYING, 95, localPath);
-
- // Replace original file with verified temp file
- if (fileSystem.exists(localPath)) {
- fileSystem.remove(localPath);
- }
- if (!fileSystem.rename(tempPath.c_str(), localPath)) {
- ESP_LOGE(TAG, "Failed to rename temporary file");
- fileSystem.remove(tempPath.c_str());
- return false;
- }
-
- updateProgress(UpdateStatus::VERIFYING, 100, localPath);
- return true;
+
+ return false; // All retries failed
}
String AppUpdater::getLocalMD5(const char* filePath){
@@ -295,6 +454,7 @@ String AppUpdater::getLocalMD5(const char* filePath){
bool AppUpdater::updateFilesArray() {
int successCount = 0;
int totalFiles = jsonFilesArray.size();
+ int failedCount = 0;
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
// Iterate over each file entry in the manifest
@@ -320,11 +480,19 @@ bool AppUpdater::updateFilesArray() {
// Attempt to update the file
if (updateFile(remotePath, localPath, expectedMd5)) {
successCount++;
+ } else {
+ failedCount++;
+ ESP_LOGE(TAG, "Failed to update file: %s", localPath);
+ // Continue with remaining files instead of stopping
}
}
- ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
- return successCount == totalFiles;
+ ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated, %d failed",
+ successCount, totalFiles, failedCount);
+
+ // Consider the update successful if most files updated correctly
+ // Allow up to 20% of files to fail before considering the entire update failed
+ return (failedCount <= totalFiles * 0.2);
}
bool AppUpdater::updateApp() {
@@ -341,17 +509,53 @@ bool AppUpdater::updateApp() {
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
String firmwareUrl = buildUrl(appName);
- // Download the firmware
+ // First, try a HEAD request to verify server connectivity and file availability
+ HTTPClient httpHead;
+ bool fileExists = false;
+ httpHead.begin(firmwareUrl);
+ httpHead.setTimeout(10);
+ int headCode = httpHead.sendRequest("HEAD");
+ if (headCode == HTTP_CODE_OK) {
+ ESP_LOGI(TAG, "Firmware file exists on server, size: %d bytes", httpHead.getSize());
+ fileExists = true;
+ } else {
+ ESP_LOGW(TAG, "HEAD request failed: %d, proceeding with direct download attempt", headCode);
+ }
+ httpHead.end();
+
+ // Download the firmware with progressive timeouts
HTTPClient http;
int httpCode = -1;
for(int attempt=0; attemptlabel, update->label);
+
+ // Check for sufficient free memory - much more lenient now with chunked downloads
+ size_t freeHeap = ESP.getFreeHeap();
+ size_t minRequiredHeap = 40 * 1024; // Only require 40KB minimum
+
+ ESP_LOGI(TAG, "Free heap: %d bytes, firmware size: %d bytes", freeHeap, firmwareSize);
+
+ if (freeHeap < minRequiredHeap) {
+ ESP_LOGE(TAG, "Critically low memory: %d bytes (minimum required: %d bytes)",
+ freeHeap, minRequiredHeap);
+ updateProgress(UpdateStatus::ERROR, 0, "Firmware: Critically low memory");
+ http.end();
+ return false;
+ }
+
+ // Check if we can roll back (just for logging, not a blocker)
+ if (!Update.canRollBack()) {
+ ESP_LOGW(TAG, "No valid firmware to roll back to");
+ }
+
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
ESP_LOGE(TAG, "Firmware: Not enough space for update");
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
@@ -372,49 +616,187 @@ bool AppUpdater::updateApp() {
MD5Builder md5;
md5.begin();
- // Download and verify firmware
+ // Prepare for download with improved resilience
WiFiClient* stream = http.getStreamPtr();
+ unsigned long lastProgressTime = millis(); // Track time since last progress
+ unsigned long connectStartTime = millis(); // Track total download time
+ unsigned long lastWatchdogKick = millis(); // Track watchdog kicks
+
+ // Calculate smaller buffer size based on heap
+ size_t bufferSize = std::min(size_t(4096), ESP.getFreeHeap() / 16); // Use at most 1/16 of free heap, max 4KB
+ if (bufferSize < 1024) bufferSize = 1024; // Minimum 1KB
+
+ ESP_LOGI(TAG, "Using download buffer size: %u bytes (free heap: %u bytes)",
+ bufferSize, ESP.getFreeHeap());
+
+ // Create a buffer just for this operation if needed
+ std::unique_ptr localBuffer;
+ uint8_t* downloadBuf = nullptr;
+
+ if (bufferSize <= BUFFER_SIZE) {
+ // Use the existing downloadBuffer
+ downloadBuf = downloadBuffer.get();
+ } else {
+ // Create a larger buffer for this specific download
+ localBuffer.reset(new uint8_t[bufferSize]);
+ downloadBuf = localBuffer.get();
+ }
+
+ updateProgress(UpdateStatus::DOWNLOADING, 0, "Firmware download started");
+
+ // Download and verify firmware
if (firmwareSize > 0) {
size_t remaining = firmwareSize;
- while (remaining > 0) {
- if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
- size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
- size_t read = stream->readBytes(downloadBuffer.get(), chunk);
-
- // Check for timeout
- if (read == 0) {
- ESP_LOGE(TAG, "Read timeout");
- Update.abort();
- http.end();
- return false;
+ size_t totalReceived = 0;
+ size_t failedReads = 0; // Count consecutive read failures
+ const size_t MAX_FAILED_READS = 10; // Allow up to 10 consecutive failures
+
+ while (remaining > 0 && failedReads < MAX_FAILED_READS) {
+ // Check for cancellation
+ if(g_UpdateCancelFlag) {
+ ESP_LOGE(TAG, "Update cancelled by user");
+ Update.abort();
+ http.end();
+ return false;
}
- // Update MD5 and write firmware
- md5.add(downloadBuffer.get(), read);
- if (Update.write(downloadBuffer.get(), read) != read) {
- ESP_LOGE(TAG, "Write failed");
- Update.abort();
- http.end();
- return false;
+ // Check WiFi status every 5 seconds
+ if (millis() - lastWatchdogKick > 5000) {
+ if (WiFi.status() != WL_CONNECTED) {
+ ESP_LOGE(TAG, "WiFi disconnected during download");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ // Reset watchdog to prevent timeout
+ esp_task_wdt_reset();
+ lastWatchdogKick = millis();
}
- remaining -= read;
- updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
+ // Calculate optimum chunk size to balance speed vs reliability
+ size_t chunk = std::min(remaining, bufferSize);
+ size_t available = stream->available();
+
+ if (available > 0) {
+ // Read what's available (up to our chunk size)
+ size_t read = stream->readBytes(downloadBuf, std::min(available, chunk));
+
+ if (read > 0) {
+ // Reset failure counter on successful read
+ failedReads = 0;
+
+ // Update MD5 and write firmware
+ md5.add(downloadBuf, read);
+ if (Update.write(downloadBuf, read) != read) {
+ ESP_LOGE(TAG, "Write failed");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ remaining -= read;
+ totalReceived += read;
+ lastProgressTime = millis(); // Reset progress timer
+
+ // Update progress every ~2% or at least every 2 seconds
+ static int lastPercent = 0;
+ int percent = (totalReceived * 100) / firmwareSize;
+ if (percent != lastPercent || (millis() - lastProgressTime > 2000)) {
+ updateProgress(UpdateStatus::DOWNLOADING, percent, "Firmware");
+ lastPercent = percent;
+ }
+ } else {
+ // Handle zero bytes read despite data being available
+ failedReads++;
+ ESP_LOGW(TAG, "Read returned 0 bytes despite data available (failure %d/%d)",
+ failedReads, MAX_FAILED_READS);
+ delay(100); // Short delay before retry
+ }
+ } else {
+ // No data currently available
+ if (millis() - lastProgressTime > 60000) {
+ // No progress for 60 seconds - too long
+ ESP_LOGE(TAG, "Download timed out - no progress for 60 seconds");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ // Send periodic progress updates to keep client informed
+ if (millis() - lastWatchdogKick > 5000) {
+ int percent = (totalReceived * 100) / firmwareSize;
+ updateProgress(UpdateStatus::DOWNLOADING, percent,
+ String("Firmware: " + String(percent) + "% - waiting for data...").c_str());
+ }
+
+ delay(100); // Short delay to prevent CPU hogging
+ }
+ }
+
+ // Check if we failed due to too many consecutive read failures
+ if (failedReads >= MAX_FAILED_READS) {
+ ESP_LOGE(TAG, "Too many consecutive read failures");
+ Update.abort();
+ http.end();
+ return false;
}
} else {
- // Unknown size: stream until end
- for (;;) {
+ // Unknown size: stream until end (less common case)
+ ESP_LOGW(TAG, "Firmware size unknown, streaming until end");
+ size_t totalReceived = 0;
+ size_t emptyReads = 0;
+ const size_t MAX_EMPTY_READS = 20; // Allow up to 20 empty reads before considering done
+
+ while (emptyReads < MAX_EMPTY_READS) {
if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
- size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
- if (read == 0) break;
- md5.add(downloadBuffer.get(), read);
- if (Update.write(downloadBuffer.get(), read) != read) {
- ESP_LOGE(TAG, "Write failed");
- Update.abort();
- http.end();
- return false;
+
+ // Reset watchdog periodically
+ if (millis() - lastWatchdogKick > 5000) {
+ esp_task_wdt_reset();
+ lastWatchdogKick = millis();
}
- updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
+
+ size_t available = stream->available();
+ if (available > 0) {
+ size_t read = stream->readBytes(downloadBuf, std::min(available, bufferSize));
+ if (read > 0) {
+ emptyReads = 0; // Reset empty read counter
+ md5.add(downloadBuf, read);
+ if (Update.write(downloadBuf, read) != read) {
+ ESP_LOGE(TAG, "Write failed");
+ Update.abort();
+ http.end();
+ return false;
+ }
+ totalReceived += read;
+ lastProgressTime = millis();
+
+ // Just update with received byte count since we don't know total
+ updateProgress(UpdateStatus::DOWNLOADING, 0,
+ String("Firmware: " + String(totalReceived / 1024) + "KB received").c_str());
+ } else {
+ emptyReads++;
+ delay(100);
+ }
+ } else {
+ // No data available
+ if (totalReceived > 0 && millis() - lastProgressTime > 30000) {
+ // If we've received data but nothing for 30s, probably done
+ ESP_LOGI(TAG, "No data for 30s after receiving %d bytes, assuming download complete", totalReceived);
+ break;
+ }
+
+ emptyReads++;
+ delay(100);
+ }
+ }
+
+ if (totalReceived < 100*1024) { // Less than 100KB received
+ ESP_LOGE(TAG, "Downloaded firmware too small (%d bytes)", totalReceived);
+ Update.abort();
+ http.end();
+ return false;
}
}
@@ -432,8 +814,16 @@ bool AppUpdater::updateApp() {
// Finish update
if (!Update.end()) {
- ESP_LOGE(TAG, "Update end failed");
+ ESP_LOGE(TAG, "Update end failed: %d", Update.getError());
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
+
+ // Try to roll back if possible
+ if (Update.hasError() && Update.canRollBack()) {
+ ESP_LOGI(TAG, "Rolling back to previous version");
+ Update.rollBack();
+ // Don't restart here, let the main task do it
+ }
+
http.end();
return false;
}
@@ -468,49 +858,131 @@ void startFirmwareUpdateTask(AsyncEventSource* evProg) {
ESP_LOGW(TAG, "Firmware update task already running");
return;
}
- xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
+ // Create task with higher priority (3) and optimized stack size
+ xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*6, NULL, 3, &Update_Task_Handle);
}
void firmwareUpdateTask(void* parameter) {
static const char* TAG = "UpdateTask";
- AppUpdater* updater = nullptr;
+
+ // Initialize watchdog timer for the update task
+ esp_task_wdt_init(60, true); // 60 second timeout, panic on timeout
+ esp_task_wdt_add(NULL); // Add current task to watchdog
try {
loadUpdateJson();
+
+ esp_task_wdt_reset(); // Reset watchdog timer after JSON loading
- // Initialize updater
- updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
+ // Initialize updater with smart pointer
+ std::unique_ptr 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());
// Check and perform updates
- if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); }
+ auto manifestResult = updater->checkManifest();
+
+ if (manifestResult != AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE) {
+ // Handle different error cases
+ std::string errorMsg;
+ switch (manifestResult) {
+ case AppUpdater::ManifestCheckResult::ERROR_FETCH_FAILED:
+ errorMsg = "Failed to fetch manifest";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_TOO_LARGE:
+ errorMsg = "Manifest file too large";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_PARSE_FAILED:
+ errorMsg = "Failed to parse manifest";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_NO_FILES_SECTION:
+ errorMsg = "Manifest missing files section";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_NO_VERSION:
+ errorMsg = "Manifest missing version section";
+ break;
+ case AppUpdater::ManifestCheckResult::VERSION_CURRENT:
+ errorMsg = "Current version is up to date";
+ // This is not actually an error
+ ESP_LOGI(TAG, "No update needed: %s", errorMsg.c_str());
+ updateProgress(AppUpdater::UpdateStatus::MESSAGE, 0, errorMsg.c_str());
+ // Don't throw, just exit gracefully
+ break;
+ default:
+ errorMsg = "Unknown manifest check error";
+ }
+
+ if (manifestResult != AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
+ ESP_LOGE(TAG, "Manifest check failed: %s", errorMsg.c_str());
+ updateProgress(AppUpdater::UpdateStatus::ERROR, 0, errorMsg.c_str());
+ }
+ }
if (updater->IsUpdateAvailable()) {
- ESP_LOGI(TAG, "Update available, updating files...");
+ bool filesUpdated = true;
+ bool firmwareUpdated = true;
- if (!updater->updateFilesArray()) {
- throw std::runtime_error("Failed to update files");
+ // Update files based on update mode
+ if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
+ ESP_LOGI(TAG, "Update mode includes files, updating files...");
+ filesUpdated = updater->updateFilesArray();
+ if (!filesUpdated) {
+ ESP_LOGW(TAG, "Some files failed to update");
+ if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY) {
+ ESP_LOGE(TAG, "Files-only update failed");
+ updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update files");
+ // Skip to cleanup since this is files-only mode and it failed
+ goto cleanup;
+ } else {
+ ESP_LOGW(TAG, "File update failed, but continuing with firmware update");
+ }
+ }
+ } else {
+ ESP_LOGI(TAG, "Skipping file updates (mode: firmware only)");
}
- ESP_LOGI(TAG, "Updating firmware...");
- if (!updater->updateApp()) {
- throw std::runtime_error("Failed to update firmware");
+ // Update firmware based on update mode
+ if (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
+ ESP_LOGI(TAG, "Update mode includes firmware, updating firmware...");
+ firmwareUpdated = updater->updateApp();
+ if (!firmwareUpdated) {
+ ESP_LOGE(TAG, "Failed to update firmware");
+ updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update firmware");
+ // Skip to cleanup since firmware update failed
+ goto cleanup;
+ }
+ } else {
+ ESP_LOGI(TAG, "Skipping firmware update (mode: files only)");
}
- ESP_LOGI(TAG, "Update successful, restarting...");
- sendUpdateMessage("Restarting ", true, 100);
- vTaskDelay(2000);
-
- ESP.restart();
+ // Determine if we need to restart
+ bool needsRestart = (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) && firmwareUpdated;
+ if (needsRestart) {
+ ESP_LOGI(TAG, "Firmware update successful, restarting...");
+ sendUpdateMessage("Restarting ", true, 100);
+ vTaskDelay(2000);
+ ESP.restart();
+ } else {
+ ESP_LOGI(TAG, "Update completed successfully (no restart required)");
+ updateProgress(AppUpdater::UpdateStatus::COMPLETE, 100, "Update completed successfully");
+ }
}
+cleanup:
} catch (const std::exception& e) {
- ESP_LOGE(TAG, "Update failed: %s", e.what());
+ ESP_LOGE(TAG, "Update failed with exception: %s", e.what());
+ updateProgress(AppUpdater::UpdateStatus::ERROR, 0, e.what());
+ } catch (...) {
+ ESP_LOGE(TAG, "Update failed with unknown exception");
+ updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Unknown error during update");
}
- delete updater;
+
+ // Clean up watchdog before exit
+ esp_task_wdt_delete(NULL);
+
Update_Task_Handle = NULL;
vTaskDelete(NULL);
}
@@ -520,7 +992,8 @@ void startVersionCheckTask() {
ESP_LOGW(TAG, "Version Check Tak already running");
return;
}
- xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
+ // Create task with higher priority (3) and optimized stack size
+ xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*6, NULL, 3, &versionCheckTask_Handle);
}
void versionCheckTask(void* parameter){
@@ -528,12 +1001,17 @@ void versionCheckTask(void* parameter){
loadUpdateJson();
}
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
- if(!updater.checkManifest()){
- ESP_LOGE(TAG, "Version check: manifest fetch failed");
- } else {
+
+ auto manifestResult = updater.checkManifest();
+
+ if (manifestResult == AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE ||
+ manifestResult == AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
otaVersion = updater.otaVersion; // capture remote
ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
+ } else {
+ ESP_LOGE(TAG, "Version check: manifest check failed with code %d", static_cast(manifestResult));
}
+
versionCheckTask_Handle = NULL;
vTaskDelete(NULL);
}
@@ -593,7 +1071,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SKIPPED:
- snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
+ snprintf(buffer, sizeof(buffer), "%s: File Skipped, up to date", safeMsg);
msg = buffer;
break;
case AppUpdater::UpdateStatus::FILE_SAVED:
@@ -624,7 +1102,9 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch
}
void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
+
if(eventProgress && eventProgress->count() > 0) {
+ // This is for the web client and not the BLE client
JsonDocument jsonDoc;
jsonDoc["message"] = message;
jsonDoc["complete"] = complete;
@@ -640,41 +1120,27 @@ void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
bleUpgrade_send_message(message);
}
-// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
-
-/*
-void setup() {
- Serial.begin(115200);
-
- // Initialize WiFi connection first
- // ... WiFi connection code ...
-
- // Initialize filesystem
- if(!LittleFS.begin()) {
- Serial.println("LittleFS Mount Failed");
- return;
- }
-
- // Create updater instance with:
- // - Current version: "1.0.0"
- // - Update server URL: "https://my-update-server.com/"
- // - Filesystem: LittleFS
- AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
-
- // Set progress callback
- updater.setProgressCallback([](int progress) {
- Serial.printf("Update progress: %d%%\n", progress);
- });
-
- // Check and update firmware
- if (updater.checkAndUpdate()) {
- Serial.println("Update successful! Rebooting...");
- ESP.restart();
- }
-
- // Update specific files from manifest
- int updatedFiles = updater.updateFilesFromManifest("test_update.json");
- Serial.printf("Updated %d files\n", updatedFiles);
+// Convenience functions for setting update mode
+void setGlobalUpdateMode(UpdateMode mode) {
+ g_UpdateMode = mode;
+ ESP_LOGI(TAG, "Global update mode set to: %s",
+ mode == UpdateMode::UPDATE_FILES_ONLY ? "UPDATE_FILES_ONLY" :
+ mode == UpdateMode::UPDATE_FIRMWARE_ONLY ? "UPDATE_FIRMWARE_ONLY" : "UPDATE_BOTH");
+}
+
+UpdateMode getGlobalUpdateMode() {
+ return g_UpdateMode;
+}
+
+void setUpdateModeFilesOnly() {
+ setGlobalUpdateMode(UpdateMode::UPDATE_FILES_ONLY);
+}
+
+void setUpdateModeFirmwareOnly() {
+ setGlobalUpdateMode(UpdateMode::UPDATE_FIRMWARE_ONLY);
+}
+
+void setUpdateModeBoth() {
+ setGlobalUpdateMode(UpdateMode::UPDATE_BOTH);
}
-*/
\ No newline at end of file
diff --git a/src/BLE_SP110E.cpp b/src/BLE_SP110E.cpp
index dd28305..8fcc164 100644
--- a/src/BLE_SP110E.cpp
+++ b/src/BLE_SP110E.cpp
@@ -4,6 +4,7 @@
#include "WiFi.h"
#include "ATALights.h"
#include "BleSettings.h"
+#include
static const char *tag = "BLE_SP110E";
@@ -134,12 +135,23 @@ class LightStickCallbacks : public NimBLECharacteristicCallbacks {
// Function to send data to all connected clients in chunks based on MTU
void sendToAllClients(const uint8_t* data, size_t len) {
- if (!pStickCharacteristic || !data || len == 0) return;
+ if (!pStickCharacteristic) {
+ ESP_LOGW(tag, "Cannot send to clients: pStickCharacteristic is null");
+ return;
+ }
+
+ if (!data || len == 0) {
+ ESP_LOGW(tag, "Cannot send to clients: data is null or length is 0");
+ return;
+ }
// Skip if no subscribed clients (if API available)
#if defined(NIMBLE_INCLUDED) || true
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
- if (pStickCharacteristic->getSubscribedCount() == 0) return;
+ if (pStickCharacteristic->getSubscribedCount() == 0) {
+ ESP_LOGD(tag, "No clients subscribed, skipping notification");
+ return;
+ }
#endif
#endif
@@ -152,9 +164,19 @@ void sendToAllClients(const uint8_t* data, size_t len) {
while (offset < len) {
size_t chunk = len - offset;
if (chunk > maxChunk) chunk = maxChunk;
- pStickCharacteristic->setValue(data + offset, chunk);
- // notify() returns void in this NimBLE version, so just call it without checking a return value
- pStickCharacteristic->notify();
+
+ try {
+ pStickCharacteristic->setValue(data + offset, chunk);
+ // notify() returns void in this NimBLE version, but wrap in try/catch for robustness
+ pStickCharacteristic->notify();
+
+ ESP_LOGD(tag, "Sent %zu bytes to clients", chunk);
+ } catch (const std::exception& e) {
+ ESP_LOGE(tag, "Exception during notification: %s", e.what());
+ // Consider adding a delay or recovery mechanism here
+ break;
+ }
+
offset += chunk;
}
}
@@ -172,100 +194,113 @@ void sendToAllClients(const uint8_t *data, size_t len) {
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
+ if (!val) {
+ ESP_LOGE(tag, "Null command data received");
+ return;
+ }
+
+ if (len < 4) {
+ ESP_LOGW(tag, "Command too short: %d bytes, expected at least 4", len);
+ return;
+ }
- if (len >= 4) {
- uint8_t command = val[3];
- ESP_LOGI(tag, "Command received: 0x%02X", command);
+ uint8_t command = val[3];
+ ESP_LOGI(tag, "Command received: 0x%02X", command);
- uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer
+ uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer
- // Handle different commands
- switch (command) {
- case TURN_ON:
- Lights_Set_ON();
- led_status.enable = 1;
- //ESP_LOGI(tag, "Lights ON");
+ // Handle different commands
+ switch (command) {
+ case TURN_ON:
+ Lights_Set_ON();
+ led_status.enable = 1;
+ //ESP_LOGI(tag, "Lights ON");
+ break;
+ case TURN_OFF:
+ Lights_Set_OFF();
+ led_status.enable = 0;
+ //ESP_LOGI(tag, "Lights OFF");
+ break;
+ case SET_STATIC_COLOR:
+ if(len < 7) {
+ ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
break;
- case TURN_OFF:
- Lights_Set_OFF();
- led_status.enable = 0;
- //ESP_LOGI(tag, "Lights OFF");
+ }
+ led_status.red = val[1];
+ led_status.green = val[2];
+ led_status.blue = val[0];
+ Lights_Set_Animation(SOLID_COLOR_INDEX, val[0], val[1], val[2]);
+ //ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
+ break;
+ case SET_BRIGHT:
+ if(len < 5) {
+ ESP_LOGW(tag, "SET_BRIGHT command requires 1 parameter (brightness)");
break;
- case SET_STATIC_COLOR:
- if(len < 7) {
- ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
- break;
- }
- led_status.red = val[1];
- led_status.green = val[2];
- led_status.blue = val[0];
- Lights_Set_Animation(SOLID_COLOR_INDEX, val[0], val[1], val[2]);
- //ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
+ }
+ led_status.bright = val[0];
+ Lights_Set_Brightness(val[0]);
+ //ESP_LOGI(tag, "Bright set to %d", led_status.bright);
+ break;
+ case SET_WHITE:
+ led_status.white = val[0];
+ Lights_Set_White(val[0]);
+ //ESP_LOGI(tag, "White set to %d", led_status.white);
+ break;
+ case SET_PRESET:
+ led_status.preset = val[0];
+ Lights_Set_Animation(val[0], val[1], val[2], 0);
+ //ESP_LOGI(tag, "Animation set to %d", led_status.preset);
+ break;
+ case SET_SPEED:
+ led_status.speed = val[0];
+ ESP_LOGI(tag, "Mode set to %d", led_status.speed);
+ break;
+ case GET_CHECK_DEVICE: // This prepends a checksum
+ led_status.checksum = calculateChecksum(val);
+ if(bleChar != nullptr){
+ bleChar->setValue((uint8_t *)&led_status, sizeof(INFO_PACK));
+ bleChar->notify(); // Send the data immediately
+ }
+ ESP_LOGI(tag, "Check Device");
+ break;
+ case GET_DEVICE_INFO: // No checksum
+ led_status.checksum = 0;
+ if(bleChar != nullptr){
+ bleChar->setValue(((uint8_t *)&led_status) + 1, sizeof(INFO_PACK) - 1);
+ bleChar->notify(); // Send the data immediately
+ }
+ ESP_LOGI(tag, "Get Device Info");
+ break;
+ case SET_IC_MODEL:
+ led_status.ic_model = val[0];
+ ESP_LOGI(tag, "IC Model set to %d", led_status.ic_model);
+ break;
+ case SET_RGB_SEQUENCE:
+ led_status.channel = 0;
+ ESP_LOGI(tag, "Set RGB Sequence");
+ break;
+ case SET_LED_NUM:
+ led_status.count_msb = 0;
+ led_status.count_lsb = 100;
+ ESP_LOGI(tag, "Set LED Num");
+ break;
+ case SET_DEVICE_NAME:
+ ESP_LOGI(tag, "Set Device Name");
+ break;
+ default:
+ ESP_LOGW(tag, "Unknown command: 0x%02X", command);
break;
- case SET_BRIGHT:
- if(len < 5) {
- ESP_LOGW(tag, "SET_BRIGHT command requires 1 parameter (brightness)");
- break;
- }
- led_status.bright = val[0];
- Lights_Set_Brightness(val[0]);
- //ESP_LOGI(tag, "Bright set to %d", led_status.bright);
- break;
- case SET_WHITE:
- led_status.white = val[0];
- Lights_Set_White(val[0]);
- //ESP_LOGI(tag, "White set to %d", led_status.white);
- break;
- case SET_PRESET:
- led_status.preset = val[0];
- Lights_Set_Animation(val[0], val[1], val[2], 0);
- //ESP_LOGI(tag, "Animation set to %d", led_status.preset);
- break;
- case SET_SPEED:
- led_status.speed = val[0];
- ESP_LOGI(tag, "Mode set to %d", led_status.speed);
- break;
- case GET_CHECK_DEVICE: // This prepends a checksum
- led_status.checksum = calculateChecksum(val);
- if(bleChar != nullptr){
- bleChar->setValue((uint8_t *)&led_status, sizeof(INFO_PACK));
- bleChar->notify(); // Send the data immediately
- }
- ESP_LOGI(tag, "Check Device");
- break;
- case GET_DEVICE_INFO: // No checksum
- led_status.checksum = 0;
- if(bleChar != nullptr){
- bleChar->setValue(((uint8_t *)&led_status) + 1, sizeof(INFO_PACK) - 1);
- bleChar->notify(); // Send the data immediately
- }
- ESP_LOGI(tag, "Get Device Info");
- break;
- case SET_IC_MODEL:
- led_status.ic_model = val[0];
- ESP_LOGI(tag, "IC Model set to %d", led_status.ic_model);
- break;
- case SET_RGB_SEQUENCE:
- led_status.channel = 0;
- ESP_LOGI(tag, "Set RGB Sequence");
- break;
- case SET_LED_NUM:
- led_status.count_msb = 0;
- led_status.count_lsb = 100;
- ESP_LOGI(tag, "Set LED Num");
- break;
- case SET_DEVICE_NAME:
- ESP_LOGI(tag, "Set Device Name");
- break;
- default:
- ESP_LOGW(tag, "Unknown command: 0x%02X", command);
- break;
- }
}
}
+
void Init_BLE_SP110E(NimBLEServer* pServer) {
+ if (!pServer) {
+ ESP_LOGE(tag, "Invalid BLE server pointer");
+ return;
+ }
+
led_status.speed = 10;
led_status.bright = 50;
led_status.ic_model = 0;
@@ -325,7 +360,14 @@ void Init_BLE_LightStick_Client(){
ESP_LOGW(tag, "Light Stick Client Task already running");
return;
}
- xTaskCreate(BLE_LightStick_Client_Task, "VersionCheckTask", 1024*8, NULL, 1, &LightStick_Client_Task_Handle);
+
+ BaseType_t result = xTaskCreate(BLE_LightStick_Client_Task, "LightStickTask", 1024*6, NULL, 1, &LightStick_Client_Task_Handle);
+ if (result != pdPASS) {
+ ESP_LOGE(tag, "Failed to create Light Stick client task, error: %d", result);
+ LightStick_Client_Task_Handle = NULL;
+ } else {
+ ESP_LOGI(tag, "Light Stick client task created successfully");
+ }
}
// Task for the BLE LightStick client
@@ -333,8 +375,14 @@ void Init_BLE_LightStick_Client(){
void BLE_LightStick_Client_Task(void *parameter) {
static const char *tag = "BLE_LightStick_Client_Task";
ESP_LOGI(tag, "BLE LightStick Client Task started");
+
+ // Register task with watchdog
+ esp_task_wdt_add(NULL);
while (true) {
+ // Reset watchdog timer
+ esp_task_wdt_reset();
+
// Only try to connect if we're not already connected and a device is set.
if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) {
// Create a new client instance if needed.
@@ -371,11 +419,20 @@ void BLE_LightStick_Client_Task(void *parameter) {
ESP_LOGE(tag, "Failed to connect to the server");
// Delete the client instance so that a new one is created next time.
if (pStickClient != nullptr) {
- NimBLEDevice::deleteClient(pStickClient);
- pStickClient = nullptr;
+ try {
+ NimBLEDevice::deleteClient(pStickClient);
+ pStickClient = nullptr;
+ } catch (const std::exception& e) {
+ ESP_LOGE(tag, "Exception deleting client: %s", e.what());
+ }
}
- // Wait before retrying.
- vTaskDelay(pdMS_TO_TICKS(5000));
+ // Implement exponential backoff for connection retries
+ static uint16_t retryDelay = 2000; // Start with 2 seconds
+ retryDelay = (retryDelay * 3) / 2; // Increase by 50% each time
+ if (retryDelay > 30000) retryDelay = 30000; // Cap at 30 seconds
+
+ ESP_LOGI(tag, "Will retry in %d ms", retryDelay);
+ vTaskDelay(pdMS_TO_TICKS(retryDelay));
continue;
}
}
@@ -390,6 +447,12 @@ void BLE_LightStick_Client_Task(void *parameter) {
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
+
+ // Task should never exit, but in case it does:
+ ESP_LOGI(tag, "BLE LightStick Client Task ending");
+ esp_task_wdt_delete(NULL);
+ LightStick_Client_Task_Handle = NULL;
+ vTaskDelete(NULL);
}
diff --git a/src/BLE_UpdateService.cpp b/src/BLE_UpdateService.cpp
index d5f3563..3232b82 100644
--- a/src/BLE_UpdateService.cpp
+++ b/src/BLE_UpdateService.cpp
@@ -80,6 +80,17 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
}
else if (value.compare("upgrade-start") == 0) { // Start OTA update
ESP_LOGI(tag, "Start OTA update command received");
+ setUpdateModeBoth();
+ startFirmwareUpdateTask(nullptr); // start the task
+ }
+ else if (value.compare("upgrade-start-files-only") == 0) { // Start OTA update
+ ESP_LOGI(tag, "Start OTA update files-only command received");
+ setUpdateModeFilesOnly();
+ startFirmwareUpdateTask(nullptr); // start the task
+ }
+ else if (value.compare("upgrade-start-firmware-only") == 0) { // Start OTA update
+ ESP_LOGI(tag, "Start OTA update firmware-only command received");
+ setUpdateModeFirmwareOnly();
startFirmwareUpdateTask(nullptr); // start the task
}
else if (value.compare("rename-device") == 0) { // Start renaming device
@@ -132,14 +143,66 @@ void bleUpgrade_send_message(String s){
if (s.length() == 0) {
return;
}
- // Set value and notify only if there are subscribers to avoid unnecessary work
- pUpgradeCharacteristic2->setValue(s.c_str());
+
+ // Log message details before sending
+ ESP_LOGI(tag, "Sending BLE message, length=%d bytes", s.length());
+ if (s.length() < 100) {
+ ESP_LOGI(tag, "Message content: '%s'", s.c_str());
+ }
+
+ // For testing - ensure string is null-terminated properly
+ String paddedString = s;
+
+ // Explicitly set using raw bytes with explicit length
+ const char* raw = paddedString.c_str();
+ size_t rawLen = paddedString.length();
+
+ // Explicitly handle null-termination ourselves
+ std::string stdStr(raw, rawLen);
+ pUpgradeCharacteristic2->setValue(stdStr);
+
+ // Log the value that was actually set
+ std::string valueAfterSet = pUpgradeCharacteristic2->getValue();
+ ESP_LOGI(tag, "Value after set: length=%d bytes", valueAfterSet.length());
+
+ if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
+ pUpgradeCharacteristic2->notify();
+ ESP_LOGI(tag, "Notification sent");
+ } else {
+ ESP_LOGW(tag, "No subscribers for notification");
+ }
+ }
+}
+
+/*
+void bleUpgrade_send_message(String s) {
+ if(pUpgradeCharacteristic2) {
+ if (s.length() == 0) {
+ return;
+ }
+
+ // OPTION 1: Sanitize non-printable characters
+ String sanitized = "";
+ for (size_t i = 0; i < s.length(); i++) {
+ char c = s[i];
+ // Only keep printable ASCII characters and common whitespace
+ if ((c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t') {
+ sanitized += c;
+ } else {
+ // Replace non-printable with hexadecimal representation or skip
+ sanitized += String("[0x") + String(c, HEX) + "]";
+ // OR just: continue; // to skip unprintable chars
+ }
+ }
+
+ // Set value and notify only if there are subscribers
+ pUpgradeCharacteristic2->setValue(sanitized.c_str());
if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
pUpgradeCharacteristic2->notify();
}
}
}
-
+*/
void Init_UpgradeBLEService(NimBLEServer *pServer){
@@ -170,6 +233,5 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
-
}
diff --git a/src/BleServer.cpp b/src/BleServer.cpp
index 5bfa60c..ff9f713 100644
--- a/src/BleServer.cpp
+++ b/src/BleServer.cpp
@@ -3,6 +3,7 @@
#include "BLE_SP110E.h"
#include "BLE_UpdateService.h"
#include "BleSettings.h"
+#include "my_buzzer.h"
static const char* tag = "BleServer";
@@ -39,11 +40,13 @@ public:
void onConnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client connected");
ensureAdvertising("onConnect");
+ Buzzer_Play_Tune(TUNE_CONNECTED);
}
void onDisconnect(NimBLEServer* /*pServer*/) override {
ESP_LOGI(tag, "Client disconnected");
ensureAdvertising("onDisconnect");
+ Buzzer_Play_Tune(TUNE_DISCONNECTED, 1);
}
private:
diff --git a/src/JsonConstrain.cpp b/src/JsonConstrain.cpp
index 9b079d9..9418666 100644
--- a/src/JsonConstrain.cpp
+++ b/src/JsonConstrain.cpp
@@ -89,26 +89,55 @@ const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, con
String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def) {
- // Check if the key exists and is not null
- if (!jsonObject[key].is()) {
- ESP_LOGW(tag, "Key [%s] not found or null. Using default value [%s].", key, def.c_str());
+ // Check if the key exists using the recommended approach
+ if (!jsonObject[key] || jsonObject[key].isNull()) {
+ ESP_LOGW(tag, "Key [%s] not found/null", key);
return def;
}
-
- // Extract the value as a String
+
+ // Handle different types to avoid unnecessary String conversions
+ if (jsonObject[key].is()) {
+ const char* charValue = jsonObject[key].as();
+ if (!charValue || *charValue == '\0') {
+ ESP_LOGW(tag, "Key [%s] empty", key);
+ return def;
+ }
+
+ // Create String object for return value (only once)
+ String value(charValue);
+
+ // Process string only if needed
+ const size_t MAX_STRING_LENGTH = 1024;
+ if (value.length() > MAX_STRING_LENGTH) {
+ value = value.substring(0, MAX_STRING_LENGTH);
+ }
+
+ // Minimal logging
+ ESP_LOGD(tag, "Key [%s] value set", key);
+ return value;
+ }
+
+ // For non-char types, use standard String conversion
String value = jsonObject[key].as();
-
- // Check if the value is empty
+
+ // Check for empty string
if (value.length() == 0) {
- ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str());
+ ESP_LOGW(tag, "Key [%s] empty", key);
return def;
}
-
- ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str());
+
+ // Apply length constraint
+ const size_t MAX_STRING_LENGTH = 1024;
+ if (value.length() > MAX_STRING_LENGTH) {
+ value = value.substring(0, MAX_STRING_LENGTH);
+ }
+
+ // Minimal logging to reduce memory usage
+ ESP_LOGD(tag, "Key [%s] value set", key);
return value;
}
-
+/*
bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
// Check if the key exists and is of type boolean
if (!jsonObject[key].is()) {
@@ -121,6 +150,50 @@ bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char
ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
return value;
}
+*/
+
+bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
+ // Check if the key exists using the recommended approach
+ if (!jsonObject[key] || jsonObject[key].isNull()) {
+ ESP_LOGW(tag, "Key [%s] not found or null. Using default [%s]", key, def ? "true" : "false");
+ return def;
+ }
+
+ // Direct boolean check first (fastest path, no conversion)
+ if (jsonObject[key].is()) {
+ bool value = jsonObject[key].as();
+ ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
+ return value;
+ }
+
+ // Numeric conversion (no heap allocation)
+ if (jsonObject[key].is() || jsonObject[key].is()) {
+ bool value = (jsonObject[key].as() != 0);
+ ESP_LOGD(tag, "Key [%s] numeric converted to: %s", key, value ? "true" : "false");
+ return value;
+ }
+
+ // String conversion - minimal processing
+ if (jsonObject[key].is()) {
+ const char* str = jsonObject[key].as();
+ // Fast direct comparison with common values (no heap allocation)
+ if (strcmp(str, "true") == 0 || strcmp(str, "1") == 0 ||
+ strcmp(str, "yes") == 0 || strcmp(str, "on") == 0) {
+ return true;
+ }
+ if (strcmp(str, "false") == 0 || strcmp(str, "0") == 0 ||
+ strcmp(str, "no") == 0 || strcmp(str, "off") == 0) {
+ return false;
+ }
+
+ ESP_LOGW(tag, "Key [%s] string couldn't convert to bool, using default [%s]",
+ key, def ? "true" : "false");
+ } else {
+ ESP_LOGW(tag, "Key [%s] type not supported for bool conversion", key);
+ }
+
+ return def;
+}
// Explicit instantiations
diff --git a/src/main.cpp b/src/main.cpp
index bc481e7..d0c468f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -144,7 +144,8 @@ void setup()
Init_Board_Basic(sys_settings.boardPins);
// Load tunes.json and initialize
- Init_Buzzer(sys_settings.boardPins.buzzer, "/system/tunes.json");
+ // Reserve channel 7 for buzzer (highest channel number to avoid conflicts)
+ Init_Buzzer(sys_settings.boardPins.buzzer, "/system/tunes.json", 7);
// Initialize PWM Outputs
Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings);
@@ -171,10 +172,13 @@ void setup()
{
setStatusPin1(true);
UpgradeMode = true;
+ ESP_LOGW(tag, "Upgrade Mode Triggered");
ESP_LOGW(tag, "Enabling BLE and Update Service");
Init_BleServer(true, true);
ESP_LOGW(tag, "Enabling Wifi AP and Client");
Wifi_Init();
+
+ //Buzzer_Play_Tune(TUNE_UPGRADE_MODE);
}
else
{
@@ -197,7 +201,7 @@ void setup()
Init_Lights_Task();
#endif
- Buzzer_Play_Tune(TUNE_BOOT, true, true);
+ Buzzer_Play_Tune(TUNE_BOOT);
// TODO... Test if this is still necessary need to configure pin 0 for some reason
// pinMode(0, INPUT); // button0/boot pin
@@ -225,29 +229,21 @@ void loop()
}
// Temperature Monitor
- ON_EVERY_N_MILLISECONDS(5000)
+ static OnEveryMsVariable temperatureMonitorTimer;
+ if (sys_settings.tSensorSettings.enabled)
{
- static float boardTemperature;
-
- // Read temperature if the sensor is enabled
- if (sys_settings.tSensorSettings.enabled)
+ if (temperatureMonitorTimer.ready(sys_settings.tSensorSettings.intervalMs))
{
+ static float boardTemperature;
boardTemperature = tSensor->readTemperatureF();
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
- }
- // Fan Control
- if (sys_settings.tSensorSettings.enabled)
- {
+ // Fan Control
UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]);
+
}
}
- // Update Tune Playing
- //if (anyrtttl::nonblocking::isPlaying())
- //{
- // anyrtttl::nonblocking::play();
- //}
// Animation TestMode Timeout
#if LEDS_ENABLED
@@ -274,7 +270,7 @@ void loop()
for (int i = 0; i < 3; i++)
{
#if BUZZER_ENABLED
- Buzzer_Play_Tune(TUNE_BEEP, false); // blocking
+ Buzzer_Play_Tune(TUNE_LOWEEP); // blocking
#endif
vTaskDelay(200);
}
@@ -296,11 +292,12 @@ void loop()
}
}
- // Upgrade Mode Tune
+ // Upgrade Mode Hearbeat tune
if(UpgradeMode){
ON_EVERY_N_MILLISECONDS(5000)
{
- Buzzer_Play_Tune(TUNE_ACK, true, true);
+ Buzzer_Play_Tune(TUNE_LOWBEEP);
+ //ESP_LOGI(tag, "Upgrade Mode Heartbeat");
}
}
@@ -479,9 +476,6 @@ void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath)
sys_settings.rampLightSettings[rampIndex].vision = jsonConstrainBool(tag, obj, "vision", true);
sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain(tag, obj, "relay-index", 0, 1, 0);
sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain(tag, obj, "button-index", 0, 1, 0);
- sys_settings.rampLightSettings[rampIndex].min = jsonConstrain(tag, obj, "min", 0.0, 100.0, 0.0);
- sys_settings.rampLightSettings[rampIndex].max = jsonConstrain(tag, obj, "max", 5.0, 100.0, 100.0);
- sys_settings.rampLightSettings[rampIndex].step = jsonConstrain(tag, obj, "step", 0.01, 100.0, 1.5);
rampIndex++;
}
ESP_LOGI(tag, "Loaded Ramp Lights settings...");
diff --git a/src/my_buttons.cpp b/src/my_buttons.cpp
index 5a1c4ee..39d945f 100644
--- a/src/my_buttons.cpp
+++ b/src/my_buttons.cpp
@@ -1,6 +1,8 @@
#include "my_buttons.h"
#include "global.h"
#include "BLE_UpdateService.h"
+#include "esp_log.h"
+#include "AppUpgrade.h"
static const char* tag = "button";
OneButton *boardButtons[3];
@@ -111,6 +113,7 @@ void btn2_click() {
//Pulse_LED_Status(150);
//Buzzer_Beep(150);
// send packet
+ sendUpdateMessage("testing....", false, -1);
ESP_LOGD(tag, "btn2 1x");
}
diff --git a/src/my_buzzer.cpp b/src/my_buzzer.cpp
index b30cfb0..930b82f 100644
--- a/src/my_buzzer.cpp
+++ b/src/my_buzzer.cpp
@@ -7,102 +7,93 @@
#include
#include "JsonConstrain.h"
+#include "global.h"
+#include "RtttlPlayer.h"
+#include "esp_log.h"
const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g";
-
-// serial debugging enabled
-//#define ANY_RTTTL_INFO
-
static const char* tag = "buzzer";
+// Define static constexpr member from RtttlPlayer class
+constexpr uint16_t RtttlPlayer::LUT4[12];
+
+RtttlPlayer *player;
BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
int8_t buzzPin;
+int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
-
-void Init_Buzzer(int8_t pin, const char* configFile)
+void Init_Buzzer(int8_t pin, const char* configFile, int8_t channel)
{
buzzPin = pin;
if(buzzPin >= 0){
- pinMode(buzzPin, OUTPUT);
+
+ player = new RtttlPlayer(pin, channel);
+ buzzerChannel = channel;
+
+ ESP_LOGI(tag, "Buzzer hardware initialized on pin %d using LEDC channel %d", buzzPin, buzzerChannel);
}
Buzzer_Load_Tunes(configFile); // Load Tunes
- ESP_LOGI(tag, "Buzzer initialized..");
+ ESP_LOGI(tag, "Buzzer initialized on pin %d, channel %d", buzzPin, buzzerChannel);
}
-void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority)
+void Buzzer_Play_Tune(TUNE_TYPE tune, int priority)
{
- static int prev_tune = -1;
- if (buzzPin < 0) return;
+ if(buzzPin < 0 || !player) return;
- // Range / data validation
- if (tune < 0 || tune >= TUNE_MAX_COUNT) {
- ESP_LOGW(tag, "Invalid tune index: %d", (int)tune);
- return;
- }
- const String &melody = buzzTune[tune].melody;
- if (melody.isEmpty()) {
- ESP_LOGW(tag, "Empty melody for tune %d", (int)tune);
+ if(tune < 0 || tune >= TUNE_MAX_COUNT){
+ ESP_LOGW(tag, "Invalid tune index %d", tune);
return;
}
- // Async mode: begin once, then caller should periodically call again to advance playback
- if (async) {
- bool playing = anyrtttl::nonblocking::isPlaying();
- if (hasPriority && playing) {
- anyrtttl::nonblocking::stop();
- playing = false;
- }
- if (!playing || prev_tune != tune) {
- // (Re)start tune
- anyrtttl::nonblocking::begin(buzzPin, melody.c_str());
- prev_tune = tune;
- ESP_LOGD(tag, "Started async tune %d (%s)", (int)tune, melody.c_str());
- }
- // Advance playback one tick
- anyrtttl::nonblocking::play();
+ int cycles = buzzTune[tune].cycles;
+ int pause = buzzTune[tune].pause;
+ String melody = buzzTune[tune].melody;
+
+ if(melody.length() == 0){
+ ESP_LOGW(tag, "Tune %d has empty melody, skipping playback", tune);
return;
}
- // Blocking mode: play full tune cycles with optional pause
- ESP_LOGD(tag, "Playing blocking tune %d cycles=%d pause=%d", (int)tune, buzzTune[tune].cycles, buzzTune[tune].pause);
- for (int c = 0; c < buzzTune[tune].cycles; ++c) {
- anyrtttl::blocking::play(buzzPin, melody.c_str());
- if (buzzTune[tune].pause > 0 && c + 1 < buzzTune[tune].cycles) {
- delay(buzzTune[tune].pause); // simple pause between cycles
+ // Play the tune the specified number of cycles
+ for(int i = 0; i < cycles; i++){
+ bool played = player->play(melody.c_str(), priority ? 2 : 1); // Use priority level
+ if(!played){
+ ESP_LOGW(tag, "Failed to play tune %d (cycle %d)", tune, i+1);
+ return;
+ }
+
+ if(pause > 0 && i < cycles - 1){
+ delay(pause);
}
- yield(); // allow other tasks to run
}
- prev_tune = tune;
}
-
-// TODO Buzzer Beep finish
-void Buzzer_Beep(int mSecs, int freq)
-{
- /*
- ledcAttachPin(buzzPin, buzzerCh);
- ledcSetup(buzzerCh, 2000, 8);
- ledcWrite(buzzerCh, 125);
- vTaskDelay(mSecs);
- ledcWrite(buzzerCh, 0);
- */
-}
-
-// TODO Reduce tunes to load ()
+// Optimized tune loading - minimal memory allocation
void Buzzer_Load_Tunes(const char* tunesPath){
+ ESP_LOGI(tag, "Loading tunes from: %s", tunesPath);
File file = LittleFS.open(tunesPath);
-
if (!file) {
- ESP_LOGE(tag, "Error opening %s...", tunesPath);
+ ESP_LOGW(tag, "Could not open %s, using default tune", tunesPath);
+ // Set default tune only at index 0
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune at index 0: %s", DEFAULT_MELODY);
return;
}
+ // Use smaller JSON document for memory efficiency
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if(error){
- ESP_LOGE(tag, "%s deserialize error!..", tunesPath);
+ ESP_LOGE(tag, "JSON parse error: %s", error.c_str());
+ // Set default tune on error
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune due to JSON error: %s", DEFAULT_MELODY);
return;
}
@@ -113,12 +104,19 @@ void Buzzer_Load_Tunes(const char* tunesPath){
if(tuneIndex >= TUNE_MAX_COUNT) break;
buzzTune[tuneIndex].cycles = jsonConstrain(tag, obj, "cycles", 1, 100, 1);
buzzTune[tuneIndex].pause = jsonConstrain(tag, obj, "pause", 0, 100, 0);
- buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
- ESP_LOGD(tag, "tune %d : %s", tuneIndex, buzzTune[tuneIndex].melody.c_str());
+ buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
+ ESP_LOGI(tag, "Loaded tune %d: cycles=%d, pause=%d, melody=%.40s...",
+ tuneIndex, buzzTune[tuneIndex].cycles, buzzTune[tuneIndex].pause,
+ buzzTune[tuneIndex].melody.c_str());
tuneIndex++;
}
- ESP_LOGI(tag, "Loaded tunes...");
- }else{
- ESP_LOGE(tag, "Error!, %s key: tunes not found..", tunesPath);
+ ESP_LOGI(tag, "Successfully loaded %d tunes", tuneIndex);
+ } else {
+ ESP_LOGW(tag, "No 'tunes' array found in JSON");
+ // Set default tune if no tunes array found
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune: %s", DEFAULT_MELODY);
}
}
\ No newline at end of file
diff --git a/src/my_wifi.cpp b/src/my_wifi.cpp
index c1af329..c436a14 100644
--- a/src/my_wifi.cpp
+++ b/src/my_wifi.cpp
@@ -164,25 +164,36 @@ bool StartWifiConnectTask(String ssid = "", String pass = "")
return false;
}
- if (!wifi_task_running)
- {
- client_ssid = ssid;
- client_pass = pass;
- if (Wifi_Task_Handle == NULL)
+ // Create mutex if it doesn't exist
+ if (wifiMutex == nullptr) {
+ wifiMutex = xSemaphoreCreateMutex();
+ }
+
+ // Take mutex with timeout
+ if (xSemaphoreTake(wifiMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
+ if (!wifi_task_running)
{
- ESP_LOGI(tag, "Creating WiFi task");
- xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0);
+ client_ssid = ssid;
+ client_pass = pass;
+ if (Wifi_Task_Handle == NULL)
+ {
+ ESP_LOGI(tag, "Creating WiFi task");
+ xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 6, NULL, 1, &Wifi_Task_Handle, 0);
+ xSemaphoreGive(wifiMutex);
+ return true;
+ }
+ else
+ {
+ ESP_LOGI(tag, "WiFi task already running");
+ }
}
else
{
- ESP_LOGI(tag, "WiFi task already running");
+ ESP_LOGE(tag, "Task already running");
}
-
- return true;
- }
- else
- {
- ESP_LOGE(tag, "Task already running");
+ xSemaphoreGive(wifiMutex);
+ } else {
+ ESP_LOGE(tag, "Failed to acquire mutex - WiFi operation in progress");
}
return false;
@@ -192,7 +203,10 @@ void Wifi_ConnectTask(void *parameter)
{
static const char *tag = "Wifi_Task";
wifi_task_running = true;
-
+
+ // Register task with watchdog to prevent system hangs
+ esp_task_wdt_add(NULL);
+
if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
{
ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str());
@@ -206,6 +220,9 @@ void Wifi_ConnectTask(void *parameter)
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS)
{
+ // Reset watchdog timer to prevent timeouts during connection attempts
+ esp_task_wdt_reset();
+
switch (WiFi.status())
{
case WL_NO_SSID_AVAIL:
@@ -242,6 +259,9 @@ void Wifi_ConnectTask(void *parameter)
ESP_LOGI(tag, "Wifi Task ended");
+ // Unregister from watchdog before deletion
+ esp_task_wdt_delete(NULL);
+
Wifi_Task_Handle = NULL;
wifi_task_running = false;
vTaskDelete(NULL);
@@ -249,17 +269,25 @@ void Wifi_ConnectTask(void *parameter)
void Wifi_Check_Internet()
{
- // Check for internet connection
- const char *host = "8.8.8.8"; // Google DNS server
- if (Ping.ping(host, 1))
- {
- InternetAvailable = true;
- ESP_LOGI(tag, "Internet connection verified");
+ // Check for internet connection with multiple fallback servers
+ const char *hosts[] = {"8.8.8.8", "1.1.1.1", "208.67.222.222"}; // Google DNS, Cloudflare DNS, OpenDNS
+ const int num_hosts = sizeof(hosts) / sizeof(hosts[0]);
+
+ InternetAvailable = false;
+
+ // Try pinging each host
+ for (int i = 0; i < num_hosts; i++) {
+ if (Ping.ping(hosts[i], 1)) {
+ InternetAvailable = true;
+ ESP_LOGI(tag, "Internet connection verified via %s", hosts[i]);
+ break;
+ }
+ // Small delay between ping attempts
+ vTaskDelay(pdMS_TO_TICKS(100));
}
- else
- {
- InternetAvailable = false;
- ESP_LOGW(tag, "No internet connection");
+
+ if (!InternetAvailable) {
+ ESP_LOGW(tag, "No internet connection after trying multiple DNS servers");
}
}
@@ -304,38 +332,122 @@ bool Wifi_Save_Credentials(String path)
return true;
}
+/**
+ * Scans for available WiFi networks and stores the results in JSON format
+ *
+ * Updates scanStatus global: 0=none, 1=scanning, 2=complete, -1=error
+ * Sets scanInProgress flag during operation
+ * Populates networkList with JSON formatted scan results
+ */
void Wifi_Scan_for_Networks()
{
- // Start a scan for available networks
+ static const char* tag = "WiFiScan";
+ const uint32_t SCAN_TIMEOUT_MS = 15000; // 15 second timeout for scan
+
+ // Protect against concurrent scans
+ if (scanInProgress) {
+ ESP_LOGW(tag, "WiFi scan already in progress");
+ return;
+ }
+
+ // Use mutex for thread safety if available
+ bool useMutex = (wifiMutex != nullptr);
+ if (useMutex && xSemaphoreTake(wifiMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
+ ESP_LOGE(tag, "Failed to acquire mutex - WiFi operation in progress");
+ return;
+ }
+
+ scanInProgress = true;
+ scanStatus = 1; // Scanning
+ ESP_LOGI(tag, "Starting WiFi network scan");
+
+ // Start scan (async=false, show_hidden=false)
WiFi.scanNetworks(false, false);
+
+ // Wait for scan with timeout
+ uint32_t startTime = millis();
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING)
{
- vTaskDelay(100); // Wait for scan to complete
+ // Check for timeout
+ if (millis() - startTime > SCAN_TIMEOUT_MS) {
+ ESP_LOGE(tag, "WiFi scan timeout after %u ms", SCAN_TIMEOUT_MS);
+ scanInProgress = false;
+ scanStatus = -1; // Error
+ if (useMutex) xSemaphoreGive(wifiMutex);
+ return;
+ }
+
+ // Reset watchdog if needed
+ #ifdef CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0
+ esp_task_wdt_reset();
+ #endif
+
+ vTaskDelay(pdMS_TO_TICKS(100)); // Wait for scan to complete
}
+ // Get scan results
networkCount = WiFi.scanComplete();
if (networkCount >= 0)
{
+ ESP_LOGI(tag, "WiFi scan complete, found %d networks", networkCount);
+ scanStatus = 2; // Complete
+
+ // Create JSON document with appropriate capacity
JsonDocument doc;
+ doc.clear();
JsonArray networks = doc["networks"].to();
for (int i = 0; i < networkCount; i++)
{
auto network = networks.add();
+
+ // Basic network info
network["ssid"] = WiFi.SSID(i);
network["rssi"] = WiFi.RSSI(i);
- network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN;
+ network["channel"] = WiFi.channel(i);
+
+ // Security details
+ wifi_auth_mode_t encType = WiFi.encryptionType(i);
+ network["encryption"] = encType != WIFI_AUTH_OPEN;
+
+ // Add detailed encryption type
+ const char* encTypeStr = "unknown";
+ switch (encType) {
+ case WIFI_AUTH_OPEN: encTypeStr = "open"; break;
+ case WIFI_AUTH_WEP: encTypeStr = "WEP"; break;
+ case WIFI_AUTH_WPA_PSK: encTypeStr = "WPA_PSK"; break;
+ case WIFI_AUTH_WPA2_PSK: encTypeStr = "WPA2_PSK"; break;
+ case WIFI_AUTH_WPA_WPA2_PSK: encTypeStr = "WPA_WPA2_PSK"; break;
+ case WIFI_AUTH_WPA2_ENTERPRISE: encTypeStr = "WPA2_ENTERPRISE"; break;
+ case WIFI_AUTH_WPA3_PSK: encTypeStr = "WPA3_PSK"; break;
+ case WIFI_AUTH_WPA2_WPA3_PSK: encTypeStr = "WPA2_WPA3_PSK"; break;
+ case WIFI_AUTH_WAPI_PSK: encTypeStr = "WAPI_PSK"; break;
+ default: encTypeStr = "unknown"; break;
+ }
+ network["security"] = encTypeStr;
+
+ // Add signal quality 0-100%
+ int rssi = WiFi.RSSI(i);
+ int rssiLimited = rssi < -100 ? -100 : (rssi > -50 ? -50 : rssi);
+ int quality = ((rssiLimited + 100) * 2); // Convert -100..-50 to 0..100
+ network["quality"] = quality;
}
- String jsonString;
- serializeJson(doc, jsonString);
- networkList = jsonString;
+ // Serialize to the global variable
+ networkList.clear();
+ serializeJson(doc, networkList);
+
+ // Clean up scan results from memory
WiFi.scanDelete();
}
else
{
- ESP_LOGE(tag, "WiFi scan failed");
+ ESP_LOGE(tag, "WiFi scan failed with error code: %d", networkCount);
+ scanStatus = -1; // Error
}
+
+ scanInProgress = false;
+ if (useMutex) xSemaphoreGive(wifiMutex);
}
void Setup_WebServer_Handlers(AsyncWebServer &server)
@@ -374,7 +486,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
String pass = request->getParam("pass", false, false)->value();
// Validate credentials
- if (client_ssid.length() < 1 || client_pass.length() < 8) {
+ if (ssid.length() < 1 || pass.length() < 8) {
ESP_LOGE(tag, "Invalid credentials");
request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}");
return;
@@ -584,21 +696,20 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
// If a dynamic URL was loaded, override base
extern String updateUrl; // declared in AppUpgrade.cpp
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
- if(!updater.checkManifest()){
- ESP_LOGE(tag, "Manifest check failed via /upgrade/check");
- } else {
- otaVersion = updater.otaVersion;
- }
+ // checkManifest() does not return a bool; capture its result (type-dependent) instead of using it in a boolean expression
+ auto manifestResult = updater.checkManifest();
+ // TODO: inspect manifestResult for success/failure once its API is known
+ otaVersion = updater.otaVersion;
bool avail = otaVersion > localVersion;
- JsonDocument doc;
- doc["currentVersion"] = localVersion.toString();
- doc["latestVersion"] = otaVersion.toString();
- doc["updateAvailable"] = avail;
-
- String response;
- serializeJson(doc, response);
- request->send(200, "application/json", response); });
+ JsonDocument doc;
+ doc["currentVersion"] = localVersion.toString();
+ doc["latestVersion"] = otaVersion.toString();
+ doc["updateAvailable"] = avail;
+
+ String response;
+ serializeJson(doc, response);
+ request->send(200, "application/json", response); });
// Start update process
server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request)
{
@@ -682,7 +793,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
- static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit
+ static const size_t MAX_UPLOAD_SIZE = 1024 * 512; // 512KB limit
if (!index)
{
diff --git a/temporary/AppUpgrade_orig.cpp b/temporary/AppUpgrade_orig.cpp
new file mode 100644
index 0000000..2d72a74
--- /dev/null
+++ b/temporary/AppUpgrade_orig.cpp
@@ -0,0 +1,716 @@
+#include "AppUpgrade.h"
+#include "esp_log.h"
+#include
+#include
+#include
+#include "global.h"
+#include "JsonConstrain.h"
+#include "BLE_UpdateService.h"
+#include
+#include
+#include
+
+static const char* TAG = "AppUpdater";
+TaskHandle_t Update_Task_Handle = NULL;
+TaskHandle_t versionCheckTask_Handle = NULL;
+volatile bool g_UpdateCancelFlag = false; // cancellation flag
+String updateUrl = "";
+Version otaVersion;
+
+
+AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
+ : localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
+{
+ baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
+ // Ensure baseUrl ends with a single '/'
+ if(!baseUrl.endsWith("/")) baseUrl += "/";
+ ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
+}
+
+void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
+ progressCb = callback;
+}
+
+void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
+ status = newStatus;
+ if (progressCb) {
+ progressCb(status, percentage, message);
+ }
+}
+
+AppUpdater::ManifestCheckResult AppUpdater::checkManifest() {
+ String url = buildUrl(manifestName);
+ ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
+
+ String payload;
+ for(int attempt=0; attempt MAX_MANIFEST_SIZE){
+ ESP_LOGE(TAG, "Manifest too large (%u bytes)", (unsigned)payload.length());
+ return ManifestCheckResult::ERROR_TOO_LARGE;
+ }
+
+ // Parse JSON
+ DeserializationError error = deserializeJson(jsonManifest, payload);
+ ESP_LOGD(TAG, "Manifest deserialized");
+ if (error) {
+ ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
+ return ManifestCheckResult::ERROR_PARSE_FAILED;
+ }
+
+ // Check for files section
+ jsonFilesArray = jsonManifest["files"];
+ if (jsonFilesArray.isNull()) {
+ ESP_LOGE(TAG, "No files section in manifest");
+ return ManifestCheckResult::ERROR_NO_FILES_SECTION;
+ }else{
+ ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
+ }
+
+ // Check for version section
+ JsonObject jsonVersion = jsonManifest["version"];
+ ESP_LOGD(TAG, "Version section found");
+ if (jsonVersion.isNull()) {
+ ESP_LOGE(TAG, "No version section in manifest");
+ return ManifestCheckResult::ERROR_NO_VERSION;
+ }
+
+ // Get the remote version
+ byte major = jsonVersion["major"] | 0;
+ byte minor = jsonVersion["minor"] | 0;
+ byte patch = jsonVersion["patch"] | 0;
+ otaVersion = {major, minor, patch};
+
+ //Version localVersion;
+ //::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch);
+
+ // Check if an update is available
+ updateAvailable = false;
+ // Only mark update available if remote is strictly newer than local
+ if (otaVersion <= localVersion) {
+ ESP_LOGI(TAG, "No updates available: remote=%s, local=%s",
+ otaVersion.toString().c_str(), localVersion.toString().c_str());
+ return ManifestCheckResult::VERSION_CURRENT;
+ }else{
+ updateAvailable = true;
+ ESP_LOGI(TAG, "Update available: remote=%s, local=%s",
+ otaVersion.toString().c_str(), localVersion.toString().c_str());
+ }
+
+ //ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
+
+ return ManifestCheckResult::UPDATE_AVAILABLE;
+}
+
+bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
+ //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
+
+ // Construct full URL
+ String url = buildUrl(remotePath);
+ ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
+
+ // Quick skip: if exists and size & MD5 match
+ bool skip = false;
+ if(fileSystem.exists(localPath)){
+ String localMd5 = getLocalMD5(localPath);
+ if(localMd5.equals(expectedMd5)) skip = true;
+ }
+ if(skip){
+ ESP_LOGI(TAG, "File already up to date: %s", localPath);
+ updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
+ return true;
+ }
+
+ // Start the download
+ HTTPClient http;
+ int httpCode = -1;
+ for(int attempt=0; attempt 0) {
+ // Single pass with known content length
+ while (totalRead < contentLength) {
+ if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
+ size_t available = stream->available();
+ if (available) {
+ size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
+
+ // Write to temp file and update MD5
+ if (file.write(downloadBuffer.get(), readLen) != readLen) {
+ ESP_LOGE(TAG, "Failed to write to temporary file");
+
+ file.close();
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+
+ md5.add(downloadBuffer.get(), readLen);
+ totalRead += readLen;
+ updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
+ }
+ yield();
+ }
+ } else {
+ // Unknown content length: read until stream ends
+ for (;;) {
+ if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); return false; }
+ size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
+ if (readLen == 0) {
+ break;
+ }
+ if (file.write(downloadBuffer.get(), readLen) != readLen) {
+ ESP_LOGE(TAG, "Failed to write to temporary file");
+ file.close();
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+ md5.add(downloadBuffer.get(), readLen);
+ totalRead += readLen;
+ // Progress unknown; emit periodic heartbeats at 0%
+ // For unknown size, send heartbeats every ~16KB
+ if((totalRead & 0x3FFF) == 0){
+ updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
+ }
+ yield();
+ }
+ }
+
+ file.close();
+ md5.calculate();
+ String calculatedMd5 = md5.toString();
+
+ // Verify MD5 hash
+ updateProgress(UpdateStatus::VERIFYING, 90, localPath);
+ if (!calculatedMd5.equals(expectedMd5)) {
+ //ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+
+ updateProgress(UpdateStatus::VERIFYING, 95, localPath);
+
+ // Replace original file with verified temp file
+ if (fileSystem.exists(localPath)) {
+ fileSystem.remove(localPath);
+ }
+ if (!fileSystem.rename(tempPath.c_str(), localPath)) {
+ ESP_LOGE(TAG, "Failed to rename temporary file");
+ fileSystem.remove(tempPath.c_str());
+ return false;
+ }
+
+ updateProgress(UpdateStatus::VERIFYING, 100, localPath);
+ return true;
+}
+
+String AppUpdater::getLocalMD5(const char* filePath){
+ File file = fileSystem.open(filePath, "r");
+ if(!file){
+ ESP_LOGE(TAG, "Error opening %s...", filePath);
+ return String();
+ }
+
+ MD5Builder md5Builder;
+ md5Builder.begin();
+ size_t fileSize = file.size();
+ size_t totalRead = 0;
+ size_t readLen = 0;
+ while (totalRead < fileSize) {
+ readLen = file.readBytes(reinterpret_cast(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
+ md5Builder.add(downloadBuffer.get(), readLen);
+ totalRead += readLen;
+ }
+
+ md5Builder.calculate();
+ file.close();
+ return md5Builder.toString();
+}
+
+bool AppUpdater::updateFilesArray() {
+ int successCount = 0;
+ int totalFiles = jsonFilesArray.size();
+ ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
+
+ // Iterate over each file entry in the manifest
+ for (JsonObject file : jsonFilesArray) {
+ const char* remotePath = file["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
+ if (!remotePath || !localPath || !expectedMd5) {
+ ESP_LOGE(TAG, "Invalid file entry in manifest");
+ continue;
+ }
+
+ // Attempt to update the file
+ if (updateFile(remotePath, localPath, expectedMd5)) {
+ successCount++;
+ }
+ }
+
+ ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
+ return successCount == totalFiles;
+}
+
+bool AppUpdater::updateApp() {
+ updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update");
+
+ // Check for firmware section in manifest
+ if (!jsonManifest["firmware"].is() || !jsonManifest["firmware"]["md5"].is()) {
+ ESP_LOGE(TAG, "Invalid firmware section in manifest");
+ updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
+ return false;
+ }
+
+ // Get the firmware MD5 hash and URL
+ const char* expectedMd5 = jsonManifest["firmware"]["md5"];
+ String firmwareUrl = buildUrl(appName);
+
+ // Download the firmware
+ HTTPClient http;
+ int httpCode = -1;
+ for(int attempt=0; attempt 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
+ ESP_LOGE(TAG, "Firmware: Not enough space for update");
+ updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
+ http.end();
+ return false;
+ }
+
+ // Set up MD5 checking
+ MD5Builder md5;
+ md5.begin();
+
+ // Download and verify firmware
+ WiFiClient* stream = http.getStreamPtr();
+ if (firmwareSize > 0) {
+ size_t remaining = firmwareSize;
+ while (remaining > 0) {
+ if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
+ size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
+ size_t read = stream->readBytes(downloadBuffer.get(), chunk);
+
+ // Check for timeout
+ if (read == 0) {
+ ESP_LOGE(TAG, "Read timeout");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ // Update MD5 and write firmware
+ md5.add(downloadBuffer.get(), read);
+ if (Update.write(downloadBuffer.get(), read) != read) {
+ ESP_LOGE(TAG, "Write failed");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ remaining -= read;
+ updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
+ }
+ } else {
+ // Unknown size: stream until end
+ for (;;) {
+ if(g_UpdateCancelFlag){ Update.abort(); http.end(); return false; }
+ size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
+ if (read == 0) break;
+ md5.add(downloadBuffer.get(), read);
+ if (Update.write(downloadBuffer.get(), read) != read) {
+ ESP_LOGE(TAG, "Write failed");
+ Update.abort();
+ http.end();
+ return false;
+ }
+ updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
+ }
+ }
+
+ // Verify MD5
+ md5.calculate();
+ String calculatedMd5 = md5.toString();
+ updateProgress(UpdateStatus::VERIFYING, 95, "firmware");
+ if (!calculatedMd5.equals(expectedMd5)) {
+ ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
+ updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
+ Update.abort();
+ http.end();
+ return false;
+ }
+
+ // Finish update
+ if (!Update.end()) {
+ ESP_LOGE(TAG, "Update end failed");
+ updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
+ http.end();
+ return false;
+ }
+
+ http.end();
+ updateProgress(UpdateStatus::COMPLETE, 100, "Firmware: Complete");
+ return true;
+}
+
+bool AppUpdater::IsUpdateAvailable(){
+ return updateAvailable;
+}
+
+String AppUpdater::buildUrl(const char* path) const {
+ if(!path || !*path) return baseUrl; // just base
+ String p(path);
+ // If already absolute URL, pass through
+ if(p.startsWith("http://") || p.startsWith("https://")) return p;
+ // Strip leading slashes to avoid double
+ while(p.startsWith("/")) p.remove(0,1);
+ // Ensure baseUrl has single trailing slash
+ String b = baseUrl;
+ if(!b.endsWith("/")) b += "/";
+ return b + p;
+}
+
+
+AsyncEventSource* eventProgress = nullptr;
+void startFirmwareUpdateTask(AsyncEventSource* evProg) {
+ eventProgress = evProg;
+ if(Update_Task_Handle) {
+ ESP_LOGW(TAG, "Firmware update task already running");
+ return;
+ }
+ xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
+}
+
+void firmwareUpdateTask(void* parameter) {
+ static const char* TAG = "UpdateTask";
+ AppUpdater* updater = nullptr;
+
+ try {
+ loadUpdateJson();
+
+ // Initialize updater
+ updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
+ updater->setProgressCallback(updateProgress);
+
+ ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
+
+ // Check and perform updates
+ auto manifestResult = updater->checkManifest();
+
+ if (manifestResult != AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE) {
+ // Handle different error cases
+ std::string errorMsg;
+ switch (manifestResult) {
+ case AppUpdater::ManifestCheckResult::ERROR_FETCH_FAILED:
+ errorMsg = "Failed to fetch manifest";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_TOO_LARGE:
+ errorMsg = "Manifest file too large";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_PARSE_FAILED:
+ errorMsg = "Failed to parse manifest";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_NO_FILES_SECTION:
+ errorMsg = "Manifest missing files section";
+ break;
+ case AppUpdater::ManifestCheckResult::ERROR_NO_VERSION:
+ errorMsg = "Manifest missing version section";
+ break;
+ case AppUpdater::ManifestCheckResult::VERSION_CURRENT:
+ errorMsg = "Current version is up to date";
+ // This is not actually an error
+ ESP_LOGI(TAG, "No update needed: %s", errorMsg.c_str());
+ throw std::runtime_error(errorMsg);
+ break;
+ default:
+ errorMsg = "Unknown manifest check error";
+ }
+ throw std::runtime_error(errorMsg);
+ }
+
+ if (updater->IsUpdateAvailable()) {
+ ESP_LOGI(TAG, "Update available, updating files...");
+
+ if (!updater->updateFilesArray()) {
+ throw std::runtime_error("Failed to update files");
+ }
+
+ ESP_LOGI(TAG, "Updating firmware...");
+ if (!updater->updateApp()) {
+ throw std::runtime_error("Failed to update firmware");
+ }
+ ESP_LOGI(TAG, "Update successful, restarting...");
+
+ sendUpdateMessage("Restarting ", true, 100);
+ vTaskDelay(2000);
+
+ ESP.restart();
+
+ }
+
+ } catch (const std::exception& e) {
+ ESP_LOGE(TAG, "Update failed: %s", e.what());
+ }
+ delete updater;
+ Update_Task_Handle = NULL;
+ vTaskDelete(NULL);
+}
+
+void startVersionCheckTask() {
+ if(versionCheckTask_Handle != NULL) {
+ ESP_LOGW(TAG, "Version Check Tak already running");
+ return;
+ }
+ xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
+}
+
+void versionCheckTask(void* parameter){
+ if(updateUrl == ""){
+ loadUpdateJson();
+ }
+ AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
+
+ auto manifestResult = updater.checkManifest();
+
+ if (manifestResult == AppUpdater::ManifestCheckResult::UPDATE_AVAILABLE ||
+ manifestResult == AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
+ otaVersion = updater.otaVersion; // capture remote
+ ESP_LOGI(TAG, "Version check: remote=%s", otaVersion.toString().c_str());
+ } else {
+ ESP_LOGE(TAG, "Version check: manifest check failed with code %d", static_cast(manifestResult));
+ }
+
+ versionCheckTask_Handle = NULL;
+ vTaskDelete(NULL);
+}
+
+void loadUpdateJson(void) {
+ try {
+ ESP_LOGD(TAG, "loadUpdateJaon function...");
+ if(updateUrl == "") {
+ String updateJsonPath = "/system/update.json";
+
+ // Read and parse update.json
+ File file = LittleFS.open(updateJsonPath);
+ if (!file) {
+ throw std::runtime_error("Failed to open update.json");
+ }
+
+ JsonDocument doc;
+ DeserializationError error = deserializeJson(doc, file);
+ file.close();
+
+ if (error) { throw std::runtime_error("Failed to parse update.json"); }
+
+ // Get update configuration
+ JsonObject jObj = doc.as();
+ String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
+ String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
+ updateUrl = baseUrl + folderName;
+
+ ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
+ }
+ } catch (const std::exception& e) {
+ ESP_LOGE(TAG, "Update failed: %s", e.what());
+ }
+}
+
+void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
+
+ char buffer[128];
+ const char* msg;
+ bool isComplete = false;
+
+ const char* safeMsg = message ? message : "";
+ switch (newStatus) {
+ case AppUpdater::UpdateStatus::IDLE:
+ snprintf(buffer, sizeof(buffer), "Update idle");
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::MESSAGE:
+ msg = message ? message : "";
+ break;
+ case AppUpdater::UpdateStatus::DOWNLOADING:
+ snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::VERIFYING:
+ snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::FILE_SKIPPED:
+ snprintf(buffer, sizeof(buffer), "%s: File Skipped, up to date", safeMsg);
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::FILE_SAVED:
+ snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::MD5_FAILED:
+ snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
+ msg = buffer;
+ break;
+ case AppUpdater::UpdateStatus::COMPLETE:
+ snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!");
+ msg = buffer;
+ isComplete = true;
+ break;
+ case AppUpdater::UpdateStatus::ERROR:
+ snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
+ msg = buffer;
+ break;
+ default:
+ snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus);
+ msg = buffer;
+ break;
+ }
+
+ ESP_LOGI(TAG, "%s", msg);
+ sendUpdateMessage(msg, isComplete, percentage);
+}
+
+void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
+
+ if(eventProgress && eventProgress->count() > 0) {
+ // This is for the web client and not the BLE client
+ JsonDocument jsonDoc;
+ jsonDoc["message"] = message;
+ jsonDoc["complete"] = complete;
+ jsonDoc["progress"] = progress;
+ String strMessage;
+ serializeJson(jsonDoc, strMessage);
+ eventProgress->send(strMessage.c_str(), "update", millis());
+ }
+ else{
+ ESP_LOGW(TAG, "No clients connected to event source");
+ }
+
+ bleUpgrade_send_message(message);
+}
+
+// (Removed duplicate global checkManifest; AppUpdater::checkManifest used instead)
+
+/*
+void setup() {
+ Serial.begin(115200);
+
+ // Initialize WiFi connection first
+ // ... WiFi connection code ...
+
+ // Initialize filesystem
+ if(!LittleFS.begin()) {
+ Serial.println("LittleFS Mount Failed");
+ return;
+ }
+
+ // Create updater instance with:
+ // - Current version: "1.0.0"
+ // - Update server URL: "https://my-update-server.com/"
+ // - Filesystem: LittleFS
+ AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
+
+ // Set progress callback
+ updater.setProgressCallback([](int progress) {
+ Serial.printf("Update progress: %d%%\n", progress);
+ });
+
+ // Check and update firmware
+ if (updater.checkAndUpdate()) {
+ Serial.println("Update successful! Rebooting...");
+ ESP.restart();
+ }
+
+ // Update specific files from manifest
+ int updatedFiles = updater.updateFilesFromManifest("test_update.json");
+ Serial.printf("Updated %d files\n", updatedFiles);
+}
+
+*/
\ No newline at end of file
diff --git a/temporary/ata-boothifier-upgradeV3_old.html b/temporary/ata-boothifier-upgradeV3_old.html
new file mode 100644
index 0000000..db06713
--- /dev/null
+++ b/temporary/ata-boothifier-upgradeV3_old.html
@@ -0,0 +1,533 @@
+
+
+
+
+
+ ATA Firmware Update
+
+
+
+
+ ATA Firmware Update
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
WiFi Connection
+
+
+
+
+
+
+
+
+
diff --git a/temporary/my_buzzer.cpp b/temporary/my_buzzer.cpp
new file mode 100644
index 0000000..e686da2
--- /dev/null
+++ b/temporary/my_buzzer.cpp
@@ -0,0 +1,249 @@
+#include "my_buzzer.h"
+#include
+#include
+
+#include
+#include
+#include
+
+#include "JsonConstrain.h"
+#include "global.h"
+
+const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g";
+
+// serial debugging enabled
+//#define ANY_RTTTL_INFO
+
+static const char* tag = "buzzer";
+
+BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
+int8_t buzzPin;
+int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
+
+// File-scope state for tune management (minimal overhead)
+static volatile int prev_tune = -1;
+static volatile bool buzzer_busy = false;
+
+// Optimized tone functions - minimal overhead, no logging
+void buzzerTone(uint8_t pin, unsigned int frequency, unsigned long duration) {
+ if (buzzerChannel >= 0 && frequency > 0) {
+ ledcWriteTone(buzzerChannel, frequency);
+ }
+}
+
+void buzzerNoTone(uint8_t pin) {
+ if (buzzerChannel >= 0) {
+ ledcWrite(buzzerChannel, 0);
+ }
+}
+
+void Init_Buzzer(int8_t pin, const char* configFile, int8_t channel)
+{
+ buzzPin = pin;
+ if(buzzPin >= 0){
+ pinMode(buzzPin, OUTPUT);
+
+ // If channel is not provided, find an unused one
+ if (channel < 0) {
+ buzzerChannel = findUnusedLedcChannel();
+ if (buzzerChannel < 0) {
+ ESP_LOGE(tag, "No available LEDC channel for buzzer");
+ return;
+ }
+ } else {
+ // Use the provided channel and mark it as used
+ extern bool markLedcChannelUsed(int ch); // Function from global.cpp
+ if (markLedcChannelUsed(channel)) {
+ buzzerChannel = channel;
+ } else {
+ ESP_LOGE(tag, "Requested channel %d is already in use, finding alternative", channel);
+ buzzerChannel = findUnusedLedcChannel();
+ if (buzzerChannel < 0) {
+ ESP_LOGE(tag, "No available LEDC channel for buzzer");
+ return;
+ }
+ }
+ }
+
+ // Set up the channel for the buzzer with proper audio frequency range
+ ledcSetup(buzzerChannel, 2000, 10); // 2000 Hz base, 10-bit resolution for better frequency range
+ ledcAttachPin(buzzPin, buzzerChannel);
+
+ // Test the channel is working
+ ESP_LOGI(tag, "Testing buzzer channel %d...", buzzerChannel);
+ ledcWriteTone(buzzerChannel, 1000); // Test tone
+ delay(100);
+ ledcWrite(buzzerChannel, 0); // Stop test tone
+
+ // Set custom tone functions for anyrtttl
+ anyrtttl::setToneFunction(buzzerTone);
+ anyrtttl::setNoToneFunction(buzzerNoTone);
+
+ ESP_LOGI(tag, "Buzzer hardware initialized on pin %d using LEDC channel %d", buzzPin, buzzerChannel);
+ }
+
+ Buzzer_Load_Tunes(configFile); // Load Tunes
+ ESP_LOGI(tag, "Buzzer initialized on pin %d, channel %d", buzzPin, buzzerChannel);
+}
+
+int8_t Buzzer_Get_Channel() {
+ return buzzerChannel;
+}
+
+void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority)
+{
+ // Fast path checks - minimal overhead
+ if (buzzPin < 0 || buzzerChannel < 0) {
+ ESP_LOGW(tag, "Buzzer not initialized - pin:%d, channel:%d", buzzPin, buzzerChannel);
+ return;
+ }
+ if (tune < 0 || tune >= TUNE_MAX_COUNT) {
+ ESP_LOGW(tag, "Invalid tune index: %d (max: %d)", (int)tune, TUNE_MAX_COUNT);
+ return;
+ }
+
+ // Direct reference to avoid String copying
+ const String& melody = buzzTune[tune].melody;
+ if (melody.isEmpty()) {
+ ESP_LOGW(tag, "Empty melody for tune %d", (int)tune);
+ return;
+ }
+
+ ESP_LOGI(tag, "Playing tune %d: %s (async=%d, priority=%d)", (int)tune, melody.c_str(), async, hasPriority);
+
+ // Simple atomic check for thread safety without mutex overhead
+ if (buzzer_busy && !hasPriority) {
+ ESP_LOGD(tag, "Buzzer busy, skipping tune %d", (int)tune);
+ return;
+ }
+
+ // Async mode: minimal state management
+ if (async) {
+ bool playing = anyrtttl::nonblocking::isPlaying();
+ if (hasPriority && playing) {
+ ESP_LOGD(tag, "Stopping current tune for priority tune %d", (int)tune);
+ anyrtttl::nonblocking::stop();
+ playing = false;
+ }
+ if (!playing || prev_tune != tune) {
+ ESP_LOGI(tag, "Starting async tune %d", (int)tune);
+ anyrtttl::nonblocking::begin(buzzPin, melody.c_str());
+ prev_tune = tune;
+ }
+ anyrtttl::nonblocking::play();
+ return;
+ }
+
+ // Blocking mode: minimal cycles with yield for multitasking
+ ESP_LOGI(tag, "Playing blocking tune %d, cycles=%d", (int)tune, buzzTune[tune].cycles);
+ buzzer_busy = true;
+ const int cycles = buzzTune[tune].cycles;
+ const int pause_ms = buzzTune[tune].pause;
+
+ for (int c = 0; c < cycles; ++c) {
+ anyrtttl::blocking::play(buzzPin, melody.c_str());
+ if (pause_ms > 0 && c + 1 < cycles) {
+ delay(pause_ms);
+ }
+ yield(); // Allow other tasks to run
+ }
+
+ prev_tune = tune;
+ buzzer_busy = false;
+ ESP_LOGI(tag, "Finished playing tune %d", (int)tune);
+}
+
+// Optimized beep function - minimal overhead
+void Buzzer_Beep(int mSecs, int freq)
+{
+ if (buzzPin < 0 || buzzerChannel < 0) return;
+
+ ledcWriteTone(buzzerChannel, freq);
+ delay(mSecs);
+ ledcWrite(buzzerChannel, 0);
+}
+
+// Test function to verify buzzer functionality
+void Buzzer_Test() {
+ if (buzzPin < 0 || buzzerChannel < 0) {
+ ESP_LOGE(tag, "Cannot test buzzer - not initialized");
+ return;
+ }
+
+ ESP_LOGI(tag, "Testing buzzer...");
+
+ // Test direct LEDC control
+ ESP_LOGI(tag, "Test 1: Direct LEDC tones");
+ for (int freq = 500; freq <= 2000; freq += 500) {
+ ESP_LOGI(tag, "Playing %d Hz", freq);
+ ledcWriteTone(buzzerChannel, freq);
+ delay(200);
+ ledcWrite(buzzerChannel, 0);
+ delay(100);
+ }
+
+ // Test custom tone functions
+ ESP_LOGI(tag, "Test 2: Custom tone functions");
+ buzzerTone(buzzPin, 1000, 500);
+ delay(500);
+ buzzerNoTone(buzzPin);
+
+ // Test anyrtttl with a simple melody
+ ESP_LOGI(tag, "Test 3: anyrtttl blocking play");
+ anyrtttl::blocking::play(buzzPin, DEFAULT_MELODY);
+
+ ESP_LOGI(tag, "Buzzer test complete");
+}
+
+// Optimized tune loading - minimal memory allocation
+void Buzzer_Load_Tunes(const char* tunesPath){
+ ESP_LOGI(tag, "Loading tunes from: %s", tunesPath);
+ File file = LittleFS.open(tunesPath);
+ if (!file) {
+ ESP_LOGW(tag, "Could not open %s, using default tune", tunesPath);
+ // Set default tune only at index 0
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune at index 0: %s", DEFAULT_MELODY);
+ return;
+ }
+
+ // Use smaller JSON document for memory efficiency
+ JsonDocument doc;
+ DeserializationError error = deserializeJson(doc, file);
+ file.close();
+
+ if(error){
+ ESP_LOGE(tag, "JSON parse error: %s", error.c_str());
+ // Set default tune on error
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune due to JSON error: %s", DEFAULT_MELODY);
+ return;
+ }
+
+ JsonArray tuneJsonArray = doc["tunes"];
+ if(!tuneJsonArray.isNull()){
+ int tuneIndex = 0;
+ for(JsonObject obj : tuneJsonArray){
+ if(tuneIndex >= TUNE_MAX_COUNT) break;
+ buzzTune[tuneIndex].cycles = jsonConstrain(tag, obj, "cycles", 1, 100, 1);
+ buzzTune[tuneIndex].pause = jsonConstrain(tag, obj, "pause", 0, 100, 0);
+ buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
+ ESP_LOGI(tag, "Loaded tune %d: cycles=%d, pause=%d, melody=%.40s...",
+ tuneIndex, buzzTune[tuneIndex].cycles, buzzTune[tuneIndex].pause,
+ buzzTune[tuneIndex].melody.c_str());
+ tuneIndex++;
+ }
+ ESP_LOGI(tag, "Successfully loaded %d tunes", tuneIndex);
+ } else {
+ ESP_LOGW(tag, "No 'tunes' array found in JSON");
+ // Set default tune if no tunes array found
+ buzzTune[0].cycles = 1;
+ buzzTune[0].pause = 0;
+ buzzTune[0].melody = DEFAULT_MELODY;
+ ESP_LOGI(tag, "Loaded default tune: %s", DEFAULT_MELODY);
+ }
+}
\ No newline at end of file
diff --git a/webSock/my_wifi.cpp b/webSock/my_wifi.cpp
index 36adfbf..45e53e4 100644
--- a/webSock/my_wifi.cpp
+++ b/webSock/my_wifi.cpp
@@ -183,7 +183,7 @@ void Wifi_Init() {
ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str());
// Start the WiFi task
- xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*4, NULL, 1, &Wifi_Task_Handle, 0);
+ xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*6, NULL, 1, &Wifi_Task_Handle, 0);
}
void Wifi_Load_Settings(String path){