From 045aa0f785e1eb3fe5a7cc364d28adcf91f5bdfc Mon Sep 17 00:00:00 2001 From: kasemir Date: Thu, 1 May 2025 15:53:21 -0400 Subject: [PATCH 1/5] Alarms: Support `infopv:NameOfPV` action Similar to `mailto` action, but sends info to PV instead of email. --- app/alarm/examples/infopv.db | 13 ++ app/alarm/ui/doc/index.rst | 20 +++ .../alarm/ui/tree/TitleDetailDelayTable.java | 14 +- .../alarm/server/ServerModel.java | 6 +- .../actions/AutomatedActionExecutor.java | 26 +-- .../server/actions/EmailActionExecutor.java | 14 +- .../server/actions/InfoPVActionExecutor.java | 161 ++++++++++++++++++ 7 files changed, 236 insertions(+), 18 deletions(-) create mode 100644 app/alarm/examples/infopv.db create mode 100644 services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java diff --git a/app/alarm/examples/infopv.db b/app/alarm/examples/infopv.db new file mode 100644 index 0000000000..555c08b52b --- /dev/null +++ b/app/alarm/examples/infopv.db @@ -0,0 +1,13 @@ +# Example for "Info PV" +# used with automated action set to "infopv:NameOfPV" +# +# softIoc -s -m N=NameOfPV -d infopv.db +# +# With Channel Access, use $(N).VAL$ to access the full text. + +record(lsi, "$(N)") +{ + field(SIZV, 1000) + field(INP, {const:""}) + field(PINI, "YES") +} diff --git a/app/alarm/ui/doc/index.rst b/app/alarm/ui/doc/index.rst index f15e789175..478bcd9d29 100644 --- a/app/alarm/ui/doc/index.rst +++ b/app/alarm/ui/doc/index.rst @@ -307,6 +307,26 @@ Sends email with alarm detail to list of recipients. The email server is configured in the alarm preferences. +``infopv:SomePV``: +Writes the alarm detail to a PV. + +The PV needs to hold a string, for example:: + + # Example for "Info PV" + # used with automated action set to "infopv:NameOfPV" + # + # softIoc -s -m N=NameOfPV -d infopv.db + + record(lsi, "$(N)") + { + field(SIZV, 1000) + field(INP, {const:""}) + field(PINI, "YES") + } + +With Channel Access, since the text usually exceeds 40 characters, use ``infopv:SomePV.VAL$``. + + ``cmd:some_command arg1 arg2``: Invokes command with list of space-separated arguments. The special argument "*" will be replaced with a list of alarm PVs and their alarm severity. diff --git a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java index 6e06d30a0f..c1ae0a051a 100644 --- a/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java +++ b/app/alarm/ui/src/main/java/org/phoebus/applications/alarm/ui/tree/TitleDetailDelayTable.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2023 Oak Ridge National Laboratory. + * Copyright (c) 2018-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -49,8 +49,16 @@ @SuppressWarnings("nls") public class TitleDetailDelayTable extends BorderPane { - private enum Option_d { - mailto, cmd, sevrpv + private enum Option_d + { + // Send email with alarm info + mailto, + // Execute external command + cmd, + // Update PV with severity + sevrpv, + // Update PV with alarm info text + infopv }; private final ObservableList items = FXCollections.observableArrayList(); diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java index c53e43e09c..73ccd79103 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/ServerModel.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2022 Oak Ridge National Laboratory. + * Copyright (c) 2018-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,7 +15,6 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import org.apache.kafka.clients.consumer.Consumer; @@ -34,6 +33,7 @@ import org.phoebus.applications.alarm.model.SeverityLevel; import org.phoebus.applications.alarm.model.json.JsonModelReader; import org.phoebus.applications.alarm.model.json.JsonModelWriter; +import org.phoebus.applications.alarm.server.actions.InfoPVActionExecutor; /** Server's model of the alarm configuration * @@ -120,6 +120,7 @@ public void start() { thread.start(); SeverityPVHandler.initialize(); + InfoPVActionExecutor.initialize(); // Alarm server startup message sendAnnunciatorMessage(root.getPathName(), SeverityLevel.OK, "* Alarm server started. Everything is going to be all right."); @@ -603,6 +604,7 @@ private void clearActionsAndStopPVs(final AlarmTreeItem node) public void shutdown() { SeverityPVHandler.stop(); + InfoPVActionExecutor.stop(); running = false; consumer.wakeup(); try diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/AutomatedActionExecutor.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/AutomatedActionExecutor.java index 33f271c31b..0750ec4ef1 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/AutomatedActionExecutor.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/AutomatedActionExecutor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2019 Oak Ridge National Laboratory. + * Copyright (c) 2018-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -27,6 +27,9 @@ *

