Skip to content
Open
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
30 changes: 18 additions & 12 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ radio:
sync_word: 0x2B
preamble_length: 16
tx_power_dbm: 22
# HAL GPS/PPS (RAK Pi HAT u-blox → concentrator PPS pin). Off by default.
# gps_pps_enabled: false
# gps_pps_tty_path: "/dev/ttyAMA0"
# gps_family: "ubx7"
# gps_pps_target_baud: 0 # 0 = HAL default (9600)

meshtastic:
default_key_b64: "AQ=="
Expand Down Expand Up @@ -50,7 +55,7 @@ device:
longitude: 0.0
altitude: 25
hardware_description: "RAK2287 + Raspberry Pi 4"
firmware_version: "0.7.6"
firmware_version: "0.7.5.1"

relay:
enabled: false
Expand All @@ -60,6 +65,9 @@ relay:
burst_size: 5
min_relay_rssi: -110.0
max_relay_rssi: -50.0
# blocklist: [] # 8-char hex node IDs — relay only, feed unchanged
# priority_list: [] # bypass burst gate under congestion
# dedup_ttl_seconds: 300

upstream:
enabled: true
Expand All @@ -80,13 +88,6 @@ transmit:
nodeinfo:
interval_minutes: 180 # 5..1440 min, default 180 (3 hr); 0 = disabled
startup_delay_seconds: 60 # delay before first broadcast on boot
position:
interval_minutes: 15
startup_delay_seconds: 180
# LoRa mesh POSITION packets (Meshtastic app map). Meshradar fleet pin
# always uses device.latitude/longitude above, not live GPS.
coordinate_source: "static" # static | live (live needs gpsd/uart source)
location_precision: "approximate" # exact | approximate | none (live only)

web_auth:
# First-run state: hashes are empty -> dashboard forces /setup.
Expand Down Expand Up @@ -124,18 +125,23 @@ mqtt:
homeassistant_discovery: false

location:
# Where live GPS fixes come from (skyplot + optional mesh POSITION).
# Registered coordinates for Meshradar live in device.latitude/longitude
# and are not overwritten by gpsd.
# Where the Meshpoint's reported lat/lon/alt comes from.
# static : use device.latitude/longitude/altitude (default)
# gpsd : poll a local or remote gpsd daemon for live fixes
# uart : reserved (RAK Pi HAT GPS, not yet wired)
# uart : on-board RAK Pi HAT GPS via /dev/ttyAMA0 (NMEA GGA)
source: "static"
# gpsd connection. Defaults match the well-known local socket; only
# change when running gpsd on a peer machine on the LAN.
gpsd_host: "127.0.0.1"
gpsd_port: 2947
uart_path: "/dev/ttyAMA0"
uart_baud: 9600
# How often the coordinator wakes to read the active source.
update_interval_seconds: 5
# Minimum acceptable fix mode: 0=any (incl. no-fix), 1=2D, 2=3D.
min_fix_quality: 1

# signal_health:
# green_rssi_floor: -100 # badge green when 1h avg RSSI is above this
# yellow_rssi_floor: -115 # badge yellow above this, red below
# min_packets_per_hour: 5 # hide badge when fewer packets in the last hour
67 changes: 54 additions & 13 deletions frontend/css/configuration.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,6 @@
line-height: 1.5;
}

.cfg-card__hint--nested {
margin: -4px 0 4px;
}

.cfg-inline-link {
color: var(--accent-cyan, #22d3ee);
text-decoration: none;
}

.cfg-inline-link:hover {
text-decoration: underline;
}

.cfg-form {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -242,6 +229,60 @@ select.cfg-field__input option:checked {
transform: translateX(-50%) translateY(0);
}

.cfg-id-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.5rem;
}

.cfg-id-empty {
margin: 0;
font-size: 12px;
color: rgba(243, 244, 246, 0.45);
font-style: italic;
}

.cfg-id-row {
display: flex;
align-items: center;
gap: 0.5rem;
}

.cfg-id-chip {
font-family: 'JetBrains Mono', Menlo, monospace;
font-size: 12px;
padding: 0.15rem 0.45rem;
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.25);
border-radius: 6px;
color: #e2e8f0;
}

.cfg-id-remove {
background: transparent;
border: none;
color: rgba(243, 244, 246, 0.5);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
padding: 0 0.25rem;
}

.cfg-id-remove:hover {
color: #ef4444;
}

.cfg-id-add {
display: flex;
gap: 0.5rem;
align-items: center;
}

