Skip to content

RemiKG/skywave

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Skywave

your radio signal, seen.

Type your callsign and watch a live map of everywhere on Earth your radio signal is reaching right now — then ask when you'll reach Japan, answered from what actually happened on the air.

Skywave pours the world's public reception-report firehose (WSPR, PSKReporter, DX-cluster) into Elasticsearch and draws it: a dark Earth with the computed day/night line, every fresh reception blooming as an arc of light between transmitter and listener, decaying like phosphor. Type a callsign and the planet redraws to that one signal's actual reach. Ask the operator a plain-words question and it answers from the index — painted on the map first, two cited sentences second.

How it works

 WSPR (wspr.live poll)      ┐
 PSKReporter (MQTT)         ├─► ingest workers ─► Elasticsearch Serverless
 DX-cluster (telnet)        ┘        │               ├─ skywave-spots     (data stream, 7-day retention)
                                     │               ├─ skywave-chatter   (semantic_text → hybrid search)
                                     ▼               └─ skywave-insights  (the agent's growing memory)
                              SSE to every                   ▲    ▲
                              open browser                   │    │ Elastic Agent Builder
                                                             │    │ custom ES|QL + search tools,
 browser (canvas planet) ─► /api/ask ─► Gemini (Vertex AI) ──┴────┘ exposed over Elastic's MCP server
                                          the operator's ONLY senses are those MCP tools

One Node process (server/) does everything: serves the static web app, ingests the three public feeds, streams fresh reports to every open page over SSE, runs the 10-minute openings sweep, and hosts the /api/ask endpoint where a Gemini agent (Vertex AI) answers questions exclusively through Elastic MCP tools.

The Elastic side (the agent's senses and memory)

  • skywave-spots — append-only time-series data stream of every reception report (who heard whom, band, SNR, grids → lat/lon, regions, distance). The map is a rolling-window query against it — never a canned animation.
  • skywave-chatter — DX-cluster free-text comments with a semantic_text field: hybrid semantic + keyword search over messy radio slang ("loud into EU, big QSB").
  • skywave-insights — every band opening the agent detects, written back in plain words. The ticker and future answers read it: the planet gets smarter in prose.
  • Agent Builder tools (created idempotently at boot by server/agent/estools.js, callable over Elastic's built-in MCP server):
    • skywave_paths_by_hour — ES|QL hour-of-day path counts between two sides of the world
    • skywave_callsign_reach — ES|QL per-band reach of one callsign
    • skywave_band_activity — ES|QL band × region-pair activity
    • skywave_chatter_search — hybrid search over cluster chatter
    • skywave_insights_recall — search over the agent's own past findings

The Google side (the operator)

server/agent/operator.js runs Gemini on Vertex AI (auth via Application Default Credentials) in a manual function-calling loop. Its only tools are the five skywave_* tools fetched live from {KIBANA_URL}/api/agent_builder/mcp (streamable HTTP MCP). It cannot see the world except through the index. Answers come back as map paint (a 24-cell hour axis along the great-circle path, sized by measured counts) plus two sentences in operator voice and a citation line listing exactly what was queried — always ending observation, not prophecy.

server/agent/sweep.js is the agent acting on its own: every 10 minutes it compares each band × region-pair's last hour against its trailing-7-day same-hour baseline, phrases detected openings in plain words (Gemini), and writes them to skywave-insights — every visitor's ticker updates at the same moment over SSE.

Repo layout

server/
  index.js        HTTP + SSE + static hosting + REST API
  config.js       all configuration (env vars)
  es.js           minimal Elasticsearch/Kibana REST client
  setup.js        idempotent index + Agent Builder tool setup (runs at boot)
  hub.js          SSE fan-out to every open page
  lib/geo.js      maidenhead grids, bands, regions, great circles
  ingest/         the three feed workers + normalization
  agent/          MCP client, the operator, ES|QL tool defs, openings sweep
web/
  index.html      the planet + the bench (one page, no framework, no build)
  app.js          canvas renderer, SSE client, focus/scrub/ask/replay
  land.js         Natural Earth coastlines (vendored)
  manifest.webmanifest, sw.js, icon.svg   (PWA shell)
Dockerfile        single container for Cloud Run

Run it

Prereqs: Node 20+, an Elastic Cloud Serverless project (Elasticsearch type, Agent Builder available on trial), and a Google Cloud project with Vertex AI enabled plus ADC (gcloud auth application-default login).

npm install
cp .env.example .env     # fill in your values
npm start                # http://localhost:4200

First boot creates the indices and the Agent Builder tools automatically. Within a minute the planet starts breathing with real reports.

Environment variables (all of them)

var default meaning
ES_URL Elasticsearch Serverless endpoint (required)
KIBANA_URL Kibana endpoint of the same project (required — hosts the MCP server)
ES_API_KEY encoded Elasticsearch API key (required)
GOOGLE_CLOUD_PROJECT GCP project for Vertex AI (required for the operator)
GOOGLE_CLOUD_LOCATION global Vertex location (gemini-3-flash-preview lives on global)
GEMINI_MODEL gemini-3-flash-preview the operator's model (fallback: gemini-2.5-flash on us-central1)
GOOGLE_GENAI_USE_VERTEXAI true keep true: Gemini via Vertex AI with ADC
PORT 4200 HTTP port (Cloud Run injects its own)
WSPR_ENABLED / WSPR_POLL_SECONDS true / 60 wspr.live poller
PSK_ENABLED / PSK_TOPICS true / pskr/filter/v2/+/FT8/# PSKReporter MQTT (one connection, filtered per their considerate-use guidance)
PSK_MAX_PER_SEC 25 politeness/volume cap; focused callsigns are exempt and never dropped
CLUSTER_ENABLED / CLUSTER_HOST / CLUSTER_PORT true / dxc.ve7cc.net / 23 DX-cluster telnet node (auto-rotates to alternates)
CLUSTER_LOGIN SKYWAVE set a real licensed callsign — public nodes reject anything else; until then the feed shows honestly cold
SWEEP_MINUTES / SWEEP_FACTOR / SWEEP_MIN_PATHS 10 / 2.0 / 8 openings sweep cadence and thresholds

Secrets live only in .env (gitignored) or your deploy platform's env config. Nothing is hardcoded.

Deploy (Cloud Run)

gcloud run deploy skywave --source . \
  --region us-central1 --allow-unauthenticated \
  --min-instances 1 --max-instances 1 \
  --set-env-vars ES_URL=...,KIBANA_URL=...,GOOGLE_CLOUD_PROJECT=...,GOOGLE_CLOUD_LOCATION=global \
  --set-secrets ES_API_KEY=skywave-es-api-key:latest

Notes:

  • --min-instances 1 --max-instances 1: the ingest workers and the sweep run inside the serving process; one always-on instance keeps the planet breathing and is the polite number of connections to the feeds.
  • Vertex AI auth is automatic on Cloud Run (the service account's ADC); grant it roles/aiplatform.user.
  • The browser app uses only relative URLs, SSE, and localStorage (bench settings) — nothing else to configure.

What is real, and what is not

Everything on screen is measured from the index — report rates, feed ages, receiver counts, dxcc counts, best-dx distances — and labeled — measured where it stands alone. The feeds are the live public WSPR / PSKReporter / DX-cluster firehoses. An inactive callsign gets a dark planet, because that is the truth.

Explicitly not real or limited:

  • REPLAY mode (the bench) replays the last full UTC day from the index through the same pipeline, with the word REPLAY printed on the planet the entire time. Never silently substituted.
  • The planet shows what the monitoring networks hear (digital weak-signal modes), not all radio activity.
  • DX-cluster spots carry no location grids, so they feed the chatter index (and the agent's reading of it), not arcs on the map.
  • "dxcc" counts are distinct coarse callsign-prefix entities, not the official DXCC entity list.
  • Answers are observations over accumulated reports, never ionosphere prediction — the citation line says so every time.

License

MIT — see LICENSE.

About

Type your callsign and watch a live map of everywhere on Earth your radio signal is reaching - powered by Elasticsearch + Gemini

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors