diff --git a/data/booths/custom.json b/data/booths/custom.json index e01c81a..9f7eb89 100644 --- a/data/booths/custom.json +++ b/data/booths/custom.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/helio-flare.json b/data/booths/helio-flare.json index f40c0e9..50419d5 100644 --- a/data/booths/helio-flare.json +++ b/data/booths/helio-flare.json @@ -56,23 +56,13 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { - "en": true, + "en": false, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true - } + "button-index": 1 + } ], "oled": { "en": false, diff --git a/data/booths/helio-posh.json b/data/booths/helio-posh.json index 396c345..d43fabd 100644 --- a/data/booths/helio-posh.json +++ b/data/booths/helio-posh.json @@ -55,23 +55,13 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { - "en": true, + "en": false, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true - } + "button-index": 1 + } ], "oled": { "en": false, diff --git a/data/booths/helio-sport.json b/data/booths/helio-sport.json index 46efdbe..d04844e 100644 --- a/data/booths/helio-sport.json +++ b/data/booths/helio-sport.json @@ -56,23 +56,13 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { - "en": true, + "en": false, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true - } + "button-index": 1 + } ], "oled": { "en": false, diff --git a/data/booths/light-stik.json b/data/booths/light-stik.json index 824685e..21c6e2e 100644 --- a/data/booths/light-stik.json +++ b/data/booths/light-stik.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/lumia-m.json b/data/booths/lumia-m.json index 2f66e2c..d576c73 100644 --- a/data/booths/lumia-m.json +++ b/data/booths/lumia-m.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/lumia-spectra.json b/data/booths/lumia-spectra.json index 7a39950..b4fae80 100644 --- a/data/booths/lumia-spectra.json +++ b/data/booths/lumia-spectra.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/lumia-xl.json b/data/booths/lumia-xl.json index 3a814b3..76d799f 100644 --- a/data/booths/lumia-xl.json +++ b/data/booths/lumia-xl.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/m1.json b/data/booths/m1.json index 7a39950..b4fae80 100644 --- a/data/booths/m1.json +++ b/data/booths/m1.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/marquee.json b/data/booths/marquee.json index 7a39950..b4fae80 100644 --- a/data/booths/marquee.json +++ b/data/booths/marquee.json @@ -56,22 +56,12 @@ { "en": true, "relay-index": 0, - "button-index": 0, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 0 }, { "en": true, "relay-index": 1, - "button-index": 1, - "min": 5.0, - "max": 100.0, - "step": 1.5, - "skip-count": 5, - "vision": true + "button-index": 1 } ], "oled": { diff --git a/data/booths/roamer-big.json b/data/booths/roamer-big.json index 9efe8c4..b555e9e 100644 --- a/data/booths/roamer-big.json +++ b/data/booths/roamer-big.json @@ -58,17 +58,17 @@ "en": true, "relay-index": 0, "button-index": 0, - "min": 5.0, + "min": 0.0, "max": 100.0, "step": 1.5, "skip-count": 5, "vision": true }, { - "en": true, + "en": false, "relay-index": 1, "button-index": 1, - "min": 5.0, + "min": 0.0, "max": 100.0, "step": 1.5, "skip-count": 5, @@ -106,6 +106,7 @@ "rgb-order": "rgb", "shift":-5, "offset": 0, + "bright": 200, "power-div": 0, "i2s-ch": 0, "core": 1 @@ -117,6 +118,7 @@ "rgb-order": "rgb", "shift":-27, "offset": 0, + "bright": 255, "power-div": 0, "i2s-ch": 0, "core": 1 diff --git a/data/booths/roamer.json b/data/booths/roamer.json index 146b248..64faa59 100644 --- a/data/booths/roamer.json +++ b/data/booths/roamer.json @@ -58,17 +58,17 @@ "en": true, "relay-index": 0, "button-index": 0, - "min": 5.0, + "min": 0.0, "max": 100.0, "step": 1.5, "skip-count": 5, "vision": true }, { - "en": true, + "en": false, "relay-index": 1, "button-index": 1, - "min": 5.0, + "min": 0.0, "max": 100.0, "step": 1.5, "skip-count": 5, diff --git a/data/www/ata-boothifier-upgradeV3.html b/data/www/ata-boothifier-upgradeV3.html index bce43fd..e8c4179 100644 --- a/data/www/ata-boothifier-upgradeV3.html +++ b/data/www/ata-boothifier-upgradeV3.html @@ -1,359 +1,545 @@ - - - ATA Firmware Update - + button:disabled { + background-color: var(--color-disabled); + color: var(--color-disabled-text); + cursor: not-allowed; + } + + /* --- Log Area --- */ + textarea#logArea { + width: 100%; + height: 250px; + font-family: "SF Mono", "Consolas", "Menlo", monospace; + font-size: 13px; + padding: 15px; + border-radius: var(--border-radius); + border: 1px solid var(--color-border); + background-color: var(--color-bg); + resize: vertical; + line-height: 1.5; + } + + /* --- Responsive --- */ + @media (max-width: 540px) { + body { + padding: 10px; + } + main { + padding: 16px 20px; + } + h1 { + font-size: 22px; + } + } + -

ATA Firmware Update

+
+

ATA Firmware Update

- -
- - -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
- -
- -
- -
- -
- - -
- - -
- - - - -
- - -
-
- -
-

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){