Fix custom radio details

This commit is contained in:
Jonathon Leight
2026-06-26 17:38:59 -04:00
parent f80015f650
commit 149e207e69
5 changed files with 92 additions and 36 deletions
+7 -4
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"html/template"
"image/color"
"math"
"net/http"
"net/url"
"strconv"
@@ -175,15 +176,17 @@ func (s *Handlers) handleAddRepeater(w http.ResponseWriter, r *http.Request) {
}
// parseRadioForm reads and validates the radio fields from a repeater form.
// Frequency and bandwidth are entered in MHz/kHz (matching the region presets)
// and stored as Hz.
func parseRadioForm(r *http.Request) (freq, bw int64, sf, cr int16, ok bool) {
f, e1 := strconv.ParseInt(r.FormValue("radio_freq_hz"), 10, 64)
b, e2 := strconv.ParseInt(r.FormValue("radio_bw_hz"), 10, 64)
mhz, e1 := strconv.ParseFloat(r.FormValue("radio_freq_mhz"), 64)
khz, e2 := strconv.ParseFloat(r.FormValue("radio_bw_khz"), 64)
s, e3 := strconv.ParseInt(r.FormValue("radio_sf"), 10, 16)
c, e4 := strconv.ParseInt(r.FormValue("radio_cr"), 10, 16)
if e1 != nil || e2 != nil || e3 != nil || e4 != nil || f <= 0 || b <= 0 {
if e1 != nil || e2 != nil || e3 != nil || e4 != nil || mhz <= 0 || khz <= 0 {
return 0, 0, 0, 0, false
}
return f, b, int16(s), int16(c), true
return int64(math.Round(mhz * 1e6)), int64(math.Round(khz * 1e3)), int16(s), int16(c), true
}
// pageEditRepeater shows the edit form for an owned repeater.
+37 -15
View File
@@ -93,8 +93,8 @@
</select>
</div>
<div class="row g-2">
<div class="col-6 col-md-3"><label class="form-label" for="radio_freq_hz">Freq (Hz)</label><input type="number" class="form-control" id="radio_freq_hz" name="radio_freq_hz" value="{{.Defaults.FreqHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_bw_hz">Bandwidth (Hz)</label><input type="number" class="form-control" id="radio_bw_hz" name="radio_bw_hz" value="{{.Defaults.BwHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_freq_mhz">Freq (MHz)</label><input type="number" step="0.001" class="form-control" id="radio_freq_mhz" name="radio_freq_mhz" value="{{mhz .Defaults.FreqHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_bw_khz">Bandwidth (kHz)</label><input type="number" step="0.1" class="form-control" id="radio_bw_khz" name="radio_bw_khz" value="{{khz .Defaults.BwHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_sf">SF</label><input type="number" class="form-control" id="radio_sf" name="radio_sf" value="{{.Defaults.SF}}" min="5" max="12" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_cr">CR</label><input type="number" class="form-control" id="radio_cr" name="radio_cr" value="{{.Defaults.CR}}" min="5" max="8" required></div>
</div>
@@ -112,23 +112,45 @@
</div>
</div>
<script>
// The form fields display MHz/kHz; preset data-* attributes are Hz-canonical.
var RADIO_FIELDS = ["radio_freq_mhz", "radio_bw_khz", "radio_sf", "radio_cr"];
function trim(n) { return String(Math.round(n * 1e6) / 1e6); } // drop float fuzz
// Seed the radio fields from the chosen preset. Fields stay editable — a
// preset is a starting point, not a lock.
function applyRegion() {
var sel = document.getElementById("region");
var opt = sel.options[sel.selectedIndex];
var fields = ["radio_freq_hz", "radio_bw_hz", "radio_sf", "radio_cr"].map(function (id) {
return document.getElementById(id);
});
if (opt.dataset.freq) {
fields[0].value = opt.dataset.freq;
fields[1].value = opt.dataset.bw;
fields[2].value = opt.dataset.sf;
fields[3].value = opt.dataset.cr;
fields.forEach(function (el) { el.readOnly = true; });
} else {
fields.forEach(function (el) { el.readOnly = false; });
}
if (!opt.dataset.freq) return; // "Custom" — leave the current values alone
document.getElementById("radio_freq_mhz").value = trim(opt.dataset.freq / 1e6);
document.getElementById("radio_bw_khz").value = trim(opt.dataset.bw / 1e3);
document.getElementById("radio_sf").value = opt.dataset.sf;
document.getElementById("radio_cr").value = opt.dataset.cr;
}
applyRegion();
// When a field is edited by hand, point the dropdown at whichever preset now
// matches (or "Custom" if none do), so it always reflects the actual values.
// Compare in Hz so display rounding can't cause a spurious mismatch.
function syncPreset() {
var sel = document.getElementById("region");
var freqHz = Math.round(parseFloat(document.getElementById("radio_freq_mhz").value) * 1e6);
var bwHz = Math.round(parseFloat(document.getElementById("radio_bw_khz").value) * 1e3);
var sf = document.getElementById("radio_sf").value;
var cr = document.getElementById("radio_cr").value;
for (var i = 0; i < sel.options.length; i++) {
var o = sel.options[i];
if (o.dataset.freq && +o.dataset.freq === freqHz && +o.dataset.bw === bwHz &&
o.dataset.sf === sf && o.dataset.cr === cr) {
sel.value = o.value;
return;
}
}
sel.value = "custom";
}
RADIO_FIELDS.forEach(function (id) {
document.getElementById(id).addEventListener("input", syncPreset);
});
</script>
{{end}}
</div>
+1 -1
View File
@@ -16,7 +16,7 @@
to verify it has access. This is optional — the repeater works either way.
</p>
<code class="pk">{{.Repeater.PublicKeyHex}}</code>
<div class="text-secondary small mt-2">{{.Repeater.RadioFreqHz}} Hz · BW {{.Repeater.RadioBwHz}} · SF{{.Repeater.RadioSF}} · CR{{.Repeater.RadioCR}}</div>
<div class="text-secondary small mt-2">{{mhz .Repeater.RadioFreqHz}} MHz · BW {{khz .Repeater.RadioBwHz}} · SF{{.Repeater.RadioSF}} · CR{{.Repeater.RadioCR}}</div>
<div id="unsupported" class="alert alert-danger mt-3" role="alert" hidden>
This browser does not support WebSerial. Use Chrome, Edge, or another Chromium-based browser over HTTPS or on localhost.
+37 -15
View File
@@ -29,8 +29,8 @@
</select>
</div>
<div class="row g-2">
<div class="col-6 col-md-3"><label class="form-label" for="radio_freq_hz">Freq (Hz)</label><input type="number" class="form-control" id="radio_freq_hz" name="radio_freq_hz" value="{{.Repeater.RadioFreqHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_bw_hz">Bandwidth (Hz)</label><input type="number" class="form-control" id="radio_bw_hz" name="radio_bw_hz" value="{{.Repeater.RadioBwHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_freq_mhz">Freq (MHz)</label><input type="number" step="0.001" class="form-control" id="radio_freq_mhz" name="radio_freq_mhz" value="{{mhz .Repeater.RadioFreqHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_bw_khz">Bandwidth (kHz)</label><input type="number" step="0.1" class="form-control" id="radio_bw_khz" name="radio_bw_khz" value="{{khz .Repeater.RadioBwHz}}" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_sf">SF</label><input type="number" class="form-control" id="radio_sf" name="radio_sf" value="{{.Repeater.RadioSF}}" min="5" max="12" required></div>
<div class="col-6 col-md-3"><label class="form-label" for="radio_cr">CR</label><input type="number" class="form-control" id="radio_cr" name="radio_cr" value="{{.Repeater.RadioCR}}" min="5" max="8" required></div>
</div>
@@ -55,22 +55,44 @@
</div>
</div>
<script>
// The form fields display MHz/kHz; preset data-* attributes are Hz-canonical.
var RADIO_FIELDS = ["radio_freq_mhz", "radio_bw_khz", "radio_sf", "radio_cr"];
function trim(n) { return String(Math.round(n * 1e6) / 1e6); } // drop float fuzz
// Seed the radio fields from the chosen preset. Fields stay editable — a
// preset is a starting point, not a lock.
function applyRegion() {
var sel = document.getElementById("region");
var opt = sel.options[sel.selectedIndex];
var fields = ["radio_freq_hz", "radio_bw_hz", "radio_sf", "radio_cr"].map(function (id) {
return document.getElementById(id);
});
if (opt.dataset.freq) {
fields[0].value = opt.dataset.freq;
fields[1].value = opt.dataset.bw;
fields[2].value = opt.dataset.sf;
fields[3].value = opt.dataset.cr;
fields.forEach(function (el) { el.readOnly = true; });
} else {
fields.forEach(function (el) { el.readOnly = false; });
}
if (!opt.dataset.freq) return; // "Custom" — leave the current values alone
document.getElementById("radio_freq_mhz").value = trim(opt.dataset.freq / 1e6);
document.getElementById("radio_bw_khz").value = trim(opt.dataset.bw / 1e3);
document.getElementById("radio_sf").value = opt.dataset.sf;
document.getElementById("radio_cr").value = opt.dataset.cr;
}
applyRegion();
// When a field is edited by hand, point the dropdown at whichever preset now
// matches (or "Custom" if none do), so it always reflects the actual values.
// Compare in Hz so display rounding can't cause a spurious mismatch.
function syncPreset() {
var sel = document.getElementById("region");
var freqHz = Math.round(parseFloat(document.getElementById("radio_freq_mhz").value) * 1e6);
var bwHz = Math.round(parseFloat(document.getElementById("radio_bw_khz").value) * 1e3);
var sf = document.getElementById("radio_sf").value;
var cr = document.getElementById("radio_cr").value;
for (var i = 0; i < sel.options.length; i++) {
var o = sel.options[i];
if (o.dataset.freq && +o.dataset.freq === freqHz && +o.dataset.bw === bwHz &&
o.dataset.sf === sf && o.dataset.cr === cr) {
sel.value = o.value;
return;
}
}
sel.value = "custom";
}
RADIO_FIELDS.forEach(function (id) {
document.getElementById(id).addEventListener("input", syncPreset);
});
</script>
{{end}}
+10 -1
View File
@@ -12,6 +12,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
@@ -117,8 +118,16 @@ func (e *Env) SetDefaultLayout(name string) { e.Renderer.defaultLayout = name }
// NewRenderer parses the shared base layout (base.html + icons.html) and composes
// each of the surface's own *.html pages onto it. Each page redefines the
// content/title/header blocks, so every page gets its own cloned template set.
// templateFuncs are helpers available to every page template. mhz/khz present
// the Hz-canonical radio values in the human-readable units the region presets
// use (MHz for frequency, kHz for bandwidth), formatted without trailing zeros.
var templateFuncs = template.FuncMap{
"mhz": func(hz int64) string { return strconv.FormatFloat(float64(hz)/1e6, 'f', -1, 64) },
"khz": func(hz int64) string { return strconv.FormatFloat(float64(hz)/1e3, 'f', -1, 64) },
}
func NewRenderer(cfg *config.Config, surfaceTemplates fs.FS) (*Renderer, error) {
base, err := template.New("").ParseFS(sharedTemplatesFS, "templates/base.html", "templates/icons.html", "templates/org_tabs.html", "templates/repeater_tabs.html")
base, err := template.New("").Funcs(templateFuncs).ParseFS(sharedTemplatesFS, "templates/base.html", "templates/icons.html", "templates/org_tabs.html", "templates/repeater_tabs.html")
if err != nil {
return nil, err
}