commit 9-7-25
This commit is contained in:
parent
12b5b25081
commit
084de5cd44
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,23 +56,13 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": false,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
}
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
"en": false,
|
"en": false,
|
||||||
|
|||||||
@ -55,23 +55,13 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": false,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
}
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
"en": false,
|
"en": false,
|
||||||
|
|||||||
@ -56,23 +56,13 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": false,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
}
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
"en": false,
|
"en": false,
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -56,22 +56,12 @@
|
|||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1
|
||||||
"min": 5.0,
|
|
||||||
"max": 100.0,
|
|
||||||
"step": 1.5,
|
|
||||||
"skip-count": 5,
|
|
||||||
"vision": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"oled": {
|
"oled": {
|
||||||
|
|||||||
@ -58,17 +58,17 @@
|
|||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0,
|
||||||
"min": 5.0,
|
"min": 0.0,
|
||||||
"max": 100.0,
|
"max": 100.0,
|
||||||
"step": 1.5,
|
"step": 1.5,
|
||||||
"skip-count": 5,
|
"skip-count": 5,
|
||||||
"vision": true
|
"vision": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": false,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1,
|
||||||
"min": 5.0,
|
"min": 0.0,
|
||||||
"max": 100.0,
|
"max": 100.0,
|
||||||
"step": 1.5,
|
"step": 1.5,
|
||||||
"skip-count": 5,
|
"skip-count": 5,
|
||||||
@ -106,6 +106,7 @@
|
|||||||
"rgb-order": "rgb",
|
"rgb-order": "rgb",
|
||||||
"shift":-5,
|
"shift":-5,
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
|
"bright": 200,
|
||||||
"power-div": 0,
|
"power-div": 0,
|
||||||
"i2s-ch": 0,
|
"i2s-ch": 0,
|
||||||
"core": 1
|
"core": 1
|
||||||
@ -117,6 +118,7 @@
|
|||||||
"rgb-order": "rgb",
|
"rgb-order": "rgb",
|
||||||
"shift":-27,
|
"shift":-27,
|
||||||
"offset": 0,
|
"offset": 0,
|
||||||
|
"bright": 255,
|
||||||
"power-div": 0,
|
"power-div": 0,
|
||||||
"i2s-ch": 0,
|
"i2s-ch": 0,
|
||||||
"core": 1
|
"core": 1
|
||||||
|
|||||||
@ -58,17 +58,17 @@
|
|||||||
"en": true,
|
"en": true,
|
||||||
"relay-index": 0,
|
"relay-index": 0,
|
||||||
"button-index": 0,
|
"button-index": 0,
|
||||||
"min": 5.0,
|
"min": 0.0,
|
||||||
"max": 100.0,
|
"max": 100.0,
|
||||||
"step": 1.5,
|
"step": 1.5,
|
||||||
"skip-count": 5,
|
"skip-count": 5,
|
||||||
"vision": true
|
"vision": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"en": true,
|
"en": false,
|
||||||
"relay-index": 1,
|
"relay-index": 1,
|
||||||
"button-index": 1,
|
"button-index": 1,
|
||||||
"min": 5.0,
|
"min": 0.0,
|
||||||
"max": 100.0,
|
"max": 100.0,
|
||||||
"step": 1.5,
|
"step": 1.5,
|
||||||
"skip-count": 5,
|
"skip-count": 5,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -34,8 +34,8 @@ UPDATE_MANIFEST = True
|
|||||||
UPLOAD_DATA = True
|
UPLOAD_DATA = True
|
||||||
|
|
||||||
DIR_SKIP_LIST = [
|
DIR_SKIP_LIST = [
|
||||||
"data/system/**/*",
|
"system", # Just directory names, not paths
|
||||||
"data/booths/**/*"
|
"booths"
|
||||||
]
|
]
|
||||||
|
|
||||||
FILES_SKIP_LIST = [
|
FILES_SKIP_LIST = [
|
||||||
|
|||||||
@ -10,17 +10,20 @@
|
|||||||
|
|
||||||
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
|
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
|
||||||
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.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
|
// Maximum allowed manifest size (bytes) to protect memory
|
||||||
#define MAX_MANIFEST_SIZE (64 * 1024)
|
#define MAX_MANIFEST_SIZE (64 * 1024)
|
||||||
|
|
||||||
// Number of HTTP retry attempts for transient failures
|
// Number of HTTP retry attempts for transient failures
|
||||||
#define HTTP_RETRY_COUNT 3
|
#define HTTP_RETRY_COUNT 5 // Increased from 3
|
||||||
#define HTTP_RETRY_DELAY_MS 500
|
#define HTTP_RETRY_DELAY_MS 1000 // Increased from 500
|
||||||
|
|
||||||
// Allow external cancellation
|
// Allow external cancellation
|
||||||
extern volatile bool g_UpdateCancelFlag;
|
extern volatile bool g_UpdateCancelFlag;
|
||||||
|
|
||||||
|
// Global update mode setting
|
||||||
|
extern UpdateMode g_UpdateMode;
|
||||||
|
|
||||||
extern TaskHandle_t Update_Task_Handle;
|
extern TaskHandle_t Update_Task_Handle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +35,15 @@ extern TaskHandle_t Update_Task_Handle;
|
|||||||
|
|
||||||
extern Version otaVersion;
|
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
|
* @brief File information structure
|
||||||
*/
|
*/
|
||||||
@ -90,6 +102,16 @@ class AppUpdater {
|
|||||||
*/
|
*/
|
||||||
const String& getBaseUrl() const { return baseUrl; }
|
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
|
* @brief Set progress callback function
|
||||||
* @param callback Function to call with progress updates
|
* @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);
|
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
|
* @brief Get manifest content
|
||||||
* @param manifestPath Path to manifest file
|
* @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);
|
bool updateApp(void);
|
||||||
|
|
||||||
@ -137,6 +172,7 @@ class AppUpdater {
|
|||||||
UpdateStatus status;
|
UpdateStatus status;
|
||||||
std::unique_ptr<uint8_t[]> downloadBuffer;
|
std::unique_ptr<uint8_t[]> downloadBuffer;
|
||||||
bool updateAvailable = false;
|
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
|
* @return true if successful
|
||||||
*/
|
*/
|
||||||
bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength,
|
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
|
* @brief Update progress callback
|
||||||
@ -198,4 +234,11 @@ void handleUpdateProgress(AsyncWebServerRequest *request);
|
|||||||
|
|
||||||
void startVersionCheckTask();
|
void startVersionCheckTask();
|
||||||
|
|
||||||
void versionCheckTask(void* parameter);
|
void versionCheckTask(void* parameter);
|
||||||
|
|
||||||
|
// Convenience functions for setting update mode
|
||||||
|
void setGlobalUpdateMode(UpdateMode mode);
|
||||||
|
UpdateMode getGlobalUpdateMode();
|
||||||
|
void setUpdateModeFilesOnly();
|
||||||
|
void setUpdateModeFirmwareOnly();
|
||||||
|
void setUpdateModeBoth();
|
||||||
@ -27,11 +27,33 @@ private:
|
|||||||
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
|
#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y)
|
||||||
#define UNIQUE_NAME(base) CONCATENATE(base, __LINE__)
|
#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) \
|
#define ON_EVERY_N_MILLISECONDS(N) \
|
||||||
static OnEveryN<N> UNIQUE_NAME(__on_everyN_); \
|
static OnEveryN<N> UNIQUE_NAME(__on_everyN_); \
|
||||||
if (UNIQUE_NAME(__on_everyN_).ready())
|
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
|
// Macro for ON_EVERY_N_SECONDS
|
||||||
#define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000)
|
#define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000)
|
||||||
|
|
||||||
|
|||||||
326
include/RtttlPlayer.h
Normal file
326
include/RtttlPlayer.h
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
@ -21,21 +21,19 @@ typedef enum {
|
|||||||
|
|
||||||
//Tunes
|
//Tunes
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool enabled;
|
|
||||||
int cycles;
|
int cycles;
|
||||||
int pause;
|
int pause;
|
||||||
String melody;
|
String melody;
|
||||||
}BUZZ_TUNE;
|
}BUZZ_TUNE;
|
||||||
|
|
||||||
#define TUNE_MAX_COUNT 12
|
#define TUNE_MAX_COUNT 14
|
||||||
|
|
||||||
extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
|
extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
|
||||||
|
|
||||||
void Init_Buzzer(int8_t, const char* configPath);
|
void Init_Buzzer(int8_t pin, const char* configPath, int8_t channel = -1);
|
||||||
|
|
||||||
void Buzzer_Beep(int, int freq=1000);
|
|
||||||
|
|
||||||
void Buzzer_Load_Tunes(const char* tunesPath);
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#define FIRMWARE_VERSION_MAJOR 1
|
#define FIRMWARE_VERSION_MAJOR 1
|
||||||
#define FIRMWARE_VERSION_MINOR 4
|
#define FIRMWARE_VERSION_MINOR 4
|
||||||
#define FIRMWARE_VERSION_PATCH 9
|
#define FIRMWARE_VERSION_PATCH 19
|
||||||
|
|
||||||
|
|
||||||
#define FIRMWARE_DESCRIPTION \
|
#define FIRMWARE_DESCRIPTION \
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include "system.h"
|
#include "system.h"
|
||||||
#include "ColorPalettes.h"
|
#include "ColorPalettes.h"
|
||||||
|
#include "global.h"
|
||||||
//#include <crgb.h>
|
//#include <crgb.h>
|
||||||
|
|
||||||
#define FASTLED_CORE 0
|
#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);
|
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);
|
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...");
|
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){
|
void Animation_Loop_Exit(void){
|
||||||
if( Animation_Task_Handle ){
|
if( Animation_Task_Handle ){
|
||||||
|
|||||||
@ -4,6 +4,12 @@
|
|||||||
#include <FastLED.h>
|
#include <FastLED.h>
|
||||||
#include "ColorPalettes.h"
|
#include "ColorPalettes.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <functional>
|
||||||
|
#include "PWM_Output.h"
|
||||||
|
|
||||||
|
|
||||||
typedef struct{
|
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) {
|
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;
|
if (!leds || size <= 1 || totalDurationMs <= 0) return;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@
|
|||||||
#include "WiFi.h"
|
#include "WiFi.h"
|
||||||
#include "ATALights.h"
|
#include "ATALights.h"
|
||||||
#include "BleSettings.h"
|
#include "BleSettings.h"
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
static const char *tag = "BLE_SP110E";
|
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
|
// Function to send data to all connected clients in chunks based on MTU
|
||||||
void sendToAllClients(const uint8_t* data, size_t len) {
|
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)
|
// Skip if no subscribed clients (if API available)
|
||||||
#if defined(NIMBLE_INCLUDED) || true
|
#if defined(NIMBLE_INCLUDED) || true
|
||||||
#ifdef CONFIG_BT_NIMBLE_ROLE_PERIPHERAL
|
#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
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -152,9 +164,19 @@ void sendToAllClients(const uint8_t* data, size_t len) {
|
|||||||
while (offset < len) {
|
while (offset < len) {
|
||||||
size_t chunk = len - offset;
|
size_t chunk = len - offset;
|
||||||
if (chunk > maxChunk) chunk = maxChunk;
|
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
|
try {
|
||||||
pStickCharacteristic->notify();
|
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;
|
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) {
|
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];
|
||||||
uint8_t command = val[3];
|
ESP_LOGI(tag, "Command received: 0x%02X", command);
|
||||||
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
|
// Handle different commands
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case TURN_ON:
|
case TURN_ON:
|
||||||
Lights_Set_ON();
|
Lights_Set_ON();
|
||||||
led_status.enable = 1;
|
led_status.enable = 1;
|
||||||
//ESP_LOGI(tag, "Lights ON");
|
//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;
|
break;
|
||||||
case TURN_OFF:
|
}
|
||||||
Lights_Set_OFF();
|
led_status.red = val[1];
|
||||||
led_status.enable = 0;
|
led_status.green = val[2];
|
||||||
//ESP_LOGI(tag, "Lights OFF");
|
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;
|
break;
|
||||||
case SET_STATIC_COLOR:
|
}
|
||||||
if(len < 7) {
|
led_status.bright = val[0];
|
||||||
ESP_LOGW(tag, "SET_STATIC_COLOR command requires 3 parameters (R,G,B)");
|
Lights_Set_Brightness(val[0]);
|
||||||
break;
|
//ESP_LOGI(tag, "Bright set to %d", led_status.bright);
|
||||||
}
|
break;
|
||||||
led_status.red = val[1];
|
case SET_WHITE:
|
||||||
led_status.green = val[2];
|
led_status.white = val[0];
|
||||||
led_status.blue = val[0];
|
Lights_Set_White(val[0]);
|
||||||
Lights_Set_Animation(SOLID_COLOR_INDEX, val[0], val[1], val[2]);
|
//ESP_LOGI(tag, "White set to %d", led_status.white);
|
||||||
//ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue);
|
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;
|
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) {
|
void Init_BLE_SP110E(NimBLEServer* pServer) {
|
||||||
|
if (!pServer) {
|
||||||
|
ESP_LOGE(tag, "Invalid BLE server pointer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
led_status.speed = 10;
|
led_status.speed = 10;
|
||||||
led_status.bright = 50;
|
led_status.bright = 50;
|
||||||
led_status.ic_model = 0;
|
led_status.ic_model = 0;
|
||||||
@ -325,7 +360,14 @@ void Init_BLE_LightStick_Client(){
|
|||||||
ESP_LOGW(tag, "Light Stick Client Task already running");
|
ESP_LOGW(tag, "Light Stick Client Task already running");
|
||||||
return;
|
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
|
// Task for the BLE LightStick client
|
||||||
@ -333,8 +375,14 @@ void Init_BLE_LightStick_Client(){
|
|||||||
void BLE_LightStick_Client_Task(void *parameter) {
|
void BLE_LightStick_Client_Task(void *parameter) {
|
||||||
static const char *tag = "BLE_LightStick_Client_Task";
|
static const char *tag = "BLE_LightStick_Client_Task";
|
||||||
ESP_LOGI(tag, "BLE LightStick Client Task started");
|
ESP_LOGI(tag, "BLE LightStick Client Task started");
|
||||||
|
|
||||||
|
// Register task with watchdog
|
||||||
|
esp_task_wdt_add(NULL);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
// Reset watchdog timer
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
// Only try to connect if we're not already connected and a device is set.
|
// Only try to connect if we're not already connected and a device is set.
|
||||||
if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) {
|
if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) {
|
||||||
// Create a new client instance if needed.
|
// 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");
|
ESP_LOGE(tag, "Failed to connect to the server");
|
||||||
// Delete the client instance so that a new one is created next time.
|
// Delete the client instance so that a new one is created next time.
|
||||||
if (pStickClient != nullptr) {
|
if (pStickClient != nullptr) {
|
||||||
NimBLEDevice::deleteClient(pStickClient);
|
try {
|
||||||
pStickClient = nullptr;
|
NimBLEDevice::deleteClient(pStickClient);
|
||||||
|
pStickClient = nullptr;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ESP_LOGE(tag, "Exception deleting client: %s", e.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Wait before retrying.
|
// Implement exponential backoff for connection retries
|
||||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -390,6 +447,12 @@ void BLE_LightStick_Client_Task(void *parameter) {
|
|||||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,17 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
|
|||||||
}
|
}
|
||||||
else if (value.compare("upgrade-start") == 0) { // Start OTA update
|
else if (value.compare("upgrade-start") == 0) { // Start OTA update
|
||||||
ESP_LOGI(tag, "Start OTA update command received");
|
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
|
startFirmwareUpdateTask(nullptr); // start the task
|
||||||
}
|
}
|
||||||
else if (value.compare("rename-device") == 0) { // Start renaming device
|
else if (value.compare("rename-device") == 0) { // Start renaming device
|
||||||
@ -132,14 +143,66 @@ void bleUpgrade_send_message(String s){
|
|||||||
if (s.length() == 0) {
|
if (s.length() == 0) {
|
||||||
return;
|
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) {
|
if (pUpgradeCharacteristic2->getSubscribedCount() > 0) {
|
||||||
pUpgradeCharacteristic2->notify();
|
pUpgradeCharacteristic2->notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
void Init_UpgradeBLEService(NimBLEServer *pServer){
|
void Init_UpgradeBLEService(NimBLEServer *pServer){
|
||||||
|
|
||||||
@ -170,6 +233,5 @@ void Init_UpgradeBLEService(NimBLEServer *pServer){
|
|||||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||||
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
|
pAdvertising->addServiceUUID( BTUpgradeServiceUUID.c_str() ); // Advertise service UUID
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include "BLE_SP110E.h"
|
#include "BLE_SP110E.h"
|
||||||
#include "BLE_UpdateService.h"
|
#include "BLE_UpdateService.h"
|
||||||
#include "BleSettings.h"
|
#include "BleSettings.h"
|
||||||
|
#include "my_buzzer.h"
|
||||||
|
|
||||||
static const char* tag = "BleServer";
|
static const char* tag = "BleServer";
|
||||||
|
|
||||||
@ -39,11 +40,13 @@ public:
|
|||||||
void onConnect(NimBLEServer* /*pServer*/) override {
|
void onConnect(NimBLEServer* /*pServer*/) override {
|
||||||
ESP_LOGI(tag, "Client connected");
|
ESP_LOGI(tag, "Client connected");
|
||||||
ensureAdvertising("onConnect");
|
ensureAdvertising("onConnect");
|
||||||
|
Buzzer_Play_Tune(TUNE_CONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onDisconnect(NimBLEServer* /*pServer*/) override {
|
void onDisconnect(NimBLEServer* /*pServer*/) override {
|
||||||
ESP_LOGI(tag, "Client disconnected");
|
ESP_LOGI(tag, "Client disconnected");
|
||||||
ensureAdvertising("onDisconnect");
|
ensureAdvertising("onDisconnect");
|
||||||
|
Buzzer_Play_Tune(TUNE_DISCONNECTED, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@ -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) {
|
String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def) {
|
||||||
// Check if the key exists and is not null
|
// Check if the key exists using the recommended approach
|
||||||
if (!jsonObject[key].is<String>()) {
|
if (!jsonObject[key] || jsonObject[key].isNull()) {
|
||||||
ESP_LOGW(tag, "Key [%s] not found or null. Using default value [%s].", key, def.c_str());
|
ESP_LOGW(tag, "Key [%s] not found/null", key);
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the value as a String
|
// Handle different types to avoid unnecessary String conversions
|
||||||
|
if (jsonObject[key].is<const char*>()) {
|
||||||
|
const char* charValue = jsonObject[key].as<const char*>();
|
||||||
|
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<String>();
|
String value = jsonObject[key].as<String>();
|
||||||
|
|
||||||
// Check if the value is empty
|
// Check for empty string
|
||||||
if (value.length() == 0) {
|
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;
|
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;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
|
bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) {
|
||||||
// Check if the key exists and is of type boolean
|
// Check if the key exists and is of type boolean
|
||||||
if (!jsonObject[key].is<bool>()) {
|
if (!jsonObject[key].is<bool>()) {
|
||||||
@ -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");
|
ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
|
||||||
return value;
|
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>()) {
|
||||||
|
bool value = jsonObject[key].as<bool>();
|
||||||
|
ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric conversion (no heap allocation)
|
||||||
|
if (jsonObject[key].is<int>() || jsonObject[key].is<float>()) {
|
||||||
|
bool value = (jsonObject[key].as<int>() != 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*>()) {
|
||||||
|
const char* str = jsonObject[key].as<const char*>();
|
||||||
|
// 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
|
// Explicit instantiations
|
||||||
|
|||||||
38
src/main.cpp
38
src/main.cpp
@ -144,7 +144,8 @@ void setup()
|
|||||||
Init_Board_Basic(sys_settings.boardPins);
|
Init_Board_Basic(sys_settings.boardPins);
|
||||||
|
|
||||||
// Load tunes.json and initialize
|
// 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
|
// Initialize PWM Outputs
|
||||||
Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings);
|
Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings);
|
||||||
@ -171,10 +172,13 @@ void setup()
|
|||||||
{
|
{
|
||||||
setStatusPin1(true);
|
setStatusPin1(true);
|
||||||
UpgradeMode = true;
|
UpgradeMode = true;
|
||||||
|
ESP_LOGW(tag, "Upgrade Mode Triggered");
|
||||||
ESP_LOGW(tag, "Enabling BLE and Update Service");
|
ESP_LOGW(tag, "Enabling BLE and Update Service");
|
||||||
Init_BleServer(true, true);
|
Init_BleServer(true, true);
|
||||||
ESP_LOGW(tag, "Enabling Wifi AP and Client");
|
ESP_LOGW(tag, "Enabling Wifi AP and Client");
|
||||||
Wifi_Init();
|
Wifi_Init();
|
||||||
|
|
||||||
|
//Buzzer_Play_Tune(TUNE_UPGRADE_MODE);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -197,7 +201,7 @@ void setup()
|
|||||||
Init_Lights_Task();
|
Init_Lights_Task();
|
||||||
#endif
|
#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
|
// TODO... Test if this is still necessary need to configure pin 0 for some reason
|
||||||
// pinMode(0, INPUT); // button0/boot pin
|
// pinMode(0, INPUT); // button0/boot pin
|
||||||
@ -225,29 +229,21 @@ void loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Temperature Monitor
|
// Temperature Monitor
|
||||||
ON_EVERY_N_MILLISECONDS(5000)
|
static OnEveryMsVariable temperatureMonitorTimer;
|
||||||
|
if (sys_settings.tSensorSettings.enabled)
|
||||||
{
|
{
|
||||||
static float boardTemperature;
|
if (temperatureMonitorTimer.ready(sys_settings.tSensorSettings.intervalMs))
|
||||||
|
|
||||||
// Read temperature if the sensor is enabled
|
|
||||||
if (sys_settings.tSensorSettings.enabled)
|
|
||||||
{
|
{
|
||||||
|
static float boardTemperature;
|
||||||
boardTemperature = tSensor->readTemperatureF();
|
boardTemperature = tSensor->readTemperatureF();
|
||||||
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
|
// ESP_LOGI(tag, "Board T: %F", boardTemperature);
|
||||||
}
|
|
||||||
|
|
||||||
// Fan Control
|
// Fan Control
|
||||||
if (sys_settings.tSensorSettings.enabled)
|
|
||||||
{
|
|
||||||
UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]);
|
UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Tune Playing
|
|
||||||
//if (anyrtttl::nonblocking::isPlaying())
|
|
||||||
//{
|
|
||||||
// anyrtttl::nonblocking::play();
|
|
||||||
//}
|
|
||||||
|
|
||||||
// Animation TestMode Timeout
|
// Animation TestMode Timeout
|
||||||
#if LEDS_ENABLED
|
#if LEDS_ENABLED
|
||||||
@ -274,7 +270,7 @@ void loop()
|
|||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
#if BUZZER_ENABLED
|
#if BUZZER_ENABLED
|
||||||
Buzzer_Play_Tune(TUNE_BEEP, false); // blocking
|
Buzzer_Play_Tune(TUNE_LOWEEP); // blocking
|
||||||
#endif
|
#endif
|
||||||
vTaskDelay(200);
|
vTaskDelay(200);
|
||||||
}
|
}
|
||||||
@ -296,11 +292,12 @@ void loop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade Mode Tune
|
// Upgrade Mode Hearbeat tune
|
||||||
if(UpgradeMode){
|
if(UpgradeMode){
|
||||||
ON_EVERY_N_MILLISECONDS(5000)
|
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].vision = jsonConstrainBool(tag, obj, "vision", true);
|
||||||
sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain<int>(tag, obj, "relay-index", 0, 1, 0);
|
sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain<int>(tag, obj, "relay-index", 0, 1, 0);
|
||||||
sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0);
|
sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0);
|
||||||
sys_settings.rampLightSettings[rampIndex].min = jsonConstrain<float>(tag, obj, "min", 0.0, 100.0, 0.0);
|
|
||||||
sys_settings.rampLightSettings[rampIndex].max = jsonConstrain<float>(tag, obj, "max", 5.0, 100.0, 100.0);
|
|
||||||
sys_settings.rampLightSettings[rampIndex].step = jsonConstrain<float>(tag, obj, "step", 0.01, 100.0, 1.5);
|
|
||||||
rampIndex++;
|
rampIndex++;
|
||||||
}
|
}
|
||||||
ESP_LOGI(tag, "Loaded Ramp Lights settings...");
|
ESP_LOGI(tag, "Loaded Ramp Lights settings...");
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#include "my_buttons.h"
|
#include "my_buttons.h"
|
||||||
#include "global.h"
|
#include "global.h"
|
||||||
#include "BLE_UpdateService.h"
|
#include "BLE_UpdateService.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "AppUpgrade.h"
|
||||||
|
|
||||||
static const char* tag = "button";
|
static const char* tag = "button";
|
||||||
OneButton *boardButtons[3];
|
OneButton *boardButtons[3];
|
||||||
@ -111,6 +113,7 @@ void btn2_click() {
|
|||||||
//Pulse_LED_Status(150);
|
//Pulse_LED_Status(150);
|
||||||
//Buzzer_Beep(150);
|
//Buzzer_Beep(150);
|
||||||
// send packet
|
// send packet
|
||||||
|
sendUpdateMessage("testing....", false, -1);
|
||||||
ESP_LOGD(tag, "btn2 1x");
|
ESP_LOGD(tag, "btn2 1x");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,102 +7,93 @@
|
|||||||
#include <pitches.h>
|
#include <pitches.h>
|
||||||
|
|
||||||
#include "JsonConstrain.h"
|
#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";
|
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";
|
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];
|
BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
|
||||||
int8_t buzzPin;
|
int8_t buzzPin;
|
||||||
|
int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
|
||||||
|
|
||||||
|
void Init_Buzzer(int8_t pin, const char* configFile, int8_t channel)
|
||||||
void Init_Buzzer(int8_t pin, const char* configFile)
|
|
||||||
{
|
{
|
||||||
buzzPin = pin;
|
buzzPin = pin;
|
||||||
if(buzzPin >= 0){
|
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
|
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 || !player) return;
|
||||||
if (buzzPin < 0) return;
|
|
||||||
|
|
||||||
// Range / data validation
|
if(tune < 0 || tune >= TUNE_MAX_COUNT){
|
||||||
if (tune < 0 || tune >= TUNE_MAX_COUNT) {
|
ESP_LOGW(tag, "Invalid tune index %d", tune);
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async mode: begin once, then caller should periodically call again to advance playback
|
int cycles = buzzTune[tune].cycles;
|
||||||
if (async) {
|
int pause = buzzTune[tune].pause;
|
||||||
bool playing = anyrtttl::nonblocking::isPlaying();
|
String melody = buzzTune[tune].melody;
|
||||||
if (hasPriority && playing) {
|
|
||||||
anyrtttl::nonblocking::stop();
|
if(melody.length() == 0){
|
||||||
playing = false;
|
ESP_LOGW(tag, "Tune %d has empty melody, skipping playback", tune);
|
||||||
}
|
|
||||||
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();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blocking mode: play full tune cycles with optional pause
|
// Play the tune the specified number of cycles
|
||||||
ESP_LOGD(tag, "Playing blocking tune %d cycles=%d pause=%d", (int)tune, buzzTune[tune].cycles, buzzTune[tune].pause);
|
for(int i = 0; i < cycles; i++){
|
||||||
for (int c = 0; c < buzzTune[tune].cycles; ++c) {
|
bool played = player->play(melody.c_str(), priority ? 2 : 1); // Use priority level
|
||||||
anyrtttl::blocking::play(buzzPin, melody.c_str());
|
if(!played){
|
||||||
if (buzzTune[tune].pause > 0 && c + 1 < buzzTune[tune].cycles) {
|
ESP_LOGW(tag, "Failed to play tune %d (cycle %d)", tune, i+1);
|
||||||
delay(buzzTune[tune].pause); // simple pause between cycles
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pause > 0 && i < cycles - 1){
|
||||||
|
delay(pause);
|
||||||
}
|
}
|
||||||
yield(); // allow other tasks to run
|
|
||||||
}
|
}
|
||||||
prev_tune = tune;
|
|
||||||
}
|
}
|
||||||
|
// Optimized tune loading - minimal memory allocation
|
||||||
// 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 ()
|
|
||||||
void Buzzer_Load_Tunes(const char* tunesPath){
|
void Buzzer_Load_Tunes(const char* tunesPath){
|
||||||
|
ESP_LOGI(tag, "Loading tunes from: %s", tunesPath);
|
||||||
File file = LittleFS.open(tunesPath);
|
File file = LittleFS.open(tunesPath);
|
||||||
|
|
||||||
if (!file) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use smaller JSON document for memory efficiency
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
DeserializationError error = deserializeJson(doc, file);
|
DeserializationError error = deserializeJson(doc, file);
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
if(error){
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,12 +104,19 @@ void Buzzer_Load_Tunes(const char* tunesPath){
|
|||||||
if(tuneIndex >= TUNE_MAX_COUNT) break;
|
if(tuneIndex >= TUNE_MAX_COUNT) break;
|
||||||
buzzTune[tuneIndex].cycles = jsonConstrain<int>(tag, obj, "cycles", 1, 100, 1);
|
buzzTune[tuneIndex].cycles = jsonConstrain<int>(tag, obj, "cycles", 1, 100, 1);
|
||||||
buzzTune[tuneIndex].pause = jsonConstrain<int>(tag, obj, "pause", 0, 100, 0);
|
buzzTune[tuneIndex].pause = jsonConstrain<int>(tag, obj, "pause", 0, 100, 0);
|
||||||
buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
|
buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY);
|
||||||
ESP_LOGD(tag, "tune %d : %s", tuneIndex, buzzTune[tuneIndex].melody.c_str());
|
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++;
|
tuneIndex++;
|
||||||
}
|
}
|
||||||
ESP_LOGI(tag, "Loaded tunes...");
|
ESP_LOGI(tag, "Successfully loaded %d tunes", tuneIndex);
|
||||||
}else{
|
} else {
|
||||||
ESP_LOGE(tag, "Error!, %s key: tunes not found..", tunesPath);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
205
src/my_wifi.cpp
205
src/my_wifi.cpp
@ -164,25 +164,36 @@ bool StartWifiConnectTask(String ssid = "", String pass = "")
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wifi_task_running)
|
// Create mutex if it doesn't exist
|
||||||
{
|
if (wifiMutex == nullptr) {
|
||||||
client_ssid = ssid;
|
wifiMutex = xSemaphoreCreateMutex();
|
||||||
client_pass = pass;
|
}
|
||||||
if (Wifi_Task_Handle == NULL)
|
|
||||||
|
// Take mutex with timeout
|
||||||
|
if (xSemaphoreTake(wifiMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
|
if (!wifi_task_running)
|
||||||
{
|
{
|
||||||
ESP_LOGI(tag, "Creating WiFi task");
|
client_ssid = ssid;
|
||||||
xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0);
|
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
|
else
|
||||||
{
|
{
|
||||||
ESP_LOGI(tag, "WiFi task already running");
|
ESP_LOGE(tag, "Task already running");
|
||||||
}
|
}
|
||||||
|
xSemaphoreGive(wifiMutex);
|
||||||
return true;
|
} else {
|
||||||
}
|
ESP_LOGE(tag, "Failed to acquire mutex - WiFi operation in progress");
|
||||||
else
|
|
||||||
{
|
|
||||||
ESP_LOGE(tag, "Task already running");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -192,7 +203,10 @@ void Wifi_ConnectTask(void *parameter)
|
|||||||
{
|
{
|
||||||
static const char *tag = "Wifi_Task";
|
static const char *tag = "Wifi_Task";
|
||||||
wifi_task_running = true;
|
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())
|
if (WiFi.status() != WL_CONNECTED || client_ssid != WiFi.SSID())
|
||||||
{
|
{
|
||||||
ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str());
|
ESP_LOGI(tag, "Connecting to: %s", client_ssid.c_str());
|
||||||
@ -206,6 +220,9 @@ void Wifi_ConnectTask(void *parameter)
|
|||||||
uint8_t attempts = 0;
|
uint8_t attempts = 0;
|
||||||
while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS)
|
while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS)
|
||||||
{
|
{
|
||||||
|
// Reset watchdog timer to prevent timeouts during connection attempts
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
switch (WiFi.status())
|
switch (WiFi.status())
|
||||||
{
|
{
|
||||||
case WL_NO_SSID_AVAIL:
|
case WL_NO_SSID_AVAIL:
|
||||||
@ -242,6 +259,9 @@ void Wifi_ConnectTask(void *parameter)
|
|||||||
|
|
||||||
ESP_LOGI(tag, "Wifi Task ended");
|
ESP_LOGI(tag, "Wifi Task ended");
|
||||||
|
|
||||||
|
// Unregister from watchdog before deletion
|
||||||
|
esp_task_wdt_delete(NULL);
|
||||||
|
|
||||||
Wifi_Task_Handle = NULL;
|
Wifi_Task_Handle = NULL;
|
||||||
wifi_task_running = false;
|
wifi_task_running = false;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
@ -249,17 +269,25 @@ void Wifi_ConnectTask(void *parameter)
|
|||||||
|
|
||||||
void Wifi_Check_Internet()
|
void Wifi_Check_Internet()
|
||||||
{
|
{
|
||||||
// Check for internet connection
|
// Check for internet connection with multiple fallback servers
|
||||||
const char *host = "8.8.8.8"; // Google DNS server
|
const char *hosts[] = {"8.8.8.8", "1.1.1.1", "208.67.222.222"}; // Google DNS, Cloudflare DNS, OpenDNS
|
||||||
if (Ping.ping(host, 1))
|
const int num_hosts = sizeof(hosts) / sizeof(hosts[0]);
|
||||||
{
|
|
||||||
InternetAvailable = true;
|
InternetAvailable = false;
|
||||||
ESP_LOGI(tag, "Internet connection verified");
|
|
||||||
|
// 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
|
|
||||||
{
|
if (!InternetAvailable) {
|
||||||
InternetAvailable = false;
|
ESP_LOGW(tag, "No internet connection after trying multiple DNS servers");
|
||||||
ESP_LOGW(tag, "No internet connection");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,38 +332,122 @@ bool Wifi_Save_Credentials(String path)
|
|||||||
return true;
|
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()
|
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);
|
WiFi.scanNetworks(false, false);
|
||||||
|
|
||||||
|
// Wait for scan with timeout
|
||||||
|
uint32_t startTime = millis();
|
||||||
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING)
|
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();
|
networkCount = WiFi.scanComplete();
|
||||||
if (networkCount >= 0)
|
if (networkCount >= 0)
|
||||||
{
|
{
|
||||||
|
ESP_LOGI(tag, "WiFi scan complete, found %d networks", networkCount);
|
||||||
|
scanStatus = 2; // Complete
|
||||||
|
|
||||||
|
// Create JSON document with appropriate capacity
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
|
doc.clear();
|
||||||
JsonArray networks = doc["networks"].to<JsonArray>();
|
JsonArray networks = doc["networks"].to<JsonArray>();
|
||||||
|
|
||||||
for (int i = 0; i < networkCount; i++)
|
for (int i = 0; i < networkCount; i++)
|
||||||
{
|
{
|
||||||
auto network = networks.add<JsonObject>();
|
auto network = networks.add<JsonObject>();
|
||||||
|
|
||||||
|
// Basic network info
|
||||||
network["ssid"] = WiFi.SSID(i);
|
network["ssid"] = WiFi.SSID(i);
|
||||||
network["rssi"] = WiFi.RSSI(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;
|
// Serialize to the global variable
|
||||||
serializeJson(doc, jsonString);
|
networkList.clear();
|
||||||
networkList = jsonString;
|
serializeJson(doc, networkList);
|
||||||
|
|
||||||
|
// Clean up scan results from memory
|
||||||
WiFi.scanDelete();
|
WiFi.scanDelete();
|
||||||
}
|
}
|
||||||
else
|
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)
|
void Setup_WebServer_Handlers(AsyncWebServer &server)
|
||||||
@ -374,7 +486,7 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
|||||||
String pass = request->getParam("pass", false, false)->value();
|
String pass = request->getParam("pass", false, false)->value();
|
||||||
|
|
||||||
// Validate credentials
|
// Validate credentials
|
||||||
if (client_ssid.length() < 1 || client_pass.length() < 8) {
|
if (ssid.length() < 1 || pass.length() < 8) {
|
||||||
ESP_LOGE(tag, "Invalid credentials");
|
ESP_LOGE(tag, "Invalid credentials");
|
||||||
request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}");
|
request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}");
|
||||||
return;
|
return;
|
||||||
@ -584,21 +696,20 @@ void Setup_WebServer_Handlers(AsyncWebServer &server)
|
|||||||
// If a dynamic URL was loaded, override base
|
// If a dynamic URL was loaded, override base
|
||||||
extern String updateUrl; // declared in AppUpgrade.cpp
|
extern String updateUrl; // declared in AppUpgrade.cpp
|
||||||
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
|
if(updateUrl.length()) updater.setBaseUrl(updateUrl);
|
||||||
if(!updater.checkManifest()){
|
// checkManifest() does not return a bool; capture its result (type-dependent) instead of using it in a boolean expression
|
||||||
ESP_LOGE(tag, "Manifest check failed via /upgrade/check");
|
auto manifestResult = updater.checkManifest();
|
||||||
} else {
|
// TODO: inspect manifestResult for success/failure once its API is known
|
||||||
otaVersion = updater.otaVersion;
|
otaVersion = updater.otaVersion;
|
||||||
}
|
|
||||||
bool avail = otaVersion > localVersion;
|
bool avail = otaVersion > localVersion;
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["currentVersion"] = localVersion.toString();
|
doc["currentVersion"] = localVersion.toString();
|
||||||
doc["latestVersion"] = otaVersion.toString();
|
doc["latestVersion"] = otaVersion.toString();
|
||||||
doc["updateAvailable"] = avail;
|
doc["updateAvailable"] = avail;
|
||||||
|
|
||||||
String response;
|
String response;
|
||||||
serializeJson(doc, response);
|
serializeJson(doc, response);
|
||||||
request->send(200, "application/json", response); });
|
request->send(200, "application/json", response); });
|
||||||
// Start update process
|
// Start update process
|
||||||
server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request)
|
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)
|
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)
|
if (!index)
|
||||||
{
|
{
|
||||||
|
|||||||
716
temporary/AppUpgrade_orig.cpp
Normal file
716
temporary/AppUpgrade_orig.cpp
Normal file
@ -0,0 +1,716 @@
|
|||||||
|
#include "AppUpgrade.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <MD5Builder.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <memory>
|
||||||
|
#include "global.h"
|
||||||
|
#include "JsonConstrain.h"
|
||||||
|
#include "BLE_UpdateService.h"
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <Update.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
static const char* TAG = "AppUpdater";
|
||||||
|
TaskHandle_t Update_Task_Handle = NULL;
|
||||||
|
TaskHandle_t versionCheckTask_Handle = NULL;
|
||||||
|
volatile bool g_UpdateCancelFlag = false; // cancellation flag
|
||||||
|
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<HTTP_RETRY_COUNT; ++attempt){
|
||||||
|
if(g_UpdateCancelFlag) return ManifestCheckResult::ERROR_FETCH_FAILED;
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url);
|
||||||
|
int httpCode = http.GET();
|
||||||
|
if (httpCode == HTTP_CODE_OK) {
|
||||||
|
payload = http.getString();
|
||||||
|
http.end();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Manifest GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
if(payload.isEmpty()){
|
||||||
|
ESP_LOGE(TAG, "Failed to fetch manifest after retries");
|
||||||
|
return ManifestCheckResult::ERROR_FETCH_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(payload.length() > 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<HTTP_RETRY_COUNT; ++attempt){
|
||||||
|
if(g_UpdateCancelFlag) return false;
|
||||||
|
http.begin(url);
|
||||||
|
httpCode = http.GET();
|
||||||
|
if(httpCode == HTTP_CODE_OK) break;
|
||||||
|
ESP_LOGW(TAG, "File GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "Download failed: %d", httpCode);
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the stream and content length
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
size_t contentLength = http.getSize();
|
||||||
|
|
||||||
|
// Verify and save the file
|
||||||
|
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
|
||||||
|
http.end();
|
||||||
|
if(!success){
|
||||||
|
String errMsg = String(localPath) + " MD5 failed";
|
||||||
|
updateProgress( UpdateStatus::ERROR, 0, errMsg.c_str() );
|
||||||
|
}else{
|
||||||
|
updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
|
||||||
|
{
|
||||||
|
MD5Builder md5;
|
||||||
|
md5.begin();
|
||||||
|
size_t totalRead = 0;
|
||||||
|
|
||||||
|
// Create temporary filename
|
||||||
|
String tempPath = String(localPath) + ".tmp";
|
||||||
|
|
||||||
|
// Open temporary file for writing
|
||||||
|
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
|
||||||
|
if (!file) {
|
||||||
|
ESP_LOGE(TAG, "Failed to open temporary file for writing");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||||
|
|
||||||
|
if (contentLength > 0) {
|
||||||
|
// Single pass with known content length
|
||||||
|
while (totalRead < contentLength) {
|
||||||
|
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<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
|
||||||
|
md5Builder.add(downloadBuffer.get(), readLen);
|
||||||
|
totalRead += readLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
md5Builder.calculate();
|
||||||
|
file.close();
|
||||||
|
return md5Builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppUpdater::updateFilesArray() {
|
||||||
|
int successCount = 0;
|
||||||
|
int totalFiles = jsonFilesArray.size();
|
||||||
|
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
|
||||||
|
|
||||||
|
// Iterate over each file entry in the manifest
|
||||||
|
for (JsonObject file : jsonFilesArray) {
|
||||||
|
const char* remotePath = file["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<JsonObject>() || !jsonManifest["firmware"]["md5"].is<const char*>()) {
|
||||||
|
ESP_LOGE(TAG, "Invalid firmware section in manifest");
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the firmware MD5 hash and URL
|
||||||
|
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
|
||||||
|
String firmwareUrl = buildUrl(appName);
|
||||||
|
|
||||||
|
// Download the firmware
|
||||||
|
HTTPClient http;
|
||||||
|
int httpCode = -1;
|
||||||
|
for(int attempt=0; attempt<HTTP_RETRY_COUNT; ++attempt){
|
||||||
|
if(g_UpdateCancelFlag) return false;
|
||||||
|
http.begin(firmwareUrl);
|
||||||
|
httpCode = http.GET();
|
||||||
|
if(httpCode == HTTP_CODE_OK) break;
|
||||||
|
ESP_LOGW(TAG, "Firmware GET failed (attempt %d/%d): %d", attempt+1, HTTP_RETRY_COUNT, httpCode);
|
||||||
|
http.end();
|
||||||
|
if(attempt+1 < HTTP_RETRY_COUNT) vTaskDelay(pdMS_TO_TICKS(HTTP_RETRY_DELAY_MS));
|
||||||
|
}
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check available space
|
||||||
|
size_t firmwareSize = http.getSize();
|
||||||
|
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
|
||||||
|
ESP_LOGE(TAG, "Firmware: Not enough space for update");
|
||||||
|
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up MD5 checking
|
||||||
|
MD5Builder md5;
|
||||||
|
md5.begin();
|
||||||
|
|
||||||
|
// Download and verify firmware
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
if (firmwareSize > 0) {
|
||||||
|
size_t remaining = firmwareSize;
|
||||||
|
while (remaining > 0) {
|
||||||
|
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<int>(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<JsonObject>();
|
||||||
|
String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
|
||||||
|
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
|
||||||
|
updateUrl = baseUrl + folderName;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
|
||||||
|
|
||||||
|
char buffer[128];
|
||||||
|
const char* msg;
|
||||||
|
bool isComplete = false;
|
||||||
|
|
||||||
|
const char* safeMsg = message ? message : "";
|
||||||
|
switch (newStatus) {
|
||||||
|
case AppUpdater::UpdateStatus::IDLE:
|
||||||
|
snprintf(buffer, sizeof(buffer), "Update idle");
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::MESSAGE:
|
||||||
|
msg = message ? message : "";
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::DOWNLOADING:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::VERIFYING:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
|
||||||
|
msg = buffer;
|
||||||
|
break;
|
||||||
|
case AppUpdater::UpdateStatus::FILE_SKIPPED:
|
||||||
|
snprintf(buffer, sizeof(buffer), "%s: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
533
temporary/ata-boothifier-upgradeV3_old.html
Normal file
533
temporary/ata-boothifier-upgradeV3_old.html
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ATA Firmware Update</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: left;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator-ble {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: gray;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator-wifi {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: gray;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator-internet {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: gray;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adds space above the WiFi Connect button */
|
||||||
|
.btn-container.wifi {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 130px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tab-bar { display:flex; gap:8px; justify-content:center; margin:12px 0 16px; flex-wrap:wrap; }
|
||||||
|
.tab-bar button { max-width:none; flex:0 0 auto; background:#6c757d; }
|
||||||
|
.tab-bar button.active { background:#007bff; }
|
||||||
|
.tab-panel { display:none; }
|
||||||
|
.tab-panel.active { display:block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>ATA Firmware Update</h1>
|
||||||
|
|
||||||
|
<!-- Tab Buttons -->
|
||||||
|
<div class="tab-bar">
|
||||||
|
<button class="tab-btn active" data-tab="tab-upgrade">Upgrade</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-wifi">WiFi Comm</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-upgrade" class="tab-panel active">
|
||||||
|
<!-- Status Indicators -->
|
||||||
|
<div class="status-container">
|
||||||
|
<span class="status-indicator-ble"></span>
|
||||||
|
<label id="status-ble-connection">BLE Status: ...</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-container">
|
||||||
|
<span class="status-indicator-wifi"></span>
|
||||||
|
<label id="status-wifi-client">Wifi Client: ...</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-container">
|
||||||
|
<span class="status-indicator-internet"></span>
|
||||||
|
<label id="status-internet">Internet: ...</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-container">
|
||||||
|
<label id="status-current-version">Curr Version: ...</label>
|
||||||
|
</div>
|
||||||
|
<div class="status-container">
|
||||||
|
<label id="status-new-version">New Version: ...</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-container">
|
||||||
|
<label id="ble-device-name">Device Name:</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-container">
|
||||||
|
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="btn-container">
|
||||||
|
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
|
||||||
|
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Area -->
|
||||||
|
<textarea id="logArea" readonly></textarea>
|
||||||
|
|
||||||
|
<div class="btn-container">
|
||||||
|
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
|
||||||
|
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
|
||||||
|
</div>
|
||||||
|
</div> <!-- /tab-upgrade -->
|
||||||
|
|
||||||
|
<div id="tab-wifi" class="tab-panel">
|
||||||
|
<h2 style="margin-top:0; font-size:18px;">WiFi Connection</h2>
|
||||||
|
<div class="input-container">
|
||||||
|
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
||||||
|
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
||||||
|
<div style="display: flex; align-items: center; gap: 5px;">
|
||||||
|
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
|
||||||
|
<label for="showPassword">Show Password</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-container wifi">
|
||||||
|
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /tab-wifi -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* ================= Constants & Packet Layout ================= */
|
||||||
|
const BLE_SERVER_NAME = "ATALIGHTS"; // Keep hardcoded per instruction (ignore external JSON)
|
||||||
|
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0";
|
||||||
|
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Control / status
|
||||||
|
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Logs / events
|
||||||
|
|
||||||
|
// Packet layout (mirrors firmware struct updateStatus)
|
||||||
|
// byte 0 : wifiStatus (enum)
|
||||||
|
// byte 1 : wifiOnline (bool)
|
||||||
|
// bytes2-5: wifiIP
|
||||||
|
// bytes6-8: currVersion (major,minor,patch)
|
||||||
|
// bytes9-11: newVersion (major,minor,patch)
|
||||||
|
// bytes12-31: wifiSSID (20 bytes, null padded)
|
||||||
|
const PACKET_LEN = 32;
|
||||||
|
const OFF_WIFI_STATUS = 0;
|
||||||
|
const OFF_WIFI_ONLINE = 1;
|
||||||
|
const OFF_WIFI_IP = 2;
|
||||||
|
const OFF_CURR_VER = 6;
|
||||||
|
const OFF_NEW_VER = 9;
|
||||||
|
const OFF_WIFI_SSID = 12;
|
||||||
|
|
||||||
|
const WIFI_STAT = { DISCONNECTED:0, BAD_CREDS:1, NO_AP:2, CONNECTED:3 };
|
||||||
|
const WIFI_STAT_TEXT = ["Disconnected","Bad Creds","No AP","Connected"];
|
||||||
|
const MAX_LOG_LINES = 400;
|
||||||
|
|
||||||
|
/* ================= State ================= */
|
||||||
|
let bleDevice=null, bleCharacteristic1=null, bleCharacteristic2=null;
|
||||||
|
let bleConnected=false;
|
||||||
|
const state = {
|
||||||
|
wifiStatus: WIFI_STAT.DISCONNECTED,
|
||||||
|
wifiOnline:false,
|
||||||
|
wifiIP:[0,0,0,0],
|
||||||
|
currVersion:[0,0,0],
|
||||||
|
newVersion:[0,0,0],
|
||||||
|
wifiSSID:""
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ================= Cached DOM ================= */
|
||||||
|
const el = {};
|
||||||
|
function cacheDom(){
|
||||||
|
el.bleIndicator = document.querySelector('.status-indicator-ble');
|
||||||
|
el.wifiIndicator = document.querySelector('.status-indicator-wifi');
|
||||||
|
el.internetIndicator = document.querySelector('.status-indicator-internet');
|
||||||
|
el.lblBle = document.getElementById('status-ble-connection');
|
||||||
|
el.lblWifi = document.getElementById('status-wifi-client');
|
||||||
|
el.lblInternet = document.getElementById('status-internet');
|
||||||
|
el.lblCurrVer = document.getElementById('status-current-version');
|
||||||
|
el.lblNewVer = document.getElementById('status-new-version');
|
||||||
|
el.inDeviceName = document.getElementById('input-DeviceName');
|
||||||
|
el.inSsid = document.getElementById('wifissid');
|
||||||
|
el.inPass = document.getElementById('wifipassword');
|
||||||
|
el.chkShowPass = document.getElementById('showPassword');
|
||||||
|
el.btnBleConnect = document.getElementById('bleConnectBtn');
|
||||||
|
el.btnCheckStatus = document.getElementById('checkStatusBtn');
|
||||||
|
el.btnCheckVersion = document.getElementById('checkVersionBtn');
|
||||||
|
el.btnStartUpgrade = document.getElementById('startUpgradeBtn');
|
||||||
|
el.btnWifiConnect = document.getElementById('wifiConnectBtn');
|
||||||
|
el.logArea = document.getElementById('logArea');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= Utilities ================= */
|
||||||
|
function logMessage(msg){
|
||||||
|
const lines = el.logArea.value.trim().length ? el.logArea.value.split(/\n/) : [];
|
||||||
|
lines.push(msg);
|
||||||
|
if(lines.length > MAX_LOG_LINES){
|
||||||
|
lines.splice(0, lines.length - MAX_LOG_LINES);
|
||||||
|
}
|
||||||
|
el.logArea.value = lines.join('\n') + '\n';
|
||||||
|
el.logArea.scrollTop = el.logArea.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a,b){
|
||||||
|
for(let i=0;i<3;i++){ if(a[i]>b[i]) return 1; if(a[i]<b[i]) return -1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorIndicator(elm, color){ if(elm) elm.style.backgroundColor = color; }
|
||||||
|
|
||||||
|
function ipToString(ip){ return ip.join('.'); }
|
||||||
|
|
||||||
|
/* ================= Packet Handling ================= */
|
||||||
|
function parsePacket(data){
|
||||||
|
if(data.length !== PACKET_LEN) return false;
|
||||||
|
state.wifiStatus = data[OFF_WIFI_STATUS];
|
||||||
|
if(state.wifiStatus > WIFI_STAT.CONNECTED) state.wifiStatus = WIFI_STAT.DISCONNECTED; // clamp
|
||||||
|
state.wifiOnline = !!data[OFF_WIFI_ONLINE];
|
||||||
|
state.wifiIP = Array.from(data.slice(OFF_WIFI_IP, OFF_WIFI_IP+4));
|
||||||
|
state.currVersion = Array.from(data.slice(OFF_CURR_VER, OFF_CURR_VER+3));
|
||||||
|
state.newVersion = Array.from(data.slice(OFF_NEW_VER, OFF_NEW_VER+3));
|
||||||
|
// Extract SSID (stop at first 0)
|
||||||
|
let rawSsidBytes = data.slice(OFF_WIFI_SSID, OFF_WIFI_SSID+20);
|
||||||
|
let zeroIndex = rawSsidBytes.indexOf(0);
|
||||||
|
if(zeroIndex >= 0) rawSsidBytes = rawSsidBytes.slice(0, zeroIndex);
|
||||||
|
state.wifiSSID = rawSsidBytes.length ? String.fromCharCode(...rawSsidBytes) : "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI(){
|
||||||
|
// BLE
|
||||||
|
el.lblBle.textContent = 'BLE Status: ' + (bleConnected ? 'Connected' : 'Disconnected');
|
||||||
|
colorIndicator(el.bleIndicator, bleConnected ? 'green' : 'gray');
|
||||||
|
|
||||||
|
// WiFi client
|
||||||
|
const statText = WIFI_STAT_TEXT[state.wifiStatus] || 'Unknown';
|
||||||
|
if(state.wifiStatus === WIFI_STAT.CONNECTED){
|
||||||
|
const ssidPart = state.wifiSSID ? ' SSID: '+state.wifiSSID : '';
|
||||||
|
el.lblWifi.textContent = 'Wifi Client: ' + statText + (state.wifiIP[0] ? ' ('+ipToString(state.wifiIP)+')':'' ) + ssidPart;
|
||||||
|
colorIndicator(el.wifiIndicator, 'green');
|
||||||
|
} else if(state.wifiStatus === WIFI_STAT.BAD_CREDS){
|
||||||
|
el.lblWifi.textContent = 'Wifi Client: Bad Credentials';
|
||||||
|
colorIndicator(el.wifiIndicator, 'orange');
|
||||||
|
} else if(state.wifiStatus === WIFI_STAT.NO_AP){
|
||||||
|
el.lblWifi.textContent = 'Wifi Client: AP Not Found';
|
||||||
|
colorIndicator(el.wifiIndicator, 'orange');
|
||||||
|
} else {
|
||||||
|
el.lblWifi.textContent = 'Wifi Client: ' + statText;
|
||||||
|
colorIndicator(el.wifiIndicator, 'gray');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internet
|
||||||
|
el.lblInternet.textContent = state.wifiOnline ? 'Online' : 'Offline';
|
||||||
|
colorIndicator(el.internetIndicator, state.wifiOnline ? 'green' : 'gray');
|
||||||
|
|
||||||
|
// Versions
|
||||||
|
el.lblCurrVer.textContent = state.currVersion[0] ? 'Curr Version: ' + state.currVersion.join('.') : 'Curr Version: ...';
|
||||||
|
el.lblNewVer.textContent = state.newVersion[0] ? 'New Version: ' + state.newVersion.join('.') : 'New Version: ...';
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
el.btnCheckStatus.disabled = !bleConnected;
|
||||||
|
el.btnWifiConnect.disabled = !bleConnected;
|
||||||
|
el.btnCheckVersion.disabled = !(bleConnected && state.wifiOnline);
|
||||||
|
const newerAvail = state.newVersion[0] && state.wifiOnline && compareVersions(state.newVersion, state.currVersion) > 0;
|
||||||
|
el.btnStartUpgrade.disabled = !newerAvail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= BLE Operations ================= */
|
||||||
|
async function connectToBle(){
|
||||||
|
if(!navigator.bluetooth){ logMessage('Web Bluetooth not supported.'); return; }
|
||||||
|
try{
|
||||||
|
bleDevice = await navigator.bluetooth.requestDevice({
|
||||||
|
filters:[{ name: el.inDeviceName.value || BLE_SERVER_NAME }],
|
||||||
|
optionalServices:[BLE_SERVICE_UUID]
|
||||||
|
});
|
||||||
|
const server = await bleDevice.gatt.connect();
|
||||||
|
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
||||||
|
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
|
||||||
|
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
|
||||||
|
await bleCharacteristic2.startNotifications();
|
||||||
|
bleCharacteristic2.addEventListener('characteristicvaluechanged', e => {
|
||||||
|
try{
|
||||||
|
const view = e.target.value; // DataView
|
||||||
|
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||||
|
|
||||||
|
// Debug info
|
||||||
|
let debugInfo = `Received ${bytes.length} bytes: `;
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
debugInfo += bytes[i].toString(16).padStart(2, '0') + ' ';
|
||||||
|
}
|
||||||
|
console.log(debugInfo);
|
||||||
|
|
||||||
|
// Try to decode as text
|
||||||
|
let txt = '';
|
||||||
|
try {
|
||||||
|
txt = new TextDecoder().decode(bytes);
|
||||||
|
// Remove null terminators and trim
|
||||||
|
const nullIdx = txt.indexOf('\0');
|
||||||
|
if (nullIdx !== -1) txt = txt.slice(0, nullIdx);
|
||||||
|
txt = txt.trim();
|
||||||
|
} catch (decodeErr) {
|
||||||
|
console.error('Text decode error:', decodeErr);
|
||||||
|
// Fallback to showing hex if text decode fails
|
||||||
|
txt = '[Binary data: ' + debugInfo + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show both raw and processed data in console
|
||||||
|
console.log('--> Raw bytes:', bytes);
|
||||||
|
console.log('--> As text:', txt);
|
||||||
|
|
||||||
|
// Log message with length info for debugging
|
||||||
|
logMessage(`--> (${bytes.length} bytes) ${txt}`);
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Processing error', err);
|
||||||
|
logMessage('--> Error processing message: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bleConnected=true;
|
||||||
|
bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
|
||||||
|
const connectedName = bleDevice.name || el.inDeviceName.value || 'Device';
|
||||||
|
logMessage('Connected to ' + connectedName);
|
||||||
|
const nameLabel = document.getElementById('ble-device-name');
|
||||||
|
if(nameLabel){ nameLabel.textContent = 'Device Name: ' + connectedName; }
|
||||||
|
await readPacket();
|
||||||
|
updateUI();
|
||||||
|
}catch(err){
|
||||||
|
logMessage( err.message.includes('cancel') ? 'Connection cancelled.' : ('Connection failed: '+err.message) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnect(){
|
||||||
|
bleConnected=false; updateUI(); logMessage('BLE disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPacket(msg){
|
||||||
|
if(!bleCharacteristic1) return;
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
for(let attempt=0; attempt<3; attempt++){
|
||||||
|
try{ await bleCharacteristic1.writeValueWithResponse(enc.encode(msg)); return true; }
|
||||||
|
catch(e){ if(attempt===2) logMessage('Send failed: '+e.message); else await delay(1000); }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPacket(){
|
||||||
|
if(!bleCharacteristic1) return false;
|
||||||
|
for(let attempt=0; attempt<3; attempt++){
|
||||||
|
try{
|
||||||
|
const val = await bleCharacteristic1.readValue();
|
||||||
|
const data = new Uint8Array(val.buffer);
|
||||||
|
if(parsePacket(data)) return true; else { logMessage('Packet parse failed (len='+data.length+')'); return false; }
|
||||||
|
}catch(e){ if(attempt===2) logMessage('Read failed: '+e.message); else await delay(1000); }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= Actions ================= */
|
||||||
|
async function wifiConnect(){
|
||||||
|
const ssid = el.inSsid.value.trim();
|
||||||
|
const pw = el.inPass.value;
|
||||||
|
if(!ssid || !pw){ alert('Enter SSID & password'); return; }
|
||||||
|
logMessage('Sending WiFi credentials...');
|
||||||
|
el.btnWifiConnect.disabled = true; // prevent multiple submissions while polling
|
||||||
|
await sendPacket('wifi-connect {"ssid":"'+ssid+'","pass":"'+pw+'"} ');
|
||||||
|
// Poll for status for up to 15s
|
||||||
|
const start = Date.now();
|
||||||
|
while(Date.now()-start < 15000){
|
||||||
|
await delay(1000);
|
||||||
|
await readPacket();
|
||||||
|
updateUI();
|
||||||
|
if(state.wifiStatus === WIFI_STAT.CONNECTED){
|
||||||
|
logMessage('WiFi Connected: '+ipToString(state.wifiIP));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(state.wifiStatus === WIFI_STAT.BAD_CREDS){ logMessage('WiFi Error: Bad Credentials'); break; }
|
||||||
|
if(state.wifiStatus === WIFI_STAT.NO_AP){ logMessage('WiFi Error: AP Not Found'); break; }
|
||||||
|
}
|
||||||
|
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ logMessage('WiFi connect attempt finished with status: '+WIFI_STAT_TEXT[state.wifiStatus]); }
|
||||||
|
if(!bleConnected) return; // keep disabled if BLE disconnected during process
|
||||||
|
// Re-enable for retry unless connected
|
||||||
|
if(state.wifiStatus !== WIFI_STAT.CONNECTED){ el.btnWifiConnect.disabled = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkStatus(){ if(await readPacket()) updateUI(); }
|
||||||
|
|
||||||
|
async function checkVersion(){
|
||||||
|
el.btnCheckVersion.disabled = true;
|
||||||
|
logMessage('Checking for new version...');
|
||||||
|
await sendPacket('version-check');
|
||||||
|
const start = Date.now();
|
||||||
|
while(Date.now()-start < 15000){
|
||||||
|
await delay(750);
|
||||||
|
await readPacket();
|
||||||
|
if(state.newVersion[0]){ logMessage('Latest version: '+state.newVersion.join('.')); break; }
|
||||||
|
}
|
||||||
|
if(!state.newVersion[0]) logMessage('No new version info received');
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startUpgrade(){
|
||||||
|
if(el.btnStartUpgrade.disabled) return;
|
||||||
|
logMessage('Starting upgrade...');
|
||||||
|
el.btnStartUpgrade.disabled = true;
|
||||||
|
await sendPacket('upgrade-start');
|
||||||
|
// Progress will arrive via characteristic2 logs
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= Helpers ================= */
|
||||||
|
const delay = ms => new Promise(r=>setTimeout(r,ms));
|
||||||
|
|
||||||
|
function togglePasswordVisibility(){ el.inPass.type = el.chkShowPass.checked ? 'text' : 'password'; }
|
||||||
|
|
||||||
|
function init(){
|
||||||
|
cacheDom();
|
||||||
|
el.inDeviceName.value = BLE_SERVER_NAME;
|
||||||
|
el.chkShowPass.addEventListener('change', togglePasswordVisibility);
|
||||||
|
// Inline onclicks already wired; ensure functions are in scope
|
||||||
|
// Tab switching
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const id = btn.getAttribute('data-tab');
|
||||||
|
const panel = document.getElementById(id);
|
||||||
|
if(panel) panel.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
updateUI();
|
||||||
|
logMessage('Ready. Enter device name or use default and press Connect.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose functions for inline handlers
|
||||||
|
window.connectToBle = connectToBle;
|
||||||
|
window.checkStatus = checkStatus;
|
||||||
|
window.checkVersion = checkVersion;
|
||||||
|
window.startUpgrade = startUpgrade;
|
||||||
|
window.wifiConnect = wifiConnect;
|
||||||
|
window.togglePasswordVisibility = togglePasswordVisibility;
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', init);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
249
temporary/my_buzzer.cpp
Normal file
249
temporary/my_buzzer.cpp
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
#include "my_buzzer.h"
|
||||||
|
#include <FS.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
|
||||||
|
#include <anyrtttl.h>
|
||||||
|
#include <binrtttl.h>
|
||||||
|
#include <pitches.h>
|
||||||
|
|
||||||
|
#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<int>(tag, obj, "cycles", 1, 100, 1);
|
||||||
|
buzzTune[tuneIndex].pause = jsonConstrain<int>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -183,7 +183,7 @@ void Wifi_Init() {
|
|||||||
ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str());
|
ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str());
|
||||||
|
|
||||||
// Start the WiFi task
|
// 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){
|
void Wifi_Load_Settings(String path){
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user