Update preset test scheduling UI

This commit is contained in:
mikecarper
2026-09-23 19:11:41 -07:00
parent 28c7141f08
commit aa4c2e3dab
5 changed files with 356 additions and 99 deletions
+115 -24
View File
@@ -2,8 +2,6 @@
"use strict";
const DEFAULTS = Object.freeze({
start: "2026-09-21T17:00:00-07:00",
end: "2026-09-23T17:00:00-07:00",
tz: "",
freq: "910.1",
bw: "500",
@@ -17,6 +15,10 @@
const SCHEDULE_HORIZON_MS = 0x7fffffff;
const SCHEDULER_EPOCH_MAX = 0xffffffff;
const EARLY_JOIN_MS = 60 * 60 * 1000;
const DEFAULT_START_HOUR = 17;
const DEFAULT_WINDOW_DAYS = 1;
const SCHEDULE_MODE_RELATIVE = "relative";
const SCHEDULE_MODE_ABSOLUTE = "absolute";
const CLOCK_RESET_COMMAND = "clkreboot";
const TIMEZONE_BOUNDARY_PATH = "../_data/timezones-2025b-simplified.json";
const TIMEZONE_MAP_STYLE = Object.freeze({
@@ -202,7 +204,7 @@
function hasPresetParameters(search) {
const params = new URLSearchParams(search || "");
return Object.keys(DEFAULTS).some(function (key) {
return ["start", "end"].concat(Object.keys(DEFAULTS)).some(function (key) {
return params.has(key);
});
}
@@ -229,7 +231,28 @@
});
}
function configFromSearch(search, fallbackTimeZone) {
function calendarDayValue(parts, daysLater) {
const value = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + daysLater));
return String(value.getUTCFullYear()).padStart(4, "0") + "-" +
twoDigits(value.getUTCMonth() + 1) + "-" + twoDigits(value.getUTCDate());
}
function defaultWindow(timeZone, nowMs) {
const zone = validateTimeZone(timeZone);
const now = nowMs == null ? Date.now() : Number(nowMs);
if (!Number.isFinite(now)) throw new PresetTestError("browser clock is unavailable");
const today = zonedParts(now, zone);
const startText = calendarDayValue(today, 1) + "T" +
twoDigits(DEFAULT_START_HOUR) + ":00";
const endText = calendarDayValue(today, 1 + DEFAULT_WINDOW_DAYS) + "T" +
twoDigits(DEFAULT_START_HOUR) + ":00";
return Object.freeze({
startMs: localDateTimeToMs(startText, zone, "default start"),
endMs: localDateTimeToMs(endText, zone, "default end"),
});
}
function configFromSearch(search, fallbackTimeZone, nowMs) {
const params = new URLSearchParams(search || "");
const raw = {};
Object.keys(DEFAULTS).forEach(function (key) {
@@ -239,9 +262,14 @@
raw.tz = fallbackTimeZone || browserTimeZone();
}
const startMs = parseTimestamp(raw.start, "start");
const endMs = parseTimestamp(raw.end, "end");
const tz = validateTimeZone(raw.tz);
const generatedWindow = defaultWindow(tz, nowMs);
let startMs = params.has("start")
? parseTimestamp(params.get("start"), "start") : generatedWindow.startMs;
let endMs = params.has("end")
? parseTimestamp(params.get("end"), "end") : generatedWindow.endMs;
if (params.has("start") && !params.has("end")) endMs = startMs + 86400000;
if (!params.has("start") && params.has("end")) startMs = endMs - 86400000;
const freq = strictNumber(raw.freq, "freq");
const bw = strictNumber(raw.bw, "bw");
const sf = strictInteger(raw.sf, "sf");
@@ -289,14 +317,17 @@
}
function isDefaultPreset(config) {
const startLocal = zonedInputValue(config.startMs, config.tz);
const endLocal = zonedInputValue(config.endMs, config.tz);
const startParts = zonedParts(config.startMs, config.tz);
return (
config.startMs === parseTimestamp(DEFAULTS.start, "start") &&
config.endMs === parseTimestamp(DEFAULTS.end, "end") &&
config.freq === Number(DEFAULTS.freq) &&
config.bw === Number(DEFAULTS.bw) &&
config.sf === Number(DEFAULTS.sf) &&
config.cr === Number(DEFAULTS.cr) &&
config.tx === Number(DEFAULTS.tx)
config.tx === Number(DEFAULTS.tx) &&
startLocal.endsWith("T17:00") &&
endLocal === calendarDayValue(startParts, DEFAULT_WINDOW_DAYS) + "T17:00"
);
}
@@ -354,30 +385,47 @@
reason: "The end is outside the firmware's roughly 24-day horizon; return closer to the test.",
};
}
return { available: true, reason: "Ready to queue after the node clock is verified." };
return { available: true, reason: "Ready to queue." };
}
function primaryScheduleAvailability(config, nowMs) {
if (nowMs >= config.startMs) {
return { available: false, reason: "The start time has passed; use the immediate option." };
}
return { available: true, reason: "Ready to queue after the node clock is verified." };
return { available: true, reason: "Ready to queue." };
}
function commandsFor(config, nowMs, clockOffsetSeconds) {
function relativeMinutes(targetMs, nowMs) {
return Math.max(1, Math.ceil((targetMs - nowMs) / 60000));
}
function commandsFor(config, nowMs, clockOffsetSeconds, scheduleMode) {
const tuple = [config.freqText, config.bwText, config.sf, config.cr].join(",");
const minutes = remainingMinutes(config, nowMs);
const scheduled = schedulerEpochs(config, clockOffsetSeconds);
const mode = scheduleMode || SCHEDULE_MODE_RELATIVE;
if (mode !== SCHEDULE_MODE_RELATIVE && mode !== SCHEDULE_MODE_ABSOLUTE) {
throw new PresetTestError("schedule mode must be relative or absolute");
}
let startArgument;
let endArgument;
if (mode === SCHEDULE_MODE_RELATIVE) {
startArgument = "+" + relativeMinutes(config.startMs, nowMs);
endArgument = "+" + relativeMinutes(config.endMs, nowMs);
} else {
const scheduled = schedulerEpochs(config, clockOffsetSeconds);
startArgument = String(scheduled.startEpoch);
endArgument = String(scheduled.endEpoch);
}
return Object.freeze({
stockNow: "tempradio " + tuple + "," + minutes,
companionNow:
"set radio2.cross on\nset tempradio2 " + tuple + ",rxtx," + minutes,
primaryScheduled:
"set tempradioat " + tuple + "," + scheduled.startEpoch + "," +
scheduled.endEpoch + "\nget tempradioat",
"set tempradioat " + tuple + "," + startArgument + "," +
endArgument + "\nget tempradioat",
companionScheduled:
"set radio2.cross on\nset tempradioat2 " + tuple + ",rxtx," +
scheduled.startEpoch + "," + scheduled.endEpoch + "\nget tempradioat2",
startArgument + "," + endArgument + "\nget tempradioat2",
stockCancelDuring: "tempradio " + tuple + ",1",
stockLeaveIn30: "tempradio " + tuple + ",30",
primaryCancel:
@@ -824,18 +872,32 @@
if (builderDisclosure) builderDisclosure.open = !showTest;
let activeClockOffsetSeconds = 0;
let scheduleMode = SCHEDULE_MODE_RELATIVE;
function setScheduledCommands() {
function setScheduledCommands(nowMs) {
const staticCommands = commandsFor(
config,
config.startMs,
activeClockOffsetSeconds
nowMs == null ? Date.now() : nowMs,
activeClockOffsetSeconds,
scheduleMode
);
setCommand(root, "primary-scheduled", staticCommands.primaryScheduled);
setCommand(root, "companion-scheduled", staticCommands.companionScheduled);
return staticCommands;
}
function setScheduleMode(nextMode) {
scheduleMode = nextMode === SCHEDULE_MODE_ABSOLUTE
? SCHEDULE_MODE_ABSOLUTE : SCHEDULE_MODE_RELATIVE;
const absoluteControls = root.querySelector('[data-role="absolute-clock-controls"]');
if (absoluteControls) absoluteControls.hidden = scheduleMode !== SCHEDULE_MODE_ABSOLUTE;
const relativeNote = root.querySelector('[data-role="relative-schedule-note"]');
if (relativeNote) relativeNote.hidden = scheduleMode !== SCHEDULE_MODE_RELATIVE;
const absoluteNote = root.querySelector('[data-role="absolute-schedule-note"]');
if (absoluteNote) absoluteNote.hidden = scheduleMode !== SCHEDULE_MODE_ABSOLUTE;
setScheduledCommands(Date.now());
}
function setNodeClockStatus(message, state) {
const status = root.querySelector('[data-role="node-clock-status"]');
if (!status) return;
@@ -848,7 +910,7 @@
const text = input ? input.value : "";
if (!String(text).trim()) {
activeClockOffsetSeconds = 0;
setScheduledCommands();
setScheduledCommands(Date.now());
setNodeClockStatus(
"No node-clock correction is applied. Scheduled commands use the normal UTC epochs.",
"normal"
@@ -860,7 +922,7 @@
const offset = nodeClockOffsetSeconds(nodeClockEpoch, Date.now());
schedulerEpochs(config, offset);
activeClockOffsetSeconds = offset;
setScheduledCommands();
setScheduledCommands(Date.now());
const direction = offset === 0
? "matches browser UTC to the minute"
: "is " + formatClockOffset(offset) + (offset > 0 ? " ahead of" : " behind") +
@@ -901,7 +963,7 @@
" window. Saved primary settings return automatically at the end."
);
const staticCommands = setScheduledCommands();
const staticCommands = setScheduledCommands(Date.now());
setCommand(root, "stock-leave-30", staticCommands.stockLeaveIn30);
setCommand(root, "primary-cancel", staticCommands.primaryCancel);
setCommand(root, "companion-cancel-before", staticCommands.companionCancelBefore);
@@ -911,6 +973,13 @@
}
if (showTest) {
root.querySelectorAll('input[name="schedule-command-mode"]').forEach(function (input) {
input.addEventListener("change", function () {
if (input.checked) setScheduleMode(input.value);
});
});
setScheduleMode(SCHEDULE_MODE_RELATIVE);
const nodeClockInput = root.querySelector('[data-role="node-clock-input"]');
const applyNodeClockButton = root.querySelector('[data-action="apply-node-clock"]');
if (nodeClockInput) {
@@ -928,6 +997,9 @@
const generator = root.querySelector('[data-role="url-generator"]');
if (generator) {
const sourceParams = new URLSearchParams(search || "");
const followsDefaultWindow = !sourceParams.has("start") && !sourceParams.has("end");
let timeFieldsEdited = false;
generator.elements.start.value = zonedInputValue(config.startMs, config.tz);
generator.elements.end.value = zonedInputValue(config.endMs, config.tz);
generator.elements.tz.value = config.tz;
@@ -987,9 +1059,19 @@
event.preventDefault();
generateUrl();
});
[generator.elements.start, generator.elements.end].forEach(function (input) {
input.addEventListener("input", function () { timeFieldsEdited = true; });
});
generator.addEventListener("change", generateUrl);
generateUrl();
initTimeZoneMap(root, config.tz, generateUrl);
initTimeZoneMap(root, config.tz, function (zone) {
if (followsDefaultWindow && !timeFieldsEdited) {
const nextWindow = defaultWindow(zone, Date.now());
generator.elements.start.value = zonedInputValue(nextWindow.startMs, zone);
generator.elements.end.value = zonedInputValue(nextWindow.endMs, zone);
}
generateUrl();
});
}
root.querySelectorAll("[data-copy-command]").forEach(function (button) {
@@ -1011,7 +1093,11 @@
const status = root.querySelector('[data-role="status"]');
const primarySchedule = primaryScheduleAvailability(config, nowMs);
const schedule = scheduleAvailability(config, nowMs);
const commands = commandsFor(config, nowMs, activeClockOffsetSeconds);
const commands = commandsFor(
config, nowMs, activeClockOffsetSeconds, scheduleMode
);
setCommand(root, "primary-scheduled", commands.primaryScheduled);
setCommand(root, "companion-scheduled", commands.companionScheduled);
status.dataset.state = phase;
if (phase === "before") {
@@ -1111,6 +1197,10 @@
SCHEDULE_HORIZON_MS: SCHEDULE_HORIZON_MS,
SCHEDULER_EPOCH_MAX: SCHEDULER_EPOCH_MAX,
EARLY_JOIN_MS: EARLY_JOIN_MS,
DEFAULT_START_HOUR: DEFAULT_START_HOUR,
DEFAULT_WINDOW_DAYS: DEFAULT_WINDOW_DAYS,
SCHEDULE_MODE_RELATIVE: SCHEDULE_MODE_RELATIVE,
SCHEDULE_MODE_ABSOLUTE: SCHEDULE_MODE_ABSOLUTE,
CLOCK_RESET_COMMAND: CLOCK_RESET_COMMAND,
PresetTestError: PresetTestError,
parseNodeClock: parseNodeClock,
@@ -1127,6 +1217,7 @@
presetEyebrow: presetEyebrow,
validateTimeZone: validateTimeZone,
browserTimeZone: browserTimeZone,
defaultWindow: defaultWindow,
hasPresetParameters: hasPresetParameters,
supportedTimeZones: supportedTimeZones,
phaseAt: phaseAt,
+70 -10
View File
@@ -393,19 +393,53 @@
content: "Hide advanced";
}
.preset-test-schedule-mode fieldset {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.65rem;
margin: 0;
border: 0;
padding: 0;
}
.preset-test-schedule-mode legend {
grid-column: 1 / -1;
margin-bottom: 0.35rem;
color: var(--md-default-fg-color--light);
font-size: 0.74rem;
font-weight: 700;
}
.preset-test-schedule-mode label {
display: flex;
gap: 0.55rem;
align-items: flex-start;
border: 1px solid var(--preset-border);
border-radius: 0.5rem;
padding: 0.75rem;
cursor: pointer;
}
.preset-test-schedule-mode input {
flex: 0 0 auto;
margin-top: 0.2rem;
accent-color: var(--preset-accent);
}
.preset-test-schedule-mode label:has(input:checked) {
border-color: var(--preset-accent);
background: color-mix(in srgb, var(--preset-accent) 8%, transparent);
}
.preset-test-generator-disclosure-body {
padding: 0 1rem 1rem;
}
.preset-test-generator-layout {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(15rem, 2fr);
gap: 1rem;
align-items: start;
min-width: 0;
}
.preset-test-generator,
.preset-test-estimates {
.preset-test-generator {
min-width: 0;
border: 1px solid var(--preset-border);
border-radius: 0.65rem;
@@ -413,12 +447,34 @@
background: var(--md-default-bg-color);
}
.preset-test-generator-fields {
.preset-test-time-fields,
.preset-test-radio-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.8rem;
}
.preset-test-time-fields {
width: 100%;
margin-bottom: 1rem;
}
.preset-test-radio-layout {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(15rem, 2fr);
gap: 1rem;
align-items: start;
margin-top: 1rem;
}
.preset-test-estimates {
min-width: 0;
border: 1px solid var(--preset-border);
border-radius: 0.65rem;
padding: 1rem;
background: var(--md-default-bg-color);
}
.preset-test-generator label {
display: grid;
align-content: start;
@@ -443,7 +499,6 @@
}
.preset-test-timezone-picker {
grid-column: 1 / -1;
min-width: 0;
}
@@ -614,8 +669,13 @@
.preset-test-grid--clock,
.preset-test-clock-check,
.preset-test-clock-override,
.preset-test-generator-layout,
.preset-test-generator-fields {
.preset-test-time-fields,
.preset-test-radio-layout,
.preset-test-radio-fields {
grid-template-columns: 1fr;
}
.preset-test-schedule-mode fieldset {
grid-template-columns: 1fr;
}
+89 -54
View File
@@ -9,7 +9,7 @@ create a shareable temporary-radio test URL.</span>
<div data-role="content">
<div data-role="test-content" hidden>
<section class="preset-test-hero" aria-labelledby="preset-test-title">
<p class="preset-test-eyebrow" data-role="preset-test-eyebrow">MeshCore · default 48-hour temporary preset test</p>
<p class="preset-test-eyebrow" data-role="preset-test-eyebrow">MeshCore · one-day temporary preset test</p>
<div class="preset-test-hero-row">
<div>
<h2 id="preset-test-title">
@@ -61,13 +61,36 @@ create a shareable temporary-radio test URL.</span>
<div data-role="test-content-footer" hidden>
<details class="preset-test-generator-disclosure preset-test-keymind-disclosure"
data-role="keymind-disclosure">
<summary><h2>KeyMind firmware and advanced commands</h2></summary>
<summary><h2>KeyMind Cascade firmware and advanced commands</h2></summary>
<div class="preset-test-generator-disclosure-body">
<p>
Use these only if your firmware supports the listed commands or you need
dual-radio operation, scheduling, or clock correction.
</p>
<p>Firmware schedule epochs: <code data-role="epoch-range"></code></p>
<section class="preset-test-schedule-mode" aria-labelledby="schedule-mode-title">
<h2 id="schedule-mode-title">Scheduled command timing</h2>
<fieldset>
<legend>Choose the syntax supported by the node</legend>
<label>
<input type="radio" name="schedule-command-mode" value="relative" checked>
<span><strong>KeyMind Cascade · <code>+minutes</code></strong> (default)</span>
</label>
<label>
<input type="radio" name="schedule-command-mode" value="absolute">
<span><strong>Absolute Unix time</strong></span>
</label>
</fieldset>
<p data-role="relative-schedule-note">
Relative commands calculate whole minutes from now to the shared start
and end. Both offsets use the node's same command-time snapshot, so no
clock correction is needed. Copy the command shortly after it is shown.
</p>
<p data-role="absolute-schedule-note" hidden>
Absolute commands use fixed UTC epochs. Verify or compensate for the
node clock with the controls below before queuing them.
</p>
</section>
<section class="preset-test-warning" aria-labelledby="continuity-plan-title">
<h2 id="continuity-plan-title">Bridges and early-revert plan</h2>
@@ -90,6 +113,8 @@ create a shareable temporary-radio test URL.</span>
</p>
</section>
<div data-role="absolute-clock-controls" hidden>
<p>Firmware schedule epochs: <code data-role="epoch-range"></code></p>
<section aria-labelledby="clock-check-title">
<h2 id="clock-check-title">Check the node clock before scheduling</h2>
<p>
@@ -188,6 +213,7 @@ clock</code></pre>
reboots.
</p>
</section>
</div>
<section aria-labelledby="join-now-title">
<h2 id="join-now-title">Companion: use both frequencies</h2>
@@ -218,11 +244,11 @@ clock</code></pre>
<p>
A Simple Repeater build can schedule its primary radio with
<code>tempradioat</code>. A Companion can instead schedule its second
profile with <code>tempradioat2</code>. Both commands use the exact UTC
epochs below, switch at the common start, and restore saved settings at
the common end. A node-clock conversion above replaces only those command
epochs with its per-node translated values. Room-server and sensor roles
use the stock join command above instead.
profile with <code>tempradioat2</code>. The default KeyMind Cascade form
uses <code>+minutes</code> for both endpoints. Switch to Absolute Unix time
above for fixed UTC epochs and the node-clock correction tools. Both forms
switch at the common start and restore saved settings at the common end.
Room-server and sensor roles use the stock join command above instead.
</p>
<div class="preset-test-grid">
@@ -348,12 +374,14 @@ clock</code></pre>
Enter the test times in the selected time zone and choose the radio
tuple. The generated URL converts the times to exact UTC instants, keeps
the date punctuation readable, and keeps <code>tz</code> so the page
displays them in the organizer's local time zone.
displays them in the organizer's local time zone. With no URL settings,
the builder starts at 5:00 PM tomorrow and ends at 5:00 PM the following
day in the selected time zone.
</p>
<div class="preset-test-generator-layout">
<form class="preset-test-generator" data-role="url-generator">
<div class="preset-test-generator-fields">
<div class="preset-test-time-fields">
<label>
<span>Start date and time</span>
<input type="datetime-local" name="start" step="60" required>
@@ -362,35 +390,40 @@ clock</code></pre>
<span>End date and time</span>
<input type="datetime-local" name="end" step="60" required>
</label>
<div class="preset-test-timezone-picker">
<div class="preset-test-timezone-toolbar">
<div>
<span>Selected time zone</span>
<strong data-role="selected-time-zone">Detecting browser time zone…</strong>
</div>
<button type="button" data-action="use-browser-time-zone">
Use browser time zone
</button>
</div>
<div class="preset-test-timezone-picker">
<div class="preset-test-timezone-toolbar">
<div>
<span>Selected time zone</span>
<strong data-role="selected-time-zone">Detecting browser time zone…</strong>
</div>
<input type="hidden" name="tz" required>
<div
class="preset-test-timezone-map"
data-role="timezone-map"
aria-label="Interactive world map for selecting a time zone"
></div>
<p class="preset-test-timezone-status" data-role="timezone-map-status" aria-live="polite">
Loading time zone map…
</p>
<small>
Click a region to select its IANA time zone. The initial selection
comes from <code>tz=</code> when present; otherwise it uses your
browser's time zone. Map design inspired by
<a href="https://zones.arilyn.cc/" target="_blank" rel="noopener">zones.arilyn.cc</a>;
boundaries from
<a href="https://github.com/evansiroky/timezone-boundary-builder" target="_blank" rel="noopener">Timezone Boundary Builder</a>
and © OpenStreetMap contributors.
</small>
<button type="button" data-action="use-browser-time-zone">
Use browser time zone
</button>
</div>
<input type="hidden" name="tz" required>
<div
class="preset-test-timezone-map"
data-role="timezone-map"
aria-label="Interactive world map for selecting a time zone"
></div>
<p class="preset-test-timezone-status" data-role="timezone-map-status" aria-live="polite">
Loading time zone map…
</p>
<small>
Click a region to select its IANA time zone. The initial selection
comes from <code>tz=</code> when present; otherwise it uses your
browser's time zone. Map design inspired by
<a href="https://zones.arilyn.cc/" target="_blank" rel="noopener">zones.arilyn.cc</a>;
boundaries from
<a href="https://github.com/evansiroky/timezone-boundary-builder" target="_blank" rel="noopener">Timezone Boundary Builder</a>
and © OpenStreetMap contributors.
</small>
</div>
<div class="preset-test-radio-layout">
<div class="preset-test-radio-fields">
<div class="preset-test-generator-subheading">Test radio profile</div>
<label>
<span>Frequency (MHz)</span>
@@ -422,26 +455,28 @@ clock</code></pre>
<input type="number" name="tx" min="-30" max="60" step="0.1" required>
<small>This estimate-only value does not change the TempRadio commands.</small>
</label>
</div>
<aside class="preset-test-estimates" aria-labelledby="radio-estimates-title">
<h3 id="radio-estimates-title">Radio estimates</h3>
<dl>
<div><dt>Nominal LoRa bitrate</dt><dd data-role="estimate-rate">—</dd></div>
<div><dt>Estimated sensitivity</dt><dd data-role="estimate-sensitivity">—</dd></div>
<div><dt>TX output used</dt><dd data-role="estimate-tx">—</dd></div>
<div><dt>Estimated link budget</dt><dd data-role="estimate-budget">—</dd></div>
</dl>
<p class="preset-test-note">
The bitrate is the nominal LoRa physical-layer rate; usable payload
throughput is lower. Sensitivity assumes a 6 dB receiver noise figure
and the standard LoRa SNR threshold for the selected spreading factor.
Link budget is TX output minus that sensitivity, before antenna gain,
cable loss, path loss, interference, and implementation differences.
</p>
</aside>
</div>
<button type="submit">Generate test URL</button>
</form>
<aside class="preset-test-estimates" aria-labelledby="radio-estimates-title">
<h3 id="radio-estimates-title">Radio estimates</h3>
<dl>
<div><dt>Nominal LoRa bitrate</dt><dd data-role="estimate-rate">—</dd></div>
<div><dt>Estimated sensitivity</dt><dd data-role="estimate-sensitivity">—</dd></div>
<div><dt>TX output used</dt><dd data-role="estimate-tx">—</dd></div>
<div><dt>Estimated link budget</dt><dd data-role="estimate-budget">—</dd></div>
</dl>
<p class="preset-test-note">
The bitrate is the nominal LoRa physical-layer rate; usable payload
throughput is lower. Sensitivity assumes a 6 dB receiver noise figure
and the standard LoRa SNR threshold for the selected spreading factor.
Link budget is TX output minus that sensitivity, before antenna gain,
cable loss, path loss, interference, and implementation differences.
</p>
</aside>
</div>
<p class="preset-test-error preset-test-generator-error"
+2 -2
View File
@@ -22,7 +22,7 @@ extra_css:
- _stylesheets/telemetry_decoder.css
- _stylesheets/management_decoder.css
- _stylesheets/filter_tool.css
- _stylesheets/preset_test.css?v=20260922-1
- _stylesheets/preset_test.css?v=20260923-1
extra_javascript:
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
@@ -30,4 +30,4 @@ extra_javascript:
- _javascript/telemetry_decoder.js
- _javascript/management_decoder.js
- _javascript/filter_tool.js
- _javascript/preset_test.js?v=20260922-1
- _javascript/preset_test.js?v=20260923-1
+80 -9
View File
@@ -51,6 +51,13 @@ assert.match(pageSource, /data-action="apply-node-clock"/);
assert.match(pageSource, /data-role="node-clock-status"/);
assert.match(pageSource, /data-command="primary-scheduled"/);
assert.match(pageSource, /data-command="primary-cancel"/);
assert.match(pageSource, /name="schedule-command-mode" value="relative" checked/);
assert.match(pageSource, /KeyMind Cascade · <code>\+minutes<\/code>/);
assert.match(pageSource, /name="schedule-command-mode" value="absolute"/);
assert.match(pageSource, /data-role="absolute-clock-controls" hidden/);
assert.match(pageSource, /class="preset-test-time-fields"/);
assert.match(pageSource, /class="preset-test-radio-layout"/);
assert.match(pageSource, /class="preset-test-radio-fields"/);
assert.match(pageSource, /Simple Repeater primary radio/);
assert.doesNotMatch(pageSource, /Stock firmware has no <code>tempradioat<\/code> command/);
assert.doesNotMatch(pageSource, /Normal return profile/);
@@ -61,7 +68,8 @@ const query =
"&tz=America%2FLos_Angeles" +
"&freq=910.1&bw=500&sf=8&cr=7";
const config = tool.configFromSearch(query);
const defaults = tool.configFromSearch("", "America/Los_Angeles");
const defaultNow = Date.parse("2026-09-22T08:00:00.000Z");
const defaults = tool.configFromSearch("", "America/Los_Angeles", defaultNow);
assert.strictEqual(tool.hasPresetParameters(""), false);
assert.strictEqual(tool.hasPresetParameters("?"), false);
@@ -107,8 +115,15 @@ assert.strictEqual(config.sf, 8);
assert.strictEqual(config.cr, 7);
assert.strictEqual(config.tz, "America/Los_Angeles");
assert.strictEqual(config.tx, 22);
assert.strictEqual(defaults.startEpoch, config.startEpoch);
assert.strictEqual(defaults.endEpoch, config.endEpoch);
assert.strictEqual(
defaults.startMs,
Date.parse("2026-09-23T17:00:00-07:00")
);
assert.strictEqual(
defaults.endMs,
Date.parse("2026-09-24T17:00:00-07:00")
);
assert.strictEqual(defaults.endEpoch - defaults.startEpoch, 24 * 60 * 60);
assert.strictEqual(defaults.freq, config.freq);
assert.strictEqual(defaults.bw, 500);
assert.strictEqual(defaults.sf, 8);
@@ -116,9 +131,26 @@ assert.strictEqual(defaults.cr, 7);
assert.strictEqual(defaults.tx, 22);
assert.strictEqual(defaults.tz, config.tz);
assert.strictEqual(tool.isDefaultPreset(defaults), true);
assert.strictEqual(tool.isDefaultPreset(config), false);
assert.strictEqual(tool.presetPageTitle(defaults), "Default temporary radio test · 910.1 MHz");
assert.match(tool.presetPageSummary(defaults), /910\.1 MHz, 500 kHz, SF8, CR7, 22 dBm/);
assert.strictEqual(tool.presetEyebrow(defaults), "MeshCore · default 48-hour temporary preset test");
assert.strictEqual(tool.presetEyebrow(defaults), "MeshCore · default 24-hour temporary preset test");
const dstDefault = tool.configFromSearch(
"?tz=America%2FLos_Angeles",
"UTC",
Date.parse("2026-03-06T08:00:00.000Z")
);
assert.strictEqual(
tool.zonedInputValue(dstDefault.startMs, dstDefault.tz),
"2026-03-07T17:00"
);
assert.strictEqual(
tool.zonedInputValue(dstDefault.endMs, dstDefault.tz),
"2026-03-08T17:00"
);
assert.strictEqual(dstDefault.endMs - dstDefault.startMs, 23 * 60 * 60 * 1000);
assert.strictEqual(tool.isDefaultPreset(dstDefault), true);
const requestedLink = tool.configFromSearch(
"?start=2026-09-22T00:00:00.000Z&end=2026-09-24T00:00:00.000Z" +
@@ -135,6 +167,15 @@ assert.strictEqual(
);
assert.strictEqual(
tool.commandsFor(requestedLink, requestedLink.startMs).primaryScheduled,
"set tempradioat 911.3,500,8,7,+1,+2880\nget tempradioat"
);
assert.strictEqual(
tool.commandsFor(
requestedLink,
requestedLink.startMs,
0,
tool.SCHEDULE_MODE_ABSOLUTE
).primaryScheduled,
"set tempradioat 911.3,500,8,7,1790035200,1790208000\nget tempradioat"
);
@@ -151,7 +192,8 @@ assert.deepStrictEqual(adjustedEpochs, {
const adjustedCommands = tool.commandsFor(
clockAdjustedLink,
clockAdjustedLink.startMs,
clockOffset
clockOffset,
tool.SCHEDULE_MODE_ABSOLUTE
);
assert.strictEqual(
adjustedCommands.primaryScheduled,
@@ -178,10 +220,12 @@ assert.throws(
/outside the firmware range/
);
const browserZoneFallback = tool.configFromSearch("", "America/New_York");
const browserZoneFallback = tool.configFromSearch(
"", "America/New_York", defaultNow
);
assert.strictEqual(browserZoneFallback.tz, "America/New_York");
assert.strictEqual(
tool.configFromSearch("?tz=UTC", "America/New_York").tz,
tool.configFromSearch("?tz=UTC", "America/New_York", defaultNow).tz,
"UTC"
);
assert.doesNotThrow(() => tool.validateTimeZone(tool.browserTimeZone()));
@@ -218,7 +262,7 @@ const commands = tool.commandsFor(config, active);
assert.strictEqual(commands.stockNow, "tempradio 910.1,500,8,7,2880");
assert.strictEqual(
commands.primaryScheduled,
"set tempradioat 910.1,500,8,7,1790035200,1790208000\nget tempradioat"
"set tempradioat 910.1,500,8,7,+1,+2880\nget tempradioat"
);
assert.strictEqual(commands.primaryCancel, "get tempradioat\ndel tempradioat all");
assert.strictEqual(
@@ -228,7 +272,7 @@ assert.strictEqual(
assert.strictEqual(
commands.companionScheduled,
"set radio2.cross on\n" +
"set tempradioat2 910.1,500,8,7,rxtx,1790035200,1790208000\n" +
"set tempradioat2 910.1,500,8,7,rxtx,+1,+2880\n" +
"get tempradioat2"
);
assert.strictEqual(commands.stockLeaveIn30, "tempradio 910.1,500,8,7,30");
@@ -243,6 +287,33 @@ assert.strictEqual(
"set tempradio2 910.1,500,8,7,rxtx,30"
);
const absoluteCommands = tool.commandsFor(
config,
active,
0,
tool.SCHEDULE_MODE_ABSOLUTE
);
assert.strictEqual(
absoluteCommands.primaryScheduled,
"set tempradioat 910.1,500,8,7,1790035200,1790208000\nget tempradioat"
);
assert.strictEqual(
absoluteCommands.companionScheduled,
"set radio2.cross on\n" +
"set tempradioat2 910.1,500,8,7,rxtx,1790035200,1790208000\n" +
"get tempradioat2"
);
assert.doesNotThrow(() => tool.commandsFor(
config,
active,
tool.SCHEDULER_EPOCH_MAX,
tool.SCHEDULE_MODE_RELATIVE
));
assert.throws(
() => tool.commandsFor(config, active, 0, "unsupported"),
/schedule mode must be relative or absolute/
);
assert.strictEqual(tool.scheduleAvailability(config, before).available, true);
assert.strictEqual(tool.scheduleAvailability(config, active).available, false);
assert.strictEqual(tool.primaryScheduleAvailability(config, before).available, true);