RDS (Radio Data System) is a digital communication standard that transmits data as an inaudible signal modulated onto FM broadcasts (87.5–108 MHz). It was standardised under ETSI EN 50067 and has been in use across Europe since the 1980s. In North America, a nearly identical standard exists called RBDS.
RDS carries a variety of continuous digital metadata alongside the analog audio signal. The most commonly used data fields are:
| Code | Name | Meaning | Size |
|---|---|---|---|
| PI | Programme Identification | Unique 16-bit station identifier (hex, e.g. D3C3). Essential for auto-tuning. | 16 bit |
| PS | Programme Service Name | Station name, max. 8 characters (e.g. "NRJ ") | 8 × 8 bit |
| RT | RadioText | Scrolling text, max. 64 characters (title, artist, show name) | 64 × 8 bit |
| PTY | Programme Type | Programme category (0–31, e.g. 10 = Pop Music) | 5 bit |
| TP | Traffic Programme | Flag indicating if the station broadcasts traffic announcements | 1 bit |
| TA | Traffic Announcement | Flag indicating if a traffic announcement is currently active | 1 bit |
| AF | Alternate Frequencies | List of alternative frequencies carrying the same programme | 8 bit each |
| ECC | Extended Country Code | Extended country identifier (combined with PI nibble to determine country) | 8 bit |
RDS transmits at 1,187.5 bit/s as a BPSK signal on a 57 kHz subcarrier. A complete basic data block containing PI, PTY, TP, and two characters of the PS name requires 104 bits (26 bits per block × 4 blocks). A complete 8-character PS transmission takes approximately 0.35 seconds under perfect, error-free conditions.
Under local reception conditions with a strong antenna, RDS decoding is instantaneous and error-free. However, during long-distance FM reception (DX) – especially via tropospheric ducting, meteor scatter, or Sporadic-E propagation – signals are often extremely weak, rapidly fading, and severely affected by multipath interference or co-channel interference.
When the signal-to-noise ratio drops, the native RDS decoder built into the receiver chips (like TEF6686 / NXP) struggles:
Version 2.7 introduces powerful raw data recording and enhanced visual identification, building upon the spatial awareness and UI features of the 2.6 series.
A new Record button (⏺) has been added to the panel header. To ensure stability and zero memory leaks during long recording sessions, recording is now handled entirely on the server side. Only logged-in Administrators can start or stop the recording. When activated, the server streams all incoming raw RDS groups, block hex codes, error levels, and the AI's real-time prediction data directly into a CSV file in the ../../web/logs directory. Stopping the recording automatically provides a download link to the RDS_RAW_YYYYMMDD_HHMM.csv file, perfectly formatted for later analysis in the standalone RDS RAW Decoder web tool.
The server plugin now extracts the precise ITU (International Telecommunication Union) country code from the FMDX reference database and transmits it over the WebSocket. The client UI uses this ITU code to seamlessly display the corresponding national flag next to the predicted station name, using a robust external countryList and a fallback flag server with a 404-loop prevention mechanism.
The AI now calculates the geometric center of the currently active Sporadic-E cloud. It tracks countries (ITU) and azimuth angles of all DX stations received in the last 5 minutes. During PI collisions, candidates aligning with the active cloud receive massive bonus points, effectively eliminating false logs from other directions.
Stations received via Sporadic-E (>800km) are now aggressively purged from the local memory exactly 5 minutes after signal loss, preventing long-term PI blocking. Furthermore, "Dummy PIs" (like 1060 in Spain/Italy) are strictly validated: single candidates are now rejected if live PS characters conflict with the FMDX database.
The RDS Follow toggle has been moved from the AI panel to the main Webserver navigation bar via a Long-Press action (600ms). Additionally, a new Padlock (🔒/🔓) allows Admins to unlock the RDS Follow feature, granting public guest users the ability to toggle the AI decoder.
To safely handle database schema updates without user intervention, a seamless One-Time-Wipe routine triggers on `DB_VERSION` bumps. It automatically resets cached memory while perfectly preserving your administrative lock and follow settings.
Dynamic mode is now 100% driven by live signal analysis. If scrolling text stops and reverts to a static station name for 50 consecutive RDS frames (~20–30 seconds), the plugin will automatically drop out of dynamic mode and lock onto the static name.
To further suppress "Ghost PIs", v2.5a introduced a dynamic Distance-Based scale: <300km requires 2 hits, 300-800km requires 4 hits, and >800km requires 6 hits for PI confirmation.
Anti-Hallucination (Scatter Mode): If a PI code is received via a brief scatter ping, but zero PS characters are decoded, the AI will not hallucinate the station name from the FMDX database. The PS field remains accurately blank.
If the receiver decodes an 8-character PS string with 100% mathematical perfection, the AI trusts the live signal over the global database to support local test transmitters and rebrands.
When two stations share the exact same PI code on the same frequency, the AI requires hard physical evidence to break the tie: either matching Alternate Frequencies (AF) or a strict 100% match of at least 3 cleanly received PS characters.
The calculatePropagationScore() algorithm uses real-world RF propagation physics to pick the most logical transmitter, evaluating ERP, Site Verification (AF network), and historical records.
The plugin is strictly split into two layers:
datahandler.js when RDS Follow Mode is enabled.The station name (PS) consists of 8 character positions. Each position is voted on separately. The receiver hardware flags every 16-bit block with an "Error Level" from 0 (perfect) to 3 (unrecoverable).
When the AI Decoder receives a raw PS character (e.g. at position 0), it looks at the hardware error level:
These points are accumulated in the server's memory. As time passes, the points decay (using a 7-day half-life formula). This allows the plugin to "forget" old corrupted data while strongly favoring a consistent signal. After enough votes, a reliable station name emerges.
On startup (and automatically every 6 hours) the plugin downloads the full FM transmitter database from maps.fmdx.org/api/?qth={lat},{lon} centred on the receiver's own location. All transmitters within a radius of 3000 km are indexed into two lightning-fast in-memory lookup tables.
Each fmdx.org entry provides:
Once a PS name has been determined with high confidence, it is locked – the displayed name stops changing until the frequency or PI code changes. This totally eliminates UI flickering.
The lock is achieved through three priority levels:
The Hybrid Case System: Many stations transmit their PS names in ALL CAPS (e.g. ANTENNE ) but they look terrible in the UI. FMDX.org contains beautifully formatted mixed-case variants (e.g. Antenne ). The plugin uses a Hybrid Constructor: if the live received uppercase letter mathematically matches the reference mixed-case letter at the exact same position, the plugin swaps in the beautiful mixed-case letter before sending it to the web server.
Before a PS name is fully locked, the decoder goes through an intermediate provisional stage. This state is communicated to the user via the STATUS row in the panel.
| State | Badge colour | Condition | Additional info shown |
|---|---|---|---|
| WAIT | Dark grey (neutral) | Default on start / after frequency change. Not enough data yet. | "collecting…" |
| PROVISIONAL | Amber (#c8a020) | A candidate PS exists with confidence ≥ 55% but is not yet locked. | Confidence % · stable time in seconds (e.g. "72% · stable 1.5s") |
| LOCKED | Green (#44ff88) | Confidence ≥ 90% and stable, OR strong fmdx.org match, OR raw-verified. | Reason for lock (e.g. "FMDX match 100%" or "DB verified string") |
When you change frequencies, the native decoder usually blanks out completely. The AI plugin behaves differently. Because it stores historical data in rdsm_memory.json, the moment you tune to a new frequency, it looks up known transmitters for that frequency.
If it finds a station in its memory that matches the frequency, it instantly prepares the "Predicted PS" and displays it in the statistics panel, even before a single RDS packet has been received from the antenna. If RDS Follow Mode is enabled, this cached prediction is immediately pushed to the web UI.
Some stations unfortunately use their PS field to transmit scrolling text (song titles, phone numbers). The AI decoder features a Dynamic Jump Engine to detect this.
When a station is flagged as dynamic (⚡), the statistical voting engine is bypassed, because voting on scrolling text produces absolute garbage. Instead, the plugin switches to a raw pass-through mode.
The 50-Frame Timeout: If the text stops scrolling and reverts to a static station name for 50 consecutive RDS frames (~20–30 seconds), the plugin automatically drops out of dynamic mode and firmly locks onto the static name.
The Alternate Frequency list is transmitted as a highly compressed set of 8-bit codes. The plugin decodes these codes according to the standard ETSI table (Method A).
The decoded frequencies are maintained in a Set to guarantee uniqueness. In the UI, the AF badge illuminates and displays the total count. Hovering over the badge reveals the exact list of frequencies.
By default, the AI Decoder operates purely as an invisible observer. It draws its own floating statistics panel but does not alter the main web interface (the large blue PS blocks and RadioText banner at the top of the FM-DX-Webserver page).
Activating RDS Follow Mode forces the AI to intercept the data pipeline and inject its mathematically stabilized, FMDX-verified PS strings and RadioText directly into the main web server UI.
UI Interaction:
Certain PI codes are considered invalid or uninformative by the standard, particularly 0000 and FFFF. Cheap transmitters or pirate stations often use these.
If the plugin detects 0000 or FFFF, it skips database lookup, FMDX verification, and statistical voting. It acts purely as a raw pass-through.
In many countries, national networks broadcast identical programming on dozens of transmitters, but insert local news for 5 minutes an hour, temporarily switching their PI code to a "Regional" PI code.
The FMDX database download brings both the Primary PI and the Regional PI. If the plugin detects the Regional PI, it mathematically treats it exactly the same as the Primary PI, locking instantly without rebuilding its statistical profile.
The floating AI panel is divided into sections:
When the statistics panel is expanded, a golden highlighted section appears displaying:
🌐 fmdx.org [Station Name]match: [XX]%[Tx Site Name] ([ITU Code])[ERP] kW [Polarization] · [Distance] km · [Azimuth]°The LOCAL DB section queries the server's rdsm_memory.json and extracts all stations seen on the currently tuned frequency. It shows the PI code, the locked PS string, dynamic status (⚡), and seen count.
Deletion: Logged-in Administrators will see a red ✕ next to every entry. Clicking this deletes a "Ghost PI" from the server's permanent memory immediately.
An RDS data stream consists of consecutive "Groups", each 104 bits long, split into 4 Blocks (A, B, C, D). Each block contains 16 bits of payload and 10 bits of Checkword.
The native receiver chip assigns an error level from 0 to 3:
The AI decoder strictly ignores any payload data from a block that has an error level of 2 or 3. Only levels 0 and 1 are permitted into the voting algorithms.
Selecting the correct transmitter when multiple sites share a PI code relies on the calculatePropagationScore() algorithm combined with a new Spatial Awareness Engine.
Base Scoring factors:
Spatial Awareness (SpE Cloud Tracking) v2.6:
When a PI collision occurs for candidates >800km, the AI scans all active DX stations logged in the last 5 minutes. It extracts their ITU country codes and Azimuths.
This completely overrides the base score, ensuring that a Spanish station with PI 1060 wins when the band is open to Spain, rather than a Greek station with PI 1060 that happens to be mathematically closer.
The checkAndLockPS(pi) function runs every time a new RDS packet arrives. Priority ladder:
entry.psVerifiedRaw), use it.psVerifiedRaw.Single-Candidate Strict Validation (Dummy-PI Blocker) v2.6:
Previously, if the database only returned one candidate for a PI/Frequency combo, it was trusted blindly. Now, if the live received characters heavily conflict with that single candidate's FMDX name, the candidate is rejected entirely. This blocks uncoordinated "Dummy PIs" (like 1060 in Spain) from incorrectly logging the only official 1060 station (in Greece).
The client UI calculates the STATUS string based on boolean flags and confidences sent by the server:
psProvisionalConf must exceed 0.55 (55%) to move from WAIT to PROVISIONAL.psStableMs measures how long the provisional string has remained totally unchanged.psLocked is a boolean. When true, the UI turns green and displays the psLockReason.Votes are stored in rdsm_memory.json in a compact format:
"ps": {
"0": {
"N": {
"w": 45.2,
"count": 120,
"firstSeen": 1712849200,
"lastSeen": 1712850000
}
}
}
The weight (w) decays mathematically based on the time elapsed since lastSeen, using the VOTE_HALFLIFE_DAYS constant (7 days).
Confidence is a floating point number between 0.0 and 1.0. It is calculated by blending:
The threshold for verification is dynamic based on calculated FMDX distance:
Hysteresis Bonus Limit v2.6:
Previously, the memory system granted a 2000-point "superglue" bonus to a station if it was successfully locked, preventing jumps during fading. In v2.6, this bonus is strictly disabled for distances >800km because SpE clouds are too volatile and a 2000-point bonus would suppress genuine, fast-changing PI collisions.
The local database rdsm_memory.json is saved automatically every 60 seconds. Memory is capped at MAX_STATIONS = 2000.
5-Minute Aggressive SpE Purge v2.6:
Any station logged with a calculated distance ≥800km is now assigned an aggressive SPE_EXPIRE_MINUTES timer (default 5 minutes). Exactly 5 minutes after the signal fades, the station is completely wiped from memory. This ensures that a SpE spaniel doesn't permanently block a local station that shares the same PI code later in the day.
One-Time Auto-Wipe Mechanism v2.6:
When the server boots, loadDB() checks the `DB_VERSION` variable. If it detects a mismatch (schema update), it safely extracts the Admin's `rdsFollowMode` and `rdsFollowLocked` preferences, dumps all memory caches to prevent corruption, and saves a fresh versioned database instantly.
The client and server communicate via a standard JSON WebSocket protocol. The server broadcasts payloads of type: 'rdsm_ai' continuously.
Client commands:
{
"type": "rdsm_delete_pi",
"pi": "D3C3"
}
And the new lock command v2.6:
{
"type": "rdsm_set_rds_lock",
"locked": false
}
To override the native datahandler.js, the server plugin intercepts the variables via Object.defineProperty getters/setters before they are dispatched via the main Webserver WebSockets. It overwrites dataHandler.dataToSend.ps and dataHandler.dataToSend.pi with the mathematically verified AI strings.
The client UI calculates the "AF Coverage %" by extracting the Alternate Frequencies from the active FMDX.org JSON reference block and comparing it against the live currentState.afSet. It generates a horizontally scrolling UI of Frequency Chips that highlight blue when a match occurs.
The server plugin connects to `ws://127.0.0.1:8080/data_plugins` to listen for GPS payloads. Coordinates are rounded to ~1km (`roundGps`), and a 30MB FMDX index rebuild is only triggered if the distance moved exceeds FMDX_REINDEX_MIN_DIST_KM (100km).
The frontend hooks into the `mouseup` event of the drag handler. It reads `el.style.left` and `el.style.top` and dumps a JSON object into `window.localStorage.setItem('rdsm_panel_pos')`. On page load, this is parsed and injected directly into the DOM CSS properties.
While the AI Decoder communicates primarily over WebSockets for live data, it exposes a silent footprint on the main HTTP API of the FM-DX Webserver.
When polling the /api endpoint of the server, standard RDS fields are replaced by the AI if Follow Mode is active:
{
"freq": "104.600",
"pi": "D3B8",
"ps": "RTL ",
"pty": 10,
"rds": true,
"ta": 0,
"tp": 1,
"ms": 1,
"af": [89000, 104600],
"rt0": "RTL - BERLINS HITRADIO",
"country_name": "Germany",
"country_iso": "DE"
}
If Follow mode is disabled, the /api returns the native hardware decoding results, which may contain spaces or errors like "R L ".
Advanced users can tweak the constants at the top of rds-ai-decoder_server.js to tune the AI behavior.
| Parameter | Default | Description |
|---|---|---|
VOTE_HALFLIFE_DAYS |
7 | Days before a vote loses 50% of its weight. |
VOTE_EXPIRE_DAYS |
30 | Days before an old vote is completely purged from memory. |
STATION_EXPIRE_DAYS |
90 | Days before an inactive local/tropo station is deleted from memory. |
SPE_EXPIRE_MINUTES |
5 | Minutes before an inactive SpE station (>800km) is purged to prevent PI collisions. v2.6 |
FMDX_RADIUS_KM |
3000 | Radius in km for downloading transmitters from maps.fmdx.org. |
PROVISIONAL_MIN_CONF |
0.55 | Confidence threshold (0.0 to 1.0) to enter the Amber PROVISIONAL state. |
LOCK_MIN_CONF |
0.90 | Confidence threshold to turn Green and LOCK the PS name permanently. |
This happens when the FMDX database variant uses special characters, differing casing, or abbreviations that mathematically clash with the raw data. The AI will eventually lock onto the Raw RDS string instead of the FMDX variant.
The signal is too weak, or the TEF chip is dropping every packet with Error Level 2 or 3. If the hardware cannot deliver a single Level 1 packet, the AI has nothing to vote on.
If you are still tuned to the frequency and the hardware is still decoding that exact PI code from the noise floor, the plugin will instantly recreate the memory entry. Tune away to a blank frequency before purging ghost PI codes.
By default, the AI Decoder operates in a secure Admin-Only mode. The red text and padlock (🔒) indicate that only logged-in Administrators can turn RDS Follow on or off (via Long-Pressing the main navigation button). Click the padlock to unlock it (🔓) and allow public users to toggle the AI, which turns the text green.
Raw RDS recording is processed on the server and saved directly to the webserver's logs folder. To prevent abuse and storage issues, only logged-in Administrators can start or stop recordings.
Ensure your GPS hardware has a solid lock. The plugin mitigates GPS jitter by rounding to ~1km and requiring a 100km total movement before triggering the 30MB FMDX index rebuild. If you see constant console logs about "fmdx.org index built", your coordinates in `config.json` might be oscillating wildly due to a bad script pushing fake GPS data to the websocket.