"mailto:user@site.org,another@else.com"
* Sends email with alarm detail to list of recipients. * + *

"infopv:ca://demo"
+ * Writes alarm detail to string PV. + * *

"cmd:some_command arg1 arg2"
* Invokes command with list of space-separated arguments. * The special argument "*" will be replaced with a list of alarm PVs and their alarm severity. @@ -44,18 +47,21 @@ public void accept(final AlarmTreeItem item, final TitleDetailDelay action) // Perform the automated action in background thread JobManager.schedule("Automated Action", monitor -> { - if (action.detail.startsWith("mailto:")) - { - if (AlarmLogic.getDisableNotify() == true) - { - return; - } - EmailActionExecutor.sendEmail(item, action.detail.substring(7).split(" *, *")); - } + final boolean mailto = action.detail.startsWith("mailto:"); + final boolean infopv = action.detail.startsWith("infopv:"); + if (mailto || infopv) + { // Are notifications disabled? + if (AlarmLogic.getDisableNotify()) + return; + if (mailto) + EmailActionExecutor.sendEmail(item, action.detail.substring(7).split(" *, *")); + else + InfoPVActionExecutor.writeInfo(item, action.detail.substring(7).trim()); + } else if (action.detail.startsWith("cmd:")) CommandActionExecutor.run(item, action.detail.substring(4)); else - logger.log(Level.WARNING, "Automated action " + action + " lacks 'mailto:' or 'cmd:' in detail"); + logger.log(Level.WARNING, "Automated action " + action + " lacks 'mailto:', 'infopv:' or 'cmd:' in detail"); }); } diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/EmailActionExecutor.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/EmailActionExecutor.java index 2ca204644f..32ce426fd2 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/EmailActionExecutor.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/EmailActionExecutor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2020 Oak Ridge National Laboratory. + * Copyright (c) 2018-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -54,7 +54,11 @@ static void sendEmail(final AlarmTreeItem item, final String[] addresses) } } - private static String createTitle(final AlarmTreeItem item) + /** Create title for email, also used by Info PV + * @param item Item for which to create title + * @return Title + */ + static String createTitle(final AlarmTreeItem item) { final StringBuilder buf = new StringBuilder(); @@ -77,7 +81,11 @@ private static String createTitle(final AlarmTreeItem item) return buf.toString(); } - private static String createBody(final AlarmTreeItem item) + /** Create info body for email, also used by Info PV + * @param item Item for which to create info + * @return Info text + */ + static String createBody(final AlarmTreeItem item) { final StringBuilder buf = new StringBuilder(); diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java new file mode 100644 index 0000000000..230e55a2c7 --- /dev/null +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2025 Oak Ridge National Laboratory. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.phoebus.applications.alarm.server.actions; + +import static org.phoebus.applications.alarm.AlarmSystem.logger; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; + +/** Executor for 'infopv:' actions + * + *

Handles automated actions with the following detail: + * + *

