From f80a2c541a77ccf8aace5cdb9f66a2040646cf5a Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Mon, 3 Nov 2025 13:10:00 +0100 Subject: [PATCH 01/10] [phoebus] add simulate / run (queued) buttons to top of scan server editor --- .../csstudio/scan/ui/editor/ScanEditor.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java index 7135c45bb2..ff630b47f0 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java @@ -249,7 +249,26 @@ private ToolBar createToolbar() jump_to_current.setOnAction(event -> scan_tree.revealActiveItem(jump_to_current.isSelected())); final Button[] undo_redo = UndoButtons.createButtons(undo); - return new ToolBar(info_text, ToolbarHelper.createStrut(), buttons, ToolbarHelper.createSpring(), undo_redo[0], undo_redo[1]); + final Button run_button = new Button(); + run_button.setGraphic(ImageCache.getImageView(ScanSystem.class, "/icons/run.png")); + run_button.setTooltip(new Tooltip(Messages.scan_submit)); + run_button.setOnAction(event -> submitOrSimulate(true)); + + final Button simulate_button = new Button(); + simulate_button.setGraphic(ImageCache.getImageView(ScanSystem.class, "/icons/simulate.png")); + simulate_button.setTooltip(new Tooltip(Messages.scan_simulate)); + simulate_button.setOnAction(event -> submitOrSimulate(null)); + + return new ToolBar( + info_text, + ToolbarHelper.createStrut(), + buttons, + ToolbarHelper.createSpring(), + undo_redo[0], + undo_redo[1], + simulate_button, + run_button + ); } private void createContextMenu() From fa209e3550d903be487b2c374cd17547728e15ad Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 6 Nov 2025 13:54:36 +0100 Subject: [PATCH 02/10] [phoebus] fix WrapAroundValueFactory --- .../main/java/org/phoebus/ui/time/WraparoundValueFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/org/phoebus/ui/time/WraparoundValueFactory.java b/core/ui/src/main/java/org/phoebus/ui/time/WraparoundValueFactory.java index 1dcd849090..a4c8238547 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/WraparoundValueFactory.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/WraparoundValueFactory.java @@ -75,7 +75,7 @@ public void decrement(final int steps) public void increment(final int steps) { final int value = getValue() + 1; - if (value != max) + if (value != max + 1) setValue(value); else { From 342258717fbc26adb945f339419a6ba3d77b41b3 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 30 Oct 2025 15:22:03 +0100 Subject: [PATCH 03/10] Scheduled scans in scan server and ActionButtonWidgetRuntime with scheduled scan support --- .../display/actions/WritePVAction.java | 32 ++++ app/display/runtime/pom.xml | 12 ++ .../actionhandlers/WritePVActionHandler.java | 19 +-- .../internal/ActionButtonWidgetRuntime.java | 66 ++++++++ .../runtime/internal/BaseWidgetRuntimes.java | 2 + .../src/main/resources/icons/clock.png | Bin 0 -> 500 bytes .../org/csstudio/scan/client/ScanClient.java | 24 ++- .../csstudio/scan/client/ScanInfoModel.java | 4 +- .../org/csstudio/scan/info/ScanState.java | 5 +- .../java/org/csstudio/scan/ui/Messages.java | 1 + .../csstudio/scan/ui/editor/ScanEditor.java | 24 ++- .../scan/ui/editor/ScheduledScanDialog.java | 48 ++++++ .../csstudio/scan/ui/monitor/ScansTable.java | 14 +- .../csstudio/scan/ui/monitor/StateCell.java | 4 + .../ui/src/main/resources/icons/clock.png | Bin 0 -> 500 bytes .../org/csstudio/scan/ui/messages.properties | 1 + .../org/csstudio/scan/server/ScanServer.java | 3 +- .../scan/server/httpd/ScanServlet.java | 26 +++- .../scan/server/internal/ExecutableScan.java | 1 - .../scan/server/internal/ScanEngine.java | 56 ++++++- .../scan/server/internal/ScanServerImpl.java | 21 ++- .../scan/server/internal/ScheduledScan.java | 142 ++++++++++++++++++ .../server/internal/ScheduledScanTask.java | 28 ++++ .../src/main/resources/examples/scan.db | 2 + .../src/main/resources/webroot/index.html | 1 + 25 files changed, 500 insertions(+), 36 deletions(-) create mode 100644 app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java create mode 100644 app/display/runtime/src/main/resources/icons/clock.png create mode 100644 app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java create mode 100644 app/scan/ui/src/main/resources/icons/clock.png create mode 100644 services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java create mode 100644 services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScanTask.java diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java index 104a82a767..1f189d64c6 100644 --- a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java @@ -5,11 +5,14 @@ package org.csstudio.display.actions; import javafx.scene.image.Image; +import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.ModelWriter; import org.csstudio.display.builder.model.persist.XMLTags; import org.csstudio.display.builder.model.properties.ActionInfoBase; import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; +import org.phoebus.framework.macros.MacroHandler; +import org.phoebus.framework.macros.MacroValueProvider; import org.phoebus.framework.persistence.XMLUtil; import org.phoebus.ui.javafx.ImageCache; import org.w3c.dom.Element; @@ -107,4 +110,33 @@ public void setPv(String pv) { public void setValue(String value) { this.value = value; } + + public String formatPv(Widget widget) { + final MacroValueProvider macros = widget.getMacrosOrProperties(); + String pvName = getPV(); + try + { + pvName = MacroHandler.replace(macros, pvName); + } + catch (Exception ignore) + { + // NOP + } + return pvName; + } + + public String formatValue(Widget widget) { + final MacroValueProvider macros = widget.getMacrosOrProperties(); + value = getValue(); + + try + { + value = MacroHandler.replace(macros, value); + } + catch (Exception ignore) + { + // NOP + } + return value; + } } diff --git a/app/display/runtime/pom.xml b/app/display/runtime/pom.xml index 3022ea40b7..01233677b1 100644 --- a/app/display/runtime/pom.xml +++ b/app/display/runtime/pom.xml @@ -60,5 +60,17 @@ app-display-actions 5.0.3-SNAPSHOT + + org.phoebus + app-scan-client + 5.0.2 + compile + + + org.phoebus + app-scan-ui + 5.0.2 + compile + diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java index 2530f0f4ca..18e7104410 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java @@ -25,23 +25,8 @@ public void handleAction(Widget sourceWidget, ActionInfo pluggableActionInfo) { // System.out.println(action.getDescription() + ": Set " + action.getPV() + " = " + action.getValue()); final MacroValueProvider macros = sourceWidget.getMacrosOrProperties(); WritePVAction writePVAction = (WritePVAction)pluggableActionInfo; - String pvName = writePVAction.getPV(), value = writePVAction.getValue(); - try - { - pvName = MacroHandler.replace(macros, pvName); - } - catch (Exception ignore) - { - // NOP - } - try - { - value = MacroHandler.replace(macros, value); - } - catch (Exception ignore) - { - // NOP - } + String pvName = writePVAction.formatPv(sourceWidget); + String value = writePVAction.formatValue(sourceWidget); try { runtime.writePV(pvName,value); diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java new file mode 100644 index 0000000000..7e32cef008 --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java @@ -0,0 +1,66 @@ +package org.csstudio.display.builder.runtime.internal; + +import javafx.scene.Node; +import org.csstudio.display.actions.WritePVAction; +import org.csstudio.display.builder.model.widgets.ActionButtonWidget; +import org.csstudio.display.builder.representation.javafx.widgets.JFXBaseRepresentation; +import org.csstudio.display.builder.runtime.RuntimeAction; +import org.csstudio.display.builder.runtime.WidgetRuntime; +import org.csstudio.scan.client.ScanInfoModel; +import org.csstudio.scan.ui.editor.ScheduledScanDialog; +import org.phoebus.ui.dialog.DialogHelper; + +import java.util.*; + + +public class ActionButtonWidgetRuntime extends WidgetRuntime { + + private class ScheduledTaskDialogAction extends RuntimeAction { + private final String script_xml; + + public ScheduledTaskDialogAction(String scriptXml) { + super("Schedule Task", "/icons/clock.png"); + script_xml = scriptXml; + } + + @Override + public void run() { + try { + ScheduledScanDialog dialog = new ScheduledScanDialog( + "Scheduled Task", + ScanInfoModel.getInstance().getScanClient(), + script_xml + ); + final Node node = JFXBaseRepresentation.getJFXNode(widget); + DialogHelper.positionDialog(dialog, node, 0, 0); + dialog.showAndWait(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public Collection getRuntimeActions() { + List commands = widget.propActions().getValue().getActions() + .stream() + .filter(action -> action instanceof WritePVAction) + .map(action -> { + WritePVAction writePVAction = (WritePVAction) action; + String pvName = writePVAction.formatPv(widget); + String value = writePVAction.formatValue(widget); + return ( + "" + + "" + pvName + "" + + "" + value + "" + + "" + ); + }).toList(); + + if (commands.isEmpty()) { + return List.of(); + } + + return List.of(new ScheduledTaskDialogAction("" + String.join("", commands) + "")); + } +} diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java index 5cd511ad70..8056cb5dcb 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java @@ -13,6 +13,7 @@ import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.widgets.ActionButtonWidget; import org.csstudio.display.builder.model.widgets.ArrayWidget; import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; import org.csstudio.display.builder.model.widgets.GroupWidget; @@ -39,6 +40,7 @@ public Map>> getWidgetRuntimeFa { return Map.ofEntries( entry(DisplayModel.WIDGET_TYPE, () -> new DisplayRuntime()), + entry(ActionButtonWidget.WIDGET_DESCRIPTOR.getType(), () -> new ActionButtonWidgetRuntime()), entry(ArrayWidget.WIDGET_DESCRIPTOR.getType(), () -> new ArrayWidgetRuntime()), entry(EmbeddedDisplayWidget.WIDGET_DESCRIPTOR.getType(), () -> new EmbeddedDisplayRuntime()), entry(GroupWidget.WIDGET_DESCRIPTOR.getType(), () -> new GroupWidgetRuntime()), diff --git a/app/display/runtime/src/main/resources/icons/clock.png b/app/display/runtime/src/main/resources/icons/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..4375b83e147d5c2ab359aee6b48fc47c6cab2be4 GIT binary patch literal 500 zcmVa{dTA;etXcixd_XY2*Te zL`fPYNKPXuZL|_oh-UW!x=sqsk@$sAIWzCF%sd7FK)<}V+xLaSYqL3>Ob|k5i^b#h zUMCVxp4Vp(0000XM8Y^K2o*)qC>`|sgMMG5w4x|cL69&ege)f!08m2WqL`Ou#u#kC z7?Uu*42LNptAK>@Z8i(L`RI1@vMgZ?0H~v?=b_L;sRRI1i)B}aF%}hsj;f+`DpgSw zy|E`#QIvElg#vE3M(N+iu8c-$!0qPdj2#<|`qDm&MUu&US*_o_9{SHywK^J&I0T?+ z8rop)a;;S^VzCpO4V^iikL5BP!0Jo>VDO0J&;dNxY95~twN*7L2wP94vsf%SWruPp;r_{2zhlc3@0000 getScanDevices(final long id) throws Exception * @param name Name of the new scan * @param xml_commands XML commands of the scan to submit * @param queue Submit to queue or for immediate execution? + * @param scheduled Time at which to run the scan * @return Scan ID * @throws Exception on error */ - public long submitScan(final String name, final String xml_commands, final boolean queue) throws Exception + public long submitScan( + final String name, + final String xml_commands, + final boolean queue, + LocalDateTime scheduled + ) throws Exception { - final HttpURLConnection connection = connect("/scan/" + name, queue ? "" : "queue=false", long_timeout); + List query = new ArrayList<>(); + if (!queue) { + query.add("queue=false"); + } + if (scheduled != null) { + query.add("scheduled=" + scheduled.format(TimestampFormats.SECONDS_FORMAT)); + } + + final HttpURLConnection connection = connect("/scan/" + name, String.join("&", query), long_timeout); connection.setReadTimeout(0); try { @@ -427,6 +443,10 @@ public long submitScan(final String name, final String xml_commands, final boole } } + public long submitScan(final String name, final String xml_commands, final boolean queue) throws Exception { + return submitScan(name, xml_commands, queue, null); + } + /** Submit a scan for simulation * @param xml_commands XML commands of the scan to submit * @return Scan ID diff --git a/app/scan/client/src/main/java/org/csstudio/scan/client/ScanInfoModel.java b/app/scan/client/src/main/java/org/csstudio/scan/client/ScanInfoModel.java index 495a0f3701..b4dd5795aa 100644 --- a/app/scan/client/src/main/java/org/csstudio/scan/client/ScanInfoModel.java +++ b/app/scan/client/src/main/java/org/csstudio/scan/client/ScanInfoModel.java @@ -65,7 +65,7 @@ public class ScanInfoModel * @throws Exception on error creating the initial instance * @see #release() */ - public static ScanInfoModel getInstance() throws Exception + public static ScanInfoModel getInstance() { synchronized (ScanInfoModel.class) { @@ -124,7 +124,7 @@ public void removeListener(final ScanInfoModelListener listener) } /** Start model, i.e. connect to server, poll, ... */ - private void start() throws Exception + private void start() { final long poll_period = Preferences.poll_period; poller = new Thread(new Runnable() diff --git a/app/scan/model/src/main/java/org/csstudio/scan/info/ScanState.java b/app/scan/model/src/main/java/org/csstudio/scan/info/ScanState.java index f2a66df91c..ec9d4a4044 100644 --- a/app/scan/model/src/main/java/org/csstudio/scan/info/ScanState.java +++ b/app/scan/model/src/main/java/org/csstudio/scan/info/ScanState.java @@ -45,7 +45,10 @@ public enum ScanState Finished("Finished - OK", false, true), /** Scan that executed in the past; data has been logged */ - Logged("Logged", false, true); + Logged("Logged", false, true), + + /** Scan is waiting to be executed */ + Scheduled("Scheduled", false, false); final private String name; final private boolean active; diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java index c520488ebe..058dd8509a 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java @@ -27,6 +27,7 @@ public class Messages public static String scan_remove; public static String scan_resume; public static String scan_resume_all; + public static String scan_schedule; public static String scan_simulate; public static String scan_submit; public static String scan_submit_unqueued; diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java index ff630b47f0..9c548f1c04 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java @@ -12,6 +12,7 @@ import java.io.File; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; @@ -249,6 +250,12 @@ private ToolBar createToolbar() jump_to_current.setOnAction(event -> scan_tree.revealActiveItem(jump_to_current.isSelected())); final Button[] undo_redo = UndoButtons.createButtons(undo); + + final Button schedule_button = new Button(); + schedule_button.setGraphic(ImageCache.getImageView(ScanSystem.class, "/icons/clock.png")); + schedule_button.setTooltip(new Tooltip(Messages.scan_schedule)); + schedule_button.setOnAction(event -> schedule(true)); + final Button run_button = new Button(); run_button.setGraphic(ImageCache.getImageView(ScanSystem.class, "/icons/run.png")); run_button.setTooltip(new Tooltip(Messages.scan_submit)); @@ -266,6 +273,7 @@ private ToolBar createToolbar() ToolbarHelper.createSpring(), undo_redo[0], undo_redo[1], + schedule_button, simulate_button, run_button ); @@ -309,6 +317,20 @@ private void createContextMenu() setContextMenu(menu); } + private void schedule(final Boolean queued) { + final String xml_commands; + try { + xml_commands = XMLCommandWriter.toXMLString(model.getCommands()); + } catch (Exception e) { + throw new RuntimeException(e); + } + final ScanClient scan_client = new ScanClient(Preferences.host, Preferences.port); + ScheduledScanDialog dialog = new ScheduledScanDialog(scan_name, scan_client, xml_commands); + DialogHelper.positionDialog(dialog, this, 100, 100); + Optional scan_id = dialog.showAndWait(); + scan_id.ifPresent(this::attachScan); + } + /** @param how true/false to submit queue/un-queued, null to simulate */ private void submitOrSimulate(final Boolean how) { @@ -381,7 +403,7 @@ UndoableActionManager getUndo() * @param id Scan ID * @throws Exception on error */ - void attachScan(final long id) throws Exception + void attachScan(final long id) { active_scan = id; diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java new file mode 100644 index 0000000000..847c322722 --- /dev/null +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java @@ -0,0 +1,48 @@ +package org.csstudio.scan.ui.editor; + +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import org.csstudio.scan.client.ScanClient; +import org.phoebus.ui.time.DateTimePane; + +import java.time.LocalDateTime; +import java.time.ZoneId; + + +public class ScheduledScanDialog extends Dialog { + final String scan_name; + final ScanClient scan_client; + final String script_xml; + + public ScheduledScanDialog(String scan_name, ScanClient scan_client, String script_xml) { + this.scan_name = scan_name; + this.scan_client = scan_client; + this.script_xml = script_xml; + + setTitle("Schedule " + scan_name); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + final DateTimePane datetime = new DateTimePane(); + getDialogPane().setContent(datetime); + + setResultConverter(button -> + { + if (button == ButtonType.OK) { + LocalDateTime scheduled = LocalDateTime.ofInstant(datetime.getInstant(), ZoneId.systemDefault()); + try { + return scan_client.submitScan( + scan_name, + script_xml, + true, + scheduled + ); + } + catch (Exception e) { + return null; + } + } else { + return null; + } + }); + } +} diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/ScansTable.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/ScansTable.java index b14826fa94..d6a9ece69c 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/ScansTable.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/ScansTable.java @@ -260,17 +260,19 @@ private static int rankState(final ScanState state) { switch (state) { - case Running: // Most important, happening right now + case Running: // Most important, happening right now + return 7; + case Paused: // Very similar to a running state return 6; - case Paused: // Very similar to a running state + case Idle: // About to run next return 5; - case Idle: // About to run next + case Scheduled: // Scheduled to run in the future return 4; - case Failed: // Of the not running ones, failure is important to know + case Failed: // Of the not running ones, failure is important to know return 3; - case Aborted: // Aborted on purpose + case Aborted: // Aborted on purpose return 2; - case Finished:// Water down the bridge + case Finished: // Water down the bridge return 1; case Logged: default: diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/StateCell.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/StateCell.java index a946f7d3ec..bc53129fb1 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/StateCell.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/monitor/StateCell.java @@ -179,6 +179,9 @@ protected void updateItem(final ScanState state, final boolean empty) show(getResume()); show(getAbort()); break; + case Scheduled: + show(getAbort()); + break; case Aborted: case Failed: case Finished: @@ -201,6 +204,7 @@ static Color getStateColor(final ScanState state) case Finished: return Color.DARKGREEN; case Paused: return Color.GRAY; case Running: return Color.GREEN; + case Scheduled: return Color.DARKBLUE; default: return Color.BLACK; } } diff --git a/app/scan/ui/src/main/resources/icons/clock.png b/app/scan/ui/src/main/resources/icons/clock.png new file mode 100644 index 0000000000000000000000000000000000000000..4375b83e147d5c2ab359aee6b48fc47c6cab2be4 GIT binary patch literal 500 zcmVa{dTA;etXcixd_XY2*Te zL`fPYNKPXuZL|_oh-UW!x=sqsk@$sAIWzCF%sd7FK)<}V+xLaSYqL3>Ob|k5i^b#h zUMCVxp4Vp(0000XM8Y^K2o*)qC>`|sgMMG5w4x|cL69&ege)f!08m2WqL`Ou#u#kC z7?Uu*42LNptAK>@Z8i(L`RI1@vMgZ?0H~v?=b_L;sRRI1i)B}aF%}hsj;f+`DpgSw zy|E`#QIvElg#vE3M(N+iu8c-$!0qPdj2#<|`qDm&MUu&US*_o_9{SHywK^J&I0T?+ z8rop)a;;S^VzCpO4V^iikL5BP!0Jo>VDO0J&;dNxY95~twN*7L2wP94vsf%SWruPp;r_{2zhlc3@0000null + * @param scheduled Datetime at which the scan should be executed (should be before the deadline, if provided) * @return ID that uniquely identifies the scan * @throws Exception on error */ - public long submitScan(String scan_name, String commands_as_xml, boolean queue, boolean pre_post, long timeout_secs, LocalDateTime deadline) throws Exception; + public long submitScan(String scan_name, String commands_as_xml, boolean queue, boolean pre_post, long timeout_secs, LocalDateTime deadline, LocalDateTime scheduled) throws Exception; /** Query server for scans * @return Info for each scan on the server, most recently submitted scan first diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java index 98abe4be19..f972372137 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java @@ -89,6 +89,7 @@ protected void doPost(final HttpServletRequest request, // Timeout or deadline? long timeout_secs = 0; LocalDateTime deadline = null; + LocalDateTime scheduled = null; String text = request.getParameter("timeout"); if (text != null) try @@ -118,6 +119,21 @@ protected void doPost(final HttpServletRequest request, throw new Exception("Cannot specify both timeout and deadline"); } + // Execute pre/post commands unless "?pre_post=false" + + text = request.getParameter("scheduled"); + if (text != null && !"0000-00-00 00:00:00".equals(text)) + { + try + { + scheduled = LocalDateTime.from(TimestampFormats.SECONDS_FORMAT.parse(text)); + } + catch (Exception ex) + { + throw new Exception("Invalid scheduled time '" + text + "'"); + } + } + // Read scan commands final String scan_commands = IOUtils.toString(request.getInputStream()); @@ -125,7 +141,15 @@ protected void doPost(final HttpServletRequest request, if (logger.isLoggable(Level.FINE)) logger.log(Level.FINE, "Scan '" + scan_name + "':\n" + scan_commands); - final long scan_id = scan_server.submitScan(scan_name, scan_commands, queue, pre_post, timeout_secs, deadline); + final long scan_id = scan_server.submitScan( + scan_name, + scan_commands, + queue, + pre_post, + timeout_secs, + deadline, + scheduled + ); // Return scan ID out.print(""); diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java index 0b64550218..b3fbeaf631 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java @@ -876,7 +876,6 @@ public void close() throws Exception { jython.close(); pre_scan.clear(); - pre_scan.clear(); implementations.clear(); post_scan.clear(); active_commands.clear(); diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java index 1bcfac54cc..0f8c33c323 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java @@ -17,8 +17,11 @@ import static org.csstudio.scan.server.ScanServerInstance.logger; +import java.time.Duration; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Timer; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -31,6 +34,7 @@ import org.csstudio.scan.server.internal.ExecutableScan.QueueState; import org.csstudio.scan.server.log.DataLogFactory; import org.phoebus.framework.jobs.NamedThreadFactory; +import org.phoebus.util.time.TimestampFormats; /** Engine that accepts {@link ExecutableScan}s, queuing them and executing * them in order @@ -46,6 +50,11 @@ public class ScanEngine * Scans that either Finished, Failed or were Aborted * are kept around for a little while. * + *

This queue is built up in a way where there is NEVER an "ExecutableScan" + * in the queue before a LoggedScan, EXCEPT when that "ExecutableScan" is scheduled to start at + * some time in the future, in which case it doesn't _really_ count as an ExecutableScan, though + * one still wants to manage creation / updating / deleting in the same way. + * *

The list is generally thread-safe (albeit slow when adding elements). * It is only locked to avoid starting a scan that's about to be moved up * for later execution @@ -54,6 +63,9 @@ public class ScanEngine */ final private List scan_queue = new CopyOnWriteArrayList<>(); + /** Timer for triggering scheduled tasks at their designated time */ + final private ScheduledExecutorService scheduled_scan_executor = Executors.newSingleThreadScheduledExecutor(); + /** Executor for executeQueuedScans() */ final private ExecutorService queue_executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("QueueHandler")); @@ -122,7 +134,16 @@ private void executeQueuedScans() final LoggedScan scan = scan_queue.get(i); // Track the last Queued scan, // which should be the next to execute - if (scan instanceof ExecutableScan) + if (scan instanceof ScheduledScan && !((ScheduledScan)scan).getExecutable()) { + // Scheduled scans which are not ready to be executed don't count as "actual" + // executable scans + // Scheduled scans are moved to the beginning of the queue whenever their scheduled + // time is reached, and are only then marked as being executable, so there can never + // be a race condition where an ExecutableScan triggers the queue, which then finds + // a ScheduledScan as first ExecutableScan, making it jump ahead of the ExecutableScan + // triggering the queue, or any other ScheduledScans. + } + else if (scan instanceof ExecutableScan) { @SuppressWarnings("resource") final ExecutableScan exe = (ExecutableScan) scan; @@ -247,6 +268,39 @@ public void submit(final ExecutableScan scan, final boolean queue) scan.submit(parallel_executor); } + /** Schedule a scan for execution + * @param scan The {@link ScheduledScan} + */ + public void schedule(final ScheduledScan scan) { + logger.log(Level.INFO, "Scheduling scan " + scan.getId() + " for " + scan.getScheduledTime().format(TimestampFormats.SECONDS_FORMAT)); + scan_queue.add(scan); + long delay = Duration.between(LocalDateTime.now(), scan.getScheduledTime()).toMillis(); + ScheduledScanTask task = new ScheduledScanTask(scan, this); + if (delay <= 0) { + task.run(); + } + else { + scheduled_scan_executor.schedule(task, delay, TimeUnit.MILLISECONDS); + } + } + + /** Resubmit a scheduled task for execution + * + */ + public void startScheduled(final ScheduledScan scan) { + logger.log(Level.INFO, "Starting scheduled scan " + scan.getId()); + synchronized (scan_queue) { + // remove scan to move it to the top + scan_queue.remove(scan); + + // mark scan as an "actual" executable scan + scan.setExecutable(); + + // resubmit as a normal executable scan + submit(scan, scan.getQueued()); + } + } + /** Check if there are any scans executing or waiting to be executed * @return Number of pending scans */ diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java index c98785fa27..0af8c30ce6 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java @@ -188,10 +188,17 @@ public long submitScan(final String scan_name, final boolean queue, final boolean pre_post, final long timeout_secs, - final LocalDateTime deadline) throws Exception + final LocalDateTime deadline, + final LocalDateTime scheduled) throws Exception { cullScans(); + if (deadline != null && scheduled != null) { + if (!scheduled.isBefore(deadline)) { + throw new Exception("Scan deadline should be _after_ scheduled time"); + } + } + try { // Parse received 'main' scan from XML final List commands = XMLCommandReader.readXMLString(commands_as_xml); @@ -237,8 +244,16 @@ public long submitScan(final String scan_name, final DeviceContext devices = new DeviceContext(); // Submit scan to engine for execution - final ExecutableScan scan = new ExecutableScan(scan_engine, jython, scan_name, devices, pre_impl, main_impl, post_impl, timeout_secs, deadline); - scan_engine.submit(scan, queue); + ExecutableScan scan; + if (scheduled == null) { + scan = new ExecutableScan(scan_engine, jython, scan_name, devices, pre_impl, main_impl, post_impl, timeout_secs, deadline); + scan_engine.submit(scan, queue); + } + else { + ScheduledScan _scan = new ScheduledScan(scheduled, queue, scan_engine, jython, scan_name, devices, pre_impl, main_impl, post_impl, timeout_secs, deadline); + scan = _scan; + scan_engine.schedule(_scan); + } logger.log(Level.CONFIG, "Submitted ID " + scan.getId() + " \"" + scan.getName() + "\""); return scan.getId(); } diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java new file mode 100644 index 0000000000..fc716871b3 --- /dev/null +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java @@ -0,0 +1,142 @@ +package org.csstudio.scan.server.internal; + +import org.csstudio.scan.info.ScanInfo; +import org.csstudio.scan.info.ScanState; +import org.csstudio.scan.server.ScanCommandImpl; +import org.csstudio.scan.server.device.DeviceContext; +import org.phoebus.util.time.TimestampFormats; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.List; +import java.util.TimeZone; + +public class ScheduledScan extends ExecutableScan { + + /** + * Time at which this scan should be queued / executed. + * */ + private final LocalDateTime when; + + /** + * Used to store the point in time in which this scan was created (for showing progress) + * */ + private final LocalDateTime created; + + /** + * Whether the scan should be queued when its time has come + * */ + private final boolean queued; + + /** + * Whether the scan is currently executable (must be AFTER 'when' has passed) + */ + private boolean executable = false; + + /** + * Initialize + * + * @param when Time at which this scan should be queued / executed + * @param queued Whether this scan should be queued when its time has come + * @param engine {@link ScanEngine} that executes this scan + * @param jython Jython support + * @param name User-provided name for this scan + * @param devices {@link DeviceContext} to use for scan + * @param pre_scan Commands to execute before the 'main' section of the scan + * @param implementations Commands to execute in this scan + * @param post_scan Commands to execute before the 'main' section of the scan + * @param timeout_secs Timeout in seconds or 0 + * @param deadline Deadline by which scan will be aborted or null + * @throws Exception on error (cannot access log, ...) + */ + public ScheduledScan( + LocalDateTime when, + boolean queued, + ScanEngine engine, + JythonSupport jython, + String name, + DeviceContext devices, + List> pre_scan, + List> implementations, + List> post_scan, + long timeout_secs, + LocalDateTime deadline) throws Exception { + super(engine, jython, name, devices, pre_scan, implementations, post_scan, timeout_secs, deadline); + this.when = when; + this.queued = queued; + this.created = LocalDateTime.now(); + } + + /** Whether this scan is ready to actually be queued / executed. Basically, if the scheduled time has not passed, + * a ScheduledScan does not count as an "actual" ExecutableScan. If the time has passed, it does count as one. + * + * @return True if the scheduled time has passed + */ + public boolean getExecutable() { + return executable; + } + + public void setExecutable() { + assert LocalDateTime.now().isAfter(when); + executable = true; + } + + /** QueueState for scheduled scans must be handled differently: + * If the scan is not meant to be executed yet, it will ALWAYS be Queued, + * so that it can be moved around (mainly to the top of the queue when it is + * meant to be executed, but users may want to move it around too for some reason) + */ + @Override + QueueState getQueueState() { + if (!getExecutable()) + return QueueState.Queued; + return super.getQueueState(); + } + + @Override + public ScanState getScanState() { + if (!getExecutable()) { + return ScanState.Scheduled; + } + return super.getScanState(); + } + + @Override + public ScanInfo getScanInfo() { + ScanInfo base_info = super.getScanInfo(); + if (!getExecutable()) { + // override finish time / current command if the scan is still scheduled + long total_duration = Duration.between(created, getScheduledTime()).toMillis(); + return new ScanInfo( + this, + base_info.getState(), + base_info.getError(), + base_info.getRuntimeMillisecs(), + getScheduledTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + total_duration - Duration.between(LocalDateTime.now(), getScheduledTime()).toMillis(), + total_duration, + base_info.getCurrentAddress(), + "Waiting until " + getScheduledTime().format(TimestampFormats.SECONDS_FORMAT) + "..." + ); + } + return base_info; + } + + + @Override + void doAbort(ScanState previous) { + // first set to executable, then abort it as usual + setExecutable(); + super.doAbort(previous); + } + + LocalDateTime getScheduledTime() { + return when; + } + + boolean getQueued() { + return queued; + } +} diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScanTask.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScanTask.java new file mode 100644 index 0000000000..4e23f6df90 --- /dev/null +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScanTask.java @@ -0,0 +1,28 @@ +package org.csstudio.scan.server.internal; + +import java.util.TimerTask; + +public class ScheduledScanTask extends TimerTask { + final ScheduledScan scan; + final ScanEngine engine; + + public ScheduledScanTask(ScheduledScan scan, ScanEngine engine) { + this.scan = scan; + this.engine = engine; + } + + /** + * The action to be performed by this timer task. + */ + @Override + public void run() { + try { + // scan may have been aborted + if (!scan.getScanState().isDone()) { + engine.startScheduled(scan); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/services/scan-server/src/main/resources/examples/scan.db b/services/scan-server/src/main/resources/examples/scan.db index 94f3fc6cb9..bd1af90184 100644 --- a/services/scan-server/src/main/resources/examples/scan.db +++ b/services/scan-server/src/main/resources/examples/scan.db @@ -33,6 +33,8 @@ record(mbbi, "$(P)State") field(FVST, "Finished") field(SXVL, "6") field(SXST, "Logged") + field(SVVL, "7") + field(SVST, "Scheduled") } record(waveform, "$(P)Status") diff --git a/services/scan-server/src/main/resources/webroot/index.html b/services/scan-server/src/main/resources/webroot/index.html index 387c5cf6ee..5a0d68d728 100644 --- a/services/scan-server/src/main/resources/webroot/index.html +++ b/services/scan-server/src/main/resources/webroot/index.html @@ -32,6 +32,7 @@

Submit Scan

/scan/{name-of-new-scan} to queue or /scan/{name-of-new-scan}?queue=false for immediate execution.
/scan/{name-of-new-scan}?pre_post=false to suppress pre- and post-scan commands.
+ /scan/{name-of-new-scan}?schedule=2025-11-04 13:36:46 to schedule the scan to run at a specific point in time.

The runtime of a scan can be limited by submitting it with either a /scan/{name-of-new-scan}?timeout=300 timeout in seconds
From 9e1061ce02a21f5be5463fafe11ce012c690eb28 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 6 Nov 2025 14:36:13 +0100 Subject: [PATCH 04/10] fix dependencies --- app/display/runtime/pom.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/display/runtime/pom.xml b/app/display/runtime/pom.xml index 01233677b1..47854b45b4 100644 --- a/app/display/runtime/pom.xml +++ b/app/display/runtime/pom.xml @@ -60,17 +60,17 @@ app-display-actions 5.0.3-SNAPSHOT - - org.phoebus - app-scan-client - 5.0.2 - compile - - - org.phoebus - app-scan-ui - 5.0.2 - compile - + + org.phoebus + app-scan-client + 5.0.3-SNAPSHOT + compile + + + org.phoebus + app-scan-ui + 5.0.3-SNAPSHOT + compile + From edee9f750cd90170a78eb20c78dd3039df78cca9 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 6 Nov 2025 15:46:05 +0100 Subject: [PATCH 05/10] add scheduled scans to editor context menu --- .../src/main/java/org/csstudio/scan/ui/Messages.java | 1 + .../java/org/csstudio/scan/ui/editor/ScanEditor.java | 12 ++++++++++-- .../csstudio/scan/ui/editor/ScheduledScanDialog.java | 8 +++++++- .../org/csstudio/scan/ui/messages.properties | 1 + 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java index 058dd8509a..fb375e9b9a 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/Messages.java @@ -28,6 +28,7 @@ public class Messages public static String scan_resume; public static String scan_resume_all; public static String scan_schedule; + public static String scan_schedule_unqueued; public static String scan_simulate; public static String scan_submit; public static String scan_submit_unqueued; diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java index 9c548f1c04..14dbc48bea 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScanEditor.java @@ -293,6 +293,14 @@ private void createContextMenu() ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); delete.setOnAction(event -> scan_tree.cutToClipboard()); + final MenuItem schedule = new MenuItem(Messages.scan_schedule, + ImageCache.getImageView(ScanSystem.class, "/icons/clock.png")); + schedule.setOnAction(event -> schedule(true)); + + final MenuItem schedule_unqueued = new MenuItem(Messages.scan_schedule_unqueued, + ImageCache.getImageView(ScanSystem.class, "/icons/clock.png")); + schedule_unqueued.setOnAction(event -> schedule(false)); + final MenuItem simulate = new MenuItem(Messages.scan_simulate, ImageCache.getImageView(ScanSystem.class, "/icons/simulate.png")); simulate.setOnAction(event -> submitOrSimulate(null)); @@ -311,7 +319,7 @@ private void createContextMenu() final ContextMenu menu = new ContextMenu(copy, paste, delete, new SeparatorMenuItem(), - simulate, submit, submit_unqueued, + schedule, schedule_unqueued, simulate, submit, submit_unqueued, new SeparatorMenuItem(), open_monitor); setContextMenu(menu); @@ -325,7 +333,7 @@ private void schedule(final Boolean queued) { throw new RuntimeException(e); } final ScanClient scan_client = new ScanClient(Preferences.host, Preferences.port); - ScheduledScanDialog dialog = new ScheduledScanDialog(scan_name, scan_client, xml_commands); + ScheduledScanDialog dialog = new ScheduledScanDialog(scan_name, scan_client, xml_commands, queued); DialogHelper.positionDialog(dialog, this, 100, 100); Optional scan_id = dialog.showAndWait(); scan_id.ifPresent(this::attachScan); diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java index 847c322722..396c43cf9b 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java @@ -13,11 +13,17 @@ public class ScheduledScanDialog extends Dialog { final String scan_name; final ScanClient scan_client; final String script_xml; + final boolean queued; public ScheduledScanDialog(String scan_name, ScanClient scan_client, String script_xml) { + this(scan_name, scan_client, script_xml, true); + } + + public ScheduledScanDialog(String scan_name, ScanClient scan_client, String script_xml, boolean queued) { this.scan_name = scan_name; this.scan_client = scan_client; this.script_xml = script_xml; + this.queued = queued; setTitle("Schedule " + scan_name); getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); @@ -33,7 +39,7 @@ public ScheduledScanDialog(String scan_name, ScanClient scan_client, String scri return scan_client.submitScan( scan_name, script_xml, - true, + queued, scheduled ); } diff --git a/app/scan/ui/src/main/resources/org/csstudio/scan/ui/messages.properties b/app/scan/ui/src/main/resources/org/csstudio/scan/ui/messages.properties index a2bc7cdd02..26ceafb11f 100644 --- a/app/scan/ui/src/main/resources/org/csstudio/scan/ui/messages.properties +++ b/app/scan/ui/src/main/resources/org/csstudio/scan/ui/messages.properties @@ -12,6 +12,7 @@ scan_remove=Remove this scan scan_resume=Resume execution scan_resume_all=Resume all paused scans scan_schedule=Schedule scan +scan_schedule_unqueued=Schedule scan (not queued) scan_simulate=Simulate scan scan_submit=Submit scan scan_submit_unqueued=Submit scan (not queued) From e4c19e899846897a99d23b93952c9578dd01b689 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 6 Nov 2025 15:48:54 +0100 Subject: [PATCH 06/10] update html api reference --- services/scan-server/src/main/resources/webroot/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/services/scan-server/src/main/resources/webroot/index.html b/services/scan-server/src/main/resources/webroot/index.html index 5a0d68d728..dfdb8d60e2 100644 --- a/services/scan-server/src/main/resources/webroot/index.html +++ b/services/scan-server/src/main/resources/webroot/index.html @@ -230,6 +230,7 @@

Versions

+
5.0.3
Support 'schedule' parameter
4.7.0
Support 'timeout' and 'deadline' parameters
4.6.1
Wait supports comparisons for strings
4.6.0
Add 'readback_value' to Set command
From 0d5b88ab9715b6eee12786b0ed6b792c6fe7f468 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 7 Nov 2025 09:45:47 +0100 Subject: [PATCH 07/10] replace LocalDateTime -> Instant --- .../org/csstudio/scan/client/ScanClient.java | 5 ++-- .../scan/ui/editor/ScheduledScanDialog.java | 5 ++-- .../org/csstudio/scan/server/ScanServer.java | 4 +-- .../scan/server/httpd/ScanServlet.java | 10 ++++---- .../scan/server/internal/ExecutableScan.java | 7 +++--- .../scan/server/internal/ScanEngine.java | 7 +++--- .../scan/server/internal/ScanServerImpl.java | 5 ++-- .../scan/server/internal/ScheduledScan.java | 25 ++++++++----------- 8 files changed, 30 insertions(+), 38 deletions(-) diff --git a/app/scan/client/src/main/java/org/csstudio/scan/client/ScanClient.java b/app/scan/client/src/main/java/org/csstudio/scan/client/ScanClient.java index 2d107d75c1..369a3bad98 100644 --- a/app/scan/client/src/main/java/org/csstudio/scan/client/ScanClient.java +++ b/app/scan/client/src/main/java/org/csstudio/scan/client/ScanClient.java @@ -23,7 +23,6 @@ import java.net.URI; import java.net.URL; import java.time.Instant; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -412,7 +411,7 @@ public long submitScan( final String name, final String xml_commands, final boolean queue, - LocalDateTime scheduled + Instant scheduled ) throws Exception { List query = new ArrayList<>(); @@ -420,7 +419,7 @@ public long submitScan( query.add("queue=false"); } if (scheduled != null) { - query.add("scheduled=" + scheduled.format(TimestampFormats.SECONDS_FORMAT)); + query.add("scheduled=" + TimestampFormats.SECONDS_FORMAT.format(scheduled)); } final HttpURLConnection connection = connect("/scan/" + name, String.join("&", query), long_timeout); diff --git a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java index 396c43cf9b..2e41541ec2 100644 --- a/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java +++ b/app/scan/ui/src/main/java/org/csstudio/scan/ui/editor/ScheduledScanDialog.java @@ -5,7 +5,7 @@ import org.csstudio.scan.client.ScanClient; import org.phoebus.ui.time.DateTimePane; -import java.time.LocalDateTime; +import java.time.Instant; import java.time.ZoneId; @@ -34,13 +34,12 @@ public ScheduledScanDialog(String scan_name, ScanClient scan_client, String scri setResultConverter(button -> { if (button == ButtonType.OK) { - LocalDateTime scheduled = LocalDateTime.ofInstant(datetime.getInstant(), ZoneId.systemDefault()); try { return scan_client.submitScan( scan_name, script_xml, queued, - scheduled + datetime.getInstant() ); } catch (Exception e) { diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/ScanServer.java b/services/scan-server/src/main/java/org/csstudio/scan/server/ScanServer.java index b556bddeaf..c5f13421a7 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/ScanServer.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/ScanServer.java @@ -15,7 +15,7 @@ ******************************************************************************/ package org.csstudio.scan.server; -import java.time.LocalDateTime; +import java.time.Instant; import java.util.List; import org.csstudio.scan.data.ScanData; @@ -63,7 +63,7 @@ public interface ScanServer * @return ID that uniquely identifies the scan * @throws Exception on error */ - public long submitScan(String scan_name, String commands_as_xml, boolean queue, boolean pre_post, long timeout_secs, LocalDateTime deadline, LocalDateTime scheduled) throws Exception; + public long submitScan(String scan_name, String commands_as_xml, boolean queue, boolean pre_post, long timeout_secs, Instant deadline, Instant scheduled) throws Exception; /** Query server for scans * @return Info for each scan on the server, most recently submitted scan first diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java index f972372137..e0631aa6dc 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/httpd/ScanServlet.java @@ -12,7 +12,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.time.LocalDateTime; +import java.time.Instant; import java.util.List; import java.util.logging.Level; @@ -88,8 +88,8 @@ protected void doPost(final HttpServletRequest request, { // Timeout or deadline? long timeout_secs = 0; - LocalDateTime deadline = null; - LocalDateTime scheduled = null; + Instant deadline = null; + Instant scheduled = null; String text = request.getParameter("timeout"); if (text != null) try @@ -108,7 +108,7 @@ protected void doPost(final HttpServletRequest request, { try { - deadline = LocalDateTime.from(TimestampFormats.SECONDS_FORMAT.parse(text)); + deadline = Instant.from(TimestampFormats.SECONDS_FORMAT.parse(text)); } catch (Exception ex) { @@ -126,7 +126,7 @@ protected void doPost(final HttpServletRequest request, { try { - scheduled = LocalDateTime.from(TimestampFormats.SECONDS_FORMAT.parse(text)); + scheduled = Instant.from(TimestampFormats.SECONDS_FORMAT.parse(text)); } catch (Exception ex) { diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java index b3fbeaf631..8dd0709537 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ExecutableScan.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.time.Instant; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Deque; import java.util.List; @@ -104,7 +103,7 @@ enum QueueState final long timeout_secs; /** Execution deadline by which scan will be aborted or null */ - final LocalDateTime deadline; + final Instant deadline; /** Log each device access, or require specific log command? */ private volatile boolean automatic_log_mode = false; @@ -173,7 +172,7 @@ public ExecutableScan(final ScanEngine engine, final JythonSupport jython, final final List> implementations, final List> post_scan, final long timeout_secs, - final LocalDateTime deadline) throws Exception + final Instant deadline) throws Exception { super(DataLogFactory.createDataLog(name)); this.engine = engine; @@ -480,7 +479,7 @@ private void executeWithDeadline() throws Exception } else if (deadline != null) { - final long seconds = Duration.between(LocalDateTime.now(), deadline).getSeconds(); + final long seconds = Duration.between(Instant.now(), deadline).getSeconds(); if (seconds > 0) { timer = engine.deadline_timer.schedule(this::abortAtDeadline, seconds, TimeUnit.SECONDS); diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java index 0f8c33c323..e50c15c0ce 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanEngine.java @@ -18,10 +18,9 @@ import static org.csstudio.scan.server.ScanServerInstance.logger; import java.time.Duration; -import java.time.LocalDateTime; +import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Timer; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -272,9 +271,9 @@ public void submit(final ExecutableScan scan, final boolean queue) * @param scan The {@link ScheduledScan} */ public void schedule(final ScheduledScan scan) { - logger.log(Level.INFO, "Scheduling scan " + scan.getId() + " for " + scan.getScheduledTime().format(TimestampFormats.SECONDS_FORMAT)); + logger.log(Level.INFO, "Scheduling scan " + scan.getId() + " for " + TimestampFormats.SECONDS_FORMAT.format(scan.getScheduledTime())); scan_queue.add(scan); - long delay = Duration.between(LocalDateTime.now(), scan.getScheduledTime()).toMillis(); + long delay = Duration.between(Instant.now(), scan.getScheduledTime()).toMillis(); ScheduledScanTask task = new ScheduledScanTask(scan, this); if (delay <= 0) { task.run(); diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java index 0af8c30ce6..a0ce029245 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScanServerImpl.java @@ -20,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.time.Instant; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -188,8 +187,8 @@ public long submitScan(final String scan_name, final boolean queue, final boolean pre_post, final long timeout_secs, - final LocalDateTime deadline, - final LocalDateTime scheduled) throws Exception + final Instant deadline, + final Instant scheduled) throws Exception { cullScans(); diff --git a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java index fc716871b3..2f9a2af2e2 100644 --- a/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java +++ b/services/scan-server/src/main/java/org/csstudio/scan/server/internal/ScheduledScan.java @@ -6,24 +6,21 @@ import org.csstudio.scan.server.device.DeviceContext; import org.phoebus.util.time.TimestampFormats; +import java.time.Instant; import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; import java.util.List; -import java.util.TimeZone; public class ScheduledScan extends ExecutableScan { /** * Time at which this scan should be queued / executed. * */ - private final LocalDateTime when; + private final Instant when; /** * Used to store the point in time in which this scan was created (for showing progress) * */ - private final LocalDateTime created; + private final Instant created; /** * Whether the scan should be queued when its time has come @@ -52,7 +49,7 @@ public class ScheduledScan extends ExecutableScan { * @throws Exception on error (cannot access log, ...) */ public ScheduledScan( - LocalDateTime when, + Instant when, boolean queued, ScanEngine engine, JythonSupport jython, @@ -62,11 +59,11 @@ public ScheduledScan( List> implementations, List> post_scan, long timeout_secs, - LocalDateTime deadline) throws Exception { + Instant deadline) throws Exception { super(engine, jython, name, devices, pre_scan, implementations, post_scan, timeout_secs, deadline); this.when = when; this.queued = queued; - this.created = LocalDateTime.now(); + this.created = Instant.now(); } /** Whether this scan is ready to actually be queued / executed. Basically, if the scheduled time has not passed, @@ -79,7 +76,7 @@ public boolean getExecutable() { } public void setExecutable() { - assert LocalDateTime.now().isAfter(when); + assert Instant.now().isAfter(when); executable = true; } @@ -114,11 +111,11 @@ public ScanInfo getScanInfo() { base_info.getState(), base_info.getError(), base_info.getRuntimeMillisecs(), - getScheduledTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), - total_duration - Duration.between(LocalDateTime.now(), getScheduledTime()).toMillis(), + getScheduledTime().toEpochMilli(), + total_duration - Duration.between(Instant.now(), getScheduledTime()).toMillis(), total_duration, base_info.getCurrentAddress(), - "Waiting until " + getScheduledTime().format(TimestampFormats.SECONDS_FORMAT) + "..." + "Waiting until " + TimestampFormats.SECONDS_FORMAT.format(getScheduledTime()) + "..." ); } return base_info; @@ -132,7 +129,7 @@ void doAbort(ScanState previous) { super.doAbort(previous); } - LocalDateTime getScheduledTime() { + Instant getScheduledTime() { return when; } From 2f5d67d945d06763320057f4a3dacd9e3a71f079 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 7 Nov 2025 10:24:08 +0100 Subject: [PATCH 08/10] remove ActionButtonWidgetRuntime and display runtime / scan server dependencies --- .../internal/ActionButtonWidgetRuntime.java | 66 ------------------- .../runtime/internal/BaseWidgetRuntimes.java | 1 - 2 files changed, 67 deletions(-) delete mode 100644 app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java deleted file mode 100644 index 7e32cef008..0000000000 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/ActionButtonWidgetRuntime.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.csstudio.display.builder.runtime.internal; - -import javafx.scene.Node; -import org.csstudio.display.actions.WritePVAction; -import org.csstudio.display.builder.model.widgets.ActionButtonWidget; -import org.csstudio.display.builder.representation.javafx.widgets.JFXBaseRepresentation; -import org.csstudio.display.builder.runtime.RuntimeAction; -import org.csstudio.display.builder.runtime.WidgetRuntime; -import org.csstudio.scan.client.ScanInfoModel; -import org.csstudio.scan.ui.editor.ScheduledScanDialog; -import org.phoebus.ui.dialog.DialogHelper; - -import java.util.*; - - -public class ActionButtonWidgetRuntime extends WidgetRuntime { - - private class ScheduledTaskDialogAction extends RuntimeAction { - private final String script_xml; - - public ScheduledTaskDialogAction(String scriptXml) { - super("Schedule Task", "/icons/clock.png"); - script_xml = scriptXml; - } - - @Override - public void run() { - try { - ScheduledScanDialog dialog = new ScheduledScanDialog( - "Scheduled Task", - ScanInfoModel.getInstance().getScanClient(), - script_xml - ); - final Node node = JFXBaseRepresentation.getJFXNode(widget); - DialogHelper.positionDialog(dialog, node, 0, 0); - dialog.showAndWait(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - @Override - public Collection getRuntimeActions() { - List commands = widget.propActions().getValue().getActions() - .stream() - .filter(action -> action instanceof WritePVAction) - .map(action -> { - WritePVAction writePVAction = (WritePVAction) action; - String pvName = writePVAction.formatPv(widget); - String value = writePVAction.formatValue(widget); - return ( - "" + - "" + pvName + "" + - "" + value + "" + - "" - ); - }).toList(); - - if (commands.isEmpty()) { - return List.of(); - } - - return List.of(new ScheduledTaskDialogAction("" + String.join("", commands) + "")); - } -} diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java index 8056cb5dcb..1da5e87196 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java @@ -40,7 +40,6 @@ public Map>> getWidgetRuntimeFa { return Map.ofEntries( entry(DisplayModel.WIDGET_TYPE, () -> new DisplayRuntime()), - entry(ActionButtonWidget.WIDGET_DESCRIPTOR.getType(), () -> new ActionButtonWidgetRuntime()), entry(ArrayWidget.WIDGET_DESCRIPTOR.getType(), () -> new ArrayWidgetRuntime()), entry(EmbeddedDisplayWidget.WIDGET_DESCRIPTOR.getType(), () -> new EmbeddedDisplayRuntime()), entry(GroupWidget.WIDGET_DESCRIPTOR.getType(), () -> new GroupWidgetRuntime()), From a4cec759d781c589a18dd698e44ecbf91e77ff52 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 7 Nov 2025 10:28:32 +0100 Subject: [PATCH 09/10] remove more dependencies --- app/display/runtime/pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/display/runtime/pom.xml b/app/display/runtime/pom.xml index 47854b45b4..3022ea40b7 100644 --- a/app/display/runtime/pom.xml +++ b/app/display/runtime/pom.xml @@ -60,17 +60,5 @@ app-display-actions 5.0.3-SNAPSHOT - - org.phoebus - app-scan-client - 5.0.3-SNAPSHOT - compile - - - org.phoebus - app-scan-ui - 5.0.3-SNAPSHOT - compile - From a82fe54523ec28922749259f8604cc2c5053c820 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Tue, 18 Nov 2025 11:23:34 +0100 Subject: [PATCH 10/10] Remove changes from app/display --- .../display/actions/WritePVAction.java | 32 ------------------ .../actionhandlers/WritePVActionHandler.java | 19 +++++++++-- .../runtime/internal/BaseWidgetRuntimes.java | 1 - .../src/main/resources/icons/clock.png | Bin 500 -> 0 bytes 4 files changed, 17 insertions(+), 35 deletions(-) delete mode 100644 app/display/runtime/src/main/resources/icons/clock.png diff --git a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java index 1f189d64c6..104a82a767 100644 --- a/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java +++ b/app/display/actions/src/main/java/org/csstudio/display/actions/WritePVAction.java @@ -5,14 +5,11 @@ package org.csstudio.display.actions; import javafx.scene.image.Image; -import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.persist.ModelReader; import org.csstudio.display.builder.model.persist.ModelWriter; import org.csstudio.display.builder.model.persist.XMLTags; import org.csstudio.display.builder.model.properties.ActionInfoBase; import org.csstudio.display.builder.representation.javafx.actionsdialog.ActionsDialog; -import org.phoebus.framework.macros.MacroHandler; -import org.phoebus.framework.macros.MacroValueProvider; import org.phoebus.framework.persistence.XMLUtil; import org.phoebus.ui.javafx.ImageCache; import org.w3c.dom.Element; @@ -110,33 +107,4 @@ public void setPv(String pv) { public void setValue(String value) { this.value = value; } - - public String formatPv(Widget widget) { - final MacroValueProvider macros = widget.getMacrosOrProperties(); - String pvName = getPV(); - try - { - pvName = MacroHandler.replace(macros, pvName); - } - catch (Exception ignore) - { - // NOP - } - return pvName; - } - - public String formatValue(Widget widget) { - final MacroValueProvider macros = widget.getMacrosOrProperties(); - value = getValue(); - - try - { - value = MacroHandler.replace(macros, value); - } - catch (Exception ignore) - { - // NOP - } - return value; - } } diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java index 18e7104410..2530f0f4ca 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/actionhandlers/WritePVActionHandler.java @@ -25,8 +25,23 @@ public void handleAction(Widget sourceWidget, ActionInfo pluggableActionInfo) { // System.out.println(action.getDescription() + ": Set " + action.getPV() + " = " + action.getValue()); final MacroValueProvider macros = sourceWidget.getMacrosOrProperties(); WritePVAction writePVAction = (WritePVAction)pluggableActionInfo; - String pvName = writePVAction.formatPv(sourceWidget); - String value = writePVAction.formatValue(sourceWidget); + String pvName = writePVAction.getPV(), value = writePVAction.getValue(); + try + { + pvName = MacroHandler.replace(macros, pvName); + } + catch (Exception ignore) + { + // NOP + } + try + { + value = MacroHandler.replace(macros, value); + } + catch (Exception ignore) + { + // NOP + } try { runtime.writePV(pvName,value); diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java index 1da5e87196..5cd511ad70 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/internal/BaseWidgetRuntimes.java @@ -13,7 +13,6 @@ import org.csstudio.display.builder.model.DisplayModel; import org.csstudio.display.builder.model.Widget; -import org.csstudio.display.builder.model.widgets.ActionButtonWidget; import org.csstudio.display.builder.model.widgets.ArrayWidget; import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; import org.csstudio.display.builder.model.widgets.GroupWidget; diff --git a/app/display/runtime/src/main/resources/icons/clock.png b/app/display/runtime/src/main/resources/icons/clock.png deleted file mode 100644 index 4375b83e147d5c2ab359aee6b48fc47c6cab2be4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmVa{dTA;etXcixd_XY2*Te zL`fPYNKPXuZL|_oh-UW!x=sqsk@$sAIWzCF%sd7FK)<}V+xLaSYqL3>Ob|k5i^b#h zUMCVxp4Vp(0000XM8Y^K2o*)qC>`|sgMMG5w4x|cL69&ege)f!08m2WqL`Ou#u#kC z7?Uu*42LNptAK>@Z8i(L`RI1@vMgZ?0H~v?=b_L;sRRI1i)B}aF%}hsj;f+`DpgSw zy|E`#QIvElg#vE3M(N+iu8c-$!0qPdj2#<|`qDm&MUu&US*_o_9{SHywK^J&I0T?+ z8rop)a;;S^VzCpO4V^iikL5BP!0Jo>VDO0J&;dNxY95~twN*7L2wP94vsf%SWruPp;r_{2zhlc3@0000