mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-23 06:12:27 +00:00
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:
1 parent
a946a07488
commit
8865a6444d
11 files changed
+721
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 £25–£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 |
|
||||
<strong>Freq:</strong> 869.618 MHz |
|
||||
<strong>BW:</strong> 62.5 kHz |
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
Vendored
+10
@@ -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
@@ -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/#
|
||||
Reference in new issue
Block a user