From 056376a00fb384db7b0b89a61d1920b737506ee2 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Tue, 20 Oct 2026 10:57:55 +0100 Subject: [PATCH 1/2] Detect disconnections in archive appliance raw and optimizedWithLastSample data --- .../ApplianceOptimizedValueIterator.java | 68 ++++++++++++++++--- .../appliance/ApplianceValueIterator.java | 54 ++++++++++++--- 2 files changed, 106 insertions(+), 16 deletions(-) diff --git a/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceOptimizedValueIterator.java b/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceOptimizedValueIterator.java index 2ffc21c533..ad49a0c31b 100644 --- a/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceOptimizedValueIterator.java +++ b/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceOptimizedValueIterator.java @@ -7,13 +7,7 @@ import org.epics.archiverappliance.retrieval.client.DataRetrieval; import org.epics.archiverappliance.retrieval.client.EpicsMessage; import org.epics.archiverappliance.retrieval.client.GenMsgIterator; -import org.epics.vtype.Alarm; -import org.epics.vtype.AlarmStatus; -import org.epics.vtype.Display; -import org.epics.vtype.Time; -import org.epics.vtype.VNumber; -import org.epics.vtype.VStatistics; -import org.epics.vtype.VType; +import org.epics.vtype.*; import org.phoebus.pv.TimeHelper; import edu.stanford.slac.archiverappliance.PB.EPICSEvent.PayloadInfo; @@ -33,6 +27,8 @@ public class ApplianceOptimizedValueIterator extends ApplianceValueIterator { private final int requestedPoints; private final boolean useStatistics; + private boolean firstDisconnnect; + private EpicsMessage previousMessage; /** * Constructor that fetches data from appliance archive reader. @@ -55,6 +51,8 @@ public ApplianceOptimizedValueIterator(ApplianceArchiveReader reader, String nam super(reader, name, start, end); this.requestedPoints = points; this.useStatistics = useStatistics; + this.firstDisconnnect = false; + this.previousMessage = null; this.display = determineDisplay(reader, name, end); fetchData(); } @@ -130,7 +128,22 @@ public VType next() { if (closed) { return null; } - message = mainIterator.next(); + if (!reuseMessage) + message = mainIterator.next(); + else { + message = savedMessage; + reuseMessage = false; + } + + // Check if a disconnect event was recorded and return a special + // value if so. + VType check = checkDisconnect(message); + if (check != null) + return check; + + savedMessage = null; + firstDisconnnect = false; + previousMessage = message; } PayloadType type = mainStream.getPayLoadInfo().getType(); if (type == PayloadType.WAVEFORM_DOUBLE) { @@ -159,4 +172,43 @@ public VType next() { return super.extractData(message); } } + + /** + * Method to check the EpicsMessage for fieldValues containing the + * 'cnxlostepsecs' and 'cnxregainedepsecs' signifying a disconnect and reconnect + * of the PV (for raw data) or 'connectionChange' (for optimized data). + * Method extracts the time of the disconnect and returns an + * ArchiveVString with the value 'Disconnect' + * + * @param message Current EpicsMessage to check + * @return a VType that can be inserted to indicate a disconnect + */ + @Override + VType checkDisconnect(EpicsMessage message) { + // Call parent method to test for "cnxlostepsecs" field in case of raw data + VType check = super.checkDisconnect(message); + if (check != null) + return check; + + // In the OptimizedWithLastSample PP we get a connectionChange and then the following bins + // will have 0 events (count) in them. + Boolean disconnectCondition = Boolean.parseBoolean(message.getFieldValues().get("connectionChange")) + && message.getNumberAt(4).intValue() == 0; + // In the Optimized PP we get a connectionChange but the following bins (which + // technically have no events) will be a repeat of the last bin to have + // events. Cannot currently detect disconnect times in the Optimized + // request so disconnectCondition will always evaluate as false. + + if (disconnectCondition && savedMessage == null) { + if (!firstDisconnnect) { + firstDisconnnect = true; + return VString.of("Disconnect in bin", Alarm.disconnected(), + Time.of(TimestampHelper.fromSQLTimestamp(message.getTimestamp()))); + } else { + return VString.of("", Alarm.disconnected(), + Time.of(TimestampHelper.fromSQLTimestamp(message.getTimestamp()))); + } + } + return null; + } } diff --git a/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceValueIterator.java b/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceValueIterator.java index 708e469aab..3bb4c00973 100644 --- a/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceValueIterator.java +++ b/app/trends/archive-reader/src/main/java/org/phoebus/archive/reader/appliance/ApplianceValueIterator.java @@ -2,14 +2,9 @@ import java.io.IOException; import java.time.Instant; -import java.util.ArrayList; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.Iterator; -import java.util.List; -import java.util.HashMap; -import java.util.Map; -import java.util.Comparator; import org.epics.archiverappliance.retrieval.client.DataRetrieval; import org.epics.archiverappliance.retrieval.client.EpicsMessage; @@ -72,6 +67,10 @@ public abstract class ApplianceValueIterator implements ValueIterator { private final Object lock = new Object(); + protected EpicsMessage savedMessage; + protected boolean reuseMessage; + + /** * Constructs a new ApplianceValueIterator. * @@ -85,6 +84,8 @@ protected ApplianceValueIterator(ApplianceArchiveReader reader, String name, Ins this.name = name; this.start = start; this.end = end; + this.reuseMessage = false; + this.savedMessage = null; } /** @@ -130,6 +131,10 @@ protected void fetchDataInternal(String pvName) throws ArchiverApplianceExceptio public boolean hasNext() { if (closed || mainIterator == null) return false; + else if (reuseMessage) + // Disconnect is at the end of the iterator so + // still need to iterate one last time + return true; return mainIterator.hasNext(); } @@ -144,7 +149,17 @@ public VType next() { synchronized (this) { if (closed) return null; - message = mainIterator.next(); + if (!reuseMessage) + message = mainIterator.next(); + else { + message = savedMessage; + reuseMessage = false; + } + + VType check = checkDisconnect(message); + if (check != null) + return check; + savedMessage = null; } return extractData(message); } @@ -232,6 +247,29 @@ protected VType extractData(EpicsMessage dataMessage) { throw new UnsupportedOperationException("PV type " + type + " is not supported."); } + /** + * Method to check the EpicsMessage for fieldValues containing the + * 'cnxlostepsecs' and 'cnxregainedepsecs' signifying a disconnect and reconnect + * of the PV. Method extracts the time of the disconnect and returns an + * ArchiveVString with the value 'Disconnect' + * + * @param message Current EpicsMessage to check + * @return a VType that can be inserted to indicate a disconnect + */ + VType checkDisconnect(EpicsMessage message) { + Set messageKeys = message.getFieldValues().keySet(); + Boolean disconnectCondition = messageKeys.contains("cnxlostepsecs") + && messageKeys.contains("cnxregainedepsecs") + && !messageKeys.contains("startup"); + if (disconnectCondition && savedMessage == null) { + long lostT = Long.parseLong(message.getFieldValues().get("cnxlostepsecs")); + reuseMessage = true; + savedMessage = message; + return VString.of("Disconnect", Alarm.disconnected(), Time.of(Instant.ofEpochSecond(lostT))); + } + return null; + } + /** * Extracts the descriptor for the value field so it can be reused on each iteration. * @@ -307,7 +345,7 @@ protected Display getDisplay(PayloadInfo info) { } /** - * Extract the labels from the given payloadinfo when processing Enum Values. + * Extract the labels from the given payloadinfo when processing Enum Values. * EnumLabels list empty if payloadinfo from request without "fetchLatestMetadata" set to true * * @param info the info to extract the labels From ad482e6af9489d6fd8fcb62cfb41ac5cb0458219 Mon Sep 17 00:00:00 2001 From: Rebecca Williams Date: Tue, 20 Oct 2026 10:59:44 +0100 Subject: [PATCH 2/2] Display disconnects in the data browser with a dashed line --- .../javafx/rtplot/internal/TracePainter.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/TracePainter.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/TracePainter.java index e735d68fb1..e08690b1cd 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/TracePainter.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/TracePainter.java @@ -306,6 +306,10 @@ private void drawValueStaircase(final Graphics2D gc, if (Double.isNaN(value)) { flushPolyLine(gc, poly_x, poly_y, line_width); + if (last_y != -1) { + highlightDiscontinuity(gc, x_transform, item); + gc.setStroke(createStroke(line_width, line_style)); + } last_x = last_y = -1; } else @@ -346,6 +350,10 @@ private void drawValueLines(final Graphics2D gc, final double value = item.getValue(); if (Double.isNaN(value)) { flushPolyLine(gc, value_poly_x, value_poly_y, line_width); + if (last_y != -1) { + highlightDiscontinuity(gc, x_transform, item); + gc.setStroke(createStroke(line_width, line_style)); + } } else { @@ -599,6 +607,20 @@ final private void drawPoint(final Graphics2D gc, final int x, final int y, fina gc.fillOval(x-size/2, y-size/2, size, size); } + /** + * Plot an additional vertical dashed line to indicate a transition to a + * non-plottable value, to highlight events such as an archiver disconnect. + * @param gc GC + * @param x_transform Horizontal axis + * @param item PlotDataItem + */ + final private void highlightDiscontinuity(final Graphics2D gc, final ScreenTransform x_transform, + final PlotDataItem item) { + final int x1 = clipX(x_transform.transform(item.getPosition())); + gc.setStroke(createStroke(1, LineStyle.DOT)); + gc.drawLine(x1, y_min, x1, y_max); + } + /** Fill area. All lists will be cleared. * @param gc GC * @param pos Horizontal screen positions