diff --git a/Dynamic_RDS.php b/Dynamic_RDS.php index 2b4ee62..0a351b9 100644 --- a/Dynamic_RDS.php +++ b/Dynamic_RDS.php @@ -491,6 +491,18 @@ function DynRDSFastUpdate() { $.get('api/plugin/Dynamic_RDS/FastUpdate'); } +// 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 (DynRDSLiveRDSSettings.indexOf(key) !== -1) { + DynRDSFastUpdate(); + } +} + function DynRDSPiBootUpdate(key) { $.post('api/plugin/Dynamic_RDS/PiBootChange/' + encodeURIComponent(key), JSON.stringify(Object.assign({}, pluginSettings))); @@ -519,15 +531,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/Dynamic_RDS_Engine.py b/Dynamic_RDS_Engine.py index 4d84ee1..e327c3c 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) @@ -294,13 +294,24 @@ def excessive(msg, *args, **kwargs): writeStatus() elif line == 'UPDATE': + priorMPCEnable = config['DynRDSmpcEnable'] read_config() mqtt.publish('config', json.dumps(config, indent=8)) - if (transmitter is not None and transmitter.active): - for key in rdsValues: - rdsValues[key] = '' + # 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}'] = '' + 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': @@ -376,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() 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 1feb232..00dbad1 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,19 +327,20 @@ "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", "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, @@ -351,19 +352,20 @@ "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", "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, @@ -476,7 +478,7 @@ "checkedValue": "1", "uncheckedValue": "0", "default": 0, - "suffix": "" + "suffix": "" }, "DynRDSmqttEnable": { "name": "DynRDSmqttEnable",