mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-06 17:03:27 +02:00
web: define meshcore modem presets (#696)
* web: define meshcore modem presets * web: address review comments
This commit is contained in:
@@ -178,3 +178,57 @@ test('normalizePresetSlot enforces placeholders and uppercase output', () => {
|
||||
assert.equal(normalizePresetSlot(''), PRESET_PLACEHOLDER);
|
||||
assert.equal(normalizePresetSlot(null), PRESET_PLACEHOLDER);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// abbreviatePreset — MeshCore SF/BW/CR presets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// [description, preset, freqMHz, expectedCode]
|
||||
const ABBREVIATE_MESHCORE_CASES = [
|
||||
['AU/NZ Wide → Wi', 'SF10/BW250/CR5', null, 'Wi'],
|
||||
['EU/UK Narrow → Na', 'SF8/BW62/CR8', null, 'Na'],
|
||||
['CZ/SK Narrow at 868 MHz → Na', 'SF7/BW62/CR5', 868, 'Na'],
|
||||
['US/CA Narrow at 915 MHz → Na', 'SF7/BW62/CR5', 915, 'Na'],
|
||||
['US/CA Narrow at exact 900 MHz boundary', 'SF7/BW62/CR5', 900, 'Na'],
|
||||
['BW fallback Na when freq unknown', 'SF7/BW62/CR5', null, 'Na'],
|
||||
['125 kHz BW fallback → St', 'SF9/BW125/CR6', null, 'St'],
|
||||
['unknown BW → null', 'SF12/BW500/CR7', null, null],
|
||||
];
|
||||
for (const [desc, preset, freq, expected] of ABBREVIATE_MESHCORE_CASES) {
|
||||
test(`abbreviatePreset MeshCore: ${desc}`, () => {
|
||||
assert.equal(abbreviatePreset(preset, freq), expected);
|
||||
});
|
||||
}
|
||||
|
||||
test('abbreviatePreset leaves Meshtastic named presets unaffected', () => {
|
||||
assert.equal(abbreviatePreset('MediumFast', null), 'MF');
|
||||
assert.equal(abbreviatePreset('LongSlow', null), 'LS');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extractChatMessageMetadata — SF/BW/CR preset + frequency
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('extractChatMessageMetadata produces Wi code for AU/NZ Wide with freq', () => {
|
||||
const result = extractChatMessageMetadata({
|
||||
region_frequency: 915,
|
||||
modem_preset: 'SF10/BW250/CR5',
|
||||
});
|
||||
assert.equal(result.presetCode, 'Wi');
|
||||
assert.equal(result.frequency, '915');
|
||||
});
|
||||
|
||||
test('extractChatMessageMetadata produces Na code for EU/UK Narrow with freq', () => {
|
||||
const result = extractChatMessageMetadata({
|
||||
lora_freq: 868,
|
||||
modem_preset: 'SF8/BW62/CR8',
|
||||
});
|
||||
assert.equal(result.presetCode, 'Na');
|
||||
});
|
||||
|
||||
test('extractChatMessageMetadata uses BW fallback Na when freq is absent', () => {
|
||||
const result = extractChatMessageMetadata({
|
||||
modem_preset: 'SF7/BW62/CR5',
|
||||
});
|
||||
assert.equal(result.presetCode, 'Na');
|
||||
});
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { extractModemMetadata, formatLoraFrequencyMHz, formatModemDisplay, __testUtils } from '../node-modem-metadata.js';
|
||||
import {
|
||||
extractModemMetadata,
|
||||
formatLoraFrequencyMHz,
|
||||
formatModemDisplay,
|
||||
resolveMeshcorePresetDisplay,
|
||||
formatPresetDisplay,
|
||||
__testUtils,
|
||||
} from '../node-modem-metadata.js';
|
||||
|
||||
const { toTrimmedString, parseMeshcorePresetTokens, bwToShortCode } = __testUtils;
|
||||
|
||||
describe('node-modem-metadata', () => {
|
||||
it('extracts modem preset and frequency from mixed payloads', () => {
|
||||
@@ -42,6 +51,12 @@ describe('node-modem-metadata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null metadata for null and non-object input', () => {
|
||||
assert.deepEqual(extractModemMetadata(null), { modemPreset: null, loraFreq: null });
|
||||
assert.deepEqual(extractModemMetadata('string'), { modemPreset: null, loraFreq: null });
|
||||
assert.deepEqual(extractModemMetadata(42), { modemPreset: null, loraFreq: null });
|
||||
});
|
||||
|
||||
it('formats positive frequencies with MHz suffix', () => {
|
||||
assert.equal(formatLoraFrequencyMHz(915), '915MHz');
|
||||
assert.equal(formatLoraFrequencyMHz(867.5), '867.5MHz');
|
||||
@@ -49,17 +64,141 @@ describe('node-modem-metadata', () => {
|
||||
assert.equal(formatLoraFrequencyMHz(null), null);
|
||||
});
|
||||
|
||||
it('combines preset and frequency for overlay display', () => {
|
||||
it('combines preset and frequency for overlay display — Meshtastic named preset', () => {
|
||||
assert.equal(formatModemDisplay('MediumFast', 868), 'MediumFast (868MHz)');
|
||||
assert.equal(formatModemDisplay('ShortSlow', null), 'ShortSlow');
|
||||
assert.equal(formatModemDisplay(null, 433), '433MHz');
|
||||
assert.equal(formatModemDisplay(undefined, undefined), null);
|
||||
});
|
||||
|
||||
it('combines named MeshCore preset and frequency for overlay display', () => {
|
||||
assert.equal(formatModemDisplay('SF10/BW250/CR5', 868), 'AU/NZ Wide (868MHz)');
|
||||
});
|
||||
|
||||
it('handles string frequency in formatModemDisplay for MeshCore presets', () => {
|
||||
// frequency is a string here; exercises the Number(frequency) branch.
|
||||
assert.equal(formatModemDisplay('SF10/BW250/CR5', '915'), 'AU/NZ Wide (915MHz)');
|
||||
});
|
||||
|
||||
it('passes null frequency to formatModemDisplay gracefully', () => {
|
||||
assert.equal(formatModemDisplay('SF10/BW62/CR5', null), 'AU/NZ Narrow');
|
||||
});
|
||||
|
||||
it('exposes trimmed string helper for targeted assertions', () => {
|
||||
const { toTrimmedString } = __testUtils;
|
||||
assert.equal(toTrimmedString(' hello '), 'hello');
|
||||
assert.equal(toTrimmedString(''), null);
|
||||
assert.equal(toTrimmedString(null), null);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseMeshcorePresetTokens
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('parseMeshcorePresetTokens', () => {
|
||||
it('returns null for non-SF/BW/CR strings', () => {
|
||||
assert.equal(parseMeshcorePresetTokens('MediumFast'), null);
|
||||
assert.equal(parseMeshcorePresetTokens('LongSlow'), null);
|
||||
assert.equal(parseMeshcorePresetTokens(''), null);
|
||||
assert.equal(parseMeshcorePresetTokens(null), null);
|
||||
});
|
||||
|
||||
it('returns null when any token is missing', () => {
|
||||
assert.equal(parseMeshcorePresetTokens('SF10/BW250'), null);
|
||||
assert.equal(parseMeshcorePresetTokens('BW250/CR5'), null);
|
||||
});
|
||||
|
||||
it('returns null when a token does not match the expected format', () => {
|
||||
assert.equal(parseMeshcorePresetTokens('SF10/BW250/XX5'), null);
|
||||
assert.equal(parseMeshcorePresetTokens('SF10/BW250/CR'), null);
|
||||
});
|
||||
|
||||
it('parses a valid SF/BW/CR string', () => {
|
||||
assert.deepEqual(parseMeshcorePresetTokens('SF10/BW250/CR5'), { sf: 10, bw: 250, cr: 5 });
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
assert.deepEqual(parseMeshcorePresetTokens('sf10/bw62/cr5'), { sf: 10, bw: 62, cr: 5 });
|
||||
});
|
||||
|
||||
it('accepts tokens in any order', () => {
|
||||
assert.deepEqual(parseMeshcorePresetTokens('CR5/SF7/BW62'), { sf: 7, bw: 62, cr: 5 });
|
||||
});
|
||||
|
||||
it('handles decimal bandwidth values like 62.5', () => {
|
||||
assert.deepEqual(parseMeshcorePresetTokens('SF7/BW62.5/CR5'), { sf: 7, bw: 62.5, cr: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bwToShortCode
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('bwToShortCode', () => {
|
||||
it('maps 62 to Na', () => assert.equal(bwToShortCode(62), 'Na'));
|
||||
it('maps 62.5 to Na', () => assert.equal(bwToShortCode(62.5), 'Na'));
|
||||
it('maps 125 to St', () => assert.equal(bwToShortCode(125), 'St'));
|
||||
it('maps 250 to Wi', () => assert.equal(bwToShortCode(250), 'Wi'));
|
||||
it('returns null for unknown bandwidths', () => {
|
||||
assert.equal(bwToShortCode(500), null);
|
||||
assert.equal(bwToShortCode(31), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveMeshcorePresetDisplay
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('resolveMeshcorePresetDisplay', () => {
|
||||
it('returns null for non-SF/BW/CR input', () => {
|
||||
assert.equal(resolveMeshcorePresetDisplay('MediumFast', null), null);
|
||||
assert.equal(resolveMeshcorePresetDisplay(null, null), null);
|
||||
});
|
||||
|
||||
// Named preset table: [description, preset, freqMHz, expected]
|
||||
const NAMED_CASES = [
|
||||
['AU/NZ Wide', 'SF10/BW250/CR5', 915, { longName: 'AU/NZ Wide', shortCode: 'Wi', displayString: 'AU/NZ Wide' }],
|
||||
['AU/NZ Narrow', 'SF10/BW62/CR5', 915, { longName: 'AU/NZ Narrow', shortCode: 'Na', displayString: 'AU/NZ Narrow' }],
|
||||
['EU/UK Wide', 'SF11/BW250/CR5', 868, { longName: 'EU/UK Wide', shortCode: 'Wi', displayString: 'EU/UK Wide' }],
|
||||
['EU/UK Narrow', 'SF8/BW62/CR8', 868, { longName: 'EU/UK Narrow', shortCode: 'Na', displayString: 'EU/UK Narrow' }],
|
||||
['CZ/SK Narrow (freq < 900)', 'SF7/BW62/CR5', 868, { longName: 'CZ/SK Narrow', shortCode: 'Na', displayString: 'CZ/SK Narrow' }],
|
||||
['US/CA Narrow (freq >= 900)', 'SF7/BW62/CR5', 915, { longName: 'US/CA Narrow', shortCode: 'Na', displayString: 'US/CA Narrow' }],
|
||||
['US/CA Narrow (exact 900 boundary)', 'SF7/BW62/CR5', 900, { longName: 'US/CA Narrow', shortCode: 'Na', displayString: 'US/CA Narrow' }],
|
||||
];
|
||||
for (const [desc, preset, freq, expected] of NAMED_CASES) {
|
||||
it(`resolves ${desc}`, () => {
|
||||
assert.deepEqual(resolveMeshcorePresetDisplay(preset, freq), expected);
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback cases: [description, preset, freqMHz, expected]
|
||||
const FALLBACK_CASES = [
|
||||
['SF7/BW62/CR5 with unknown freq uses BW fallback', 'SF7/BW62/CR5', null, { longName: null, shortCode: 'Na', displayString: 'BW62/SF7/CR5' }],
|
||||
['unknown BW has no short code', 'SF12/BW500/CR7', null, { longName: null, shortCode: null, displayString: 'BW500/SF12/CR7' }],
|
||||
['125 kHz BW gives St short code', 'SF9/BW125/CR6', null, { longName: null, shortCode: 'St', displayString: 'BW125/SF9/CR6' }],
|
||||
];
|
||||
for (const [desc, preset, freq, expected] of FALLBACK_CASES) {
|
||||
it(`falls back: ${desc}`, () => {
|
||||
assert.deepEqual(resolveMeshcorePresetDisplay(preset, freq), expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// formatPresetDisplay
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('formatPresetDisplay', () => {
|
||||
it('returns long name for named MeshCore presets', () => {
|
||||
assert.equal(formatPresetDisplay('SF10/BW250/CR5', 915), 'AU/NZ Wide');
|
||||
});
|
||||
|
||||
it('returns re-ordered BW/SF/CR for unknown SF/BW/CR presets', () => {
|
||||
assert.equal(formatPresetDisplay('SF12/BW500/CR7', null), 'BW500/SF12/CR7');
|
||||
});
|
||||
|
||||
it('returns raw string for non-SF/BW/CR presets', () => {
|
||||
assert.equal(formatPresetDisplay('MediumFast', null), 'MediumFast');
|
||||
});
|
||||
|
||||
it('returns null when preset is absent', () => {
|
||||
assert.equal(formatPresetDisplay(null, null), null);
|
||||
assert.equal(formatPresetDisplay(' ', null), null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,8 @@ export function extractChatMessageMetadata(message) {
|
||||
);
|
||||
|
||||
const modemPreset = normalizePresetString(resolveModemPresetCandidate(message));
|
||||
const presetCode = modemPreset ? abbreviatePreset(modemPreset) : null;
|
||||
const numericFreq = frequency != null ? Number(frequency) : null;
|
||||
const presetCode = modemPreset ? abbreviatePreset(modemPreset, numericFreq) : null;
|
||||
|
||||
return { frequency, channelName, presetCode };
|
||||
}
|
||||
@@ -186,6 +187,7 @@ function firstNonNull(...candidates) {
|
||||
// normalizeString is the canonical implementation in utils.js; imported here
|
||||
// so callers of chat-format.js that use it directly continue to work.
|
||||
import { normalizeString } from './utils.js';
|
||||
import { resolveMeshcorePresetDisplay } from './node-modem-metadata.js';
|
||||
|
||||
/**
|
||||
* Convert various frequency representations into clean strings.
|
||||
@@ -273,13 +275,24 @@ function normalizePresetString(value) {
|
||||
/**
|
||||
* Produce a two-character abbreviation for a modem preset.
|
||||
*
|
||||
* SF/BW/CR preset strings (MeshCore) are resolved via
|
||||
* {@link resolveMeshcorePresetDisplay} so they bypass the Meshtastic
|
||||
* initials-derivation path entirely. Meshtastic named presets (e.g.
|
||||
* ``"MediumFast"``) are unaffected.
|
||||
*
|
||||
* @param {string} preset Normalized preset string.
|
||||
* @param {number|null} [freqMHz] Frequency in MHz, used for frequency-gated lookups.
|
||||
* @returns {string|null} Uppercase abbreviation or ``null``.
|
||||
*/
|
||||
function abbreviatePreset(preset) {
|
||||
function abbreviatePreset(preset, freqMHz = null) {
|
||||
if (!preset) {
|
||||
return null;
|
||||
}
|
||||
// MeshCore SF/BW/CR presets take priority over the Meshtastic lookup table.
|
||||
const resolved = resolveMeshcorePresetDisplay(preset, freqMHz);
|
||||
if (resolved !== null) {
|
||||
return resolved.shortCode;
|
||||
}
|
||||
const token = preset.replace(/[^A-Za-z]/g, '').toLowerCase();
|
||||
if (token && PRESET_ABBREVIATIONS[token]) {
|
||||
return PRESET_ABBREVIATIONS[token];
|
||||
|
||||
@@ -56,7 +56,7 @@ import { enhanceCoordinateCell } from './nodes-coordinate-links.js';
|
||||
import { createShortInfoOverlayStack } from './short-info-overlay-manager.js';
|
||||
import { createNodeDetailOverlayManager } from './node-detail-overlay.js';
|
||||
import { refreshNodeInformation } from './node-details.js';
|
||||
import { extractModemMetadata, formatLoraFrequencyMHz, formatModemDisplay } from './node-modem-metadata.js';
|
||||
import { extractModemMetadata, formatLoraFrequencyMHz, formatModemDisplay, formatPresetDisplay } from './node-modem-metadata.js';
|
||||
import {
|
||||
TELEMETRY_FIELDS,
|
||||
buildTelemetryDisplayEntries,
|
||||
@@ -4009,7 +4009,8 @@ export function initializeApp(config) {
|
||||
const modemMetadata = extractModemMetadata(n);
|
||||
const loraFrequencyText = formatLoraFrequencyMHz(modemMetadata.loraFreq);
|
||||
const loraFrequencyDisplay = loraFrequencyText ? escapeHtml(loraFrequencyText) : '';
|
||||
const modemPresetDisplay = modemMetadata.modemPreset ? escapeHtml(modemMetadata.modemPreset) : '';
|
||||
const resolvedPreset = formatPresetDisplay(modemMetadata.modemPreset, modemMetadata.loraFreq);
|
||||
const modemPresetDisplay = resolvedPreset ? escapeHtml(resolvedPreset) : '';
|
||||
const longNameHtml = renderNodeLongNameLink(n.long_name, n.node_id);
|
||||
const protocolIconCell = protocolIconPrefixHtml(n.protocol);
|
||||
tr.innerHTML = `
|
||||
|
||||
@@ -68,6 +68,144 @@ export function formatLoraFrequencyMHz(value) {
|
||||
return `${formatter.format(numeric)}MHz`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Named MeshCore modem preset definitions.
|
||||
*
|
||||
* Each entry describes a specific SF/BW/CR combination together with an
|
||||
* optional frequency gate. Frequency-gated entries are skipped when
|
||||
* ``freqMHz`` is not known.
|
||||
*
|
||||
* @type {Array<{
|
||||
* sf: number, bw: number, cr: number,
|
||||
* longName: string,
|
||||
* minFreqMHz?: number, maxFreqMHz?: number
|
||||
* }>}
|
||||
*/
|
||||
const MESHCORE_NAMED_PRESETS = [
|
||||
{ sf: 10, bw: 250, cr: 5, longName: 'AU/NZ Wide' },
|
||||
{ sf: 10, bw: 62, cr: 5, longName: 'AU/NZ Narrow' },
|
||||
{ sf: 11, bw: 250, cr: 5, longName: 'EU/UK Wide' },
|
||||
{ sf: 8, bw: 62, cr: 8, longName: 'EU/UK Narrow' },
|
||||
// SF7/BW62/CR5 is region-disambiguated by frequency threshold.
|
||||
{ sf: 7, bw: 62, cr: 5, longName: 'CZ/SK Narrow', maxFreqMHz: 900 },
|
||||
{ sf: 7, bw: 62, cr: 5, longName: 'US/CA Narrow', minFreqMHz: 900 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse an SF/BW/CR preset token string into its numeric components.
|
||||
*
|
||||
* Accepts any ordering of the three tokens separated by ``/``. Returns
|
||||
* ``null`` when the string does not look like an SF/BW/CR pattern (e.g.
|
||||
* Meshtastic named presets such as ``"MediumFast"``).
|
||||
*
|
||||
* @param {*} preset Candidate preset string.
|
||||
* @returns {{ sf: number, bw: number, cr: number } | null} Parsed tokens or ``null``.
|
||||
*/
|
||||
function parseMeshcorePresetTokens(preset) {
|
||||
const str = toTrimmedString(preset);
|
||||
if (!str) return null;
|
||||
|
||||
const parts = str.split('/');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const values = { sf: null, bw: null, cr: null };
|
||||
for (const part of parts) {
|
||||
const match = part.match(/^(SF|BW|CR)(\d+(?:\.\d+)?)$/i);
|
||||
if (!match) return null;
|
||||
const key = match[1].toLowerCase();
|
||||
values[key] = Number(match[2]);
|
||||
}
|
||||
|
||||
if (values.sf === null || values.bw === null || values.cr === null) return null;
|
||||
return { sf: values.sf, bw: values.bw, cr: values.cr };
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a LoRa bandwidth to the canonical short code used in preset display.
|
||||
*
|
||||
* Covers the three standard bandwidths (62 kHz / 62.5 kHz, 125 kHz,
|
||||
* 250 kHz). Any other value returns ``null``.
|
||||
*
|
||||
* @param {number} bw Bandwidth in kHz.
|
||||
* @returns {'Na'|'St'|'Wi'|null} Short code or ``null``.
|
||||
*/
|
||||
function bwToShortCode(bw) {
|
||||
// Accept both 62 and 62.5 as the narrow band value.
|
||||
if (bw === 62 || bw === 62.5) return 'Na';
|
||||
if (bw === 125) return 'St';
|
||||
if (bw === 250) return 'Wi';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a MeshCore SF/BW/CR preset into display metadata.
|
||||
*
|
||||
* Returns ``null`` for any preset that is not in SF/BW/CR format (e.g.
|
||||
* Meshtastic named presets), so callers can fall back to their own
|
||||
* handling.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Parse the preset into ``{sf, bw, cr}`` — return ``null`` if not parseable.
|
||||
* 2. Derive ``shortCode`` from BW alone (always BW-driven, not table-driven).
|
||||
* 3. Walk ``MESHCORE_NAMED_PRESETS`` for a matching ``{sf, bw, cr}`` entry
|
||||
* respecting any frequency gate. Frequency-gated entries are skipped when
|
||||
* ``freqMHz`` is ``null``.
|
||||
* 4. Named match → ``displayString = longName``.
|
||||
* No named match → ``longName = null``, ``displayString = "BW{bw}/SF{sf}/CR{cr}"``.
|
||||
*
|
||||
* @param {*} preset Raw preset string.
|
||||
* @param {number|null} freqMHz Frequency in MHz, or ``null`` when unknown.
|
||||
* @returns {{ longName: string|null, shortCode: string|null, displayString: string } | null}
|
||||
* Display metadata, or ``null`` when not an SF/BW/CR string.
|
||||
*/
|
||||
export function resolveMeshcorePresetDisplay(preset, freqMHz) {
|
||||
const tokens = parseMeshcorePresetTokens(preset);
|
||||
if (!tokens) return null;
|
||||
|
||||
const { sf, bw, cr } = tokens;
|
||||
const shortCode = bwToShortCode(bw);
|
||||
|
||||
const match = MESHCORE_NAMED_PRESETS.find(entry => {
|
||||
if (entry.sf !== sf || entry.bw !== bw || entry.cr !== cr) return false;
|
||||
if (entry.maxFreqMHz !== undefined) {
|
||||
if (freqMHz === null || freqMHz === undefined) return false;
|
||||
if (freqMHz >= entry.maxFreqMHz) return false;
|
||||
}
|
||||
if (entry.minFreqMHz !== undefined) {
|
||||
if (freqMHz === null || freqMHz === undefined) return false;
|
||||
if (freqMHz < entry.minFreqMHz) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
return { longName: match.longName, shortCode, displayString: match.longName };
|
||||
}
|
||||
|
||||
return {
|
||||
longName: null,
|
||||
shortCode,
|
||||
displayString: `BW${bw}/SF${sf}/CR${cr}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the best available display string for a modem preset.
|
||||
*
|
||||
* For SF/BW/CR presets this returns either the named long name or the
|
||||
* re-ordered ``BW/SF/CR`` fallback. For Meshtastic named presets and any
|
||||
* other non-SF/BW/CR strings the raw trimmed value is returned unchanged.
|
||||
*
|
||||
* @param {*} preset Raw preset value.
|
||||
* @param {number|null} [freqMHz] Frequency in MHz, used for frequency-gated lookups.
|
||||
* @returns {string|null} Display string or ``null`` when no preset is available.
|
||||
*/
|
||||
export function formatPresetDisplay(preset, freqMHz = null) {
|
||||
const resolved = resolveMeshcorePresetDisplay(preset, freqMHz);
|
||||
if (resolved !== null) return resolved.displayString;
|
||||
return toTrimmedString(preset) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a combined modem preset and frequency description suitable for overlays.
|
||||
*
|
||||
@@ -76,7 +214,8 @@ export function formatLoraFrequencyMHz(value) {
|
||||
* @returns {string|null} Human-readable description or ``null`` when no data available.
|
||||
*/
|
||||
export function formatModemDisplay(preset, frequency) {
|
||||
const presetText = toTrimmedString(preset);
|
||||
const numericFreq = typeof frequency === 'number' ? frequency : Number(frequency);
|
||||
const presetText = formatPresetDisplay(preset, Number.isFinite(numericFreq) && numericFreq > 0 ? numericFreq : null);
|
||||
const freqText = formatLoraFrequencyMHz(frequency);
|
||||
|
||||
if (!presetText && !freqText) {
|
||||
@@ -92,4 +231,6 @@ export function formatModemDisplay(preset, frequency) {
|
||||
|
||||
export const __testUtils = {
|
||||
toTrimmedString,
|
||||
parseMeshcorePresetTokens,
|
||||
bwToShortCode,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user