"infopv:SomePVName"
+ * Writes alarm detail as string to PV. + * + * @author Kay Kasemir + */ +public class InfoPVActionExecutor +{ + /** Map of PV name to PV. + * PVs are created/added on first use and then kept in here until the server shuts down. + */ + private static final ConcurrentHashMap pvs = new ConcurrentHashMap<>(); + + /** Info to write to a PV and time it was requested */ + private static record TimedInfo(Instant time, String info) {}; + + /** Map of PV name to most recent info. + * Catches updates to a PV. + * If several updates arrive for the same PV, they are not queued + * because older messages tend to be obsolete. + * They are written with a slight delay to reduce traffic, then removed. + */ + private static final ConcurrentHashMap updates = new ConcurrentHashMap<>(); + + /** Flag for thread to exit */ + private static CountDownLatch done = new CountDownLatch(1); + + /** Thread that performs updates */ + private static final Thread thread = new Thread(InfoPVActionExecutor::run, "InfoPVActionExecutor"); + + + /** Initialize, start InfoPVActionExecutor thread */ + public static void initialize() + { + thread.setDaemon(true); + thread.start(); + } + + + /** Request writing alarm info text to PV + * @param item Alarm item from which to get alarm info + * @param pv_name Name of PV to update + */ + public static void writeInfo(final AlarmTreeItem item, final String pv_name) + { + final String info = EmailActionExecutor.createTitle(item) + + EmailActionExecutor.createBody(item); + + // Register PV or find existing one + PV pv = pvs.computeIfAbsent(pv_name, name -> + { + try + { + return PVPool.getPV(pv_name); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Cannot create PV '" + pv_name + "'", ex); + } + return null; + }); + // On success, register update + if (pv != null) + updates.put(pv_name, new TimedInfo(Instant.now(), info)); + } + + + /** Executed by InfoPVActionExecutor: + * Waits for requested updates and performs them. + */ + private static void run() + { + try + { + // Delay to throttle the rate of writes and re-tries + while (! done.await(1, TimeUnit.SECONDS)) + { + // Keep trying to write an update until it's old + final Instant old = Instant.now().minus(Duration.ofSeconds(30)); + for (String pv_name : updates.keySet()) + { + final TimedInfo update = updates.get(pv_name); + boolean success = false; + try + { + final PV pv = pvs.get(pv_name); + pv.write(update.info); + success = true; + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Failed to write alarm info to " + pv_name, ex); + } + // If update was handled, or failed several times and is now old, remove it + if (success || update.time.isBefore(old)) + { // Only remove the one we're dealing with! + if (updates.remove(pv_name, update)) + { + if (success) + logger.log(Level.INFO, "Wrote alarm info to " + pv_name); + else + logger.log(Level.WARNING, "Give up writing alarm info to " + pv_name); + } + // else: There's already a new update, keep that + } + // else: Update failed, not old, try again + } + + } + } + catch (Exception ex) + { + logger.log(Level.WARNING, Thread.currentThread().getName() + " error", ex); + } + } + + + /** Release all PVs */ + public static void stop() + { + // Stop thread + done.countDown(); + try + { + thread.join(Duration.ofSeconds(5)); + } + catch (InterruptedException ex) + { + // Ignore, closing down anyway + } + // Release all PVs + for (PV pv : pvs.values()) + PVPool.releasePV(pv); + pvs.clear(); + } +} From 82ec2a10b6896ae4e64463948ddf28c7420c10e9 Mon Sep 17 00:00:00 2001 From: kasemir Date: Thu, 1 May 2025 16:07:39 -0400 Subject: [PATCH 2/5] Avoid Thread.join(Duration) with older JRE --- .../applications/alarm/server/actions/InfoPVActionExecutor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java index 230e55a2c7..c3f5a4c08a 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java @@ -147,7 +147,7 @@ public static void stop() done.countDown(); try { - thread.join(Duration.ofSeconds(5)); + thread.join(5000); } catch (InterruptedException ex) { From c73e72e3a2fdfe1306623d1656b858d0bdd71a69 Mon Sep 17 00:00:00 2001 From: kasemir Date: Mon, 5 May 2025 12:43:55 -0400 Subject: [PATCH 3/5] MQTT info --- app/alarm/ui/doc/index.rst | 6 +- core/pv-mqtt/Readme.md | 107 ++++++++++++++++++ services/alarm-server/.classpath | 5 +- .../resources/alarm_server_logging.properties | 1 + 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 core/pv-mqtt/Readme.md diff --git a/app/alarm/ui/doc/index.rst b/app/alarm/ui/doc/index.rst index 478bcd9d29..921ec89f4d 100644 --- a/app/alarm/ui/doc/index.rst +++ b/app/alarm/ui/doc/index.rst @@ -310,7 +310,11 @@ The email server is configured in the alarm preferences. ``infopv:SomePV``: Writes the alarm detail to a PV. -The PV needs to hold a string, for example:: +The PV needs to hold a string, for example +`mqtt://alarm/message` for an MQTT topic +or +`ca://NameOfPV.VAL$` +for Channel Access where the PV refers to a string record:: # Example for "Info PV" # used with automated action set to "infopv:NameOfPV" diff --git a/core/pv-mqtt/Readme.md b/core/pv-mqtt/Readme.md new file mode 100644 index 0000000000..b71bc81afd --- /dev/null +++ b/core/pv-mqtt/Readme.md @@ -0,0 +1,107 @@ +MQTT PV Support +=============== + +MQTT is a broker-based protocol used in the Internet-of-Things (IoT) ecosystem. +See https://mqtt.org for details. +The `mqtt:...` PV support allows CS-Studio tools to read and write PVs via MQTT. + + +Example Broker Setup and first steps +------------------------------------ + +This example uses Eclipse Mosquitto on Linux. +See https://mqtt.org for links to several other MQTT brokers and clients. + +Download: Get source release `mosquitto-....tar.gz` from https://mosquitto.org/download + +Unpack: `tar vzxf mosquitto-....tar.gz` + +Build: `make WITH_CJSON=no` + +Install: + +``` +export LD_LIBRARY_PATH=`pwd`/lib +# Optionally, add the following to a `bin` folder that's on your $PATH +# src/mosquitto, client/mosquitto_sub, client/mosquitto_pub +``` + +Run broker: + +``` +# Allow remote access through firewall. +# Depending on Linux release, similar to this +sudo firewall-cmd --add-port=1883/tcp + +# Create configuration file that allows remote access +echo "listener 1883" >> mosquitto.conf +echo "allow_anonymous true" >> mosquitto.conf + +# Start broker with that configuration file +src/mosquitto -c mosquitto.conf +mosquitto version ... starting +Config loaded from mosquitto.conf. +... +Opening ipv4 listen socket on port 1883. +``` + +See https://mosquitto.org/documentation/authentication-methods +for a more secure configuration. + +Subscribe to value updates: `client/mosquitto_sub -t sensors/temperature -q 1 ` + +Publish a value: `client/mosquitto_pub -t sensors/temperature -q 1 -m 42` + +By default, the broker will not persist messages. +The subscribe command shown above will receive all newly +published messages. If you close the `mosquitto_sub` and then restart it, +it will show nothing until a new value is published. + +To persist data on the broker, each client that publishes or subscribes +needs to connect with a unique client ID and an option to _not_ 'clean' the session, +using options like `--id 8765 --disable-clean-session` for the `.._sub` and `.._pub` +commands shown above. + + +MQTT PV Configuration +--------------------- + +By default, the MQTT PV will look for a broker on localhost +and the default MQTT broker port 1883. + +To change this, add a variant of the following to your Phoebus settings: + +``` +# MQTT Broker +# All "mqtt://some/tag" PVs will use this broker +#org.phoebus.pv.mqtt/mqtt_broker=tcp://localhost:1883 +org.phoebus.pv.mqtt/mqtt_broker=tcp://my_host.site.org:1883 +``` + +The MQTT PV will create a unique internal ID to read persisted messages, +allowing the PV to start up with the last known value of an MQTT topic +without need to wait for the next update. + + +MQTT PV Syntax +-------------- + +To interface with the example MQTT tag shown above, +use the PV `mqtt://sensors/temperature`. + +The general format is `mqtt://` followed by the MQTT topic, +for example `sensors/temperature`, +and an optional ``. + +MQTT treats all tag data as text. By default, an MQTT PV expects +the text to contain a number, but the optional `` will +instruct the PV to parse the text in other ways. + +| `VType` | PV Value Parser | +| ---------------- | ---------------------------------------------------------------------------- | +| `` | This is the default, expecting the topic to parse as a floating point number | +| `` | PV reads text as string | +| `` | Parse as long integer | +| `` | Parse as array of comma-separated floating point numbers | +| `` | Parse text as array of comma-separated strings | + diff --git a/services/alarm-server/.classpath b/services/alarm-server/.classpath index 6dee175f7c..0d6d1b39f0 100644 --- a/services/alarm-server/.classpath +++ b/services/alarm-server/.classpath @@ -9,7 +9,10 @@ - + + + + diff --git a/services/alarm-server/src/main/resources/alarm_server_logging.properties b/services/alarm-server/src/main/resources/alarm_server_logging.properties index 3a5efd95eb..fee1014910 100644 --- a/services/alarm-server/src/main/resources/alarm_server_logging.properties +++ b/services/alarm-server/src/main/resources/alarm_server_logging.properties @@ -29,5 +29,6 @@ org.apache.kafka.level = WARNING org.phoebus.applications.alarm.level = INFO com.cosylab.epics.caj.level = WARNING +org.eclipse.paho.client.level = CONFIG org.phoebus.framework.rdb.level = WARNING org.phoebus.pv.level = CONFIG From e04f22a587c94cab7236ea0b0810374edb262df2 Mon Sep 17 00:00:00 2001 From: kasemir Date: Mon, 5 May 2025 13:08:21 -0400 Subject: [PATCH 4/5] By default, include infopv: in follow-up --- app/alarm/model/src/main/resources/alarm_preferences.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/alarm/model/src/main/resources/alarm_preferences.properties b/app/alarm/model/src/main/resources/alarm_preferences.properties index 1fe4640435..e6e8fa83e3 100644 --- a/app/alarm/model/src/main/resources/alarm_preferences.properties +++ b/app/alarm/model/src/main/resources/alarm_preferences.properties @@ -101,7 +101,7 @@ automated_email_sender=Alarm Notifier # Comma-separated list of automated actions on which to follow up # Options include mailto:, cmd: -automated_action_followup=mailto:, cmd: +automated_action_followup=mailto:, cmd:, infopv: # Optional heartbeat PV # When defined, alarm server will set it to 1 every heartbeat_secs From 5751351a846d22009f6220c995fd4a616746bdcd Mon Sep 17 00:00:00 2001 From: kasemir Date: Mon, 5 May 2025 13:09:30 -0400 Subject: [PATCH 5/5] infopv: Separate title and body --- .../alarm/server/actions/InfoPVActionExecutor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java index c3f5a4c08a..8ecddee003 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/actions/InfoPVActionExecutor.java @@ -61,14 +61,13 @@ public static void initialize() thread.start(); } - /** Request writing alarm info text to PV * @param item Alarm item from which to get alarm info * @param pv_name Name of PV to update */ public static void writeInfo(final AlarmTreeItem item, final String pv_name) { - final String info = EmailActionExecutor.createTitle(item) + + final String info = EmailActionExecutor.createTitle(item) + System.lineSeparator() + EmailActionExecutor.createBody(item); // Register PV or find existing one