diff --git a/docs/_javascript/preset_test.js b/docs/_javascript/preset_test.js
index 5de9023c..ed6038d7 100644
--- a/docs/_javascript/preset_test.js
+++ b/docs/_javascript/preset_test.js
@@ -9,6 +9,10 @@
bw: "500",
sf: "8",
cr: "7",
+ normalfreq: "910.525",
+ normalbw: "62.5",
+ normalsf: "7",
+ normalcr: "5",
tx: "22",
});
const VALID_BANDWIDTHS = Object.freeze([
@@ -108,6 +112,10 @@
const bw = strictNumber(raw.bw, "bw");
const sf = strictInteger(raw.sf, "sf");
const cr = strictInteger(raw.cr, "cr");
+ const normalFreq = strictNumber(raw.normalfreq, "normalfreq");
+ const normalBw = strictNumber(raw.normalbw, "normalbw");
+ const normalSf = strictInteger(raw.normalsf, "normalsf");
+ const normalCr = strictInteger(raw.normalcr, "normalcr");
const tx = strictSignedNumber(raw.tx, "tx");
if (endMs <= startMs) {
@@ -129,6 +137,22 @@
if (cr < 5 || cr > 8) {
throw new PresetTestError("cr must be between 5 and 8");
}
+ if (normalFreq < 150 || normalFreq > 2500) {
+ throw new PresetTestError("normalfreq must be between 150 and 2500 MHz");
+ }
+ if (!VALID_BANDWIDTHS.some(function (allowed) {
+ return Math.abs(allowed - normalBw) < 0.01;
+ })) {
+ throw new PresetTestError(
+ "normalbw must be one of " + VALID_BANDWIDTHS.join(", ") + " kHz"
+ );
+ }
+ if (normalSf < 5 || normalSf > 12) {
+ throw new PresetTestError("normalsf must be between 5 and 12");
+ }
+ if (normalCr < 5 || normalCr > 8) {
+ throw new PresetTestError("normalcr must be between 5 and 8");
+ }
if (tx < -30 || tx > 60) {
throw new PresetTestError("tx must be between -30 and 60 dBm");
}
@@ -143,9 +167,15 @@
bw: bw,
sf: sf,
cr: cr,
+ normalFreq: normalFreq,
+ normalBw: normalBw,
+ normalSf: normalSf,
+ normalCr: normalCr,
tx: tx,
freqText: numberText(freq),
bwText: numberText(bw),
+ normalFreqText: numberText(normalFreq),
+ normalBwText: numberText(normalBw),
txText: numberText(tx),
});
}
@@ -179,6 +209,9 @@
function commandsFor(config, nowMs) {
const tuple = [config.freqText, config.bwText, config.sf, config.cr].join(",");
+ const normalTuple = [
+ config.normalFreqText, config.normalBwText, config.normalSf, config.normalCr,
+ ].join(",");
const minutes = remainingMinutes(config, nowMs);
return Object.freeze({
stockNow: "tempradio " + tuple + "," + minutes,
@@ -191,7 +224,7 @@
"set radio2.cross on\nset tempradioat2 " + tuple + ",rxtx," +
config.startEpoch + "," + config.endEpoch + "\nget tempradioat2",
stockCancelBefore: "get tempradioat\ndel tempradioat all",
- stockCancelDuring: "normalradio",
+ stockCancelDuring: "tempradio " + normalTuple + ",1",
stockLeaveIn30: "tempradio " + tuple + ",30",
companionCancelBefore:
"get tempradioat2\ndel tempradioat2 all\nset radio2.cross auto",
@@ -369,6 +402,10 @@
params.set("bw", values.bw);
params.set("sf", values.sf);
params.set("cr", values.cr);
+ params.set("normalfreq", values.normalfreq);
+ params.set("normalbw", values.normalbw);
+ params.set("normalsf", values.normalsf);
+ params.set("normalcr", values.normalcr);
params.set("tx", values.tx);
return configFromSearch("?" + params.toString());
}
@@ -397,6 +434,10 @@
url.searchParams.set("bw", config.bwText);
url.searchParams.set("sf", String(config.sf));
url.searchParams.set("cr", String(config.cr));
+ url.searchParams.set("normalfreq", config.normalFreqText);
+ url.searchParams.set("normalbw", config.normalBwText);
+ url.searchParams.set("normalsf", String(config.normalSf));
+ url.searchParams.set("normalcr", String(config.normalCr));
url.searchParams.set("tx", config.txText);
return url.toString();
}
@@ -472,6 +513,11 @@
setText(root, '[data-field="bw-display"]', config.bwText);
setText(root, '[data-field="sf"]', String(config.sf));
setText(root, '[data-field="cr"]', String(config.cr));
+ setText(
+ root,
+ '[data-field="normal-profile"]',
+ [config.normalFreqText, config.normalBwText, config.normalSf, config.normalCr].join(",")
+ );
setText(root, '[data-role="start-zoned"]', formatZoned(config.startMs, config.tz));
setText(root, '[data-role="end-zoned"]', formatZoned(config.endMs, config.tz));
setText(root, '[data-role="display-zone"]', config.tz);
@@ -507,6 +553,10 @@
generator.elements.bw.value = config.bwText;
generator.elements.sf.value = String(config.sf);
generator.elements.cr.value = String(config.cr);
+ generator.elements.normalfreq.value = config.normalFreqText;
+ generator.elements.normalbw.value = config.normalBwText;
+ generator.elements.normalsf.value = String(config.normalSf);
+ generator.elements.normalcr.value = String(config.normalCr);
generator.elements.tx.value = config.txText;
const zoneList = root.querySelector("#preset-test-time-zones");
@@ -531,6 +581,10 @@
bw: generator.elements.bw.value,
sf: generator.elements.sf.value,
cr: generator.elements.cr.value,
+ normalfreq: generator.elements.normalfreq.value,
+ normalbw: generator.elements.normalbw.value,
+ normalsf: generator.elements.normalsf.value,
+ normalcr: generator.elements.normalcr.value,
tx: generator.elements.tx.value,
});
const url = configuredUrl(
diff --git a/docs/_stylesheets/preset_test.css b/docs/_stylesheets/preset_test.css
index c383a6c9..ffd5f0f6 100644
--- a/docs/_stylesheets/preset_test.css
+++ b/docs/_stylesheets/preset_test.css
@@ -278,6 +278,17 @@
font-weight: 700;
}
+.preset-test-generator-subheading {
+ grid-column: 1 / -1;
+ border-bottom: 1px solid var(--preset-border);
+ padding: 0.3rem 0 0.35rem;
+ color: var(--preset-cyan);
+ font-size: 0.74rem;
+ font-weight: 800;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+}
+
.preset-test-generator input,
.preset-test-generator select {
box-sizing: border-box;
diff --git a/docs/preset_test.md b/docs/preset_test.md
index dd886275..ea9d773e 100644
--- a/docs/preset_test.md
+++ b/docs/preset_test.md
@@ -47,7 +47,8 @@ tuple; no source edit is required.
The test will include bridges. If the Puget Sound area experiences a
widespread power outage or loss of cellular service, we will end the
- test early and revert to the normal 910.525 MHz channel.
+ test early and revert to the normal
+ 910.525,62.5,7,5 profile.
@@ -212,12 +213,14 @@ clock
Stock · while it is running
- Leave now
+ Leave now · stock compatible
- Copy restore command
+ Copy stock leave command
- normalradio cancels pending and active primary TempRadio
- windows and restores the saved primary tuple after its reply drains.
+ Stock MeshCore has no normalradio command. This starts a
+ one-minute temporary session on the normal return profile without
+ saving over the node's configuration. When that minute ends, the
+ node restores its saved radio settings.
Leave in 30 minutes
@@ -283,6 +286,7 @@ clock
autocomplete="off" spellcheck="false">
Use an IANA name such as America/Los_Angeles or UTC.
+ Test radio profile
Frequency (MHz)
@@ -313,6 +317,32 @@ clock
This estimate-only value does not change the TempRadio commands.
+ Normal return profile
+
+ Normal frequency (MHz)
+
+
+
+ Normal bandwidth (kHz)
+
+ 7.8 10.4 15.6
+ 20.8 31.25 41.7
+ 62.5 125 250 500
+
+
+
+ Normal spreading factor
+
+ 5 6 7 8
+ 9 10 11 12
+
+
+
+ Normal coding-rate denominator
+
+ 5 6 7 8
+
+
Generate test URL
@@ -361,7 +391,9 @@ clock
Advanced use: start and end also accept ISO-8601
timestamps with explicit UTC offsets or Unix epoch seconds. The other URL
parameters are tz, freq, bw,
- sf, cr, and estimate-only tx.
+ sf, cr, normalfreq,
+ normalbw, normalsf, normalcr, and
+ estimate-only tx.
diff --git a/test/test_preset_test.js b/test/test_preset_test.js
index dfcba65d..3d34dcab 100644
--- a/test/test_preset_test.js
+++ b/test/test_preset_test.js
@@ -18,6 +18,10 @@ assert.strictEqual(config.sf, 8);
assert.strictEqual(config.cr, 7);
assert.strictEqual(config.tz, "America/Los_Angeles");
assert.strictEqual(config.tx, 22);
+assert.strictEqual(config.normalFreq, 910.525);
+assert.strictEqual(config.normalBw, 62.5);
+assert.strictEqual(config.normalSf, 7);
+assert.strictEqual(config.normalCr, 5);
assert.strictEqual(defaults.startEpoch, config.startEpoch);
assert.strictEqual(defaults.endEpoch, config.endEpoch);
assert.strictEqual(defaults.freq, config.freq);
@@ -58,6 +62,10 @@ assert.strictEqual(
"get tempradioat2"
);
assert.strictEqual(commands.stockLeaveIn30, "tempradio 910.1,500,8,7,30");
+assert.strictEqual(
+ commands.stockCancelDuring,
+ "tempradio 910.525,62.5,7,5,1"
+);
assert.strictEqual(
commands.companionLeaveIn30,
"set radio2.cross on\n" +
@@ -101,6 +109,10 @@ const generated = tool.configFromGenerator({
bw: "500",
sf: "8",
cr: "7",
+ normalfreq: "910.525",
+ normalbw: "62.5",
+ normalsf: "7",
+ normalcr: "5",
tx: "22",
});
assert.strictEqual(generated.startEpoch, config.startEpoch);
@@ -136,6 +148,18 @@ assert.throws(
() => tool.configFromSearch("?tx=61"),
/tx must be between/
);
+assert.throws(
+ () => tool.configFromSearch("?normalbw=100"),
+ /normalbw must be one of/
+);
+
+const alternateReturn = tool.configFromSearch(
+ "?normalfreq=915.5&normalbw=125&normalsf=9&normalcr=6"
+);
+assert.strictEqual(
+ tool.commandsFor(alternateReturn, alternateReturn.startMs).stockCancelDuring,
+ "tempradio 915.5,125,9,6,1"
+);
const changed = tool.configFromSearch(
"?start=1790035200&end=1790208000&freq=915.25&bw=125&sf=10&cr=5"
@@ -154,6 +178,10 @@ assert.strictEqual(shared.searchParams.get("end"), "2026-09-24T00:00:00.000Z");
assert.strictEqual(shared.searchParams.get("freq"), "915.25");
assert.strictEqual(shared.searchParams.get("tz"), "America/Los_Angeles");
assert.strictEqual(shared.searchParams.get("tx"), "22");
+assert.strictEqual(shared.searchParams.get("normalfreq"), "910.525");
+assert.strictEqual(shared.searchParams.get("normalbw"), "62.5");
+assert.strictEqual(shared.searchParams.get("normalsf"), "7");
+assert.strictEqual(shared.searchParams.get("normalcr"), "5");
assert.strictEqual(shared.searchParams.has("stale"), false);
assert.strictEqual(shared.hash, "#commands");