From 976be02d5728b5fa91bb6beaa1aaf69ac2dadb8e Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Wed, 9 Sep 2026 21:10:21 -0400 Subject: [PATCH 1/4] Allow Style Text to be updated without a restart. Cleaned up settings file. --- Dynamic_RDS.php | 15 +++++++++++---- settings.json | 20 +++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index 2b4ee62..a956a20 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -491,6 +491,15 @@ function DynRDSFastUpdate() { $.get('api/plugin/Dynamic_RDS/FastUpdate'); } +// The RDS Settings group shares a single callback, but only the Style Text +// fields are re-read by the Engine on UPDATE. Everything else in the group is +// consumed when the transmitter is set up, so it still needs an FPP restart. +function DynRDSRDSSettingsUpdate(key) { + if (key === 'DynRDSPSStyle' || key === 'DynRDSRTStyle') { + DynRDSFastUpdate(); + } +} + function DynRDSPiBootUpdate(key) { $.post('api/plugin/Dynamic_RDS/PiBootChange/' + encodeURIComponent(key), JSON.stringify(Object.assign({}, pluginSettings))); @@ -519,15 +528,13 @@ function ScriptStreamProgressDialogDone() { * Display all settings groups */ function displaySettingsGroups(array $settings): void { - PrintSettingGroup("DynRDSRDSSettings", getRDSStyleGuideHTML(), "", 1, "Dynamic_RDS", "UpdateDynRDSTransmitterChildren"); + PrintSettingGroup("DynRDSRDSSettings", getRDSStyleGuideHTML(), "", 1, "Dynamic_RDS", "DynRDSRDSSettingsUpdate"); PrintSettingGroup("DynRDSTransmitterSettings", "", "", 1, "Dynamic_RDS", "DynRDSTransmitterFrequencyUpdate"); // Wrap Audio in div id to make it easy to hide/show for Si4713 echo '
'; - PrintSettingGroup("DynRDSAudioSettings", "", - "indicates a live change to transmitter, no FPP restart required", - 1, "Dynamic_RDS", "DynRDSFastUpdate"); + PrintSettingGroup("DynRDSAudioSettings", "", "", 1, "Dynamic_RDS", "DynRDSFastUpdate"); echo '
'; PrintSettingGroup("DynRDSPowerSettings", "", "", 1, "Dynamic_RDS", "DynRDSPiBootUpdate"); diff --git a/settings.json b/settings.json index 1feb232..6767ab9 100644 --- a/settings.json +++ b/settings.json @@ -174,7 +174,7 @@ "name": "DynRDSQN8066AmpPower", "description": "Amp Power (0-100)", "tip": "Adjust the power output for the amplifier after the transmitter chip. This is controlled by PWM output.", - "suffix": " - Controlled by PWM", + "suffix": " - Controlled by PWM", "restart": 0, "reboot": 0, "type": "number", @@ -273,7 +273,7 @@ "max": 20, "step": 1, "default": 0, - "suffix": "" + "suffix": "" }, "DynRDSQN8066SoftClipping": { "name": "DynRDSQN8066SoftClipping", @@ -284,7 +284,7 @@ "checkedValue": "1", "uncheckedValue": "0", "default": 1, - "suffix": "" + "suffix": "" }, "DynRDSQN8066AGC": { "name": "DynRDSQN8066AGC", @@ -295,7 +295,7 @@ "checkedValue": "1", "uncheckedValue": "0", "default": 0, - "suffix": "" + "suffix": "" }, "DynRDSSi4713TestAudio": { "name": "DynRDSSi4713TestAudio", @@ -327,12 +327,13 @@ "name": "DynRDSPSStyle", "description": "PS Style Text (8 chars per update)", "tip": "Sent 8 characters at a time. Program Service is the most commonly displayed part of RDS.", - "restart": 1, + "restart": 0, "reboot": 0, "type": "text", "size": 32, "maxlength": 64, - "default": "{T}|{A}[|{P} of {C}]|Merry|Christ-| -mas!" + "default": "{T}|{A}[|{P} of {C}]|Merry|Christ-| -mas!", + "suffix": "" }, "DynRDSPSUpdateRate": { "name": "DynRDSPSUpdateRate", @@ -351,12 +352,13 @@ "name": "DynRDSRTStyle", "description": "RT Style Text", "tip": "Recommended to send 32 characters at a time, but can send up to 64 characters. Radio Text is intended for longer message with a slower update rate.", - "restart": 1, + "restart": 0, "reboot": 0, "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!", + "suffix": "" }, "DynRDSRTUpdateRate": { "name": "DynRDSRTUpdateRate", @@ -476,7 +478,7 @@ "checkedValue": "1", "uncheckedValue": "0", "default": 0, - "suffix": "" + "suffix": "" }, "DynRDSmqttEnable": { "name": "DynRDSmqttEnable", From 090fcf142deb5bffee9c9a7d4664a2783194680b Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Wed, 9 Sep 2026 23:07:43 -0400 Subject: [PATCH 2/4] On UPDATE only clear the {T} value if MPC changed and not playing a playlist --- Dynamic_RDS_Engine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dynamic_RDS_Engine.py b/Dynamic_RDS_Engine.py index 4d84ee1..07e51ba 100755 --- a/Dynamic_RDS_Engine.py +++ b/Dynamic_RDS_Engine.py @@ -294,11 +294,12 @@ def excessive(msg, *args, **kwargs): writeStatus() elif line == 'UPDATE': + priorMPCEnable = config['DynRDSmpcEnable'] read_config() mqtt.publish('config', json.dumps(config, indent=8)) + if config['DynRDSmpcEnable'] != priorMPCEnable and not activePlaylist: + rdsValues['{T}'] = '' if (transmitter is not None and transmitter.active): - for key in rdsValues: - rdsValues[key] = '' updateRDSData() transmitter.update() writeStatus() From 66d92b4521d17810900f8d3f63e4c5b60ebce3c2 Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Thu, 10 Sep 2026 18:00:59 -0400 Subject: [PATCH 3/4] A bit of clean up. Made RDS Update Rates live update as well --- Dynamic_RDS.php | 11 +++++++---- Dynamic_RDS_Engine.py | 16 +++++++++++++--- README.md | 2 +- settings.json | 8 ++++---- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index a956a20..0a351b9 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -491,11 +491,14 @@ function DynRDSFastUpdate() { $.get('api/plugin/Dynamic_RDS/FastUpdate'); } -// The RDS Settings group shares a single callback, but only the Style Text -// fields are re-read by the Engine on UPDATE. Everything else in the group is -// consumed when the transmitter is set up, so it still needs an FPP restart. +// The RDS Settings group shares a single callback, but only the Style Text and +// Update Rate fields are re-read by the Engine on UPDATE. Everything else in the +// group is consumed when the transmitter is set up, so it still needs an FPP restart. +var DynRDSLiveRDSSettings = ['DynRDSPSStyle', 'DynRDSRTStyle', + 'DynRDSPSUpdateRate', 'DynRDSRTUpdateRate']; + function DynRDSRDSSettingsUpdate(key) { - if (key === 'DynRDSPSStyle' || key === 'DynRDSRTStyle') { + if (DynRDSLiveRDSSettings.indexOf(key) !== -1) { DynRDSFastUpdate(); } } diff --git a/Dynamic_RDS_Engine.py b/Dynamic_RDS_Engine.py index 07e51ba..2e8e9e4 100755 --- a/Dynamic_RDS_Engine.py +++ b/Dynamic_RDS_Engine.py @@ -60,7 +60,7 @@ def read_config(): config['DynRDSQN8066BufferGain'] = totalGain % 18 // 3 if not (os.path.exists('/bin/mpc') or os.path.exists('/usr/bin/mpc')): - config['DynRDSmpcEnable'] = 0 + config['DynRDSmpcEnable'] = '0' logging.getLogger().setLevel(config['DynRDSEngineLogLevel']) logging.info('Config %s', config) @@ -297,11 +297,21 @@ def excessive(msg, *args, **kwargs): priorMPCEnable = config['DynRDSmpcEnable'] read_config() mqtt.publish('config', json.dumps(config, indent=8)) + # MPC only ever sources {T}, so toggling it while idle would otherwise leave + # the last polled title stale. During a playlist {T} comes from FPP media + # events, so leave it alone. if config['DynRDSmpcEnable'] != priorMPCEnable and not activePlaylist: rdsValues['{T}'] = '' - if (transmitter is not None and transmitter.active): + nextMPCUpdate = datetime.now() + if transmitter is not None: + # Buffers cache their delay at construction, so re-sync on config changes + # or the status panel and the chip disagree on the update rate + if getattr(transmitter, 'PS', None) is not None: + transmitter.PS.delay = int(config['DynRDSPSUpdateRate']) + transmitter.RT.delay = int(config['DynRDSRTUpdateRate']) updateRDSData() - transmitter.update() + if transmitter.active: + transmitter.update() writeStatus() elif line == 'START': diff --git a/README.md b/README.md index 2dd0f71..3113772 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ During the plugin install, an example script is copied to the FPP `media/scripts All settings are on the plugin's config page, reachable from **Status/Control -> Dynamic RDS**. The page auto-detects your transmitter over I2C and hides the settings that don't apply to it. > [!NOTE] -> Settings marked with a lightning bolt icon take effect immediately on the transmitter — no FPP restart needed. +> Settings marked with a lightning bolt icon take effect immediately — no FPP restart needed. ### RDS Settings | Setting | Default | Notes | diff --git a/settings.json b/settings.json index 6767ab9..00dbad1 100644 --- a/settings.json +++ b/settings.json @@ -339,8 +339,8 @@ "name": "DynRDSPSUpdateRate", "description": "PS Update Rate", "tip": "Interval between updating the 8 characters being sent. It takes ~1 second to send the 8 characters and some radios only display the text after receiving the full group twice.", - "suffix": "seconds", - "restart": 1, + "suffix": "seconds ", + "restart": 0, "reboot": 0, "type": "number", "min": 3, @@ -364,8 +364,8 @@ "name": "DynRDSRTUpdateRate", "description": "RT Update Rate", "tip": "Interval between updating the 32-64 characters being sent. It takes ~4 seconds to send the 64 characters and some radios only display the text after receiving the full group twice.", - "suffix": "seconds", - "restart": 1, + "suffix": "seconds ", + "restart": 0, "reboot": 0, "type": "number", "min": 3, From f666abb20fe21b496686e912f74d91bf30d3b9e5 Mon Sep 17 00:00:00 2001 From: ShadowLight8 Date: Sun, 13 Sep 2026 19:03:53 -0400 Subject: [PATCH 4/4] Add a process timeout and back-off for mpc --- Dynamic_RDS_Engine.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dynamic_RDS_Engine.py b/Dynamic_RDS_Engine.py index 2e8e9e4..e327c3c 100755 --- a/Dynamic_RDS_Engine.py +++ b/Dynamic_RDS_Engine.py @@ -387,9 +387,23 @@ def excessive(msg, *args, **kwargs): if not activePlaylist and transmitter is not None and transmitter.active and config['DynRDSmpcEnable'] == "1" and datetime.now() > nextMPCUpdate: logging.debug('Processing mpc') nextMPCUpdate = datetime.now() + timedelta(seconds=12) - # TODO: Error handling might be needed here if the mpc execution has an issue + # TODO: Future idea to handle multiple fields from mpc, but I've not seen them used yet. [{A}%artist%][{T}%title%][{N}%track%] - mpcLatest = subprocess.run(['mpc', 'current', '-f', '%title%'], stdout=subprocess.PIPE, check=False).stdout.decode('utf-8').strip() + try: + mpcLatest = subprocess.run(['mpc', 'current', '-f', '%title%'], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + check=False, timeout=2, + encoding='utf-8', errors='replace').stdout.strip() + except subprocess.TimeoutExpired: + # mpc or MPD is wedged - back off so RDS group output isn't stalled every 12 seconds + logging.warning('mpc timed out - backing off for 60 seconds') + nextMPCUpdate = datetime.now() + timedelta(seconds=60) + mpcLatest = rdsValues['{T}'] + except OSError as error: + logging.warning('mpc could not be run (%s) - backing off for 60 seconds', error) + nextMPCUpdate = datetime.now() + timedelta(seconds=60) + mpcLatest = rdsValues['{T}'] + if rdsValues['{T}'] != mpcLatest: rdsValues['{T}'] = mpcLatest updateRDSData()