Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions 3RD_PARTY_LICENSES
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,18 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

==============================================
picocss/pico
==============================================

MIT License

Copyright (c) 2019-2024 Pico

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19 changes: 18 additions & 1 deletion data/css/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,21 @@ hr.section-divider {
#444 88%,
transparent
);
}
}

details > div {
padding-top: 1rem;
}

details summary[role="button"] {
background-color: #6a2525;
border-color: #7a2525;
color: #F0F1F3;
}

details summary[role="button"]:hover,
details summary[role="button"]:focus {
background-color: #7a2525;
border-color: #8a2525;
color: #F5F5F8
}
255 changes: 177 additions & 78 deletions data/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,91 +29,95 @@ <h1>Settings</h1>

<!-- WiFi settings -->
<form action="/save_wifi" method="POST">
<fieldset>
<legend>WiFi Configuration</legend>
<hr class="section-divider">
<label for="ssid_select">SSID:</label>
<select name="ssid" id="ssid_select" required>
<option value="">Scanning...</option>
</select>
<div id="custom_ssid_wrapper" style="display:none; margin-top:8px;">
<label for="ssid_custom">Custom SSID:</label>
<input type="text" name="ssid_custom" id="ssid_custom" placeholder="Enter custom SSID">
<details id="wifi_configuration_page" style="display: none;">
<summary role="button" class="outline secondary">WiFi Configuration</summary>
<div>
<label for="ssid_select">SSID:</label>
<select name="ssid" id="ssid_select" required>
<option value="">Scanning...</option>
</select>
<div id="custom_ssid_wrapper" style="display:none; margin-top:8px;">
<label for="ssid_custom">Custom SSID:</label>
<input type="text" name="ssid_custom" id="ssid_custom" placeholder="Enter custom SSID">
</div>
<label>Password <input type="password" name="pass" required></label>
<button type="submit">Save WiFi & Restart</button>
</div>
<label>Password <input type="password" name="pass" required></label>
<button type="submit">Save WiFi & Restart</button>
</fieldset>
</details>
</form>

<hr>

<!-- Settings -->
<form action="/save_config" method="POST">
<fieldset>
<legend>LED Strip</legend>
<hr class="section-divider">
<label>LED Type
<select id="ledType" name="type" required>
<option value="0">WS2812B NeoPixel (RGB)</option>
<option value="1">SK6812 NeoPixel (RGBW)</option>
<option value="2">APA102 DotStar SPI (RGB)</option>
</select>
</label>

<div id="whiteCalibration" style="display: none;">
<h5>White channel calibration</h5>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
<label>R <input type="number" name="calRed" min="0" max="255" value="160"></label>
<label>G <input type="number" name="calGreen" min="0" max="255" value="160"></label>
<label>B <input type="number" name="calBlue" min="0" max="255" value="160"></label>
<label>White <input type="number" name="calGain" min="0" max="255" value="255"></label>
<button type="button" onclick="setCalibration(255, 160, 160, 160)">Set Cold</button>
<button type="button" onclick="setCalibration(255, 176, 176, 112)">Set Neutral</button>
<details>
<summary role="button" class="outline secondary">LED Hardware</summary>
<div>
<label>LED type
<select id="ledType" name="type" required>
<option value="0">WS2812B NeoPixel (RGB)</option>
<option value="1">SK6812 NeoPixel (RGBW)</option>
<option value="2">APA102 DotStar SPI (RGB)</option>
</select>
</label>

<label>Data Pin (GPIO)
<select type="number" name="dataPin" required></select>
</label>

<label id="clockPinLabel">Clock Pin (GPIO)
<select type="number" name="clockPin"></select>
</label>

<label>Number of LEDs
<input type="number" name="numLeds" min="1" max="2000" value="16" required>
</label>

