9-29 commit
This commit is contained in:
parent
0040c64da8
commit
660fa5f1c7
14
.clang-format
Normal file
14
.clang-format
Normal file
@ -0,0 +1,14 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
BraceWrapping:
|
||||
AfterFunction: false
|
||||
AfterControlStatement: false
|
||||
AfterClass: false
|
||||
AfterEnum: false
|
||||
AfterStruct: false
|
||||
AfterNamespace: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
10
ToDo.txt
10
ToDo.txt
@ -1,10 +0,0 @@
|
||||
|
||||
1) OTA switch to Minio
|
||||
|
||||
2) Global variable ( Circular or Linear LEDS)
|
||||
|
||||
3) Long Hold to switch
|
||||
|
||||
4) Fix white flasing at booth
|
||||
|
||||
5)
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"profile": "big-roam",
|
||||
"mode": "roamer",
|
||||
"limited-mode": false,
|
||||
"button": 0,
|
||||
"buttons":
|
||||
[
|
||||
@ -58,7 +60,7 @@
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0,
|
||||
"min": 0.0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
@ -66,10 +68,10 @@
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1,
|
||||
"min": 0.0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
@ -106,9 +108,9 @@
|
||||
"size": 168,
|
||||
"chip": "SK6812",
|
||||
"rgb-order": "rgb",
|
||||
"shift":-5,
|
||||
"shift":-42,
|
||||
"offset": 0,
|
||||
"bright": 200,
|
||||
"bright": 240,
|
||||
"power-div": 0,
|
||||
"i2s-ch": 0,
|
||||
"core": 1
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "custom",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
@ -54,14 +56,26 @@
|
||||
"ramp-lights":
|
||||
[
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"mode": 0,
|
||||
"buttons":
|
||||
"id": "flare-posh",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
"en": true
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
@ -1,124 +0,0 @@
|
||||
{
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
"en": true
|
||||
},
|
||||
{
|
||||
"en": true
|
||||
},
|
||||
{
|
||||
"en": true
|
||||
}
|
||||
],
|
||||
"pwmout":
|
||||
[
|
||||
{
|
||||
"en": true,
|
||||
"freq": 250,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"default": 0,
|
||||
"vision": false,
|
||||
"deltarate": 0
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"freq": 250,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"default": 0,
|
||||
"vision": false,
|
||||
"deltarate": 0
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"freq": 250,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"default": 0,
|
||||
"vision": true,
|
||||
"deltarate": 0
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"freq": 250,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"default": 0,
|
||||
"vision": true,
|
||||
"deltarate": 0
|
||||
}
|
||||
],
|
||||
"ramp-lights":
|
||||
[
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
"en": false,
|
||||
"height": 64,
|
||||
"width": 128
|
||||
},
|
||||
"t-sensor": {
|
||||
"en": true,
|
||||
"addr": 72,
|
||||
"sp1": 85.0,
|
||||
"fan-pwr1": 50.0,
|
||||
"sp2": 90.0,
|
||||
"fan-pwr2": 100.0,
|
||||
"hyst": 1.0,
|
||||
"relay": 3,
|
||||
"interval": 5000
|
||||
},
|
||||
"adc": {
|
||||
"ain1_factor": 1.0
|
||||
},
|
||||
"status-led":{
|
||||
"interval": 250
|
||||
},
|
||||
"strips":
|
||||
[
|
||||
{
|
||||
"en": true,
|
||||
"size": 138,
|
||||
"chip": "SK6812",
|
||||
"rgb-order": "rgb",
|
||||
"shift":-27,
|
||||
"offset": 0,
|
||||
"power-div": 0,
|
||||
"i2s-ch": 0,
|
||||
"core": 1
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"size": 30,
|
||||
"chip": "SK6812",
|
||||
"rgb-order": "rgb",
|
||||
"shift":-27,
|
||||
"offset": 0,
|
||||
"power-div": 0,
|
||||
"i2s-ch": 0,
|
||||
"core": 1
|
||||
}
|
||||
],
|
||||
"rx433": {
|
||||
"en": "true"
|
||||
},
|
||||
"tx433": {
|
||||
"en": "true"
|
||||
},
|
||||
"ble":{
|
||||
"en": true,
|
||||
"name": "Ata_Lights",
|
||||
"core": 1
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
{
|
||||
"id": "m-lumia",
|
||||
"limited-mode": false,
|
||||
"mode": 0,
|
||||
"buttons":
|
||||
[
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"id": "m1",
|
||||
"mode": 0,
|
||||
"buttons":
|
||||
[
|
||||
@ -56,12 +57,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "marquee",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "orig-roam",
|
||||
"mode": "roamer",
|
||||
"limited-mode": false,
|
||||
"button": 0,
|
||||
"buttons":
|
||||
[
|
||||
@ -58,19 +60,21 @@
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0,
|
||||
"min": 0.0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1,
|
||||
"min": 0.0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "spectra",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "sport",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"profile": "stik",
|
||||
"mode": "stik",
|
||||
"buttons":
|
||||
[
|
||||
@ -56,13 +57,25 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
}
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
"en": false,
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"profile": "testbooth",
|
||||
"mode": 0,
|
||||
"buttons":
|
||||
[
|
||||
@ -54,24 +55,26 @@
|
||||
"ramp-lights":
|
||||
[
|
||||
{
|
||||
"en": false,
|
||||
"relay-index": 0,
|
||||
"button-index": 0,
|
||||
"min": 5.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": false,
|
||||
"relay-index": 1,
|
||||
"button-index": 1,
|
||||
"min": 5.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
"en": true,
|
||||
"relay-index": 1,
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"id": "xl-lumia",
|
||||
"mode": 0,
|
||||
"limited-mode": false,
|
||||
"buttons":
|
||||
[
|
||||
{
|
||||
@ -56,12 +58,24 @@
|
||||
{
|
||||
"en": true,
|
||||
"relay-index": 0,
|
||||
"button-index": 0
|
||||
"button-index": 0,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
},
|
||||
{
|
||||
"en": true,
|
||||
"en": false,
|
||||
"relay-index": 1,
|
||||
"button-index": 1
|
||||
"button-index": 1,
|
||||
"min": 1.0,
|
||||
"max": 100.0,
|
||||
"step": 1.5,
|
||||
"rate": 30.0,
|
||||
"skip-count": 5,
|
||||
"vision": true
|
||||
}
|
||||
],
|
||||
"oled": {
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ATALIGHTS",
|
||||
"name": "SP110E-ATA",
|
||||
"unique": true,
|
||||
"lights-service": "FFE0",
|
||||
"lights-char": "FFE1",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"boardfile": "/boards/board15.json",
|
||||
"configfile": "/booths/roamer-big.json"
|
||||
"configfile": "/booths/big-roam.json"
|
||||
}
|
||||
@ -589,67 +589,67 @@ IMPORTANT NOTES:
|
||||
|
||||
try{
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
filters:[{ namePrefix: el.inDeviceName.value || BLE_SERVER_NAME }],
|
||||
optionalServices:[BLE_SERVICE_UUID]
|
||||
});
|
||||
filters:[{ namePrefix: 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();
|
||||
let prevProgressFound = false;
|
||||
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);
|
||||
txt = new TextDecoder().decode(e.target.value);
|
||||
// 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]';
|
||||
}
|
||||
|
||||
// Show both raw and processed data in console
|
||||
//console.log('--> Raw bytes:', bytes);
|
||||
//console.log('--> As text:', txt);
|
||||
|
||||
//logMessage(`--> (${bytes.length} bytes) ${txt}`);
|
||||
// progress messages handling variable captured from outer scope
|
||||
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();
|
||||
let prevProgressFound = false;
|
||||
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);
|
||||
txt = new TextDecoder().decode(e.target.value);
|
||||
// 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]';
|
||||
}
|
||||
|
||||
// Show both raw and processed data in console
|
||||
//console.log('--> Raw bytes:', bytes);
|
||||
//console.log('--> As text:', txt);
|
||||
|
||||
//logMessage(`--> (${bytes.length} bytes) ${txt}`);
|
||||
// progress messages handling variable captured from outer scope
|
||||
|
||||
if(prevProgressFound && txt.includes('progress')){
|
||||
logMessage(`${txt}`, true); // overwrite last line for progress updates
|
||||
}
|
||||
else{
|
||||
logMessage(`${txt}`);
|
||||
prevProgressFound = false; // reset if current message isn't progress
|
||||
}
|
||||
if(prevProgressFound && txt.includes('progress')){
|
||||
logMessage(`${txt}`, true); // overwrite last line for progress updates
|
||||
}
|
||||
else{
|
||||
logMessage(`${txt}`);
|
||||
prevProgressFound = false; // reset if current message isn't progress
|
||||
}
|
||||
|
||||
if(txt.includes('progress')){
|
||||
prevProgressFound = true;
|
||||
}
|
||||
if(txt.includes('progress')){
|
||||
prevProgressFound = true;
|
||||
}
|
||||
|
||||
|
||||
} catch(err) {
|
||||
console.error('Processing error', err);
|
||||
logMessage('--> Error processing message: ' + err.message);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error('Processing error', err);
|
||||
logMessage('--> Error processing message: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
bleConnected=true;
|
||||
|
||||
1324
data/www/usercfg.html
Normal file
1324
data/www/usercfg.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/SP110E Info.xlsx
Normal file
BIN
docs/SP110E Info.xlsx
Normal file
Binary file not shown.
24
docs/ToDo.txt
Normal file
24
docs/ToDo.txt
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
Board to do:
|
||||
2) make sure no fixed indexes to pwmOutput and other peripherals
|
||||
|
||||
3) Implement sending packet with current values to ble appearance
|
||||
|
||||
4) figure out how to compare profile comparison before savoing to system.json
|
||||
|
||||
5) made mode string instead of number.
|
||||
|
||||
6) Comments on json file Enable?
|
||||
#define ARDUINOJSON_ENABLE_COMMENTS 1
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
permits: // or /* lsslslslslsl */
|
||||
|
||||
|
||||
7) Use same struct of setting for to and from in ble
|
||||
|
||||
8) learn about Blob in BLE
|
||||
|
||||
######### Configuration Page ############
|
||||
done
|
||||
|
||||
BIN
docs/~$SP110E Info.xlsx
Normal file
BIN
docs/~$SP110E Info.xlsx
Normal file
Binary file not shown.
@ -2,9 +2,6 @@
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../esp32s3_module_8mb"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@ -5,13 +5,21 @@
|
||||
#include "ColorPalettes.h"
|
||||
#include "PWM_Output.h"
|
||||
|
||||
#define PIXEL_INDEX -3
|
||||
#define SHIFT_INDEX -3
|
||||
#define SOLID_COLOR_INDEX -2
|
||||
#define OFF_INDEX -1
|
||||
|
||||
#define WITH_GAPS true
|
||||
#define NO_GAPS false
|
||||
|
||||
enum CHIP_TYPE {
|
||||
CHIP_WS2812B = 0,
|
||||
CHIP_SK6812,
|
||||
CHIP_WS2811,
|
||||
CHIP_WS2815,
|
||||
CHIP_UNKNOWN
|
||||
};
|
||||
|
||||
extern uint32_t whiteTimeout;
|
||||
|
||||
typedef struct {
|
||||
@ -39,19 +47,53 @@ typedef struct {
|
||||
int shift;
|
||||
int offset;
|
||||
int powerDiv;
|
||||
int effSize;
|
||||
//int effSize;
|
||||
uint8_t bright;
|
||||
uint8_t i2sCh;
|
||||
uint8_t core;
|
||||
uint8_t pin;
|
||||
}LEDSTRIP_SETTINGS;
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t cmd;
|
||||
uint8_t limitedMode;
|
||||
float temperature;
|
||||
float vIn;
|
||||
char profile[10];
|
||||
|
||||
uint8_t ledChipset1;
|
||||
char rgbOrder1[4]; // 3 chars + null terminator
|
||||
uint8_t ledCount1;
|
||||
uint8_t ledShift1;
|
||||
uint8_t ledBrightness1;
|
||||
|
||||
uint8_t ledChipset2;
|
||||
char rgbOrder2[4]; // 3 chars + null terminator
|
||||
uint8_t ledCount2;
|
||||
uint8_t ledShift2;
|
||||
uint8_t ledBrightness2;
|
||||
|
||||
uint8_t frontLightMin;
|
||||
uint8_t frontLightMax;
|
||||
uint8_t rearLightMin;
|
||||
uint8_t rearLightMax;
|
||||
uint8_t fanLowerTemp;
|
||||
uint8_t fanUpperTemp;
|
||||
} USER_SETTINGS;
|
||||
|
||||
enum BOOTH_PROFILE {
|
||||
PROFILE_CUSTOM=0,
|
||||
PROFILE_ROAMER,
|
||||
PROFILE_STIK,
|
||||
PROFILE_COUNT
|
||||
};
|
||||
|
||||
extern LEDSTRIP_SETTINGS ledSettings[2];
|
||||
|
||||
|
||||
void RGB_Lights_Control_Task(void *parameters);
|
||||
|
||||
void Init_RGB_Lights_Task(void);
|
||||
void Init_RGB_Lights_Task(bool upgrade);
|
||||
|
||||
void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder, const String& chipType, uint8_t bright);
|
||||
|
||||
@ -72,6 +114,8 @@ void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack);
|
||||
|
||||
void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src);
|
||||
|
||||
void SetAndSaveUserSettings(USER_SETTINGS &userSettings);
|
||||
|
||||
|
||||
//void Init_Ramp_Front_Light_Task(void);
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "AppVersion.h"
|
||||
|
||||
|
||||
//#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/"
|
||||
#define DEFAULT_MANIFEST_URL "https://minio.boothwizard.com/boothifier/latest/"
|
||||
#define BUFFER_SIZE 2048 // Reduced from 4096 to use less memory
|
||||
@ -171,6 +172,7 @@ class AppUpdater {
|
||||
fs::FS& fileSystem;
|
||||
UpdateStatus status;
|
||||
std::unique_ptr<uint8_t[]> downloadBuffer;
|
||||
size_t downloadBufferSize = 0; /**< Actual size allocated for downloadBuffer */
|
||||
bool updateAvailable = false;
|
||||
UpdateMode updateMode = UpdateMode::UPDATE_BOTH; // Default to updating both files and firmware
|
||||
|
||||
@ -223,7 +225,7 @@ void firmwareUpdateTask(void* param);
|
||||
|
||||
void startFirmwareUpdateTask(AsyncEventSource* eventSrc);
|
||||
|
||||
void loadUpdateJson(void);
|
||||
bool loadUpdateJson(void);
|
||||
|
||||
void updateProgress(AppUpdater::UpdateStatus status, int percentage, const char* message);
|
||||
|
||||
@ -243,3 +245,5 @@ void setUpdateModeFilesOnly();
|
||||
void setUpdateModeFirmwareOnly();
|
||||
void setUpdateModeBoth();
|
||||
|
||||
|
||||
|
||||
|
||||
@ -19,6 +19,11 @@
|
||||
#define SET_SPEED 0x03
|
||||
#define SET_AUTO_MODE 0x06
|
||||
|
||||
// My custom commands
|
||||
#define SET_USER_SETTINGS 0xB0
|
||||
#define SET_SHIFT 0xB1
|
||||
|
||||
|
||||
// Initializes the BLE server, services, and advertising
|
||||
void Init_BLE_SP110E(NimBLEServer* pServer);
|
||||
|
||||
|
||||
@ -28,9 +28,9 @@ const COLOR_PACK colorPack_RED_WHITE PROGMEM = { 2, { CRGB::Red, CRGB::White } }
|
||||
const COLOR_PACK colorPack_ORANGE_WHITE PROGMEM = { 2, { CRGB::DarkOrange, CRGB::White } };
|
||||
const COLOR_PACK colorPack_GREEN_WHITE PROGMEM = { 2, { CRGB::Green, CRGB::White } };
|
||||
const COLOR_PACK colorPack_BLUE_WHITE PROGMEM = { 2, { CRGB::Blue, CRGB::White } };
|
||||
const COLOR_PACK colorPack_PINK_WHITE PROGMEM = { 2, { CRGB::Pink, CRGB::White } };
|
||||
const COLOR_PACK colorPack_MAGENTA_WHITE PROGMEM = { 2, { CRGB::Magenta, CRGB::White } };
|
||||
const COLOR_PACK colorPack_PURPLE_WHITE PROGMEM = { 2, { CRGB::DarkViolet, CRGB::White } };
|
||||
const COLOR_PACK colorPack_PURPLE_PINK PROGMEM = { 2, { CRGB::DarkViolet, CRGB::Pink } };
|
||||
const COLOR_PACK colorPack_PURPLE_MAGENTA PROGMEM = { 2, { CRGB::DarkViolet, CRGB::Magenta } };
|
||||
const COLOR_PACK colorPack_PURPLE_YELLOW PROGMEM = { 2, { CRGB::Purple, CRGB::Yellow } };
|
||||
|
||||
|
||||
@ -45,11 +45,10 @@ const COLOR_PACK colorPack_BLUE_RED PROGMEM = { 2, { CRGB::Blue, CRGB::Red } };
|
||||
const COLOR_PACK colorPack_ORANGE_BLUE PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Blue } };
|
||||
const COLOR_PACK colorPack_RED_GREEN PROGMEM = { 2, { CRGB::Red, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_CYAN_RED PROGMEM = { 2, { CRGB::Cyan, CRGB::Red } };
|
||||
const COLOR_PACK colorPack_MAGENTA_GREEN PROGMEM = { 2, { CRGB::Magenta, CRGB::Green } };
|
||||
|
||||
// Warm/cool combinations
|
||||
const COLOR_PACK colorPack_ORANGE_CYAN PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Cyan } };
|
||||
const COLOR_PACK colorPack_PINK_GREEN PROGMEM = { 2, { CRGB::Pink, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_MAGENTA_GREEN PROGMEM = { 2, { CRGB::Magenta, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_VIOLET_LIME PROGMEM = { 2, { CRGB::DarkViolet, CRGB::Lime } };
|
||||
|
||||
// Analogous combinations
|
||||
@ -67,28 +66,28 @@ const COLOR_PACK double_colorPacks[] PROGMEM = {
|
||||
colorPack_RED_WHITE,
|
||||
colorPack_RED_YELLOW,
|
||||
colorPack_BLUE_WHITE,
|
||||
colorPack_PINK_WHITE,
|
||||
colorPack_GREEN_WHITE,
|
||||
colorPack_PURPLE_WHITE,
|
||||
colorPack_GREEN_YELLOW,
|
||||
colorPack_PURPLE_PINK,
|
||||
colorPack_CYAN_RED,
|
||||
|
||||
colorPack_MAGENTA_WHITE,
|
||||
colorPack_GREEN_WHITE,
|
||||
colorPack_ORANGE_WHITE,
|
||||
colorPack_PURPLE_MAGENTA,
|
||||
colorPack_GREEN_YELLOW,
|
||||
// 9 pack
|
||||
|
||||
|
||||
colorPack_PURPLE_PINK,
|
||||
colorPack_CYAN_RED,
|
||||
colorPack_PURPLE_MAGENTA,
|
||||
colorPack_PURPLE_YELLOW,
|
||||
|
||||
colorPack_BLUE_YELLOW,
|
||||
|
||||
colorPack_BLUE_GREEN,
|
||||
colorPack_BLUE_RED,
|
||||
colorPack_ORANGE_BLUE,
|
||||
// 16 pack
|
||||
|
||||
|
||||
colorPack_RED_GREEN,
|
||||
|
||||
colorPack_MAGENTA_GREEN,
|
||||
colorPack_ORANGE_CYAN,
|
||||
colorPack_PINK_GREEN,
|
||||
colorPack_VIOLET_LIME,
|
||||
colorPack_RED_ORANGE,
|
||||
colorPack_BLUE_PURPLE,
|
||||
@ -101,12 +100,12 @@ const COLOR_PACK double_colorPacks[] PROGMEM = {
|
||||
|
||||
// Sectors
|
||||
|
||||
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } };
|
||||
const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::DarkOrange, CRGB::LightYellow, CRGB::Green, CRGB::Blue, CRGB::DarkViolet, CRGB::MediumVioletRed } };
|
||||
// Triple Color Packs, Common Flags
|
||||
const COLOR_PACK colorPack_RED_WHITE_BLUE PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } };
|
||||
const COLOR_PACK colorPack_RED_WHITE_GREEN PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_RED_YELLOW_BLUE PROGMEM = { 3, { CRGB::Red, CRGB::Yellow, CRGB::Blue } };
|
||||
const COLOR_PACK colorPack_RED_ORANGE_YELLOW PROGMEM = { 3, { CRGB::Red, CRGB::DarkOrange, CRGB::Yellow } };
|
||||
const COLOR_PACK colorPack_RED_ORANGE_YELLOW PROGMEM = { 3, { CRGB::Red, CRGB::DarkOrange, CRGB::Yellow } };
|
||||
const COLOR_PACK colorPack_RED_YELLOW_GREEN PROGMEM = { 3, { CRGB::Red, CRGB::Yellow, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_RED_PURPLE_BLUE PROGMEM = { 3, { CRGB::Red, CRGB::Purple, CRGB::Blue } };
|
||||
|
||||
@ -114,13 +113,11 @@ const COLOR_PACK colorPack_GREEN_WHITE_RED PROGMEM = { 3, { CRGB::Green, CRGB::
|
||||
const COLOR_PACK colorPack_GREEN_WHITE_ORANGE PROGMEM = { 3, { CRGB::Green, CRGB::White, CRGB::DarkOrange } };
|
||||
const COLOR_PACK colorPack_GREEN_WHITE_BLUE PROGMEM = { 3, { CRGB::Green, CRGB::White, CRGB::Blue } };
|
||||
|
||||
|
||||
const COLOR_PACK colorPack_BLUE_WHITE_GREEN PROGMEM = { 3, { CRGB::Blue, CRGB::White, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_BLUE_YELLOW_GREEN PROGMEM = { 3, { CRGB::Blue, CRGB::Yellow, CRGB::Green } };
|
||||
const COLOR_PACK colorPack_BLUE_YELLOW_RED PROGMEM = { 3, { CRGB::Blue, CRGB::Yellow, CRGB::Red } };
|
||||
|
||||
// Additional triple color combinations
|
||||
const COLOR_PACK colorPack_PURPLE_PINK_CYAN PROGMEM = { 3, { CRGB::Purple, CRGB::Pink, CRGB::Cyan } };
|
||||
const COLOR_PACK colorPack_MAGENTA_WHITE_CYAN PROGMEM = { 3, { CRGB::Magenta, CRGB::White, CRGB::Cyan } };
|
||||
const COLOR_PACK colorPack_ORANGE_YELLOW_LIME PROGMEM = { 3, { CRGB::DarkOrange, CRGB::Yellow, CRGB::Lime } };
|
||||
const COLOR_PACK colorPack_MAGENTA_YELLOW_CYAN PROGMEM = { 3, { CRGB::Magenta, CRGB::Yellow, CRGB::Cyan } };
|
||||
const COLOR_PACK colorPack_LIME_CYAN_MAGENTA PROGMEM = { 3, { CRGB::Lime, CRGB::Cyan, CRGB::Magenta } };
|
||||
@ -149,27 +146,30 @@ const COLOR_PACK tripple_colorPacks[] PROGMEM = {
|
||||
colorPack_GREEN_WHITE_ORANGE,
|
||||
colorPack_BLUE_YELLOW_GREEN,
|
||||
colorPack_RED_GREEN_BLUE,
|
||||
colorPack_PURPLE_PINK_CYAN,
|
||||
colorPack_MAGENTA_WHITE_CYAN,
|
||||
colorPack_YELLOW_ORANGE_RED,
|
||||
colorPack_BLUE_CYAN_LIME,
|
||||
colorPack_PINK_PURPLE_MAGENTA,
|
||||
|
||||
colorPack_RED_YELLOW_BLUE,
|
||||
// 9 pack
|
||||
|
||||
|
||||
colorPack_RED_ORANGE_YELLOW,
|
||||
colorPack_RED_YELLOW_GREEN,
|
||||
colorPack_RED_PURPLE_BLUE,
|
||||
colorPack_GREEN_WHITE_RED,
|
||||
colorPack_GREEN_WHITE_BLUE,
|
||||
colorPack_BLUE_WHITE_GREEN,
|
||||
colorPack_BLUE_YELLOW_RED,
|
||||
colorPack_GREEN_CYAN_BLUE,
|
||||
// 16 pack
|
||||
|
||||
|
||||
colorPack_ORANGE_YELLOW_LIME,
|
||||
colorPack_MAGENTA_YELLOW_CYAN,
|
||||
colorPack_LIME_CYAN_MAGENTA,
|
||||
colorPack_RED_ORANGE_PINK,
|
||||
colorPack_PURPLE_BLUE_CYAN,
|
||||
colorPack_GREEN_CYAN_BLUE,
|
||||
colorPack_YELLOW_PURPLE_ORANGE,
|
||||
colorPack_PINK_LIME_PURPLE
|
||||
|
||||
};
|
||||
|
||||
const COLOR_PACK colorPack_grad_blueish PROGMEM = { 3, { CRGB::Blue, CRGB::Cyan, CRGB::Green } };
|
||||
@ -200,7 +200,7 @@ const COLOR_PACK colorPack_Single_Viloet PROGMEM = { 1, { CRGB::DarkViolet } };
|
||||
const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } };
|
||||
const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } };
|
||||
|
||||
// 9 Single Color Packs
|
||||
// 8 Single Color Packs
|
||||
const COLOR_PACK single_colorPacks[] PROGMEM = {
|
||||
colorPack_Single_White,
|
||||
colorPack_Single_Red,
|
||||
|
||||
@ -38,6 +38,8 @@ class PWM_Output {
|
||||
|
||||
float standardFactor;
|
||||
float visionFactor;
|
||||
int8_t pin;
|
||||
bool initialized = false;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ private:
|
||||
float currentValue;
|
||||
bool IsOn = false;
|
||||
RAMP_STATE rampState;
|
||||
int tickCount;
|
||||
int tickCount = 0;
|
||||
|
||||
void tick();
|
||||
void singleClick();
|
||||
|
||||
@ -60,3 +60,5 @@ void Log_CPU_Load(void);
|
||||
|
||||
void print_task_watermarks(void);
|
||||
|
||||
float updateLowpass(float currentValue, float newValue, float alpha);
|
||||
|
||||
|
||||
@ -9,6 +9,10 @@ extern SYS_SETTINGS sys_settings;
|
||||
extern PWM_Output *pwmOutputs[4];
|
||||
extern RAMP_LIGHT *rampLight1;
|
||||
extern RAMP_LIGHT *rampLight2;
|
||||
extern String booth_file_path;
|
||||
extern float PowerVin;
|
||||
extern float PowerVinAlpha;
|
||||
extern float prevPowerVin;
|
||||
|
||||
void Init_ADC(void);
|
||||
float readBoardInputVoltage(void);
|
||||
|
||||
@ -23,7 +23,7 @@ void handleGET_Query(AsyncWebServerRequest *request);
|
||||
|
||||
void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
|
||||
void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&));
|
||||
bool sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&));
|
||||
String fileManagerHtmlProcessor(const String& var);
|
||||
String HomeHtmlProcessor(const String& var);
|
||||
String listDirAsHtml(String directoryList[], int count);
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
#include <FastLED.h>
|
||||
#include "ATALights.h"
|
||||
|
||||
|
||||
enum LED_CHIPSET { LED_CHIPSET_WS2812B=0, LED_CHIPSET_SK6812=1, LED_CHIPSET_WS2811_400=2, LED_CHIPSET_WS2815=3, LED_CHIPSET_NONE=255 };
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
}BTN_SETTINGS;
|
||||
@ -79,6 +82,8 @@ typedef struct {
|
||||
|
||||
enum BOOTH_MODE { BOOTH_MODE_NONE=0, BOOTH_MODE_ROAMER=1, BOOTH_MODE_STIK=2 };
|
||||
typedef struct {
|
||||
String profile;
|
||||
bool limitedMode;
|
||||
BOOTH_MODE mode;
|
||||
BOARD_PINS boardPins;
|
||||
BTN_SETTINGS btnSettings[3];
|
||||
|
||||
@ -24,6 +24,7 @@ lib_deps =
|
||||
jeremycole/I2C Temperature Sensors derived from the LM75 @ ^1.0.3
|
||||
mathertel/OneButton @ ^2.6.1
|
||||
h2zero/NimBLE-Arduino @ ^1.4.1
|
||||
#h2zero/NimBLE-Arduino @ ^2.3.6
|
||||
adafruit/Adafruit SSD1306 @ ^2.5.7
|
||||
fastled/FastLED @ ^3.9.4
|
||||
marian-craciunescu/ESP32Ping@^1.7
|
||||
@ -35,6 +36,11 @@ build_flags =
|
||||
#-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
|
||||
-D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_INFO
|
||||
-D CONFIG_ARDUHAL_LOG_COLORS=1
|
||||
#-Os
|
||||
#-ffunction-sections
|
||||
#-fdata-sections
|
||||
#-Wl,--gc-sections
|
||||
|
||||
upload_port = COM5
|
||||
debug_init_break = tbreak setup
|
||||
monitor_port = COM5
|
||||
|
||||
@ -9,16 +9,16 @@
|
||||
#include "system.h"
|
||||
#include "ColorPalettes.h"
|
||||
#include "global.h"
|
||||
//#include <crgb.h>
|
||||
#include "PWM_Output.h"
|
||||
#include "my_device.h"
|
||||
#include "system.h"
|
||||
#include "my_device.h"
|
||||
#include "LittleFS.h"
|
||||
|
||||
#define FASTLED_CORE 0
|
||||
static const char* tag = "strips";
|
||||
|
||||
uint32_t whiteTimeout = 0;
|
||||
//uint32_t whiteTimeout = 0;
|
||||
|
||||
TaskHandle_t Animation_Task_Handle;
|
||||
TaskHandle_t Ramp_Front_Light_Task_Handle;
|
||||
@ -27,7 +27,13 @@ volatile bool AnimationLooping = false;
|
||||
ANIM_EVENT prevAnimEvent = {0};
|
||||
QueueHandle_t animationQueue = xQueueCreate( 4, sizeof( ANIM_EVENT ) );
|
||||
|
||||
bool fillAnimationActive = false;
|
||||
bool upgradeMode = false;
|
||||
|
||||
#define LIMITED_ANIMATION true
|
||||
|
||||
#define DEFAULT_ANIMATION 23
|
||||
|
||||
//bool fillAnimationActive = false;
|
||||
|
||||
void RGB_Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t blu){
|
||||
|
||||
@ -47,7 +53,8 @@ void RGB_Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t b
|
||||
}
|
||||
|
||||
void RGB_Animations_ON(){
|
||||
RGB_Lights_Set_Animation(prevAnimEvent.AnimationIndex, prevAnimEvent.data.red, prevAnimEvent.data.grn, prevAnimEvent.data.blu);
|
||||
//RGB_Lights_Set_Animation(prevAnimEvent.AnimationIndex, prevAnimEvent.data.red, prevAnimEvent.data.grn, prevAnimEvent.data.blu);
|
||||
RGB_Lights_Set_Animation(DEFAULT_ANIMATION, 0, 0, 0); // set comet rainbow animation
|
||||
}
|
||||
|
||||
void RGB_Animations_OFF(){
|
||||
@ -63,7 +70,9 @@ void Lights_Set_White(uint8_t val){
|
||||
//pwmOut[0]->setOutput(val / 2.5f);
|
||||
}
|
||||
|
||||
void Init_RGB_Lights_Task(void){
|
||||
|
||||
void Init_RGB_Lights_Task(bool upgrade){
|
||||
upgradeMode = upgrade;
|
||||
ledSettings[0].leds = new CRGB[ledSettings[0].size];
|
||||
Init_RGB_Strip(ledSettings[0].leds, ledSettings[0].pin, ledSettings[0].size, ledSettings[0].rgbOrder, ledSettings[0].chip, ledSettings[0].bright);
|
||||
ESP_LOGD(tag, "Initializing Strip1: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[0].pin, ledSettings[0].size, ledSettings[0].rgbOrder, ledSettings[0].chip);
|
||||
@ -234,31 +243,61 @@ inline int calcPixelIndex(LEDSTRIP_SETTINGS& strip, int index) {
|
||||
return (x +strip.offset) % strip.effSize;
|
||||
}
|
||||
*/
|
||||
// ...existing code...
|
||||
inline void setPixel1(LEDSTRIP_SETTINGS& leds, int pixelIndex, const CRGB col) {
|
||||
// Guard against invalid configuration that can cause crashes (null pointer or zero size)
|
||||
if (leds.size <= 0 || leds.leds == nullptr) {
|
||||
ESP_LOGW(tag, "setPixel1: invalid strip (Size=%d, leds=%p)", leds.size, (void*)leds.leds);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a signed type so negative pixelIndex is preserved
|
||||
int x = pixelIndex + leds.shift;
|
||||
int n = leds.size;
|
||||
|
||||
// If strip.effSize is power of 2: (n & (n - 1)) == 0
|
||||
int idx;
|
||||
if ((n & (n - 1)) == 0) {
|
||||
// Masking is safe if we treat x as unsigned for masking,
|
||||
// this yields the correct wrapped index for two's-complement
|
||||
idx = static_cast<int>( (static_cast<uint32_t>(x)) & (static_cast<uint32_t>(n) - 1u) );
|
||||
} else {
|
||||
// General modulo: C++ '%' can be negative, so normalize
|
||||
idx = x % n;
|
||||
if (idx < 0) idx += n;
|
||||
}
|
||||
|
||||
// Normalize final position robustly (handles negative offsets)
|
||||
int pos = (idx + leds.offset) % n;
|
||||
if (pos < 0) pos += n;
|
||||
|
||||
leds.leds[pos] = col;
|
||||
}
|
||||
/*
|
||||
inline void setPixel1(LEDSTRIP_SETTINGS& leds, int pixelIndex, const CRGB col) {
|
||||
register uint16_t x = pixelIndex + ledSettings[0].shift;
|
||||
// If strip.effSize is power of 2, use faster bit masking
|
||||
if ((leds.effSize & (leds.effSize - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + leds.effSize) & (leds.effSize - 1)) : (x & (leds.effSize - 1));
|
||||
leds.leds[(x + leds.offset) & (leds.effSize - 1)] = col;
|
||||
// If strip.size is power of 2, use faster bit masking
|
||||
if ((leds.size & (leds.size - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + leds.size) & (leds.size - 1)) : (x & (leds.size - 1));
|
||||
leds.leds[(x + leds.offset) & (leds.size - 1)] = col;
|
||||
} else {
|
||||
// For non-power-of-2 sizes, still need modulo
|
||||
x = (x < 0) ? ((x + ledSettings[0].effSize) % ledSettings[0].effSize) : (x % ledSettings[0].effSize);
|
||||
ledSettings[0].leds[(x + ledSettings[0].offset) % ledSettings[0].effSize] = col;
|
||||
x = (x < 0) ? ((x + ledSettings[0].size) % ledSettings[0].size) : (x % ledSettings[0].size);
|
||||
ledSettings[0].leds[(x + ledSettings[0].offset) % ledSettings[0].size] = col;
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
inline void setPixel2(int pixelIndex, const CRGB col) {
|
||||
register uint16_t x = pixelIndex + ledSettings[1].shift;
|
||||
// If strip.effSize is power of 2, use faster bit masking
|
||||
if ((ledSettings[1].effSize & (ledSettings[1].effSize - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + ledSettings[1].effSize) & (ledSettings[1].effSize - 1)) : (x & (ledSettings[1].effSize - 1));
|
||||
ledSettings[1].leds[(x + ledSettings[1].offset) & (ledSettings[1].effSize - 1)] = col;
|
||||
// If strip.size is power of 2, use faster bit masking
|
||||
if ((ledSettings[1].size & (ledSettings[1].size - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + ledSettings[1].size) & (ledSettings[1].size - 1)) : (x & (ledSettings[1].size - 1));
|
||||
ledSettings[1].leds[(x + ledSettings[1].offset) & (ledSettings[1].size - 1)] = col;
|
||||
} else {
|
||||
// For non-power-of-2 sizes, still need modulo
|
||||
x = (x < 0) ? ((x + ledSettings[1].effSize) % ledSettings[1].effSize) : (x % ledSettings[1].effSize);
|
||||
ledSettings[1].leds[(x + ledSettings[1].offset) % ledSettings[1].effSize] = col;
|
||||
x = (x < 0) ? ((x + ledSettings[1].size) % ledSettings[1].size) : (x % ledSettings[1].size);
|
||||
ledSettings[1].leds[(x + ledSettings[1].offset) % ledSettings[1].size] = col;
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,7 +325,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
|
||||
if(pin == 3){
|
||||
// First level: Chip type selection
|
||||
if (chipUpper == "WS2812B" || chipUpper == "SK6812") {
|
||||
if (chipUpper == "WS2812B") {
|
||||
switch(rgbOrder) {
|
||||
case RGB: FastLED.addLeds<WS2812B, 3, RGB>(leds, size); break;
|
||||
case RBG: FastLED.addLeds<WS2812B, 3, RBG>(leds, size); break;
|
||||
@ -308,7 +347,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
default: FastLED.addLeds<SK6812, 3, GRB>(leds, size); break;
|
||||
}
|
||||
}
|
||||
else if (chipUpper == "WS2811_400") {
|
||||
else if (chipUpper == "WS2811") {
|
||||
switch(rgbOrder) {
|
||||
case RGB: FastLED.addLeds<WS2811_400, 3, RGB>(leds, size); break;
|
||||
case RBG: FastLED.addLeds<WS2811_400, 3, RBG>(leds, size); break;
|
||||
@ -361,7 +400,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
default: FastLED.addLeds<SK6812, 9, GRB>(leds, size); break;
|
||||
}
|
||||
}
|
||||
else if (chipUpper == "WS2811_400") {
|
||||
else if (chipUpper == "WS2811") {
|
||||
switch(rgbOrder) {
|
||||
case RGB: FastLED.addLeds<WS2811_400, 9, RGB>(leds, size); break;
|
||||
case RBG: FastLED.addLeds<WS2811_400, 9, RBG>(leds, size); break;
|
||||
@ -392,7 +431,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
}
|
||||
else if(pin == 46){
|
||||
// First level: Chip type selection
|
||||
if (chipUpper == "WS2812B" || chipUpper == "SK6812") {
|
||||
if (chipUpper == "WS2812B") {
|
||||
switch(rgbOrder) {
|
||||
case RGB: FastLED.addLeds<WS2812B, 46, RGB>(leds, size); break;
|
||||
case RBG: FastLED.addLeds<WS2812B, 46, RBG>(leds, size); break;
|
||||
@ -414,7 +453,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
default: FastLED.addLeds<SK6812, 46, GRB>(leds, size); break;
|
||||
}
|
||||
}
|
||||
else if (chipUpper == "WS2811_400") {
|
||||
else if (chipUpper == "WS2811") {
|
||||
switch(rgbOrder) {
|
||||
case RGB: FastLED.addLeds<WS2811_400, 46, RGB>(leds, size); break;
|
||||
case RBG: FastLED.addLeds<WS2811_400, 46, RBG>(leds, size); break;
|
||||
@ -439,7 +478,7 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
else {
|
||||
// Default to WS2812B if unknown chip type
|
||||
ESP_LOGW(tag, "Unknown LED chip type: %s, defaulting to WS2812B", chipType.c_str());
|
||||
FastLED.addLeds<WS2812B, 9, GRB>(leds, size);
|
||||
FastLED.addLeds<WS2812B, 46, GRB>(leds, size);
|
||||
}
|
||||
ESP_LOGI(tag, "Initialized %s LED strip with %d LEDs on pin %d", chipType.c_str(), size, pin);
|
||||
}
|
||||
@ -449,147 +488,392 @@ void Init_RGB_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder,
|
||||
|
||||
|
||||
void RGB_Lights_Control_Task(void *parameters){
|
||||
// Wait for other tasks to initialize
|
||||
vTaskDelay(pdMS_TO_TICKS(200)); // wait for everything to settle
|
||||
RGB_Lights_Set_Brightness(ledSettings[0].bright);
|
||||
|
||||
if(upgradeMode){
|
||||
// fill with 3 colors equally magenta, cyan, lime
|
||||
RGB_Lights_Set_Brightness(64);
|
||||
fill_gradient_RGB(ledSettings[0].leds, ledSettings[0].size, CRGB(CRGB::Magenta), CRGB(CRGB::Cyan), CRGB(CRGB::Lime), CRGB(CRGB::Magenta));
|
||||
FastLED.show();
|
||||
}else{
|
||||
RGB_Lights_Set_Animation(DEFAULT_ANIMATION, 0, 0, 0); // set comet rainbow animation
|
||||
}
|
||||
|
||||
ANIM_EVENT AnimEvent;
|
||||
CRGB col;
|
||||
COLOR_PACK colorPack;
|
||||
CRGBPalette16 firePalette;
|
||||
|
||||
if (LIMITED_ANIMATION){
|
||||
while (true) {
|
||||
if (xQueueReceive(animationQueue, &AnimEvent, portMAX_DELAY) == pdTRUE) {
|
||||
ESP_LOGD(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex);
|
||||
switch (AnimEvent.AnimationIndex) {
|
||||
|
||||
// Wait for other tasks to initialize (including front light task)
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // wait for everything to settle
|
||||
|
||||
RGB_Lights_Set_Brightness(48);
|
||||
RGB_Lights_Set_Animation(23, 0, 0, 0); // set comet rainbow animation
|
||||
|
||||
|
||||
while (true) {
|
||||
if (xQueueReceive(animationQueue, &AnimEvent, portMAX_DELAY) == pdTRUE) {
|
||||
ESP_LOGI(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex);
|
||||
switch (AnimEvent.AnimationIndex) {
|
||||
case -3: // Set Pixel by index
|
||||
if (AnimEvent.data.data[7] >= 0 && AnimEvent.data.data[7] < ledSettings[0].size) {
|
||||
ledSettings[0].leds[AnimEvent.data.data[7]] = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu);
|
||||
case -3: // Set Shift
|
||||
if (AnimEvent.data.data[0] >= 0 && AnimEvent.data.data[0] < ledSettings[0].size) {
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB::Black);
|
||||
ledSettings[0].shift = AnimEvent.data.data[0];
|
||||
vTaskDelay(25 / portTICK_PERIOD_MS);
|
||||
setPixel1(ledSettings[0], 0, CRGB::White * FastLED.getBrightness() / 255);
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "Set Shift: %d", ledSettings[0].shift);
|
||||
} else {
|
||||
ESP_LOGW(tag, "Pixel index out of range: %d", AnimEvent.data.data[0]);
|
||||
}
|
||||
break;
|
||||
case -2: // Fill Static Color
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu));
|
||||
FastLED.show();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Pixel index out of range: %d", AnimEvent.data.data[7]);
|
||||
ESP_LOGD(tag, "Static Color");
|
||||
break;
|
||||
case -1:
|
||||
FastLED.clear();
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "LEDs Off");
|
||||
break;
|
||||
case 0:
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB::Black);
|
||||
FastLED.show();
|
||||
break;
|
||||
case 1 ... 5: { // Timed Fill Animations
|
||||
int timeDuration = AnimEvent.AnimationIndex * 1000 - 400;
|
||||
int whiteDelay = timeDuration - 1000;
|
||||
if (whiteDelay < 800) whiteDelay = 800; // minimum 8 seconds
|
||||
Anim_TimedFill_Flash(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, pwmOutputs[0], &sys_settings.pwmOutSettings[0], whiteDelay, 20000, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case -2: // Fill Static Color
|
||||
col = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu);
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, col);
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "Static Color");
|
||||
break;
|
||||
case -1:
|
||||
FastLED.clear();
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "LEDs Off");
|
||||
break;
|
||||
case 0:
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB::Black);
|
||||
FastLED.show();
|
||||
break;
|
||||
case 1: case 2: case 3: case 4: case 5:{ // Timed Fill Animations
|
||||
int timeDuration = AnimEvent.AnimationIndex * 1000;
|
||||
int whiteDelay = timeDuration - 1000;
|
||||
Anim_TimedFill_Flash(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, pwmOutputs[0], &sys_settings.pwmOutSettings[0], whiteDelay, 10000, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
|
||||
break;
|
||||
}
|
||||
case 6:
|
||||
// RGB White and Dedicated White all on with timeout and brightness reduction
|
||||
if (pwmOutputs[0] != nullptr) {
|
||||
case 6:
|
||||
// RGB White and Dedicated White all on with timeout and brightness reduction
|
||||
Anim_SolidWhite(AnimationLooping, ledSettings[0].leds, pwmOutputs[0], ledSettings[0].size, 240, 30000);
|
||||
break;
|
||||
case 7:
|
||||
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60);
|
||||
break;
|
||||
case 8 ... 12:
|
||||
Anim_GradientRotate(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, gradient_colorPack[AnimEvent.AnimationIndex - 8], 80);
|
||||
break;
|
||||
case 13 ... 17: { // Fire Animations
|
||||
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 13]; // copy const pack to mutable
|
||||
CRGBPalette16 firePalette;
|
||||
createFirePalette(firePalette, fp);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60);
|
||||
break;
|
||||
case 8: case 9: case 10: case 11: case 12:
|
||||
Anim_GradientRotate(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, gradient_colorPack[AnimEvent.AnimationIndex - 8], 80);
|
||||
break;
|
||||
case 13: case 14: case 15: case 16: case 17: { // Fire Animations
|
||||
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 13]; // copy const pack to mutable
|
||||
createFirePalette(firePalette, fp);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
break;
|
||||
case 18 ... 20: // Sparkle/Twinkle
|
||||
Anim_Sparkle(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[0], 40, 20);
|
||||
break;
|
||||
case 21:
|
||||
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 7000, 90);
|
||||
break;
|
||||
case 22: // Rain Animation
|
||||
Anim_Morph(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 20, 40);
|
||||
break;
|
||||
|
||||
// Comets
|
||||
case 23:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 40, 85, RANDOM_DECAY, 1);
|
||||
break;
|
||||
case 24 ... 31:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 24], 40, 85, RANDOM_DECAY, 6);
|
||||
break;
|
||||
case 32 ... 40:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 32], 40, 85, RANDOM_DECAY, 3);
|
||||
break;
|
||||
case 41 ... 49:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 41], 40, 85, RANDOM_DECAY, 2);
|
||||
break;
|
||||
|
||||
// Snakes
|
||||
case 50:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 50);
|
||||
break;
|
||||
case 51 ... 58:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 51], 60, 6);
|
||||
break;
|
||||
case 59 ... 67:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 59], 60, 3);
|
||||
break;
|
||||
case 68 ... 76:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 68], 60, 2);
|
||||
break;
|
||||
|
||||
// Sectors
|
||||
case 77:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, NO_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 78 ... 86:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 78], NO_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 87 ... 95:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 87], NO_GAPS, 2, 40, 90);
|
||||
break;
|
||||
|
||||
// Dashes
|
||||
case 96:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, WITH_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 97 ... 104:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 97], WITH_GAPS, 6, 40, 90);
|
||||
break;
|
||||
case 105 ... 113:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 105], WITH_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 114 ... 122:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 114], WITH_GAPS, 2, 40, 90);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(tag, "Loop default");
|
||||
break;
|
||||
}
|
||||
case 18: case 19: case 20:// Sparkle/Twinkle
|
||||
Anim_Sparkle(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[0], 40, 20);
|
||||
break;
|
||||
case 21:
|
||||
//Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 7000, 90);
|
||||
Anim_Morph(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 20, 40);
|
||||
break;
|
||||
case 22: // Rain Animation
|
||||
break;
|
||||
|
||||
|
||||
// Comets
|
||||
case 23:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 40, 85, RANDOM_DECAY, 1);
|
||||
break;
|
||||
case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 24], 40, 85, RANDOM_DECAY, 6);
|
||||
break;
|
||||
case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 40:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 32], 40, 85, RANDOM_DECAY, 3);
|
||||
break;
|
||||
case 41: case 42: case 43: case 44: case 45: case 46: case 47: case 48: case 49:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 41], 40, 85, RANDOM_DECAY, 2);
|
||||
break;
|
||||
|
||||
// Snakes
|
||||
case 50:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 50);
|
||||
break;
|
||||
case 51: case 52: case 53: case 54: case 55: case 56: case 57: case 58:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 51], 60, 6);
|
||||
break;
|
||||
case 59: case 60: case 61: case 62: case 63: case 64: case 65: case 66: case 67:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 59], 60, 3);
|
||||
break;
|
||||
case 68: case 69: case 70: case 71: case 72: case 73: case 74: case 75: case 76:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 68], 60, 2);
|
||||
break;
|
||||
|
||||
// Sectors
|
||||
case 77:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, NO_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 78: case 79: case 80: case 81: case 82: case 83: case 84: case 85: case 86:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 78], NO_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 87: case 88: case 89: case 90: case 91: case 92: case 93: case 94: case 95:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 87], NO_GAPS, 2, 40, 90);
|
||||
break;
|
||||
|
||||
// Dashes
|
||||
case 96:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, WITH_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 97:case 98:case 99: case 100: case 101: case 102: case 103: case 104:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 97], WITH_GAPS, 6, 40, 90);
|
||||
break;
|
||||
case 105: case 106: case 107: case 108: case 109: case 110: case 111: case 112: case 113:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 105], WITH_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 114: case 115: case 116: case 117: case 118: case 119: case 120: case 121: case 122:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 114], WITH_GAPS, 2, 40, 90);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(tag, "Loop default");
|
||||
break;
|
||||
AnimationLooping = false;
|
||||
prevAnimEvent = AnimEvent;
|
||||
ESP_LOGD(tag, "Going to Queue to Wait");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extended Animation Set
|
||||
while (true) {
|
||||
if (xQueueReceive(animationQueue, &AnimEvent, portMAX_DELAY) == pdTRUE) {
|
||||
ESP_LOGD(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex);
|
||||
switch (AnimEvent.AnimationIndex) {
|
||||
case -3: // Set Shift
|
||||
if (AnimEvent.data.data[0] >= 0 && AnimEvent.data.data[0] < ledSettings[0].size) {
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB::Black);
|
||||
ledSettings[0].shift = AnimEvent.data.data[0];
|
||||
|
||||
AnimationLooping = false;
|
||||
prevAnimEvent = AnimEvent;
|
||||
ESP_LOGD(tag, "Going to Queue to Wait");
|
||||
ESP_LOGD(tag, "Set Shift: %d", ledSettings[0].shift);
|
||||
vTaskDelay(25 / portTICK_PERIOD_MS);
|
||||
setPixel1(ledSettings[0], 0, CRGB::White);
|
||||
|
||||
FastLED.show();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Pixel index out of range: %d", AnimEvent.data.data[0]);
|
||||
}
|
||||
break;
|
||||
case -2: // Fill Static Color
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu));
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "Static Color");
|
||||
break;
|
||||
case -1:
|
||||
FastLED.clear();
|
||||
FastLED.show();
|
||||
ESP_LOGD(tag, "LEDs Off");
|
||||
break;
|
||||
case 0:
|
||||
fill_solid(ledSettings[0].leds, ledSettings[0].size, CRGB::Black);
|
||||
FastLED.show();
|
||||
break;
|
||||
case 1 ... 5: { // Timed Fill Animations
|
||||
int timeDuration = AnimEvent.AnimationIndex * 1000 - 400;
|
||||
int whiteDelay = timeDuration - 1000;
|
||||
if (whiteDelay < 800) whiteDelay = 800; // minimum 8 seconds
|
||||
Anim_TimedFill_Flash(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, pwmOutputs[0], &sys_settings.pwmOutSettings[0], whiteDelay, 10000, CRGB::Black, CRGB::White, timeDuration, ledSettings[0].shift);
|
||||
break;
|
||||
}
|
||||
case 6:
|
||||
// RGB White and Dedicated White all on with timeout and brightness reduction
|
||||
Anim_SolidWhite(AnimationLooping, ledSettings[0].leds, pwmOutputs[0], ledSettings[0].size, 240, 30000);
|
||||
break;
|
||||
case 7:
|
||||
Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60);
|
||||
break;
|
||||
case 8 ... 12:
|
||||
Anim_GradientRotate(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, gradient_colorPack[AnimEvent.AnimationIndex - 8], 80);
|
||||
break;
|
||||
case 13 ... 17: { // Fire Animations
|
||||
COLOR_PACK fp = fireColorPacks[AnimEvent.AnimationIndex - 13]; // copy const pack to mutable
|
||||
CRGBPalette16 firePalette;
|
||||
createFirePalette(firePalette, fp);
|
||||
Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, ledSettings[0].shift);
|
||||
break;
|
||||
}
|
||||
case 18 ... 20: // Sparkle/Twinkle
|
||||
Anim_Sparkle(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[0], 40, 20);
|
||||
break;
|
||||
case 21:
|
||||
Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 7000, 90);
|
||||
break;
|
||||
case 22: // Rain Animation
|
||||
Anim_Morph(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 20, 40);
|
||||
break;
|
||||
|
||||
// Comets
|
||||
case 23:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 40, 85, RANDOM_DECAY, 1);
|
||||
break;
|
||||
case 24 ... 31:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 24], 40, 85, RANDOM_DECAY, 6);
|
||||
break;
|
||||
case 32 ... 47:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 32], 40, 85, RANDOM_DECAY, 3);
|
||||
break;
|
||||
case 48 ... 63:
|
||||
Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 48], 40, 85, RANDOM_DECAY, 2);
|
||||
break;
|
||||
|
||||
// Snakes
|
||||
case 64:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, 50);
|
||||
break;
|
||||
case 65 ... 72:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 65], 60, 6);
|
||||
break;
|
||||
case 73 ... 88:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 73], 60, 3);
|
||||
break;
|
||||
case 89 ... 104:
|
||||
Anim_Snakes(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 89], 60, 2);
|
||||
break;
|
||||
|
||||
// Sectors
|
||||
case 105:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, NO_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 106 ... 121:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 106], NO_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 122 ... 137:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 122], NO_GAPS, 2, 40, 90);
|
||||
break;
|
||||
|
||||
// Dashes
|
||||
case 138:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack_RAINBOW, WITH_GAPS, 1, 40, 90);
|
||||
break;
|
||||
case 139 ... 146:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, single_colorPacks[AnimEvent.AnimationIndex - 139], WITH_GAPS, 6, 40, 90);
|
||||
break;
|
||||
case 147 ... 162:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, double_colorPacks[AnimEvent.AnimationIndex - 147], WITH_GAPS, 3, 40, 90);
|
||||
break;
|
||||
case 163 ... 178:
|
||||
Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, tripple_colorPacks[AnimEvent.AnimationIndex - 163], WITH_GAPS, 2, 40, 90);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(tag, "Loop default");
|
||||
break;
|
||||
}
|
||||
|
||||
AnimationLooping = false;
|
||||
prevAnimEvent = AnimEvent;
|
||||
ESP_LOGD(tag, "Going to Queue to Wait");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void SetAndSaveUserSettings(USER_SETTINGS &userSettings) {
|
||||
|
||||
// Apply the settings to sys_settings
|
||||
sys_settings.limitedMode = userSettings.limitedMode;
|
||||
|
||||
sys_settings.ledStripSettings[0]->chip = userSettings.ledChipset1;
|
||||
sys_settings.ledStripSettings[0]->shift = userSettings.ledShift1;
|
||||
sys_settings.ledStripSettings[0]->bright = userSettings.ledBrightness1;
|
||||
RGB_Lights_Set_Brightness(userSettings.ledBrightness1);
|
||||
|
||||
sys_settings.ledStripSettings[0]->chip = userSettings.ledChipset2;
|
||||
sys_settings.ledStripSettings[0]->shift = userSettings.ledShift2;
|
||||
sys_settings.ledStripSettings[0]->bright = userSettings.ledBrightness2;
|
||||
|
||||
sys_settings.tSensorSettings.setpoint1 = userSettings.fanLowerTemp;
|
||||
sys_settings.tSensorSettings.setpoint2 = userSettings.fanUpperTemp;
|
||||
sys_settings.rampLightSettings[0].min = userSettings.frontLightMin;
|
||||
sys_settings.rampLightSettings[0].max = userSettings.frontLightMax;
|
||||
sys_settings.rampLightSettings[1].min = userSettings.rearLightMin;
|
||||
sys_settings.rampLightSettings[1].max = userSettings.rearLightMax;
|
||||
|
||||
if (!LittleFS.begin()) {
|
||||
ESP_LOGE(tag, "Failed to mount file system");
|
||||
return;
|
||||
}
|
||||
|
||||
// First, open the existing file for reading so we preserve unrelated keys
|
||||
File file = LittleFS.open(booth_file_path, "r");
|
||||
JsonDocument jsonDocument;
|
||||
|
||||
if (file) {
|
||||
// Try to deserialize existing content; if it fails, start with an empty document
|
||||
DeserializationError err = deserializeJson(jsonDocument, file);
|
||||
if (err) {
|
||||
ESP_LOGW(tag, "Failed to parse existing JSON (%s), starting fresh", err.c_str());
|
||||
jsonDocument.clear();
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Settings file not found, a new one will be created");
|
||||
}
|
||||
|
||||
// Update only the keys we care about
|
||||
jsonDocument["limied-mode"] = userSettings.limitedMode;
|
||||
|
||||
jsonDocument["ramp-lights"][0]["min"] = userSettings.frontLightMin;
|
||||
jsonDocument["ramp-lights"][0]["max"] = userSettings.frontLightMax;
|
||||
jsonDocument["ramp-lights"][1]["min"] = userSettings.rearLightMin;
|
||||
jsonDocument["ramp-lights"][1]["max"] = userSettings.rearLightMax;
|
||||
|
||||
String chipName = "Unknown";
|
||||
if (userSettings.ledChipset1 == CHIP_WS2812B) chipName = "WS2812B";
|
||||
else if (userSettings.ledChipset1 == CHIP_SK6812) chipName = "SK6812";
|
||||
else if (userSettings.ledChipset1 == CHIP_WS2811) chipName = "WS2811";
|
||||
else if (userSettings.ledChipset1 == CHIP_WS2815) chipName = "WS2815";
|
||||
jsonDocument["strips"][0]["chip"] = chipName;
|
||||
userSettings.rgbOrder1[3] = '\0'; // Ensure null-terminated string
|
||||
jsonDocument["strips"][0]["rgb-order"] = String(userSettings.rgbOrder1);
|
||||
jsonDocument["strips"][0]["shift"] = userSettings.ledShift1;
|
||||
jsonDocument["strips"][0]["bright"] = userSettings.ledBrightness1;
|
||||
jsonDocument["strips"][0]["count"] = userSettings.ledCount1;
|
||||
|
||||
|
||||
if (userSettings.ledChipset2 == CHIP_WS2812B) chipName = "WS2812B";
|
||||
else if (userSettings.ledChipset2 == CHIP_SK6812) chipName = "SK6812";
|
||||
else if (userSettings.ledChipset2 == CHIP_WS2811) chipName = "WS2811";
|
||||
else if (userSettings.ledChipset2 == CHIP_WS2815) chipName = "WS2815";
|
||||
jsonDocument["strips"][1]["chip"] = chipName;
|
||||
userSettings.rgbOrder2[3] = '\0'; // Ensure null-terminated string
|
||||
jsonDocument["strips"][1]["rgb-order"] = String(userSettings.rgbOrder2);
|
||||
jsonDocument["strips"][1]["shift"] = userSettings.ledShift2;
|
||||
jsonDocument["strips"][1]["bright"] = userSettings.ledBrightness2;
|
||||
jsonDocument["strips"][1]["count"] = userSettings.ledCount2;
|
||||
|
||||
jsonDocument["t-sensor"]["sp1"] = userSettings.fanLowerTemp;
|
||||
jsonDocument["t-sensor"]["sp2"] = userSettings.fanUpperTemp;
|
||||
|
||||
//jsonDocument["stripsr"][0]["chip"] = settings.ledChipset;
|
||||
|
||||
// Now open the file for writing (overwrite) and serialize back
|
||||
File out = LittleFS.open(booth_file_path, "w");
|
||||
if (!out) {
|
||||
ESP_LOGE(tag, "Failed to open file for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (serializeJson(jsonDocument, out) == 0) {
|
||||
ESP_LOGE(tag, "Failed to write to file");
|
||||
} else {
|
||||
ESP_LOGI(tag, "User settings saved successfully");
|
||||
}
|
||||
|
||||
out.close();
|
||||
|
||||
|
||||
// Modify system.json with booth type
|
||||
String sysProf = String(sys_settings.profile);
|
||||
String userProf = String(userSettings.profile);
|
||||
sysProf.toLowerCase();
|
||||
userProf.toLowerCase();
|
||||
ESP_LOGI(tag, "System Profile: '%s', User Profile: '%s'", sysProf.c_str(), userProf.c_str());
|
||||
|
||||
if (userProf.length() > 0 && sysProf.indexOf(userProf) >= 0) {
|
||||
// userSettings.profile is contained in sys_settings.profile (case-insensitive)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void createFirePalette(CRGBPalette16& palette, const COLOR_PACK& colorPack) {
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
if (i < 3) palette[i] = CRGB::Black;
|
||||
|
||||
@ -27,6 +27,7 @@ void Animation_Init(void){
|
||||
}
|
||||
|
||||
// Animation Loop Template
|
||||
/*
|
||||
void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function<int()> callback) {
|
||||
if (!callback) {
|
||||
ESP_LOGE("Animation_Loop", "Invalid callback function");
|
||||
@ -65,7 +66,62 @@ void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function<in
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
*/
|
||||
void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function<int()> callback) {
|
||||
if (!callback) {
|
||||
ESP_LOGE("Animation_Loop", "Invalid callback function");
|
||||
return;
|
||||
}
|
||||
|
||||
loop_active_flag = true;
|
||||
speed = constrain(speed, 0, MaxSpeed);
|
||||
|
||||
// compute desired loop delay in milliseconds and clamp
|
||||
int loopDelayMs = max(MaxSpeed - speed, MinLoopDelay);
|
||||
const int MAX_LOOP_DELAY_MS = 60 * 1000; // 60s safety cap
|
||||
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
|
||||
|
||||
// Convert ms -> RTOS ticks
|
||||
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
|
||||
|
||||
TickType_t xLastWakeTime;
|
||||
TickType_t elapsedTicks;
|
||||
TickType_t delayTicks;
|
||||
int retVal = 0;
|
||||
while(!retVal && loop_active_flag) {
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
try {
|
||||
retVal = callback(); // Call animation function
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!loop_active_flag) break;
|
||||
|
||||
// compute elapsed ticks since callback start and remaining ticks
|
||||
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
|
||||
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
|
||||
|
||||
if (delayTicks == 0) {
|
||||
// yield a tick to avoid busy-spin
|
||||
vTaskDelay(1);
|
||||
if (ulTaskNotifyTake(pdTRUE, 0)) break; // notified -> exit
|
||||
} else {
|
||||
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } // notified -> exit
|
||||
}
|
||||
}
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
|
||||
/*
|
||||
void Animation_Loop_Variable(bool volatile& loop_active_flag, std::function<int()> callback) {
|
||||
if (!callback) {
|
||||
ESP_LOGE("Animation_Loop", "Invalid callback function");
|
||||
@ -105,8 +161,74 @@ void Animation_Loop_Variable(bool volatile& loop_active_flag, std::function<int(
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
*/
|
||||
|
||||
void Animation_Loop_Variable(bool volatile& loop_active_flag, std::function<int()> callback) {
|
||||
if (!callback) {
|
||||
ESP_LOGE("Animation_Loop", "Invalid callback function");
|
||||
return;
|
||||
}
|
||||
|
||||
loop_active_flag = true;
|
||||
|
||||
// clear any pending notification
|
||||
ulTaskNotifyTake(pdTRUE, 0);
|
||||
|
||||
TickType_t xLastWakeTime;
|
||||
TickType_t elapsedTicks;
|
||||
TickType_t delayTicks;
|
||||
|
||||
// Define sensible upper bound for delay (ms) to avoid indefinite blocking due to bad callback
|
||||
const int MAX_LOOP_DELAY_MS = 60 * 1000; // 60 seconds
|
||||
|
||||
while (loop_active_flag) {
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
int loopDelayMs = 0;
|
||||
try {
|
||||
loopDelayMs = callback(); // callback returns desired delay in milliseconds
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
|
||||
if (!loop_active_flag) break;
|
||||
|
||||
// sanitize returned ms value and enforce minimum/maximum
|
||||
if (loopDelayMs < (int)MinLoopDelay) loopDelayMs = MinLoopDelay;
|
||||
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
|
||||
|
||||
// convert ms -> RTOS ticks (safe conversion)
|
||||
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
|
||||
|
||||
// compute elapsed ticks since callback start
|
||||
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
|
||||
|
||||
// compute remaining delay (in ticks)
|
||||
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
|
||||
|
||||
if (delayTicks == 0) {
|
||||
// Ensure we yield at least one tick to avoid busy-looping
|
||||
vTaskDelay(1);
|
||||
// Check if notified during the yield
|
||||
if (ulTaskNotifyTake(pdTRUE, 0)) break;
|
||||
} else {
|
||||
// Wait for either a notification (termination) or the timeout
|
||||
if (ulTaskNotifyTake(pdTRUE, delayTicks)) {
|
||||
// notified => exit loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
|
||||
// Animation Loop Template
|
||||
/*
|
||||
void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickType_t durationMs, std::function<int()> callback) {
|
||||
loop_active_flag = true;
|
||||
speed = constrain(speed, 0, MaxSpeed);
|
||||
@ -159,8 +281,72 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
*/
|
||||
|
||||
void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickType_t durationMs, std::function<int()> callback) {
|
||||
loop_active_flag = true;
|
||||
speed = constrain(speed, 0, MaxSpeed);
|
||||
|
||||
// treat durationMs as milliseconds (convert to ticks)
|
||||
const TickType_t durationTicks = pdMS_TO_TICKS(durationMs);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
|
||||
TickType_t startTicks = xTaskGetTickCount();
|
||||
TickType_t xLastWakeTime;
|
||||
|
||||
const int MAX_LOOP_DELAY_MS = 60 * 1000; // safety cap
|
||||
while (loop_active_flag) {
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
// Call animation function
|
||||
int speedIncrease = 0;
|
||||
try {
|
||||
speedIncrease = callback();
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop_Duration", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop_Duration", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!loop_active_flag) break;
|
||||
|
||||
// Calculate combined speed with bounds protection
|
||||
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
|
||||
|
||||
// Calculate delay with minimum protection (ms)
|
||||
int loopDelayMs = MaxSpeed - totalSpeed;
|
||||
loopDelayMs = max(loopDelayMs, MinLoopDelay);
|
||||
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
|
||||
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
|
||||
|
||||
// Calculate remaining time with overflow protection
|
||||
TickType_t elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
|
||||
TickType_t delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
|
||||
|
||||
// Delay and Check for termination request
|
||||
if (delayTicks == 0) {
|
||||
vTaskDelay(1);
|
||||
if (ulTaskNotifyTake(pdTRUE, 0)) break;
|
||||
} else {
|
||||
if (ulTaskNotifyTake(pdTRUE, delayTicks)) break;
|
||||
}
|
||||
|
||||
// Check if duration reached and wait for loop_active_flag
|
||||
if (durationTicks > 0) {
|
||||
TickType_t totalElapsed = xTaskGetTickCount() - startTicks;
|
||||
if (totalElapsed >= durationTicks) {
|
||||
break; // Auto-terminate when duration reached
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
|
||||
// Animation Loop Template
|
||||
/*
|
||||
void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function<int()> callback) {
|
||||
loop_active_flag = true;
|
||||
uint32_t loop_cycle_count = 0;
|
||||
@ -211,6 +397,66 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
*/
|
||||
|
||||
void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function<int()> callback) {
|
||||
loop_active_flag = true;
|
||||
uint32_t loop_cycle_count = 0;
|
||||
speed = constrain(speed, 0, MaxSpeed);
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
|
||||
TickType_t xLastWakeTime;
|
||||
TickType_t elapsedTicks;
|
||||
TickType_t delayTicks;
|
||||
|
||||
const int MAX_LOOP_DELAY_MS = 60 * 1000; // safety cap
|
||||
for(;;) {
|
||||
xLastWakeTime = xTaskGetTickCount();
|
||||
|
||||
int speedIncrease = 0;
|
||||
try {
|
||||
speedIncrease = callback(); // Call animation function
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Animation_Loop_Cycles", "Callback exception: %s", e.what());
|
||||
break;
|
||||
} catch (...) {
|
||||
ESP_LOGE("Animation_Loop_Cycles", "Callback unknown exception");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!loop_active_flag) break;
|
||||
|
||||
// Calculate combined speed with bounds protection
|
||||
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
|
||||
|
||||
// Calculate delay with minimum protection (ms)
|
||||
int loopDelayMs = MaxSpeed - totalSpeed;
|
||||
loopDelayMs = max(loopDelayMs, MinLoopDelay);
|
||||
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
|
||||
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
|
||||
|
||||
// Calculate remaining time with overflow protection
|
||||
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
|
||||
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
|
||||
|
||||
// Delay and Check for termination request
|
||||
if (delayTicks == 0) {
|
||||
vTaskDelay(1);
|
||||
if (ulTaskNotifyTake(pdTRUE, 0)) break;
|
||||
} else {
|
||||
if (ulTaskNotifyTake(pdTRUE, delayTicks)) break;
|
||||
}
|
||||
|
||||
// Check if cycles reached and exit
|
||||
if (loop_cycle_count >= loop_cycles) {
|
||||
break;
|
||||
}
|
||||
|
||||
loop_cycle_count++;
|
||||
}
|
||||
|
||||
loop_active_flag = false;
|
||||
}
|
||||
|
||||
|
||||
/********************************************************************************
|
||||
@ -399,76 +645,6 @@ void Anim_Color_Sectors(bool volatile& activeFlag, CRGB* leds, int size, const C
|
||||
//#define COMET_FADE_FACTOR COMET_FADE_FACTOR2
|
||||
#define MAX_COMETS 8 // Maximum number of comets supported
|
||||
/*
|
||||
void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed, bool randomDecay, bool shorterTail, int cometMultiplier = 1) {
|
||||
// Validate inputs
|
||||
int numComets = colorPack.size;
|
||||
if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0) return;
|
||||
|
||||
// Calculate comet size
|
||||
int cometSize = (size / (numComets * cometMultiplier)) * COMET_SIZE_FACTOR;
|
||||
if (cometSize < 1) cometSize = 1;
|
||||
|
||||
// Set fade factor
|
||||
uint8_t fadeFactor = shorterTail ? COMET_FADE_FACTOR2 : COMET_FADE_FACTOR1;
|
||||
|
||||
// Initialize comet positions to be equally spaced
|
||||
std::unique_ptr<int[]> cometPositions(new (std::nothrow) int[numComets * cometMultiplier]);
|
||||
if (!cometPositions) return;
|
||||
int totalComets = numComets * cometMultiplier;
|
||||
int spacing = size / totalComets;
|
||||
for (int i = 0; i < totalComets; i++) {
|
||||
cometPositions[i] = i * spacing;
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
bool direction = true; // true = forward, false = backward
|
||||
int loopCounter = 0;
|
||||
try {
|
||||
const int loopTick = 30; // ms per animation tick
|
||||
Animation_Loop(activeFlag, loopTick, [&]() -> int {
|
||||
// Fade all LEDs
|
||||
for (int i = 0; i < size; i++) {
|
||||
if(!randomDecay) {
|
||||
leds[i].fadeToBlackBy(fadeFactor);
|
||||
} else if(random(10) > 5){
|
||||
leds[i].fadeToBlackBy(fadeFactor);
|
||||
}
|
||||
}
|
||||
|
||||
// Move and draw comets
|
||||
for (int i = 0; i < totalComets; i++) {
|
||||
if (direction) {
|
||||
cometPositions[i] = (cometPositions[i] + 1) % size;
|
||||
} else {
|
||||
cometPositions[i] = (cometPositions[i] - 1 + size) % size;
|
||||
}
|
||||
|
||||
// Draw comet with solid color
|
||||
CRGB color = colorPack.col[i % colorPack.size];
|
||||
for (int j = 0; j < cometSize; j++) {
|
||||
int pos = (cometPositions[i] - j + size) % size;
|
||||
leds[pos] += color;
|
||||
}
|
||||
}
|
||||
|
||||
loopCounter++;
|
||||
if(loopCounter >= (size * CYCLES_PER_DIRECTION)){
|
||||
direction = !direction;
|
||||
loopCounter = 0;
|
||||
}
|
||||
|
||||
FastLED.show();
|
||||
return 0;
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what());
|
||||
} catch (...) {
|
||||
ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop");
|
||||
}
|
||||
// No need to delete cometPositions as it is managed by std::unique_ptr
|
||||
}
|
||||
*/
|
||||
|
||||
void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int minSpeed, int maxSpeed, bool randomDecay, int cometMultiplier) {
|
||||
// Validate inputs
|
||||
int numComets = colorPack.size;
|
||||
@ -479,8 +655,8 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate comet size
|
||||
int cometSize = (size / totalComets) * COMET_SIZE_FACTOR;
|
||||
// Calculate comet size (use float math and round to avoid integer-division truncation)
|
||||
int cometSize = (int)(((float)size * (float)COMET_SIZE_FACTOR) / (float)totalComets + 0.5f);
|
||||
if (cometSize < 1) cometSize = 1;
|
||||
|
||||
// Set fade factor
|
||||
@ -500,9 +676,14 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
int pos;
|
||||
CRGB color;
|
||||
int speed = minSpeed;
|
||||
int loopDuration = (size * CYCLES_PER_DIRECTION);
|
||||
int loopDuration = size * CYCLES_PER_DIRECTION;
|
||||
// Defensive: ensure loopDuration isn't zero to avoid divide-by-zero later
|
||||
if (loopDuration < 1) loopDuration = 1;
|
||||
int thirdLoop = loopDuration / 3;
|
||||
int twoThirdLoop = (2 * loopDuration) / 3;
|
||||
// Defensive: ensure thirdLoop is at least 1 for progress calculations
|
||||
if (thirdLoop < 1) thirdLoop = 1;
|
||||
// Keep twoThirdLoop consistent with thirdLoop to avoid rounding surprises
|
||||
int twoThirdLoop = 2 * thirdLoop;
|
||||
try {
|
||||
Animation_Loop_Variable(activeFlag, [&]() -> int {
|
||||
// Fade all LEDs
|
||||
@ -558,7 +739,11 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
|
||||
FastLED.show();
|
||||
|
||||
return max(MaxSpeed - speed, MinLoopDelay);
|
||||
// Compute delay to return to Animation_Loop_Variable.
|
||||
// Ensure we never return a value less than MinLoopDelay.
|
||||
int computed = MaxSpeed - speed;
|
||||
if (computed < MinLoopDelay) computed = MinLoopDelay;
|
||||
return computed;
|
||||
});
|
||||
|
||||
fill_solid(leds, size, CRGB::Black);
|
||||
@ -569,6 +754,131 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA
|
||||
ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop");
|
||||
}
|
||||
}
|
||||
*/
|
||||
void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int minSpeed, int maxSpeed, bool randomDecay, int cometMultiplier) {
|
||||
// Validate inputs
|
||||
int numComets = colorPack.size;
|
||||
int totalComets = numComets * cometMultiplier;
|
||||
|
||||
if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0 || totalComets > MAX_COMETS) {
|
||||
ESP_LOGE("Anim_Comets", "Invalid input parameters or too many comets (max: %d, requested: %d)", MAX_COMETS, totalComets);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize speed inputs to known bounds
|
||||
minSpeed = constrain(minSpeed, 0, MaxSpeed);
|
||||
maxSpeed = constrain(maxSpeed, 0, MaxSpeed);
|
||||
if (minSpeed > maxSpeed) { int t=minSpeed; minSpeed=maxSpeed; maxSpeed=t; }
|
||||
|
||||
// Calculate comet size (use float math and round to avoid integer-division truncation)
|
||||
int cometSize = (int)(((float)size * (float)COMET_SIZE_FACTOR) / (float)totalComets + 0.5f);
|
||||
if (cometSize < 1) cometSize = 1;
|
||||
|
||||
// Set fade factor
|
||||
uint8_t fadeFactor = map(totalComets, 1, MAX_COMETS, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
|
||||
fadeFactor = constrain(fadeFactor, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
|
||||
|
||||
// Initialize comet positions with fixed array, evenly distributed
|
||||
int cometPositions[MAX_COMETS] = {0};
|
||||
for (int i = 0; i < totalComets; i++) {
|
||||
// Even distribution even when size not divisible by totalComets
|
||||
cometPositions[i] = (i * size) / totalComets;
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
bool direction = true; // true = forward, false = backward
|
||||
int loopCounter = 0;
|
||||
int pos;
|
||||
CRGB color;
|
||||
int speed = minSpeed;
|
||||
|
||||
// Cache size/limits locally for speed
|
||||
const int localSize = size;
|
||||
const int localTotalComets = totalComets;
|
||||
int loopDuration = localSize * CYCLES_PER_DIRECTION;
|
||||
// Defensive: ensure loopDuration isn't zero to avoid divide-by-zero later
|
||||
if (loopDuration < 1) loopDuration = 1;
|
||||
int thirdLoop = loopDuration / 3;
|
||||
if (thirdLoop < 1) thirdLoop = 1;
|
||||
int twoThirdLoop = 2 * thirdLoop;
|
||||
|
||||
try {
|
||||
Animation_Loop_Variable(activeFlag, [&]() -> int {
|
||||
// Fade all LEDs (keep this O(N) operation but keep it tight)
|
||||
for (int i = 0; i < localSize; i++) {
|
||||
if (!randomDecay) {
|
||||
leds[i].fadeToBlackBy(fadeFactor);
|
||||
} else if (getRandomValue(10) > 5) {
|
||||
leds[i].fadeToBlackBy(fadeFactor);
|
||||
}
|
||||
}
|
||||
|
||||
// Move and draw comets
|
||||
for (int i = 0; i < localTotalComets; i++) {
|
||||
if (direction) {
|
||||
cometPositions[i] = (cometPositions[i] + 1) % localSize;
|
||||
} else {
|
||||
cometPositions[i] = (cometPositions[i] - 1 + localSize) % localSize;
|
||||
}
|
||||
|
||||
// Draw comet with solid color (safe modulo)
|
||||
color = colorPack.col[i % colorPack.size];
|
||||
for (int j = 0; j < cometSize; j++) {
|
||||
// Tail follows the direction of movement
|
||||
pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j);
|
||||
pos = (pos % localSize + localSize) % localSize; // safe modulus
|
||||
leds[pos] += color;
|
||||
}
|
||||
}
|
||||
|
||||
// Speed ramping: 1/3 aggressive ramp up, 1/3 constant, 1/3 aggressive ramp down
|
||||
if (loopCounter < thirdLoop) {
|
||||
// First third: ease-out quartic implemented without pow()
|
||||
float progress = (float)loopCounter / (float)thirdLoop; // 0.0 to 1.0
|
||||
float oneMinus = 1.0f - progress;
|
||||
float eased = 1.0f - (oneMinus * oneMinus * oneMinus * oneMinus); // 1 - (1-t)^4
|
||||
speed = minSpeed + (int)((maxSpeed - minSpeed) * eased);
|
||||
}
|
||||
else if (loopCounter < twoThirdLoop) {
|
||||
// Middle third: constant at maxSpeed
|
||||
speed = maxSpeed;
|
||||
}
|
||||
else {
|
||||
// Last third: ease-in quartic implemented without pow()
|
||||
float progress = (float)(loopCounter - twoThirdLoop) / (float)thirdLoop; // 0.0 to 1.0
|
||||
if (progress < 0.0f) progress = 0.0f; if (progress > 1.0f) progress = 1.0f;
|
||||
float eased = (progress * progress * progress * progress); // t^4
|
||||
speed = maxSpeed - (int)((maxSpeed - minSpeed) * eased);
|
||||
}
|
||||
|
||||
if (++loopCounter >= loopDuration) {
|
||||
direction = !direction;
|
||||
loopCounter = 0;
|
||||
speed = minSpeed;
|
||||
}
|
||||
|
||||
FastLED.show();
|
||||
|
||||
// Compute delay to return to Animation_Loop_Variable.
|
||||
// Ensure we never return a value less than MinLoopDelay.
|
||||
int computed = MaxSpeed - speed;
|
||||
if (computed < MinLoopDelay) computed = MinLoopDelay;
|
||||
return computed;
|
||||
});
|
||||
|
||||
fill_solid(leds, size, CRGB::Black);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what());
|
||||
} catch (...) {
|
||||
ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop");
|
||||
}
|
||||
|
||||
// NOTE: For production/stability consider:
|
||||
// - Replacing volatile<bool> activeFlag with std::atomic<bool> or task-notify mechanism.
|
||||
// - If FastLED.show() blocks too long for very long strips consider chunked updates or an alternate driver.
|
||||
// - If pow() usage was widespread, replace with fixed-point or inline multiplies as done above.
|
||||
}
|
||||
|
||||
|
||||
void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0)
|
||||
@ -698,6 +1008,8 @@ void Anim_TimedFill_Flash(bool volatile& activeFlag, CRGB* leds, int size, PWM_O
|
||||
if (flashElapsed >= flashTimeout) {
|
||||
// Flash timeout expired, set PWM to minimum
|
||||
pwmOut->setOutput(pwmMin);
|
||||
// exit the loop
|
||||
activeFlag = false;
|
||||
ESP_LOGI("Anim_TimedFill_Flash", "Flash timeout expired, PWM set to minimum");
|
||||
}
|
||||
// Continue in infinite loop regardless of timeout status
|
||||
@ -754,9 +1066,8 @@ void Anim_SolidWhite(bool volatile& activeFlag, CRGB* leds, PWM_Output* pwmOut,
|
||||
// No animation, just maintain the solid white state
|
||||
return 0;
|
||||
});
|
||||
if(pwmOut){
|
||||
pwmOut->setOutput(0); // Turn off PWM output
|
||||
}
|
||||
|
||||
if(pwmOut){ pwmOut->setOutput(0); }// Turn off PWM output
|
||||
FastLED.setBrightness(origBright); // Restore original brightness
|
||||
}
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, con
|
||||
if (buffer_size < 1024) buffer_size = 1024; // Absolute minimum is 1KB
|
||||
|
||||
downloadBuffer.reset(new uint8_t[buffer_size]);
|
||||
downloadBufferSize = buffer_size;
|
||||
|
||||
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
|
||||
// Ensure baseUrl ends with a single '/'
|
||||
@ -81,6 +82,7 @@ AppUpdater::ManifestCheckResult AppUpdater::checkManifest() {
|
||||
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;
|
||||
@ -159,6 +161,7 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Local file does not exist: %s", localPath);
|
||||
}
|
||||
|
||||
if(skip){
|
||||
ESP_LOGI(TAG, "File already up to date: %s", localPath);
|
||||
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
|
||||
@ -179,9 +182,12 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
||||
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 for %s: HTTP code %d", localPath, httpCode);
|
||||
updateProgress(UpdateStatus::ERROR, 0, String(String("Download failed: ") + localPath).c_str());
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Download failed: %s", localPath);
|
||||
updateProgress(UpdateStatus::ERROR, 0, buf);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
@ -215,33 +221,36 @@ bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const
|
||||
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* remotePath, const char* expectedMd5)
|
||||
{
|
||||
const int MAX_RETRIES = 2; // Maximum number of retries for MD5 failure
|
||||
|
||||
|
||||
for (int retry = 0; retry <= MAX_RETRIES; retry++) {
|
||||
HTTPClient retryHttp; // kept alive for the duration of this iteration if used
|
||||
WiFiClient* activeStream = stream;
|
||||
size_t activeContentLength = contentLength;
|
||||
|
||||
if (retry > 0) {
|
||||
ESP_LOGW(TAG, "Retrying download of %s (attempt %d/%d)", localPath, retry, MAX_RETRIES);
|
||||
// Need to re-fetch the file for retry - use the REMOTE path, not local path
|
||||
String url = buildUrl(remotePath);
|
||||
HTTPClient http;
|
||||
http.begin(url);
|
||||
int httpCode = http.GET();
|
||||
retryHttp.begin(url);
|
||||
int httpCode = retryHttp.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "Retry download failed: %d", httpCode);
|
||||
http.end();
|
||||
retryHttp.end();
|
||||
continue; // Try next retry if available
|
||||
}
|
||||
stream = http.getStreamPtr();
|
||||
contentLength = http.getSize();
|
||||
activeStream = retryHttp.getStreamPtr();
|
||||
activeContentLength = retryHttp.getSize();
|
||||
}
|
||||
|
||||
|
||||
MD5Builder md5;
|
||||
md5.begin();
|
||||
size_t totalRead = 0;
|
||||
|
||||
|
||||
// Create temporary filename in the same directory as the target file
|
||||
String targetDir = String(localPath);
|
||||
int lastSlash = targetDir.lastIndexOf('/');
|
||||
String tempPath;
|
||||
|
||||
|
||||
if (lastSlash >= 0) {
|
||||
// Extract directory and filename
|
||||
String directory = targetDir.substring(0, lastSlash + 1);
|
||||
@ -251,62 +260,84 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
// File is in root directory
|
||||
tempPath = "/temp_" + String(localPath) + ".download";
|
||||
}
|
||||
|
||||
|
||||
// Clean up any existing temp file first
|
||||
if (fileSystem.exists(tempPath.c_str())) {
|
||||
ESP_LOGW(TAG, "Removing existing temp file: %s", tempPath.c_str());
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
}
|
||||
|
||||
|
||||
ESP_LOGI(TAG, "Using temp file path: %s for target: %s", tempPath.c_str(), localPath);
|
||||
|
||||
ESP_LOGI(TAG, "verifyAndSaveFile: downloadBufferSize=%u", (unsigned)downloadBufferSize);
|
||||
|
||||
// Check available space if content length is known
|
||||
if (activeContentLength > 0) {
|
||||
size_t freeBytes = LittleFS.totalBytes() - LittleFS.usedBytes();
|
||||
// Leave a small headroom (5%) to avoid filling filesystem completely
|
||||
if (activeContentLength > freeBytes * 95 / 100) {
|
||||
ESP_LOGE(TAG, "Not enough LittleFS space for %s (%u bytes needed, %u bytes free)",
|
||||
localPath, (unsigned)activeContentLength, (unsigned)freeBytes);
|
||||
if (retry > 0) retryHttp.end();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Open temporary file for writing (LittleFS will create directories automatically)
|
||||
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE, true); // true = create if not exists
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG, "Failed to open temporary file for writing: %s", tempPath.c_str());
|
||||
|
||||
|
||||
// Try to diagnose the issue
|
||||
ESP_LOGE(TAG, "LittleFS info - Used: %u bytes, Total: %u bytes",
|
||||
LittleFS.usedBytes(), LittleFS.totalBytes());
|
||||
|
||||
ESP_LOGE(TAG, "LittleFS info - Used: %u bytes, Total: %u bytes",
|
||||
(unsigned)LittleFS.usedBytes(), (unsigned)LittleFS.totalBytes());
|
||||
|
||||
// Check if we're out of space
|
||||
if (LittleFS.usedBytes() >= LittleFS.totalBytes() * 0.95) {
|
||||
ESP_LOGE(TAG, "LittleFS nearly full - may not have space for temp file");
|
||||
}
|
||||
|
||||
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
|
||||
if (contentLength > 0) {
|
||||
|
||||
// Defensive: ensure download buffer exists
|
||||
if (!downloadBuffer || downloadBufferSize == 0) {
|
||||
ESP_LOGE(TAG, "No download buffer available");
|
||||
file.close();
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeContentLength > 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();
|
||||
while (totalRead < activeContentLength) {
|
||||
if(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); if (retry > 0) retryHttp.end(); return false; }
|
||||
size_t available = activeStream->available();
|
||||
if (available) {
|
||||
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
|
||||
|
||||
size_t readLen = activeStream->readBytes(downloadBuffer.get(), std::min(available, downloadBufferSize));
|
||||
|
||||
// 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());
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
md5.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / contentLength , localPath);
|
||||
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 80) / activeContentLength , 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(g_UpdateCancelFlag){ file.close(); fileSystem.remove(tempPath.c_str()); if (retry > 0) retryHttp.end(); return false; }
|
||||
size_t readLen = activeStream->readBytes(downloadBuffer.get(), downloadBufferSize);
|
||||
if (readLen == 0) {
|
||||
break;
|
||||
}
|
||||
@ -314,6 +345,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||
file.close();
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
md5.add(downloadBuffer.get(), readLen);
|
||||
@ -326,44 +358,45 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
file.close();
|
||||
md5.calculate();
|
||||
String calculatedMd5 = md5.toString();
|
||||
|
||||
|
||||
// Verify MD5 hash
|
||||
updateProgress(UpdateStatus::VERIFYING, 90, localPath);
|
||||
|
||||
ESP_LOGI(TAG, "MD5 verification for %s: Expected='%s', Calculated='%s'",
|
||||
|
||||
ESP_LOGI(TAG, "MD5 verification for %s: Expected='%s', Calculated='%s'",
|
||||
localPath, expectedMd5, calculatedMd5.c_str());
|
||||
|
||||
|
||||
// Compare MD5 case-insensitively (in case there are case differences)
|
||||
String expectedMd5Lower = String(expectedMd5);
|
||||
expectedMd5Lower.toLowerCase();
|
||||
String calculatedMd5Lower = calculatedMd5;
|
||||
calculatedMd5Lower.toLowerCase();
|
||||
|
||||
|
||||
if (!calculatedMd5Lower.equals(expectedMd5Lower)) {
|
||||
ESP_LOGE(TAG, "MD5 mismatch for %s (attempt %d/%d). Expected: %s, Got: %s",
|
||||
ESP_LOGE(TAG, "MD5 mismatch for %s (attempt %d/%d). Expected: %s, Got: %s",
|
||||
localPath, retry+1, MAX_RETRIES+1, expectedMd5, calculatedMd5.c_str());
|
||||
ESP_LOGE(TAG, "Length comparison - Expected: %d chars, Got: %d chars",
|
||||
strlen(expectedMd5), calculatedMd5.length());
|
||||
ESP_LOGE(TAG, "Length comparison - Expected: %d chars, Got: %d chars",
|
||||
(int)strlen(expectedMd5), (int)calculatedMd5.length());
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
|
||||
|
||||
if (retry < MAX_RETRIES) {
|
||||
// Will retry in next loop iteration
|
||||
if (retry > 0) retryHttp.end();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Special case for certain file types - allow them to be used even with MD5 mismatch
|
||||
// This is a fallback option for non-critical files like HTML pages
|
||||
bool isNonCriticalFile = false;
|
||||
if (String(localPath).endsWith(".html") ||
|
||||
String(localPath).endsWith(".css") ||
|
||||
if (String(localPath).endsWith(".html") ||
|
||||
String(localPath).endsWith(".css") ||
|
||||
String(localPath).endsWith(".js")) {
|
||||
isNonCriticalFile = true;
|
||||
}
|
||||
|
||||
|
||||
if (isNonCriticalFile) {
|
||||
ESP_LOGW(TAG, "Using file %s despite MD5 mismatch (non-critical file)", localPath);
|
||||
// We'll still keep this file but report it as a verification failure
|
||||
@ -383,26 +416,29 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rename the temp file to the final location
|
||||
if (fileSystem.exists(localPath)) {
|
||||
fileSystem.remove(localPath);
|
||||
}
|
||||
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
|
||||
ESP_LOGE(TAG, "Failed to rename temporary file for non-critical use: %s -> %s",
|
||||
ESP_LOGE(TAG, "Failed to rename temporary file for non-critical use: %s -> %s",
|
||||
tempPath.c_str(), localPath);
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
if (retry > 0) retryHttp.end();
|
||||
// Return false to indicate verification failure, but the file will still be used
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
updateProgress(UpdateStatus::VERIFYING, 95, localPath);
|
||||
|
||||
|
||||
// Ensure target directory exists before rename
|
||||
String dirPath = String(localPath);
|
||||
int targetLastSlash = dirPath.lastIndexOf('/');
|
||||
@ -419,7 +455,7 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Replace original file with verified temp file
|
||||
if (fileSystem.exists(localPath)) {
|
||||
fileSystem.remove(localPath);
|
||||
@ -427,13 +463,15 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con
|
||||
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
|
||||
ESP_LOGE(TAG, "Failed to rename temporary file: %s -> %s", tempPath.c_str(), localPath);
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
if (retry > 0) retryHttp.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
updateProgress(UpdateStatus::VERIFYING, 100, localPath);
|
||||
if (retry > 0) retryHttp.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false; // All retries failed
|
||||
}
|
||||
|
||||
@ -450,7 +488,7 @@ String AppUpdater::getLocalMD5(const char* filePath){
|
||||
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)));
|
||||
readLen = file.readBytes(reinterpret_cast<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, downloadBufferSize));
|
||||
md5Builder.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
}
|
||||
@ -569,6 +607,7 @@ bool AppUpdater::updateApp() {
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -662,12 +701,12 @@ bool AppUpdater::updateApp() {
|
||||
|
||||
while (remaining > 0 && failedReads < MAX_FAILED_READS) {
|
||||
// Check for cancellation
|
||||
if(g_UpdateCancelFlag) {
|
||||
ESP_LOGE(TAG, "Update cancelled by user");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
if(g_UpdateCancelFlag) {
|
||||
ESP_LOGE(TAG, "Update cancelled by user");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check WiFi status every 5 seconds
|
||||
if (millis() - lastWatchdogKick > 5000) {
|
||||
@ -735,8 +774,9 @@ bool AppUpdater::updateApp() {
|
||||
// Send periodic progress updates to keep client informed
|
||||
if (millis() - lastWatchdogKick > 5000) {
|
||||
int percent = (totalReceived * 100) / firmwareSize;
|
||||
updateProgress(UpdateStatus::DOWNLOADING, percent,
|
||||
String("Firmware: " + String(percent) + "% - waiting for data...").c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Firmware: %d%% - waiting for data...", percent);
|
||||
updateProgress(UpdateStatus::DOWNLOADING, percent, buf);
|
||||
}
|
||||
|
||||
delay(100); // Short delay to prevent CPU hogging
|
||||
@ -782,8 +822,9 @@ bool AppUpdater::updateApp() {
|
||||
lastProgressTime = millis();
|
||||
|
||||
// Just update with received byte count since we don't know total
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0,
|
||||
String("Firmware: " + String(totalReceived / 1024) + "KB received").c_str());
|
||||
char buf2[64];
|
||||
snprintf(buf2, sizeof(buf2), "Firmware: %uKB received", (unsigned)(totalReceived / 1024));
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0, buf2);
|
||||
} else {
|
||||
emptyReads++;
|
||||
delay(100);
|
||||
@ -878,115 +919,122 @@ void firmwareUpdateTask(void* parameter) {
|
||||
esp_task_wdt_init(60, true); // 60 second timeout, panic on timeout
|
||||
esp_task_wdt_add(NULL); // Add current task to watchdog
|
||||
|
||||
try {
|
||||
loadUpdateJson();
|
||||
|
||||
esp_task_wdt_reset(); // Reset watchdog timer after JSON loading
|
||||
|
||||
// Initialize updater with smart pointer
|
||||
std::unique_ptr<AppUpdater> 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());
|
||||
updateProgress(AppUpdater::UpdateStatus::MESSAGE, 0, errorMsg.c_str());
|
||||
// Don't throw, just exit gracefully
|
||||
break;
|
||||
default:
|
||||
errorMsg = "Unknown manifest check error";
|
||||
}
|
||||
|
||||
if (manifestResult != AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
|
||||
ESP_LOGE(TAG, "Manifest check failed: %s", errorMsg.c_str());
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, errorMsg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (updater->IsUpdateAvailable()) {
|
||||
bool filesUpdated = true;
|
||||
bool firmwareUpdated = false; // Initialize to false - only set to true if firmware is actually updated
|
||||
|
||||
// Update files based on update mode
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
|
||||
ESP_LOGI(TAG, "Update mode includes files, updating files...");
|
||||
filesUpdated = updater->updateFilesArray();
|
||||
if (!filesUpdated) {
|
||||
ESP_LOGW(TAG, "Some files failed to update");
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY) {
|
||||
ESP_LOGE(TAG, "Files-only update failed");
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update files");
|
||||
// Skip to cleanup since this is files-only mode and it failed
|
||||
goto cleanup;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "File update failed, but continuing with firmware update");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Skipping file updates (mode: firmware only)");
|
||||
}
|
||||
|
||||
// Update firmware based on update mode
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
|
||||
ESP_LOGI(TAG, "Update mode includes firmware, updating firmware...");
|
||||
firmwareUpdated = updater->updateApp();
|
||||
if (!firmwareUpdated) {
|
||||
ESP_LOGE(TAG, "Failed to update firmware");
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update firmware");
|
||||
// Skip to cleanup since firmware update failed
|
||||
goto cleanup;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Skipping firmware update (mode: files only)");
|
||||
}
|
||||
|
||||
// Determine if we need to restart
|
||||
bool needsRestart = (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) && firmwareUpdated;
|
||||
|
||||
if (needsRestart) {
|
||||
ESP_LOGI(TAG, "Firmware update successful, restarting...");
|
||||
sendUpdateMessage("Restarting... ", true, 100);
|
||||
vTaskDelay(2000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Update completed successfully (no restart required)");
|
||||
updateProgress(AppUpdater::UpdateStatus::COMPLETE, 100, "Update completed successfully");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE(TAG, "Update failed with exception: %s", e.what());
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, e.what());
|
||||
} catch (...) {
|
||||
ESP_LOGE(TAG, "Update failed with unknown exception");
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Unknown error during update");
|
||||
// Load update.json; proceed only if successful
|
||||
if (!loadUpdateJson()) {
|
||||
ESP_LOGE(TAG, "Failed to load update.json, aborting update task");
|
||||
// Clean up watchdog and exit task
|
||||
esp_task_wdt_delete(NULL);
|
||||
Update_Task_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_task_wdt_reset(); // Reset watchdog timer after JSON loading
|
||||
|
||||
// Initialize updater with smart pointer
|
||||
std::unique_ptr<AppUpdater> 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());
|
||||
updateProgress(AppUpdater::UpdateStatus::MESSAGE, 0, errorMsg.c_str());
|
||||
// Don't throw, just exit gracefully
|
||||
break;
|
||||
default:
|
||||
errorMsg = "Unknown manifest check error";
|
||||
}
|
||||
|
||||
if (manifestResult != AppUpdater::ManifestCheckResult::VERSION_CURRENT) {
|
||||
ESP_LOGE(TAG, "Manifest check failed: %s", errorMsg.c_str());
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, errorMsg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (updater->IsUpdateAvailable()) {
|
||||
bool filesUpdated = true;
|
||||
bool firmwareUpdated = false; // Initialize to false - only set to true if firmware is actually updated
|
||||
|
||||
// Update files based on update mode
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
|
||||
ESP_LOGI(TAG, "Update mode includes files, updating files...");
|
||||
filesUpdated = updater->updateFilesArray();
|
||||
if (!filesUpdated) {
|
||||
ESP_LOGW(TAG, "Some files failed to update");
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FILES_ONLY) {
|
||||
ESP_LOGE(TAG, "Files-only update failed");
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update files");
|
||||
// Clean up and exit task
|
||||
esp_task_wdt_delete(NULL);
|
||||
Update_Task_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "File update failed, but continuing with firmware update");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Skipping file updates (mode: firmware only)");
|
||||
}
|
||||
|
||||
// Update firmware based on update mode
|
||||
if (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) {
|
||||
ESP_LOGI(TAG, "Update mode includes firmware, updating firmware...");
|
||||
firmwareUpdated = updater->updateApp();
|
||||
if (!firmwareUpdated) {
|
||||
ESP_LOGE(TAG, "Failed to update firmware");
|
||||
updateProgress(AppUpdater::UpdateStatus::ERROR, 0, "Failed to update firmware");
|
||||
// Clean up and exit task
|
||||
esp_task_wdt_delete(NULL);
|
||||
Update_Task_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Skipping firmware update (mode: files only)");
|
||||
}
|
||||
|
||||
// Determine if we need to restart
|
||||
bool needsRestart = (g_UpdateMode == UpdateMode::UPDATE_FIRMWARE_ONLY || g_UpdateMode == UpdateMode::UPDATE_BOTH) && firmwareUpdated;
|
||||
|
||||
if (needsRestart) {
|
||||
ESP_LOGI(TAG, "Firmware update successful, restarting...");
|
||||
sendUpdateMessage("Restarting... ", true, 100);
|
||||
vTaskDelay(2000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Update completed successfully (no restart required)");
|
||||
updateProgress(AppUpdater::UpdateStatus::COMPLETE, 100, "Update completed successfully");
|
||||
}
|
||||
}
|
||||
|
||||
// No C++ exceptions used - errors are handled inline and via return codes
|
||||
|
||||
cleanup:
|
||||
// Clean up watchdog before exit
|
||||
@ -1007,7 +1055,12 @@ void startVersionCheckTask() {
|
||||
|
||||
void versionCheckTask(void* parameter){
|
||||
if(updateUrl == ""){
|
||||
loadUpdateJson();
|
||||
if(!loadUpdateJson()){
|
||||
ESP_LOGE(TAG, "versionCheckTask: failed to load update.json");
|
||||
versionCheckTask_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
AppUpdater updater(LittleFS, localVersion, updateUrl.c_str(), "manifest.json", "firmware.bin");
|
||||
|
||||
@ -1025,35 +1078,36 @@ void versionCheckTask(void* parameter){
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void loadUpdateJson(void) {
|
||||
try {
|
||||
ESP_LOGD(TAG, "loadUpdateJaon function...");
|
||||
if(updateUrl == "") {
|
||||
String updateJsonPath = "/system/update.json";
|
||||
bool loadUpdateJson(void) {
|
||||
ESP_LOGD(TAG, "loadUpdateJson 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());
|
||||
// Read and parse update.json
|
||||
File file = LittleFS.open(updateJsonPath);
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG, "Failed to open update.json");
|
||||
return false;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
ESP_LOGE(TAG, "Failed to parse update.json: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
#include "ATALights.h"
|
||||
#include "BleSettings.h"
|
||||
#include <esp_task_wdt.h>
|
||||
#include "my_device.h"
|
||||
#include "global.h" // for get_chip_mac
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *tag = "BLE_SP110E";
|
||||
|
||||
@ -33,7 +36,6 @@ TaskHandle_t LightStick_Client_Task_Handle = NULL;
|
||||
|
||||
//#define UPGRADE_SERVICE_UUID "abcdef01-2345-6789-1234-56789abcdef0"
|
||||
//#define UPGRADE_CHARACTERISTIC_UUID "abcdef01-2345-6789-1234-56789abcdef1"
|
||||
|
||||
//typedef enum {SM16703,TM1804,UCS1903,WS2811,WS2801,SK6812,LPD6803,LPD8806,APA102,APA105,DMX512,TM1914,TM1913,P9813,INK1003,P943S,P9411,P9413,TX1812,TX1813,GS8206,GS8208,SK9822,TM1814,SK6812_RGBW,P9414,PG412,IC_MODEL_COUNT } IC_MODELS;
|
||||
//typedef enum {RGB,RBG,GRB,GBR,BRG,BGR,SEQUENCE_COUNT} SEQUENCES;
|
||||
|
||||
@ -133,6 +135,78 @@ public:
|
||||
process_BLE_SP110E_Command(data, procLen, pCharacteristic);
|
||||
}
|
||||
|
||||
// Respond to read requests by returning the current led_status structure
|
||||
void onRead(NimBLECharacteristic* pCharacteristic) override {
|
||||
if (!pCharacteristic) return;
|
||||
|
||||
USER_SETTINGS userSettings = {};
|
||||
|
||||
userSettings.cmd = 0; // Indicate this is a status response
|
||||
userSettings.limitedMode = sys_settings.limitedMode ? 1 : 0;
|
||||
userSettings.profile[9] = '\0'; // Ensure null-terminated
|
||||
{
|
||||
// sys_settings.profile is an Arduino String; use c_str() to obtain a const char*
|
||||
const char* src = sys_settings.profile.c_str();
|
||||
// copy up to size-1 characters to ensure null-termination
|
||||
size_t maxCopy = sizeof(userSettings.profile) - 1;
|
||||
size_t srcLen = strlen(src);
|
||||
size_t copyLen = srcLen < maxCopy ? srcLen : maxCopy;
|
||||
if (copyLen) memcpy(userSettings.profile, src, copyLen);
|
||||
// null terminate right after copied data
|
||||
userSettings.profile[copyLen] = '\0';
|
||||
// ensure the remaining bytes are zeroed out
|
||||
for (size_t i = copyLen + 1; i < sizeof(userSettings.profile); ++i) userSettings.profile[i] = '\0';
|
||||
}
|
||||
|
||||
userSettings.temperature = boardTemperature; // Placeholder, implement actual temperature reading if needed
|
||||
userSettings.vIn = PowerVin; // Placeholder, implement actual voltage reading if needed
|
||||
|
||||
// Determine chipset with case‐insensitive substring match
|
||||
std::string chip = std::string(sys_settings.ledStripSettings[0]->chip.c_str());
|
||||
std::string chipLower;
|
||||
chipLower.resize(chip.size());
|
||||
std::transform(chip.begin(), chip.end(), chipLower.begin(), [](unsigned char c){ return std::tolower(c); });
|
||||
if (chipLower.find("WS2812b") != std::string::npos) userSettings.ledChipset1 = 0;
|
||||
else if (chipLower.find("SK6812") != std::string::npos) userSettings.ledChipset1 = 1;
|
||||
else if (chipLower.find("WS2811") != std::string::npos) userSettings.ledChipset1 = 2;
|
||||
else if (chipLower.find("WS2815") != std::string::npos) userSettings.ledChipset1 = 3;
|
||||
else userSettings.ledChipset1 = 0;
|
||||
|
||||
|
||||
strncpy(userSettings.rgbOrder1, sys_settings.ledStripSettings[0]->rgbOrder.c_str(), 4);
|
||||
userSettings.ledCount1 = sys_settings.ledStripSettings[0]->size;
|
||||
userSettings.ledShift1 = sys_settings.ledStripSettings[0]->shift;
|
||||
userSettings.ledBrightness1= sys_settings.ledStripSettings[0]->bright;
|
||||
|
||||
std::string chip2 = std::string(sys_settings.ledStripSettings[1]->chip.c_str());
|
||||
std::string chipLower2;
|
||||
chipLower2.resize(chip2.size());
|
||||
std::transform(chip2.begin(), chip2.end(), chipLower2.begin(),
|
||||
[](unsigned char c){ return std::tolower(c); });
|
||||
if (chipLower2.find("WS2812b") != std::string::npos) userSettings.ledChipset2 = 0;
|
||||
else if (chipLower2.find("SK6812") != std::string::npos) userSettings.ledChipset2 = 1;
|
||||
else if (chipLower2.find("WS2811") != std::string::npos) userSettings.ledChipset2 = 2;
|
||||
else if (chipLower2.find("WS2815") != std::string::npos) userSettings.ledChipset2 = 3;
|
||||
else userSettings.ledChipset2 = 0;
|
||||
|
||||
strncpy(userSettings.rgbOrder2, sys_settings.ledStripSettings[1]->rgbOrder.c_str(), 4);
|
||||
userSettings.ledCount2 = sys_settings.ledStripSettings[1]->size;
|
||||
userSettings.ledShift2 = sys_settings.ledStripSettings[1]->shift;
|
||||
userSettings.ledBrightness2= sys_settings.ledStripSettings[1]->bright;
|
||||
|
||||
userSettings.frontLightMin = sys_settings.rampLightSettings[0].min;
|
||||
userSettings.frontLightMax = sys_settings.rampLightSettings[0].max;
|
||||
userSettings.rearLightMin = sys_settings.rampLightSettings[1].min;
|
||||
userSettings.rearLightMax = sys_settings.rampLightSettings[1].max;
|
||||
userSettings.fanLowerTemp = sys_settings.tSensorSettings.setpoint1;
|
||||
userSettings.fanUpperTemp = sys_settings.tSensorSettings.setpoint2;
|
||||
|
||||
// Provide the full INFO_PACK as the characteristic value so readers get device info
|
||||
pCharacteristic->setValue((uint8_t *)&userSettings, sizeof(USER_SETTINGS));
|
||||
// Do not automatically notify on read (notify is for subscriptions), but keep a log
|
||||
ESP_LOGD(tag, "onRead: returning INFO_PACK (%zu bytes)", sizeof(INFO_PACK));
|
||||
}
|
||||
|
||||
private:
|
||||
static void logBytes(const uint8_t* data, size_t len) {
|
||||
if (!data) return;
|
||||
@ -222,20 +296,24 @@ void sendToAllClients(const uint8_t *data, size_t len) {
|
||||
|
||||
|
||||
void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) {
|
||||
if (!val) {
|
||||
ESP_LOGE(tag, "Null command data received");
|
||||
return;
|
||||
}
|
||||
|
||||
if (len < 4) {
|
||||
ESP_LOGW(tag, "Command too short: %d bytes, expected at least 4", len);
|
||||
return;
|
||||
//uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer
|
||||
|
||||
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; }
|
||||
|
||||
//ESP_LOGI(tag, "USER_SETTING size is: %d", sizeof(USER_SETTINGS));
|
||||
|
||||
uint8_t command = 0;
|
||||
if (len == 4){
|
||||
command = val[3];
|
||||
}else if (len == sizeof(USER_SETTINGS)){
|
||||
command = val[0];
|
||||
//ESP_LOGI(tag, "Command received: 0x%02X, length: %d", command, len);
|
||||
//ESP_LOGI(tag, "byte1: %d byte2: %d byte3: %d byte4: %d byte5: %d byte6: %d byte7: %d byte8: %d",
|
||||
// val[1], val[2], val[3], val[4], val[5], val[6], val[7], val[8]);
|
||||
}
|
||||
|
||||
uint8_t command = val[3];
|
||||
//ESP_LOGI(tag, "Command received: 0x%02X, length: %d", command, len);
|
||||
|
||||
uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer
|
||||
ESP_LOGI(tag, "Command received: 0x%02X, data0: %d, data1: %d, data2: %d, length: %d", command, val[0], val[1], val[2], len);
|
||||
|
||||
// Handle different commands
|
||||
switch (command) {
|
||||
@ -273,7 +351,7 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
|
||||
break;
|
||||
case SET_SPEED:
|
||||
led_status.speed = val[0];
|
||||
ESP_LOGI(tag, "Mode set to %d", led_status.speed);
|
||||
ESP_LOGI(tag, "Speed set to %d", led_status.speed);
|
||||
break;
|
||||
case GET_CHECK_DEVICE: // This prepends a checksum
|
||||
led_status.checksum = calculateChecksum(val);
|
||||
@ -307,9 +385,42 @@ void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacter
|
||||
case SET_DEVICE_NAME:
|
||||
ESP_LOGI(tag, "Set Device Name");
|
||||
break;
|
||||
case SET_SHIFT:
|
||||
RGB_Lights_Set_Animation(SHIFT_INDEX, val[0], val[1], val[2]);
|
||||
ESP_LOGI(tag, "Set Shift to %d", val[0]);
|
||||
break;
|
||||
case SET_USER_SETTINGS:{
|
||||
USER_SETTINGS userSettings;
|
||||
|
||||
if (len < sizeof(USER_SETTINGS)) {
|
||||
ESP_LOGW(tag, "SET_USER_SETTINGS command too short: %d bytes, expected at least %d", len, sizeof(USER_SETTINGS));
|
||||
break;
|
||||
}
|
||||
memcpy(&userSettings, &val[0], sizeof(USER_SETTINGS) );
|
||||
|
||||
// Print out all the received settings for debugging
|
||||
/*
|
||||
ESP_LOGI(tag, "Received User Settings:");
|
||||
ESP_LOGI(tag, " Limited Mode: %d", userSettings.limitedMode);
|
||||
|
||||
ESP_LOGI(tag, " LED Chip Type: %d", userSettings.ledChipset1);
|
||||
ESP_LOGI(tag, " LED Color Order: %s", userSettings.rgbOrder1);
|
||||
ESP_LOGI(tag, " LED Count: %d", userSettings.ledCount1);
|
||||
ESP_LOGI(tag, " LED Shift: %d", userSettings.ledShift1);
|
||||
ESP_LOGI(tag, " LED Brightness: %d", userSettings.ledBrightness1);
|
||||
|
||||
ESP_LOGI(tag, " Fan Lower Temp: %d", userSettings.fanLowerTemp);
|
||||
ESP_LOGI(tag, " Fan Upper Temp: %d", userSettings.fanUpperTemp);
|
||||
*/
|
||||
|
||||
SetAndSaveUserSettings(userSettings);
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGW(tag, "Unknown command: 0x%02X", command);
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,7 +442,7 @@ void Init_BLE_SP110E(NimBLEServer* pServer) {
|
||||
// Create FFE1 Characteristic with WRITE and NOTIFY properties
|
||||
pSP110ECharacteristic = pService->createCharacteristic(
|
||||
BTSP110ECharacteristicUUID.c_str(),
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
|
||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
// Register the callback with the characteristic
|
||||
@ -422,11 +533,8 @@ void BLE_LightStick_Client_Task(void *parameter) {
|
||||
// Get the characteristic.
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(BTStickCharacteristicUUID.c_str());
|
||||
if (pRemoteCharacteristic != nullptr && pRemoteCharacteristic->canNotify()) {
|
||||
pRemoteCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pRemoteCharacteristic,
|
||||
uint8_t* pData, size_t length, bool isNotify) {
|
||||
Serial.print("Notification received: ");
|
||||
Serial.write(pData, length);
|
||||
Serial.println();
|
||||
pRemoteCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
|
||||
ESP_LOGD(tag, "Notification received: %zu bytes", length);
|
||||
process_BLE_SP110E_Command(pData, length, nullptr);
|
||||
});
|
||||
} else {
|
||||
@ -483,7 +591,7 @@ class MyAdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks {
|
||||
NimBLEDevice::getScan()->stop();
|
||||
myDevice = advertisedDevice;
|
||||
masterFound = true;
|
||||
Serial.println("Device found!");
|
||||
ESP_LOGE(tag, "Device found!");
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -38,35 +38,87 @@ class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
public:
|
||||
void onConnect(NimBLEServer* /*pServer*/) override {
|
||||
ESP_LOGI(tag, "Client connected");
|
||||
ensureAdvertising("onConnect");
|
||||
ServerCallbacks() : connectedClients(0) {}
|
||||
|
||||
void onConnect(NimBLEServer* pServer) override {
|
||||
// Use server's connected count to avoid double-handling between overloads
|
||||
int count = 0;
|
||||
if (pServer) count = pServer->getConnectedCount();
|
||||
connectedClients = count;
|
||||
ESP_LOGI(tag, "Client connected (count=%d) [no-desc overload]", connectedClients);
|
||||
if (connectedClients == 1) {
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
if (adv && adv->isAdvertising()) {
|
||||
if (adv->stop()) ESP_LOGI(tag, "Advertising stopped after client connected");
|
||||
else ESP_LOGE(tag, "Failed to stop advertising after connect");
|
||||
}
|
||||
} else if (connectedClients > 1) {
|
||||
ESP_LOGW(tag, "Additional client connected while one is active (no-desc)");
|
||||
}
|
||||
Buzzer_Play_Tune(TUNE_CONNECTED, 1);
|
||||
}
|
||||
|
||||
// This overload provides connection descriptor details (conn handle) so we can reject extra clients.
|
||||
void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) override {
|
||||
// desc contains the connection handle for this new client
|
||||
int connHandle = desc ? desc->conn_handle : -1;
|
||||
// Use server's connected count to determine how many clients are attached
|
||||
int count = 0;
|
||||
if (pServer) count = pServer->getConnectedCount();
|
||||
connectedClients = count;
|
||||
ESP_LOGI(tag, "Client connected (count=%d) conn_handle=%d", connectedClients, connHandle);
|
||||
|
||||
// If this is the first client, stop advertising so no additional clients can connect
|
||||
if (connectedClients == 1) {
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
if (adv && adv->isAdvertising()) {
|
||||
if (adv->stop()) ESP_LOGI(tag, "Advertising stopped after client connected");
|
||||
else ESP_LOGE(tag, "Failed to stop advertising after connect");
|
||||
}
|
||||
} else if (connectedClients > 1) {
|
||||
// Reject this new connection immediately to enforce single-client policy
|
||||
ESP_LOGW(tag, "Rejecting extra client conn_handle=%d (server count=%d)", connHandle, connectedClients);
|
||||
if (pServer) {
|
||||
try {
|
||||
pServer->disconnect(connHandle);
|
||||
ESP_LOGI(tag, "Disconnected extra client conn_handle=%d", connHandle);
|
||||
} catch (...) {
|
||||
ESP_LOGE(tag, "Exception while disconnecting extra client conn_handle=%d", connHandle);
|
||||
}
|
||||
}
|
||||
// Update connectedClients after rejecting (get fresh count if possible)
|
||||
if (pServer) connectedClients = pServer->getConnectedCount();
|
||||
}
|
||||
//Buzzer_Play_Tune(TUNE_CONNECTED, 1);
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* /*pServer*/) override {
|
||||
ESP_LOGI(tag, "Client disconnected");
|
||||
ensureAdvertising("onDisconnect");
|
||||
if (connectedClients > 0) connectedClients--;
|
||||
ESP_LOGI(tag, "Client disconnected (count=%d)", connectedClients);
|
||||
|
||||
// If there are no clients left, restart advertising so a new client can connect
|
||||
if (connectedClients == 0) {
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
if (!adv) {
|
||||
ESP_LOGE(tag, "Advertising object unavailable on disconnect");
|
||||
return;
|
||||
}
|
||||
if (adv->isAdvertising()) {
|
||||
ESP_LOGD(tag, "Advertising already running on disconnect");
|
||||
return;
|
||||
}
|
||||
if (adv->start()) {
|
||||
ESP_LOGI(tag, "Advertising restarted after client disconnect");
|
||||
} else {
|
||||
ESP_LOGE(tag, "Failed to start advertising after disconnect");
|
||||
}
|
||||
}
|
||||
|
||||
Buzzer_Play_Tune(TUNE_DISCONNECTED, 1);
|
||||
}
|
||||
|
||||
private:
|
||||
void ensureAdvertising(const char* reason) {
|
||||
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||
if (!adv) {
|
||||
ESP_LOGE(tag, "[%s] Advertising object unavailable", reason);
|
||||
return;
|
||||
}
|
||||
if (adv->isAdvertising()) {
|
||||
ESP_LOGD(tag, "[%s] Advertising already running", reason);
|
||||
return;
|
||||
}
|
||||
if (adv->start()) {
|
||||
ESP_LOGI(tag, "[%s] Advertising (re)started", reason);
|
||||
} else {
|
||||
ESP_LOGE(tag, "[%s] Failed to start advertising", reason);
|
||||
}
|
||||
}
|
||||
int connectedClients;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -70,8 +70,6 @@ void Load_BLE_Settings(const String &configPath) {
|
||||
BTDeviceName += macSuffix[0]; // Add first character
|
||||
BTDeviceName += macSuffix[1]; // Add second character
|
||||
|
||||
BTDeviceName = "SP110E";
|
||||
|
||||
ESP_LOGI(tag, "Loaded BLE config: name=%s svc=%s char1=%s stick=%s upg_svc=%s upg1=%s upg2=%s",
|
||||
BTDeviceName.c_str(), BTServiceUUID.c_str(), BTSP110ECharacteristicUUID.c_str(),
|
||||
BTStickCharacteristicUUID.c_str(), BTUpgradeServiceUUID.c_str(),
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
#include "global.h"
|
||||
|
||||
|
||||
const char* tag = "pwmout";
|
||||
|
||||
const float binaryPow[17] = {
|
||||
0, 2, 4, 8, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535
|
||||
};
|
||||
static const char* tag = "pwmout";
|
||||
|
||||
PWM_Output::PWM_Output(int8_t pin, uint8_t ch, uint8_t res, uint32_t freq, float maxDuty, bool visionCorrected){
|
||||
this->currDuty = 0;
|
||||
@ -15,17 +11,21 @@ PWM_Output::PWM_Output(int8_t pin, uint8_t ch, uint8_t res, uint32_t freq, float
|
||||
this->maxDuty = maxDuty;
|
||||
this->visionCorrected = visionCorrected;
|
||||
this->freq = freq;
|
||||
this->pin = pin;
|
||||
setResolution(res);
|
||||
pinMode(pin, OUTPUT);
|
||||
|
||||
uint32_t actualFreq = ledcSetup(ch, freq, res);
|
||||
if (actualFreq != freq) {
|
||||
ESP_LOGE(tag, "pwmOut-> ch:%d ledcSetup failed! Requested freq: %d, Actual freq: %d", ch, freq, actualFreq);
|
||||
// continue but mark as uninitialized to prevent usage
|
||||
this->initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ledcAttachPin(pin, ch);
|
||||
setOutput(this->currDuty);
|
||||
this->initialized = true;
|
||||
}
|
||||
|
||||
void PWM_Output::setMaxDuty(float duty) {
|
||||
@ -41,6 +41,10 @@ void PWM_Output::setMaxDuty(float duty) {
|
||||
|
||||
// Range is 0 to 100%
|
||||
void PWM_Output::setOutput(float duty){
|
||||
if(!this->initialized){
|
||||
ESP_LOGW(tag, "setOutput called on uninitialized PWM_Output (ch=%d)", this->ch);
|
||||
return;
|
||||
}
|
||||
// Clamp the duty cycle to the range [0.0, maxDuty]
|
||||
if (duty < 0.0) {
|
||||
duty = 0.0;
|
||||
@ -50,15 +54,14 @@ void PWM_Output::setOutput(float duty){
|
||||
|
||||
// calculate correct duty value
|
||||
int outDutyVal;
|
||||
if(this->visionCorrected){
|
||||
outDutyVal = linearizeOutput(duty);
|
||||
}
|
||||
else{
|
||||
if(this->visionCorrected){
|
||||
outDutyVal = linearizeOutput(duty);
|
||||
} else {
|
||||
outDutyVal = static_cast<int>(duty * this->standardFactor);
|
||||
}
|
||||
|
||||
// Clamp to valid resolution range [0, 2^res - 1]
|
||||
int maxVal = static_cast<int>(binaryPow[this->res]);
|
||||
// Clamp to valid resolution range [0, (1<<res)-1]
|
||||
int maxVal = (this->res >= 31) ? INT32_MAX : ((1 << this->res) - 1);
|
||||
if (outDutyVal < 0) outDutyVal = 0;
|
||||
if (outDutyVal > maxVal) outDutyVal = maxVal;
|
||||
|
||||
@ -72,8 +75,10 @@ void PWM_Output::setFreq(uint32_t fq){
|
||||
uint32_t newFreq;
|
||||
if(this->freq != fq){
|
||||
newFreq = ledcChangeFrequency(this->ch, fq, this->res);
|
||||
if(newFreq){
|
||||
this->freq = fq;
|
||||
if(newFreq > 0){
|
||||
this->freq = newFreq;
|
||||
} else {
|
||||
ESP_LOGW(tag, "ledcChangeFrequency failed for ch:%d requested:%u", this->ch, fq);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,8 +89,9 @@ void PWM_Output::setResolution(uint8_t res){
|
||||
if(this->res > 16) this->res = 16;
|
||||
|
||||
// Use the clamped resolution when computing factors
|
||||
this->standardFactor = binaryPow[this->res] * 0.01f;
|
||||
this->visionFactor = binaryPow[this->res] * 0.0001f;
|
||||
int maxVal = (this->res >= 31) ? INT32_MAX : ((1 << this->res) - 1);
|
||||
this->standardFactor = static_cast<float>(maxVal) * 0.01f;
|
||||
this->visionFactor = static_cast<float>(maxVal) * 0.0001f;
|
||||
ESP_LOGD(tag, "factor=%f, vision=%f", this->standardFactor, this->visionFactor);
|
||||
}
|
||||
|
||||
|
||||
@ -14,9 +14,12 @@ RAMP_LIGHT::RAMP_LIGHT(OneButton* button, PWM_Output* pwmOutput, float min, floa
|
||||
button->attachLongPressStop([](void* context) { static_cast<RAMP_LIGHT*>(context)->longPressStop(); }, this);
|
||||
button->attachDuringLongPress([](void* context) { static_cast<RAMP_LIGHT*>(context)->duringLongPress(); }, this);
|
||||
|
||||
if(min < 0.0) min = 0.0;
|
||||
if(max > 100.0) max = 100.0;
|
||||
currentValue = min;
|
||||
|
||||
if(min < 0.0) this->min = 0.0;
|
||||
if(max > 100.0) this->max = 100.0;
|
||||
if(this->max < this->min) this->max = this->min;
|
||||
if(step <= 0.0) this->step = 1.0;
|
||||
currentValue = this->min;
|
||||
IsOn = false;
|
||||
rampState = RampingUp;
|
||||
}
|
||||
@ -52,11 +55,17 @@ void RAMP_LIGHT::longPressStop(){
|
||||
void RAMP_LIGHT::duringLongPress(){
|
||||
if(IsOn){
|
||||
if (tickCount > 0 && --tickCount == 0) {
|
||||
// When ramping down, currentValue may go below min if step is large; constrain ensures bounds.
|
||||
// Ensure currentValue stays within [min, max] bounds for safe PWM operation
|
||||
currentValue = constrain(currentValue, min, max);
|
||||
pwmOutput->setOutput(currentValue);
|
||||
//ESP_LOGD(tag, "duty: %f, sent val: %d, actual val: %d", currentValue, pwmOutput->currOutVal, pwmOutput->getOutVal());
|
||||
// Adjust currentValue based on ramp direction
|
||||
if(rampState == RampingUp){
|
||||
currentValue += step;
|
||||
} else {
|
||||
currentValue -= step;
|
||||
}
|
||||
|
||||
// Constrain and apply
|
||||
currentValue = constrain(currentValue, this->min, this->max);
|
||||
if(pwmOutput) pwmOutput->setOutput(currentValue);
|
||||
ESP_LOGD(tag, "duty: %f, sent val: %d, actual val: %d", currentValue, pwmOutput? pwmOutput->currOutVal : -1, pwmOutput? pwmOutput->getOutVal() : -1);
|
||||
tickCount = TickDelayCount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,23 +23,31 @@ void Init_File_System(void){
|
||||
//printAllSystemFiles();
|
||||
}
|
||||
|
||||
void getAllDirectories(String (&directoryList)[MAX_DIRECTORIES], int &count){
|
||||
void getAllDirectories(String directoryList[], int &count){
|
||||
File root = LittleFS.open("/");
|
||||
if (!root || !root.isDirectory()){
|
||||
ESP_LOGE("FileSystem", "Failed to open root directory or root is not a directory");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize with root directory
|
||||
if (MAX_DIRECTORIES <= 0) {
|
||||
ESP_LOGW("FileSystem", "MAX_DIRECTORIES is not set or invalid");
|
||||
root.close();
|
||||
return;
|
||||
}
|
||||
directoryList[0] = "/";
|
||||
count = 1;
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
if (file.isDirectory()){
|
||||
if (count < 10){ // Ensure we don't overflow the array
|
||||
directoryList[count] = '/' + String(file.name());
|
||||
if (count < MAX_DIRECTORIES){ // Ensure we don't overflow the caller's array
|
||||
String entry = String(file.name());
|
||||
if (!entry.startsWith("/")) entry = "/" + entry;
|
||||
directoryList[count] = entry;
|
||||
count++;
|
||||
}else{
|
||||
} else {
|
||||
ESP_LOGW("FileSystem", "Directory list array is full");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ void writeFilesToSerial(void);
|
||||
//void getAllDirectoriesRecursive(const String& path, String directoryList[], int& count, int maxSize);
|
||||
|
||||
//void getAllDirectories(String directoryList[], int& count, int maxSize);
|
||||
void getAllDirectories(String (&directoryList)[MAX_DIRECTORIES], int& count);
|
||||
void getAllDirectories(String directoryList[], int& count);
|
||||
|
||||
void printFilesInDirectories(const String directoryList[], int count);
|
||||
|
||||
|
||||
@ -412,4 +412,9 @@ void print_task_watermarks(void) {
|
||||
} else {
|
||||
ESP_LOGE(tag, "Failed to get current task handle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float updateLowpass(float currentValue, float newValue, float alpha) {
|
||||
return (alpha * newValue) + ((1 - alpha) * currentValue);
|
||||
}
|
||||
|
||||
44
src/main.cpp
44
src/main.cpp
@ -82,6 +82,9 @@ TimerHandle_t upgradeHeartbeatTimer = NULL;
|
||||
TimerHandle_t diagnosticsTimer = NULL;
|
||||
#define diagnosticsInterval 60000 // ms
|
||||
|
||||
TimerHandle_t analogInputTimer = NULL;
|
||||
#define analogInputInterval 3000 // ms
|
||||
|
||||
|
||||
|
||||
void setupLogLevels(esp_log_level_t logLevel);
|
||||
@ -120,6 +123,13 @@ void DiagnosticsCallback(TimerHandle_t xTimer) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void AnalogInputCallback(TimerHandle_t xTimer) {
|
||||
float v = readBoardInputVoltage();
|
||||
PowerVin = updateLowpass(prevPowerVin, v*1.01, PowerVinAlpha);
|
||||
prevPowerVin = PowerVin;
|
||||
//ESP_LOGI(tag, "Input Voltage = %f", PowerVin);
|
||||
}
|
||||
|
||||
|
||||
void checkLEDCChannels()
|
||||
{
|
||||
@ -167,7 +177,7 @@ void setup()
|
||||
printAllSystemFiles();
|
||||
}
|
||||
|
||||
String board_file_path, booth_file_path;
|
||||
String board_file_path;
|
||||
Get_Board_and_Booth_File_Paths("/system/system.json", board_file_path, booth_file_path);
|
||||
|
||||
// Load Board Pins
|
||||
@ -205,15 +215,10 @@ void setup()
|
||||
// Initialize Temperature Sensor
|
||||
Init_TSensor(72, &sys_settings.tSensorSettings);
|
||||
|
||||
|
||||
float val = readBoardInputVoltage();
|
||||
ESP_LOGI(tag, "Input Volage = %f", val);
|
||||
|
||||
// Initialize BLE & Wifi
|
||||
// If button 1 is held during boot, enable upgrade mode
|
||||
Load_BLE_Settings("/system/ble.json");
|
||||
if (digitalRead(sys_settings.boardPins.btn[0]) == LOW)
|
||||
{
|
||||
if (digitalRead(sys_settings.boardPins.btn[0]) == LOW){
|
||||
setStatusPin1(true);
|
||||
ESP_LOGW(tag, "Upgrade Mode Triggered");
|
||||
ESP_LOGW(tag, "Enabling BLE and Update Service");
|
||||
@ -223,8 +228,7 @@ void setup()
|
||||
UpgradeMode = true;
|
||||
upgradeHeartbeatTimer = xTimerCreate("UpgradeHeartbeat", pdMS_TO_TICKS(5000), pdTRUE, NULL, UpgradeHeartbeatCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
else {
|
||||
ESP_LOGI(tag, "Enabling BLE, No Update Service");
|
||||
Init_BleServer(true, false); // Dont start the Upgrade service
|
||||
}
|
||||
@ -236,20 +240,18 @@ void setup()
|
||||
}
|
||||
|
||||
#if OLED_ENABLED
|
||||
// Init OLED
|
||||
Init_OLED(sys_settings.oledSettings.width, sys_settings.oledSettings.height, sys_settings.boardPins.oled_mosi, sys_settings.boardPins.oled_sck, sys_settings.boardPins.oled_dc, sys_settings.boardPins.oled_rst, sys_settings.boardPins.oled_cs);
|
||||
#endif
|
||||
|
||||
#if STRIPS_ENABLED
|
||||
Init_RGB_Lights_Task();
|
||||
//vTaskDelay(100);
|
||||
//Init_Ramp_Front_Light_Task();
|
||||
Init_RGB_Lights_Task(UpgradeMode);
|
||||
#endif
|
||||
|
||||
// Create and start software timers
|
||||
buttonScanTimer = xTimerCreate("ButtonScan", pdMS_TO_TICKS(buttonScanInterval), pdTRUE, NULL, ButtonScanCallback);
|
||||
temperatureTimer = xTimerCreate("Temperature", pdMS_TO_TICKS(sys_settings.tSensorSettings.intervalMs), pdTRUE, NULL, TemperatureCallback);
|
||||
statusLedTimer = xTimerCreate("StatusLED", pdMS_TO_TICKS(statusLedInterval), pdTRUE, NULL, StatusLedCallback);
|
||||
analogInputTimer = xTimerCreate("AnalogInput", pdMS_TO_TICKS(analogInputInterval), pdTRUE, NULL, AnalogInputCallback);
|
||||
|
||||
|
||||
#if FREERTOs_DIAGNOSTICS
|
||||
@ -257,10 +259,11 @@ void setup()
|
||||
#endif
|
||||
|
||||
// Start the timers
|
||||
if (buttonScanTimer) xTimerStart(buttonScanTimer, 25);
|
||||
//if (buttonScanTimer) xTimerStart(buttonScanTimer, 100);
|
||||
if (temperatureTimer && sys_settings.tSensorSettings.enabled) xTimerStart(temperatureTimer, 100);
|
||||
if (statusLedTimer) xTimerStart(statusLedTimer, 0);
|
||||
if (upgradeHeartbeatTimer && UpgradeMode) xTimerStart(upgradeHeartbeatTimer, upgradeHeartbeatInterval);
|
||||
if (analogInputTimer) xTimerStart(analogInputTimer, 0);
|
||||
|
||||
#if FREERTOS_DIAGNOSTICS
|
||||
if (diagnosticsTimer) xTimerStart(diagnosticsTimer, diagnosticsInterval);
|
||||
@ -315,9 +318,20 @@ void loop()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (boardButtons[i] != NULL) {
|
||||
boardButtons[i]->tick();
|
||||
}
|
||||
}
|
||||
vTaskDelay(buttonScanInterval);
|
||||
*/
|
||||
|
||||
// Small delay to prevent busy-waiting
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
//vTaskDelay(pdMS_TO_TICKS(100));
|
||||
|
||||
vTaskDelay(portMAX_DELAY);
|
||||
}
|
||||
|
||||
void setupLogLevels(esp_log_level_t logLevel)
|
||||
|
||||
211
src/my_board.cpp
211
src/my_board.cpp
@ -4,124 +4,143 @@
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
#include "global.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "global.h"
|
||||
#include "system.h"
|
||||
|
||||
static const char* tag = "board";
|
||||
static const char *tag = "board";
|
||||
|
||||
BOARD_PINS* thisBoardPins;
|
||||
BOARD_PINS *thisBoardPins = nullptr;
|
||||
|
||||
// Basic validator for ESP32-S3 GPIOs; rejects common reserved/USB/strap pins
|
||||
static bool isValidGpio(int pin) {
|
||||
if (pin < 0 || pin > 48) return false;
|
||||
switch (pin) {
|
||||
if (pin < 0 || pin > 48)
|
||||
return false;
|
||||
switch (pin) {
|
||||
case 19: // USB D-
|
||||
case 20: // USB D+
|
||||
case 45: // strapping
|
||||
case 46: // strapping
|
||||
return false;
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path){
|
||||
// Default initialize to -1 to avoid stale values on partial loads
|
||||
memset(&boardPins, -1, sizeof(boardPins));
|
||||
thisBoardPins = &boardPins;
|
||||
File file = LittleFS.open(path);
|
||||
bool Load_Board_Pins(BOARD_PINS &boardPins, const String &path) {
|
||||
// Default initialize to -1 to avoid stale values on partial loads
|
||||
memset(&boardPins, -1, sizeof(boardPins));
|
||||
thisBoardPins = &boardPins;
|
||||
File file = LittleFS.open(path);
|
||||
|
||||
if (!file) {
|
||||
ESP_LOGE(tag, "Error opening %s...", path.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!file) {
|
||||
ESP_LOGE(tag, "Error opening %s...", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "%s deserialize error!..", path.c_str());
|
||||
return false;
|
||||
}
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "%s deserialize error: %s", path.c_str(), error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject boardJson = doc.as<JsonObject>();
|
||||
boardPins.rgb1 = jsonConstrain<int>(tag, boardJson, "rgb1", -1, 48, -1);
|
||||
boardPins.rgb2 = jsonConstrain<int>(tag, boardJson, "rgb2", -1, 48, -1);
|
||||
boardPins.btn[0] = jsonConstrain<int>(tag, boardJson, "btn1", -1, 48, -1);
|
||||
boardPins.btn[1] = jsonConstrain<int>(tag, boardJson, "btn2", -1, 48, -1);
|
||||
boardPins.btn[2] = jsonConstrain<int>(tag, boardJson, "btn3", -1, 48, -1);
|
||||
boardPins.buzzer = jsonConstrain<int>(tag, boardJson, "buzzer", -1, 48, -1);
|
||||
boardPins.touch[0] = jsonConstrain<int>(tag, boardJson, "touch1", -1, 48, -1);
|
||||
boardPins.touch[1] = jsonConstrain<int>(tag, boardJson, "touch2", -1, 48, -1);
|
||||
boardPins.touch[2] = jsonConstrain<int>(tag, boardJson, "touch3", -1, 48, -1);
|
||||
boardPins.touch[3] = jsonConstrain<int>(tag, boardJson, "touch4", -1, 48, -1);
|
||||
boardPins.touch[4] = jsonConstrain<int>(tag, boardJson, "touch5", -1, 48, -1);
|
||||
boardPins.shield = jsonConstrain<int>(tag, boardJson, "shield", -1, 48, -1);
|
||||
boardPins.relay[0] = jsonConstrain<int>(tag, boardJson, "relay1", -1, 48, -1);
|
||||
boardPins.relay[1] = jsonConstrain<int>(tag, boardJson, "relay2", -1, 48, -1);
|
||||
boardPins.relay[2] = jsonConstrain<int>(tag, boardJson, "relay3", -1, 48, -1);
|
||||
boardPins.relay[3] = jsonConstrain<int>(tag, boardJson, "relay4", -1, 48, -1);
|
||||
boardPins.stat[0] = jsonConstrain<int>(tag, boardJson, "stat1", -1, 48, -1);
|
||||
boardPins.stat[1] = jsonConstrain<int>(tag, boardJson, "stat2", -1, 48, -1);
|
||||
boardPins.adc1 = jsonConstrain<int>(tag, boardJson, "adc1", -1, 48, -1);
|
||||
boardPins.oled_dc = jsonConstrain<int>(tag, boardJson, "oled_dc", -1, 48, -1);
|
||||
boardPins.oled_rst = jsonConstrain<int>(tag, boardJson, "oled_rst", -1, 48, -1);
|
||||
boardPins.oled_mosi = jsonConstrain<int>(tag, boardJson, "oled_mosi", -1, 48, -1);
|
||||
boardPins.oled_sck = jsonConstrain<int>(tag, boardJson, "oled_sck", -1, 48, -1);
|
||||
boardPins.oled_cs = jsonConstrain<int>(tag, boardJson, "oled_cs", -1, 48, -1);
|
||||
boardPins.ext[0] = jsonConstrain<int>(tag, boardJson, "ext1", -1, 48, -1);
|
||||
boardPins.ext[1] = jsonConstrain<int>(tag, boardJson, "ext2", -1, 48, -1);
|
||||
boardPins.rf433tx = jsonConstrain<int>(tag, boardJson, "rf433tx", -1, 48, -1);
|
||||
boardPins.rf433rx = jsonConstrain<int>(tag, boardJson, "rf433rx", -1, 48, -1);
|
||||
JsonObject boardJson = doc.as<JsonObject>();
|
||||
boardPins.rgb1 = jsonConstrain<int>(tag, boardJson, "rgb1", -1, 48, -1);
|
||||
boardPins.rgb2 = jsonConstrain<int>(tag, boardJson, "rgb2", -1, 48, -1);
|
||||
boardPins.btn[0] = jsonConstrain<int>(tag, boardJson, "btn1", -1, 48, -1);
|
||||
boardPins.btn[1] = jsonConstrain<int>(tag, boardJson, "btn2", -1, 48, -1);
|
||||
boardPins.btn[2] = jsonConstrain<int>(tag, boardJson, "btn3", -1, 48, -1);
|
||||
boardPins.buzzer = jsonConstrain<int>(tag, boardJson, "buzzer", -1, 48, -1);
|
||||
boardPins.touch[0] = jsonConstrain<int>(tag, boardJson, "touch1", -1, 48, -1);
|
||||
boardPins.touch[1] = jsonConstrain<int>(tag, boardJson, "touch2", -1, 48, -1);
|
||||
boardPins.touch[2] = jsonConstrain<int>(tag, boardJson, "touch3", -1, 48, -1);
|
||||
boardPins.touch[3] = jsonConstrain<int>(tag, boardJson, "touch4", -1, 48, -1);
|
||||
boardPins.touch[4] = jsonConstrain<int>(tag, boardJson, "touch5", -1, 48, -1);
|
||||
boardPins.shield = jsonConstrain<int>(tag, boardJson, "shield", -1, 48, -1);
|
||||
boardPins.relay[0] = jsonConstrain<int>(tag, boardJson, "relay1", -1, 48, -1);
|
||||
boardPins.relay[1] = jsonConstrain<int>(tag, boardJson, "relay2", -1, 48, -1);
|
||||
boardPins.relay[2] = jsonConstrain<int>(tag, boardJson, "relay3", -1, 48, -1);
|
||||
boardPins.relay[3] = jsonConstrain<int>(tag, boardJson, "relay4", -1, 48, -1);
|
||||
boardPins.stat[0] = jsonConstrain<int>(tag, boardJson, "stat1", -1, 48, -1);
|
||||
boardPins.stat[1] = jsonConstrain<int>(tag, boardJson, "stat2", -1, 48, -1);
|
||||
boardPins.adc1 = jsonConstrain<int>(tag, boardJson, "adc1", -1, 48, -1);
|
||||
boardPins.oled_dc = jsonConstrain<int>(tag, boardJson, "oled_dc", -1, 48, -1);
|
||||
boardPins.oled_rst = jsonConstrain<int>(tag, boardJson, "oled_rst", -1, 48, -1);
|
||||
boardPins.oled_mosi = jsonConstrain<int>(tag, boardJson, "oled_mosi", -1, 48, -1);
|
||||
boardPins.oled_sck = jsonConstrain<int>(tag, boardJson, "oled_sck", -1, 48, -1);
|
||||
boardPins.oled_cs = jsonConstrain<int>(tag, boardJson, "oled_cs", -1, 48, -1);
|
||||
boardPins.ext[0] = jsonConstrain<int>(tag, boardJson, "ext1", -1, 48, -1);
|
||||
boardPins.ext[1] = jsonConstrain<int>(tag, boardJson, "ext2", -1, 48, -1);
|
||||
boardPins.rf433tx = jsonConstrain<int>(tag, boardJson, "rf433tx", -1, 48, -1);
|
||||
boardPins.rf433rx = jsonConstrain<int>(tag, boardJson, "rf433rx", -1, 48, -1);
|
||||
|
||||
// Validate pins against reserved GPIOs
|
||||
auto clampPin = [](int v){ return isValidGpio(v) ? v : -1; };
|
||||
boardPins.rgb1 = clampPin(boardPins.rgb1);
|
||||
boardPins.rgb2 = clampPin(boardPins.rgb2);
|
||||
for (int i=0;i<3;i++) boardPins.btn[i] = clampPin(boardPins.btn[i]);
|
||||
boardPins.buzzer = clampPin(boardPins.buzzer);
|
||||
for (int i=0;i<5;i++) boardPins.touch[i] = clampPin(boardPins.touch[i]);
|
||||
boardPins.shield = clampPin(boardPins.shield);
|
||||
for (int i=0;i<4;i++) boardPins.relay[i] = clampPin(boardPins.relay[i]);
|
||||
for (int i=0;i<2;i++) boardPins.stat[i] = clampPin(boardPins.stat[i]);
|
||||
boardPins.adc1 = clampPin(boardPins.adc1);
|
||||
boardPins.oled_dc = clampPin(boardPins.oled_dc);
|
||||
boardPins.oled_rst = clampPin(boardPins.oled_rst);
|
||||
boardPins.oled_mosi = clampPin(boardPins.oled_mosi);
|
||||
boardPins.oled_sck = clampPin(boardPins.oled_sck);
|
||||
boardPins.oled_cs = clampPin(boardPins.oled_cs);
|
||||
for (int i=0;i<2;i++) boardPins.ext[i] = clampPin(boardPins.ext[i]);
|
||||
boardPins.rf433tx = clampPin(boardPins.rf433tx);
|
||||
boardPins.rf433rx = clampPin(boardPins.rf433rx);
|
||||
// Validate pins against reserved GPIOs
|
||||
auto clampPin = [](int v) { return isValidGpio(v) ? v : -1; };
|
||||
boardPins.rgb1 = clampPin(boardPins.rgb1);
|
||||
boardPins.rgb2 = clampPin(boardPins.rgb2);
|
||||
for (int i = 0; i < 3; i++)
|
||||
boardPins.btn[i] = clampPin(boardPins.btn[i]);
|
||||
boardPins.buzzer = clampPin(boardPins.buzzer);
|
||||
for (int i = 0; i < 5; i++)
|
||||
boardPins.touch[i] = clampPin(boardPins.touch[i]);
|
||||
boardPins.shield = clampPin(boardPins.shield);
|
||||
for (int i = 0; i < 4; i++)
|
||||
boardPins.relay[i] = clampPin(boardPins.relay[i]);
|
||||
for (int i = 0; i < 2; i++)
|
||||
boardPins.stat[i] = clampPin(boardPins.stat[i]);
|
||||
boardPins.adc1 = clampPin(boardPins.adc1);
|
||||
boardPins.oled_dc = clampPin(boardPins.oled_dc);
|
||||
boardPins.oled_rst = clampPin(boardPins.oled_rst);
|
||||
boardPins.oled_mosi = clampPin(boardPins.oled_mosi);
|
||||
boardPins.oled_sck = clampPin(boardPins.oled_sck);
|
||||
boardPins.oled_cs = clampPin(boardPins.oled_cs);
|
||||
for (int i = 0; i < 2; i++)
|
||||
boardPins.ext[i] = clampPin(boardPins.ext[i]);
|
||||
boardPins.rf433tx = clampPin(boardPins.rf433tx);
|
||||
boardPins.rf433rx = clampPin(boardPins.rf433rx);
|
||||
|
||||
ESP_LOGI(tag, "loaded Pins from %s", path.c_str());
|
||||
return true;
|
||||
ESP_LOGI(tag, "loaded Pins from %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Init_Board_Basic(BOARD_PINS &boardPins) {
|
||||
|
||||
void Init_Board_Basic(BOARD_PINS& boardPins)
|
||||
{
|
||||
|
||||
if(boardPins.stat[0] >= 0){ pinMode(boardPins.stat[0], OUTPUT); }
|
||||
if(boardPins.stat[1] >= 0){ pinMode(boardPins.stat[1], OUTPUT); }
|
||||
if (boardPins.stat[0] >= 0) {
|
||||
pinMode(boardPins.stat[0], OUTPUT);
|
||||
}
|
||||
if (boardPins.stat[1] >= 0) {
|
||||
pinMode(boardPins.stat[1], OUTPUT);
|
||||
}
|
||||
if (boardPins.btn[0] >= 0) {
|
||||
pinMode(boardPins.btn[0], INPUT_PULLUP);
|
||||
}
|
||||
if (boardPins.btn[1] >= 0) {
|
||||
pinMode(boardPins.btn[1], INPUT_PULLUP);
|
||||
}
|
||||
if (boardPins.btn[2] >= 0) {
|
||||
pinMode(boardPins.btn[2], INPUT);
|
||||
}
|
||||
if (boardPins.rgb1 >= 0) {
|
||||
pinMode(boardPins.rgb1, OUTPUT);
|
||||
}
|
||||
if (boardPins.rgb2 >= 0) {
|
||||
pinMode(boardPins.rgb2, OUTPUT);
|
||||
}
|
||||
if (boardPins.relay[0] >= 0) {
|
||||
pinMode(boardPins.relay[0], OUTPUT);
|
||||
}
|
||||
if (boardPins.relay[1] >= 0) {
|
||||
pinMode(boardPins.relay[1], OUTPUT);
|
||||
}
|
||||
if (boardPins.relay[2] >= 0) {
|
||||
pinMode(boardPins.relay[2], OUTPUT);
|
||||
}
|
||||
if (boardPins.relay[3] >= 0) {
|
||||
pinMode(boardPins.relay[3], OUTPUT);
|
||||
}
|
||||
|
||||
if(boardPins.btn[0] >= 0){ pinMode(boardPins.btn[0], INPUT_PULLUP); }
|
||||
if(boardPins.btn[1] >= 0){ pinMode(boardPins.btn[1], INPUT_PULLUP); }
|
||||
if(boardPins.btn[2] >= 0){ pinMode(boardPins.btn[2], INPUT); }
|
||||
|
||||
if(boardPins.rgb1 >= 0){ pinMode(boardPins.rgb1, OUTPUT); }
|
||||
if(boardPins.rgb2 >= 0){ pinMode(boardPins.rgb2, OUTPUT); }
|
||||
|
||||
if(boardPins.relay[0] >= 0){ pinMode(boardPins.relay[0], OUTPUT); }
|
||||
if(boardPins.relay[1] >= 0){ pinMode(boardPins.relay[1], OUTPUT); }
|
||||
if(boardPins.relay[2] >= 0){ pinMode(boardPins.relay[2], OUTPUT); }
|
||||
if(boardPins.relay[3] >= 0){ pinMode(boardPins.relay[3], OUTPUT); }
|
||||
|
||||
ESP_LOGI(tag, "Board pins initialized...");
|
||||
ESP_LOGI(tag, "Board pins initialized...");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -5,11 +5,10 @@
|
||||
#include "AppUpgrade.h"
|
||||
|
||||
static const char* tag = "button";
|
||||
OneButton *boardButtons[3];
|
||||
OneButton *boardButtons[3] = { nullptr, nullptr, nullptr };
|
||||
|
||||
void Init_ButtonEvents(int8_t (&pin)[3]){
|
||||
|
||||
|
||||
// Initialize buttons if pins are valid and not already initialized
|
||||
if (pin[0] >= 0) {
|
||||
if (boardButtons[0] == nullptr) {
|
||||
boardButtons[0] = new OneButton(pin[0], true, true);
|
||||
|
||||
@ -17,7 +17,7 @@ static const char* tag = "buzzer";
|
||||
// Define static constexpr member from RtttlPlayer class
|
||||
constexpr uint16_t RtttlPlayer::LUT4[12];
|
||||
|
||||
RtttlPlayer *player;
|
||||
RtttlPlayer *player = nullptr;
|
||||
BUZZ_TUNE buzzTune[TUNE_MAX_COUNT];
|
||||
int8_t buzzPin;
|
||||
int8_t buzzerChannel = -1; // Store the LEDC channel used by the buzzer
|
||||
@ -105,7 +105,7 @@ void Buzzer_Load_Tunes(const char* tunesPath){
|
||||
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...",
|
||||
ESP_LOGD(tag, "Loaded tune %d: cycles=%d, pause=%d, melody=%.40s...",
|
||||
tuneIndex, buzzTune[tuneIndex].cycles, buzzTune[tuneIndex].pause,
|
||||
buzzTune[tuneIndex].melody.c_str());
|
||||
tuneIndex++;
|
||||
|
||||
@ -1,79 +1,94 @@
|
||||
#include "my_device.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "global.h"
|
||||
#include "system.h"
|
||||
#include "my_buttons.h"
|
||||
#include "PWM_Output.h"
|
||||
#include "Ramp_Lights.h"
|
||||
#include "esp_log.h"
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include "global.h"
|
||||
#include "my_buttons.h"
|
||||
#include "system.h"
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <esp_system.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_adc_cal.h>
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include <OneButton.h>
|
||||
#include <driver/adc.h>
|
||||
#include <driver/ledc.h>
|
||||
#include <OneButton.h>
|
||||
#include <esp_adc_cal.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_system.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
static const char *tag = "my_device";
|
||||
|
||||
SYS_SETTINGS sys_settings;
|
||||
PWM_Output *pwmOutputs[4];
|
||||
RAMP_LIGHT *rampLight1;
|
||||
RAMP_LIGHT *rampLight2;
|
||||
PWM_Output *pwmOutputs[4] = { nullptr, nullptr, nullptr, nullptr };
|
||||
RAMP_LIGHT *rampLight1 = nullptr;
|
||||
RAMP_LIGHT *rampLight2 = nullptr;
|
||||
|
||||
String booth_file_path;
|
||||
|
||||
float PowerVinAlpha = 0.5; // Low-pass filter alpha
|
||||
float prevPowerVin = 0.0;
|
||||
float PowerVin = 0.0;
|
||||
|
||||
|
||||
|
||||
// TODO Restore original setOutput code..
|
||||
#define RELAY_RES 10
|
||||
void Init_PWM_Outputs(int8_t (&pin)[4], PWM_OUT_SETTINGS (&pwmSettings)[4])
|
||||
{
|
||||
// Initialize all pointers to nullptr first
|
||||
void Init_PWM_Outputs(int8_t (&pin)[4], PWM_OUT_SETTINGS (&pwmSettings)[4]) {
|
||||
// Delete any existing objects to avoid leaks on re-init
|
||||
for (int i = 0; i < 4; i++) {
|
||||
pwmOutputs[i] = nullptr;
|
||||
if (pwmOutputs[i]) {
|
||||
delete pwmOutputs[i];
|
||||
pwmOutputs[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int chIndex = findUnusedLedcChannel();
|
||||
if (chIndex < 0)
|
||||
{
|
||||
if (chIndex < 0) {
|
||||
ESP_LOGE(tag, "No available LEDC channel for PWM Output%d", i);
|
||||
continue;
|
||||
}
|
||||
pwmOutputs[i] = new PWM_Output(pin[i], chIndex, RELAY_RES, pwmSettings[i].freq, pwmSettings[i].max, false);
|
||||
pwmOutputs[i]->setOutput(pwmSettings[i].def);
|
||||
pwmOutputs[i] = new PWM_Output(pin[i], chIndex, RELAY_RES, pwmSettings[i].freq,
|
||||
pwmSettings[i].max, false);
|
||||
if (pwmOutputs[i]) {
|
||||
pwmOutputs[i]->setOutput(pwmSettings[i].def);
|
||||
} else {
|
||||
ESP_LOGE(tag, "Allocation failed for PWM Output%d", i);
|
||||
}
|
||||
// pwmOutputs[i]->setOutput(5.0);
|
||||
ESP_LOGI(tag, "PWM Output%d: Pin=%d, Freq=%d, ch=%d", i, pin[i], pwmSettings[i].freq, chIndex);
|
||||
ESP_LOGI(tag, "PWM Output%d: Pin=%d, Freq=%d, ch=%d", i, pin[i], pwmSettings[i].freq,
|
||||
chIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Init_Ramp_Lights(RAMP_LIGHT_SETTINGS (&settings)[2], OneButton *(&btn)[3], PWM_Output *(&pwm)[4])
|
||||
{
|
||||
if (settings[0].enabled)
|
||||
{
|
||||
rampLight1 = new RAMP_LIGHT(btn[settings[0].btnIndex], pwm[settings[0].pwmOutIndex], settings[0].min, settings[0].max, settings[0].step);
|
||||
ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d", 1, settings[0].btnIndex, settings[0].pwmOutIndex);
|
||||
void Init_Ramp_Lights(RAMP_LIGHT_SETTINGS (&settings)[2], OneButton *(&btn)[3], PWM_Output *(&pwm)[4]) {
|
||||
if (settings[0].enabled) {
|
||||
if (rampLight1) { delete rampLight1; rampLight1 = nullptr; }
|
||||
rampLight1 = new RAMP_LIGHT(btn[settings[0].btnIndex], pwm[settings[0].pwmOutIndex], settings[0].min, settings[0].max, settings[0].step);
|
||||
if (rampLight1)
|
||||
ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d, min=%f, max=%f, step=%f", 1, settings[0].btnIndex, settings[0].pwmOutIndex, settings[0].min, settings[0].max, settings[0].step);
|
||||
else
|
||||
ESP_LOGE(tag, "Failed to allocate RampLight1");
|
||||
}
|
||||
|
||||
if (settings[1].enabled)
|
||||
{
|
||||
rampLight2 = new RAMP_LIGHT(btn[settings[1].btnIndex], pwm[settings[1].pwmOutIndex], settings[1].min, settings[1].max, settings[1].step);
|
||||
ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d", 2, settings[1].btnIndex, settings[1].pwmOutIndex);
|
||||
if (settings[1].enabled) {
|
||||
if (rampLight2) { delete rampLight2; rampLight2 = nullptr; }
|
||||
rampLight2 = new RAMP_LIGHT(btn[settings[1].btnIndex], pwm[settings[1].pwmOutIndex], settings[1].min, settings[1].max, settings[1].step);
|
||||
if (rampLight2)
|
||||
ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d, min=%f, max=%f, step=%f", 2, settings[1].btnIndex, settings[1].pwmOutIndex, settings[1].min, settings[1].max, settings[1].step);
|
||||
else
|
||||
ESP_LOGE(tag, "Failed to allocate RampLight2");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get the files that should be used to setup the system
|
||||
void Get_Board_and_Booth_File_Paths(const char *sysPath, String &boardPath, String &boothPath)
|
||||
{
|
||||
void Get_Board_and_Booth_File_Paths(const char *sysPath, String &boardPath, String &boothPath) {
|
||||
File file = LittleFS.open(sysPath);
|
||||
|
||||
if (!file)
|
||||
{
|
||||
if (!file) {
|
||||
ESP_LOGE(tag, "Error opening %s...", sysPath);
|
||||
return;
|
||||
}
|
||||
@ -82,23 +97,21 @@ void Get_Board_and_Booth_File_Paths(const char *sysPath, String &boardPath, Stri
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error)
|
||||
{
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "%s deserialize error!..", sysPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// get hardware version string
|
||||
boardPath = jsonConstrainString(tag, doc.as<JsonObject>(), "boardfile", "/cfg/boards/board15.json");
|
||||
boothPath = jsonConstrainString(tag, doc.as<JsonObject>(), "configfile", "/cfg/booths/custom.json");
|
||||
boardPath =
|
||||
jsonConstrainString(tag, doc.as<JsonObject>(), "boardfile", "/cfg/boards/board15.json");
|
||||
boothPath =
|
||||
jsonConstrainString(tag, doc.as<JsonObject>(), "configfile", "/cfg/booths/custom.json");
|
||||
}
|
||||
|
||||
|
||||
void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath)
|
||||
{
|
||||
void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath) {
|
||||
File file = LittleFS.open(boothPath);
|
||||
if (!file)
|
||||
{
|
||||
if (!file) {
|
||||
ESP_LOGE(tag, "Error opening %s...", boothPath.c_str());
|
||||
return;
|
||||
}
|
||||
@ -107,139 +120,150 @@ void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath)
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error)
|
||||
{
|
||||
if (error) {
|
||||
ESP_LOGE(tag, "%s deserialize error!..", boothPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
sys_settings.profile = jsonConstrainString(tag, doc.as<JsonObject>(), "profile", "custom");
|
||||
|
||||
// ********** Mode ***********
|
||||
String modeStr = jsonConstrainString(tag, doc.as<JsonObject>(), "mode", "booth");
|
||||
if (modeStr == "roamer")
|
||||
{
|
||||
if (modeStr == "roamer") {
|
||||
sys_settings.mode = BOOTH_MODE_ROAMER;
|
||||
}
|
||||
else if (modeStr == "stik")
|
||||
{
|
||||
} else if (modeStr == "stik") {
|
||||
sys_settings.mode = BOOTH_MODE_STIK;
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
sys_settings.mode = BOOTH_MODE_NONE;
|
||||
}
|
||||
|
||||
// ********** PWM Out ***********
|
||||
JsonArray pwmJsonArray = doc["pwmout"];
|
||||
if (!pwmJsonArray.isNull())
|
||||
{
|
||||
if (!pwmJsonArray.isNull()) {
|
||||
int pwmIndex = 0;
|
||||
for (JsonObject obj : pwmJsonArray)
|
||||
{
|
||||
if (pwmIndex >= sizeof(sys_settings.pwmOutSettings) / sizeof(sys_settings.pwmOutSettings[0]))
|
||||
for (JsonObject obj : pwmJsonArray) {
|
||||
if (pwmIndex >=
|
||||
sizeof(sys_settings.pwmOutSettings) / sizeof(sys_settings.pwmOutSettings[0]))
|
||||
break;
|
||||
sys_settings.pwmOutSettings[pwmIndex].enabled = jsonConstrainBool(tag, obj, "en", true);
|
||||
sys_settings.pwmOutSettings[pwmIndex].freq = jsonConstrain<int>(tag, obj, "freq", 100, 5000, 250);
|
||||
sys_settings.pwmOutSettings[pwmIndex].min = jsonConstrain<float>(tag, obj, "min", 0.0, 95.0, 0.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].max = jsonConstrain<float>(tag, obj, "max", 5.0, 100.0, 100.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].def = jsonConstrain<float>(tag, obj, "default", 0.0, 100.0, 0.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].freq =
|
||||
jsonConstrain<int>(tag, obj, "freq", 100, 5000, 250);
|
||||
sys_settings.pwmOutSettings[pwmIndex].min =
|
||||
jsonConstrain<float>(tag, obj, "min", 0.0, 95.0, 0.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].max =
|
||||
jsonConstrain<float>(tag, obj, "max", 5.0, 100.0, 100.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].def =
|
||||
jsonConstrain<float>(tag, obj, "default", 0.0, 100.0, 0.0);
|
||||
sys_settings.pwmOutSettings[pwmIndex].deltaRate = 1.0;
|
||||
// sys_settings.pwmOutSettings[pwmIndex]. = jsonConstrainBool(tag, obj, "vision", true);
|
||||
pwmIndex++;
|
||||
}
|
||||
ESP_LOGI(tag, "Loaded PWmOutput settings...");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ESP_LOGE(tag, "Error!, %s key: pwmout not found..", boothPath);
|
||||
}
|
||||
|
||||
// ********** Ramp Lights ***********
|
||||
JsonArray rampJsonArray = doc["ramp-lights"];
|
||||
if (!rampJsonArray.isNull())
|
||||
{
|
||||
if (!rampJsonArray.isNull()) {
|
||||
int rampIndex = 0;
|
||||
for (JsonObject obj : rampJsonArray)
|
||||
{
|
||||
if (rampIndex >= sizeof(sys_settings.rampLightSettings) / sizeof(sys_settings.rampLightSettings[0]))
|
||||
for (JsonObject obj : rampJsonArray) {
|
||||
if (rampIndex >=
|
||||
sizeof(sys_settings.rampLightSettings) / sizeof(sys_settings.rampLightSettings[0]))
|
||||
break;
|
||||
sys_settings.rampLightSettings[rampIndex].enabled = jsonConstrainBool(tag, obj, "en", 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].btnIndex = jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0);
|
||||
sys_settings.rampLightSettings[rampIndex].min = jsonConstrain<float>(tag, obj, "min", 0.0, 95.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.1, 10.0, 1.5);
|
||||
sys_settings.rampLightSettings[rampIndex].enabled =
|
||||
jsonConstrainBool(tag, obj, "en", 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].btnIndex =
|
||||
jsonConstrain<int>(tag, obj, "button-index", 0, 1, 0);
|
||||
sys_settings.rampLightSettings[rampIndex].min =
|
||||
jsonConstrain<float>(tag, obj, "min", 0.0, 95.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.1, 10.0, 1.5);
|
||||
rampIndex++;
|
||||
}
|
||||
ESP_LOGI(tag, "Loaded Ramp Lights settings...");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ESP_LOGE(tag, "Error!, %s key: ramp-lights not found..", boothPath);
|
||||
}
|
||||
|
||||
// ********** Fan ***********
|
||||
JsonObject sensorJson = doc["t-sensor"];
|
||||
if (!sensorJson.isNull())
|
||||
{
|
||||
if (!sensorJson.isNull()) {
|
||||
sys_settings.tSensorSettings.enabled = jsonConstrainBool(tag, sensorJson, "en", true);
|
||||
sys_settings.tSensorSettings.pwmIndex = jsonConstrain<int>(tag, sensorJson, "relay", 0, 3, 3);
|
||||
sys_settings.tSensorSettings.setpoint1 = jsonConstrain<float>(tag, sensorJson, "sp1", 50.0, 100.0, 80.0);
|
||||
sys_settings.tSensorSettings.setpoint2 = jsonConstrain<float>(tag, sensorJson, "sp2", 60.0, 110.0, 90.0);
|
||||
sys_settings.tSensorSettings.fanPower1 = jsonConstrain<float>(tag, sensorJson, "fan-pwr1", 0.0, 100.0, 50.0);
|
||||
sys_settings.tSensorSettings.fanPower2 = jsonConstrain<float>(tag, sensorJson, "fan-pwr2", 50.0, 100.0, 50.0);
|
||||
sys_settings.tSensorSettings.hyst = jsonConstrain<float>(tag, sensorJson, "hyst", 1.0, 10.0, 1.0);
|
||||
sys_settings.tSensorSettings.intervalMs = jsonConstrain<int>(tag, sensorJson, "interval", 1000, 30000, 5000);
|
||||
sys_settings.tSensorSettings.pwmIndex =
|
||||
jsonConstrain<int>(tag, sensorJson, "relay", 0, 3, 3);
|
||||
sys_settings.tSensorSettings.setpoint1 =
|
||||
jsonConstrain<float>(tag, sensorJson, "sp1", 50.0, 100.0, 80.0);
|
||||
sys_settings.tSensorSettings.setpoint2 =
|
||||
jsonConstrain<float>(tag, sensorJson, "sp2", 60.0, 110.0, 90.0);
|
||||
sys_settings.tSensorSettings.fanPower1 =
|
||||
jsonConstrain<float>(tag, sensorJson, "fan-pwr1", 0.0, 100.0, 50.0);
|
||||
sys_settings.tSensorSettings.fanPower2 =
|
||||
jsonConstrain<float>(tag, sensorJson, "fan-pwr2", 50.0, 100.0, 50.0);
|
||||
sys_settings.tSensorSettings.hyst =
|
||||
jsonConstrain<float>(tag, sensorJson, "hyst", 1.0, 10.0, 1.0);
|
||||
sys_settings.tSensorSettings.intervalMs =
|
||||
jsonConstrain<int>(tag, sensorJson, "interval", 1000, 30000, 5000);
|
||||
ESP_LOGI(tag, "Loaded TSensor settings...");
|
||||
ESP_LOGI(tag, " SP1: %F, SP2 %F, Hyst: %F", sys_settings.tSensorSettings.setpoint1, sys_settings.tSensorSettings.setpoint2, sys_settings.tSensorSettings.hyst);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI(tag, " SP1: %F, SP2 %F, Hyst: %F", sys_settings.tSensorSettings.setpoint1,
|
||||
sys_settings.tSensorSettings.setpoint2, sys_settings.tSensorSettings.hyst);
|
||||
} else {
|
||||
ESP_LOGE(tag, "Error!, %s key: t-sensor not found..", boothPath);
|
||||
}
|
||||
|
||||
// ********** RGB Strips ***********
|
||||
JsonArray stripsJsonArray = doc["strips"];
|
||||
if (!stripsJsonArray.isNull())
|
||||
{
|
||||
if (!stripsJsonArray.isNull()) {
|
||||
int stripIndex = 0;
|
||||
for (JsonObject obj : stripsJsonArray)
|
||||
{
|
||||
for (JsonObject obj : stripsJsonArray) {
|
||||
if (stripIndex >= 2)
|
||||
break;
|
||||
sys_settings.ledStripSettings[stripIndex]->enabled = jsonConstrainBool(tag, obj, "en", true);
|
||||
sys_settings.ledStripSettings[stripIndex]->size = jsonConstrain<int>(tag, obj, "size", 1, 250, 25);
|
||||
sys_settings.ledStripSettings[stripIndex]->chip = jsonConstrainString(tag, obj, "chip", "WS2812B");
|
||||
sys_settings.ledStripSettings[stripIndex]->rgbOrder = jsonConstrainString(tag, obj, "rgb-order", "WS2812B");
|
||||
sys_settings.ledStripSettings[stripIndex]->shift = jsonConstrain<int>(tag, obj, "shift", -250, 250, 0);
|
||||
sys_settings.ledStripSettings[stripIndex]->offset = jsonConstrain<int>(tag, obj, "offset", -250, 250, 0);
|
||||
sys_settings.ledStripSettings[stripIndex]->bright = jsonConstrain<int>(tag, obj, "bright", 5, 255, 200);
|
||||
sys_settings.ledStripSettings[stripIndex]->powerDiv = 0;
|
||||
sys_settings.ledStripSettings[stripIndex]->i2sCh = 0;
|
||||
sys_settings.ledStripSettings[stripIndex]->core = jsonConstrain<int>(tag, obj, "core", 0, 1, 0);
|
||||
if (sys_settings.ledStripSettings[stripIndex]) {
|
||||
sys_settings.ledStripSettings[stripIndex]->enabled =
|
||||
jsonConstrainBool(tag, obj, "en", true);
|
||||
sys_settings.ledStripSettings[stripIndex]->size =
|
||||
jsonConstrain<int>(tag, obj, "size", 1, 250, 25);
|
||||
sys_settings.ledStripSettings[stripIndex]->chip =
|
||||
jsonConstrainString(tag, obj, "chip", "WS2812B");
|
||||
sys_settings.ledStripSettings[stripIndex]->rgbOrder =
|
||||
jsonConstrainString(tag, obj, "rgb-order", "WS2812B");
|
||||
sys_settings.ledStripSettings[stripIndex]->shift =
|
||||
jsonConstrain<int>(tag, obj, "shift", -250, 250, 0);
|
||||
sys_settings.ledStripSettings[stripIndex]->offset =
|
||||
jsonConstrain<int>(tag, obj, "offset", -250, 250, 0);
|
||||
sys_settings.ledStripSettings[stripIndex]->bright =
|
||||
jsonConstrain<int>(tag, obj, "bright", 5, 255, 200);
|
||||
sys_settings.ledStripSettings[stripIndex]->powerDiv = 0;
|
||||
sys_settings.ledStripSettings[stripIndex]->i2sCh = 0;
|
||||
sys_settings.ledStripSettings[stripIndex]->core =
|
||||
jsonConstrain<int>(tag, obj, "core", 0, 1, 0);
|
||||
} else {
|
||||
ESP_LOGW(tag, "ledStripSettings[%d] is null, skipping config", stripIndex);
|
||||
}
|
||||
stripIndex++;
|
||||
}
|
||||
|
||||
sys_settings.ledStripSettings[0]->pin = sys_settings.boardPins.rgb1;
|
||||
sys_settings.ledStripSettings[1]->pin = sys_settings.boardPins.rgb2;
|
||||
ESP_LOGI(tag, "Loaded LED Strip settings...");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ESP_LOGE(tag, "Error!, %s key: strips not found..");
|
||||
}
|
||||
|
||||
// ********** BLE ***********
|
||||
JsonObject bleJson = doc["ble"];
|
||||
if (!bleJson.isNull())
|
||||
{
|
||||
if (!bleJson.isNull()) {
|
||||
sys_settings.bleSettings.enabled = jsonConstrainBool(tag, bleJson, "en", true);
|
||||
sys_settings.bleSettings.name = jsonConstrainString(tag, bleJson, "name", "ATA_LIGHTS");
|
||||
|
||||
ESP_LOGI(tag, "Loaded BLE settings...");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ESP_LOGE(tag, "Error!, %s key: ble not found..", boothPath);
|
||||
}
|
||||
|
||||
@ -255,14 +279,11 @@ void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath)
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
void Init_ADC(void)
|
||||
{
|
||||
void Init_ADC(void) {
|
||||
// Configure ADC
|
||||
analogReadResolution(12); // 12-bit ADC
|
||||
analogSetAttenuation(ADC_11db);
|
||||
if (sys_settings.boardPins.adc1 >= 0)
|
||||
{
|
||||
if (sys_settings.boardPins.adc1 >= 0) {
|
||||
analogSetPinAttenuation(sys_settings.boardPins.adc1, ADC_11db);
|
||||
}
|
||||
|
||||
@ -271,35 +292,26 @@ void Init_ADC(void)
|
||||
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &adc_chars);
|
||||
|
||||
// Check calibration success
|
||||
if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK)
|
||||
{
|
||||
if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK) {
|
||||
ESP_LOGI(tag, "ADC calibration: Using Two Point values from eFuse");
|
||||
}
|
||||
else if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK)
|
||||
{
|
||||
} else if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) {
|
||||
ESP_LOGI(tag, "ADC calibration: Using reference voltage from eFuse");
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
ESP_LOGW(tag, "ADC calibration: Using default reference voltage");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float readBoardInputVoltage(void)
|
||||
{
|
||||
float readBoardInputVoltage(void) {
|
||||
const int SAMPLES = 64;
|
||||
uint32_t reading = 0;
|
||||
|
||||
if (sys_settings.boardPins.adc1 < 0)
|
||||
{
|
||||
if (sys_settings.boardPins.adc1 < 0) {
|
||||
ESP_LOGE(tag, "ADC Pin not valid");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Multiple readings for averaging
|
||||
for (int i = 0; i < SAMPLES; i++)
|
||||
{
|
||||
for (int i = 0; i < SAMPLES; i++) {
|
||||
reading += analogRead(sys_settings.boardPins.adc1);
|
||||
delayMicroseconds(50); // Small delay between samples
|
||||
}
|
||||
@ -311,8 +323,15 @@ float readBoardInputVoltage(void)
|
||||
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars);
|
||||
voltage_mv = esp_adc_cal_raw_to_voltage(reading, &adc_chars);
|
||||
|
||||
// Scale to 12V range
|
||||
float voltage = (voltage_mv / 1000.0f) * (10470.0f / 470.0f);
|
||||
// Voltage divider: R1 = 10k (series to input), R2 = 470 (to ground). Measurement is across R2.
|
||||
// Vin = Vout * (R1 + R2) / R2
|
||||
const float R1 = 10000.0f;
|
||||
const float R2 = 470.0f;
|
||||
const float dividerFactor = (R1 + R2) / R2;
|
||||
|
||||
return voltage;
|
||||
// Convert mV to V and apply divider factor
|
||||
float vout = voltage_mv / 1000.0f;
|
||||
float vin = vout * dividerFactor;
|
||||
|
||||
return vin;
|
||||
}
|
||||
121
src/my_oled.cpp
121
src/my_oled.cpp
@ -1,24 +1,25 @@
|
||||
#include "my_oled.h"
|
||||
#include <SPI.h>
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <SPI.h>
|
||||
|
||||
static const char* tag = "oled";
|
||||
Adafruit_SSD1306 *oled;
|
||||
static const char *tag = "oled";
|
||||
Adafruit_SSD1306 *oled = nullptr;
|
||||
|
||||
void Init_OLED(uint8_t width, uint8_t height, uint8_t mosiPin, uint8_t sckPin, uint8_t dcPin, uint8_t rstPin, uint8_t csPin)
|
||||
{
|
||||
oled = new Adafruit_SSD1306(width, height, mosiPin, sckPin, dcPin, rstPin, csPin);
|
||||
void Init_OLED(uint8_t width, uint8_t height, uint8_t mosiPin, uint8_t sckPin, uint8_t dcPin,
|
||||
uint8_t rstPin, uint8_t csPin) {
|
||||
oled = new Adafruit_SSD1306(width, height, mosiPin, sckPin, dcPin, rstPin, csPin);
|
||||
|
||||
if(!oled->begin(SSD1306_SWITCHCAPVCC)) {
|
||||
ESP_LOGE(tag, "SSD1306 allocation failed");
|
||||
for(;;); // Don't proceed, loop forever
|
||||
}
|
||||
if (!oled->begin(SSD1306_SWITCHCAPVCC)) {
|
||||
ESP_LOGE(tag, "SSD1306 allocation failed");
|
||||
for (;;)
|
||||
; // Don't proceed, loop forever
|
||||
}
|
||||
|
||||
oled_ShowInfo();
|
||||
oled_ShowInfo();
|
||||
}
|
||||
|
||||
void oled_ShowInfo(void){
|
||||
//if(sysProps.oledEnabled) {
|
||||
void oled_ShowInfo(void) {
|
||||
// if(sysProps.oledEnabled) {
|
||||
oled->display();
|
||||
vTaskDelay(2000); // Pause for 2 seconds
|
||||
|
||||
@ -37,89 +38,89 @@ void oled_ShowInfo(void){
|
||||
// drawing operations and then update the screen all at once by calling
|
||||
// display.display(). These examples demonstrate both approaches...
|
||||
|
||||
//testdrawline(*oled); // Draw many lines
|
||||
// testdrawline(*oled); // Draw many lines
|
||||
|
||||
//testdrawrect(*oled); // Draw rectangles (outlines)
|
||||
// testdrawrect(*oled); // Draw rectangles (outlines)
|
||||
|
||||
testdrawline(oled); // Draw many lines
|
||||
testdrawline(oled); // Draw many lines
|
||||
|
||||
testdrawrect(oled); // Draw rectangles (outlines)
|
||||
//}
|
||||
testdrawrect(oled); // Draw rectangles (outlines)
|
||||
//}
|
||||
}
|
||||
|
||||
void testdrawline(Adafruit_SSD1306* display) {
|
||||
//if(sysProps.oledEnabled) {
|
||||
void testdrawline(Adafruit_SSD1306 *display) {
|
||||
// if(sysProps.oledEnabled) {
|
||||
int16_t i;
|
||||
|
||||
display->clearDisplay(); // Clear display buffer
|
||||
|
||||
for(i=0; i<display->width(); i+=4) {
|
||||
display->drawLine(0, 0, i, display->height()-1, SSD1306_WHITE);
|
||||
display->display(); // Update screen with each newly-drawn line
|
||||
vTaskDelay(1);
|
||||
for (i = 0; i < display->width(); i += 4) {
|
||||
display->drawLine(0, 0, i, display->height() - 1, SSD1306_WHITE);
|
||||
display->display(); // Update screen with each newly-drawn line
|
||||
vTaskDelay(1);
|
||||
}
|
||||
for(i=0; i<display->height(); i+=4) {
|
||||
display->drawLine(0, 0, display->width()-1, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = 0; i < display->height(); i += 4) {
|
||||
display->drawLine(0, 0, display->width() - 1, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vTaskDelay(250);
|
||||
|
||||
display->clearDisplay();
|
||||
|
||||
for(i=0; i<display->width(); i+=4) {
|
||||
display->drawLine(0, display->height()-1, i, 0, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = 0; i < display->width(); i += 4) {
|
||||
display->drawLine(0, display->height() - 1, i, 0, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
for(i=display->height()-1; i>=0; i-=4) {
|
||||
display->drawLine(0, display->height()-1, display->width()-1, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = display->height() - 1; i >= 0; i -= 4) {
|
||||
display->drawLine(0, display->height() - 1, display->width() - 1, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vTaskDelay(250);
|
||||
|
||||
display->clearDisplay();
|
||||
|
||||
for(i=display->width()-1; i>=0; i-=4) {
|
||||
display->drawLine(display->width()-1, display->height()-1, i, 0, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = display->width() - 1; i >= 0; i -= 4) {
|
||||
display->drawLine(display->width() - 1, display->height() - 1, i, 0, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
for(i=display->height()-1; i>=0; i-=4) {
|
||||
display->drawLine(display->width()-1, display->height()-1, 0, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = display->height() - 1; i >= 0; i -= 4) {
|
||||
display->drawLine(display->width() - 1, display->height() - 1, 0, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
vTaskDelay(250);
|
||||
|
||||
display->clearDisplay();
|
||||
|
||||
for(i=0; i<display->height(); i+=4) {
|
||||
display->drawLine(display->width()-1, 0, 0, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = 0; i < display->height(); i += 4) {
|
||||
display->drawLine(display->width() - 1, 0, 0, i, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
for(i=0; i<display->width(); i+=4) {
|
||||
display->drawLine(display->width()-1, 0, i, display->height()-1, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
for (i = 0; i < display->width(); i += 4) {
|
||||
display->drawLine(display->width() - 1, 0, i, display->height() - 1, SSD1306_WHITE);
|
||||
display->display();
|
||||
vTaskDelay(1);
|
||||
}
|
||||
|
||||
vTaskDelay(2000); // Pause for 2 seconds
|
||||
//}
|
||||
//}
|
||||
}
|
||||
|
||||
void testdrawrect(Adafruit_SSD1306* display) {
|
||||
//if(sysProps.oledEnabled) {
|
||||
void testdrawrect(Adafruit_SSD1306 *display) {
|
||||
// if(sysProps.oledEnabled) {
|
||||
display->clearDisplay();
|
||||
|
||||
for(int16_t i=0; i<display->height()/2; i+=2) {
|
||||
display->drawRect(i, i, display->width()-2*i, display->height()-2*i, SSD1306_WHITE);
|
||||
display->display(); // Update screen with each newly-drawn rectangle
|
||||
vTaskDelay(1);
|
||||
for (int16_t i = 0; i < display->height() / 2; i += 2) {
|
||||
display->drawRect(i, i, display->width() - 2 * i, display->height() - 2 * i, SSD1306_WHITE);
|
||||
display->display(); // Update screen with each newly-drawn rectangle
|
||||
vTaskDelay(1);
|
||||
}
|
||||
|
||||
vTaskDelay(2000);
|
||||
//}
|
||||
//}
|
||||
}
|
||||
@ -21,7 +21,7 @@ void Init_TSensor(uint8_t addr, TSENSOR_SETTINGS *tsettings) {
|
||||
// Initialize the temperature sensor once with the provided I2C address
|
||||
if (tSensor == nullptr) {
|
||||
tSensor = new TI_TMP102_Compatible(addr);
|
||||
ESP_LOGI(tag, "TSensor initialized at I2C addr 0x%02X", addr);
|
||||
ESP_LOGD(tag, "TSensor initialized at I2C addr 0x%02X", addr);
|
||||
} else {
|
||||
ESP_LOGW(tag, "TSensor already initialized; ignoring re-init request (addr 0x%02X)", addr);
|
||||
}
|
||||
@ -77,7 +77,7 @@ void Init_TSensor(uint8_t addr, TSENSOR_SETTINGS *tsettings) {
|
||||
// Fan is off - check if we should turn it on
|
||||
if (temperature >= sp1) {
|
||||
fanIsOn = true;
|
||||
ESP_LOGI(tag, "Fan turning ON - temp %.2f >= setpoint1 %.2f", temperature, sp1);
|
||||
ESP_LOGD(tag, "Fan turning ON - temp %.2f >= setpoint1 %.2f", temperature, sp1);
|
||||
} else {
|
||||
newDuty = 0.0f; // Stay off
|
||||
}
|
||||
@ -86,7 +86,7 @@ void Init_TSensor(uint8_t addr, TSENSOR_SETTINGS *tsettings) {
|
||||
if (temperature < (sp1 - hyst)) {
|
||||
fanIsOn = false;
|
||||
newDuty = 0.0f;
|
||||
ESP_LOGI(tag, "Fan turning OFF - temp %.2f < (setpoint1 - hyst) %.2f", temperature, sp1 - hyst);
|
||||
ESP_LOGD(tag, "Fan turning OFF - temp %.2f < (setpoint1 - hyst) %.2f", temperature, sp1 - hyst);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ void Init_TSensor(uint8_t addr, TSENSOR_SETTINGS *tsettings) {
|
||||
float tempRatio = (temperature - sp1) / tempRange;
|
||||
newDuty = fp1 + (tempRatio * powerRange);
|
||||
|
||||
ESP_LOGV(tag, "Linear scaling: temp=%.2f, ratio=%.3f, duty=%.2f", temperature, tempRatio, newDuty);
|
||||
ESP_LOGD(tag, "Linear scaling: temp=%.2f, ratio=%.3f, duty=%.2f", temperature, tempRatio, newDuty);
|
||||
}
|
||||
|
||||
// Ensure duty is within bounds
|
||||
@ -115,7 +115,7 @@ void Init_TSensor(uint8_t addr, TSENSOR_SETTINGS *tsettings) {
|
||||
// Apply new duty cycle if changed (with small tolerance to avoid constant updates)
|
||||
if (fabs(currentDuty - newDuty) > 0.1f) {
|
||||
pwmOut->setOutput(newDuty);
|
||||
ESP_LOGI(tag, "Board T: %.2f F, Fan -> %.2f%% (on=%s)", temperature, newDuty, fanIsOn ? "true" : "false");
|
||||
ESP_LOGD(tag, "Board T: %.2f F, Fan -> %.2f%% (on=%s)", temperature, newDuty, fanIsOn ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1315
src/my_wifi.cpp
1315
src/my_wifi.cpp
File diff suppressed because it is too large
Load Diff
@ -1,673 +0,0 @@
|
||||
#include "AppUpgrade.h"
|
||||
#include "esp_log.h"
|
||||
#include <MD5Builder.h>
|
||||
#include <LittleFS.h>
|
||||
#include <memory>
|
||||
#include "global.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "BLE_UpdateService.h"
|
||||
#include <HTTPClient.h>
|
||||
#include <Update.h>
|
||||
|
||||
static const char* TAG = "AppUpdater";
|
||||
TaskHandle_t Update_Task_Handle = NULL;
|
||||
TaskHandle_t versionCheckTask_Handle = NULL;
|
||||
|
||||
// Queue handle for firmware update messages
|
||||
//QueueHandle_t updateMsgQueue = NULL;
|
||||
|
||||
String updateUrl = "";
|
||||
|
||||
Version otaVersion;
|
||||
|
||||
|
||||
AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin)
|
||||
: localVersion(localVersion), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE])
|
||||
{
|
||||
baseUrl = bucket ? String(bucket) : String(DEFAULT_MANIFEST_URL);
|
||||
// Ensure baseUrl ends with a single '/'
|
||||
if(!baseUrl.endsWith("/")) baseUrl += "/";
|
||||
ESP_LOGI(TAG, "AppUpdater initialized (local v%s) baseUrl=%s", localVersion.toString().c_str(), baseUrl.c_str());
|
||||
}
|
||||
|
||||
void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) {
|
||||
progressCb = callback;
|
||||
}
|
||||
|
||||
void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) {
|
||||
status = newStatus;
|
||||
if (progressCb) {
|
||||
progressCb(status, percentage, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool AppUpdater::checkManifest() {
|
||||
String url = buildUrl(manifestName);
|
||||
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||
|
||||
// Start the HTTP client and Send GET request for manifest
|
||||
HTTPClient http;
|
||||
http.begin(url);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the response
|
||||
String payload = http.getString();
|
||||
http.end();
|
||||
|
||||
// Parse JSON
|
||||
DeserializationError error = deserializeJson(jsonManifest, payload);
|
||||
ESP_LOGD(TAG, "Manifest deserialized");
|
||||
if (error) {
|
||||
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for files section
|
||||
jsonFilesArray = jsonManifest["files"];
|
||||
if (jsonFilesArray.isNull()) {
|
||||
ESP_LOGE(TAG, "No files section in manifest");
|
||||
return false;
|
||||
}else{
|
||||
ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size());
|
||||
}
|
||||
|
||||
// Check for version section
|
||||
JsonObject jsonVersion = jsonManifest["version"];
|
||||
ESP_LOGD(TAG, "Version section found");
|
||||
if (jsonVersion.isNull()) {
|
||||
ESP_LOGE(TAG, "No version section in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the remote version
|
||||
byte major = jsonVersion["major"] | 0;
|
||||
byte minor = jsonVersion["minor"] | 0;
|
||||
byte patch = jsonVersion["patch"] | 0;
|
||||
otaVersion = {major, minor, patch};
|
||||
|
||||
//Version localVersion;
|
||||
//::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch);
|
||||
|
||||
// Check if an update is available
|
||||
updateAvailable = false;
|
||||
// Only mark update available if remote is strictly newer than local
|
||||
if (otaVersion <= localVersion) {
|
||||
ESP_LOGI(TAG, "No updates available");
|
||||
return false;
|
||||
}else{
|
||||
updateAvailable = true;
|
||||
ESP_LOGD(TAG, "Update available");
|
||||
}
|
||||
|
||||
//ESP_LOGD(TAG, "Manifest content: %s", payload.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) {
|
||||
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
|
||||
// Construct full URL
|
||||
String url = buildUrl(remotePath);
|
||||
ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath);
|
||||
|
||||
String localMd5 = getLocalMD5(localPath);
|
||||
|
||||
if (localMd5.equals(expectedMd5)) {
|
||||
ESP_LOGI(TAG, "File already up to date: %s", localPath);
|
||||
updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start the download
|
||||
HTTPClient http;
|
||||
http.begin(url);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "Download failed: %d", httpCode);
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Download failed");
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the stream and content length
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
size_t contentLength = http.getSize();
|
||||
|
||||
// Verify and save the file
|
||||
bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5);
|
||||
http.end();
|
||||
if(!success){
|
||||
updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed");
|
||||
}else{
|
||||
updateProgress( UpdateStatus::FILE_SAVED, 100, localPath);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5)
|
||||
{
|
||||
MD5Builder md5;
|
||||
md5.begin();
|
||||
size_t totalRead = 0;
|
||||
|
||||
// Create temporary filename
|
||||
String tempPath = String(localPath) + ".tmp";
|
||||
|
||||
// Open temporary file for writing
|
||||
File file = fileSystem.open(tempPath.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
ESP_LOGE(TAG, "Failed to open temporary file for writing");
|
||||
return false;
|
||||
}
|
||||
|
||||
//updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
|
||||
if (contentLength > 0) {
|
||||
// Single pass with known content length
|
||||
while (totalRead < contentLength) {
|
||||
size_t available = stream->available();
|
||||
if (available) {
|
||||
size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE)));
|
||||
|
||||
// Write to temp file and update MD5
|
||||
if (file.write(downloadBuffer.get(), readLen) != readLen) {
|
||||
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||
|
||||
file.close();
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
md5.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath);
|
||||
}
|
||||
yield();
|
||||
}
|
||||
} else {
|
||||
// Unknown content length: read until stream ends
|
||||
for (;;) {
|
||||
size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||
if (readLen == 0) {
|
||||
break;
|
||||
}
|
||||
if (file.write(downloadBuffer.get(), readLen) != readLen) {
|
||||
ESP_LOGE(TAG, "Failed to write to temporary file");
|
||||
file.close();
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
md5.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
// Progress unknown; emit periodic heartbeats at 0%
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0, localPath);
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
md5.calculate();
|
||||
String calculatedMd5 = md5.toString();
|
||||
|
||||
// Verify MD5 hash
|
||||
if (!calculatedMd5.equals(expectedMd5)) {
|
||||
//ESP_LOGE(TAG, "MD5 mismatch for %s", localPath);
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Replace original file with verified temp file
|
||||
if (fileSystem.exists(localPath)) {
|
||||
fileSystem.remove(localPath);
|
||||
}
|
||||
if (!fileSystem.rename(tempPath.c_str(), localPath)) {
|
||||
ESP_LOGE(TAG, "Failed to rename temporary file");
|
||||
fileSystem.remove(tempPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String AppUpdater::getLocalMD5(const char* filePath){
|
||||
File file = fileSystem.open(filePath, "r");
|
||||
if(!file){
|
||||
ESP_LOGE(TAG, "Error opening %s...", filePath);
|
||||
return String();
|
||||
}
|
||||
|
||||
MD5Builder md5Builder;
|
||||
md5Builder.begin();
|
||||
size_t fileSize = file.size();
|
||||
size_t totalRead = 0;
|
||||
size_t readLen = 0;
|
||||
while (totalRead < fileSize) {
|
||||
readLen = file.readBytes(reinterpret_cast<char*>(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE)));
|
||||
md5Builder.add(downloadBuffer.get(), readLen);
|
||||
totalRead += readLen;
|
||||
}
|
||||
|
||||
md5Builder.calculate();
|
||||
file.close();
|
||||
return md5Builder.toString();
|
||||
}
|
||||
|
||||
bool AppUpdater::updateFilesArray() {
|
||||
int successCount = 0;
|
||||
int totalFiles = jsonFilesArray.size();
|
||||
ESP_LOGI(TAG, "Found %d files in manifest", totalFiles);
|
||||
|
||||
// Iterate over each file entry in the manifest
|
||||
for (JsonObject file : jsonFilesArray) {
|
||||
const char* remotePath = file["remote"];
|
||||
const char* localPath = file["local"];
|
||||
const char* expectedMd5 = file["md5"];
|
||||
|
||||
// Skip invalid entries
|
||||
if (!remotePath || !localPath || !expectedMd5) {
|
||||
ESP_LOGE(TAG, "Invalid file entry in manifest");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to update the file
|
||||
if (updateFile(remotePath, localPath, expectedMd5)) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles);
|
||||
return successCount == totalFiles;
|
||||
}
|
||||
|
||||
bool AppUpdater::updateApp() {
|
||||
updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update");
|
||||
|
||||
// Check for firmware section in manifest
|
||||
if (!jsonManifest["firmware"].is<JsonObject>() || !jsonManifest["firmware"]["md5"].is<const char*>()) {
|
||||
ESP_LOGE(TAG, "Invalid firmware section in manifest");
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the firmware MD5 hash and URL
|
||||
const char* expectedMd5 = jsonManifest["firmware"]["md5"];
|
||||
String firmwareUrl = buildUrl(appName);
|
||||
|
||||
// Download the firmware
|
||||
HTTPClient http;
|
||||
http.begin(firmwareUrl);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "Firmware download failed: %d", httpCode);
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed");
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check available space
|
||||
size_t firmwareSize = http.getSize();
|
||||
if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) {
|
||||
ESP_LOGE(TAG, "Firmware: Not enough space for update");
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update");
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up MD5 checking
|
||||
MD5Builder md5;
|
||||
md5.begin();
|
||||
|
||||
// Download and verify firmware
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
if (firmwareSize > 0) {
|
||||
size_t remaining = firmwareSize;
|
||||
while (remaining > 0) {
|
||||
size_t chunk = std::min(remaining, size_t(BUFFER_SIZE));
|
||||
size_t read = stream->readBytes(downloadBuffer.get(), chunk);
|
||||
|
||||
// Check for timeout
|
||||
if (read == 0) {
|
||||
ESP_LOGE(TAG, "Read timeout");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update MD5 and write firmware
|
||||
md5.add(downloadBuffer.get(), read);
|
||||
if (Update.write(downloadBuffer.get(), read) != read) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
remaining -= read;
|
||||
updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware");
|
||||
}
|
||||
} else {
|
||||
// Unknown size: stream until end
|
||||
for (;;) {
|
||||
size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE);
|
||||
if (read == 0) break;
|
||||
md5.add(downloadBuffer.get(), read);
|
||||
if (Update.write(downloadBuffer.get(), read) != read) {
|
||||
ESP_LOGE(TAG, "Write failed");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify MD5
|
||||
md5.calculate();
|
||||
String calculatedMd5 = md5.toString();
|
||||
if (!calculatedMd5.equals(expectedMd5)) {
|
||||
ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str());
|
||||
updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch");
|
||||
Update.abort();
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finish update
|
||||
if (!Update.end()) {
|
||||
ESP_LOGE(TAG, "Update end failed");
|
||||
updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed");
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
http.end();
|
||||
updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AppUpdater::IsUpdateAvailable(){
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
String AppUpdater::buildUrl(const char* path) const {
|
||||
if(!path || !*path) return baseUrl; // just base
|
||||
String p(path);
|
||||
// If already absolute URL, pass through
|
||||
if(p.startsWith("http://") || p.startsWith("https://")) return p;
|
||||
// Strip leading slashes to avoid double
|
||||
while(p.startsWith("/")) p.remove(0,1);
|
||||
// Ensure baseUrl has single trailing slash
|
||||
String b = baseUrl;
|
||||
if(!b.endsWith("/")) b += "/";
|
||||
return b + p;
|
||||
}
|
||||
|
||||
|
||||
AsyncEventSource* eventProgress = nullptr;
|
||||
void startFirmwareUpdateTask(AsyncEventSource* evProg) {
|
||||
eventProgress = evProg;
|
||||
if(Update_Task_Handle) {
|
||||
ESP_LOGW(TAG, "Firmware update task already running");
|
||||
return;
|
||||
}
|
||||
xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle);
|
||||
}
|
||||
|
||||
void firmwareUpdateTask(void* parameter) {
|
||||
static const char* TAG = "UpdateTask";
|
||||
AppUpdater* updater = nullptr;
|
||||
|
||||
try {
|
||||
loadUpdateJson();
|
||||
|
||||
// Initialize updater
|
||||
updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin");
|
||||
updater->setProgressCallback(updateProgress);
|
||||
|
||||
ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str());
|
||||
|
||||
// Check and perform updates
|
||||
if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); }
|
||||
|
||||
if (updater->IsUpdateAvailable()) {
|
||||
ESP_LOGI(TAG, "Update available, updating files...");
|
||||
|
||||
if (!updater->updateFilesArray()) {
|
||||
throw std::runtime_error("Failed to update files");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Updating firmware...");
|
||||
if (!updater->updateApp()) {
|
||||
throw std::runtime_error("Failed to update firmware");
|
||||
}
|
||||
ESP_LOGI(TAG, "Update successful, restarting...");
|
||||
|
||||
sendUpdateMessage("Restarting ", true, 100);
|
||||
vTaskDelay(2000);
|
||||
|
||||
ESP.restart();
|
||||
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||
}
|
||||
end:
|
||||
delete updater;
|
||||
Update_Task_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void startVersionCheckTask() {
|
||||
if(versionCheckTask_Handle != NULL) {
|
||||
ESP_LOGW(TAG, "Version Check Tak already running");
|
||||
return;
|
||||
}
|
||||
xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle);
|
||||
}
|
||||
|
||||
void versionCheckTask(void* parameter){
|
||||
|
||||
if(updateUrl == ""){
|
||||
loadUpdateJson();
|
||||
}
|
||||
|
||||
if(checkManifest(otaVersion) == false){
|
||||
ESP_LOGE(TAG, "Error checking manifest");
|
||||
}
|
||||
|
||||
versionCheckTask_Handle = NULL;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void loadUpdateJson(void) {
|
||||
try {
|
||||
ESP_LOGD(TAG, "loadUpdateJaon function...");
|
||||
if(updateUrl == "") {
|
||||
String updateJsonPath = "/system/update.json";
|
||||
|
||||
// Read and parse update.json
|
||||
File file = LittleFS.open(updateJsonPath);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open update.json");
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) { throw std::runtime_error("Failed to parse update.json"); }
|
||||
|
||||
// Get update configuration
|
||||
JsonObject jObj = doc.as<JsonObject>();
|
||||
String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/");
|
||||
String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://s3-minio.boothwizard.com/boothifier/");
|
||||
updateUrl = baseUrl + folderName;
|
||||
|
||||
ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE(TAG, "Update failed: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) {
|
||||
|
||||
char buffer[128];
|
||||
const char* msg;
|
||||
bool isComplete = false;
|
||||
|
||||
const char* safeMsg = message ? message : "";
|
||||
switch (newStatus) {
|
||||
case AppUpdater::UpdateStatus::IDLE:
|
||||
snprintf(buffer, sizeof(buffer), "Update idle");
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::MESSAGE:
|
||||
msg = message ? message : "";
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::DOWNLOADING:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::VERIFYING:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::FILE_SKIPPED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::FILE_SAVED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::MD5_FAILED:
|
||||
snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg);
|
||||
msg = buffer;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::COMPLETE:
|
||||
snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!");
|
||||
msg = buffer;
|
||||
isComplete = true;
|
||||
break;
|
||||
case AppUpdater::UpdateStatus::ERROR:
|
||||
snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg);
|
||||
msg = buffer;
|
||||
break;
|
||||
default:
|
||||
snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus);
|
||||
msg = buffer;
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "%s", msg);
|
||||
sendUpdateMessage(msg, isComplete, percentage);
|
||||
}
|
||||
|
||||
void sendUpdateMessage(const char* message, bool complete, int progress = -1) {
|
||||
if(eventProgress && eventProgress->count() > 0) {
|
||||
JsonDocument jsonDoc;
|
||||
jsonDoc["message"] = message;
|
||||
jsonDoc["complete"] = complete;
|
||||
jsonDoc["progress"] = progress;
|
||||
String strMessage;
|
||||
serializeJson(jsonDoc, strMessage);
|
||||
eventProgress->send(strMessage.c_str(), "update", millis());
|
||||
}
|
||||
else{
|
||||
ESP_LOGW(TAG, "No clients connected to event source");
|
||||
}
|
||||
|
||||
bleUpgrade_send_message(message);
|
||||
}
|
||||
|
||||
bool checkManifest(Version& remoteVersion) {
|
||||
const char* TAG = "manifestCheck";
|
||||
String url = updateUrl + "update.json";
|
||||
ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str());
|
||||
|
||||
// Start the HTTP client and send GET request for manifest
|
||||
HTTPClient http;
|
||||
http.begin(url);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the response
|
||||
String payload = http.getString();
|
||||
http.end();
|
||||
|
||||
//ESP_LOGD(TAG, "%s", payload.c_str());
|
||||
|
||||
// Parse JSON
|
||||
JsonDocument jsonManifest;
|
||||
DeserializationError error = deserializeJson(jsonManifest, payload);
|
||||
if (error) {
|
||||
ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for version section
|
||||
JsonObject jsonVersion = jsonManifest["version"];
|
||||
|
||||
if (jsonVersion.isNull()) {
|
||||
ESP_LOGE(TAG, "No version section in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the remote version
|
||||
byte major = jsonVersion["major"] | 0;
|
||||
byte minor = jsonVersion["minor"] | 0;
|
||||
byte patch = jsonVersion["patch"] | 0;
|
||||
remoteVersion = {major, minor, patch};
|
||||
ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// Initialize WiFi connection first
|
||||
// ... WiFi connection code ...
|
||||
|
||||
// Initialize filesystem
|
||||
if(!LittleFS.begin()) {
|
||||
Serial.println("LittleFS Mount Failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create updater instance with:
|
||||
// - Current version: "1.0.0"
|
||||
// - Update server URL: "https://my-update-server.com/"
|
||||
// - Filesystem: LittleFS
|
||||
AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS);
|
||||
|
||||
// Set progress callback
|
||||
updater.setProgressCallback([](int progress) {
|
||||
Serial.printf("Update progress: %d%%\n", progress);
|
||||
});
|
||||
|
||||
// Check and update firmware
|
||||
if (updater.checkAndUpdate()) {
|
||||
Serial.println("Update successful! Rebooting...");
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// Update specific files from manifest
|
||||
int updatedFiles = updater.updateFilesFromManifest("test_update.json");
|
||||
Serial.printf("Updated %d files\n", updatedFiles);
|
||||
}
|
||||
|
||||
*/
|
||||
@ -1,716 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
*/
|
||||
@ -1,178 +0,0 @@
|
||||
#include "ATALights.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <Arduino.h>
|
||||
#include "Animations.h"
|
||||
#include <functional>
|
||||
#include <NeoPixelBus.h>
|
||||
|
||||
|
||||
static const char* tag = "strips";
|
||||
|
||||
// Define constants for maximum LEDs
|
||||
#define MAX_LEDS 300 // Adjust based on your LED requirements
|
||||
|
||||
// Create pointers for NeoPixelBus objects
|
||||
void* strip1 = nullptr;
|
||||
void* strip2 = nullptr;
|
||||
|
||||
TaskHandle_t Animation_Task_Handle;
|
||||
|
||||
// Runtime configuration variables
|
||||
int numLeds1 = 0, numLeds2 = 0; // Number of LEDs for each strip
|
||||
int dataPin1 = -1, dataPin2 = -1; // Data pins for each strip
|
||||
String chipType1, chipType2; // Chip types (e.g., WS2812, SK6812, TM1814)
|
||||
String colorOrder1, colorOrder2; // Color orders (e.g., GRB, RGB, BGR)
|
||||
|
||||
|
||||
void Init_Lights_Task(void){
|
||||
|
||||
xTaskCreatePinnedToCore(Lights_Control_Task, "LumaMaster_Task", 1024*6, NULL, 1, &Animation_Task_Handle, CONFIG_ARDUINO_RUNNING_CORE);
|
||||
ESP_LOGI(tag, "Lights Task Created...");
|
||||
|
||||
|
||||
// Example runtime configuration for two strips
|
||||
dataPin1 = 5; // Pin for Strip 1
|
||||
numLeds1 = 150; // Number of LEDs on Strip 1
|
||||
chipType1 = "WS2812"; // Chip type for Strip 1
|
||||
colorOrder1 = "GRB"; // Color order for Strip 1
|
||||
|
||||
dataPin2 = 18; // Pin for Strip 2
|
||||
numLeds2 = 100; // Number of LEDs on Strip 2
|
||||
chipType2 = "WS2812"; // Chip type for Strip 2
|
||||
colorOrder2 = "RGB"; // Color order for Strip 2
|
||||
|
||||
// Dynamically initialize the strips
|
||||
strip1 = initializeStrip(dataPin1, numLeds1, chipType1, colorOrder1);
|
||||
strip2 = initializeStrip(dataPin2, numLeds2, chipType2, colorOrder2);
|
||||
|
||||
|
||||
// Start the strips if initialized
|
||||
if (strip1) static_cast<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>*>(strip1)->Begin();
|
||||
if (strip2) static_cast<NeoPixelBus<NeoRgbFeature, Neo800KbpsMethod>*>(strip2)->Begin();
|
||||
}
|
||||
|
||||
|
||||
void Animation_Loop_Exit(void){
|
||||
if( Animation_Task_Handle ){
|
||||
xTaskNotifyGive( Animation_Task_Handle );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void LightsON(void){
|
||||
FastLED.show();
|
||||
}
|
||||
|
||||
|
||||
void LightsOff(void){
|
||||
FastLED.clear();
|
||||
FastLED.show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline void setPixel1(int pixelIndex, const CRGB col) {
|
||||
register uint16_t x = pixelIndex + ledSettings[0].shift;
|
||||
// If strip.effSize is power of 2, use faster bit masking
|
||||
if ((ledSettings[0].effSize & (ledSettings[0].effSize - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + ledSettings[0].effSize) & (ledSettings[0].effSize - 1)) : (x & (ledSettings[0].effSize - 1));
|
||||
leds1[(x + ledSettings[0].offset) & (ledSettings[0].effSize - 1)] = col;
|
||||
} else {
|
||||
// For non-power-of-2 sizes, still need modulo
|
||||
x = (x < 0) ? ((x + ledSettings[0].effSize) % ledSettings[0].effSize) : (x % ledSettings[0].effSize);
|
||||
leds1[(x + ledSettings[0].offset) % ledSettings[0].effSize] = col;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
inline void setPixel2(int pixelIndex, const CRGB col) {
|
||||
register uint16_t x = pixelIndex + ledSettings[1].shift;
|
||||
// If strip.effSize is power of 2, use faster bit masking
|
||||
if ((ledSettings[1].effSize & (ledSettings[1].effSize - 1)) == 0) {
|
||||
x = (x < 0) ? ((x + ledSettings[1].effSize) & (ledSettings[1].effSize - 1)) : (x & (ledSettings[1].effSize - 1));
|
||||
leds2[(x + ledSettings[1].offset) & (ledSettings[1].effSize - 1)] = col;
|
||||
} else {
|
||||
// For non-power-of-2 sizes, still need modulo
|
||||
x = (x < 0) ? ((x + ledSettings[1].effSize) % ledSettings[1].effSize) : (x % ledSettings[1].effSize);
|
||||
leds2[(x + ledSettings[1].offset) % ledSettings[1].effSize] = col;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Lights_Control_Task_Resume(void){
|
||||
vTaskResume(Animation_Task_Handle);
|
||||
}
|
||||
|
||||
void Lights_Control_Task(void *parameters){
|
||||
|
||||
ESP_LOGD(tag, "Lights Control Task Entered...");
|
||||
vTaskSuspend(NULL);
|
||||
ESP_LOGD(tag, "Lights Control Task Resumed...");
|
||||
vTaskDelay(2000);
|
||||
fill_solid(leds1, ledSettings[0].size-1, CRGB::Blue);
|
||||
FastLED.show();
|
||||
vTaskDelay(5000);
|
||||
|
||||
while(true){
|
||||
Animation_Loop(2000, [&]() {
|
||||
ESP_LOGD(tag, "Looping....");
|
||||
|
||||
// Example animation: Alternate colors between strips
|
||||
setStripColor<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>>(strip1, numLeds1, RgbColor(255, 0, 0)); // Red for Strip 1
|
||||
setStripColor<NeoPixelBus<NeoRgbFeature, Neo800KbpsMethod>>(strip2, numLeds2, RgbColor(0, 255, 0)); // Green for Strip 2
|
||||
delay(1000);
|
||||
|
||||
setStripColor<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>>(strip1, numLeds1, RgbColor(0, 0, 255)); // Blue for Strip 1
|
||||
setStripColor<NeoPixelBus<NeoRgbFeature, Neo800KbpsMethod>>(strip2, numLeds2, RgbColor(255, 255, 0)); // Yellow for Strip 2
|
||||
delay(1000);
|
||||
});
|
||||
|
||||
/*
|
||||
uint8_t animMode = 1;
|
||||
switch(animMode){
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Init_FastLED_Strip(CRGB* leds, uint8_t pin, int size, EOrder rgbOrder, const String& chipType) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Function to initialize a strip dynamically (Non-SPI chipsets only)
|
||||
void* initializeStrip(int dataPin, int numLeds, const String& chipType, const String& colorOrder) {
|
||||
if (chipType == "WS2812" || chipType == "SK6812") {
|
||||
if (colorOrder == "GRB") {
|
||||
return new NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
} else if (colorOrder == "RGB") {
|
||||
return new NeoPixelBus<NeoRgbFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
} else if (colorOrder == "BGR") {
|
||||
return new NeoPixelBus<NeoBgrFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
}
|
||||
}
|
||||
Serial.println("Unsupported chipset or color order!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Function to set all LEDs of a strip to a specific color
|
||||
template <typename T>
|
||||
void setStripColor(void* strip, int numLeds, RgbColor color) {
|
||||
if (strip) {
|
||||
T* actualStrip = static_cast<T*>(strip);
|
||||
for (int i = 0; i < numLeds; i++) {
|
||||
actualStrip->SetPixelColor(i, color);
|
||||
}
|
||||
actualStrip->Show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <NimBLEDevice.h>
|
||||
#include <ArduinoJson.h>
|
||||
@ -1,157 +0,0 @@
|
||||
#include "BLE-FlashStick-Service.h"
|
||||
#include "WiFi.h"
|
||||
#include "my_wifi.h"
|
||||
#include "global.h"
|
||||
#include "AppUpgrade.h"
|
||||
#include "AppVersion.h"
|
||||
|
||||
static const char *tag = "BLE_FlashStickService";
|
||||
|
||||
#define UPGRADE_SERVICE_UUID "abcdef03-2345-6789-1234-56789abcdef0"
|
||||
#define UPGRADE_CHARACTERISTIC1_UUID "abcdef03-2345-6789-1234-56789abcdef1"
|
||||
|
||||
NimBLEService *pUpgradeService = nullptr;
|
||||
NimBLECharacteristic *pUpgradeCharacteristic1 = nullptr;
|
||||
|
||||
enum WIFI_STAT : byte { WIFI_DISCONNECTED=0, WIFI_BAD_CREDS=1, WIFI_NO_AP=2, WIFI_CONNECTED=3 };
|
||||
|
||||
struct FLASHSTICK_PACKET {
|
||||
bool reistered = false;
|
||||
char msg[16] = "Hello...";
|
||||
}flashstickPacket;
|
||||
|
||||
|
||||
// Class for handling server events
|
||||
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||
void onConnect(NimBLEServer* pServer) override {
|
||||
ESP_LOGI(tag, "Flash-Stick connected");
|
||||
}
|
||||
|
||||
void onDisconnect(NimBLEServer* pServer) override {
|
||||
ESP_LOGI(tag, "Flash-Stick disconnected");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Class for handling characteristic events
|
||||
class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks {
|
||||
|
||||
void onWrite(NimBLECharacteristic *pCharacteristic) override {
|
||||
std::string value = pCharacteristic->getValue();
|
||||
ESP_LOGD(tag, "Upgrade Char written with value: %s", value.c_str());
|
||||
|
||||
if (value.compare(0, 12, "wifi-connect") == 0) { // Update WiFi credentials
|
||||
JsonDocument doc;
|
||||
deserializeJson(doc, value.substr(13));
|
||||
JsonObject wifiJson = doc.as<JsonObject>();
|
||||
String ssid = wifiJson["ssid"].as<String>();
|
||||
String pass = wifiJson["pass"].as<String>();
|
||||
ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str());
|
||||
|
||||
bool status = StartWifiConnectTask(ssid, pass);
|
||||
if(status == true){
|
||||
updatePacket.wifiStatus = WIFI_DISCONNECTED;
|
||||
updatePacket.wifiOnline = false;
|
||||
updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0;
|
||||
}else{
|
||||
ESP_LOGI(tag, "Failed to start WiFi connection task");
|
||||
}
|
||||
}
|
||||
else if (value.compare("version-check") == 0) { // Check if new version is available
|
||||
ESP_LOGI(tag, "Version check command received: newVersion=%d.%d.%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch());
|
||||
if(updatePacket.newVersion[0] == 0){
|
||||
startVersionCheckTask(); // start the task and done
|
||||
}else{
|
||||
ESP_LOGI(tag, "Version already checked");
|
||||
}
|
||||
}
|
||||
else if (value.compare("upgrade-start") == 0) { // Start OTA update
|
||||
ESP_LOGI(tag, "Start OTA update command received");
|
||||
startFirmwareUpdateTask(nullptr); // start the task
|
||||
}
|
||||
else if (value.compare("rename-device") == 0) { // Start renaming device
|
||||
ESP_LOGI(tag, "Start renane device command received");
|
||||
}
|
||||
else {
|
||||
ESP_LOGW(tag, "Unknown command received: %s", value.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void onRead(NimBLECharacteristic *pCharacteristic) override {
|
||||
updatePacket.wifiOnline = InternetAvailable;
|
||||
if(WiFi.status() == WL_CONNECTED){
|
||||
updatePacket.wifiStatus = WIFI_CONNECTED;
|
||||
if(updatePacket.wifiIP[0] == 0){
|
||||
updatePacket.wifiIP[0] = WiFi.localIP()[0];
|
||||
updatePacket.wifiIP[1] = WiFi.localIP()[1];
|
||||
updatePacket.wifiIP[2] = WiFi.localIP()[2];
|
||||
updatePacket.wifiIP[3] = WiFi.localIP()[3];
|
||||
}
|
||||
}else{
|
||||
updatePacket.wifiStatus = WIFI_DISCONNECTED;
|
||||
if(updatePacket.wifiIP[0] > 0){
|
||||
updatePacket.wifiIP[0] = 0;
|
||||
updatePacket.wifiIP[1] = 0;
|
||||
updatePacket.wifiIP[2] = 0;
|
||||
updatePacket.wifiIP[3] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//update version
|
||||
if(otaVersion.major() != 0){
|
||||
ESP_LOGI(tag, "Updated new version: major=%d, minor=%d, patch=%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch());
|
||||
updatePacket.newVersion[0] = otaVersion.major();
|
||||
updatePacket.newVersion[1] = otaVersion.minor();
|
||||
updatePacket.newVersion[2] = otaVersion.patch();
|
||||
}
|
||||
|
||||
pCharacteristic->setValue(reinterpret_cast<uint8_t*>(&updatePacket), sizeof(updatePacket));
|
||||
ESP_LOGI(tag, "Upgrade Char read");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void bleUpgrade_send_message(String s){
|
||||
if(pUpgradeCharacteristic2){
|
||||
if (s != nullptr) {
|
||||
pUpgradeCharacteristic2->setValue(s);
|
||||
pUpgradeCharacteristic2->notify();
|
||||
} else {
|
||||
ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Init_UpgradeBLEService(NimBLEServer *pServer){
|
||||
|
||||
// Create Upgrade BLE Service
|
||||
pUpgradeService= pServer->createService( UPGRADE_SERVICE_UUID );
|
||||
|
||||
pUpgradeCharacteristic1 = pUpgradeService->createCharacteristic(
|
||||
UPGRADE_CHARACTERISTIC1_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
// Register the callback with the characteristic
|
||||
pUpgradeCharacteristic1->setCallbacks(new UpgradeChar_Callbacks());
|
||||
ESP_LOGI(tag, "Upgrade callback registered!");
|
||||
|
||||
|
||||
pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic(
|
||||
UPGRADE_CHARACTERISTIC2_UUID,
|
||||
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||
);
|
||||
|
||||
// Register the callback with the characteristic
|
||||
pUpgradeCharacteristic2->setCallbacks(new UpgradeChar_Callbacks());
|
||||
ESP_LOGI(tag, "Upgrade callback registered!");
|
||||
|
||||
pUpgradeService->start();
|
||||
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->addServiceUUID( UPGRADE_SERVICE_UUID ); // Advertise service UUID
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,240 +0,0 @@
|
||||
#include "BTSerial.h"
|
||||
#include <esp_task_wdt.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include <NimBLEDevice.h>
|
||||
#include <string>
|
||||
#include "command_processor.h"
|
||||
#include "led_strip.h"
|
||||
#include "global.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "my_buzzer.h"
|
||||
#include "common/led_animation.h"
|
||||
|
||||
|
||||
static const char* tag = "ble";
|
||||
TaskHandle_t BTSerial_Task_Handle;
|
||||
bool BTDeviceConnected = false;
|
||||
|
||||
BLEServer *pServer = NULL;
|
||||
BLECharacteristic * pTxCharacteristic;
|
||||
BLECharacteristic * pRxCharacteristic;
|
||||
bool oldDeviceConnected = false;
|
||||
uint8_t txValue = 0;
|
||||
|
||||
// See the following for generating UUIDs:
|
||||
// https://www.uuidgenerator.net/
|
||||
#define SERVICE_UUID_DEF "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID
|
||||
#define CHARACTERISTIC_UUID_RX_DEF "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
#define CHARACTERISTIC_UUID_TX_DEF "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
|
||||
String BLEDeviceName;
|
||||
String BLEKey;
|
||||
String SERVICE_UUID;
|
||||
String CHARACTERISTIC_UUID_RX;
|
||||
String CHARACTERISTIC_UUID_TX;
|
||||
|
||||
#define replyActive true
|
||||
|
||||
|
||||
void Init_BTSerial(void)
|
||||
{
|
||||
File file = LittleFS.open("/cfg/ble.json");
|
||||
if(!file){
|
||||
ESP_LOGE(tag, "Error opening ble.json...");
|
||||
}
|
||||
else{
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
if(error){ ESP_LOGE(tag, "ble.json deserialize error!.."); return;}
|
||||
|
||||
JsonObject bleJson = doc.as<JsonObject>();
|
||||
|
||||
// if(jsonConstrainBool(bleJson, "en", false)){
|
||||
SERVICE_UUID = jsonConstrainString(tag, bleJson, "service-uuid", SERVICE_UUID_DEF);
|
||||
ESP_LOGD(tag, "SERVICE_UUID: %s", SERVICE_UUID.c_str());
|
||||
|
||||
CHARACTERISTIC_UUID_RX = jsonConstrainString(tag, bleJson, "char-uuid-rx", CHARACTERISTIC_UUID_RX_DEF);
|
||||
ESP_LOGD(tag, "Char UUID RX: %s", CHARACTERISTIC_UUID_RX.c_str());
|
||||
|
||||
CHARACTERISTIC_UUID_TX = jsonConstrainString(tag, bleJson, "char-uuid-tx", CHARACTERISTIC_UUID_TX_DEF);
|
||||
ESP_LOGD(tag, "Char UUID TX: %s", CHARACTERISTIC_UUID_TX.c_str());
|
||||
|
||||
BLEDeviceName = jsonConstrainString(tag, bleJson, "device-name", "ATA_COMM");
|
||||
String hexStr = String(chipInfo.macByte[0], HEX);
|
||||
hexStr.toUpperCase();
|
||||
BLEDeviceName += '_';
|
||||
BLEDeviceName += hexStr;
|
||||
|
||||
BLEKey = jsonConstrainString(tag, bleJson, "key", "123456");
|
||||
int core = jsonConstrain<int>(tag, bleJson, "core", 0, 1, 0);
|
||||
ESP_LOGD(tag, "BLE SSID: %s, key: %s, core: %d", BLEDeviceName.c_str(),BLEKey.c_str(), core);
|
||||
|
||||
xTaskCreatePinnedToCore(BTSerial_Task, "BTSerial_Task", 12000, NULL, 1, &BTSerial_Task_Handle, core);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
/** None of these are required as they will be handled by the library with defaults. **
|
||||
** Remove as you see fit for your needs */
|
||||
class MyServerCallbacks: public BLEServerCallbacks {
|
||||
void onConnect(BLEServer* pServer) {
|
||||
BTDeviceConnected = true;
|
||||
//BLEDevice::startAdvertising();//adding this line allows for multiple simultaneous BLE connections
|
||||
};
|
||||
/*
|
||||
void onConnect(BLEServer* pServer, BLEClient* pClient) {
|
||||
BTDeviceConnected = true;
|
||||
BLEAddress connectedAddress = pClient->getPeerAddress();
|
||||
Log.traceln("Client connected: %s", connectedAddress.toString().c_str());
|
||||
}
|
||||
*/
|
||||
|
||||
void onDisconnect(BLEServer* pServer) {
|
||||
BTDeviceConnected = false;
|
||||
}
|
||||
/***************** New - Security handled here ********************
|
||||
****** Note: these are the same return values as defaults ********/
|
||||
uint32_t onPassKeyRequest(){
|
||||
ESP_LOGD(tag, "Server PassKeyRequest");
|
||||
return 123456;
|
||||
}
|
||||
|
||||
bool onConfirmPIN(uint32_t pass_key){
|
||||
ESP_LOGD(tag, "The passkey YES/NO number: %d", pass_key);
|
||||
return true;
|
||||
}
|
||||
|
||||
void onAuthenticationComplete(ble_gap_conn_desc desc){
|
||||
ESP_LOGD(tag, "Starting BLE work!");
|
||||
}
|
||||
/*******************************************************************/
|
||||
};
|
||||
|
||||
#define MAX_PACKET_PARAMS 8
|
||||
int packet_data[MAX_PACKET_PARAMS];
|
||||
int paramCount = 0;
|
||||
class MyCallbacks: public BLECharacteristicCallbacks
|
||||
{
|
||||
void onWrite(BLECharacteristic *pCharacteristic)
|
||||
{
|
||||
std::string rxValue = pCharacteristic->getValue();
|
||||
|
||||
ESP_LOGD(tag, "raw: %s", rxValue.c_str());
|
||||
if(!rxValue.empty() && ((rxValue[0] == '$') || rxValue[0] == '%')){
|
||||
extractCommand(&rxValue[0], packet_data, ¶mCount); // delimited command
|
||||
|
||||
// Call Animation Index Update
|
||||
if(packet_data[0] == 100){
|
||||
int cntDown = 0;
|
||||
if(packet_data[2]){
|
||||
cntDown = packet_data[2];
|
||||
}
|
||||
animProps.event[packet_data[1]].countDown = cntDown;
|
||||
animProps.event[packet_data[1]].type = EV_NORMAL;
|
||||
PostNewEvent(animProps.event[packet_data[1]]);
|
||||
}else{
|
||||
// TODO: Process other Bluetooth commands
|
||||
}
|
||||
|
||||
if(replyActive){
|
||||
rxValue[0] = '%';
|
||||
pTxCharacteristic->setValue(rxValue);
|
||||
pTxCharacteristic->notify();
|
||||
}
|
||||
// print params
|
||||
ESP_LOGD(tag, "packet: %c%d, %d", rxValue[0], packet_data[0], packet_data[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void extractCommand(char* packet, int* data, int* count)
|
||||
{
|
||||
int base = 10;
|
||||
//if(*packet == '#'){
|
||||
// base = 16;
|
||||
//}
|
||||
packet++;
|
||||
|
||||
char* token = strtok(packet, ",\n");
|
||||
int8_t index = 0;
|
||||
|
||||
while (token != NULL && index < MAX_PACKET_PARAMS) {
|
||||
data[index] = strtol(token, NULL, base);
|
||||
index++;
|
||||
token = strtok(NULL, ",\n");
|
||||
}
|
||||
*count = index;
|
||||
}
|
||||
|
||||
void BTSerial_Task(void *parameters){
|
||||
vTaskDelay(1000);
|
||||
// Extend watchdog timer
|
||||
esp_task_wdt_init(5, false);
|
||||
|
||||
// Create the BLE Device.
|
||||
BLEDevice::init(BLEDeviceName.c_str());
|
||||
|
||||
// Create the BLE Server
|
||||
pServer = BLEDevice::createServer();
|
||||
pServer->setCallbacks(new MyServerCallbacks());
|
||||
|
||||
// Create the BLE Service
|
||||
BLEService *pService = pServer->createService((const char*)SERVICE_UUID.c_str());
|
||||
|
||||
// Create a BLE Characteristic
|
||||
pTxCharacteristic = pService->createCharacteristic( (const char*)CHARACTERISTIC_UUID_TX.c_str(), NIMBLE_PROPERTY::NOTIFY );
|
||||
/***************************************************
|
||||
NOTE: DO NOT create a 2902 descriptor, it will be created auto.. if notifications
|
||||
or indications are enabled on a characteristic.
|
||||
|
||||
pCharacteristic->addDescriptor(new BLE2902());
|
||||
****************************************************/
|
||||
pRxCharacteristic = pService->createCharacteristic( (const char*)CHARACTERISTIC_UUID_RX.c_str(), NIMBLE_PROPERTY::WRITE );
|
||||
pRxCharacteristic->setCallbacks(new MyCallbacks());
|
||||
// Start the service
|
||||
pService->start();
|
||||
|
||||
// Start advertising
|
||||
vTaskDelay(100);
|
||||
pServer->getAdvertising()->start();
|
||||
ESP_LOGV(tag, "Waiting for a client...");
|
||||
ANIMATION_EVENT newEvent;
|
||||
for(;;){
|
||||
//if (BTDeviceConnected) {
|
||||
//pTxCharacteristic->setValue(&txValue, 1);
|
||||
//pTxCharacteristic->notify();
|
||||
//txValue++;
|
||||
//vTaskDelay(100); // bluetooth stack will go into congestion, if too many packets are sent
|
||||
//}
|
||||
|
||||
// disconnecting
|
||||
if (!BTDeviceConnected && oldDeviceConnected) {
|
||||
vTaskDelay(750); // give the bluetooth stack the chance to get things ready
|
||||
pServer->startAdvertising(); // restart advertising
|
||||
oldDeviceConnected = BTDeviceConnected;
|
||||
newEvent.animIndex = POP_BLE_DISC;
|
||||
newEvent.type = EV_INJECT;
|
||||
PostNewEvent(newEvent);
|
||||
ESP_LOGI(tag, "Client disconnected...");
|
||||
ESP_LOGI(tag, "Advertising again...");
|
||||
Buzzer_Play_Tune(TUNE_BLE_DISCONNECTED);
|
||||
}
|
||||
|
||||
// connecting
|
||||
if (BTDeviceConnected && !oldDeviceConnected) {
|
||||
// do stuff here on connecting
|
||||
oldDeviceConnected = BTDeviceConnected;
|
||||
newEvent.animIndex = POP_BLE_CONN;
|
||||
newEvent.type = EV_INJECT;
|
||||
PostNewEvent(newEvent);
|
||||
ESP_LOGI(tag, "Client connected...");
|
||||
Buzzer_Play_Tune(TUNE_BLE_CONNECTED);
|
||||
}
|
||||
|
||||
vTaskDelay(200);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
#ifndef _BTSERIAL_H
|
||||
#define _BTSERIAL_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
|
||||
extern bool BTDeviceConnected;
|
||||
extern String BLEDeviceName;
|
||||
|
||||
void Init_BTSerial(void);
|
||||
|
||||
void BTSerial_Task(void *parameters);
|
||||
|
||||
void extractCommand(char* packet, int* data, int* count);
|
||||
|
||||
#endif
|
||||
@ -1,82 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="event-box.js"></script>
|
||||
<script src="hue-select.js"></script>
|
||||
<title>Event Container</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
min-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
transform: scale(2.5);
|
||||
}
|
||||
input[type="checkbox"].css-checkbox + label.css-label {
|
||||
display: inline-block;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 1px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<br>
|
||||
<div>
|
||||
|
||||
<event-box id="event1"></event-box>
|
||||
<!--<hue-select id="hueItem"></hue-select>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.onload = function() { onLoad(); };
|
||||
|
||||
const event1 = document.getElementById('event1');
|
||||
const hueItem = document.getElementById('hueItem');
|
||||
|
||||
|
||||
|
||||
function onLoad(){
|
||||
//debugger;
|
||||
hueItem.setHue(145);
|
||||
/*
|
||||
event1.setTitle("Event 0");
|
||||
debugger;
|
||||
//event1.setHueValue(101);
|
||||
|
||||
|
||||
//event1.setParam1Name(`Param1`);
|
||||
event1.setParam2Name("Cooling");
|
||||
event1.setCheck1Name("CHECK1");
|
||||
event1.setCheck2Name(`Check2`);
|
||||
|
||||
event1.setHueValue(140);
|
||||
event1.setSpeedSettings("SPEED: ");
|
||||
event1.addOptionToList(0, "Animation0");
|
||||
event1.addOptionToList(1, "Animation1");
|
||||
event1.addOptionToList(2, "Animation2");
|
||||
event1.setAnimationIndex(1);
|
||||
*/
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,164 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
def copy_folder_to_destination(src_path, dest_path, skip_dirs=None, skip_files=None):
|
||||
# Ensure the destination directory exists
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
|
||||
# Remove existing folder if present
|
||||
if os.path.exists(dest_path):
|
||||
shutil.rmtree(dest_path)
|
||||
|
||||
# Create destination directory
|
||||
os.makedirs(dest_path)
|
||||
|
||||
# Walk through source directory
|
||||
for root, dirs, files in os.walk(src_path):
|
||||
# Remove directories to skip from dirs list
|
||||
if skip_dirs:
|
||||
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
||||
|
||||
# Calculate relative path
|
||||
rel_path = os.path.relpath(root, src_path)
|
||||
dest_dir = os.path.join(dest_path, rel_path)
|
||||
|
||||
# Create corresponding destination directory
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
# Copy files that aren't in skip_files
|
||||
for file in files:
|
||||
if skip_files and file in skip_files:
|
||||
continue
|
||||
src_file = os.path.join(root, file)
|
||||
dest_file = os.path.join(dest_dir, file)
|
||||
shutil.copy2(src_file, dest_file)
|
||||
|
||||
def calculate_md5(file_path):
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def get_file_size(file_path):
|
||||
return os.path.getsize(file_path)
|
||||
|
||||
def update_json_file(json_array, folder_path):
|
||||
# Create new data array for files
|
||||
file_array = []
|
||||
|
||||
# Walk through the copied folder and collect file details
|
||||
for root, _, files in os.walk(folder_path):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
relative_path = os.path.relpath(file_path, folder_path)
|
||||
|
||||
# Replace backslashes with forward slashes
|
||||
relative_path = relative_path.replace('\\', '/')
|
||||
|
||||
file_entry = {
|
||||
"remote": os.path.join("data/", relative_path),
|
||||
"local": os.path.join("/", relative_path),
|
||||
"md5": calculate_md5(file_path),
|
||||
"size": get_file_size(file_path)
|
||||
}
|
||||
file_array.append(file_entry)
|
||||
|
||||
# Replace the contents of the input json_array with new data
|
||||
json_array.clear()
|
||||
json_array.extend(file_array)
|
||||
|
||||
def update_files(src_path, dest_path, skip_dirs=None, skip_files=None):
|
||||
# Check if the source folder exists
|
||||
if not os.path.isdir(src_path) or not os.path.isdir(dest_path):
|
||||
print("Invalid folder path!")
|
||||
return
|
||||
|
||||
# Copy all data contents
|
||||
copy_folder_to_destination(src_path, dest_path, skip_dirs, skip_files)
|
||||
print("Folder copied successfully.")
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
here_path = os.path.dirname(os.path.abspath(__file__))
|
||||
project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Path of the folder to copy (you can modify this)
|
||||
src_folder_name = "data"
|
||||
src_path = os.path.join(project_path, src_folder_name)
|
||||
#print(f"source path: {src_path}")
|
||||
|
||||
# Path of the destination folder
|
||||
dest_folder_name = "firmware_update\\latest\\data"
|
||||
dest_path = os.path.join(project_path, dest_folder_name)
|
||||
#print(f"destination path: {dest_path}")
|
||||
|
||||
# Path of the firmware binary file
|
||||
bin_name = ".pio\\build\\esp32s3dev\\firmware.bin"
|
||||
|
||||
# Skip these directories
|
||||
skip_dirs = ["boards", "booths"]
|
||||
|
||||
# Skip these files
|
||||
skip_files = ["wifi.json", "system.json", "luma-stiks.json", ]
|
||||
|
||||
|
||||
update_files = input("Do you want to update the files? (y/n): ")
|
||||
|
||||
# *********************** Copy Data Files ***********************
|
||||
if update_files.lower() == "y":
|
||||
update_files(src_path, dest_path, skip_dirs, skip_files)
|
||||
|
||||
|
||||
# *********************** Copy Binary file ***********************
|
||||
update_firmware = input("Do you want to update the firmware? (y/n): ")
|
||||
if update_firmware.lower() == "y":
|
||||
# Copy firmware.bin to the destination
|
||||
bin_path = os.path.join(project_path, bin_name)
|
||||
shutil.copy(bin_path, here_path)
|
||||
#print(f"firmware path: {bin_path}")
|
||||
print("firmware.bin copied successfully.")
|
||||
|
||||
|
||||
|
||||
# *********************** Process update.json ***********************
|
||||
# Update the JSON file
|
||||
json_path = os.path.join(here_path, "update.json")
|
||||
print(f"json path: {json_path}")
|
||||
|
||||
# Read existing JSON
|
||||
with open(json_path, "r") as f:
|
||||
try:
|
||||
json_doc = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
print("Invalid JSON file!")
|
||||
return
|
||||
|
||||
# process the files array
|
||||
#if update_files.lower() == "y":
|
||||
json_files_array = json_doc["files"]
|
||||
update_json_file(json_files_array, dest_path)
|
||||
#print(f"Folder {os.path.basename(src_path)} processed successfully.")
|
||||
|
||||
|
||||
# *********************** Process firmwware.bin in update.json ***********************
|
||||
# process the firmware
|
||||
if update_firmware.lower() == "y":
|
||||
json_firmware = json_doc["firmware"]
|
||||
firmware_path = os.path.join(here_path, "firmware.bin")
|
||||
json_firmware["md5"] = calculate_md5(firmware_path)
|
||||
json_firmware["size"] = get_file_size(firmware_path)
|
||||
|
||||
|
||||
# Write updated JSON
|
||||
with open(json_path, "w") as f:
|
||||
json.dump(json_doc, f, indent=4)
|
||||
|
||||
print("Update JSON files created successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,47 +0,0 @@
|
||||
#include <NeoPixelBus.h>
|
||||
|
||||
// Define constants for maximum LEDs
|
||||
#define MAX_LEDS 300 // Adjust based on your LED requirements
|
||||
|
||||
// Wrapper class for dynamic LED strips
|
||||
class DynamicLedStrip {
|
||||
private:
|
||||
void* strip; // Pointer to the NeoPixelBus object
|
||||
int numLeds; // Number of LEDs
|
||||
String colorOrder; // Color order (for reference)
|
||||
|
||||
public:
|
||||
DynamicLedStrip() : strip(nullptr), numLeds(0), colorOrder("") {}
|
||||
|
||||
// Initialize the LED strip
|
||||
void initialize(int dataPin, int numLeds, const String& chipType, const String& colorOrder) {
|
||||
this->numLeds = numLeds;
|
||||
this->colorOrder = colorOrder;
|
||||
|
||||
if (chipType == "WS2812" || chipType == "SK6812") {
|
||||
if (colorOrder == "GRB") {
|
||||
strip = new NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
} else if (colorOrder == "RGB") {
|
||||
strip = new NeoPixelBus<NeoRgbFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
} else if (colorOrder == "BGR") {
|
||||
strip = new NeoPixelBus<NeoBgrFeature, Neo800KbpsMethod>(numLeds, dataPin);
|
||||
}
|
||||
}
|
||||
|
||||
if (strip) {
|
||||
static_cast<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>*>(strip)->Begin();
|
||||
} else {
|
||||
Serial.println("Unsupported chipset or color order!");
|
||||
}
|
||||
}
|
||||
|
||||
// Set all LEDs to a specific color
|
||||
void setColor(const RgbColor& color) {
|
||||
if (strip) {
|
||||
for (int i = 0; i < numLeds; i++) {
|
||||
static_cast<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>*>(strip)->SetPixelColor(i, color);
|
||||
}
|
||||
static_cast<NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>*>(strip)->Show();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,337 +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 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 = False
|
||||
UPLOAD_MANIFEST = True
|
||||
UPLOAD_DATA = False
|
||||
|
||||
# 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')
|
||||
|
||||
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(LOCAL_ROOT_PATH / 'latest' / 'firmware.bin')
|
||||
LOCAL_MANIFEST_PATH = str(LOCAL_ROOT_PATH / 'latest' / 'update.json')
|
||||
LOCAL_DATA_DIRECTORY = str(LOCAL_ROOT_PATH / 'latest' / 'data')
|
||||
|
||||
# =============================================================================
|
||||
# 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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# Manifest
|
||||
if UPLOAD_MANIFEST:
|
||||
manifest_key = join_key(dest_prefix, 'update.json') if dest_prefix else 'update.json'
|
||||
upload_file(client, LOCAL_MANIFEST_PATH, manifest_key)
|
||||
|
||||
# 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.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,623 +0,0 @@
|
||||
{{NAVBAR}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>App Control Configuration</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="global-style.css" rel="stylesheet">
|
||||
<style>
|
||||
#table{
|
||||
overflow: auto;
|
||||
}
|
||||
#first_td_th {
|
||||
width:325px;
|
||||
}
|
||||
#v2_td_th {
|
||||
width:200px;
|
||||
}
|
||||
#color-picker{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: transparent;
|
||||
}
|
||||
#select-anim {
|
||||
width:175px;
|
||||
}
|
||||
input{
|
||||
cursor:pointer;
|
||||
}
|
||||
#slide-density{
|
||||
width: 70%;
|
||||
}
|
||||
#slide-speed{
|
||||
accent-color: rgb(181, 53, 53);
|
||||
width: 70%;
|
||||
}
|
||||
input::-webkit-slider-runnable-track {
|
||||
width: 450px;
|
||||
/*background: linear-gradient(to right, red, orange, yellow, lime, green, Turquoise, Cyan, Blue, Violet, Magenta, Crimson, red);*/
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Control Configuration</h1>
|
||||
<!--<form action="/upload" method="POST" enctype="multipart/form-data"> -->
|
||||
|
||||
<fieldset>
|
||||
<legend>Saved Animation Profiles</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label for="selSavedAnimProfiles">Profiles:</label>
|
||||
<select name= "selSavedAnimProfiles" id="selSaveddAnimProfiles" style="width:150px">
|
||||
<option>(1)</option>
|
||||
<option>(2)</option>
|
||||
<option>(3)</option>
|
||||
<option>(4)</option>
|
||||
<option>(5)</option>
|
||||
<option>(6)</option>
|
||||
<option>(7)</option>
|
||||
<option>(8)</option>
|
||||
</select>
|
||||
  
|
||||
<label for="profileName">Name:</label>
|
||||
<input type="text" name="inputProfileName" id="profileName">
|
||||
   
|
||||
<button id="saveProfile" onclick="sendProfilesToServer()">Save Profile</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<tr><td>
|
||||
<div id="spacer-20"></div>
|
||||
</td></tr>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendLights">Countdown Animation ( White Fill )</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label for="constlight">Light Min: (0-100)</label> <br>
|
||||
<input type="range" name="constLightMin" id="constlight" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="constlight">Light Max:</label> <br>
|
||||
<input type="range" name="constLightMax" id="constlight" min="0" max="100" value="90" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<!-- <button>Try</button> -->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td>
|
||||
<div id="spacer-10"></div>
|
||||
</td></tr>
|
||||
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label for="Holdtime">Hold time(ms):</label><br>
|
||||
<input type="number" name="holdTime" id="holdtime" min="0" max="5000" value="500">
|
||||
</td>
|
||||
<td>
|
||||
<label for="Ramptime">Ramp down(ms):</label><br>
|
||||
<input type="number" name="rampTime" id="Ramptime" min="0" max="5000" value="500">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim0">Event 1</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim0" id="select-anim0">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim0" id="color-picker" > 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim0" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density0" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed0" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(0)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim1">Event 2</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim1" id="select-anim1">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim1" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim1" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density1" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed1" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(1)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim2">Event 3</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim2" id="select-anim2">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim2" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim2" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density2" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed2" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(2)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim3">Event 4</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim3" id="select-anim3">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim3" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim3" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density3" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed3" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(3)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim4">Event 5</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim4" id="select-anim4">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim4" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim4" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density4" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed4" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(4)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim5">Event 6</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim5" id="select-anim5">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim5" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim5" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density5" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed5" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(5)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim6">Event 7</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim6" id="select-anim6">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim6" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim6" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density6" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed6" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(6)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendAnim7">Event 8</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim7" id="select-anim7">
|
||||
<option>Animation 1</option>
|
||||
<option>Animation 2</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Main:</label><br>
|
||||
<input type="color" name="main-color-anim7" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Base:</label><br>
|
||||
<input type="color" name="base-color-anim7" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-density">Density:</label> <br>
|
||||
<input type="range" name="slide-density7" id="slide-density" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<label for="slide-speed">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed7" id="slide-speed" min="0" max="100" value="25" step="1">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="sendPlayAnim(7)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<!--</form> -->
|
||||
<script>
|
||||
|
||||
//const websocket = new WebSocket(`ws://${window.location.hostname}/wsdata`);
|
||||
//const websocket = new WebSocket({{WEBSOCKET_ADDR}});
|
||||
|
||||
// Initialize Names
|
||||
var animProf = [8];
|
||||
var profilesJson;
|
||||
const inputProfileName = document.getElementsByName('inputProfileName')[0];
|
||||
const selSavedAnimProfiles = document.getElementsByName('selSavedAnimProfiles')[0];
|
||||
const constLightMin = document.getElementsByName('constLightMin')[0];
|
||||
const constLightMax = document.getElementsByName('constLightMax')[0];
|
||||
const holdTime = document.getElementsByName('holdTime')[0];
|
||||
const rampTime = document.getElementsByName('rampTime')[0];
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
animProf[i] = {
|
||||
selAnim: document.getElementsByName('sel-anim' + i)[0],
|
||||
colMain: document.getElementsByName('main-color-anim' + i)[0],
|
||||
colBase: document.getElementsByName('base-color-anim' + i)[0],
|
||||
density: document.getElementsByName('slide-density' + i)[0],
|
||||
speed: document.getElementsByName('slide-speed' + i)[0]
|
||||
};
|
||||
}
|
||||
|
||||
// When new profile is selected
|
||||
selSaveddAnimProfiles.addEventListener('change', function(event) {
|
||||
const selectedValue = event.target.selectedIndex;
|
||||
console.log('Selected value:', selectedValue);
|
||||
setProfile(selectedValue);
|
||||
});
|
||||
|
||||
// update the animation data
|
||||
function setProfile(index){
|
||||
// update profiles
|
||||
inputProfileName.value = profilesJson.profiles[index].name;
|
||||
//selSavedAnimProfiles.selectedIndex = index;
|
||||
constLightMin.value = profilesJson.countdown.min;
|
||||
constLightMax.value = profilesJson.countdown.max;
|
||||
holdTime.value = profilesJson.countdown.hold;
|
||||
rampTime.value = profilesJson.countdown.ramp;
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
animProf[i].selAnim.selectedIndex = profilesJson.profiles[index].anims[i].anim;
|
||||
animProf[i].colMain.value = profilesJson.profiles[index].anims[i].colmain;
|
||||
animProf[i].colBase.value = profilesJson.profiles[index].anims[i].colbase;
|
||||
animProf[i].density.value = profilesJson.profiles[index].anims[i].density;
|
||||
animProf[i].speed.value = profilesJson.profiles[index].anims[i].speed;
|
||||
}
|
||||
}
|
||||
/*
|
||||
// Send test animation for viewing
|
||||
function sendPlayAnim(animIndex){
|
||||
const type = 'try-anim';
|
||||
console.log("try index= " + animIndex);
|
||||
profileIndex = selSavedAnimProfiles.selectedIndex;
|
||||
|
||||
const playAnim = {
|
||||
anim: animProf[animIndex].selAnim.selectedIndex,
|
||||
colmain: animProf[animIndex].colMain.value,
|
||||
colbase: animProf[animIndex].colBase.value,
|
||||
density: animProf[animIndex].density.value,
|
||||
speed: animProf[animIndex].speed.value
|
||||
};
|
||||
|
||||
const payload = { type:"try-anim", data: JSON.stringify(playAnim) }
|
||||
console.log(payload);
|
||||
websocket.send( JSON.stringify(payload) );
|
||||
}
|
||||
|
||||
// Send complete json to server
|
||||
function sendProfilesToServer(event) {
|
||||
//event.preventDefault();
|
||||
|
||||
var index = selSavedAnimProfiles.selectedIndex;
|
||||
profilesJson.profiles[index].name = inputProfileName.value;
|
||||
profilesJson.countdown = {
|
||||
min: constLightMin.value,
|
||||
max: constLightMax.value,
|
||||
hold: holdTime.value,
|
||||
ramp: rampTime.value
|
||||
};
|
||||
|
||||
for(let i = 0; i < 8; i++){
|
||||
profilesJson.profiles[index].anims[i] = {
|
||||
anim: animProf[i].selAnim.selectedIndex,
|
||||
colmain: animProf[i].colMain.value,
|
||||
colbase: animProf[i].colBase.value,
|
||||
density: animProf[i].density.value,
|
||||
speed: animProf[i].speed.value
|
||||
};
|
||||
}
|
||||
|
||||
const payload = { type:"save-profiles", data: JSON.stringify(profilesJson) }
|
||||
//console.log(profilesJson);
|
||||
websocket.send( JSON.stringify(payload) );
|
||||
};
|
||||
|
||||
// Receive profiles json
|
||||
websocket.onmessage = (event) => {
|
||||
try{
|
||||
console.log('Received data:', event.data);
|
||||
profilesJson = JSON.parse(event.data);
|
||||
index = profilesJson.index;
|
||||
|
||||
inputProfileName.value = profilesJson.profiles[index].name;
|
||||
//Upate profile list
|
||||
for(let i = 0; i < 8; i++){
|
||||
opText = selSavedAnimProfiles.options[i].textContent + ' - ' + profilesJson.profiles[i].name;
|
||||
selSavedAnimProfiles.options[i].textContent = opText;
|
||||
}
|
||||
setProfile(index);
|
||||
}
|
||||
catch(error){
|
||||
console.error('Error parsing received JSON data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get profiles json
|
||||
websocket.onopen = () => {
|
||||
const type = 'get-profiles';
|
||||
const data = {};
|
||||
|
||||
const payload = { type: type, data: data }
|
||||
websocket.send(JSON.stringify(payload));
|
||||
};
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
function handleColorChange(event) {
|
||||
const colorPicker = event.target;
|
||||
const colorValue = colorPicker.value;
|
||||
|
||||
// Extract the hue and lightness values from the color value
|
||||
const hue = colorValue.substring(1, 4);
|
||||
const lightness = colorValue.substring(5, 7);
|
||||
|
||||
// Set the saturation value to a fixed value (e.g., 100%)
|
||||
const lockedColorValue = `hsl(${hue}, 100%, ${lightness}%)`;
|
||||
colorPicker.value = lockedColorValue;
|
||||
}
|
||||
*/
|
||||
// save profilesJson
|
||||
function postProfiles(){
|
||||
fetch('/post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'type': 'profiles'
|
||||
} ,
|
||||
body: JSON.stringify(jsonProfiles)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Handle successful response
|
||||
console.log('Request successful');
|
||||
} else {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle any errors that occurred during the request
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
// send anim to test out
|
||||
function postPlayAnim(animIndex){
|
||||
fetch('/post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'type': 'play-anim'
|
||||
} ,
|
||||
body: JSON.stringify(jsonProfiles.profiles[0])
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Handle successful response
|
||||
console.log('Request successful');
|
||||
} else {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle any errors that occurred during the request
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Get profiles
|
||||
function getProfiles(){
|
||||
fetch('/get',{
|
||||
method: 'GET',
|
||||
headers:{'type':'profiles'}
|
||||
})
|
||||
.then(response => {
|
||||
const index = response.headers.get('index');
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
profilesJsaon = data;
|
||||
console.log(data);
|
||||
setProfile(index);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,508 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATA Firmware Update</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-indicator-ble {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-wifi {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-internet {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Adds space above the WiFi Connect button */
|
||||
.btn-container.wifi {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
max-width: 130px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ATA Firmware Update</h1>
|
||||
|
||||
<!-- Status Indicators -->
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-ble"></span>
|
||||
<label id="status-ble-connection">Device: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-wifi"></span>
|
||||
<label id="status-wifi-client">Wifi Client: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-internet"></span>
|
||||
<label id="status-internet">Internet: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<label id="status-current-version">Curr Version: ...</label>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<label id="status-new-version">New Version: ...</label>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="btn-container">
|
||||
<button id="bleConnectBtn">Connect</button>
|
||||
<button id="checkStatusBtn" disabled>Check Status</button>
|
||||
</div>
|
||||
|
||||
<!-- Log Area -->
|
||||
<textarea id="logArea" readonly></textarea>
|
||||
|
||||
<div class="btn-container">
|
||||
<button id="checkVersionBtn" disabled>Check Version</button>
|
||||
<button id="startUpgradeBtn" disabled>Start Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Wi-Fi Input Fields -->
|
||||
<div class="input-container">
|
||||
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
||||
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
||||
<div style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" id="showPassword" style="width: auto;">
|
||||
<label for="showPassword">Show Password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Added margin-top above this button -->
|
||||
<div class="btn-container wifi">
|
||||
<button id="wifiConnectBtn" disabled>Connect Wifi</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Constants
|
||||
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
|
||||
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
|
||||
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
|
||||
let bleDevice = null;
|
||||
let bleCharacteristic1 = null;
|
||||
let bleCharacteristic2 = null;
|
||||
let bleConnected = false;
|
||||
|
||||
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
|
||||
|
||||
let updatePacket = {
|
||||
wifiConnected: false,
|
||||
wifiOnline: false,
|
||||
wifiIP: [0, 0, 0, 0],
|
||||
currVersion: [0, 0, 0],
|
||||
newVersion: [0, 0, 0]
|
||||
};
|
||||
|
||||
// Log messages to the textarea
|
||||
function logMessage(message) {
|
||||
const logArea = document.getElementById('logArea');
|
||||
logArea.value += message + '\n';
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to scan for BLE devices
|
||||
async function scanForDevices() {
|
||||
logMessage('Scanning for BLE devices...');
|
||||
try {
|
||||
const device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: true,
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
if (device) {
|
||||
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
|
||||
} else {
|
||||
logMessage('No devices found.');
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage(`Scan failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to connect to the BLE server
|
||||
async function connectToBle() {
|
||||
try {
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ name: BLE_SERVER_NAME }],
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
//logMessage(`Connecting to ${bleDevice.name}`);
|
||||
const server = await bleDevice.gatt.connect();
|
||||
//await server.setPreferredMtu(247); // Request larger MTU size
|
||||
|
||||
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
||||
|
||||
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
//await bleCharacteristic1.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
|
||||
|
||||
|
||||
//logMessage('Getting characteristic...');
|
||||
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
await bleCharacteristic2.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
|
||||
const value = event.target.value;
|
||||
const decoder = new TextDecoder();
|
||||
const decodedValue = decoder.decode(value);
|
||||
logMessage('--> ' + decodedValue);
|
||||
});
|
||||
|
||||
bleConnected = true;
|
||||
document.getElementById('bleConnectBtn').disabled = true;
|
||||
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
|
||||
document.getElementById('status-ble-connection').textContent = 'Device: Connected';
|
||||
document.getElementById('wifiConnectBtn').disabled = false;
|
||||
document.getElementById('checkStatusBtn').disabled = false;
|
||||
logMessage(`Connected to ${bleDevice.name}`);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes("cancelled")) {
|
||||
logMessage("Connection cancelled by user.");
|
||||
} else {
|
||||
logMessage(`Connection failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPacket(packetMsg) {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot send packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
|
||||
const encoder = new TextEncoder();
|
||||
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
|
||||
//console.log("Request sent successfully");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to send packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to send request.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readPacket() {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot read packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const value = await bleCharacteristic1.readValue();
|
||||
const data = new Uint8Array(value.buffer);
|
||||
if (data.length === 12) {
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.wifiOnline = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
//processUpdatePacket(updatePacket);
|
||||
return;
|
||||
}
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to read packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to read packet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process update packet
|
||||
function processUpdatePacket(packet) {
|
||||
// Process the packet data
|
||||
//console.log("Processing update packet:", packet);
|
||||
if(packet.wifiConnected === true) {
|
||||
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
|
||||
}
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
|
||||
}
|
||||
|
||||
if(packet.wifiOnline === true) {
|
||||
document.getElementById('status-internet').textContent = 'Online';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
|
||||
document.getElementById('checkVersionBtn').disabled = false;
|
||||
|
||||
} else {
|
||||
document.getElementById('status-internet').textContent = 'Offline';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
}
|
||||
|
||||
if (packet.currVersion[0] > 0) {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
|
||||
} else {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
|
||||
}
|
||||
|
||||
if (packet.newVersion[0] > 0) {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
|
||||
if(packet.wifiOnline && packet.newVersion[0] > packet.currVersion[0] ||
|
||||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] > packet.currVersion[1]) ||
|
||||
(packet.newVersion[0] === packet.currVersion[0] && packet.newVersion[1] === packet.currVersion[1] && packet.newVersion[2] > packet.currVersion[2])) {
|
||||
|
||||
//enable start upgrade button
|
||||
logMessage("New Version Available:");
|
||||
document.getElementById('startUpgradeBtn').disabled = false;
|
||||
} else {
|
||||
//disable start upgrade button
|
||||
document.getElementById('startUpgradeBtn').disabled = true;
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
} else {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ...';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
|
||||
function handleChar1Notifications(event) {
|
||||
const data = new Uint8Array(event.data);
|
||||
|
||||
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing updatePacket object instead of creating new one
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.internetAvailable = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
processUpdatePacket(updatePacket);
|
||||
}
|
||||
|
||||
document.getElementById('showPassword').addEventListener('change', function() {
|
||||
const passwordInput = document.getElementById('wifipassword');
|
||||
passwordInput.type = this.checked ? 'text' : 'password';
|
||||
});
|
||||
|
||||
// Event listeners for buttons
|
||||
document.getElementById('bleConnectBtn').addEventListener('click', connectToBle);
|
||||
document.getElementById('checkStatusBtn').addEventListener('click', async () => {
|
||||
if (bleCharacteristic1) {
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
logMessage('BLE device not connected.');
|
||||
}
|
||||
});
|
||||
document.getElementById('checkVersionBtn').addEventListener('click', async () => {
|
||||
await sendPacket('version-check');
|
||||
// loop and monitor the the updatePacket.newVersion
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
success = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await readPacket();
|
||||
if (updatePacket.newVersion[0] > 0) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
if(success) {
|
||||
processUpdatePacket(updatePacket);
|
||||
logMessage("New Version: Available");
|
||||
} else {
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
|
||||
});
|
||||
document.getElementById('wifiConnectBtn').addEventListener('click', async () => {
|
||||
const ssid = document.getElementById('wifissid').value;
|
||||
const password = document.getElementById('wifipassword').value;
|
||||
if (ssid && password) {
|
||||
// Send credentials to the device
|
||||
jsonString = ' {"ssid":"' + ssid + '","pass":"' + password + '"} ';
|
||||
await sendPacket('wifi-connect' + jsonString);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
alert('Please enter both SSID and password.');
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
document.getElementById('startUpgradeBtn').addEventListener('click', async () => {
|
||||
try {
|
||||
await sendPacket('upgrade-start');
|
||||
logMessage("Upgrade Starting... Please wait.");
|
||||
} catch (error) {
|
||||
logMessage(`Error starting upgrade: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial call to process the update packet with defaults
|
||||
processUpdatePacket(updatePacket);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,547 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATA Firmware Update</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status-indicator-ble {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-wifi {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-internet {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Adds space above the WiFi Connect button */
|
||||
.btn-container.wifi {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
max-width: 130px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>ATA Firmware Update</h1>
|
||||
|
||||
<!-- Status Indicators -->
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-ble"></span>
|
||||
<label id="status-ble-connection">BLE Status: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-wifi"></span>
|
||||
<label id="status-wifi-client">Wifi Client: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<span class="status-indicator-internet"></span>
|
||||
<label id="status-internet">Internet: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<label id="status-current-version">Curr Version: ...</label>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<label id="status-new-version">New Version: ...</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<label id="ble-device-name">Device Name:</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<input type="text" id="input-DeviceName" placeholder="..." style="width: 100%; max-width: 220px;" required>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="btn-container">
|
||||
<button id="bleConnectBtn" onclick="connectToBle()">Connect</button>
|
||||
<button id="checkStatusBtn" onclick="checkStatus()" disabled>Check Status</button>
|
||||
</div>
|
||||
|
||||
<!-- Log Area -->
|
||||
<textarea id="logArea" readonly></textarea>
|
||||
|
||||
<div class="btn-container">
|
||||
<button id="checkVersionBtn" onclick="checkVersion()" disabled>Check Version</button>
|
||||
<button id="startUpgradeBtn" onclick="startUpgrade()" disabled>Start Update</button>
|
||||
</div>
|
||||
|
||||
<!-- Wi-Fi Input Fields -->
|
||||
<div class="input-container">
|
||||
<input type="text" id="wifissid" name="wifissid" placeholder="Enter WiFi SSID" required>
|
||||
<input type="password" id="wifipassword" name="wifipassword" placeholder="Enter WiFi Password" required>
|
||||
<div style="display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" id="showPassword" onclick="togglePasswordVisibility()" style="width: auto;">
|
||||
<label for="showPassword">Show Password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Added margin-top above this button -->
|
||||
<div class="btn-container wifi">
|
||||
<button id="wifiConnectBtn" onclick="wifiConnect()" disabled>Connect Wifi</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
// Constants
|
||||
const BLE_SERVER_NAME = "ATALIGHTS"; // Replace with your server name
|
||||
const BLE_SERVICE_UUID = "abcdef01-2345-6789-1234-56789abcdef0"; // Replace with your service UUID
|
||||
const BLE_CHARACTERISTIC1_UUID = "abcdef01-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
const BLE_CHARACTERISTIC2_UUID = "abcdef02-2345-6789-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
|
||||
let bleDevice = null;
|
||||
let bleCharacteristic1 = null;
|
||||
let bleCharacteristic2 = null;
|
||||
let bleConnected = false;
|
||||
|
||||
const WIFI_STAT = { WIFI_DISCONNECTED:0, WIFI_BAD_CREDS:1, WIFI_NO_AP:2, WIFI_CONNECTED:3 };
|
||||
|
||||
const updatePacket = {
|
||||
wifiConnected: false,
|
||||
wifiOnline: false,
|
||||
wifiIP: [0, 0, 0, 0],
|
||||
currVersion: [0, 0, 0],
|
||||
newVersion: [0, 0, 0]
|
||||
};
|
||||
|
||||
// Function to automatically start the connection process when the page loads
|
||||
function autoStart() {
|
||||
document.getElementById('input-DeviceName').value = BLE_SERVER_NAME;
|
||||
}
|
||||
|
||||
// Call autoStart when the page loads
|
||||
window.addEventListener('DOMContentLoaded', autoStart);
|
||||
|
||||
function compareVersions(a, b){
|
||||
for(let i=0;i<3;i++){ if(a[i] > b[i]) return 1; if(a[i] < b[i]) return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Log messages to the textarea
|
||||
function logMessage(message) {
|
||||
const logArea = document.getElementById('logArea');
|
||||
logArea.value += message + '\n';
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to scan for BLE devices
|
||||
async function scanForDevices() {
|
||||
logMessage('Scanning for BLE devices...');
|
||||
try {
|
||||
const device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: true,
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
if (device) {
|
||||
logMessage(`Found device: ${device.name || "Unnamed"} (ID: ${device.id})`);
|
||||
} else {
|
||||
logMessage('No devices found.');
|
||||
}
|
||||
} catch (error) {
|
||||
logMessage(`Scan failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to connect to the BLE server
|
||||
async function connectToBle() {
|
||||
if(!navigator.bluetooth){
|
||||
logMessage('Web Bluetooth not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ name: document.getElementById('input-DeviceName').value }],
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
//logMessage(`Connecting to ${bleDevice.name}`);
|
||||
const server = await bleDevice.gatt.connect();
|
||||
//await server.setPreferredMtu(247); // Request larger MTU size
|
||||
|
||||
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
||||
|
||||
bleCharacteristic1 = await service.getCharacteristic(BLE_CHARACTERISTIC1_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
//await bleCharacteristic1.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
//bleCharacteristic1.addEventListener('characteristicvaluechanged', handleChar1Notifications);
|
||||
|
||||
|
||||
//logMessage('Getting characteristic...');
|
||||
bleCharacteristic2 = await service.getCharacteristic(BLE_CHARACTERISTIC2_UUID);
|
||||
|
||||
// Subscribe to notifications
|
||||
await bleCharacteristic2.startNotifications();
|
||||
|
||||
// Add event listener for incoming notifications
|
||||
bleCharacteristic2.addEventListener('characteristicvaluechanged', (event) => {
|
||||
const value = event.target.value;
|
||||
const decoder = new TextDecoder();
|
||||
const decodedValue = decoder.decode(value);
|
||||
logMessage('--> ' + decodedValue);
|
||||
});
|
||||
|
||||
bleConnected = true;
|
||||
// Auto-reconnect / state reset handler
|
||||
bleDevice.addEventListener('gattserverdisconnected', handleDisconnect);
|
||||
document.getElementById('bleConnectBtn').disabled = true;
|
||||
document.querySelector('.status-indicator-ble').style.backgroundColor = 'green';
|
||||
document.getElementById('status-ble-connection').textContent ="BLE Status: Connected";
|
||||
document.getElementById('wifiConnectBtn').disabled = false;
|
||||
document.getElementById('checkStatusBtn').disabled = false;
|
||||
logMessage(`Connected to ${bleDevice.name}`);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes("cancelled")) {
|
||||
logMessage("Connection cancelled by user.");
|
||||
} else {
|
||||
logMessage(`Connection failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function wifiConnect() {
|
||||
const ssid = document.getElementById('wifissid').value;
|
||||
const password = document.getElementById('wifipassword').value;
|
||||
if (ssid && password) {
|
||||
// Send credentials to the device (retain original spacing format expected by firmware)
|
||||
const jsonString = ' {"ssid":"' + ssid.trim() + '","pass":"' + password + '"} ';
|
||||
await sendPacket('wifi-connect' + jsonString);
|
||||
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
alert('Please enter both SSID and password.');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
if (bleCharacteristic1) {
|
||||
await readPacket();
|
||||
processUpdatePacket(updatePacket);
|
||||
} else {
|
||||
logMessage('BLE device not connected.');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVersion() {
|
||||
await sendPacket('version-check');
|
||||
// loop and monitor the the updatePacket.newVersion
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
let success = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await readPacket();
|
||||
if (updatePacket.newVersion[0] > 0) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
if(success) {
|
||||
processUpdatePacket(updatePacket);
|
||||
logMessage("New Version: Available");
|
||||
} else {
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
}
|
||||
|
||||
async function startUpgrade() {
|
||||
try {
|
||||
await sendPacket('upgrade-start');
|
||||
logMessage("Upgrade Starting... Please wait.");
|
||||
} catch (error) {
|
||||
logMessage(`Error starting upgrade: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDisconnect(){
|
||||
bleConnected = false;
|
||||
document.querySelector('.status-indicator-ble').style.backgroundColor = 'gray';
|
||||
document.getElementById('status-ble-connection').textContent = 'BLE Status: Disconnected';
|
||||
document.getElementById('bleConnectBtn').disabled = false;
|
||||
document.getElementById('checkStatusBtn').disabled = true;
|
||||
document.getElementById('wifiConnectBtn').disabled = true;
|
||||
logMessage('BLE disconnected');
|
||||
}
|
||||
|
||||
async function sendPacket(packetMsg) {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot send packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
//logMessage(`Sending request: ${packetMsg} (Attempt ${attempt + 1})`);
|
||||
const encoder = new TextEncoder();
|
||||
await bleCharacteristic1.writeValueWithResponse(encoder.encode(packetMsg));
|
||||
//console.log("Request sent successfully");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to send packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to send request.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readPacket() {
|
||||
if (!bleCharacteristic1) {
|
||||
console.log("Cannot read packet: Not connected to BLE server.");
|
||||
return;
|
||||
}
|
||||
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 1000; // 1 second
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const value = await bleCharacteristic1.readValue();
|
||||
const data = new Uint8Array(value.buffer);
|
||||
if (data.length === 12) {
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.wifiOnline = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
//processUpdatePacket(updatePacket);
|
||||
return;
|
||||
}
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error(`Failed to read packet: ${error.message}`);
|
||||
attempt++;
|
||||
if (attempt < maxRetries) {
|
||||
console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
} else {
|
||||
console.error("Max retries reached. Failed to read packet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process update packet
|
||||
function processUpdatePacket(packet) {
|
||||
// Process the packet data
|
||||
//console.log("Processing update packet:", packet);
|
||||
if(packet.wifiConnected === true) {
|
||||
if(packet.wifiConnected && packet.wifiIP[0] > 0) {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected (' + packet.wifiIP.join('.') + ')';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: Connected';
|
||||
}
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'green';
|
||||
} else {
|
||||
document.getElementById('status-wifi-client').textContent = 'Wifi Client: ...';
|
||||
document.querySelector('.status-indicator-wifi').style.backgroundColor = 'gray';
|
||||
}
|
||||
|
||||
if(packet.wifiOnline === true) {
|
||||
document.getElementById('status-internet').textContent = 'Online';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'green';
|
||||
document.getElementById('checkVersionBtn').disabled = false;
|
||||
|
||||
} else {
|
||||
document.getElementById('status-internet').textContent = 'Offline';
|
||||
document.querySelector('.status-indicator-internet').style.backgroundColor = 'gray';
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
}
|
||||
|
||||
if (packet.currVersion[0] > 0) {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ' + packet.currVersion.join('.');
|
||||
} else {
|
||||
document.getElementById('status-current-version').textContent = 'Curr Version: ...';
|
||||
}
|
||||
|
||||
if (packet.newVersion[0] > 0) {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ' + packet.newVersion.join('.');
|
||||
document.getElementById('checkVersionBtn').disabled = true;
|
||||
|
||||
|
||||
logMessage("Latest Update is: " + packet.newVersion.join('.'));
|
||||
if(packet.wifiOnline && compareVersions(packet.newVersion, packet.currVersion) > 0) {
|
||||
//enable start upgrade button
|
||||
logMessage("New Version: Available");
|
||||
document.getElementById('startUpgradeBtn').disabled = false;
|
||||
} else {
|
||||
//disable start upgrade button
|
||||
document.getElementById('startUpgradeBtn').disabled = true;
|
||||
logMessage("New Version: Not Available");
|
||||
}
|
||||
} else {
|
||||
document.getElementById('status-new-version').textContent = 'New Version: ...';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//BLE_Characteristic.addEventListener('characteristicvaluechanged', handleNotifications);
|
||||
function handleChar1Notifications(event) {
|
||||
const data = new Uint8Array(event.data);
|
||||
|
||||
if (data.length !== 12) { // 1 byte for id, 4 bytes for booleans, 4 bytes for wifiIP, 3 bytes for currVersion, 3 bytes for newVersion
|
||||
console.log("Invalid packet length");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing updatePacket object instead of creating new one
|
||||
updatePacket.wifiConnected = data[0] !== 0;
|
||||
updatePacket.wifiOnline = data[1] !== 0;
|
||||
updatePacket.wifiIP = [data[2], data[3], data[4], data[5]];
|
||||
updatePacket.currVersion = [data[6], data[7], data[8]];
|
||||
updatePacket.newVersion = [data[9], data[10], data[11]];
|
||||
|
||||
processUpdatePacket(updatePacket);
|
||||
}
|
||||
|
||||
// Event listeners for buttons
|
||||
function togglePasswordVisibility() {
|
||||
const passwordInput = document.getElementById('wifipassword');
|
||||
passwordInput.type = this.checked ? 'text' : 'password';
|
||||
}
|
||||
|
||||
// Initial call to process the update packet with defaults
|
||||
processUpdatePacket(updatePacket);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,16 +0,0 @@
|
||||
{{NAVBAR}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>BLE Config</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="global-style.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Control - Bluetooth Configuration</h1>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Color Picker</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="pageTitle" ></div>
|
||||
<div class="secTitle"> <h1 class="secTitleFont">Countdown Animation</h1> </div>
|
||||
<div class="secCntrls" height: 200px>
|
||||
<label for="dropList">Animation: </label>
|
||||
<select class="dropList">
|
||||
<option>Custom 1</option>
|
||||
<option>Custom 2</option>
|
||||
<option>Custom 3</option>
|
||||
<option>Custom 4</option>
|
||||
<option>Rainbow</option>
|
||||
<option>Chasing</option>
|
||||
<option>Meteors</option>
|
||||
<option>Fire (red)</option>
|
||||
</select>
|
||||
<label for="color-picker">Color: </label>
|
||||
<input type="color" id="color-picker">
|
||||
</div>
|
||||
<div class="secTitle"> <h1 class="secTitleFont"> Selection Screen</h1> </div>
|
||||
<div class="secCntrls">
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,71 +0,0 @@
|
||||
#include "command_processor.h"
|
||||
|
||||
int boothState[16];
|
||||
|
||||
void process_command(int* data, int paramCount)
|
||||
{
|
||||
bool reply = false;
|
||||
|
||||
switch(*data){
|
||||
case COMM_NULL:
|
||||
break;
|
||||
case COMM_REBOOT:
|
||||
break;
|
||||
case COMM_DIN:
|
||||
break;
|
||||
case COMM_DOUT:
|
||||
break;
|
||||
case COMM_RELAY:
|
||||
break;
|
||||
case COMM_AOUT:
|
||||
break;
|
||||
case COMM_AIN:
|
||||
break;
|
||||
case COMM_ECHO:
|
||||
break;
|
||||
case COMM_LED_STATUS:
|
||||
break;
|
||||
case COMM_GET_TEMP:
|
||||
break;
|
||||
case COMM_PLAY:
|
||||
break;
|
||||
case COMM_RF:
|
||||
break;
|
||||
case COMM_NEST:
|
||||
break;
|
||||
case COMM_BOOTH_STATE:
|
||||
break;
|
||||
case COMM_STRIP1:
|
||||
|
||||
break;
|
||||
case COMM_STRIP2:
|
||||
break;
|
||||
case COMM_OLED:
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
|
||||
|
||||
if(reply){
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
int AppStates[16];
|
||||
|
||||
int getEvent(int index, int& ev){
|
||||
return ev[index];
|
||||
|
||||
}
|
||||
enum BoothStates = { }
|
||||
void RunAnimation(int stateIndex){
|
||||
switch (AppStates[stateIndex]){
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1,31 +0,0 @@
|
||||
#ifndef _COMMAND_PROCESSOR_H
|
||||
#define _COMMAND_PROCESSOR_H
|
||||
|
||||
extern int boothState[16];
|
||||
|
||||
void process_command(int* data, int paramCount);
|
||||
|
||||
enum COMM_FUNC{
|
||||
COMM_NULL = 0,
|
||||
COMM_REBOOT = 1,
|
||||
COMM_DIN = 2,
|
||||
COMM_DOUT = 3,
|
||||
COMM_RELAY = 4,
|
||||
COMM_AOUT = 5,
|
||||
COMM_AIN = 6,
|
||||
COMM_ECHO = 7,
|
||||
COMM_LED_STATUS = 8,
|
||||
COMM_GET_TEMP = 9,
|
||||
COMM_PLAY = 10,
|
||||
COMM_RF = 11,
|
||||
|
||||
COMM_NEST = 30,
|
||||
COMM_BOOTH_STATE = 50,
|
||||
COMM_STRIP1 = 75,
|
||||
COMM_STRIP2 = 100,
|
||||
COMM_OLED = 125
|
||||
};
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,145 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Event Container</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 40vh;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.outer-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 300px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 20px 5px 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
|
||||
width:90%;
|
||||
/*margin:0 auto;*/
|
||||
}
|
||||
.select-container {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.row > * {
|
||||
flex: 0 0 calc(50% - 20px); /* Adjusted width for better fit */
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
select, input[type="range"], input[type="color"] {
|
||||
width:90%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
select{
|
||||
width: 90%;
|
||||
}
|
||||
input[type="color"] {
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
}
|
||||
.row:last-child {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
.try-button-container {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.center-text {
|
||||
text-align: center;
|
||||
font-size: larger;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="outer-container">
|
||||
<div class="container">
|
||||
<legend class="center-text">Event0</legend><br>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="animation-list" id="list-label">Event:</label>
|
||||
<select id="animation-list">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="color-picker">Color:</label>
|
||||
<input type="color" id="color-picker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="speed" id="speed-label">Speed:</label>
|
||||
<input type="range" id="speed" min="1" max="10">
|
||||
</div>
|
||||
<div>
|
||||
<label for="range1">Col. Range:</label>
|
||||
<input type="range" id="range1" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="param1" id="param1-label">Param1:</label>
|
||||
<input type="range" id="param1" min="0" max="50">
|
||||
</div>
|
||||
<div>
|
||||
<label for="param2" id="param2-label">Param2:</label>
|
||||
<input type="range" id="param2" min="0" max="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="check1">Mix</input>
|
||||
<input type="checkbox" id="check1">Fwd</input>
|
||||
</div>
|
||||
<div class="try-button-container">
|
||||
<button id="try-button">Try</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,215 +0,0 @@
|
||||
#ifndef _FIRMWARE_HTML_H
|
||||
#define _FIRMWARE_HTML_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char firmware_html_page[] PROGMEM = R"rawliteral(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firmware Update</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
h1{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: larger;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
min-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.outer-container {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px 20px 4px 20px
|
||||
}
|
||||
.container {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 20px 10px 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
|
||||
width:90%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.center-text {
|
||||
text-align: center;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
width: calc(50% - 10px);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.row > *:last-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
progress{
|
||||
flex: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
#lblprogress{
|
||||
flex: 0;
|
||||
}
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
button{
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 name="h1element">Firmware Update</h1>
|
||||
<div class="outer-container">
|
||||
<div class="container">
|
||||
<legend class="center-text">Local Update</legend>
|
||||
<div class="row">
|
||||
<div>
|
||||
<input type="file" id="update-file" name="update-file">
|
||||
</div>
|
||||
<div>
|
||||
<button id="submit-update-local" onclick="uploadFile()">Update!</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-container">
|
||||
<label id="lblprogress" for="firm-progress">Progress:</label>
|
||||
<progress id="firm-progress" value="0" max="100"></progress>
|
||||
<label id="lbl-firm-progress" for="firm-progress">- - - -</label>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<label>File name: "ata_fw_booth_xxx.bin" or "ata_fs_booth_xxx.bin"</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('update-file');
|
||||
const progressBar = document.getElementById('firm-progress');
|
||||
const progressLabel = document.getElementById('lbl-firm-progress');
|
||||
const submitButton = document.getElementById('submit-update-local');
|
||||
|
||||
//submitButton.addEventListener('click', uploadFile);
|
||||
|
||||
function uploadFile() {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
|
||||
const file = fileInput.files[0];
|
||||
const url = '/update'; // Replace with the actual upload URL
|
||||
|
||||
// File Checks
|
||||
//*********************************************************
|
||||
// Check file extension
|
||||
if (!file.name.toLowerCase().endsWith('.bin')) {
|
||||
alert('Please select a file with the ".bin" extension.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check filename prefix
|
||||
if (!file.name.toLowerCase().startsWith('fwata') && !file.name.toLowerCase().startsWith('lfsata')) {
|
||||
alert('Please select a file with a filename starting with "fwata" or "lfsata".');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
const maxSizeBytes = 2.7 * 1024 * 1024; // 2.75Mb in bytes
|
||||
if (file.size > maxSizeBytes) {
|
||||
alert('Please select a file with a size not exceeding 2.7Mb.');
|
||||
return;
|
||||
}
|
||||
//*********************************************************
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file-size', file.size); // Include the file size as a parameter
|
||||
formData.append('update-file', file);
|
||||
let s = "file-size: " + file.size;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progressPercent = Math.round((event.loaded * 100) / event.total);
|
||||
progressBar.value = progressPercent;
|
||||
progressLabel.innerHTML =progressBar.value + "%";
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
console.log("received status: %d", xhr.status);
|
||||
if (xhr.status === 200) {
|
||||
// Upload completed successfully
|
||||
alert('Upload completed!');
|
||||
progressLabel.innerHTML = "Completed!";
|
||||
} else if (xhr.status === 500) {
|
||||
// Request was aborted (server-side)
|
||||
alert('Upload aborted by the server.');
|
||||
progressLabel.innerHTML = "Aborted!";
|
||||
} else {
|
||||
// Handle other error cases
|
||||
alert('An error occurred during the upload.');
|
||||
progressLabel.innerHTML = "Error!";
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
progressBar.value = 0;
|
||||
progressLabel.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('POST', url, true);
|
||||
xhr.send(formData);
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)rawliteral";
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,30 +0,0 @@
|
||||
#ifndef CERT_H
|
||||
|
||||
#define CERT_H
|
||||
|
||||
const char * github_rootCACertificate = \
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n"
|
||||
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
|
||||
"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n"
|
||||
"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n"
|
||||
"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n"
|
||||
"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n"
|
||||
"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n"
|
||||
"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n"
|
||||
"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n"
|
||||
"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n"
|
||||
"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n"
|
||||
"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n"
|
||||
"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n"
|
||||
"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n"
|
||||
"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n"
|
||||
"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n"
|
||||
"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n"
|
||||
"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n"
|
||||
"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n"
|
||||
"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n"
|
||||
"+OkuE6N36B9K\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
#endif
|
||||
@ -1,6 +0,0 @@
|
||||
.container{
|
||||
color:blue;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 50% 1fr 1fr;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="gridstyles.css">
|
||||
<title> CSS Grid</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="flow"> Test text 1</div>
|
||||
<div class="flow"> Test text 1</div>
|
||||
<div class="flow"> Test text 1</div>
|
||||
<div class="flow"> Test text 1</div>
|
||||
</div>
|
||||
</body>
|
||||
1
temporary/Temp/hue-select-min.js
vendored
1
temporary/Temp/hue-select-min.js
vendored
@ -1 +0,0 @@
|
||||
class HueSelect extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.shadowRoot.innerHTML='\n\t\t<style>\n\t\t .color-option {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tcursor: pointer;\n\t\t \t}\n\t\t \t.color-patch {\n\t\t\t\twidth: 30px;\n\t\t\t\theight: 30px;\n\t\t\t\tmargin-top: 18px;\n\t\t\t\tmargin-right: 10px;\n\t\t\t\tborder: 1px solid #000;\n\t\t \t}\n\t\t \t.hue-label {\n\t\t\t\tmargin-top: 18px;\n\t\t\t\twidth: 90px;\n\t\t\t\ttext-align: left;\n\t \t\t}\n\t\t\t.label-container {\n display: flex;\n align-items: center;\n }\n\t\t\t.dropdown {\n\t\t\t\tposition: relative;\n\t\t\t\tdisplay: inline-block;\n\t\t\t}\n\t\t \t.dropdown-content {\n\t\t\t\tdisplay: none;\n\t\t\t\tposition: absolute;\n\t\t\t\tbackground-color: #f9f9f9;\n\t\t\t\tmin-width: 180px;\n\t\t\t\tbox-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);\n\t\t\t\tz-index: 1;\n\t\t\t\tmax-height: 300px;\n\t\t\t\toverflow-y: auto;\n\t\t \t}\n\t\t\t.dropdown-content .color-option {\n\t\t\t\tpadding: 8px 12px;\n\t\t\t}\n\t\t \t.dropdown:hover .dropdown-content {\n\t\t\t\tdisplay: block;\n\t\t \t}\n\t\t</style>\n\t\t<div class="dropdown">\n\t\t \t<div class="color-option" id="selectedColor">\n\t\t\t\t<span class="color-patch" style="background-color: hsl(0, 100%, 50%)"></span>\n\t\t\t\t<div class="label-container">\n\t\t\t\t\t<label class="hue-label" id="hue-label">0</label>\n\t\t\t\t</div>\n\t\t\t</div> \n\t\t \t<div class="dropdown-content" id="colorPicker">\n\t\t\t\t\x3c!-- The options will be added dynamically using JavaScript --\x3e\n\t\t \t</div>\n\t\t</div>\n\t ',this.currentHue=0,this.colorList,this.hueLabel=this.shadowRoot.getElementById("hue-label");this.shadowRoot.getElementById("selectedColor").querySelector(".color-patch").addEventListener("mouseenter",(()=>{this.shadowRoot.querySelector(".dropdown-content").style.display="block"})),window.addEventListener("click",(t=>{const e=this.shadowRoot.querySelector(".dropdown-content");t.target.closest(".dropdown")||(e.style.display="none")})),this.createColorOptions(),this.setHue(0)}generateColors(){const t=[];for(let e=0;e<=360;e+=10)360==e&&(e=359),t.push(e);return t.push(-1),t.push(-2),t}createColorOption(t){const e=document.createElement("div");e.classList.add("color-option");const o=document.createElement("span");o.classList.add("color-patch");const n=document.createElement("span");n.classList.add("color-text"),n.textContent=t;const s=document.createElement("span");if(s.classList.add("rgb-hex"),-2===t)o.style.backgroundColor="rgb(0,0,0)",s.innerHTML=" #000000";else if(-1===t)o.style.backgroundColor="rgb(255,255,255)",s.innerHTML=" #FFFFFF";else{o.style.backgroundColor=`hsl(${t}, 100%, 50%)`;const e=this.hslToRgb(t,100,50).toUpperCase();s.innerHTML=`<span> ${e}</span>`}return e.appendChild(o),e.appendChild(n),e.appendChild(s),e.addEventListener("click",(()=>this.handleColorSelection(t))),e}createColorOptions(){const t=this.shadowRoot.querySelector(".dropdown-content");this.colorList=this.generateColors(),this.colorList.forEach((e=>{const o=this.createColorOption(e);t.appendChild(o)}))}hslToRgb(t,e,o){let n,s,l;if(t/=360,o/=100,0===(e/=100))n=s=l=o;else{const r=(t,e,o)=>(o<0&&(o+=1),o>1&&(o-=1),o<1/6?t+6*(e-t)*o:o<.5?e:o<2/3?t+(e-t)*(2/3-o)*6:t),i=o<.5?o*(1+e):o+e-o*e,d=2*o-i;n=r(d,i,t+1/3),s=r(d,i,t),l=r(d,i,t-1/3)}const r=t=>{const e=Math.round(255*t).toString(16);return 1===e.length?"0"+e:e};return`#${r(n)}${r(s)}${r(l)}`}handleColorSelection(t){this.currentHue=t;const e=this.shadowRoot.getElementById("selectedColor").querySelector(".color-patch"),o=this.getRGBfromHue(t);e.style.backgroundColor=o,console.log(e.style.backgroundColor),this.setHueLabel(t),this.hideDropdown(),this.dispatchEvent(new CustomEvent("change",{detail:{hue:t,rgb:o}}))}getSelectedHue(){return this.currentHue}getSelectedRGB(){const t=this.shadowRoot.getElementById("selectedColor").textContent.trim();return parseFloat(t)}getRGBfromHue(t){return-1==t?"#FFFFFF":-2==t?"#000000":this.hslToRgb(t,100,50)}getSelectedRgb(){const t=this.getSelectedHue();return getRGBfromHue(t)}setHue(t){let e=t;-1===t||-2===t?e=t:(e=10*Math.round(t/10),e>=360&&(e=359),e<0&&(e=0)),this.currentHue=e,this.colorList.forEach((t=>{t===e&&this.handleColorSelection(e)})),this.hideDropdown()}hideDropdown(){this.shadowRoot.querySelector(".dropdown-content").style.display="none"}setHueLabel(t){this.hueLabel.textContent="Hue: "+t}}customElements.define("hue-select",HueSelect);
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,205 +0,0 @@
|
||||
#ifndef _LED_ANIMATION_H
|
||||
#define _LED_ANIMATION_H
|
||||
|
||||
#include "common/LEDStrip.h"
|
||||
#include "led_strip.h"
|
||||
#include "my_board.h"
|
||||
#include <functional>
|
||||
#include "common/HSVTable.h"
|
||||
|
||||
#define HUE_MAX 767
|
||||
|
||||
enum EXIT_TYPE { EXIT_NORMAL, EXIT_FROM_NOTIFY, EXIT_FINISHED, EXIT_TIMEOUT };
|
||||
|
||||
|
||||
#define HUE_CALC_METHOD 0
|
||||
// TODO Fix hue range... 0-359 or 0-360;
|
||||
#if HUE_CALC_METHOD == 0
|
||||
#define HUEtoRGB(x) trueHSV(x)
|
||||
#elif HUE_CALC_METHOD == 1
|
||||
#define HUEtoRGB(x) sineHSV(x)
|
||||
#elif HUE_CALC_METHOD == 2
|
||||
#define HUEtoRGB(x) pwrHSV(x)
|
||||
#endif
|
||||
|
||||
#define RGBtoHUE(x) RGBToHSV(x)
|
||||
|
||||
#define INFINITE_LOOP 0
|
||||
|
||||
|
||||
enum DIR_TYPE { DT_REV, DT_FWD, DT_BOTH };
|
||||
|
||||
typedef struct {
|
||||
int RampUpTime;
|
||||
int msRampDownTime;
|
||||
int msHoldTime;
|
||||
int msFreq;
|
||||
float minDuty;
|
||||
float maxDuty;
|
||||
int interval;
|
||||
uint8_t pwr;
|
||||
}params_whiteFill;
|
||||
|
||||
typedef struct {
|
||||
int hue;
|
||||
int cooling;
|
||||
int sparking;
|
||||
int interval;
|
||||
uint8_t pwr;
|
||||
}params_fire_hue;
|
||||
|
||||
typedef struct {
|
||||
rgbpixel_t col1;
|
||||
rgbpixel_t col2;
|
||||
uint8_t range; /* hue range 0-127*/
|
||||
int density;
|
||||
uint8_t step; /* range step inteval*/
|
||||
int interval;
|
||||
uint8_t pwr;
|
||||
}params_rain;
|
||||
|
||||
#define ANIMTESTMODE_TIMEOUT (20 * 1000) // 20 secs
|
||||
|
||||
typedef struct {
|
||||
int EventTestCountdown = 0;
|
||||
int EventsIndex; // upto 8 events based on anim-events.json
|
||||
int EventsCount; // upto 8 events based on anim-events.json
|
||||
int PopupAnimIndex;
|
||||
bool busy;
|
||||
bool repeat;
|
||||
int countStatus;
|
||||
}ANIM_STATUS;
|
||||
|
||||
typedef struct {
|
||||
bool busy;
|
||||
int dslrCountStatus;
|
||||
}WHITE_FILL_STATUS;
|
||||
|
||||
extern ANIM_STATUS animStatus;
|
||||
extern WHITE_FILL_STATUS whiteStatus;
|
||||
|
||||
float calculateSteps(int msFreq, float LEDCount, int msFillTime);
|
||||
void GetOptimizedStepInterval(float &steps, int &interval, int LEDCount, int msFillTime, int startInterval, float tolerance);
|
||||
|
||||
float GetNextPalletHue(void);
|
||||
void Init_Hue_Range_Pallet(float hue1, float hue2, int colSteps);
|
||||
|
||||
float GetHueRange(float hue1, float hue2);
|
||||
float GetHueRangeWeb(float hue1, float hue2);
|
||||
|
||||
int CalcEventInterval(float speed, int min, int max);
|
||||
|
||||
/******************************** ANIMATION LOOP HELPER *********************************
|
||||
* All Animations should use this lopp helper.
|
||||
* Logic for monitoring exit notifications
|
||||
* Timeout in mSec can be set to exit
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Loop(ANIM_STATUS &stateMon, int msFreq, TickType_t duration, std::function<int()> callback=NULL);
|
||||
|
||||
// min:0-100, max:0-100
|
||||
//int Const_White_Fill(WHITE_FILL_STATUS &status, int msRampUpTime, int msHoldTime, int msRampDownTime, int msFreq, float minDuty, float maxDuty);
|
||||
int Const_White_Fill(WHITE_FILL_STATUS &status, FRONT_LIGHT light , float delayFactor, int countDown, int msFreq);
|
||||
//int Const_White_Fill_HelioType(ANIM_STATUS &stateMon, float startPoint, int msRampUpTime, int msRampDownTime, int msHoldTime, int msFreq, float minDuty, float maxDuty);
|
||||
|
||||
EXIT_TYPE Animation_White_Fill_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
|
||||
EXIT_TYPE Animation_Linear_Brighten(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
EXIT_TYPE Animation_Tick_Fill(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
|
||||
|
||||
/******************************** COLORED SNAKES *********************************
|
||||
* cycles=0=infinite
|
||||
* (check1) "mix"=false, all appearing sectors will be the same color until the next iteration
|
||||
* "mix"=true, each meteor color will be different
|
||||
* (check2) "roate", add rotation to animation
|
||||
* (hue range) hue range (0-360)
|
||||
* (param1) sectors
|
||||
* (param2) colorCount, number of color divisions
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Snakes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0);
|
||||
void drawSnakeMix(LEDSTRIP& strip, int sectors, float hue1, float hue2, int colorCount);
|
||||
|
||||
|
||||
/******************************** COLORED COMETS *********************************
|
||||
*(check1) "mix"=false, all appearing comets will be the same color until the next iteration
|
||||
* (check2) "mix"=true, each comets color will be different
|
||||
* (param1) hue range (0-360)
|
||||
* (param2) colorCount, number of color divisions
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Comets(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0);
|
||||
|
||||
void fadeToBlackBy(rgbpixel_t& pixel, uint8_t fadeAmount);
|
||||
inline uint8_t nscale8(uint8_t i, uint8_t scale);
|
||||
|
||||
/******************************** COLORED SECTORS *********************************
|
||||
* (check1) "mix"=false, all appearing sectors will be the same color until the next iteration
|
||||
* (check2) "mix"=true, each meteor color will be different
|
||||
* (param1) hue range (0-360)
|
||||
* (param2) colorCount, number of color divisions
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Sectors(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0);
|
||||
|
||||
|
||||
/******************************** COLORED DASHES *********************************
|
||||
* (check1) "mix"=false, all appearing dashes will be the same color until the next iteration
|
||||
* (check2) "mix"=true, each dash color will be different
|
||||
* (param1) hue range (0-360)
|
||||
* (param2) colorCount, number of color divisions
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Dashes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType=DT_BOTH, int cycles=0);
|
||||
|
||||
|
||||
/******************************** HUE SPECTRUM *********************************
|
||||
* (check1) "mix"=false, all appearing dashes will be the same color until the next iteration
|
||||
* (check2) "mix"=true, each dash color will be different
|
||||
* (param1) hue range (0-360)
|
||||
* (param2) colorCount, number of color divisions
|
||||
**************************************************************************************/
|
||||
EXIT_TYPE Animation_Hue_Spectrum(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration);
|
||||
EXIT_TYPE Animation_Hue_Spectrum_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration);
|
||||
void Fill_Hue_Spectrum(LEDSTRIP& strip, float hue1, float hue2);
|
||||
void Fill_Hue_Spectrum_Mirrored(LEDSTRIP& strip, float hue1, float hue2);
|
||||
|
||||
|
||||
/************************ RAINBOW *********************/
|
||||
EXIT_TYPE Animation_Rainbow(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration);
|
||||
|
||||
/************************ PULSE COLOR *********************/
|
||||
EXIT_TYPE Animation_Pulse_Color_Cycling(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
|
||||
|
||||
/**************** FIRE ****************/
|
||||
enum FIRE_COLOR { RED_FIRE, GREEN_FIRE, BLUE_FIRE};
|
||||
//void Animation_Fire(LEDSTRIP& strip, ANIMATION_EVENT& event, FIRE_COLOR fire, TickType_t duration);
|
||||
EXIT_TYPE Animation_Fire(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
void LEDStrip_FireInit(LEDSTRIP& strip);
|
||||
void Fire_Update( LEDSTRIP& strip, FIRE_COLOR fire, int Cooling, int Sparking);
|
||||
rgbpixel_t GetPixelHeatColor ( FIRE_COLOR fire, uint8_t temperature);
|
||||
|
||||
|
||||
|
||||
|
||||
/************************ *********************/
|
||||
void Animation_Splash(LEDSTRIP& strip);
|
||||
void Animation_SparkleColor(LEDSTRIP& strip, int msFreq);
|
||||
void Animation_SparkleHue(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
|
||||
EXIT_TYPE Animation_Serial_Test2(LEDSTRIP& strip, int msFreq, const char* s1, const char* s2);
|
||||
// msTransTime(Duration), msFreq(update interval)
|
||||
void Animation_TransTo_Color(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msTransTime, int msFreq, rgbpixel_t col);
|
||||
|
||||
|
||||
void drawSectorsHueSingle(LEDSTRIP& strip, rgbpixel_t col, float hue1, float hue2, int sectors) ;
|
||||
void drawSectorsHueMix(LEDSTRIP& strip, float hue1, float hue2, int sectors, int colorCount);
|
||||
|
||||
|
||||
|
||||
/************************ TEST ANIMATIONS *********************/
|
||||
EXIT_TYPE Animation_SetPixel(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
EXIT_TYPE Animation_Clear(LEDSTRIP& strip, ANIMATION_EVENT& event);
|
||||
|
||||
|
||||
rgbpixel_t GetColorFromHue(int hue);
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,564 +0,0 @@
|
||||
#include "led_strip.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include "common/led_animation.h"
|
||||
#include "common/LEDStrip.h"
|
||||
#include "global.h"
|
||||
#include "common/neo_colors.h"
|
||||
#include "common/HSVTable.h"
|
||||
#include "JsonConstrain.h"
|
||||
#include "my_board.h"
|
||||
#include "luma_master.h"
|
||||
#include "common/luma-stiks.h"
|
||||
|
||||
#define STRIP1_PIN RGBLED1_Pin
|
||||
#define STRIP2_PIN RGBLED2_Pin
|
||||
|
||||
#define POWERUP_TIME 3000
|
||||
|
||||
static const char* tag = "led_strip";
|
||||
|
||||
ANIMATION_PROPS animProps;
|
||||
|
||||
LEDSTRIP *strip1;
|
||||
LEDSTRIP *strip2;
|
||||
TaskHandle_t Strip1_Task_Handle;
|
||||
TaskHandle_t Strip2_Task_Handle;
|
||||
TaskHandle_t FrontLight_Task_Handle;
|
||||
|
||||
QueueHandle_t eventQueue = xQueueCreate( 4, sizeof( ANIMATION_EVENT ) );
|
||||
QueueHandle_t whiteQueue = xQueueCreate( 4, sizeof( ANIMATION_EVENT ) );
|
||||
|
||||
ANIMATION_EVENT lastNormalEventMsg;
|
||||
ANIMATION_EVENT eventMsg;
|
||||
|
||||
LUMA_PACKET TempLumaPacket;
|
||||
|
||||
int CurrentRunningEventIndex = -1;
|
||||
|
||||
|
||||
void Init_LED_Devices(BOARD_PINS boardPins)
|
||||
{
|
||||
ESP_LOGD(tag, "Strips Initialization entered...");
|
||||
File file = LittleFS.open("/cfg/led-devices.json");
|
||||
|
||||
if(!file){
|
||||
ESP_LOGE(tag,"Error opening led-devices.json!");
|
||||
file.close();
|
||||
}else{
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
if(error){ ESP_LOGE(tag, "led-devices.json deserialize error!.."); return;}
|
||||
|
||||
// ********** STRIP1 ********************************
|
||||
JsonObject stripJson = doc["strip1"];
|
||||
if(jsonConstrainBool(tag, stripJson, "en", false)){
|
||||
int port = jsonConstrain<int>(tag, stripJson, "i2s-ch", 0, 1, 0);
|
||||
int size = jsonConstrain<int>(tag, stripJson, "size", 1, 200, 10);
|
||||
//int pin = jsonConstrainInt(stripJson, "pin", 0, 48, RGBLED1_Pin);
|
||||
LED_ORDER order= getRGBOrder( jsonConstrainString(tag, stripJson, "rgb-order", "grb").c_str() );
|
||||
int shift = jsonConstrain<int>(tag, stripJson, "shift", -size, size, 0);
|
||||
int offset = jsonConstrain<int>(tag, stripJson, "offset", -size, size, 0);
|
||||
int core = jsonConstrain<int>(tag, stripJson, "core", 0, 1, 0);
|
||||
int powerDiv = jsonConstrain<int>(tag, stripJson, "power-div", 0, 3, 0);
|
||||
|
||||
strip1 = new LEDSTRIP(port, size, boardPins.rgb1, order, shift, offset);
|
||||
strip1->setPowerDiv(powerDiv);
|
||||
ESP_LOGI(tag,"Strip1 initialized.");
|
||||
ESP_LOGD(tag," Size: %d, port: %d, shift: %d, offset: %d", size, port, shift, offset);
|
||||
ESP_LOGD(tag," Strip1 Task Creating...Core: %d", core);
|
||||
xTaskCreatePinnedToCore(Strip1_Control_Task, "LED_Strip1_Task", 1024*10, NULL, 1, &Strip1_Task_Handle, core);
|
||||
}
|
||||
|
||||
// ********** STRIP2 ********************************
|
||||
stripJson.clear();
|
||||
stripJson = doc["strip2"];
|
||||
if(jsonConstrainBool(tag, stripJson, "en", false)){
|
||||
int port = jsonConstrain<int>(tag, stripJson, "i2s-ch", 0, 1, 0);
|
||||
int size = jsonConstrain<int>(tag, stripJson, "size", 1, 200, 10);
|
||||
//int pin = jsonConstrainInt(stripJson, "pin", 0, 48, RGBLED2_Pin);
|
||||
LED_ORDER order= getRGBOrder( jsonConstrainString(tag, stripJson, "rgb-order", "grb").c_str() );
|
||||
int shift = jsonConstrain<int>(tag, stripJson, "shift", -size, size, 0);
|
||||
int offset = jsonConstrain<int>(tag, stripJson, "offset", -size, size, 0);
|
||||
int core2 = jsonConstrain<int>(tag, stripJson, "core", 0, 1, 0);
|
||||
int powerDiv = jsonConstrain<int>(tag, stripJson, "power-div", 0, 3, 0);
|
||||
|
||||
strip2 = new LEDSTRIP(port, size, boardPins.rgb2, order, shift, offset);
|
||||
strip2->setPowerDiv(powerDiv);
|
||||
ESP_LOGI(tag,"Strip2 initialized.");
|
||||
ESP_LOGD(tag," Size: %d, port: %d, shift: %d, offset: %d", size, port, shift, offset);
|
||||
ESP_LOGD(tag,"Strip2 Task Creating...Core: %d", core2);
|
||||
xTaskCreatePinnedToCore(Strip2_Control_Task, "LED_Strip2_Task", 1024*8, NULL, 1, &Strip2_Task_Handle, core2);
|
||||
}else{
|
||||
ESP_LOGE(tag,"Strip2 disabled");
|
||||
}
|
||||
|
||||
JsonObject flightJson = doc["front-light"];
|
||||
animProps.frontLight.enabled = jsonConstrainBool(tag, flightJson, "en", false);
|
||||
if(animProps.frontLight.enabled){
|
||||
animProps.frontLight.relayIndex = jsonConstrain<int>(tag, flightJson, "relay", 0, 3, 0);
|
||||
animProps.frontLight.core = jsonConstrain<int>(tag, flightJson, "core", 0, 1, 1);
|
||||
animProps.frontLight.style = (FRONT_LIGHT_STYLE)(flightJson, "style", 0, 1, 0);
|
||||
ESP_LOGI(tag, "Front Light initialized..");
|
||||
Init_FrontLight();
|
||||
}
|
||||
|
||||
JsonObject rlightJson = doc["rear-light"];
|
||||
animProps.rearLight.enabled = jsonConstrainBool(tag, rlightJson, "en", false);
|
||||
if(animProps.rearLight.enabled){
|
||||
animProps.rearLight.relayIndex = jsonConstrain<int>(tag, rlightJson, "relay", 0, 3, 1);
|
||||
animProps.rearLight.buttonIndex = jsonConstrain<int>(tag, rlightJson, "button", 0, 2, 2);
|
||||
animProps.rearLight.rampTime = jsonConstrain<int>(tag, rlightJson, "ramp", 1000, 20000, 3000);
|
||||
animProps.rearLight.rampSteps = jsonConstrain<int>(tag, rlightJson, "steps", 2, 20, 8);
|
||||
animProps.rearLight.min = jsonConstrain<float>(tag, rlightJson, "min", 0.0f, 50.0f, 0.0f);
|
||||
animProps.rearLight.max = jsonConstrain<float>(tag, rlightJson, "max", animProps.rearLight.min, 100.0f, 100.0f);
|
||||
ESP_LOGD(tag, "relay: %d, btn: %d, ramp: %d, steps: %d, min: %.2f, max: %.2f", animProps.rearLight.relayIndex, animProps.rearLight.buttonIndex, animProps.rearLight.rampTime, animProps.rearLight.rampSteps, animProps.rearLight.min, animProps.rearLight.max);
|
||||
|
||||
Initialize_Rear_Control(animProps.rearLight.relayIndex, animProps.rearLight.buttonIndex, animProps.rearLight.rampTime, animProps.rearLight.rampSteps , animProps.rearLight.min, animProps.rearLight.max);
|
||||
ESP_LOGI(tag, "Rear Light initialized..");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Init_FrontLight(void)
|
||||
{
|
||||
xTaskCreatePinnedToCore(FrontLight_Task, "FrontLight_Task", 5000, NULL, 1, &FrontLight_Task_Handle, animProps.frontLight.core);
|
||||
}
|
||||
|
||||
void Init_RearLight(void)
|
||||
{
|
||||
//if(jsonOb.isNull()){ Serial.println(" rear_light json is Null.."); return; }
|
||||
}
|
||||
|
||||
void Test_Animations(void){
|
||||
ANIMATION_EVENT event;
|
||||
|
||||
strip1->fill({0,0,0}, 0, strip1->effSize);
|
||||
strip1->show(true);
|
||||
vTaskDelay(100);
|
||||
|
||||
event.hue = RGBtoHUE(col_blue);
|
||||
event.hueRange = 180;
|
||||
event.param1 = 11;
|
||||
event.speed = 70;
|
||||
|
||||
LEDStrip_FireInit(*strip1);
|
||||
|
||||
while(1){
|
||||
event.hue = 0;
|
||||
event.hueRange = 359;
|
||||
event.param1 = 22;
|
||||
event.speed = 70;
|
||||
//Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2);
|
||||
event.param1 = 30;
|
||||
//Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2);
|
||||
event.param1 = 40;
|
||||
//Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2);
|
||||
event.param1 = 50;
|
||||
Animation_Sectors(*strip1, event, DT_BOTH, 2);
|
||||
|
||||
event.param1 = 50;
|
||||
Animation_Dashes(*strip1, event, DT_BOTH, 3);
|
||||
|
||||
event.param1 = 50;
|
||||
Animation_Dashes(*strip1, event, DT_BOTH, 3);
|
||||
/*
|
||||
Animation_Fire(*strip1, event, RED_FIRE, 5000);
|
||||
Animation_Fire(*strip1, event, GREEN_FIRE, 5000);
|
||||
Animation_Fire(*strip1, event, BLUE_FIRE, 5000);
|
||||
|
||||
event.col1 = col_blue;
|
||||
event.col2 = {255,136,0};
|
||||
event.density = 11;
|
||||
event.speed = 100;
|
||||
Animation_Snakes(*strip1, event, false, DT_FWD, 2, 1);
|
||||
|
||||
event.col1 = HUEtoRGB(359);
|
||||
event.col2 = HUEtoRGB(0);
|
||||
event.density = 22;
|
||||
event.speed = 70;
|
||||
Animation_Snakes(*strip1, event, true, DT_BOTH, 12, 4);
|
||||
|
||||
event.col1 = HUEtoRGB(359);
|
||||
event.col2 = HUEtoRGB(0);
|
||||
event.density = 56;
|
||||
event.speed = 50;
|
||||
Animation_Snakes(*strip1, event, true, DT_BOTH, 12, 4);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
void Strip1_Control_Task(void *parameters)
|
||||
{
|
||||
vTaskDelay(1000); // small start delay
|
||||
ESP_LOGD(tag, "Strip1 Task Entered....");
|
||||
//Test_Animations();
|
||||
|
||||
LEDStrip_FireInit(*strip1); // must be initialized before calling fire animation
|
||||
|
||||
// Set default event 1 animation base on profile values
|
||||
ANIMATION_EVENT ev; // empty event
|
||||
ev.animIndex = 0;
|
||||
ev.countDown = 6000;
|
||||
ev.param2 = 75;
|
||||
ev.type = EV_NON_INJECT;
|
||||
PostNewEvent(ev); // Start Animation (White Fill)
|
||||
|
||||
animProps.event[1].type = EV_NORMAL;
|
||||
PostNewEvent(animProps.event[1]); // default attraction animation
|
||||
|
||||
while(true){
|
||||
xQueueReceive( eventQueue, &eventMsg, portMAX_DELAY );
|
||||
ESP_LOGD(tag, "Queue waiting: %d", uxQueueMessagesWaiting(eventQueue));
|
||||
|
||||
if(eventMsg.type == EV_NORMAL ) { // EV_NORMAL
|
||||
ESP_LOGD(tag, "Running event: type: EV_NORMAL, event: %d, anim: %d", eventMsg.selfIndex, eventMsg.animIndex);
|
||||
lastNormalEventMsg = eventMsg; //save last Normal EventMsg
|
||||
if( eventMsg.selfIndex >= 0 ) {
|
||||
CurrentRunningEventIndex = eventMsg.selfIndex;
|
||||
}
|
||||
|
||||
if(Luma_PeerCount() > 0){
|
||||
TempLumaPacket.type = LPT_ANIM;
|
||||
memcpy(&TempLumaPacket.data, &eventMsg, sizeof(ANIMATION_EVENT));
|
||||
Luma_Master_Broadcast(TempLumaPacket);
|
||||
}
|
||||
|
||||
if( eventMsg.selfIndex == 0 ){
|
||||
RunWhitefillAnimation( eventMsg );
|
||||
}else{
|
||||
RunAnimation( eventMsg );
|
||||
}
|
||||
}
|
||||
else if( eventMsg.type == EV_INJECT ){ // insert and return to previous animation
|
||||
ESP_LOGD(tag, "EV_INJECT");
|
||||
xQueueSend( eventQueue, &lastNormalEventMsg, 100 ); // requeue previous normal event
|
||||
RunPopUpAnimations( eventMsg );
|
||||
}
|
||||
else if( eventMsg.type == EV_NON_INJECT ){
|
||||
ESP_LOGD(tag, "EV_NON_INJECT");
|
||||
RunPopUpAnimations( eventMsg );
|
||||
}
|
||||
else if( eventMsg.type == EV_TEST ){
|
||||
ESP_LOGD(tag, "EV_TEST");
|
||||
animStatus.EventTestCountdown = EVENT_TESTMODE_TIMEOUT; // start timeout countdown
|
||||
// Should not re-post last event from here in case repeated tests are started.
|
||||
// that would cause a backlog of the same previous event.
|
||||
if( eventMsg.selfIndex == 0 ){
|
||||
RunWhitefillAnimation( eventMsg);
|
||||
}else{
|
||||
RunAnimation( eventMsg );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PostLastNormalEvent(){
|
||||
xQueueSend( eventQueue, &lastNormalEventMsg, 100 ); // requeue previous normal event
|
||||
ESP_LOGD(tag, "RePosted Last Normal Event Index: %d", lastNormalEventMsg.selfIndex);
|
||||
}
|
||||
|
||||
// Used to cycle to the next event primarity when usinging buttons
|
||||
int IncrementEventIndex(){
|
||||
int x = (CurrentRunningEventIndex + 1) % animStatus.EventsCount;
|
||||
ESP_LOGD(tag, "Increment Event Index from %d to %d", CurrentRunningEventIndex, x);
|
||||
|
||||
animProps.event[x].type = EV_NORMAL;
|
||||
PostNewEvent(animProps.event[x]);
|
||||
return animStatus.EventsIndex;
|
||||
}
|
||||
|
||||
//EVENT_MSG newEvent;
|
||||
void PostNewEvent( ANIMATION_EVENT &animEvent) {
|
||||
//newEvent.type = type;
|
||||
//newEvent.event = animEvent;
|
||||
xQueueSend(eventQueue, &animEvent, 100); // queue new event
|
||||
|
||||
// if(type == EV_NORMAL){
|
||||
// If whitefill index ( could be restarted )
|
||||
// if(animEvent.animIndex == ANIM_WHITEFILL_INDEX){
|
||||
//if( countDown == 0 ) { animStatus.countDown = 1000; } // TODO Countdown calc maybe wrong...double check
|
||||
//if( animStatus.busy ) { animStatus.repeat = true; }
|
||||
// if( FrontLight_Task_Handle ) { xTaskNotifyGive( FrontLight_Task_Handle ); }
|
||||
// }
|
||||
// }
|
||||
|
||||
if( Strip1_Task_Handle ){ xTaskNotifyGive( Strip1_Task_Handle ); }
|
||||
}
|
||||
|
||||
void Strip2_Control_Task(void *parameters)
|
||||
{
|
||||
vTaskDelay(100);
|
||||
ESP_LOGD(tag, "Strip2 Task Entered....\n");
|
||||
|
||||
for(;;){
|
||||
vTaskDelay(10000);
|
||||
}
|
||||
}
|
||||
|
||||
void FrontLight_Task(void *parameters){
|
||||
animStatus.busy = false;
|
||||
ANIMATION_EVENT whiteMsg;
|
||||
for(;;){
|
||||
xQueueReceive( whiteQueue, &whiteMsg, portMAX_DELAY );
|
||||
|
||||
int msFillTime = 0;
|
||||
if(whiteMsg.countDown > 0){ // use countdown if available
|
||||
msFillTime = whiteMsg.countDown;
|
||||
}else{
|
||||
msFillTime = map(whiteMsg.param1, 0, 100, 0, 10000);
|
||||
if(msFillTime < 1000){ msFillTime = 1000; }
|
||||
}
|
||||
float delayFactor = (float)map(whiteMsg.param2, 0, 100, 0, 100) / 100.0;
|
||||
Const_White_Fill(whiteStatus, animProps.frontLight, delayFactor, msFillTime, 50);
|
||||
}
|
||||
}
|
||||
|
||||
void load_animation_profile(void){
|
||||
File file = LittleFS.open("/cfg/anim-profile-common.json");
|
||||
|
||||
if(!file){
|
||||
ESP_LOGE(tag, "Error opening anim-profile-common.json...");
|
||||
file.close();
|
||||
}else{
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
if(error){ ESP_LOGE(tag, "anim-profile-common.json deserialize error!.."); return;}
|
||||
|
||||
JsonObject frontJson = doc["countdown"];
|
||||
animProps.frontLight.minDuty = jsonConstrain<float>(tag, frontJson, "min", 0.0f, 99.0f, 20.0f);
|
||||
animProps.frontLight.maxDuty = jsonConstrain<float>(tag, frontJson, "max", 1.0f, 100.0f, 99.0f);
|
||||
animProps.frontLight.holdTime = jsonConstrain<int>(tag, frontJson, "hold", 100, 5000, 1000);
|
||||
animProps.frontLight.rampTime = jsonConstrain<int>(tag, frontJson, "ramp", 100, 5000, 500);
|
||||
|
||||
animProps.profileIndex = jsonConstrain<int>(tag, doc.as<JsonObject>(), "profile-index", 0, 7, 0);
|
||||
|
||||
ESP_LOGI(tag, "anim-profile-common.json settings loaded.");
|
||||
}
|
||||
}
|
||||
|
||||
void load_animation_profileByIndex(int index){
|
||||
|
||||
//for(int index = 1; index <= 8; index++){
|
||||
String strFileName = "/cfg/anim-profile" + String(index + 1) + ".json";
|
||||
File file = LittleFS.open(strFileName);
|
||||
|
||||
if(!file){
|
||||
ESP_LOGE(tag, "%s", "Error opening " + strFileName);
|
||||
file.close();
|
||||
}else{
|
||||
JsonDocument doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
if(error){ ESP_LOGE(tag, "%s deserialize error!..", strFileName.c_str()); return;}
|
||||
|
||||
JsonObject profJson = doc.as<JsonObject>();
|
||||
animProps.profileName = jsonConstrainString(tag, profJson, "name", "").c_str();
|
||||
ESP_LOGD(tag, "Profile Index: %d, Name: %s", index, animProps.profileName);
|
||||
|
||||
//
|
||||
JsonArray eventsJson = profJson["events"];
|
||||
for(int i = 0; i < animStatus.EventsCount; i++){
|
||||
animProps.event[i].selfIndex = i; // Self Index
|
||||
|
||||
if(i == 0){
|
||||
animProps.event[i].animIndex = jsonConstrain<int>(tag, eventsJson[i], "anim", 0, animProps.whitefillsCount-1, 0);
|
||||
}else{
|
||||
animProps.event[i].animIndex = jsonConstrain<int>(tag, eventsJson[i], "anim", 0, animProps.animationsCount-1, 0);
|
||||
}
|
||||
|
||||
animProps.event[i].hue = jsonConstrain<int>(tag, eventsJson[i], "hue", -2, 360, 0);
|
||||
animProps.event[i].hueRange = jsonConstrain<int>(tag, eventsJson[i], "hue-range", 0, 360, 1);
|
||||
animProps.event[i].speed = jsonConstrain<int>(tag, eventsJson[i], "speed", 0, 100, 50);
|
||||
animProps.event[i].param1 = jsonConstrain<int>(tag, eventsJson[i], "param1", 0, 100, 50);
|
||||
animProps.event[i].param2 = jsonConstrain<int>(tag, eventsJson[i], "param2", 0, 100, 50);
|
||||
animProps.event[i].check1 = jsonConstrainBool(tag, eventsJson[i], "check1", false);
|
||||
animProps.event[i].check2 = jsonConstrainBool(tag, eventsJson[i], "check2", false);
|
||||
animProps.event[i].check3 = jsonConstrainBool(tag, eventsJson[i], "check3", false);
|
||||
animProps.event[i].check4 = jsonConstrainBool(tag, eventsJson[i], "check4", false);
|
||||
|
||||
ESP_LOGD(tag, " self: %d, anim: %d, param1: %d, speed: %d", animProps.event[i].selfIndex, animProps.event[i].animIndex, animProps.event[i].param1, animProps.event[i].speed);
|
||||
}
|
||||
ESP_LOGI(tag, "%s settings loaded.", strFileName.c_str());
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
void RunWhitefillAnimation(ANIMATION_EVENT &event){
|
||||
//Start Constant Light Task if available
|
||||
if( FrontLight_Task_Handle ){
|
||||
xQueueSend(whiteQueue, &event, 100); // queue new event
|
||||
xTaskNotifyGive( FrontLight_Task_Handle );
|
||||
}
|
||||
|
||||
int ret;
|
||||
switch(event.animIndex){
|
||||
case 0: // Bottom/Up Fill
|
||||
ESP_LOGD(tag, " Running WhiteFill: %d , bottom/up", event.animIndex);
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
case 1: // Snake Fill
|
||||
ESP_LOGD(tag, " Running WhiteFill: %d , Snake fill", event.animIndex);
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
case 2: // Tick Fill
|
||||
ESP_LOGD(tag, " Running WhiteFill: %d , Tick fill", event.animIndex);
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
case 3: // Smooth Brighten
|
||||
ESP_LOGD(tag, " Running WhiteFill: %d , Smooth Brighten", event.animIndex);
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
case 4: // Cycle All
|
||||
ESP_LOGD(tag, " Running WhiteFill: %d , Cycle All", event.animIndex);
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
default: // Random Select
|
||||
ESP_LOGD(tag, " Running WhiteFill: default , bottom/up");
|
||||
ret = Animation_White_Fill_Mirrored(*strip1, event);
|
||||
break;
|
||||
}
|
||||
|
||||
// if exited loop naturally... not from a notification so keep waiting
|
||||
if(ret == EXIT_FINISHED || ret == EXIT_TIMEOUT){
|
||||
ESP_LOGD(tag, "Whitefill normal exit... waiting");
|
||||
ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
|
||||
}
|
||||
}
|
||||
|
||||
void RunAnimation(ANIMATION_EVENT &event){
|
||||
if(event.animIndex < 80){
|
||||
switch(event.animIndex){ // AnimationEventIndex (0-X)
|
||||
case 0: // Rainbow
|
||||
ESP_LOGD(tag, " Running Anim: %d , Rainbow", event.animIndex);
|
||||
Animation_Rainbow(*strip1, event, INFINITE_LOOP);
|
||||
break;
|
||||
case 1: // Hue Spectrum Mirrored
|
||||
ESP_LOGD(tag, " Running Anim: %d , Hue Spectrum Mirrored", event.animIndex);
|
||||
Animation_Hue_Spectrum_Mirrored(*strip1, event, INFINITE_LOOP);
|
||||
break;
|
||||
case 2: // Color Pulse Cycle
|
||||
ESP_LOGD(tag, " Running Anim: %d , Color Pulse Cycle", event.animIndex);
|
||||
Animation_Pulse_Color_Cycling(*strip1, event);
|
||||
break;
|
||||
case 3: // Comets Mixed Colors
|
||||
ESP_LOGD(tag, " Running Anim: %d , Comets", event.animIndex);
|
||||
Animation_Comets(*strip1, event, DT_BOTH, INFINITE_LOOP);
|
||||
break;
|
||||
case 4: // Dashes Single and Mixed Colors
|
||||
ESP_LOGD(tag, " Running Anim: %d , Dashes", event.animIndex);
|
||||
Animation_Dashes(*strip1, event, DT_BOTH, INFINITE_LOOP);
|
||||
break;
|
||||
case 5: // Snakes Single and Mixed Colors
|
||||
ESP_LOGD(tag, " Running Anim: %d , Snakes", event.animIndex);
|
||||
Animation_Snakes(*strip1, event, DT_BOTH, INFINITE_LOOP);
|
||||
break;
|
||||
case 6: // Sectors Mixed Colors (No Sigle Colors because pointless)
|
||||
ESP_LOGD(tag, " Running Anim: %d , Sectors", event.animIndex);
|
||||
Animation_Sectors(*strip1, event, DT_BOTH, INFINITE_LOOP);
|
||||
break;
|
||||
case 7: // Fire (Red)
|
||||
ESP_LOGD(tag, " Running Anim: %d , Fire", event.animIndex);
|
||||
Animation_Fire(*strip1, event);
|
||||
break;
|
||||
case 8: //
|
||||
break;
|
||||
case 9: // Color Strobing
|
||||
break;
|
||||
case 10: // Random Color Twinkle
|
||||
break;
|
||||
case 11: // Stacking
|
||||
break;
|
||||
case 12: // Rain
|
||||
break;
|
||||
case 13: // Stacking
|
||||
break;
|
||||
case 14: // RED/WHITE/BLU Flag (density selects color pallet) (sector/comets,dashes, snakes)
|
||||
break;
|
||||
case 15:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}else{ // Non Looping Functions
|
||||
switch(event.animIndex){ // AnimationEventIndex (0-X)
|
||||
case 80: // clear strip
|
||||
strip1->fill(col_black, 0, strip1->effSize);
|
||||
strip1->show(true);
|
||||
ESP_LOGD(tag, "Animation Strip1 Clear");
|
||||
ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
|
||||
break;
|
||||
case 81: // set pixel
|
||||
strip1->setPixel(event.param1, GetColorFromHue(event.hue));
|
||||
strip1->show(true);
|
||||
ESP_LOGD(tag, "Animation Set Pixel: %d", event.param1);
|
||||
ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
|
||||
break;
|
||||
case 82: //
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// These animations should be quick and not infinite
|
||||
void RunPopUpAnimations(ANIMATION_EVENT &event){
|
||||
|
||||
switch(event.animIndex){ // AnimationEventIndex (0-X)
|
||||
case 0: // Start/Boot/Power Up Sequence
|
||||
ANIMATION_EVENT ev;
|
||||
ev.animIndex = 0;
|
||||
ev.hue = -1; //{200, 200, 200}; //col_white;
|
||||
ev.hueRange = 1; //col_black;
|
||||
ev.countDown = 6000; // 6 seconds
|
||||
ev.param2 = 75;
|
||||
if( FrontLight_Task_Handle ){
|
||||
xQueueSend(whiteQueue, &ev, 100); // queue new event
|
||||
xTaskNotifyGive( FrontLight_Task_Handle );
|
||||
}
|
||||
Animation_White_Fill_Mirrored(*strip1, ev);
|
||||
|
||||
break;
|
||||
case 1: // BLE Connected
|
||||
break;
|
||||
case 2: // BLE Disconnected
|
||||
break;
|
||||
case 3: // WiFi Connected
|
||||
break;
|
||||
case 4: // WiFi Disconnected
|
||||
break;
|
||||
case 5: // Luma Stick Connected
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// json color to rgbPixel conversion
|
||||
// Unused now
|
||||
rgbpixel_t hexToRGB(const char* hexColor) {
|
||||
long hexValue = strtol(hexColor + 1, nullptr, 16); // Skip the '#' character
|
||||
rgbpixel_t pix;
|
||||
pix.red = (hexValue >> 16) & 0xFF;
|
||||
pix.grn = (hexValue >> 8) & 0xFF;
|
||||
pix.blu = hexValue & 0xFF;
|
||||
return pix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
#ifndef _LED_STRIP_H
|
||||
#define _LED_STRIP_H
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "common/LEDStrip.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include "my_board.h"
|
||||
|
||||
extern LEDSTRIP *strip1;
|
||||
extern LEDSTRIP *strip2;
|
||||
extern TaskHandle_t Strip1_Task_Handle;
|
||||
extern TaskHandle_t Strip2_Task_Handle;
|
||||
extern TaskHandle_t FrontLight_Task_Handle;
|
||||
|
||||
#define ANIM_WHITEFILL_INDEX 0
|
||||
#define ANIM_DEFAULT_INDEX 1
|
||||
#define ANIM_EVENT_SIZE 12
|
||||
#define PROFILES_SIZE 8
|
||||
|
||||
#define MIN_LED_UPDATE_INTERVAL 13
|
||||
|
||||
enum OtherANIMS { OA_SET_PIXEL=100, OA_CLEAR, };
|
||||
|
||||
enum FRONT_LIGHT_STYLE { FRONT_LIKE_LUMIA, FRONT_LIKE_ROAMER };
|
||||
|
||||
enum EVENT_TYPE { EV_NORMAL, EV_INJECT, EV_NON_INJECT, EV_TEST,};
|
||||
|
||||
// Single web page event (upto 8)
|
||||
typedef struct{
|
||||
EVENT_TYPE type;
|
||||
int selfIndex;
|
||||
int animIndex;
|
||||
int hue;
|
||||
int hueRange;
|
||||
int speed;
|
||||
int param1;
|
||||
int param2;
|
||||
bool check1;
|
||||
bool check2;
|
||||
bool check3;
|
||||
bool check4;
|
||||
int countDown;
|
||||
}ANIMATION_EVENT;
|
||||
|
||||
typedef struct{
|
||||
bool enabled;
|
||||
uint8_t relayIndex;
|
||||
uint8_t core;
|
||||
int holdTime;
|
||||
int rampTime;
|
||||
float minDuty;
|
||||
float maxDuty;
|
||||
FRONT_LIGHT_STYLE style;
|
||||
//float delayFactor;
|
||||
}FRONT_LIGHT;
|
||||
|
||||
typedef struct{
|
||||
bool enabled;
|
||||
uint8_t relayIndex;
|
||||
uint8_t buttonIndex;
|
||||
uint8_t core;
|
||||
int rampTime;
|
||||
uint8_t rampSteps;
|
||||
float min;
|
||||
float max;
|
||||
}REAR_LIGHT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
EVENT_TYPE type;
|
||||
ANIMATION_EVENT event;
|
||||
}EVENT_MSG;
|
||||
|
||||
// Properties from webpage
|
||||
typedef struct {
|
||||
int profileIndex;
|
||||
const char* profileName;
|
||||
int whitefillsCount;
|
||||
int animationsCount;
|
||||
FRONT_LIGHT frontLight;
|
||||
REAR_LIGHT rearLight;
|
||||
ANIMATION_EVENT event[ANIM_EVENT_SIZE];
|
||||
}ANIMATION_PROPS;
|
||||
|
||||
extern ANIMATION_PROPS animProps;
|
||||
|
||||
enum POPUP_ANIM { POP_STATUP, POP_BLE_CONN, POP_BLE_DISC, POP_WIFI_CONN, POP_WIFI_DISC, POP_STICK_CONN, POP_STICK_DISC };
|
||||
|
||||
void RunWhitefillAnimation(ANIMATION_EVENT &event);
|
||||
|
||||
void RunAnimation(ANIMATION_EVENT &event);
|
||||
|
||||
void RunPopUpAnimations(ANIMATION_EVENT &event);
|
||||
|
||||
//void SetEventIndex(int index, int countdown=0, bool test=false);
|
||||
|
||||
//void PostNewEvent( int countDown, EVENT_TYPE type, ANIMATION_EVENT &animEvent);
|
||||
void PostNewEvent( ANIMATION_EVENT &animEvent);
|
||||
|
||||
int IncrementEventIndex(void);
|
||||
|
||||
void PostLastNormalEvent(void);
|
||||
|
||||
void load_animation_profile(void); // cfg/anim-profiles.json
|
||||
void load_animation_profileByIndex(int); // cfg/anim-profileX.json
|
||||
|
||||
void Init_LED_Devices(BOARD_PINS); // cfg/led-devices.json
|
||||
|
||||
rgbpixel_t hexToRGB(const char* hexColor);
|
||||
|
||||
//void Init_LEDStrip2(void);
|
||||
|
||||
void Init_FrontLight(void);
|
||||
|
||||
//void Init_RearLight(void);
|
||||
|
||||
void Strip1_Control_Task(void *parameters);
|
||||
|
||||
void Strip2_Control_Task(void *parameters);
|
||||
|
||||
void FrontLight_Task(void *parameters);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,755 +0,0 @@
|
||||
{{NAVBAR}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>App Control Configuration</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="global-style.css" rel="stylesheet">
|
||||
<style>
|
||||
#table{
|
||||
overflow: auto;
|
||||
}
|
||||
#first_td_th {
|
||||
width:300px;
|
||||
}
|
||||
#v2_td_th {
|
||||
width:180px;
|
||||
}
|
||||
#color-picker{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
background-color: transparent;
|
||||
}
|
||||
#select-anim {
|
||||
width:175px;
|
||||
}
|
||||
select{
|
||||
width:150px;
|
||||
}
|
||||
#slide-density{
|
||||
width: 70%;
|
||||
}
|
||||
#slide-speed{
|
||||
accent-color: rgb(181, 53, 53);
|
||||
width: 70%;
|
||||
}
|
||||
input::-webkit-slider-runnable-track {
|
||||
width: 450px;
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 name="h1element">App Control Configuration</h1>
|
||||
|
||||
<fieldset>
|
||||
<legend>Saved Animation Profiles</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label for="selSavedAnimProfiles">Profiles:</label>
|
||||
<select name= "selSavedAnimProfiles" id="selSaveddAnimProfiles" style="width:150px" onchange="OnSavedAnimProfilesChanged(this)">
|
||||
<option>(1)</option>
|
||||
<option>(2)</option>
|
||||
<option>(3)</option>
|
||||
<option>(4)</option>
|
||||
<option>(5)</option>
|
||||
<option>(6)</option>
|
||||
<option>(7)</option>
|
||||
<option>(8)</option>
|
||||
</select>
|
||||
  
|
||||
<label for="profileName">Rename:</label>
|
||||
<input type="text" name="inputProfileName" id="profileName" style="width:140px">
|
||||
   
|
||||
<button id="saveProfile" onclick="SaveProfilesToServer()">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
<div ></div>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<tr><td>
|
||||
<div id="spacer-20"></div>
|
||||
</td></tr>
|
||||
|
||||
<fieldset>
|
||||
<legend id="legendLights">Countdown ( Constant Light )</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label id="constLightMin-label" for="constlightMin">Light Min:</label> <br>
|
||||
<input type="range" name="constLightMin" id="constlightMin" min="0" max="99" value="25" step="1" onchange="updateLabel('Light Min: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="constLightMax-label" for="constlightMax">Light Max:</label> <br>
|
||||
<input type="range" name="constLightMax" id="constlightMax" min="1" max="100" value="90" step="1" onchange="updateLabel('Light Max: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<!-- <button>Try</button> -->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td>
|
||||
<div id="spacer-10"></div>
|
||||
</td></tr>
|
||||
|
||||
<tr>
|
||||
<td id="first_td_th">
|
||||
<label for="Holdtime">Hold time(ms):</label><br>
|
||||
<input type="number" name="holdTime" id="holdtime" min="0" max="5000" value="500">
|
||||
</td>
|
||||
<td>
|
||||
<label for="Ramptime">Ramp dn(ms):</label><br>
|
||||
<input type="number" name="rampTime" id="Ramptime" min="0" max="5000" value="500">
|
||||
</td>
|
||||
<tr><td>
|
||||
<div id="spacer-10"></div>
|
||||
</td></tr>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset0">
|
||||
<legend name="legendAnim0">Event0</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">White Fill Animation: </label> <br>
|
||||
<select name="sel-anim0" id="select-anim0"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim0" id="color-picker" > 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim0" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density0-label" for="slide-density0">Density:</label> <br>
|
||||
<input type="range" name="slide-density0" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed0-label" for="slide-speed0">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed0" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(0)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset1">
|
||||
<legend name="legendAnim1">Event1</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim1" id="select-anim1"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim1" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim1" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density1-label" for="slide-density1">Density:</label> <br>
|
||||
<input type="range" name="slide-density1" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed1-label" for="slide-speed1">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed1" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(1)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset2">
|
||||
<legend name="legendAnim2">Event2</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim2" id="select-anim2"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim2" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim2" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density2-label" for="slide-density2">Density:</label> <br>
|
||||
<input type="range" name="slide-density2" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed2-label" for="slide-speed2">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed2" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(2)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset3">
|
||||
<legend name="legendAnim3">Event3</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim3" id="select-anim3"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim3" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim3" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density3-label" for="slide-density3">Density:</label> <br>
|
||||
<input type="range" name="slide-density3" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed3-label" for="slide-speed3">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed3" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(3)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset4">
|
||||
<legend name="legendAnim4">Event4</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim4" id="select-anim4"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim4" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim4" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density4-label" for="slide-density4">Density:</label> <br>
|
||||
<input type="range" name="slide-density4" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed4-label" for="slide-speed4">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed4" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(4)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset5">
|
||||
<legend name="legendAnim5">Event5</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim5" id="select-anim5"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim5" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim5" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density5-label" for="slide-density5">Density:</label> <br>
|
||||
<input type="range" name="slide-density5" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed5-label" for="slide-speed5">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed5" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(5)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset6">
|
||||
<legend name="legendAnim6">Event6</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim6" id="select-anim6"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim6" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim6" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density6-label" for="slide-density6">Density:</label> <br>
|
||||
<input type="range" name="slide-density6" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed6-label" for="slide-speed6">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed6" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(6)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset7">
|
||||
<legend name="legendAnim7">Event7</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim7" id="select-anim7"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim7" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim7" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density7-label" for="slide-density7">Density:</label> <br>
|
||||
<input type="range" name="slide-density7" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed7-label" for="slide-speed7">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed7" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(7)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset8">
|
||||
<legend name="legendAnim8">Event8</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim8" id="select-anim8"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim8" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim8" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density8-label" for="slide-density8">Density:</label> <br>
|
||||
<input type="range" name="slide-density8" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed8-label" for="slide-speed8">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed8" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(8)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset9">
|
||||
<legend name="legendAnim9">Event9</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim9" id="select-anim9"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim9" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim9" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density9-label" for="slide-density9">Density:</label> <br>
|
||||
<input type="range" name="slide-density9" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed9-label" for="slide-speed9">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed9" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(9)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset10">
|
||||
<legend name="legendAnim10">Event10</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim10" id="select-anim10"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim10" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim10" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density10-label" for="slide-density10">Density:</label> <br>
|
||||
<input type="range" name="slide-density10" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed10-label" for="slide-speed10">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed10" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(10)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<div id="spacer-20"></div>
|
||||
|
||||
<fieldset name="event-fieldset11">
|
||||
<legend name="legendAnim11">Event11</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="v2_td_th">
|
||||
<label for="sel-count-anim">Animation: </label> <br>
|
||||
<select name="sel-anim11" id="select-anim11"></select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col1:</label><br>
|
||||
<input type="color" name="col1-color-anim11" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label for="color-picker">Col2:</label><br>
|
||||
<input type="color" name="col2-color-anim11" id="color-picker"> 
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-density11-label" for="slide-density11">Density:</label> <br>
|
||||
<input type="range" name="slide-density11" id="slide-density" min="0" max="100" value="25" step="1" onchange="updateLabel('Density: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<label id="slide-speed11-label" for="slide-speed11">Speed:</label> <br>
|
||||
<input type="range" name="slide-speed11" id="slide-speed" min="0" max="100" value="25" step="1" onchange="updateLabel('Speed: ', this)">
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="postPlayAnim(11)">Try</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<script>
|
||||
window.onload = function() { getProfilesAndEvents(); };
|
||||
|
||||
// Initialize Names
|
||||
const EVENTCOUNT = 12;
|
||||
const PROFILECOUNT = 8;
|
||||
var animEvent = [EVENTCOUNT];
|
||||
var profilesJson;
|
||||
var eventsJson;
|
||||
var animListJson;
|
||||
const inputProfileName = document.getElementsByName('inputProfileName')[0];
|
||||
const selSavedAnimProfiles = document.getElementsByName('selSavedAnimProfiles')[0];
|
||||
const constLightMin = document.getElementsByName('constLightMin')[0];
|
||||
const constLightMax = document.getElementsByName('constLightMax')[0];
|
||||
const holdTime = document.getElementsByName('holdTime')[0];
|
||||
const rampTime = document.getElementsByName('rampTime')[0];
|
||||
var eventFieldset = [EVENTCOUNT];
|
||||
var eventLegend = [EVENTCOUNT];
|
||||
var lastAnimProfileIndex = 0;
|
||||
var h1element = document.getElementsByName("h1element")[0];
|
||||
|
||||
// set event names by app type and if they are visible
|
||||
for (let i = 0; i < EVENTCOUNT; i++) {
|
||||
eventFieldset[i] = document.getElementsByName("event-fieldset" + i)[0];
|
||||
eventLegend[i] = document.getElementsByName("legendAnim" + i)[0];
|
||||
}
|
||||
|
||||
for (let i = 0; i < EVENTCOUNT; i++) {
|
||||
animEvent[i] = {
|
||||
anim: document.getElementsByName('sel-anim' + i)[0],
|
||||
col1: document.getElementsByName('col1-color-anim' + i)[0],
|
||||
col2: document.getElementsByName('col2-color-anim' + i)[0],
|
||||
density: document.getElementsByName('slide-density' + i)[0],
|
||||
speed: document.getElementsByName('slide-speed' + i)[0]
|
||||
};
|
||||
}
|
||||
|
||||
// update the form with animation data when profile list item selected
|
||||
function setProfile(index){
|
||||
var changeEvent = new Event("change");
|
||||
|
||||
// update profiles
|
||||
inputProfileName.value = "";
|
||||
constLightMin.value = profilesJson.countdown.min;
|
||||
constLightMin.dispatchEvent(changeEvent);
|
||||
constLightMax.value = profilesJson.countdown.max;
|
||||
constLightMax.dispatchEvent(changeEvent);
|
||||
holdTime.value = profilesJson.countdown.hold;
|
||||
rampTime.value = profilesJson.countdown.ramp;
|
||||
|
||||
// Update Events
|
||||
for (let i = 0; i < EVENTCOUNT; i++) {
|
||||
animEvent[i].anim.selectedIndex = profilesJson.profiles[index].events[i].anim;
|
||||
animEvent[i].col1.value = profilesJson.profiles[index].events[i].col1;
|
||||
animEvent[i].col2.value = profilesJson.profiles[index].events[i].col2;
|
||||
animEvent[i].density.value = profilesJson.profiles[index].events[i].density;
|
||||
animEvent[i].density.dispatchEvent(changeEvent);
|
||||
animEvent[i].speed.value = profilesJson.profiles[index].events[i].speed;
|
||||
animEvent[i].speed.dispatchEvent(changeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// When new profile is selected
|
||||
function OnSavedAnimProfilesChanged( event ){
|
||||
console.log('Anim Profile Selected index:', event.selectedIndex);
|
||||
console.log('Anim Profile Updated index:', lastAnimProfileIndex);
|
||||
updateProfilesJson(lastAnimProfileIndex); // save current setting in ram but on in server yet
|
||||
setProfile(event.selectedIndex);
|
||||
lastAnimProfileIndex = event.selectedIndex;
|
||||
}
|
||||
|
||||
//Update the profilesJson obj with updated values
|
||||
function updateProfilesJson(index){
|
||||
|
||||
//let index = selSavedAnimProfiles.selectedIndex;
|
||||
if(inputProfileName.value != ""){
|
||||
profilesJson.profiles[index].name = inputProfileName.value;
|
||||
selSavedAnimProfiles.options[index].text = '(' + (index + 1) + ') ' + inputProfileName.value;
|
||||
selSavedAnimProfiles.options[index].value = inputProfileName.value;
|
||||
}
|
||||
profilesJson.countdown.min = constLightMin.value;
|
||||
profilesJson.countdown.max = constLightMax.value;
|
||||
profilesJson.countdown.hold = holdTime.value;
|
||||
profilesJson.countdown.ramp = rampTime.value;
|
||||
|
||||
// Update Events
|
||||
for (let i = 0; i < EVENTCOUNT; i++) {
|
||||
profilesJson.profiles[index].events[i].anim = animEvent[i].anim.selectedIndex;
|
||||
profilesJson.profiles[index].events[i].col1 = animEvent[i].col1.value;
|
||||
profilesJson.profiles[index].events[i].col2 = animEvent[i].col2.value;
|
||||
profilesJson.profiles[index].events[i].density = animEvent[i].density.value;
|
||||
profilesJson.profiles[index].events[i].speed = animEvent[i].speed.value;
|
||||
}
|
||||
}
|
||||
|
||||
// save profilesJson
|
||||
function SaveProfilesToServer(){
|
||||
// update profilesJson obj with current settings before posting
|
||||
updateProfilesJson(selSavedAnimProfiles.selectedIndex);
|
||||
profilesJson["profile-index"] = selSavedAnimProfiles.selectedIndex; // Set at active profile
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('type', 'anim-profiles');
|
||||
const url = '/post?' + params.toString();
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain' } ,
|
||||
body: JSON.stringify(profilesJson) // convert to string
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { console.log('Request successful');}
|
||||
else { throw new Error('Request failed');}
|
||||
})
|
||||
.catch(error => { console.error(error);});
|
||||
}
|
||||
|
||||
// send anim event to test out
|
||||
function postPlayAnim(eventIndex){
|
||||
let tempAnimProps = {};
|
||||
tempAnimProps.event = eventIndex;
|
||||
tempAnimProps.anim = animEvent[eventIndex].anim.selectedIndex;
|
||||
tempAnimProps.col1 = animEvent[eventIndex].col1.value
|
||||
tempAnimProps.col2 = animEvent[eventIndex].col2.value
|
||||
tempAnimProps.density = animEvent[eventIndex].density.value;
|
||||
tempAnimProps.speed = animEvent[eventIndex].speed.value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('type', 'play-anim');
|
||||
const url = '/post?' + params.toString();
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/plain'} ,
|
||||
body: JSON.stringify(tempAnimProps)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) { throw new Error('Request failed'); }
|
||||
})
|
||||
.catch(error => { console.error(error); });
|
||||
}
|
||||
|
||||
// Get profiles
|
||||
function getProfilesAndEvents(){
|
||||
try{
|
||||
fetch_json_file('anim-profiles').then(result =>{
|
||||
profilesJson = result.data;
|
||||
//console.log({profilesJson});
|
||||
|
||||
fetch_json_file('app-events').then(result =>{
|
||||
eventsJson = result.data.apps[result.data.index]
|
||||
//console.log({evetnsJson});
|
||||
|
||||
fetch_json_file('anim-list').then(result =>{
|
||||
animListJson = result.data;
|
||||
//console.log({animListJson});
|
||||
|
||||
FillWhitefilList(animListJson.whitefills);
|
||||
FillAnimationsList(animListJson.animations);
|
||||
FillProfilesList(profilesJson);
|
||||
selSavedAnimProfiles.selectedIndex = profilesJson['profile-index'];
|
||||
lastAnimProfileIndex = selSavedAnimProfiles.selectedIndex;
|
||||
setProfile(selSavedAnimProfiles.selectedIndex);
|
||||
NameAndHideEvents();
|
||||
});
|
||||
});
|
||||
});
|
||||
}catch (error){
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function fetch_json_file(fileName){
|
||||
const params = new URLSearchParams();
|
||||
params.append('type', fileName);
|
||||
const url = '/get?' + params.toString();
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) { return response.json(); }
|
||||
else { throw new Error('fetching ' + fileName + ' failed'); }
|
||||
})
|
||||
.then(data => { return{data:data}; })
|
||||
}
|
||||
|
||||
// Rename legends and or hide unused events property boxes
|
||||
function NameAndHideEvents(){
|
||||
h1element.textContent = eventsJson.name;
|
||||
for(let i = 0; i < EVENTCOUNT; i++){
|
||||
eventLegend[i].textContent = eventsJson.events[i];
|
||||
if(eventsJson.events[i] === ''){
|
||||
eventFieldset[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill Profiles List
|
||||
function FillProfilesList(profJson){
|
||||
for(let i = 0; i < PROFILECOUNT; i++){
|
||||
selSavedAnimProfiles.options[i].text = '(' + (i + 1) + ') ' + profJson.profiles[i].name;
|
||||
selSavedAnimProfiles.options[i].value = profJson.profiles[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
function FillWhitefilList(whiteJson){
|
||||
for(let x = 0; x < whiteJson.length; x++){
|
||||
var op = document.createElement("option");
|
||||
op.text = whiteJson[x];
|
||||
animEvent[0].anim.appendChild(op);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill Animations drop down lists
|
||||
function FillAnimationsList(animJson){
|
||||
for(let x = 0; x < animJson.length; x++){
|
||||
if(animJson[x] == ""){break;}// stop if blank
|
||||
for(let i = 1; i < EVENTCOUNT; i++){
|
||||
var op = document.createElement("option");
|
||||
op.text = animJson[x];
|
||||
op.value = animJson[x];
|
||||
animEvent[i].anim.appendChild(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLabel(labelText, slider) {
|
||||
try{
|
||||
let label = document.getElementById(slider.name + "-label");
|
||||
label.innerHTML = labelText + slider.value;
|
||||
}catch(e){
|
||||
debugger;
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System Log</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>System Log</h1>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,111 +0,0 @@
|
||||
#include "luma_master.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <FS.h>
|
||||
#include <LittleFS.h>
|
||||
#include "JsonConstrain.h"
|
||||
#include <esp_now.h>
|
||||
#include <WiFi.h>
|
||||
#include "global.h"
|
||||
|
||||
static const char* tag = "lumaM";
|
||||
uint8_t broadcastAddress[] = {0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF};
|
||||
uint8_t availableStiks = 0;
|
||||
LUMA_NODE lumaStation[LUMA_NODE_MAX_COUNT];
|
||||
bool LUMASTIKS_READY = false;
|
||||
|
||||
TaskHandle_t LumaMaster_Task_Handle;
|
||||
|
||||
bool SendRegistrationPacket = false;
|
||||
LUMA_PACKET RegistrationPacket;
|
||||
esp_now_peer_info_t TempNode;
|
||||
|
||||
|
||||
|
||||
QueueHandle_t lumaQueue = xQueueCreate( 4, sizeof( LUMA_PACKET ) );
|
||||
|
||||
void Init_Luma_Master(void){
|
||||
|
||||
if (esp_now_init() != ESP_OK) {
|
||||
ESP_LOGD(tag, "Error initializing ESP-NOW\n");
|
||||
return;
|
||||
}
|
||||
|
||||
xTaskCreatePinnedToCore(LumaMaster_Task, "LumaMaster_Task", 1024*6, NULL, 1, &LumaMaster_Task_Handle, CONFIG_ARDUINO_RUNNING_CORE);
|
||||
ESP_LOGV(tag, "Initialized LumaMaster task created...");
|
||||
|
||||
esp_now_register_recv_cb(Luma_Master_Data_Received);
|
||||
esp_now_register_send_cb(Luma_Master_Data_Sent);
|
||||
}
|
||||
|
||||
void LumaMaster_Task(void *parameters){
|
||||
|
||||
while(1){
|
||||
vTaskSuspend(NULL);
|
||||
|
||||
if(SendRegistrationPacket){
|
||||
Luma_Send_Packet(TempNode.peer_addr, RegistrationPacket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LUMA_PACKET lumaPacket;
|
||||
|
||||
while(1){
|
||||
xQueueReceive( lumaQueue, &lumaPacket, portMAX_DELAY );
|
||||
|
||||
switch(lumaPacket.type){
|
||||
case LPT_RAW:
|
||||
break;
|
||||
case LPT_REG:
|
||||
Luma_Send_Packet(TempNode.peer_addr, lumaPacket);
|
||||
break;
|
||||
case LPT_ANIM:
|
||||
Luma_Master_Broadcast(lumaPacket);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Luma_Master_Data_Sent(const uint8_t *mac_addr, esp_now_send_status_t status){
|
||||
|
||||
}
|
||||
|
||||
void Luma_Master_Data_Received(const uint8_t *mac, const uint8_t *data, int len){
|
||||
|
||||
LUMA_PACKET *packet = (LUMA_PACKET*)data;
|
||||
|
||||
printf("** Data Received **\n\n");
|
||||
printf("Received from MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
printf("Length: %d byte(s)\n", len);
|
||||
printf("Data: %d\n\n", data[0]);
|
||||
|
||||
switch(packet->type){
|
||||
case LPT_RAW:
|
||||
break;
|
||||
case LPT_REG:
|
||||
LUMA_REGISTER_PACKET *reg = (LUMA_REGISTER_PACKET*)&packet->data;
|
||||
ESP_LOGD(tag, "Node SSID: %s", reg->ssid);
|
||||
|
||||
// Register
|
||||
memcpy(TempNode.peer_addr, mac, 6);
|
||||
TempNode.channel = WiFi.channel();
|
||||
Luma_Add_Peer(TempNode);
|
||||
|
||||
// Notify Peer
|
||||
RegistrationPacket.type = LPT_REG;
|
||||
xQueueSend( lumaQueue, &RegistrationPacket, 100 );
|
||||
break;
|
||||
//default:
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
void Luma_Master_Broadcast(LUMA_PACKET packet){
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
#ifndef _LUMA_MASTER_H
|
||||
#define _LUMA_MASTER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <esp_now.h>
|
||||
#include "common/luma-stiks.h"
|
||||
|
||||
|
||||
extern bool LUMASTIKS_READY;
|
||||
|
||||
#define LUMA_NODE_MAX_COUNT 16
|
||||
//extern LUMA_NODE lumaStation[];
|
||||
|
||||
void LumaMaster_Task(void *parameters);
|
||||
|
||||
void Init_Luma_Master(void);
|
||||
|
||||
void Luma_Master_Data_Received(const uint8_t *mac, const uint8_t *data, int len);
|
||||
|
||||
void Luma_Master_Data_Sent(const uint8_t *mac_addr, esp_now_send_status_t status);
|
||||
|
||||
void Luma_Master_Broadcast(LUMA_PACKET packet);
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,66 +0,0 @@
|
||||
#ifndef _MY_WIFI_H
|
||||
#define _MY_WIFI_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
extern int RebootSystem;
|
||||
|
||||
void Wifi_Start_WebServer(void);
|
||||
void Init_Wifi_Task(void);
|
||||
void Wifi_Start_MDNS(void);
|
||||
void Wifi_Task(void *parameters);
|
||||
String getSoftAPMacAddress(void);
|
||||
//void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
|
||||
void onWiFiEvent(WiFiEvent_t event);
|
||||
void Wifi_Request_Task(void *parameters);
|
||||
void Setup_WebServer_Handlers(AsyncWebServer* serv);
|
||||
|
||||
void Wifi_Read_Credentials(const JsonObject &credJson);
|
||||
void Wifi_Save_Credentials(String newSSID, String newPass);
|
||||
bool isInternetConnected(void);
|
||||
void Wifi_Client_Loop(void);
|
||||
|
||||
// Handlers
|
||||
void handlePOST_RegStick(AsyncWebServerRequest *request);
|
||||
|
||||
void handleGET_DSLRBooth(AsyncWebServerRequest *request);
|
||||
void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
void handlePOST_Update(AsyncWebServerRequest *request);
|
||||
void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
void updateFirmwareProgress(size_t progress, size_t total);
|
||||
|
||||
void handleGET_SendFile(AsyncWebServerRequest *request);
|
||||
void onFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
|
||||
void handleGET_Get(AsyncWebServerRequest *request);
|
||||
void handlePOST_Post(AsyncWebServerRequest *request);
|
||||
void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total);
|
||||
|
||||
|
||||
void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&));
|
||||
String htmlProcessor(const String& var);
|
||||
String HomeHtmlProcessor(const String& var);
|
||||
//const char* htmlProcessor(const char* var);
|
||||
String listDir(fs::FS &fs, const char *dirname, bool isSubdirectory);
|
||||
|
||||
const char* getFileExtension(const char* filename);
|
||||
const char* getFileType(const char* ext);
|
||||
const char* convertFileSize(const size_t bytes);
|
||||
char* readFile(fs::FS &fs, const char* path);
|
||||
void writeFile(fs::FS &fs, const char * path, const char * message);
|
||||
|
||||
String varReplace(const String& input, String (*callback)(const String&));
|
||||
//const char* varReplace(const char* input, const char* (*callback)(const char*));
|
||||
|
||||
void CreateSysSummmaryPacket(JsonDocument &doc);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,78 +0,0 @@
|
||||
#ifndef NEO_COLORS_H
|
||||
#define NEO_COLORS_H
|
||||
|
||||
#include <NeoPixelBus.h>
|
||||
|
||||
#define USE_CORRECTED_COLORS 0
|
||||
|
||||
const RgbColor col_black(0, 0, 0);
|
||||
const RgbColor col_white(255, 255, 255);
|
||||
const RgbColor col_red(255, 0, 0);
|
||||
const RgbColor col_green(0, 255, 0);
|
||||
const RgbColor col_blue(0, 0, 255);
|
||||
|
||||
#if USE_CORRECTED_COLORS == 0
|
||||
|
||||
const RgbColor col_orange(255, 165, 0);
|
||||
const RgbColor col_yellow(255, 255, 0);
|
||||
const RgbColor col_cyan(0, 255, 255);
|
||||
const RgbColor col_magenta(255, 0, 255);
|
||||
const RgbColor col_purple(128, 0, 128);
|
||||
const RgbColor col_pink(255, 192, 203);
|
||||
const RgbColor col_teal(0, 128, 128);
|
||||
const RgbColor col_lime(0, 255, 0);
|
||||
const RgbColor col_indigo(75, 0, 130);
|
||||
const RgbColor col_maroon(128, 0, 0);
|
||||
const RgbColor col_navy(0, 0, 128);
|
||||
const RgbColor col_olive(128, 128, 0);
|
||||
const RgbColor col_beige(245, 245, 220);
|
||||
const RgbColor col_brown(165, 42, 42);
|
||||
const RgbColor col_coral(255, 127, 80);
|
||||
const RgbColor col_gold(255, 215, 0);
|
||||
const RgbColor col_gray(128, 128, 128);
|
||||
const RgbColor col_ivory(255, 255, 240);
|
||||
const RgbColor col_khaki(240, 230, 140);
|
||||
const RgbColor col_lavender(230, 230, 250);
|
||||
const RgbColor col_peach(255, 218, 185);
|
||||
const RgbColor col_periwinkle(204, 204, 255);
|
||||
const RgbColor col_salmon(250, 128, 114);
|
||||
const RgbColor col_sienna(160, 82, 45);
|
||||
const RgbColor col_silver(192, 192, 192);
|
||||
const RgbColor col_tan(210, 180, 140);
|
||||
const RgbColor col_turquoise(64, 224, 208);
|
||||
const RgbColor col_violet(238, 130, 238);
|
||||
|
||||
#else
|
||||
|
||||
const RgbColor col_orange(255, 128, 0);
|
||||
const RgbColor col_yellow(255, 255, 0);
|
||||
const RgbColor col_cyan(0, 255, 255);
|
||||
const RgbColor col_magenta(255, 0, 255);
|
||||
const RgbColor col_purple(170, 0, 255);
|
||||
const RgbColor col_pink(255, 170, 255);
|
||||
const RgbColor col_teal(0, 128, 128);
|
||||
const RgbColor col_lime(128, 255, 0);
|
||||
const RgbColor col_indigo(85, 0, 255);
|
||||
const RgbColor col_maroon(128, 0, 0);
|
||||
const RgbColor col_navy(0, 0, 128);
|
||||
const RgbColor col_olive(128, 128, 0);
|
||||
const RgbColor col_beige(255, 230, 204);
|
||||
const RgbColor col_brown(153, 51, 0);
|
||||
const RgbColor col_coral(255, 102, 102);
|
||||
const RgbColor col_gold(204, 153, 0);
|
||||
const RgbColor col_gray(128, 128, 128);
|
||||
const RgbColor col_ivory(255, 255, 204);
|
||||
const RgbColor col_khaki(204, 204, 0);
|
||||
const RgbColor col_lavender(204, 153, 255);
|
||||
const RgbColor col_peach(255, 204, 153);
|
||||
const RgbColor col_periwinkle(153, 153, 255);
|
||||
const RgbColor col_salmon(255, 153, 102);
|
||||
const RgbColor col_sienna(153, 76, 0);
|
||||
const RgbColor col_silver(204, 204, 204);
|
||||
const RgbColor col_tan(204, 153, 102);
|
||||
const RgbColor col_turquoise(0, 204, 204);
|
||||
const RgbColor col_violet(204, 0, 255);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@ -1,96 +0,0 @@
|
||||
#include "otaupdate.h"
|
||||
#include "LittleFS.h"
|
||||
#include <HTTPClient.h>
|
||||
#include <Update.h>
|
||||
#include "githubCert.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
|
||||
String binFile = "manifest.json";
|
||||
String rootUrl = "https://raw.githubusercontent.com/ataindustries/atacontrolboardV1/main/";
|
||||
String github_token = "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";
|
||||
|
||||
const float FirmwareVer = 1.0;
|
||||
|
||||
JsonDocument doc;
|
||||
|
||||
int otaupdate_check(void)
|
||||
{/*
|
||||
String payload;
|
||||
int httpCode;
|
||||
WiFiClientSecure * client = new WiFiClientSecure;
|
||||
HTTPClient https;
|
||||
|
||||
String manifest_url = rootUrl + binFile + "?token=" + github_token;
|
||||
https.begin(manifest_url);
|
||||
httpCode = https.GET();
|
||||
|
||||
if (httpCode == HTTP_CODE_OK) // if version received
|
||||
{
|
||||
String payload = https.getString();
|
||||
JsonDocument doc;
|
||||
deserializeJson(doc, payload);
|
||||
|
||||
float firmver = doc["firmver"].as<float>();
|
||||
const char* firmfile = doc["firmfile"];
|
||||
const char* fsfile = doc["fsfile"];
|
||||
|
||||
Serial.printf("From manifest: %.1f, %s, $s\n\r", firmver, firmfile, fsfile);
|
||||
|
||||
if(firmver > FirmwareVer){
|
||||
Serial.println("New firmware available!");
|
||||
return 1;
|
||||
}else{
|
||||
Serial.println("firmware is upto date!");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
void otaupdate_perform(void)
|
||||
{
|
||||
|
||||
// setup events
|
||||
ota_events();
|
||||
|
||||
|
||||
// Pause unnecessary tasks
|
||||
|
||||
//check for update
|
||||
}
|
||||
|
||||
void ota_events(void)
|
||||
{
|
||||
/*
|
||||
fota->setUpdateEndCb( [](int partition)
|
||||
{
|
||||
//Serial.printf("Update could not finish with %s partition\n", partition==U_SPIFFS ? "spiffs" : "firmware" );
|
||||
});
|
||||
|
||||
fota->setProgressCb( [](size_t progress, size_t size)
|
||||
{
|
||||
//if( progress == size || progress == 0 ) Serial.println();
|
||||
//Serial.print(".");
|
||||
});
|
||||
|
||||
fota->setUpdateCheckFailCb( [](int partition, int error_code)
|
||||
{
|
||||
//Serial.printf("Update could validate %s partition (error %d)\n", partition==U_SPIFFS ? "spiffs" : "firmware", error_code );
|
||||
// error codes:
|
||||
// -1 : partition not found
|
||||
// -2 : validation (signature check) failed
|
||||
});
|
||||
|
||||
fota->setUpdateFinishedCb( [](int partition, bool restart_after)
|
||||
{
|
||||
//Serial.printf("Update could not begin with %s partition\n", partition==U_SPIFFS ? "spiffs" : "firmware" );
|
||||
|
||||
if( restart_after ) {
|
||||
ESP.restart();
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
#ifndef _OTAUPDATE_H
|
||||
#define _OTAUPDATE_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
|
||||
int otaupdate_check(float myVer);
|
||||
|
||||
void otaupdate_perform(String filePath);
|
||||
|
||||
void ota_events(void);
|
||||
|
||||
|
||||
#endif
|
||||
@ -1,44 +0,0 @@
|
||||
|
||||
#include "rf_transceiver.h"
|
||||
|
||||
static const char* tag = "trx433";
|
||||
TaskHandle_t RF_Task_Handle;
|
||||
|
||||
const int txChannel = RMT_CHANNEL_0;
|
||||
const int rxChannel = RMT_CHANNEL_1;
|
||||
|
||||
void Init_RF_Receiver(void)
|
||||
{
|
||||
/*
|
||||
// Configure RMT receiver
|
||||
rmt_config_t rxConfig;
|
||||
rxConfig.channel = (rmt_channel_t)rxChannel;
|
||||
rxConfig.gpio_num = static_cast<gpio_num_t>(RMT_RX_PIN);
|
||||
// Set other reception settings as needed
|
||||
rmt_config(&rxConfig);
|
||||
rmt_driver_install(rxConfig.channel, 1000, 0);
|
||||
|
||||
xTaskCreatePinnedToCore(RF_Control_Task, "RF_Task", 8000, NULL, 1, &RF_Task_Handle, 0);
|
||||
*/
|
||||
}
|
||||
|
||||
void Init_RF_Transmitter(void)
|
||||
{
|
||||
/*
|
||||
rmt_config_t txConfig;
|
||||
txConfig.channel = (rmt_channel_t)txChannel;
|
||||
txConfig.gpio_num = static_cast<gpio_num_t>(RMT_TX_PIN);
|
||||
// Set other transmission settings as needed
|
||||
rmt_config(&txConfig);
|
||||
rmt_driver_install(txConfig.channel, 0, 0);
|
||||
*/
|
||||
}
|
||||
|
||||
void RF_Control_Task(void *parameters)
|
||||
{
|
||||
for(;;){
|
||||
vTaskDelay(100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
#ifndef _RF_TRANSCEIVER_H
|
||||
#define _RF_TRANSCEIVER_H
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <driver/rmt.h>
|
||||
#include "my_board.h"
|
||||
|
||||
#define RMT_RX_PIN RX433_Pin
|
||||
#define RMT_TX_PIN TX433_Pin
|
||||
#define RTM_RX_CHANNEL 0
|
||||
#define RTM_TX_CHANNEL 1
|
||||
|
||||
void Init_RF_Receiver(void);
|
||||
|
||||
void Init_RF_Transmitter(void);
|
||||
|
||||
void RF_Control_Task(void *parameters);
|
||||
|
||||
#endif
|
||||
@ -1,50 +0,0 @@
|
||||
.blue {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
h1{
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
body{
|
||||
color: red;
|
||||
}
|
||||
|
||||
.secTitleFont{
|
||||
color: white;
|
||||
|
||||
}
|
||||
.secTitle{
|
||||
min-width: none;
|
||||
height: 25px;
|
||||
width: auto;
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/*border: 3px solid black;*/
|
||||
color: rgb(56, 132, 255);
|
||||
background-color: rgb(61, 118, 153);
|
||||
font-family: Tahoma, Arial, sans-serif;
|
||||
font-size: smaller;
|
||||
font-weight: bold;
|
||||
flex-direction: ;
|
||||
}
|
||||
|
||||
.secCntrls{
|
||||
height: 100px;
|
||||
width: auto;
|
||||
margin: 5px;
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
color: rgba(255, 255, 255, 0);
|
||||
font-family: Tahoma, Arial, sans-serif;
|
||||
/*display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;*/
|
||||
}
|
||||
|
||||
.dropList{
|
||||
margin-right: 40px;
|
||||
}
|
||||
@ -1,190 +0,0 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"remote": "data/ata-boothifier-upgrade.html",
|
||||
"local": "/ata-boothifier-upgrade.html",
|
||||
"md5": "92074ec24fd467545272eb9e837d644c",
|
||||
"size": 16980
|
||||
},
|
||||
{
|
||||
"remote": "data/ata-boothifier-upgradeV2.html",
|
||||
"local": "/ata-boothifier-upgradeV2.html",
|
||||
"md5": "484648993370e4b6aff13124bd1c55c1",
|
||||
"size": 18178
|
||||
},
|
||||
{
|
||||
"remote": "data/ata-boothifier-upgradeV3.html",
|
||||
"local": "/ata-boothifier-upgradeV3.html",
|
||||
"md5": "79426145db379ac15ccc742245a31ecb",
|
||||
"size": 17233
|
||||
},
|
||||
{
|
||||
"remote": "data/favicon.ico",
|
||||
"local": "/favicon.ico",
|
||||
"md5": "ba4c4e3bf5e5db2bbfc56a52f3657d79",
|
||||
"size": 1150
|
||||
},
|
||||
{
|
||||
"remote": "data/flashstik-reg.html",
|
||||
"local": "/flashstik-reg.html",
|
||||
"md5": "394fdfe3cd3fb89a8ddb3d6edf2d2da4",
|
||||
"size": 15651
|
||||
},
|
||||
{
|
||||
"remote": "data/css/global-style.css",
|
||||
"local": "/css/global-style.css",
|
||||
"md5": "217a9cca8b4eae2d28fa8bc5a0f6db09",
|
||||
"size": 1239
|
||||
},
|
||||
{
|
||||
"remote": "data/css/nav.css",
|
||||
"local": "/css/nav.css",
|
||||
"md5": "e653a75433056f28e3f410f567b8622c",
|
||||
"size": 1538
|
||||
},
|
||||
{
|
||||
"remote": "data/images/atalogo.png",
|
||||
"local": "/images/atalogo.png",
|
||||
"md5": "5a18c88a4ea80c8d8d0ad52b2fdbbadc",
|
||||
"size": 16690
|
||||
},
|
||||
{
|
||||
"remote": "data/images/favicon-32x32.png",
|
||||
"local": "/images/favicon-32x32.png",
|
||||
"md5": "d80cf74ace3a8be487a5158bca48f9b6",
|
||||
"size": 2428
|
||||
},
|
||||
{
|
||||
"remote": "data/js/event-box.js",
|
||||
"local": "/js/event-box.js",
|
||||
"md5": "553f26707686038e275227a97eb96659",
|
||||
"size": 12341
|
||||
},
|
||||
{
|
||||
"remote": "data/js/fwUoload.js",
|
||||
"local": "/js/fwUoload.js",
|
||||
"md5": "d4a734cce529c3adf831ea46259f9c2d",
|
||||
"size": 1524
|
||||
},
|
||||
{
|
||||
"remote": "data/js/hue-select.js",
|
||||
"local": "/js/hue-select.js",
|
||||
"md5": "0a58f1a339af5c54aecfc5ce1aac7168",
|
||||
"size": 6367
|
||||
},
|
||||
{
|
||||
"remote": "data/js/jquery-3.7.1.js",
|
||||
"local": "/js/jquery-3.7.1.js",
|
||||
"md5": "fdb81281d3773a7462998fdddbe6f5bf",
|
||||
"size": 196885
|
||||
},
|
||||
{
|
||||
"remote": "data/system/ble.json",
|
||||
"local": "/system/ble.json",
|
||||
"md5": "faebefd5b50046a01d4a8aa27f8504d0",
|
||||
"size": 307
|
||||
},
|
||||
{
|
||||
"remote": "data/system/readme.txt",
|
||||
"local": "/system/readme.txt",
|
||||
"md5": "198c92a2cc06b3effce3b5ba313cc6a0",
|
||||
"size": 86
|
||||
},
|
||||
{
|
||||
"remote": "data/system/tunes.json",
|
||||
"local": "/system/tunes.json",
|
||||
"md5": "814999e88296bee179cef5d394f3f696",
|
||||
"size": 1149
|
||||
},
|
||||
{
|
||||
"remote": "data/system/update.json",
|
||||
"local": "/system/update.json",
|
||||
"md5": "01cc6a1935601085df49308cab646673",
|
||||
"size": 156
|
||||
},
|
||||
{
|
||||
"remote": "data/www/about.html",
|
||||
"local": "/www/about.html",
|
||||
"md5": "23f0c991c5c67e7eefe6c24aa50b59fb",
|
||||
"size": 4222
|
||||
},
|
||||
{
|
||||
"remote": "data/www/edit.html",
|
||||
"local": "/www/edit.html",
|
||||
"md5": "fed238dcf87d3797b08ed371330ea800",
|
||||
"size": 5546
|
||||
},
|
||||
{
|
||||
"remote": "data/www/edit_old.html",
|
||||
"local": "/www/edit_old.html",
|
||||
"md5": "9aafba533ac77352d30841cdc34db0c4",
|
||||
"size": 4240
|
||||
},
|
||||
{
|
||||
"remote": "data/www/failed.html",
|
||||
"local": "/www/failed.html",
|
||||
"md5": "a24025d56bef1cd2ed5375f6e8853dde",
|
||||
"size": 828
|
||||
},
|
||||
{
|
||||
"remote": "data/www/files.html",
|
||||
"local": "/www/files.html",
|
||||
"md5": "52d82d4d23b038929691cd0fb20e8ee0",
|
||||
"size": 9369
|
||||
},
|
||||
{
|
||||
"remote": "data/www/home.html",
|
||||
"local": "/www/home.html",
|
||||
"md5": "767fd73b733d8e37dc79252fcd20b610",
|
||||
"size": 7822
|
||||
},
|
||||
{
|
||||
"remote": "data/www/index.html",
|
||||
"local": "/www/index.html",
|
||||
"md5": "af690aaa4dec02691ffdb2765c837370",
|
||||
"size": 806
|
||||
},
|
||||
{
|
||||
"remote": "data/www/lights.html",
|
||||
"local": "/www/lights.html",
|
||||
"md5": "272f8dc423923bb5026837240f654efe",
|
||||
"size": 19056
|
||||
},
|
||||
{
|
||||
"remote": "data/www/navbar.html",
|
||||
"local": "/www/navbar.html",
|
||||
"md5": "89af40b990e297e41e116105b04b66ee",
|
||||
"size": 533
|
||||
},
|
||||
{
|
||||
"remote": "data/www/ok.html",
|
||||
"local": "/www/ok.html",
|
||||
"md5": "db42bd2ff52293c3b4d6ef62fb4e3a42",
|
||||
"size": 865
|
||||
},
|
||||
{
|
||||
"remote": "data/www/setup.html",
|
||||
"local": "/www/setup.html",
|
||||
"md5": "19e1f419844852800757ccd36bb7892a",
|
||||
"size": 11349
|
||||
},
|
||||
{
|
||||
"remote": "data/www/upgrade.html",
|
||||
"local": "/www/upgrade.html",
|
||||
"md5": "f4a3efb67be66b5214d905e605984c93",
|
||||
"size": 9080
|
||||
},
|
||||
{
|
||||
"remote": "data/www/wifi.html",
|
||||
"local": "/www/wifi.html",
|
||||
"md5": "c6e55946a02cb0ff28689dd10d265987",
|
||||
"size": 5320
|
||||
}
|
||||
],
|
||||
"firmware": {
|
||||
"md5": "fcec6e659842cc0333880b701a3e2634",
|
||||
"size": 1401712
|
||||
},
|
||||
"release_date": "2025-08-20",
|
||||
"release_time": "08:53:05"
|
||||
}
|
||||
@ -1,228 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Firmware Update</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="/css/nav.css" rel="stylesheet">
|
||||
<style>
|
||||
h1{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: larger;
|
||||
}
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 0;
|
||||
background-color: #f0f0f0;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
min-width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.outer-container {
|
||||
padding: 4px 20px 4px 20px
|
||||
}
|
||||
.container {
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 20px 10px 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
|
||||
width:90%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.center-text {
|
||||
text-align: center;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
width: calc(50% - 10px);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.row > *:last-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
.webupdate {
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: auto;
|
||||
}
|
||||
progress{
|
||||
flex: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
#lblprogress{
|
||||
flex: 0;
|
||||
}
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar"></div>
|
||||
<script>
|
||||
fetch('/www/navbar.html')
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
document.getElementById('navbar').innerHTML = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1 name="h1element">Firmware Update</h1>
|
||||
|
||||
<div class="outer-container">
|
||||
<div class="container">
|
||||
<legend class="center-text">Local Update</legend>
|
||||
<div class="row">
|
||||
<div>
|
||||
<input type="file" id="update-file" name="update-file" accept=".bin">
|
||||
</div>
|
||||
<div>
|
||||
<button id="submit-update-local" onclick="uploadFile(this)">Update!</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<label>File name: "ata_fw_booth_xxx.bin" or "ata_fs_booth_xxx.bin"</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="outer-container">
|
||||
<div class="container">
|
||||
<legend class="center-text">Web Update</legend>
|
||||
<div class="webupdate">
|
||||
<button id="submit-update-local" onclick="CheckUpdates(this)">Check for Updates</button>
|
||||
</div>
|
||||
|
||||
<div class="webupdate">
|
||||
<button id="submit-update-local" onclick="InstallFirmware">Install Firmware</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="outer-container">
|
||||
<div class="container">
|
||||
<legend class="center-text">Update Progress</legend>
|
||||
<div class="progress-container">
|
||||
<label id="lblprogress" for="firm-progress">Progress:</label>
|
||||
<progress id="firm-progress" value="0" max="100"></progress>
|
||||
<label id="lbl-firm-progress" for="firm-progress">- - - -</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('update-file');
|
||||
const progressBar = document.getElementById('firm-progress');
|
||||
const progressLabel = document.getElementById('lbl-firm-progress');
|
||||
const submitButton = document.getElementById('submit-update-local');
|
||||
|
||||
|
||||
function uploadFile(event) {
|
||||
event.preventDefault();
|
||||
const file = fileInput.files[0];
|
||||
const url = '/update'; // Replace with the actual upload URL
|
||||
|
||||
// File Checks
|
||||
//*********************************************************
|
||||
// Check file extension
|
||||
if (!file.name.toLowerCase().endsWith('.bin')) {
|
||||
alert('Please select a file with the ".bin" extension.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check filename prefix
|
||||
if (!file.name.toLowerCase().startsWith('ata_fw_booth') && !file.name.toLowerCase().startsWith('ata_fs_booth')) {
|
||||
alert('Please select a file with a filename starting with "ata_fw_booth" or "ata_fs_booth".');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
const maxSizeBytes = 2.7 * 1024 * 1024; // 2.75Mb in bytes
|
||||
if (file.size > maxSizeBytes) {
|
||||
alert('Please select a file with a size not exceeding 2.7Mb.');
|
||||
return;
|
||||
}
|
||||
//*********************************************************
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file-size', file.size); // Include the file size as a parameter
|
||||
formData.append('update-file', file);
|
||||
let s = "file-size: " + file.size;
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
const progressPercent = Math.round((event.loaded * 100) / event.total);
|
||||
progressBar.value = progressPercent;
|
||||
progressLabel.innerHTML =progressBar.value + "%";
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
console.log("received status: %d", xhr.status);
|
||||
if (xhr.status === 200) {
|
||||
// Upload completed successfully
|
||||
alert('Upload completed!');
|
||||
progressLabel.innerHTML = "Completed!";
|
||||
} else if (xhr.status === 500) {
|
||||
// Request was aborted (server-side)
|
||||
alert('Upload aborted by the server.');
|
||||
progressLabel.innerHTML = "Aborted!";
|
||||
} else {
|
||||
// Handle other error cases
|
||||
alert('An error occurred during the upload.');
|
||||
progressLabel.innerHTML = "Error!";
|
||||
}
|
||||
submitButton.disabled = false;
|
||||
progressBar.value = 0;
|
||||
progressLabel.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open('POST', url, true);
|
||||
xhr.send(formData);
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
|
||||
function getAvailableUpdates(){
|
||||
//Read Json
|
||||
|
||||
// Loop to create table
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,113 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BLE Interaction</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>BLE Interaction</h1>
|
||||
<button id="connectBtn">Connect</button>
|
||||
<button id="checkBtn" disabled>Check</button>
|
||||
<textarea id="logArea" readonly></textarea>
|
||||
<button id="startUpgradeBtn" disabled>Start Upgrade</button>
|
||||
|
||||
<script>
|
||||
// Constants
|
||||
const BLE_SERVER_NAME = "BLE_Server_Name"; // Replace with your server name
|
||||
const BLE_SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0"; // Replace with your service UUID
|
||||
const BLE_CHARACTERISTIC_UUID = "12345678-1234-5678-1234-56789abcdef1"; // Replace with your characteristic UUID
|
||||
|
||||
let bleDevice = null;
|
||||
let bleCharacteristic = null;
|
||||
|
||||
// Log messages to the textarea
|
||||
function logMessage(message) {
|
||||
const logArea = document.getElementById('logArea');
|
||||
logArea.value += message + '\n';
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
// Function to connect to the BLE server
|
||||
async function connectToBle() {
|
||||
logMessage('Requesting BLE device...');
|
||||
try {
|
||||
bleDevice = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ name: BLE_SERVER_NAME }],
|
||||
optionalServices: [BLE_SERVICE_UUID]
|
||||
});
|
||||
|
||||
logMessage('Connecting to GATT server...');
|
||||
const server = await bleDevice.gatt.connect();
|
||||
|
||||
logMessage('Getting service...');
|
||||
const service = await server.getPrimaryService(BLE_SERVICE_UUID);
|
||||
|
||||
logMessage('Getting characteristic...');
|
||||
bleCharacteristic = await service.getCharacteristic(BLE_CHARACTERISTIC_UUID);
|
||||
|
||||
logMessage('Connected to BLE server.');
|
||||
document.getElementById('checkBtn').disabled = false;
|
||||
document.getElementById('startUpgradeBtn').disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
logMessage('Connection failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send a JSON packet and wait for a reply
|
||||
async function sendJsonPacket(packet) {
|
||||
if (!bleCharacteristic) {
|
||||
logMessage('Not connected to BLE server.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logMessage('Sending JSON packet...');
|
||||
const encoder = new TextEncoder();
|
||||
const encodedPacket = encoder.encode(JSON.stringify(packet));
|
||||
await bleCharacteristic.writeValue(encodedPacket);
|
||||
|
||||
logMessage('Waiting for response...');
|
||||
const response = await bleCharacteristic.readValue();
|
||||
const decoder = new TextDecoder();
|
||||
const decodedResponse = decoder.decode(response);
|
||||
|
||||
logMessage('Received response: ' + decodedResponse);
|
||||
return JSON.parse(decodedResponse);
|
||||
} catch (error) {
|
||||
logMessage('Error during communication: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for buttons
|
||||
document.getElementById('connectBtn').addEventListener('click', connectToBle);
|
||||
document.getElementById('checkBtn').addEventListener('click', () => {
|
||||
const packet = { action: 'check' };
|
||||
sendJsonPacket(packet);
|
||||
});
|
||||
document.getElementById('startUpgradeBtn').addEventListener('click', () => {
|
||||
const packet = { action: 'start_upgrade' };
|
||||
sendJsonPacket(packet);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,533 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATA Firmware Update</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f4f4f4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status-indicator-ble {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-wifi {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-indicator-internet {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: gray;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Adds space above the WiFi Connect button */
|
||||
.btn-container.wifi {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
max-width: 130px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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>
|
||||
@ -1,13 +0,0 @@
|
||||
|
||||
Choose Control App:
|
||||
1) Open app-events.json
|
||||
2) Change index to corresponding app
|
||||
a. 0=DSLRBooth, 1=...., 2=.....
|
||||
|
||||
Front Constant Light: ( to enable the light)
|
||||
1) Open led-devices.json
|
||||
2) Change "front-light", "en" to true
|
||||
|
||||
Rear Constant Light:
|
||||
1) Open led-devices.json
|
||||
2) Change "rear-light", "en" to true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user