1324 lines
49 KiB
HTML
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> |