<div id="whiteCalibration" style="display: none;">
<hr class="section-divider">
<h5>White channel calibration</h5>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
<label>R <input type="number" name="calRed" min="0" max="255" value="160"></label>
<label>G <input type="number" name="calGreen" min="0" max="255" value="160"></label>
<label>B <input type="number" name="calBlue" min="0" max="255" value="160"></label>
<label>White <input type="number" name="calGain" min="0" max="255" value="255"></label>
<button type="button" onclick="setCalibration(255, 160, 160, 160)">Set Cold</button>
<button type="button" onclick="setCalibration(255, 176, 176, 112)">Set Neutral</button>
</div>
</div>
<hr class="section-divider">
</div>

<label>Data Pin (GPIO)
<input type="number" name="dataPin" min="0" max="48" value="2" required>
</label>

<label>Clock Pin (only for SPI LEDs)
<input type="number" name="clockPin" min="0" max="39" value="0">
</label>

<label>Number of LEDs
<input type="number" name="numLeds" min="1" max="2000" value="16" required>
</label>
</fieldset>

<fieldset>
<legend>Default Appearance</legend>
<label>Brightness (1–255)
<input type="number" name="brightness" min="1" max="255" value="255" oninput="this.nextElementSibling.value=this.value"><input type="range" min="1" max="255" value="255" oninput="this.previousElementSibling.value=this.value">
</label>

<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
<label>R <input type="number" name="r" min="0" max="255" value="196" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="196" oninput="this.previousElementSibling.value=this.value"></label>
<label>G <input type="number" name="g" min="0" max="255" value="32" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="32" oninput="this.previousElementSibling.value=this.value"></label>
<label>B <input type="number" name="b" min="0" max="255" value="8" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="8" oninput="this.previousElementSibling.value=this.value"></label>
</details>

<details>
<summary role="button" class="outline secondary">Light Preset</summary>
<div>
<label>Brightness (1–255)
<input type="number" name="brightness" min="1" max="255" value="255" oninput="this.nextElementSibling.value=this.value"><input type="range" min="1" max="255" value="255" oninput="this.previousElementSibling.value=this.value">
</label>

<label>Default Color
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
<label>R <input type="number" name="r" min="0" max="255" value="196" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="196" oninput="this.previousElementSibling.value=this.value"></label>
<label>G <input type="number" name="g" min="0" max="255" value="32" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="32" oninput="this.previousElementSibling.value=this.value"></label>
<label>B <input type="number" name="b" min="0" max="255" value="8" oninput="this.nextElementSibling.value=this.value"><input type="range" min="0" max="255" value="8" oninput="this.previousElementSibling.value=this.value"></label>
</div>
</label>

<label>Default Effect
<select name="effect">
<option value="0">Solid Color</option>
<option value="1">Rainbow</option>
</select>
</label>
</div>

<label>Default Effect
<select name="effect">
<option value="0">Solid Color</option>
<option value="1">Rainbow</option>
</select>
</label>
</fieldset>

<fieldset>
<legend>Misc</legend>
<hr class="section-divider">
<label>Extra MDNS tag
<input type="text" name="extraMdnsTag" maxlength="15">
</label>

</fieldset>
</details>

<details>
<summary role="button" class="outline secondary">Network</summary>
<div>
<label>Extra MDNS tag
<input type="text" name="extraMdnsTag" maxlength="15">
</label>
</div>
</details>

<button type="submit">Save Settings</button>
</form>
Expand All @@ -132,6 +136,84 @@ <h5>White channel calibration</h5>
</article>
<script>
let scanInterval = 3500;
let cfgDeviceArchitecture = "";

