boothifier/data/www/usercfg.html
2025-09-28 23:18:18 -07:00

1324 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ATA Lights Config</title>
<style>
:root {
--bg: #0b0f14;
--card: #121922;
--ink: #e7ecf3;
--muted: #a5b3c4;
--accent: #3b82f6;
--accent-2: #2563eb;
--border: #1f2a3a;
--input: #0f1722;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f6f8fb;
--card: #ffffff;
--ink: #0f1620;
--muted: #4b5563;
--accent: #1e40af;
--accent-2: #15327f;
--border: #d9e0e8;
--input: #ffffff;
}
}
* {
box-sizing: border-box
}
html,
body {
height: 100%
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
line-height: 1.55;
}
/* Top bar */
.topbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
backdrop-filter: saturate(140%) blur(6px);
background: color-mix(in srgb, var(--bg) 86%, transparent);
z-index: 10;
}
.topbar h1 {
margin: 0;
font-size: clamp(1.1rem, 2.2vw, 1.6rem);
text-align: center;
}
.left {
justify-self: start;
}
.right {
justify-self: end;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--card);
color: var(--muted);
font-size: .95rem;
}
.chip strong {
color: var(--ink)
}
/* Layout */
.wrap {
max-width: 980px;
margin: 0 auto;
padding: 16px;
display: grid;
gap: 16px;
}
/* Tabs (CSS-only) */
.tabshell {
margin-top: 4px;
}
/* Hide radios but keep them focusable for a11y */
.tabshell input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.tabbar {
display: flex;
gap: 6px;
align-items: flex-end;
border-bottom: 1px solid var(--border);
}
.tabbar label {
cursor: pointer;
user-select: none;
padding: 10px 14px;
border: 1px solid var(--border);
border-bottom: none;
border-radius: 10px 10px 0 0;
background: var(--card);
color: var(--muted);
font-weight: 600;
}
/* Active tab style */
#tab-comm:checked~.tabbar label[for="tab-comm"],
#tab-settings:checked~.tabbar label[for="tab-settings"],
#tab-tools:checked~.tabbar label[for="tab-tools"] {
color: var(--ink);
background: var(--bg);
border-color: var(--border);
}
.panel {
display: none;
padding: 16px 0 0;
}
#tab-comm:checked~#panel-comm {
display: block;
}
#tab-settings:checked~#panel-settings {
display: block;
}
#tab-tools:checked~#panel-tools {
display: block;
}
/* Groups / cards */
.grid {
display: grid;
gap: 16px;
}
@media (min-width: 860px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
fieldset {
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
background: var(--card);
}
legend {
padding: 0 8px;
font-weight: 700;
color: var(--ink);
font-size: 1.05rem;
}
/* Rows */
.row {
display: grid;
grid-template-columns: 1fr 160px auto;
/* label | input | trailing */
align-items: center;
gap: 12px;
padding: 8px 0;
}
.row+.row {
border-top: 1px dashed var(--border);
}
.label {
font-weight: 600;
color: var(--ink);
}
.unit {
color: var(--muted);
min-width: 2ch;
}
/* Controls */
input[type="number"],
select {
width: 100%;
max-width: 160px;
padding: 10px 12px;
font-size: 1rem;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--input);
color: var(--ink);
}
input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent);
}
.checkbox-wrap {
display: flex;
align-items: center;
gap: 10px;
justify-content: flex-end;
}
input[type="color"] {
width: 48px;
height: 34px;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--input);
}
/* Buttons (decorative, no JS) */
.btn {
appearance: none;
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
background: var(--accent);
color: #fff;
cursor: pointer;
font-size: 1rem;
}
.btn:hover {
background: var(--accent-2);
}
.btn.ghost {
background: transparent;
color: var(--ink);
}
.btn.ghost:hover {
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
margin-top: 8px;
}
/* Small screens */
@media (max-width:520px) {
.row {
grid-template-columns: 1fr 1fr;
}
.unit {
display: none;
}
}
/* Communication status */
:root {
--comm-green: #22c55e;
--comm-red: #ef4444;
}
.comm-status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
color: var(--muted);
}
.comm-status .dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--comm-red);
box-shadow: 0 0 0 4px color-mix(in srgb, transparent 70%, var(--comm-red));
}
.comm-status .dot.connected {
background: var(--comm-green);
box-shadow: 0 0 0 4px color-mix(in srgb, transparent 70%, var(--comm-green));
}
.comm-status .status-text {
font-weight: 700;
color: var(--ink);
}
/* sr-only helper */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 1, 1);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<header class="topbar">
<div class="left">
<span class="chip">🌡️ Temp: <strong id="temp">--.-</strong> °F</span>
</div>
<h1>ATA Lights Config</h1>
<div class="right">
<span class="chip">⚡ V-IN: <strong id="vin">--.-</strong> V</span>
</div>
</header>
<main class="wrap">
<div class="comm-status" id="bleCommStatus">
<div class="dot" id="bleDot"></div>
<div class="status-text">BLE Comm: <span id="bleText">Disconnected</span></div>
</div>
<div class="tabshell">
<!-- Radios control which panel is visible (CSS-only) -->
<input type="radio" name="tab" id="tab-comm" checked>
<input type="radio" name="tab" id="tab-settings">
<input type="radio" name="tab" id="tab-tools">
<div class="tabbar" role="tablist" aria-label="Config Tabs">
<label for="tab-comm" id="label-comm" role="tab" aria-controls="panel-comm">Communication</label>
<label for="tab-settings" id="label-settings" role="tab" aria-controls="panel-settings">Settings</label>
<label for="tab-tools" id="label-tools" role="tab" aria-controls="panel-tools">Tools</label>
</div>
<!-- Panel: Communication -->
<section id="panel-comm" class="panel" role="tabpanel" aria-labelledby="label-comm">
<fieldset>
<legend>Communication</legend>
<div class="row">
<label class="label" for="deviceName">Device Name</label>
<input id="deviceName" type="text" placeholder="ATALIGHTS" />
<div class="unit">
<button class="btn" type="button" id="btnConnect">Connect</button>
</div>
</div>
</fieldset>
</section>
<!-- Panel: Settings (existing groups) -->
<section id="panel-settings" class="panel" role="tabpanel" aria-labelledby="label-settings">
<section class="grid">
<!-- Core Settings -->
<fieldset>
<legend>Core Settings</legend>
<div class="row">
<div class="label">Limited Mode</div>
<div class="checkbox-wrap">
<input id="limited" type="checkbox" />
<label for="limited" class="sr-only">Limited Mode</label>
</div>
<div class="unit"></div>
</div>
<div class="row">
<label class="label" for="profile">Booth Profile</label>
<select id="profile">
<option value=custom>Custom</option>
<option value=orig-roam>Roamer Original</option>
<option value=big-roam>Roamer Big</option>
<option value=sport>Helio Sport</option>
<option value=flare-posh>Helio Flare-Posh</option>
<option value=m-lumia>Lumia M</option>
<option value=xl-lumia>Lumia XL</option>
<option value=spectra>Lumia Spectra</option>
<option value=marquee>Marquee</option>
<option value=m1>m1</option>
</select>
<div class="unit"></div>
</div>
<div class="row">
<label class="label" for="ledChipset">LED Chipset</label>
<select id="ledChipset">
<option value=0>WS2812B</option>
<option value=1>SK6812</option>
<option value=2>WS2811-400</option>
<option value=3>WS2815</option>
</select>
<div class="unit"></div>
</div>
<div class="row">
<label class="label" for="rgbOrder">RGB Order</label>
<select id="rgbOrder">
<option value="RGB">RGB</option>
<option value="RBG">RBG</option>
<option value="GRB">GRB</option>
<option value="GBR">GBR</option>
<option value="BRG">BRG</option>
<option value="BGR">BGR</option>
</select>
<div class="unit"></div>
</div>
<div class="row">
<label class="label" for="ledCount">LED Count</label>
<input id="ledCount" type="number" min="1" max="200" step="1" value="30" />
<div class="unit"></div>
</div>
<div class="row">
<label class="label" for="ledShift">Pixel Shift</label>
<input id="ledShift" type="number" min="-200" max="200" step="1" value="0" />
<div class="unit">px</div>
</div>
<div class="row">
<label class="label" for="ledBrightness">RGB Brightness</label>
<input id="ledBrightness" type="number" min="0" max="255" step="1" value="128" />
<div class="unit"></div>
</div>
</fieldset>
<!-- Fan / Temperature -->
<fieldset>
<legend>Fan / Temperature</legend>
<div class="row">
<label class="label" for="fanMin">Fan Min Trigger</label>
<input id="fanMin" type="number" min="70" max="100" step="1" value="75" />
<div class="unit">°C</div>
</div>
<div class="row">
<label class="label" for="fanMax">Fan Max Trigger</label>
<input id="fanMax" type="number" min="75" max="120" step="1" value="90" />
<div class="unit">°C</div>
</div>
</fieldset>
<!-- Front White Light Range -->
<fieldset>
<legend>Front White Light Range</legend>
<div class="row">
<label class="label" for="frontMin">Min %</label>
<input id="frontMin" type="number" min="1" max="100" step="1" value="10" />
<div class="unit">%</div>
</div>
<div class="row">
<label class="label" for="frontMax">Max %</label>
<input id="frontMax" type="number" min="2" max="100" step="1" value="100" />
<div class="unit">%</div>
</div>
</fieldset>
<!-- Rear White Light Range -->
<fieldset>
<legend>Rear White Light Range</legend>
<div class="row">
<label class="label" for="rearMin">Min %</label>
<input id="rearMin" type="number" min="1" max="100" step="1" value="10" />
<div class="unit">%</div>
</div>
<div class="row">
<label class="label" for="rearMax">Max %</label>
<input id="rearMax" type="number" min="2" max="100" step="1" value="100" />
<div class="unit">%</div>
</div>
</fieldset>
</section>
<!-- Decorative actions (no JS yet) -->
<section class="actions" style="justify-content:space-between;">
<div style="display:flex; gap:10px;">
<button class="btn ghost" type="button" aria-disabled="true" id="btnReboot">ReBoot Device</button>
</div>
<div style="display:flex; gap:10px;">
<button class="btn ghost" type="button" aria-disabled="true" id="btnReload">Reload</button>
<button class="btn" type="button" aria-disabled="true" id="btnSave">Save</button>
</div>
</section>
<!-- Reboot confirmation dialog (simple) -->
<div id="rebootConfirm" role="dialog" aria-modal="true"
style="display:none; position:fixed; inset:0; align-items:center; justify-content:center;">
<div
style="background:var(--card); color:var(--ink); border:1px solid var(--border); padding:18px; border-radius:12px; width:320px; box-shadow:0 10px 30px rgba(0,0,0,0.6);">
<h3 style="margin:0 0 8px;">ReBoot Device?</h3>
<p style="margin:0 0 16px; color:var(--muted);">This will reboot the connected device. Are you sure?</p>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button class="btn ghost" id="btnRebootCancel">Cancel</button>
<button class="btn" id="btnRebootYes">Yes</button>
</div>
</div>
</div>
</section>
<!-- Panel: Tools -->
<section id="panel-tools" class="panel" role="tabpanel" aria-labelledby="label-tools">
<fieldset>
<legend>Tools</legend>
<div class="row">
<label class="label" for="toolSpeed">Speed</label>
<input id="toolSpeed" type="range" min="1" max="100" step="1" value="50" style="width: 100%;" />
<div class="unit">
<button class="btn" type="button" aria-disabled="true" id="btnToolSpeed">Set</button>
</div>
</div>
<div class="row">
<label class="label" for="toolBrightness">Brightness</label>
<input id="toolBrightness" type="range" min="1" max="255" step="1" value="128" style="width: 100%;" />
<div class="unit">
<button class="btn" type="button" aria-disabled="true" id="btnToolBrightness">Set</button>
</div>
</div>
<div class="row">
<label class="label" for="toolColor">Set Color</label>
<input id="toolColor" type="color" value="#ffffff" />
<div class="unit">
<button class="btn" type="button" aria-disabled="true" id="btnToolColor">Set</button>
</div>
</div>
<div class="row">
<label class="label" for="toolAnimation">Animation</label>
<input id="toolAnimation" type="number" min="0" max="240" step="1" value="0" />
<div class="unit">
<button class="btn" type="button" aria-disabled="true" id="btnToolAnim">Set</button>
</div>
</div>
<div class="row">
<label class="label" for="toolShift">Shift</label>
<input id="toolShift" type="number" min="0" max="200" step="1" value="0" />
<div class="unit">
<button class="btn" type="button" aria-disabled="true" id="btnToolShift">Set</button>
</div>
</div>
<div class="row" style="grid-template-columns: 1fr;">
<button class="btn" type="button" aria-disabled="true" id="btnClearPixels">Clear all Pixels</button>
</div>
</fieldset>
</section>
</div>
</main>
<!-- On-page logger removed; logs are sent to console only -->
<!-- sr-only helper -->
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 1, 1);
white-space: nowrap;
border: 0;
}
</style>
<script>
const BLE_SERVER_NAME = "ATALIGHTS";
const BLE_SERVICE_UUID = 0xFFE0;
const BLE_CHARACTERISTIC1_UUID = 0xFFE1;
const BLE_CHARACTERISTIC2_UUID = 0xFFE2; // light stick characteristic
let bleDevice = null, bleCharacteristic1 = null, bleCharacteristic2 = null;
let bleConnected = false;
// 4 byte commands
const CMD_SET_SPEED = 0x03;
const CMD_SET_BRIGHTNESS = 0x2A;
const CMD_SET_STATIC_COLOR = 0x1E;
const CMD_SET_ANIMATION = 0x2C;
const CMD_SET_SHIFT = 0xB1;
// other commands
const CMD_SET_SETTINGS = 0xB0;
const CMD_REBOOT = 0x99; // Custom reboot command (device must handle this)
const ledCommand = {
data0: 0x00,
data1: 0x00,
data2: 0x00,
cmd: 0x00
}
const coreSettings = {
commandId: CMD_SET_SETTINGS,
limitedMode: document.getElementById('limited'),
temperature: 0.0,
vIn: 0.0,
profile: 0,
ledChipset1: document.getElementById('ledChipset'),
rgbOrder1: document.getElementById('rgbOrder'),
ledCount1: document.getElementById('ledCount'),
ledShift1: document.getElementById('ledShift'),
ledBrightness1: document.getElementById('ledBrightness'),
ledChipset2: document.getElementById('ledChipset'),
rgbOrder2: document.getElementById('rgbOrder'),
ledCount2: document.getElementById('ledCount'),
ledShift2: document.getElementById('ledShift'),
ledBrightness2: document.getElementById('ledBrightness'),
frontMin: document.getElementById('frontMin'),
frontMax: document.getElementById('frontMax'),
rearMin: document.getElementById('rearMin'),
rearMax: document.getElementById('rearMax'),
fanMin: document.getElementById('fanMin'),
fanMax: document.getElementById('fanMax')
}
const PACKET_LEN = 42; // Packed struct: 1+1+4+4+3+3+20 = 36 bytes, but check actual size
const OFF_COMMAND_ID = 0; // Offset of command ID in the packet
const OFF_LIMITED_MODE = 1; // Offset of limited mode in the packet
const OFF_TEMPERATURE = 2; // Offset of temperature in the packet
const OFF_VIN = 6; // Offset of VIN in the packet
const OFF_PROFILE = 10; // Offset of booth profile
const OFF_LED_CHIPSET1 = 20; // Offset of LED chip type
const OFF_RGB_ORDER1 = 21; // Offset of RGB order
const OFF_LED_COUNT1 = 25; // Offset of LED count
const OFF_LED_SHIFT1 = 26; // Offset of pixel shift
const OFF_LED_BRIGHTNESS1 = 27; // Offset of RGB brightness
const OFF_LED_CHIPSET2 = 28; // Offset of LED chip type
const OFF_RGB_ORDER2 = 29; // Offset of RGB order
const OFF_LED_COUNT2 = 33; // Offset of LED count
const OFF_LED_SHIFT2 = 34; // Offset of pixel shift
const OFF_LED_BRIGHTNESS2 = 35; // Offset of RGB brightness
const OFF_FRONT_MIN = 36; // Offset of front light minimum
const OFF_FRONT_MAX = 37; // Offset of front light maximum
const OFF_REAR_MIN = 38; // Offset of rear light minimum
const OFF_REAR_MAX = 39; // Offset of rear light maximum
const OFF_FAN_MIN = 40; // Offset of fan minimum
const OFF_FAN_MAX = 41; // Offset of fan maximum
// Mapping for RGB order strings to numeric codes (normalize input before lookup)
const RGB_MAP = { 'RGB': 0, 'RBG': 1, 'GRB': 2, 'GBR': 3, 'BRG': 4, 'BGR': 5 };
// Helper to update the BLE communication status indicator
function setBLEStatus(connected) {
const dot = document.getElementById('bleDot');
const text = document.getElementById('bleText');
if (!dot || !text) return;
if (connected) {
dot.classList.add('connected');
text.textContent = 'Connected';
} else {
dot.classList.remove('connected');
text.textContent = 'Disconnected';
}
}
async function connectToBle() {
if (!navigator.bluetooth) { logMessage('Web Bluetooth not supported.'); return; }
try {
const devNameInput = document.getElementById('deviceName');
const namePrefix = devNameInput && devNameInput.value ? devNameInput.value.trim() : BLE_SERVER_NAME;
logMessage('Requesting device with namePrefix: ' + namePrefix);
try {
bleDevice = await navigator.bluetooth.requestDevice({
filters: [{ namePrefix: namePrefix }],
optionalServices: [BLE_SERVICE_UUID]
});
} catch (filterErr) {
// Some browsers/platforms may reject filters or there may be no devices matching.
// Fall back to showing all devices so the user can pick one.
console.warn('Filtered requestDevice failed, falling back to acceptAllDevices:', filterErr);
logMessage('Filtered device request failed, showing all devices');
bleDevice = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
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 {
// 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]';
}
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.toLowerCase().includes('ready')) {
// Device is ready, request current settings
requestCurrentSettings();
}
} catch (err) {
console.error('Processing error', err);
logMessage('--> Error processing message: ' + err.message);
}
});
bleConnected = true;
setBLEStatus(true);
enableSettingsUI(true);
// After connecting, request current settings from the device and populate the form
try {
logMessage('Requesting current settings from device...');
requestCurrentSettings();
} catch (e) { console.warn('requestCurrentSettings failed', e); }
// listen for GATT server disconnects
if (bleDevice && bleDevice.gatt) {
bleDevice.addEventListener('gattserverdisconnected', onDisconnect);
}
} catch (error) {
console.error('BLE Connection Error:', error);
logMessage('BLE Connection Error: ' + error.message);
bleConnected = false;
setBLEStatus(false);
}
}
// wire Connect button
// Try to attach handler immediately; fall back to DOMContentLoaded if needed
(function attachConnect() {
const btn = document.getElementById('btnConnect');
if (btn) {
btn.addEventListener('click', async function onClickConnect(e) {
const origText = btn.textContent;
try {
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
btn.textContent = 'Connecting...';
logMessage('Connect button clicked');
await connectToBle();
logMessage('connectToBle completed');
} catch (err) { console.error('Connect handler error', err); logMessage('Connect error: ' + err.message); }
finally { btn.disabled = false; btn.setAttribute('aria-disabled', 'false'); btn.textContent = origText; }
});
return;
}
// If button not present yet, attach on DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
const btn2 = document.getElementById('btnConnect');
if (btn2) btn2.addEventListener('click', () => { connectToBle(); });
});
})();
// Enable or disable Settings/Tools UI when BLE connection state changes
function enableSettingsUI(enabled) {
const settingsRadio = document.getElementById('tab-settings');
const toolsRadio = document.getElementById('tab-tools');
const btnReload = document.getElementById('btnReload');
const btnSave = document.getElementById('btnSave');
// Tool buttons
const btnToolSpeed = document.getElementById('btnToolSpeed');
const btnToolBrightness = document.getElementById('btnToolBrightness');
const btnToolColor = document.getElementById('btnToolColor');
const btnToolAnim = document.getElementById('btnToolAnim');
const btnToolShift = document.getElementById('btnToolShift');
const btnClearPixels = document.getElementById('btnClearPixels');
const btnReboot = document.getElementById('btnReboot');
if (settingsRadio) settingsRadio.disabled = !enabled;
if (toolsRadio) toolsRadio.disabled = !enabled;
if (btnReload) btnReload.disabled = !enabled;
if (btnSave) btnSave.disabled = !enabled;
// toggle tool buttons
if (btnToolSpeed) btnToolSpeed.disabled = !enabled;
if (btnToolBrightness) btnToolBrightness.disabled = !enabled;
if (btnToolColor) btnToolColor.disabled = !enabled;
if (btnToolAnim) btnToolAnim.disabled = !enabled;
if (btnToolShift) btnToolShift.disabled = !enabled;
if (btnClearPixels) btnClearPixels.disabled = !enabled;
if (btnReboot) btnReboot.disabled = !enabled;
// also update ARIA attributes
if (btnReload) btnReload.setAttribute('aria-disabled', (!enabled).toString());
if (btnSave) btnSave.setAttribute('aria-disabled', (!enabled).toString());
// update ARIA for tool buttons
if (btnToolSpeed) btnToolSpeed.setAttribute('aria-disabled', (!enabled).toString());
if (btnToolBrightness) btnToolBrightness.setAttribute('aria-disabled', (!enabled).toString());
if (btnToolColor) btnToolColor.setAttribute('aria-disabled', (!enabled).toString());
if (btnToolAnim) btnToolAnim.setAttribute('aria-disabled', (!enabled).toString());
if (btnToolShift) btnToolShift.setAttribute('aria-disabled', (!enabled).toString());
if (btnClearPixels) btnClearPixels.setAttribute('aria-disabled', (!enabled).toString());
if (btnReboot) btnReboot.setAttribute('aria-disabled', (!enabled).toString());
}
// Provide safe fallbacks if these helper functions are defined elsewhere
if (typeof logMessage === 'undefined') {
function logMessage(msg, overwrite = false) {
// Console-only logging. Use browser DevTools to inspect messages.
if (overwrite) { console.log('[LOG][OVERWRITE]', msg); }
else { console.log('[LOG]', msg); }
}
}
if (typeof requestCurrentSettings === 'undefined') {
function requestCurrentSettings() {
// Try to read a packet from the device to populate settings
readPacket().catch(() => { });
}
}
function onDisconnect() {
bleConnected = false;
logMessage('BLE disconnected');
enableSettingsUI(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;
}
/* ================= Packet Handling ================= */
function parsePacket(data) {
if (!data || data.length === 0) return false;
//console.log('Received packet data:', data);
//console.log(`Received packet: ${data.length} bytes`);
// Prefer the full coreSettings packet when available
if (data.length >= PACKET_LEN) {
try {
// Limited mode
const limited = data[OFF_LIMITED_MODE];
if (coreSettings.limitedMode) coreSettings.limitedMode.checked = !!limited;
// Temperature (4-byte little-endian float)
try {
const tempBytes = data.slice(OFF_TEMPERATURE, OFF_TEMPERATURE + 4);
const tempView = new DataView(tempBytes.buffer, tempBytes.byteOffset, 4);
coreSettings.temperature = tempView.getFloat32(0, true);
const tempEl = document.getElementById('temp');
if (tempEl) tempEl.textContent = coreSettings.temperature.toFixed(1);
} catch (e) { console.error('temp parse', e); }
// VIN (4-byte little-endian float)
try {
const vinBytes = data.slice(OFF_VIN, OFF_VIN + 4);
const vinView = new DataView(vinBytes.buffer, vinBytes.byteOffset, 4);
coreSettings.vIn = vinView.getFloat32(0, true);
const vinEl = document.getElementById('vin');
if (vinEl) vinEl.textContent = coreSettings.vIn.toFixed(2);
} catch (e) { console.error('vin parse', e); }
// Profile (read up to 9 chars from OFF_PROFILE then match against <select id="profile">)
try {
// Build profile string from bytes (max 9 chars, 10th is null terminator)
let profStr = '';
for (let i = 0; i < 9; i++) {
const b = data[OFF_PROFILE + i];
if (!b || b === 0) break;
profStr += String.fromCharCode(b);
}
profStr = profStr.trim();
console.log('Parsed profile string:', profStr);
if (profStr) {
const profileEl = document.getElementById('profile');
if (profileEl && profileEl.options && profileEl.options.length) {
const target = profStr.substring(0, 9).toLowerCase();
console.log('Profile match target:', target);
let matchIndex = -1;
for (let i = 0; i < profileEl.options.length; i++) {
const opt = profileEl.options[i];
const val = (opt.value || '').toString().substring(0, 9).toLowerCase();
console.log(`Comparing option[${i}] value:`, val);
if (val === target) { matchIndex = i; break; }
}
if (matchIndex >= 0) profileEl.selectedIndex = matchIndex;
}
}
} catch (e) { /* ignore profile parse errors */ }
// Strip #1
try {
const chip1 = data[OFF_LED_CHIPSET1] || 0;
if (coreSettings.ledChipset1) coreSettings.ledChipset1.selectedIndex = chip1;
// RGB order stored as 3 chars + null at OFF_RGB_ORDER1..OFF_RGB_ORDER1+3
let rgb1 = '';
for (let i = 0; i < 3; i++) {
const c = data[OFF_RGB_ORDER1 + i];
if (!c || c === 0) break;
// Convert to uppercase as we read each character
rgb1 += String.fromCharCode(c).toUpperCase();
}
if (rgb1 && coreSettings.rgbOrder1) {
// Make case-insensitive by normalizing to uppercase
try {
// Try to match the option regardless of case
const rgbValue = rgb1.toUpperCase();
coreSettings.rgbOrder1.value = rgbValue;
} catch (e) { /* ignore */ }
}
const lc = data[OFF_LED_COUNT1] || 0;
if (coreSettings.ledCount1) coreSettings.ledCount1.value = String(lc);
// Set tools shift max to ledCount
const toolShiftEl = document.getElementById('toolShift');
if (toolShiftEl) toolShiftEl.max = coreSettings.ledCount1.value;
// treat shift as an unsigned byte (0..255) and clamp to 0..ledCount
let shift = Number(data[OFF_LED_SHIFT1] || 0);
const maxShift = Number(lc) || 0;
if (shift < 0) shift = 0;
if (shift > maxShift) shift = maxShift;
if (coreSettings.ledShift1) coreSettings.ledShift1.value = String(shift);
const bright = data[OFF_LED_BRIGHTNESS1] || 0;
if (coreSettings.ledBrightness1) coreSettings.ledBrightness1.value = String(bright);
} catch (e) { console.error('parse strip1', e); }
// Strip #2
/*
try{
const chip2 = data[OFF_LED_CHIPSET2] || 0;
if(coreSettings.ledChipset2) coreSettings.ledChipset2.selectedIndex = chip2;
let rgb2 = '';
for(let i=0;i<3;i++){ const c = data[OFF_RGB_ORDER2 + i]; if(!c || c === 0) break; rgb2 += String.fromCharCode(c); }
if(rgb2 && coreSettings.rgbOrder2){ try{ coreSettings.rgbOrder2.value = rgb2; }catch(e){} }
const lc2 = data[OFF_LED_COUNT2] || 0;
if(coreSettings.ledCount2) coreSettings.ledCount2.value = String(lc2);
let shift2 = data[OFF_LED_SHIFT2] || 0; if(shift2 & 0x80) shift2 = shift2 - 256;
if(coreSettings.ledShift2) coreSettings.ledShift2.value = String(shift2);
const bright2 = data[OFF_LED_BRIGHTNESS2] || 0;
if(coreSettings.ledBrightness2) coreSettings.ledBrightness2.value = String(bright2);
}catch(e){ console.error('parse strip2', e); }
*/
// Front/Rear/Fan
try {
const fm = data[OFF_FRONT_MIN] || 0; const fM = data[OFF_FRONT_MAX] || 0;
const rm = data[OFF_REAR_MIN] || 0; const rM = data[OFF_REAR_MAX] || 0;
const fanMin = data[OFF_FAN_MIN] || 0; const fanMax = data[OFF_FAN_MAX] || 0;
if (coreSettings.frontMin) coreSettings.frontMin.value = String(fm);
if (coreSettings.frontMax) coreSettings.frontMax.value = String(fM);
if (coreSettings.rearMin) coreSettings.rearMin.value = String(rm);
if (coreSettings.rearMax) coreSettings.rearMax.value = String(rM);
if (coreSettings.fanMin) coreSettings.fanMin.value = String(fanMin);
if (coreSettings.fanMax) coreSettings.fanMax.value = String(fanMax);
} catch (e) { console.error('parse front/rear/fan', e); }
logMessage('Core settings loaded');
return true;
} catch (e) { console.error('parsePacket coreSettings error', e); return false; }
}
// Backwards-compat INFO_PACK (13 bytes) handling
if (data.length >= 13) {
try {
const speed = data[3];
const bright = data[4];
const ic_model = data[5];
const count_msb = data[7];
const count_lsb = data[8];
const red = data[9];
const green = data[10];
const blue = data[11];
const ledCount = ((count_msb << 8) | count_lsb) & 0xFFFF;
try { if (coreSettings.ledBrightness1) coreSettings.ledBrightness1.value = String(bright); } catch (e) { }
try { if (coreSettings.ledChipset1) coreSettings.ledChipset1.selectedIndex = Number(ic_model); } catch (e) { }
try { if (coreSettings.ledCount1) coreSettings.ledCount1.value = String(ledCount); } catch (e) { }
try { document.getElementById('toolSpeed').value = String(speed); } catch (e) { }
try { document.getElementById('toolBrightness').value = String(bright); } catch (e) { }
try { document.getElementById('toolColor').value = '#' + [red, green, blue].map(b => b.toString(16).padStart(2, '0')).join(''); } catch (e) { }
logMessage('Legacy device settings loaded');
return true;
} catch (parseErr) { console.error('parsePacket INFO_PACK error', parseErr); return false; }
}
console.warn('Unknown packet format, length=', data.length);
return false;
}
// Send the 4-byte ledCommand over bleCharacteristic1
async function sendLedCommand() {
if (!bleCharacteristic1) { logMessage('No BLE characteristic for sending'); return; }
try {
const buf = new Uint8Array(4);
buf[0] = ledCommand.data0 & 0xFF;
buf[1] = ledCommand.data1 & 0xFF;
buf[2] = ledCommand.data2 & 0xFF;
buf[3] = ledCommand.cmd & 0xFF;
await bleCharacteristic1.writeValue(buf);
logMessage('ledCommand sent');
} catch (err) { console.error('sendLedCommand failed', err); logMessage('sendLedCommand failed: ' + err.message); }
}
// Placeholder handlers for tool buttons - implement logic here later
function handleToolSpeed() {
try {
spd = parseInt(document.getElementById('toolSpeed').value) || 50;
ledCommand.data0 = spd; ledCommand.data1 = 0; ledCommand.data2 = 0; ledCommand.cmd = CMD_SET_SPEED & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleToolSpeed', e); }
}
function handleToolBrightness() {
try {
const brightness = parseInt(document.getElementById('toolBrightness').value) || 0;
ledCommand.data0 = brightness; ledCommand.data1 = 0; ledCommand.data2 = 0; ledCommand.cmd = CMD_SET_BRIGHTNESS & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleToolBrightness', e); }
}
function handleToolColor() {
try {
const hex = document.getElementById('toolColor').value || '#ffffff';
const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16);
ledCommand.data0 = r; ledCommand.data1 = g; ledCommand.data2 = b; ledCommand.cmd = CMD_SET_STATIC_COLOR & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleToolColor', e); }
}
function handleToolAnim() {
try {
const val = parseInt(document.getElementById('toolAnimation').value) || 0;
ledCommand.data0 = val & 0xFF; ledCommand.data1 = 0; ledCommand.data2 = 0; ledCommand.cmd = CMD_SET_ANIMATION & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleToolAnim', e); }
}
function handleToolShift() {
try {
const val = parseInt(document.getElementById('toolShift').value) || 0;
ledCommand.data0 = val & 0xFF; ledCommand.data1 = val & 0xFF; ledCommand.data2 = 0; ledCommand.cmd = CMD_SET_SHIFT & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleToolShift', e); }
}
function handleClearPixels() {
try {
const r = 0; const g = 0; const b = 0;
ledCommand.data0 = r; ledCommand.data1 = g; ledCommand.data2 = b; ledCommand.cmd = CMD_SET_STATIC_COLOR & 0xFF;
sendLedCommand();
} catch (e) { console.error('handleClearPixels', e); }
}
// Reboot dialog helpers
function showRebootConfirm(show) {
const dlg = document.getElementById('rebootConfirm');
if (!dlg) return;
dlg.style.display = show ? 'flex' : 'none';
}
async function handleReboot() {
try {
// Prepare a 4-byte reboot command
ledCommand.data0 = 0x00; ledCommand.data1 = 0x00; ledCommand.data2 = 0x00; ledCommand.cmd = CMD_REBOOT & 0xFF;
await sendLedCommand();
showRebootConfirm(false);
logMessage('Reboot command sent');
} catch (e) { console.error('handleReboot', e); logMessage('Reboot failed: ' + (e && e.message ? e.message : String(e))); }
}
// Build and send the full settings packet (PACKET_LEN bytes) over bleCharacteristic1
async function sendAllSettings() {
if (!bleCharacteristic1) { logMessage('No BLE characteristic for sending settings'); return; }
try {
const buf = new Uint8Array(PACKET_LEN);
// command id
buf[OFF_COMMAND_ID] = coreSettings.commandId & 0xFF;
// limited mode
buf[OFF_LIMITED_MODE] = coreSettings.limitedMode && coreSettings.limitedMode.checked ? 1 : 0;
// temperature and VIN are read-only from device, leave as 0 in outgoing
// profile / booth profile (store up to 9 bytes, 10th is null terminator)
(function () {
const profileEl = document.getElementById('profile');
let profStr = '';
if (profileEl) {
const opt = profileEl.options[profileEl.selectedIndex];
profStr = opt && typeof opt.value === 'string' && opt.value.length > 0 ? opt.value : (opt ? opt.text : '');
}
if (typeof profStr !== 'string') profStr = String(profStr || '');
// safety check: ensure we don't write past buffer end
if (OFF_PROFILE + 9 >= buf.length) {
console.warn('OFF_PROFILE out of range for settings buffer');
} else {
const maxLen = 9; // copy up to 9 bytes
for (let i = 0; i < maxLen; i++) {
buf[OFF_PROFILE + i] = i < profStr.length ? profStr.charCodeAt(i) & 0xFF : 0;
}
// null terminator at 10th byte
buf[OFF_PROFILE + 9] = 0;
}
})();
// Strip #1 settings
// chip / chipset
const chipVal = (coreSettings.ledChipset1 && coreSettings.ledChipset1.value) ? parseInt(coreSettings.ledChipset1.value) : 0;
buf[OFF_LED_CHIPSET1] = chipVal & 0xFF;
// rgb order
// RGB order - store string directly with null terminator
const rgbOrderStr = (coreSettings.rgbOrder1 && coreSettings.rgbOrder1.value) ?
String(coreSettings.rgbOrder1.value).trim().toUpperCase() : 'RGB';
// Write each character of the RGB order string to consecutive bytes
for (let i = 0; i < 3; i++) {
buf[OFF_RGB_ORDER1 + i] = i < rgbOrderStr.length ? rgbOrderStr.charCodeAt(i) : 0;
}
// Add null terminator
buf[OFF_RGB_ORDER1 + 3] = 0;
// led count
const lc = (coreSettings.ledCount1 && coreSettings.ledCount1.value) ? parseInt(coreSettings.ledCount1.value) : 0;
buf[OFF_LED_COUNT1] = lc & 0xFF;
// shift
const shift = (coreSettings.ledShift1 && coreSettings.ledShift1.value) ? parseInt(coreSettings.ledShift1.value) : 0;
buf[OFF_LED_SHIFT1] = shift & 0xFF;
// brightness
const bright = (coreSettings.ledBrightness1 && coreSettings.ledBrightness1.value) ? parseInt(coreSettings.ledBrightness1.value) : 0;
buf[OFF_LED_BRIGHTNESS1] = bright & 0xFF;
// Strip #2 settings
// chip / chipset
const chipVal2 = (coreSettings.ledChipset2 && coreSettings.ledChipset2.value) ? parseInt(coreSettings.ledChipset2.value) : 0;
buf[OFF_LED_CHIPSET2] = chipVal2 & 0xFF;
// rgb order
// RGB order - store string directly with null terminator
const rgbOrderStr2 = (coreSettings.rgbOrder1 && coreSettings.rgbOrder1.value) ?
String(coreSettings.rgbOrder1.value).trim().toUpperCase() : 'RGB';
// Write each character of the RGB order string to consecutive bytes
for (let i = 0; i < 3; i++) {
buf[OFF_RGB_ORDER1 + i] = i < rgbOrderStr2.length ? rgbOrderStr2.charCodeAt(i) : 0;
}
// Add null terminator
buf[OFF_RGB_ORDER2 + 3] = 0;
// led count
const lc2 = (coreSettings.ledCount2 && coreSettings.ledCount2.value) ? parseInt(coreSettings.ledCount2.value) : 0;
buf[OFF_LED_COUNT2] = lc2 & 0xFF;
// shift
const shift2 = (coreSettings.ledShift2 && coreSettings.ledShift2.value) ? parseInt(coreSettings.ledShift2.value) : 0;
buf[OFF_LED_SHIFT2] = shift2 & 0xFF;
// brightness
const bright2 = (coreSettings.ledBrightness2 && coreSettings.ledBrightness2.value) ? parseInt(coreSettings.ledBrightness2.value) : 0;
buf[OFF_LED_BRIGHTNESS2] = bright2 & 0xFF;
// front/rear/fan
buf[OFF_FRONT_MIN] = (coreSettings.frontMin && coreSettings.frontMin.value) ? parseInt(coreSettings.frontMin.value) & 0xFF : 0;
buf[OFF_FRONT_MAX] = (coreSettings.frontMax && coreSettings.frontMax.value) ? parseInt(coreSettings.frontMax.value) & 0xFF : 0;
buf[OFF_REAR_MIN] = (coreSettings.rearMin && coreSettings.rearMin.value) ? parseInt(coreSettings.rearMin.value) & 0xFF : 0;
buf[OFF_REAR_MAX] = (coreSettings.rearMax && coreSettings.rearMax.value) ? parseInt(coreSettings.rearMax.value) & 0xFF : 0;
buf[OFF_FAN_MIN] = (coreSettings.fanMin && coreSettings.fanMin.value) ? parseInt(coreSettings.fanMin.value) & 0xFF : 0;
buf[OFF_FAN_MAX] = (coreSettings.fanMax && coreSettings.fanMax.value) ? parseInt(coreSettings.fanMax.value) & 0xFF : 0;
// Log buffer contents in decimal format
console.log('Settings packet by index (decimal):');
for (let i = 0; i < buf.length; i++) {
console.log(`buf[${i}] = ${buf[i]}`);
}
// Alternative format as a single object for easier inspection
const bufObj = {};
for (let i = 0; i < buf.length; i++) {
bufObj[i] = buf[i];
}
console.log('Settings packet as object:', bufObj);
await bleCharacteristic1.writeValue(buf);
logMessage('Settings packet sent');
} catch (err) { console.error('sendAllSettings failed', err); logMessage('sendAllSettings failed: ' + err.message); }
}
// Hook up Save button and example tool actions when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const btnSave = document.getElementById('btnSave');
if (btnSave) btnSave.addEventListener('click', () => { sendAllSettings(); });
const btnReload = document.getElementById('btnReload');
if (btnReload) btnReload.addEventListener('click', async () => {
try {
btnReload.disabled = true;
btnReload.setAttribute('aria-disabled', 'true');
logMessage('Reload pressed: reading settings...');
await readPacket();
logMessage('Reload complete');
} catch (e) {
console.error('btnReload handler', e);
logMessage('Reload failed: ' + (e && e.message ? e.message : String(e)));
} finally {
btnReload.disabled = false;
btnReload.setAttribute('aria-disabled', 'false');
}
});
// Wire tool buttons to send 4-byte ledCommand packets
const btnToolAnim = document.getElementById('btnToolAnim');
if (btnToolAnim) btnToolAnim.addEventListener('click', async () => { handleToolAnim(); });
const btnToolShift = document.getElementById('btnToolShift');
if (btnToolShift) btnToolShift.addEventListener('click', async () => { handleToolShift(); });
const btnToolSpeed = document.getElementById('btnToolSpeed');
if (btnToolSpeed) btnToolSpeed.addEventListener('click', async () => { handleToolSpeed(); });
const btnToolBrightness = document.getElementById('btnToolBrightness');
if (btnToolBrightness) btnToolBrightness.addEventListener('click', async () => { handleToolBrightness(); });
const btnToolColor = document.getElementById('btnToolColor');
if (btnToolColor) btnToolColor.addEventListener('click', async () => { handleToolColor(); });
const btnClearPixels = document.getElementById('btnClearPixels');
if (btnClearPixels) btnClearPixels.addEventListener('click', async () => { handleClearPixels(); });
// Reboot dialog wiring
const btnReboot = document.getElementById('btnReboot');
if (btnReboot) btnReboot.addEventListener('click', async () => { showRebootConfirm(true); });
const btnRebootCancel = document.getElementById('btnRebootCancel');
if (btnRebootCancel) btnRebootCancel.addEventListener('click', async () => { showRebootConfirm(false); });
const btnRebootYes = document.getElementById('btnRebootYes');
if (btnRebootYes) btnRebootYes.addEventListener('click', async () => { await handleReboot(); });
});
// Example: if you want to test in browser console, call setBLEStatus(true);
</script>
</body>
</html>