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:

SNR ENA to ENA on 40m

ENA to ENA, 20m, light theme, small:

SNR ENA to ENA on 20m

ENA to ENA, 80m, dark theme, full size:

SNR ENA to ENA on 80m

ENA to ENA, all bands, dark theme:

All bands ENA to ENA

Parameters

/badge/region endpoint

ParameterValuesDefaultNotes
fromENA, CNA, WNA, SA, EU, AF, AS, OCENATransmitting region (your skimmer vantage)
toENA, CNA, WNA, SA, EU, AF, AS, OCEUReceiving region (target DX area)
band160m, 80m, 60m, 40m, 30m, 20m, 17m, 15m, 12m, 10m, 6m, all20mall returns a single image with all bands
themedark, lightdarkColor scheme
sizesmall, fullsmallsmall = 220x56px, full = 380x80px

/badge/grid endpoint

Same as above, but replaces from with:

ParameterValuesDefaultNotes
gridAny valid Maidenhead grid (4 or 6 chars)requirede.g. FN64
radius125, 250, 500, 1000, 1500, 2000, 5000500Miles radius for skimmer selection

Region key reference:

KeyRegion
ENAEastern North America
CNACentral North America
WNAWestern North America
SASouth America
EUEurope
AFAfrica
ASAsia
OCOceania

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

  1. Go to api.slack.com/apps and create a new app.
  2. Under Slash Commands, add a new command: /rbsm
  3. Set the Request URL to wherever you will host the handler (see below).
  4. 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

Leave a comment

Your email address will not be published. Required fields are marked *