function setupPinValidator() {
const hardwareLimits = {
"ESP32-C6": { gpio: [0,1,2,3,4,5,6,7,8,10,15,18,19,20,21,22], spi: {6:5} },
"ESP32-S3": { gpio: [1,2,4,5,6,7,8,10,16,17,18,48], spi: {11:12} },
"ESP32-C3": { gpio: [0,1,2,3,4,5,6,7,8,10,20,21], spi: {7:6} },
"ESP8266": { gpio: [2], spi: {19:18} },
"ESP32": { gpio: null, spi: {23:18} },
"ESP32-S2": { gpio: null, spi: {19:18} }
};

const arch = (typeof cfgDeviceArchitecture !== 'undefined') ? cfgDeviceArchitecture : "";
const els = { type: document.getElementById('ledType'), clkLabel: document.getElementById('clockPinLabel') };

if (!els.type || !els.clkLabel) {
console.warn("LED Validator: Missing required DOM elements (ledType/clockPinLabel)");
return;
}

// Re-add ARIA-live to clkLabel for better accessibility during dynamic updates
els.clkLabel.setAttribute('aria-live', 'polite');

const setField = (name, opts) => {
const old = document.getElementsByName(name)[0];
if (!old) return null;

const isSel = (opts != null);
const signature = "sig_" + JSON.stringify(opts);

if ((old.tagName === 'SELECT') === isSel && old.dataset.sig === signature) return old;

// Remember focus state
const wasFocused = (document.activeElement === old);

const el = isSel ? document.createElement('select') : Object.assign(document.createElement('input'), {
type: 'number', min: 0, max: (arch.includes('8266') ? 16 : 48), step: '1'
});

el.name = name; el.id = old.id; el.required = true;
el.dataset.sig = signature;

if (isSel) {
opts.forEach(p => el.add(new Option(`GPIO ${p}`, p)));
el.value = opts.includes(parseInt(old.value)) ? old.value : opts[0];
// If only one option, disable for clarity and add tooltip
if (opts.length === 1) {
el.disabled = true;
el.title = "Only one valid GPIO for this mode/architecture";
}
} else {
el.value = old.value || 0;
}

old.replaceWith(el);
el.addEventListener('change', updateUI);

if (wasFocused) el.focus(); // Restore focus
return el;
};

function updateUI() {
const isSpi = els.type.value == "2";
const cfg = hardwareLimits[arch];

let validPins = cfg ? (isSpi ? Object.keys(cfg.spi).map(Number) : cfg.gpio) : null;
const dataPinEditor = setField('dataPin', validPins);

const autoClk = (cfg && cfg.spi) ? (cfg.spi[dataPinEditor.value] ?? null) : null;
const clockPinEditor = setField('clockPin', ((autoClk !== null) ? [autoClk] : null));

els.clkLabel.style.display = isSpi ? 'block' : 'none';
clockPinEditor.disabled = clockPinEditor.disabled || !isSpi;
}

els.type.onchange = updateUI;
updateUI();
}

function setCalibration(gain, r, g, b) {
const fields = { 'calGain': gain, 'calRed': r, 'calGreen': g, 'calBlue': b };
Expand Down Expand Up @@ -227,16 +309,30 @@ <h5>White channel calibration</h5>
const c = await res.json();
const l = c.config;

const archField = l["architecture"];
if (archField !== undefined){
cfgDeviceArchitecture = archField;
}

const fields = ['type', 'dataPin', 'clockPin', 'numLeds', 'brightness', 'r', 'g', 'b', 'effect', 'extraMdnsTag', 'calGain', 'calRed', 'calGreen', 'calBlue'];
fields.forEach(field => {
const el = document.querySelector(`[name="${field}"]`);
if (el && l[field] !== undefined){
if (field == 'dataPin' || field == 'clockPin')
{
el.add(new Option(`GPIO ${l[field]}`, l[field]));
}
el.value = l[field];
el.dispatchEvent(new Event('input'));
}
});

if (l["apMode"] === true) {
document.getElementById("wifi_configuration_page").style.display = "block";
}

toggleCalibration();
setupPinValidator();
}
catch (e) {
console.error("Config load failed:", e);
Expand Down Expand Up @@ -281,7 +377,10 @@ <h5>White channel calibration</h5>
msg = document.createElement('small');
msg.id = 'wifi-error-msg';
msg.className = 'form-error-msg';
document.querySelector('form[action="/save_wifi"] fieldset').appendChild(msg);
const container = document.querySelector('#wifi-section > div') || document.querySelector('#wifi-section');
if (container) {
container.appendChild(msg);
}
}
msg.textContent = 'Custom SSID is required';
customInput.classList.add('error-active');
Expand Down
Loading