Add ukmesh.com frontend and MQTT infrastructure

www.ukmesh.com (website-ukmesh container, port 3004):
- UKLayout, UKHomePage, UKInstallPage, UKMqttPage components
- VITE_SITE=ukmesh build arg routes to UK pages in main.tsx
- Stats fetched via relative /api path (proxied by Nginx)

app.ukmesh.com (app-ukmesh container, port 3003):
- Same dashboard build with VITE_APP_HOSTNAME=app.ukmesh.com
- VITE_NETWORK=ukmesh for future API filtering

MQTT:
- node-ukmesh Mosquitto user for ukmesh observer nodes
- ACL updated: backend reads meshcore/# and ukmesh/#
- ukmesh nodes publish to ukmesh/{IATA}/{PUBKEY}/packets

Cloudflare routes to add in Zero Trust:
  www.ukmesh.com  -> http://website-ukmesh:80
  app.ukmesh.com  -> http://app-ukmesh:80
  mqtt.ukmesh.com -> http://mosquitto:9001

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BenandClaude Sonnet 4.6 committed 2026-03-04 11:16:26 +00:00
1 parent a946a07488
commit 8865a6444d
11 files changed
+721 -9

No files matched your search

+2
View File
@@ -5,7 +5,9 @@ COPY frontend/package.json ./
RUN npm install
COPY frontend/ ./
ARG VITE_APP_HOSTNAME
ARG VITE_NETWORK=teesside
ENV VITE_APP_HOSTNAME=$VITE_APP_HOSTNAME
ENV VITE_NETWORK=$VITE_NETWORK
RUN npm run build
FROM nginx:alpine
+2
View File
@@ -4,7 +4,9 @@ COPY frontend/package.json ./
RUN npm install
COPY frontend/ ./
ARG VITE_APP_HOSTNAME
ARG VITE_SITE=teesside
ENV VITE_APP_HOSTNAME=$VITE_APP_HOSTNAME
ENV VITE_SITE=$VITE_SITE
RUN npm run build
FROM nginx:alpine
+29
View File
@@ -103,12 +103,39 @@ services:
dockerfile: Dockerfile.website
args:
VITE_APP_HOSTNAME: ${VITE_APP_HOSTNAME:-}
VITE_SITE: teesside
restart: unless-stopped
ports:
- "3002:80"
depends_on:
- backend
app-ukmesh:
build:
context: .
dockerfile: Dockerfile.app
args:
VITE_APP_HOSTNAME: app.ukmesh.com
VITE_NETWORK: ukmesh
restart: unless-stopped
depends_on:
- backend
ports:
- "3003:80"
website-ukmesh:
build:
context: .
dockerfile: Dockerfile.website
args:
VITE_APP_HOSTNAME: app.ukmesh.com
VITE_SITE: ukmesh
restart: unless-stopped
ports:
- "3004:80"
depends_on:
- backend
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
@@ -120,6 +147,8 @@ services:
- backend
- app
- website
- app-ukmesh
- website-ukmesh
- mosquitto
volumes:
+29 -3
View File
@@ -116,6 +116,32 @@ function distKm(a: MeshNode, b: MeshNode): number {
return Math.hypot(dlat, dlon);
}
// ── Line-of-sight check (smooth Earth, k=0.25 refraction) ────────────────────
const R_EFF_M = 6_371_000 / (1 - 0.25); // ~8,495,000 m effective Earth radius
/**
* Smooth-Earth LOS check with atmospheric refraction k=0.25 (equivalent to
* the standard 4/3-Earth model). Uses each node's elevation_m (terrain ASL)
* plus 5 m antenna height. Samples 20 evenly-spaced points along the path
* and rejects the hop if Earth curvature alone would block the ray at any
* point. Does not model terrain between the nodes — confirmed links already
* have full ITM terrain analysis; this guards unconfirmed candidates.
*/
function hasLoS(a: MeshNode, b: MeshNode): boolean {
const hA = (a.elevation_m ?? 0) + 5; // metres ASL at antenna
const hB = (b.elevation_m ?? 0) + 5;
const d = distKm(a, b) * 1000; // metres
if (d < 1) return true;
for (let i = 1; i < 20; i++) {
const t = i / 20;
const x = t * d;
const los = hA + (hB - hA) * t;
const bulge = x * (d - x) / (2 * R_EFF_M);
if (los < bulge) return false;
}
return true;
}
// Fallback range check (used when no ITM data exists for a pair).
function nodeRange(nodeId: string, coverage: NodeCoverage[]): number {
const cov = coverage.find((c) => c.node_id === nodeId);
@@ -169,15 +195,15 @@ function resolveBetaPath(
.map((c) => { usedIds.add(c.node_id); return { node: c, conf: 0.9 }; });
const reachable = all
.filter((c) => !usedIds.has(c.node_id) && canReach(c, prevNode, coverage))
.filter((c) => !usedIds.has(c.node_id) && canReach(c, prevNode, coverage) && hasLoS(c, prevNode))
.sort((a, b) => distKm(a, prevNode) - distKm(b, prevNode))
.slice(0, 2)
.map((c) => { usedIds.add(c.node_id); return { node: c, conf: 0.3 / Math.max(1, all.length) }; });
// Last resort: closest prefix match within 150 km, even if outside coverage radius.
// Last resort: closest prefix match within 50 km with LOS clearance.
// Prevents hops being silently dropped just because link data is sparse.
const fallback = all
.filter((c) => !usedIds.has(c.node_id) && distKm(c, prevNode) < 150)
.filter((c) => !usedIds.has(c.node_id) && distKm(c, prevNode) < 50 && hasLoS(c, prevNode))
.sort((a, b) => distKm(a, prevNode) - distKm(b, prevNode))
.slice(0, 1)
.map((c) => ({ node: c, conf: 0.05 / Math.max(1, all.length) }));
+22 -3
View File
@@ -10,19 +10,38 @@ import { OpenSourcePage } from './pages/OpenSourcePage.js';
import { MqttPage } from './pages/MqttPage.js';
import { StatsPage } from './pages/StatsPage.js';
import { PacketsPage } from './pages/PacketsPage.js';
import { UKLayout } from './pages/ukmesh/UKLayout.js';
import { UKHomePage } from './pages/ukmesh/UKHomePage.js';
import { UKInstallPage } from './pages/ukmesh/UKInstallPage.js';
import { UKMqttPage } from './pages/ukmesh/UKMqttPage.js';
import './styles/globals.css';
const root = document.getElementById('root')!;
const { hostname } = window.location;
const APP_HOSTNAME = import.meta.env['VITE_APP_HOSTNAME'];
const isAppDomain = !APP_HOSTNAME || hostname === APP_HOSTNAME;
const SITE = import.meta.env['VITE_SITE'] ?? 'teesside';
const isAppDomain = !APP_HOSTNAME || hostname === APP_HOSTNAME;
document.title = isAppDomain ? 'MeshCore Analytics' : 'Teesside Mesh';
document.title = isAppDomain
? 'MeshCore Analytics'
: SITE === 'ukmesh' ? 'UK Mesh Network' : 'Teesside Mesh';
ReactDOM.createRoot(root).render(
<React.StrictMode>
{isAppDomain ? (
<App />
) : SITE === 'ukmesh' ? (
<BrowserRouter>
<Routes>
<Route element={<UKLayout />}>
<Route index element={<UKHomePage />} />
<Route path="about" element={<AboutPage />} />
<Route path="install" element={<UKInstallPage />} />
<Route path="mqtt" element={<UKMqttPage />} />
<Route path="open-source" element={<OpenSourcePage />} />
</Route>
</Routes>
</BrowserRouter>
) : (
<BrowserRouter>
<Routes>
@@ -31,8 +50,8 @@ ReactDOM.createRoot(root).render(
<Route path="about" element={<AboutPage />} />
<Route path="install" element={<InstallPage />} />
<Route path="mqtt" element={<MqttPage />} />
<Route path="open-source" element={<OpenSourcePage />} />
<Route path="packets" element={<PacketsPage />} />
<Route path="open-source" element={<OpenSourcePage />} />
<Route path="stats" element={<StatsPage />} />
</Route>
</Routes>
+192
View File
@@ -0,0 +1,192 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
interface SiteStats {
packetsDay: number;
totalNodes: number;
longestHop: number;
longestHopHash: string | null;
}
function useFlash(value: number) {
const [flash, setFlash] = useState(false);
const prev = useRef(value);
useEffect(() => {
if (value !== prev.current) {
prev.current = value;
setFlash(true);
const t = setTimeout(() => setFlash(false), 600);
return () => clearTimeout(t);
}
}, [value]);
return flash;
}
const StatCard: React.FC<{ value: number; label: string; suffix?: string; format?: (n: number) => string }> = ({
value, label, suffix = '', format,
}) => {
const flash = useFlash(value);
const display = format ? format(value) : value.toLocaleString();
return (
<div className="site-stat">
<span className={`site-stat__value${flash ? ' tick-flash' : ''}`}>
{display}{suffix && <span className="site-stat__suffix">{suffix}</span>}
</span>
<span className="site-stat__label">{label}</span>
</div>
);
};
export const UKHomePage: React.FC = () => {
const [stats, setStats] = useState<SiteStats>({ packetsDay: 0, totalNodes: 0, longestHop: 0, longestHopHash: null });
const hopFlash = useFlash(stats.longestHop);
useEffect(() => {
const fetch_ = () =>
fetch('/api/stats')
.then(r => r.json())
.then(d => setStats({ packetsDay: d.packetsDay, totalNodes: d.totalNodes, longestHop: d.longestHop, longestHopHash: d.longestHopHash ?? null }))
.catch(() => {});
fetch_();
const t = setInterval(fetch_, 30_000);
return () => clearInterval(t);
}, []);
return (
<>
{/* ── Hero ─────────────────────────────────────────────────────── */}
<section className="site-hero">
<div className="site-hero__glow" aria-hidden />
<div className="site-content">
<div className="site-hero__badge">United Kingdom · LoRa 868 MHz</div>
<h1 className="site-hero__title">
UK<br />
<span className="site-hero__title--accent">Mesh Network</span>
</h1>
<p className="site-hero__sub">
A UK-wide off-grid communications network built on{' '}
<a href="https://meshcore.co.uk" target="_blank" rel="noopener noreferrer">MeshCore</a>,{' '}
a free, open-source LoRa mesh platform. No internet. No infrastructure. Just radio.
</p>
<div className="site-hero__actions">
<a href="https://app.ukmesh.com" className="site-btn site-btn--primary">Open Live Map →</a>
<Link to="/about" className="site-btn site-btn--ghost">Learn more</Link>
</div>
</div>
</section>
{/* ── Live stats ───────────────────────────────────────────────── */}
<section className="site-stats-section">
<div className="site-content">
<p className="site-stats-section__eyebrow">Live network stats · updates every 30s</p>
<div className="site-stats-grid">
<StatCard value={stats.packetsDay} label="Packets in the last 24 hours" />
<StatCard value={stats.totalNodes} label="Nodes ever heard on the network" />
<div className="site-stat">
<span className={`site-stat__value${hopFlash ? ' tick-flash' : ''}`}>
{stats.longestHop.toLocaleString()}<span className="site-stat__suffix"> hops</span>
</span>
<span className="site-stat__label">Longest relay chain ever recorded</span>
{stats.longestHopHash && (
<span className="site-stat__hash" title={stats.longestHopHash}>
{stats.longestHopHash}
</span>
)}
</div>
</div>
</div>
</section>
{/* ── Radio config ─────────────────────────────────────────────── */}
<section className="site-stats-section site-stats-section--alt">
<div className="site-content">
<p className="site-stats-section__eyebrow">Network radio configuration</p>
<div className="site-stats-grid site-stats-grid--6">
<div className="site-stat">
<span className="site-stat__value">EU/UK Narrow</span>
<span className="site-stat__label">Profile</span>
</div>
<div className="site-stat">
<span className="site-stat__value">869.618</span>
<span className="site-stat__label">Frequency (MHz)</span>
</div>
<div className="site-stat">
<span className="site-stat__value">62.5<span className="site-stat__suffix">kHz</span></span>
<span className="site-stat__label">Bandwidth</span>
</div>
<div className="site-stat">
<span className="site-stat__value">SF8</span>
<span className="site-stat__label">Spreading Factor</span>
</div>
<div className="site-stat">
<span className="site-stat__value">CR8</span>
<span className="site-stat__label">Coding Rate</span>
</div>
</div>
</div>
</section>
{/* ── About cards ─────────────────────────────────────────────── */}
<section className="site-section">
<div className="site-content site-cards-row">
<div className="site-card">
<div className="site-card__icon">📡</div>
<h2 className="site-card__title">What is MeshCore?</h2>
<p className="site-card__body">
MeshCore is open-source firmware for ESP32 LoRa hardware. Each node acts as
both a radio and a relay. Packets hop between nodes automatically, extending
range far beyond what a single radio can achieve.
</p>
<Link to="/about" className="site-card__link">Learn more →</Link>
</div>
<div className="site-card">
<div className="site-card__icon">🔧</div>
<h2 className="site-card__title">Become an observer</h2>
<p className="site-card__body">
Connect your repeater node to the UK Mesh MQTT broker and contribute live
packet data from your area. All you need is a Linux device, a USB cable,
and about 15 minutes.
</p>
<Link to="/mqtt" className="site-card__link">Observer setup →</Link>
</div>
<div className="site-card">
<div className="site-card__icon">🗺️</div>
<h2 className="site-card__title">Live map and analytics</h2>
<p className="site-card__body">
The live dashboard shows every packet heard across the UK in real time: node
positions, relay paths, RF coverage, and a decoded packet feed. Built entirely
on open source tools.
</p>
<a href="https://app.ukmesh.com" className="site-card__link">Open map →</a>
</div>
</div>
</section>
{/* ── Discord CTA ─────────────────────────────────────────────── */}
<section className="site-section site-section--dark">
<div className="site-content site-cta">
<div className="site-cta__text">
<h2 className="site-cta__title">Join the conversation</h2>
<p className="site-cta__body">
We hang out on the MeshCore Discord. Come say hello, ask questions, or
coordinate coverage with other UK operators. DM <strong>ibengr</strong> to
get set up as an observer.
</p>
</div>
<a
href="https://discord.gg/bSuST8xvet"
target="_blank"
rel="noopener noreferrer"
className="site-btn site-btn--primary"
>
Join Discord →
</a>
</div>
</section>
</>
);
};
+153
View File
@@ -0,0 +1,153 @@
import React from 'react';
export const UKInstallPage: React.FC = () => (
<>
<section className="site-page-hero">
<div className="site-content">
<h1 className="site-page-hero__title">Install MeshCore</h1>
<p className="site-page-hero__sub">
Get a companion node on the air in about 10 minutes. No soldering, no special tools. Just a browser, a USB cable, and a phone.
</p>
</div>
</section>
<div className="site-content site-prose">
<section className="prose-section">
<h2>
<span className="prose-step">1</span>
What you need
</h2>
<p>
A companion node is a handheld or portable device you use to send and receive messages on the mesh.
Any ESP32-based LoRa board supported by MeshCore will work. The most common choice in the UK
is the <strong>Heltec WiFi LoRa 32 V4</strong>. It has a built-in OLED display,
USB-C charging, integrated battery management, and costs around &pound;25&ndash;&pound;35.
</p>
<div className="hw-cards">
<div className="hw-card hw-card--recommended">
<div className="hw-card__badge">Recommended</div>
<div className="hw-card__name">Heltec WiFi LoRa 32 V4</div>
<div className="hw-card__detail">ESP32-S3 · 868 MHz · OLED · USB-C · Li-Po</div>
</div>
<div className="hw-card">
<div className="hw-card__name">Heltec WiFi LoRa 32 V3</div>
<div className="hw-card__detail">ESP32 · 868 MHz · OLED · USB-C</div>
</div>
<div className="hw-card">
<div className="hw-card__name">LILYGO T3S3</div>
<div className="hw-card__detail">ESP32-S3 · 868 MHz · E-Paper optional</div>
</div>
<div className="hw-card">
<div className="hw-card__name">Heltec Mesh Node T114</div>
<div className="hw-card__detail">nRF52840 · 868 MHz · Ultra-compact</div>
</div>
</div>
<p className="prose-note">
<strong>You will also need:</strong> a USB cable to match your board (USB-C for V4/V3), an Android
phone to run the MeshCore companion app, and optionally a 3.7 V Li-Po battery to run the board untethered.
</p>
</section>
<section className="prose-section">
<h2>
<span className="prose-step">2</span>
Flash the firmware
</h2>
<p>
Everything is done in the browser, no software to install. You need Chrome or Edge (Web Serial API required).
</p>
<ol className="prose-steps">
<li>
Connect your board to your PC via USB and open the{' '}
<a href="https://flasher.meshcore.co.uk/" target="_blank" rel="noopener noreferrer">
MeshCore web flasher
</a>.
</li>
<li>
Select your <strong>device hardware</strong> from the dropdown, then select{' '}
<strong>Companion Radio (Bluetooth)</strong> as the firmware type.
</li>
<li>
Click <strong>Enter DFU Mode</strong>, then <strong>Erase Flash</strong>, then <strong>Flash</strong>.
The flash takes about 30 seconds.
</li>
<li>
Once complete, your board will reboot and show the MeshCore splash screen on its OLED (if it has one).
</li>
</ol>
<p className="prose-note">
If your board does not appear in the port list, try holding the <strong>BOOT</strong> button
while plugging in the USB cable to enter bootloader mode.
</p>
</section>
<section className="prose-section">
<h2>
<span className="prose-step">3</span>
Configure your node
</h2>
<p>
Install the <strong>MeshCore</strong> companion app on your Android phone
(search for "MeshCore" on Google Play). iOS support is in development.
</p>
<ol className="prose-steps">
<li>
Open the MeshCore app and tap <strong>Add device</strong>. Your node will appear in the
Bluetooth scan.
</li>
<li>
Set a <strong>node name</strong>. Your callsign, name, or location works well.
</li>
<li>
Set the <strong>device role</strong> to <em>Client</em> for a handheld companion node.
</li>
<li>
Set the radio to the UK network configuration:
<br />
<strong>Profile:</strong> EU/UK Narrow &nbsp;|&nbsp;
<strong>Freq:</strong> 869.618 MHz &nbsp;|&nbsp;
<strong>BW:</strong> 62.5 kHz &nbsp;|&nbsp;
<strong>SF8 / CR8</strong>
</li>
<li>
Leave the channel set to the default <strong>Public</strong> channel.
</li>
<li>
Add your <strong>GPS coordinates</strong> so the network can place you on the map.
</li>
</ol>
</section>
<section className="prose-section">
<h2>
<span className="prose-step">4</span>
Get on the network
</h2>
<p>
If your node is on and configured to the Public channel, and within range of any
UK Mesh repeater, you should be able to send and receive messages straight away.
Send a message in the Public channel. If it shows <strong>"Heard X Repeats"</strong> instead
of just "Sent", you are on the network.
</p>
<p>
Check the{' '}
<a href="https://app.ukmesh.com">live map</a>{' '}
to see if your node appears. Come say hello on the MeshCore Discord.
DM <strong>ibengr</strong> if you have any questions.
</p>
<a
href="https://discord.gg/bSuST8xvet"
target="_blank"
rel="noopener noreferrer"
className="site-btn site-btn--primary"
>
Join us on Discord →
</a>
</section>
</div>
</>
);
+64
View File
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { NavLink, Outlet, Link, useNavigate } from 'react-router-dom';
const APP_URL = 'https://app.ukmesh.com';
export const UKLayout: React.FC = () => {
const [menuOpen, setMenuOpen] = useState(false);
const navigate = useNavigate();
const closeMenu = () => setMenuOpen(false);
const handleNavClick = (to: string) => { closeMenu(); navigate(to); };
return (
<div className="site-layout">
<nav className="site-nav">
<Link to="/" className="site-nav__brand" onClick={closeMenu}>
<span className="site-nav__icon">◈</span>
<span className="site-nav__name">UK Mesh</span>
</Link>
<div className={`site-nav__links${menuOpen ? ' site-nav__links--open' : ''}`}>
<NavLink to="/" end onClick={() => handleNavClick('/')} className={({ isActive }) => isActive ? 'site-nav__link site-nav__link--active' : 'site-nav__link'}>
Home
</NavLink>
<NavLink to="/about" onClick={() => handleNavClick('/about')} className={({ isActive }) => isActive ? 'site-nav__link site-nav__link--active' : 'site-nav__link'}>
What is MeshCore
</NavLink>
<NavLink to="/install" onClick={() => handleNavClick('/install')} className={({ isActive }) => isActive ? 'site-nav__link site-nav__link--active' : 'site-nav__link'}>
Install
</NavLink>
<NavLink to="/mqtt" onClick={() => handleNavClick('/mqtt')} className={({ isActive }) => isActive ? 'site-nav__link site-nav__link--active' : 'site-nav__link'}>
MQTT
</NavLink>
<NavLink to="/open-source" onClick={() => handleNavClick('/open-source')} className={({ isActive }) => isActive ? 'site-nav__link site-nav__link--active' : 'site-nav__link'}>
Open Source
</NavLink>
<a href={APP_URL} className="site-nav__link">Live Map</a>
</div>
<button
className="site-nav__hamburger"
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
onClick={() => setMenuOpen((o) => !o)}
>
{menuOpen ? '✕' : '☰'}
</button>
</nav>
<main className="site-main">
<Outlet />
</main>
<footer className="site-footer">
<span>UK Mesh Network</span>
<span className="site-footer__sep">·</span>
<a href="https://discord.gg/bSuST8xvet" target="_blank" rel="noopener noreferrer">Discord</a>
<span className="site-footer__sep">·</span>
<Link to="/open-source">Open Source</Link>
<span className="site-footer__sep">·</span>
<a href={APP_URL}>Live Map</a>
</footer>
</div>
);
};
+211
View File
@@ -0,0 +1,211 @@
import React from 'react';
export const UKMqttPage: React.FC = () => (
<>
<section className="site-page-hero">
<div className="site-content">
<h1 className="site-page-hero__title">MQTT Observer Setup</h1>
<p className="site-page-hero__sub">
Connect your repeater node to the UK Mesh MQTT broker and contribute live packet
data to the national dashboard.
</p>
</div>
</section>
<div className="site-content site-prose">
<section className="prose-section">
<h2>What is this?</h2>
<p>
<strong>meshcoretomqtt</strong> is software that runs on a Linux device (such as a
Raspberry Pi) connected to your MeshCore repeater node via USB. It reads the packet
stream from the node's serial port and publishes it to an MQTT broker over the internet.
</p>
<p>
Once connected, every packet your node hears will appear on the live map in real time,
your node's position and RF coverage will be shown, and you will be contributing to the
UK-wide picture of the network.
</p>
<div className="prose-note">
<strong>Access is by request.</strong> Message <strong>ibengr</strong> on the{' '}
<a href="https://discord.gg/bSuST8xvet" target="_blank" rel="noopener noreferrer">Discord</a>{' '}
to get your credentials before going through this setup.
</div>
</section>
<section className="prose-section">
<h2>
<span className="prose-step">1</span>
Flash firmware with packet logging
</h2>
<p>
We use the pre-built firmware from the team at{' '}
<a href="https://analyzer.letsmesh.net" target="_blank" rel="noopener noreferrer">letsmesh</a>,
a great project for global MeshCore network stats. You will need their firmware with
packet logging enabled. The current version is <strong>1.13.0</strong>.
</p>
<ol className="prose-steps">
<li>
Go to the{' '}
<a href="https://analyzer.letsmesh.net/observer/onboard?type=repeater" target="_blank" rel="noopener noreferrer">
letsmesh firmware flasher
</a>{' '}
and select your device variant.
</li>
<li>Choose <strong>Custom</strong> as the flash option to get the packet logging build.</li>
<li>
Connect your node via USB and flash it using the web flasher. No software install
required, just Chrome or Edge.
</li>
</ol>
<p className="prose-note">
This only applies if you are setting up a fresh node. If your repeater is already running
1.13.0 from letsmesh it likely already has packet logging enabled.
</p>
</section>
<section className="prose-section">
<h2>
<span className="prose-step">2</span>
Run the installer
</h2>
<p>
Connect your node to your Raspberry Pi (or other Linux device) via USB, then run the
install script:
</p>
<div className="code-block">
<pre>{'curl -fsSL https://raw.githubusercontent.com/Cisien/meshcoretomqtt/main/install.sh | bash'}</pre>
</div>
<p className="prose-note">
meshcoretomqtt is for <strong>Repeater</strong> or <strong>Room Server</strong> nodes only.
</p>
<p>
The script will walk you through setup interactively. Key choices to make:
</p>
<h3>Enable LetsMesh Packet Analyzer</h3>
<p>
Choose <strong>y</strong>. This enables packet logging and registers your node with
the letsmesh global stats platform.
</p>
<div className="code-block">
<pre>{'Enable LetsMesh Packet Analyzer MQTT servers? [Y/n]: y'}</pre>
</div>
<h3>IATA region code</h3>
<p>
Enter the three-letter IATA code for your nearest commercial airport. Some examples:
</p>
<div className="prose-facts">
<div className="prose-fact">
<span className="prose-fact__value">MME</span>
<span className="prose-fact__label">Teesside</span>
</div>
<div className="prose-fact">
<span className="prose-fact__value">NCL</span>
<span className="prose-fact__label">Newcastle</span>
</div>
<div className="prose-fact">
<span className="prose-fact__value">MAN</span>
<span className="prose-fact__label">Manchester</span>
</div>
<div className="prose-fact">
<span className="prose-fact__value">LHR</span>
<span className="prose-fact__label">London</span>
</div>
<div className="prose-fact">
<span className="prose-fact__value">LBA</span>
<span className="prose-fact__label">Leeds</span>
</div>
<div className="prose-fact">
<span className="prose-fact__value">BHX</span>
<span className="prose-fact__label">Birmingham</span>
</div>
</div>
<p className="prose-note">
Use the correct IATA code for your location. A quick search will confirm the right one.
</p>
<h3>Add the UK Mesh broker</h3>
<p>
When asked if you'd like to configure additional MQTT brokers, choose <strong>y</strong>{' '}
and add <strong>1</strong> broker. Enter the following details using the credentials
you received from ibengr:
</p>
<div className="code-block">
<pre>{`Server hostname/IP: mqtt.ukmesh.com
Port [1883]: 443
Use WebSockets transport? [y/N]: y
Use TLS/SSL encryption? [y/N]: y
Verify TLS certificates? [Y/n]: y
Choose authentication method [1-3] [1]: 1
Username: <your username>
Password: <your password>`}</pre>
</div>
<h3>Custom topic</h3>
<p>
The UK Mesh broker uses a different topic prefix from the default. After the installer
finishes, add the following to your config at{' '}
<code>~/.meshcoretomqtt/.env.local</code> (replacing <code>MQTT3_</code> with whichever
broker slot you used):
</p>
<div className="code-block">
<pre>{'MCTOMQTT_MQTT3_TOPIC_PACKETS=ukmesh/{IATA}/{PUBLIC_KEY}/packets'}</pre>
</div>
<p>
Then restart the service:
</p>
<div className="code-block">
<pre>{'sudo systemctl restart mctomqtt'}</pre>
</div>
<p>
Your node should appear on the{' '}
<a href="https://app.ukmesh.com" target="_blank" rel="noopener noreferrer">live map</a>{' '}
within a few minutes once an advert packet is heard.
</p>
</section>
<section className="prose-section">
<h2>Tips for a stable setup</h2>
<ul>
<li>
<strong>Use a good USB cable.</strong> Cheap or long cables cause intermittent serial
disconnects. Keep the node physically away from the Pi to avoid RF noise on 868 MHz.
</li>
<li>
<strong>Disable USB autosuspend</strong> for the node. Find your device vendor ID with{' '}
<code>lsusb</code>, then create a udev rule:
<div className="code-block" style={{ marginTop: '10px' }}>
<pre>{'ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="XXXX", ATTR{power/autosuspend}="-1"'}</pre>
</div>
</li>
<li>
<strong>If the node stops sending data</strong>, check{' '}
<code>sudo journalctl -u mctomqtt -n 20</code>. If the serial watchdog is cycling,
run <code>sudo systemctl restart mctomqtt</code>. If the serial port has disappeared,
reseat the USB cable on the node.
</li>
</ul>
</section>
<section className="prose-section prose-section--muted">
<h2>Get access</h2>
<p>
Message <strong>ibengr</strong> on the MeshCore Discord with your node name, location,
and IATA code and we will get you set up with credentials for the UK Mesh broker.
</p>
<a
href="https://discord.gg/bSuST8xvet"
target="_blank"
rel="noopener noreferrer"
className="site-btn site-btn--primary"
style={{ marginTop: '8px', display: 'inline-flex' }}
>
Join Discord →
</a>
</section>
</div>
</>
);
+10
View File
@@ -1 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_HOSTNAME: string;
readonly VITE_SITE: string; // 'teesside' | 'ukmesh'
readonly VITE_NETWORK: string; // 'teesside' | 'ukmesh'
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+7 -3
View File
@@ -1,8 +1,12 @@
# backend service — subscribe to everything (used by the analytics app)
# backend service — subscribe to all network topic prefixes
user backend
topic readwrite meshcore/#
topic readwrite ukmesh/#
# node observers — publish to meshcore/{IATA}/{PUBKEY}/packets
# IATA and pubkey are not known at registration time so allow the full namespace
# teesside node observers — publish to meshcore/{IATA}/{PUBKEY}/packets
user node1
topic write meshcore/#
# ukmesh node observers — publish to ukmesh/{IATA}/{PUBKEY}/packets
user node-ukmesh
topic write ukmesh/#