First, Some Thank-Yous
None of this would exist without the Reverse Beacon Network and the people who keep it running. The RBN is operated by a dedicated group of volunteers, and the network itself depends on dozens of skimmer owners around the world who leave their radios and computers on around the clock, quietly listening and reporting. Every S-meter reading in this app is built from their work. Thank you.
Since the first post went up, a lot of people have taken the time to write in with feedback, suggestions, and bug reports. A few things that came directly from your messages: the color-blind safe theme, the mode badge reordering to put SSB right after CW, the WARC bands in the badge API, and the idea for the web badge API itself. You know who you are. Much appreciated.
What Is New Since the Last Post
The original post introduced the RBN S-Meter as a progressive web app running at rbsm.rm.gl. Here is what has changed since then.
Three Color Themes
The app now has a three-position theme selector sitting right next to the Reset button: Dark, Light, and CB.
Dark is the original green-on-black look, which is easy on the eyes at night and feels at home next to radio logging software. Light flips to a clean white background for bright environments or for embedding screenshots in documents. The CB (color-blind safe) theme replaces the green/red color scheme with blue and amber, which remain visually distinct for the most common forms of color vision deficiency.
Your choice is saved and restored the next time you open the app.
Mode Badges
Below each S-meter bar, the app now shows four small badges: CW, SSB, RY, and FTx. A green checkmark means the RBN is currently seeing that mode on that band from that region. A red X means spots are coming in but not on that mode. A dim badge means no data at all.
SSB is a special case because the RBN does not directly spot SSB signals. Instead, the app infers SSB workability from the CW signal-to-noise ratio: if skimmers in your area are hearing CW signals at a strong enough level, the ionosphere is clearly in good shape and SSB is likely workable too.
The order CW, SSB, RY, FTx was chosen to reflect the rough order of activity you would expect to find on most HF bands.
WARC Bands
The app and the badge API now include 60m, 30m, 17m, and 12m alongside the traditional contest bands.
How It Works: The Tech Stack
You do not need to know any of this to use the app, but a few people have asked how the pieces fit together. Here is a plain-language explanation.
┌─────────────────────────────────────────────────────────────────┐
│ RBN TELNET FEED │
│ telnet.reversebeacon.net:7000 │
│ (live stream of CW/RTTY/FT8 spots from global skimmers) │
└───────────────────────────┬─────────────────────────────────────┘
│ raw spot lines, one per signal heard
▼
┌─────────────────────────────────────────────────────────────────┐
│ rbn-feed │
│ (Node.js, runs inside Docker) │
│ Maintains a persistent telnet connection to the RBN. │
│ Converts each spot line into a tidy JSON message and │
│ broadcasts it over an internal WebSocket to any listener. │
└───────────────────────────┬─────────────────────────────────────┘
│ WebSocket (internal Docker network)
▼
┌─────────────────────────────────────────────────────────────────┐
│ rbn-proxy │
│ (Node.js, runs inside Docker) │
│ Subscribes to rbn-feed and keeps a rolling 2-minute window │
│ of spots in memory. Also fetches solar data (SFI, K-index, │
│ solar wind) from NOAA. Serves everything via simple HTTP │
│ endpoints that the browser app and badge API both use. │
└────────────────┬──────────────────────────┬─────────────────────┘
│ │
│ HTTP /rbn │ HTTP /rbn
│ HTTP /solar │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────────────┐
│ rbn-smeter │ │ rbn-badge │
│ (nginx + PWA, Docker) │ │ (Node.js + canvas, Docker) │
│ │ │ │
│ Serves the web app files │ │ Fetches spot data from proxy, │
│ (HTML, CSS, JavaScript). │ │ computes median SNR per band │
│ Your browser runs the │ │ and region, and renders a PNG │
│ app and polls /rbn every │ │ badge image on the fly. │
│ 15 seconds. │ │ Caches results for 10 minutes. │
└───────────────────────────┘ └───────────────────────────────────┘
▲ ▲
│ HTTPS │ HTTPS /badge/...
│ │
Your browser Any website or Slack bot
The key thing to understand is that the RBN is a real telnet service, like the packet cluster networks many operators are familiar with. The feed container maintains a single persistent connection to that service and shares the data internally so that nothing else in the stack needs to manage the connection.
The proxy keeps only a two-minute rolling window of spots. This means the S-meter readings are genuinely current: if a band closes, the readings drop within two minutes. There is no stale data sitting in a database inflating the numbers.
The badge service sits alongside the proxy and draws PNG images on demand using the same spot data the web app uses. No screenshots, no browser, just server-side canvas rendering.
Everything runs in Docker containers on a single VPS, and nginx sits in front routing traffic to the right container.
The Web Badge API
The badge API lets you embed live RBN signal level indicators directly in web pages, forum signatures, club websites, or anywhere else that accepts an image tag. Each badge is a PNG image generated fresh from current RBN data.
The base URL is:
https://rbsm.rm.gl/badge/
There are two endpoints: /badge/region for region-to-region paths, and /badge/grid for grid-square-based paths.
Live Examples
Here are some live badges pulling current data right now. Each image refreshes every 60 seconds.
ENA to ENA, 40m, dark theme, small:
ENA to ENA, 20m, light theme, small:
ENA to ENA, 80m, dark theme, full size:
ENA to ENA, all bands, dark theme:
Parameters
/badge/region endpoint
| Parameter | Values | Default | Notes |
|---|---|---|---|
from | ENA, CNA, WNA, SA, EU, AF, AS, OC | ENA | Transmitting region (your skimmer vantage) |
to | ENA, CNA, WNA, SA, EU, AF, AS, OC | EU | Receiving region (target DX area) |
band | 160m, 80m, 60m, 40m, 30m, 20m, 17m, 15m, 12m, 10m, 6m, all | 20m | all returns a single image with all bands |
theme | dark, light | dark | Color scheme |
size | small, full | small | small = 220x56px, full = 380x80px |
/badge/grid endpoint
Same as above, but replaces from with:
| Parameter | Values | Default | Notes |
|---|---|---|---|
grid | Any valid Maidenhead grid (4 or 6 chars) | required | e.g. FN64 |
radius | 125, 250, 500, 1000, 1500, 2000, 5000 | 500 | Miles radius for skimmer selection |
Region key reference:
| Key | Region |
|---|---|
| ENA | Eastern North America |
| CNA | Central North America |
| WNA | Western North America |
| SA | South America |
| EU | Europe |
| AF | Africa |
| AS | Asia |
| OC | Oceania |
Example API Calls
# ENA to EU on 40m, dark, small
https://rbsm.rm.gl/badge/region?from=ENA&to=EU&band=40m&theme=dark&size=small
# ENA to EU on 20m, light, full size
https://rbsm.rm.gl/badge/region?from=ENA&to=EU&band=20m&theme=light&size=full
# All bands, ENA to ENA, dark
https://rbsm.rm.gl/badge/region?from=ENA&to=ENA&band=all&theme=dark
# Grid mode: 500mi radius around FN64, to EU, 20m
https://rbsm.rm.gl/badge/grid?grid=FN64&to=EU&band=20m&theme=dark&size=small
# Grid mode: 1000mi radius, all bands
https://rbsm.rm.gl/badge/grid?grid=FN64&to=ENA&band=all&radius=1000&theme=dark
# Health check (returns JSON)
https://rbsm.rm.gl/badge/health
Embedding in a Web Page
Drop an image tag anywhere you would normally put an image. The browser will cache it for 60 seconds and then fetch a fresh one:
<!-- Single band badge -->
<img src="https://rbsm.rm.gl/badge/region?from=ENA&to=EU&band=20m&theme=dark&size=small"
alt="RBN S-Meter: ENA to EU on 20m"
style="display:block;">
<!-- All-bands panel -->
<img src="https://rbsm.rm.gl/badge/region?from=ENA&to=ENA&band=all&theme=dark"
alt="RBN S-Meter: all bands ENA to ENA"
style="display:block;">
<!-- Grid mode, clickable link to the full app -->
<a href="https://rbsm.rm.gl" target="_blank">
<img src="https://rbsm.rm.gl/badge/grid?grid=FN64&to=EU&band=40m&theme=dark&size=small"
alt="RBN S-Meter live">
</a>
For a club website or forum that allows HTML, that is all you need. The image updates itself automatically.
Slack Chatbot
If your club or net has a Slack workspace, you can add a slash command that lets anyone check current band conditions without leaving Slack. Here is a minimal implementation using a Node.js serverless function or any small web server you already run.
What It Does
A user types /rbsm ENA EU 40m in any Slack channel and gets back the current S-meter reading for that path, along with the badge image inline.
Setting Up the Slack App
- Go to api.slack.com/apps and create a new app.
- Under Slash Commands, add a new command:
/rbsm - Set the Request URL to wherever you will host the handler (see below).
- Under OAuth and Permissions, install the app to your workspace.
The Handler Code
Save this as rbsm-slack.js and run it with Node.js on any server or hosting service that can receive HTTP POST requests:
'use strict';
const http = require('http');
const https = require('https');
const qs = require('querystring');
const PORT = process.env.PORT || 3100;
// Slack sends the slash command payload as URL-encoded form data
const server = http.createServer((req, res) => {
if (req.method !== 'POST') { res.writeHead(405); res.end(); return; }
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
const params = qs.parse(body);
const text = (params.text || '').trim().toUpperCase();
const parts = text.split(/\s+/);
// Defaults: ENA EU 20m dark small
const from = parts[0] || 'ENA';
const to = parts[1] || 'EU';
const band = (parts[2] || '20m').toLowerCase();
const theme = (parts[3] || 'dark').toLowerCase();
const size = (parts[4] || 'small').toLowerCase();
const badgeUrl =
`https://rbsm.rm.gl/badge/region` +
`?from=${from}&to=${to}&band=${band}&theme=${theme}&size=${size}`;
const healthUrl = 'https://rbsm.rm.gl/badge/health';
// Fetch health to get data age, then respond
fetchJson(healthUrl)
.then(health => {
const age = health.rbnAge < 120
? `${health.rbnAge}s ago`
: `${Math.floor(health.rbnAge / 60)}m ago`;
const response = {
response_type: 'in_channel',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*RBN S-Meter* | ${from} to ${to} on ${band.toUpperCase()} | data ${age}`
}
},
{
type: 'image',
image_url: badgeUrl,
alt_text: `SNR ${from} to ${to} on ${band}`
},
{
type: 'context',
elements: [{
type: 'mrkdwn',
text: `<https://rbsm.rm.gl|Open RBN S-Meter> | Usage: \`/rbsm [from] [to] [band] [theme] [size]\``
}]
}
]
};
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
})
.catch(() => {
// Respond even if health check fails
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
response_type: 'in_channel',
text: `RBN S-Meter: ${from} to ${to} on ${band.toUpperCase()}\n${badgeUrl}`
}));
});
});
});
function fetchJson(url) {
return new Promise((resolve, reject) => {
https.get(url, { timeout: 5000 }, res => {
let d = '';
res.on('data', c => { d += c; });
res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { reject(e); } });
}).on('error', reject).on('timeout', reject);
});
}
server.listen(PORT, () => console.log(`RBN Slack bot listening on port ${PORT}`));
Running It
node rbsm-slack.js
Or with PM2 to keep it running:
pm2 start rbsm-slack.js --name rbsm-slack
pm2 save
Usage in Slack
/rbsm # ENA to EU on 20m (defaults)
/rbsm ENA EU 40m # 40m path
/rbsm ENA ENA all # All bands, ENA to ENA
/rbsm EU ENA 20m light full # Light theme, full size
/rbsm WNA AS 15m # Western NA to Asia on 15m
Slack will render the badge image inline in the channel. Everyone in the channel sees it, making it handy for a net preamble or contest check-in.
The app is at rbsm.rm.gl. Source code, bug reports, and comments always welcome.
73 de W1VE