.cfg-id-add .cfg-field__input {
flex: 1;
}

@keyframes cfgFadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
Expand Down
4 changes: 4 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<link rel="stylesheet" href="css/settings.css" />
<link rel="stylesheet" href="css/configuration.css" />
<link rel="stylesheet" href="css/gps.css" />
<link rel="stylesheet" href="css/packet_detail_modal.css" />
<link rel="stylesheet" href="css/terminal_theme.css" />
<link rel="stylesheet" href="css/terminal.css" />
<link rel="stylesheet" href="/vendor/xterm/xterm.css" />
Expand Down Expand Up @@ -677,10 +678,12 @@ <h3 class="dangerous-panel__title">Service actions</h3>
<script src="js/meshpoint_display_units.js"></script>
<script src="js/meshpoint_node_favorites.js"></script>
<script src="js/node_cards_sort.js"></script>
<script src="js/signal_sparkline.js"></script>
<script src="js/node_cards.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<script src="js/node_metrics_chart.js"></script>
<script src="js/node_drawer.js"></script>
<script src="js/packet_detail_modal.js"></script>
<script src="js/simple_packet_feed.js"></script>
<script src="js/messaging_contacts.js"></script>
<script src="js/messaging_chat.js"></script>
Expand Down Expand Up @@ -718,6 +721,7 @@ <h3 class="dangerous-panel__title">Service actions</h3>
<script src="js/configuration/gps_stats_column.js"></script>
<script src="js/configuration/gps_card.js"></script>
<script src="js/configuration/advanced_card.js"></script>
<script src="js/configuration/relay_filters_card.js"></script>
<script src="js/configuration/configuration_panel.js"></script>
<script src="/vendor/xterm/xterm.js"></script>
<script src="/vendor/xterm/xterm-addon-fit.js"></script>
Expand Down
12 changes: 10 additions & 2 deletions frontend/js/configuration/configuration_panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,18 @@ class ConfigurationPanel {
} else if (section === 'advanced' && window.AdvancedConfigCard) {
const host = document.getElementById('cfg-advanced-panel');
if (host) {
host.innerHTML = '';
host.innerHTML = `
<div data-cfg-advanced></div>
<div data-cfg-relay-filters></div>
`;
const card = new window.AdvancedConfigCard(api);
card.mount(host);
card.mount(host.querySelector('[data-cfg-advanced]'));
this._cards.set('advanced', card);
if (window.RelayFiltersCard) {
const relayCard = new window.RelayFiltersCard(api);
relayCard.mount(host.querySelector('[data-cfg-relay-filters]'));
this._cards.set('relay-filters', relayCard);
}
}
}
this._mounted.add(section);
Expand Down
181 changes: 181 additions & 0 deletions frontend/js/configuration/relay_filters_card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* Configuration → Advanced — relay filter controls.
*
* Blocklist, priority list, and dedup TTL via PUT /api/config/relay.
* Filter changes hot-reload without a full service restart.
*/

