From 26bfd7fa56c4a6821f803c65b39b22fc21f32981 Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Thu, 13 Aug 2026 22:22:18 -0400 Subject: [PATCH 1/8] Initial live rds display --- .gitignore | 4 + Dynamic_RDS.php | 212 ++++++++++++++++++++++++++++++++++++++++++ Dynamic_RDS_Engine.py | 61 ++++++++++-- api.php | 35 +++++++ 4 files changed, 305 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 524ce9d..44aa170 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,7 @@ dmypy.json .pyre/ *.py.swp + +# Dynamic_RDS runtime status +Dynamic_RDS_Status.json +Dynamic_RDS_Status.json.tmp diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index b725961..b455404 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -226,6 +226,7 @@ public function createAndDownload(): void { $this->addFileToZip($zip, $rotatedLog, basename($rotatedLog)); } $this->addFileToZip($zip, $this->configDirectory . "/plugin.Dynamic_RDS", "plugin.Dynamic_RDS"); + $this->addFileToZip($zip, $this->dynRDSDir . "/Dynamic_RDS_Status.json", "Dynamic_RDS_Status.json"); $this->addFileToZip($zip, "/boot/firmware/config.txt", "config.txt"); $this->addFileToZip($zip, "/boot/uEnv.txt", "uEnv.txt"); @@ -349,6 +350,9 @@ function renderDynamicRDSStatus( // Display all status messages $status->displayMessages(); + // Display live RDS output + displayLiveRDSSection($engineRunning); + // Output JavaScript outputJavaScript($transmitterType); @@ -616,6 +620,213 @@ function displayMQTTSection(array $settings): void { } } +/** + * Display live RDS output section + */ +function displayLiveRDSSection(bool $engineRunning): void { + //if (!$engineRunning) { + // return; + //} + ?> +
+ +
+
+ Now Broadcasting + ON AIR +
+
+
+ PS + + +
+
+
+
+
+ RT + + +
+
+
+
+
Full PS
+
Full RT
+
+
Loading...
+
+
+
+ +
  • Log - plugin-Dynamic_RDS.log (plus rotated copies, if present)
  • +
  • Last RDS output - Dynamic_RDS_Status.json
  • Config - plugin.Dynamic_RDS
  • Version from git rev-parse --short HEAD
  • Pi/BBB boot config - /boot/firmware/config.txt or /boot/uEnv.txt
  • diff --git a/Dynamic_RDS_Engine.py b/Dynamic_RDS_Engine.py index b510685..5a5d717 100755 --- a/Dynamic_RDS_Engine.py +++ b/Dynamic_RDS_Engine.py @@ -31,6 +31,11 @@ def cleanup(): os.unlink(fifo_path) except: pass + try: + logging.debug('Cleaning up status file') + os.unlink(status_path) + except: + pass try: transmitter.basicPWM.shutdown() if mqtt.connected: @@ -77,13 +82,47 @@ def updateRDSData(): transmitter.updateRDSData(rdsStyleToString(config['DynRDSPSStyle'], 8), rdsStyleToString(config['DynRDSRTStyle'], int(config['DynRDSRTSize']))) if config['DynRDSmqttEnable'] == '1': - mqttStatus = {} - mqttStatus['PStext'] = transmitter.PStext - mqttStatus['RTtext'] = transmitter.RTtext - mqttStatus['PSfragments'] = transmitter.PS.fragments - mqttStatus['RTfragments'] = transmitter.RT.fragments - mqttStatus['RDSValues'] = rdsValues - mqtt.publish('status', json.dumps(mqttStatus, indent=8)) + mqtt.publish('status', json.dumps(buildStatus(), indent=8)) + + writeStatus() + +def buildStatus(): + # Single source of truth for what is being broadcast - used by MQTT and the web UI + # Si4713 writes PS/RT directly to the chip and has no buffer objects + PSbuffer = getattr(transmitter, 'PS', None) + RTbuffer = getattr(transmitter, 'RT', None) + return { + 'PStext': transmitter.PStext, + 'RTtext': transmitter.RTtext, + 'PSfragments': PSbuffer.fragments if PSbuffer is not None else [], + 'RTfragments': RTbuffer.fragments if RTbuffer is not None else [], + 'PSdelay': int(config.get('DynRDSPSUpdateRate', 4)), + 'RTdelay': int(config.get('DynRDSRTUpdateRate', 7)), + 'RDSValues': rdsValues, + 'RDSEnabled': config['DynRDSEnableRDS'] == '1', + 'transmitterActive': transmitter.active, + 'transmitterType': config['DynRDSTransmitter'] + } + +def writeStatus(): + # Status file for Dynamic_RDS.php - written on RDS data changes and + # transmitter state transitions, so mtime reflects the last real change + global lastStatus # pylint: disable=global-statement + if transmitter is None: + return + try: + payload = json.dumps(buildStatus(), indent=2) + if payload == lastStatus: + logging.excessive('Status unchanged, skipping write') + return + # Write to a temp file and replace so the web UI never reads a partial file + with open(status_path + '.tmp', 'w', encoding='UTF-8') as f: + f.write(payload) + os.replace(status_path + '.tmp', status_path) + lastStatus = payload + logging.excessive('Status file written') + except Exception: + logging.exception('writeStatus') def rdsStyleToString(rdsStyle, groupSize): outputRDS = [] @@ -163,6 +202,10 @@ def excessive(msg, *args, **kwargs): logging.error('Unable to create lock. Another instance of Dynamic_RDS_Engine.py running?') sys.exit(1) +# Status file for the web UI +status_path = script_dir + '/Dynamic_RDS_Status.json' +lastStatus = None + # Setup fifo fifo_path = script_dir + "/Dynamic_RDS_FIFO" try: @@ -207,6 +250,7 @@ def excessive(msg, *args, **kwargs): if line == 'EXIT': logging.info('Processing exit') transmitter.shutdown() # TODO: Can fail if transmitter wasn't set - Can fix with an if statement or look into using Transmitter base class initially + writeStatus() mqtt.disconnect() sys.exit() @@ -217,6 +261,7 @@ def excessive(msg, *args, **kwargs): transmitter.reset() if config['DynRDSStart'] == "FPPDStart": transmitter.startup() + writeStatus() elif line == 'INIT': # From --list with callback.py logging.info('Processing init') @@ -263,6 +308,7 @@ def excessive(msg, *args, **kwargs): if config['DynRDSStart'] == "PlaylistStart" or not transmitter.active: transmitter.startup() activePlaylist = True + writeStatus() elif line == 'STOP': logging.info('Processing stop') @@ -274,6 +320,7 @@ def excessive(msg, *args, **kwargs): if config['DynRDSStop'] == "PlaylistStop": transmitter.shutdown() logging.info('Transmitter stopped') + writeStatus() elif line.startswith('MAINLIST'): logging.info('Processing MainPlaylist') diff --git a/api.php b/api.php index bf4cf75..917131a 100644 --- a/api.php +++ b/api.php @@ -3,6 +3,7 @@ function getEndpointsDynamic_RDS() { $endpoints = array( array('method' => 'GET', 'endpoint' => 'FastUpdate', 'callback' => 'DynRDSFastUpdate'), + array('method' => 'GET', 'endpoint' => 'Status', 'callback' => 'DynRDSStatus'), array('method' => 'POST', 'endpoint' => 'PiBootChange/:SettingName', 'callback' => 'DynRDSPiBootChange'), array('method' => 'POST', 'endpoint' => 'ScriptStream', 'callback' => 'DynRDSScriptStream') ); @@ -13,6 +14,40 @@ function DynRDSFastUpdate() { shell_exec("sudo /home/fpp/media/plugins/Dynamic_RDS/callbacks.py --update"); } +function DynRDSStatus() { + $statusFile = __DIR__ . '/Dynamic_RDS_Status.json'; + + if (!is_file($statusFile)) { + return json_encode(['error' => 'No status file - Dynamic RDS Engine may not have started']); + } + + $status = json_decode(file_get_contents($statusFile), true); + if (!is_array($status)) { + return json_encode(['error' => 'Unable to parse status file']); + } + + // Writes only happen when the broadcast content changes, so file age is not + // a heartbeat - minutes of silence during a long track is normal. Liveness + // comes from checking the recorded pid is still a running Engine. + $status['age'] = time() - filemtime($statusFile); + $status['running'] = DynRDSEngineAlive($status['pid'] ?? 0); + + return json_encode($status); +} + +function DynRDSEngineAlive($pid) { + $pid = (int)$pid; + if ($pid <= 0) { + return false; + } + $cmdline = @file_get_contents("/proc/{$pid}/cmdline"); + if ($cmdline === false) { + return false; + } + // Guard against a recycled pid now belonging to an unrelated process + return str_contains($cmdline, 'Dynamic_RDS_Engine.py'); +} + function DynRDSPiBootChange() { $settingName = params('SettingName'); $myPluginSettings = json_decode(file_get_contents('php://input'), true); From b78eaa50b0d803a8860fb68632e3d2eeca10b415 Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Fri, 14 Aug 2026 22:15:10 -0400 Subject: [PATCH 2/8] Next round of work for the live rds display --- Dynamic_RDS.php | 76 +++++++++++++++++++++++-------------------- Dynamic_RDS_Engine.py | 4 ++- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index b455404..2c2ad1e 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -56,10 +56,6 @@ public function addWarning(string $message): void { $this->warnings[] = $message; } - public function addSuccess(string $message): void { - $this->successes[] = $message; - } - public function hasErrors(): bool { return !empty($this->errors); } @@ -334,24 +330,18 @@ function renderDynamicRDSStatus( checkRaspberryPiConfiguration($status, $pluginSettings); } - // Add success messages - if ($engineRunning) { - $status->addSuccess('Dynamic RDS Engine is running'); - } - + $transmitterInfo = 'No transmitter detected'; if ($transmitterType !== TransmitterType::NONE) { $i2cType = determineI2CType($platform, $pluginSettings); - $status->addSuccess( - 'Detected ' . $transmitterType->value . ' on I2C ' . - $i2cType . ' bus ' . $i2cBus . ' at address ' . $transmitterType->getAddressHex() - ); + $transmitterInfo = $transmitterType->value . ' · I2C ' . + $i2cType . ' bus ' . $i2cBus . ' · ' . $transmitterType->getAddressHex(); } // Display all status messages $status->displayMessages(); // Display live RDS output - displayLiveRDSSection($engineRunning); + displayLiveRDSSection($transmitterInfo); // Output JavaScript outputJavaScript($transmitterType); @@ -623,24 +613,20 @@ function displayMQTTSection(array $settings): void { /** * Display live RDS output section */ -function displayLiveRDSSection(bool $engineRunning): void { - //if (!$engineRunning) { - // return; - //} - ?> +function displayLiveRDSSection(string $transmitterInfo): void { +?>
    - Now Broadcasting +
    +
    Now Broadcasting ·
    +
    ON AIR
    @@ -694,8 +692,11 @@ function displayLiveRDSSection(bool $engineRunning): void {
    Full PS
    Full RT
    -
    Loading...
    -
    +
    + + Engine Active + Loading... +

    Date: Wed, 19 Aug 2026 22:25:27 -0400 Subject: [PATCH 8/8] Added light mode support. Minor code changes. Default and doc updates and making consistent --- Dynamic_RDS.php | 79 ++++++++++++++++++++----------- Dynamic_RDS_Engine.py | 6 ++- README.md | 8 ++-- Transmitter.py | 4 +- config.py | 4 +- scripts/src_Dynamic_RDS_config.sh | 2 +- settings.json | 4 +- 7 files changed, 66 insertions(+), 41 deletions(-) diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index 8677e28..2b4ee62 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -608,52 +608,74 @@ function displayLiveRDSSection(string $transmitterInfo, bool $transmitterFound): ?>
    @@ -859,7 +881,7 @@ function poll() { // Only poll while the tab is visible document.addEventListener('visibilitychange', function () { - if (document.hidden) { clearInterval(pollTimer); pollTimer = null; } + if (document.hidden) { stopCycling(); clearInterval(pollTimer); pollTimer = null; } else if (!pollTimer) { poll(); pollTimer = setInterval(poll, POLL_MS); } }); @@ -870,6 +892,7 @@ function poll() { }); })(); +
    [!NOTE] > Dynamic_RDS supports the **QN8066** and **Si4713** FM transmitter chips -Originally created for Falcon Player 6.0 (FPP) and updated to support FPP 10.0+, the Dynamic_RDS plugin can generate RDS (radio data system) messages similar to what is seen from typical FM stations. The RDS messages are fully customizable with static text, breaks, and grouping along with the supported file tag data fields of title, artist, album, genre, track number, and track length, as well as main playlist position and item count. Currently, the plugin run on Raspberry Pi or BBB and supports the QN8066 chip and the Si4713 chip. The chips are controlled via the I2C bus. +Originally created for Falcon Player 6.0 (FPP) and updated to support FPP 10.0+, the Dynamic_RDS plugin can generate RDS (radio data system) messages similar to what is seen from typical FM stations. The RDS messages are fully customizable with static text, breaks, and grouping along with the supported file tag data fields of title, artist, album, genre, track number, and track length, as well as main playlist position and item count. Currently, the plugin runs on Raspberry Pi or BBB and supports the QN8066 chip and the Si4713 chip. The chips are controlled via the I2C bus. ## Si4713 transmitter board Originally, the Si4713 breakout board was available from [AdaFruit](https://www.adafruit.com/product/1958) but it now out of stock. There are many clones of this board that can be found on [AliExpress](https://www.aliexpress.us/w/wholesale-Si4713-transmitter.html) or by a [Google Search](https://www.google.com/search?q=Si4713+transmitter) @@ -145,9 +145,9 @@ All settings are on the plugin's config page, reachable from **Status/Control -> | Program Type | 2 - Information / Current Affairs | Standard PTY list; assignments differ between North America and Europe | | PS Style Text | `{T}\|{A}[\|{P} of {C}]\|Merry\|Christ-\| -mas!` | Program Service, sent 8 characters at a time. This is what most radios display | | PS Update Rate | 4 sec | Interval between 8-character updates (3-60). It takes ~1 second to send 8 characters, and some radios only display text after receiving a group twice | -| RT Style Text | `{T}[ by {A}][\|Track {P} of {C} ]Merry Christmas!` | Radio Text — longer messages, slower update rate | +| RT Style Text | `{T}[ by {A}][\|Track {P} of {C}] Merry Christmas!` | Radio Text — longer messages, slower update rate | | RT Update Size | 32 chars | RT supports up to 64, but not all radios display that much at once. 32 is recommended | -| RT Update Rate | 8 sec | Interval between RT updates (3-60). Sending a full 64 characters takes ~4 seconds | +| RT Update Rate | 7 sec | Interval between RT updates (3-60). Sending a full 64 characters takes ~4 seconds | ### Style Text Substitutions The PS and RT style text fields accept substitutions that are filled in from the currently playing media: @@ -211,7 +211,7 @@ Publishes plugin status to MQTT. Requires MQTT to be configured first under **FP Callback and Engine logging levels are set separately (Errors Only / Warn / Info / Debug, plus Excessive for the Engine). Both write to `plugin-Dynamic_RDS.log`, viewable from the config page. ### Report an Issue -Set the log levels to Debug, reproduce the problem, then use **Download log and config zip** and attach the file to a [new issue](https://github.com/ShadowLight8/Dynamic_RDS/issues). The zip contains the log and rotated copies, the `plugin.Dynamic_RDS` config, the plugin version, and your Pi/BBB boot config. +Set the log levels to Debug, reproduce the problem, then use **Download log and config zip** and attach the file to a [new issue](https://github.com/ShadowLight8/Dynamic_RDS/issues). The zip contains the log and rotated copies, the `plugin.Dynamic_RDS` config, last RDS output, the plugin version, and your Pi/BBB boot config. ### Advanced Options Software I2C mode for the Raspberry Pi, and PWM pin selection for both the Pi and BeagleBone Black. Most setups won't need to touch these. diff --git a/Transmitter.py b/Transmitter.py index ddc9783..1d7a924 100644 --- a/Transmitter.py +++ b/Transmitter.py @@ -78,11 +78,11 @@ def __init__(self, data='', frag_size=0, group_size=0, delay=4): self.pi_byte1 = int('0x' + config['DynRDSPICode'][0:2], 16) self.pi_byte2 = int('0x' + config['DynRDSPICode'][2:4], 16) self.pty = int(config['DynRDSPty']) - self.updateData(data) self.fragments = [] self.currentFragment = 0 - self.lastFragmentTime = 0 + self.lastFragmentTime = datetime.now() self.currentGroup = 0 + self.updateData(data) def updateData(self, data): logging.debug('RDSBuffer updateData') diff --git a/config.py b/config.py index 68fb7ae..033a540 100644 --- a/config.py +++ b/config.py @@ -5,9 +5,9 @@ 'DynRDSEnableRDS': '1', 'DynRDSPSUpdateRate': '4', 'DynRDSPSStyle': '{T}|{A}[|{P} of {C}]|Merry|Christ-| -mas!', -'DynRDSRTUpdateRate': '8', +'DynRDSRTUpdateRate': '7', 'DynRDSRTSize': '32', -'DynRDSRTStyle': '{T}[ by {A}][|Track {P} of {C} ]Merry Christmas!', +'DynRDSRTStyle': '{T}[ by {A}][|Track {P} of {C}] Merry Christmas!', 'DynRDSPty': '2', 'DynRDSPICode': '819b', 'DynRDSTransmitter': 'None', diff --git a/scripts/src_Dynamic_RDS_config.sh b/scripts/src_Dynamic_RDS_config.sh index 35e992e..02c7865 100755 --- a/scripts/src_Dynamic_RDS_config.sh +++ b/scripts/src_Dynamic_RDS_config.sh @@ -7,7 +7,7 @@ PS='{T}|{A}[|{P} of {C}]|Merry|Christ-| -mas!' # Set the RT text (set to '' or comment out to leave unchanged) -RT='{T}[ by {A} ][|Track {P} of {C} ]Merry Christmas!' +RT='{T}[ by {A}][|Track {P} of {C}] Merry Christmas!' if [ "$PS" != "" ]; then echo 'Setting PS Style Text to: '$PS diff --git a/settings.json b/settings.json index e1604ba..1feb232 100644 --- a/settings.json +++ b/settings.json @@ -356,7 +356,7 @@ "type": "text", "size": 64, "maxlength": 256, - "default": "{T}[ by {A}][|Track {P} of {C} ]Merry Christmas!" + "default": "{T}[ by {A}][|Track {P} of {C}] Merry Christmas!" }, "DynRDSRTUpdateRate": { "name": "DynRDSRTUpdateRate", @@ -369,7 +369,7 @@ "min": 3, "max": 60, "step": 1, - "default": 8 + "default": 7 }, "DynRDSRTSize": { "name": "DynRDSRTSize",