class RelayFiltersCard {
static NODE_ID_RE = /^[0-9a-f]{8}$/i;

constructor(api) {
this._api = api;
this._root = null;
this._blocklist = [];
this._priority = [];
}

mount(root) {
this._root = root;
this._root.innerHTML = `
<article class="cfg-card">
<header class="cfg-card__head">
<h3 class="cfg-card__title">Relay filters</h3>
<p class="cfg-card__hint">
Blocklist affects <strong>relay only</strong> — packets still appear in the live feed.
Priority nodes bypass the burst gate (not the per-minute cap). Dedup default is 300 s.
</p>
</header>
<form class="cfg-form" data-relay-filters-form>
<div class="cfg-field">
<span class="cfg-field__label">Blocklist</span>
<div class="cfg-id-list" data-blocklist></div>
<div class="cfg-id-add">
<input class="cfg-field__input" type="text"
placeholder="a3f2b1c0" maxlength="9"
data-blocklist-input>
<button type="button" class="terminal-button"
data-blocklist-add>Add</button>
</div>
</div>
<div class="cfg-field">
<span class="cfg-field__label">Priority list</span>
<div class="cfg-id-list" data-priority-list></div>
<div class="cfg-id-add">
<input class="cfg-field__input" type="text"
placeholder="deadbeef" maxlength="9"
data-priority-input>
<button type="button" class="terminal-button"
data-priority-add>Add</button>
</div>
</div>
<label class="cfg-field">
<span class="cfg-field__label">Dedup TTL (seconds)</span>
<input class="cfg-field__input" type="number" min="5" max="3600"
step="1" data-dedup-ttl>
<span class="cfg-field__hint">How long a packet ID is remembered (default 300).</span>
</label>
<div class="cfg-card__actions">
<button class="terminal-button terminal-button--primary"
type="submit">Save relay filters</button>
</div>
<p class="cfg-status" data-relay-filters-status aria-live="polite"></p>
</form>
</article>
`;

this._blocklistEl = this._root.querySelector('[data-blocklist]');
this._priorityEl = this._root.querySelector('[data-priority-list]');
this._form = this._root.querySelector('[data-relay-filters-form]');
this._statusEl = this._root.querySelector('[data-relay-filters-status]');

this._root.querySelector('[data-blocklist-add]').addEventListener('click', () => {
this._addId('blocklist');
});
this._root.querySelector('[data-priority-add]').addEventListener('click', () => {
this._addId('priority');
});
this._form.addEventListener('submit', (e) => this._onSubmit(e));
}

render(config) {
const relay = config.relay || {};
this._blocklist = (relay.blocklist || []).map((id) => this._normalizeId(id));
this._priority = (relay.priority_list || []).map((id) => this._normalizeId(id));
this._paintLists();
const ttl = relay.dedup_ttl_seconds;
const ttlEl = this._root.querySelector('[data-dedup-ttl]');
if (ttlEl) ttlEl.value = ttl != null ? ttl : 300;
}

_normalizeId(raw) {
return String(raw || '').trim().toLowerCase().replace(/^!/, '');
}

_addId(which) {
const input = this._root.querySelector(
which === 'blocklist' ? '[data-blocklist-input]' : '[data-priority-input]',
);
const id = this._normalizeId(input.value);
if (!RelayFiltersCard.NODE_ID_RE.test(id)) {
this._setStatus('error', 'Node ID must be 8 hex characters (no ! prefix).');
return;
}
const list = which === 'blocklist' ? this._blocklist : this._priority;
if (!list.includes(id)) list.push(id);
input.value = '';
this._paintLists();
}

_removeId(which, id) {
if (which === 'blocklist') {
this._blocklist = this._blocklist.filter((x) => x !== id);
} else {
this._priority = this._priority.filter((x) => x !== id);
}
this._paintLists();
}

_paintLists() {
this._blocklistEl.innerHTML = this._listHtml('blocklist', this._blocklist);
this._priorityEl.innerHTML = this._listHtml('priority', this._priority);
this._blocklistEl.querySelectorAll('[data-remove-id]').forEach((btn) => {
btn.addEventListener('click', () => {
this._removeId(btn.dataset.list, btn.dataset.id);
});
});
this._priorityEl.querySelectorAll('[data-remove-id]').forEach((btn) => {
btn.addEventListener('click', () => {
this._removeId(btn.dataset.list, btn.dataset.id);
});
});
}

_listHtml(which, ids) {
if (!ids.length) {
return '<p class="cfg-id-empty">None</p>';
}
return ids.map((id) => `
<div class="cfg-id-row">
<code class="cfg-id-chip">!${this._api.escape(id)}</code>
<button type="button" class="cfg-id-remove" data-remove-id
data-list="${which}" data-id="${this._api.escape(id)}"
aria-label="Remove ${id}">×</button>
</div>
`).join('');
}

async _onSubmit(event) {
event.preventDefault();
const ttl = Number(this._root.querySelector('[data-dedup-ttl]').value);
if (!Number.isFinite(ttl) || ttl < 5 || ttl > 3600) {
this._setStatus('error', 'Dedup TTL must be between 5 and 3600 seconds.');
return;
}
this._setStatus('pending', 'Saving…');
const result = await this._api.put('/api/config/relay', {
blocklist: this._blocklist,
priority_list: this._priority,
dedup_ttl_seconds: ttl,
});
if (!result) {
this._setStatus('error', 'Save failed.');
return;
}
this._setStatus('success', 'Saved.');
if (result.restart_required) {
this._api.signalRestart('Relay settings updated.');
} else {
this._api.toast('Relay filters applied (no restart required).');
}
this._api.refresh();
}

_setStatus(kind, message) {
if (!this._statusEl) return;
this._statusEl.dataset.kind = kind;
this._statusEl.textContent = message;
}
}

window.RelayFiltersCard = RelayFiltersCard;
Loading
Loading