From 26414aa10696e08b77dcf8babaec61a18e02b7f7 Mon Sep 17 00:00:00 2001 From: Katy Saintin Date: Wed, 22 Oct 2025 11:10:17 +0200 Subject: [PATCH 1/3] Delete core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java Remove modification on JCA_PV, to manage Boolean Type --- .../main/java/org/phoebus/pv/ca/JCA_PV.java | 680 ------------------ 1 file changed, 680 deletions(-) delete mode 100644 core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java diff --git a/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java b/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java deleted file mode 100644 index 9ed631b820..0000000000 --- a/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java +++ /dev/null @@ -1,680 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017-2023 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.pv.ca; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; - -import org.epics.vtype.VBoolean; -import org.epics.vtype.VDouble; -import org.epics.vtype.VEnum; -import org.epics.vtype.VFloat; -import org.epics.vtype.VInt; -import org.epics.vtype.VLong; -import org.epics.vtype.VShort; -import org.epics.vtype.VString; -import org.epics.vtype.VType; -import org.phoebus.pv.PV; - -import gov.aps.jca.CAStatus; -import gov.aps.jca.Channel; -import gov.aps.jca.Monitor; -import gov.aps.jca.dbr.DBR; -import gov.aps.jca.dbr.DBRType; -import gov.aps.jca.event.AccessRightsEvent; -import gov.aps.jca.event.AccessRightsListener; -import gov.aps.jca.event.ConnectionEvent; -import gov.aps.jca.event.ConnectionListener; -import gov.aps.jca.event.GetEvent; -import gov.aps.jca.event.GetListener; -import gov.aps.jca.event.MonitorEvent; -import gov.aps.jca.event.MonitorListener; -import gov.aps.jca.event.PutEvent; -import gov.aps.jca.event.PutListener; - -/** Channel Access {@link PV} - * @author Kay Kasemir - */ -@SuppressWarnings("nls") -public class JCA_PV extends PV implements ConnectionListener, MonitorListener, AccessRightsListener -{ - /** Threshold above which arrays use a lower channel priority - * (idea from PVManager) - */ - private static final int LARGE_ARRAY_THRESHOLD = JCA_Preferences.getInstance().largeArrayThreshold(); - - /** Priority to use for channel */ - private static final short base_priority = ((JCA_Preferences.getInstance().getMonitorMask() & Monitor.VALUE) == Monitor.VALUE) - ? Channel.PRIORITY_OPI - : Channel.PRIORITY_ARCHIVE; - - /** Request plain DBR type or ..TIME..? */ - private final boolean plain_dbr; - - /** Channel Access does not really distinguish between array and scalar. - * An array may at times only have one value, like a scalar. - * To get more consistent decoding, channels with a max. element count other - * than 1 are considered arrays. - */ - private volatile boolean is_array = false; - - /** Array with more than LARGE_ARRAY_THRESHOLD elements? */ - private volatile boolean is_large_array = false; - - /** JCA Channel */ - private volatile Channel channel; - - /** Meta data. - * - *

May be - *

- */ - private volatile DBR metadata = null; - - /** Listener to initial get-callback for meta data */ - final private GetListener meta_get_listener = (GetEvent ev) -> - { - try - { - // Use channel from the event, not the volatile channel - if (! (ev.getSource() instanceof Channel)) - throw new Exception("Missing channel"); - final Channel safe_channel = (Channel) ev.getSource(); - - final DBR old_metadata = metadata; - final Class old_type = old_metadata == null ? null : old_metadata.getClass(); - // Channels from CAS, not based on records, may fail - // to provide meta data - if (ev.getStatus().isSuccessful()) - { - metadata = ev.getDBR(); - logger.log(Level.FINE, "{0} received meta data: {1}", new Object[] { getName(), metadata }); - } - else - { - metadata = null; - logger.log(Level.FINE, "{0} has no meta data: {1}", new Object[] { getName(), ev.getStatus() }); - } - - // If channel changed its type, cancel potentially existing subscription - final Class new_type = metadata == null ? null : metadata.getClass(); - if (old_type != new_type) - unsubscribe(safe_channel); - // Subscribe, either for the first time or because type changed requires new one. - // NOP if channel is already subscribed. - subscribe(safe_channel); - } - catch (Throwable ex) - { - // One scenario: Channel was closed while the metadata arrived, - // so subscription will fail. - logger.log(Level.WARNING, "Error handling metadata for channel " + getName(), ex); - } - }; - - /** Listener to meta data changes */ - final private MonitorListener meta_change_listener = (MonitorEvent ev) -> - { - if (ev.getStatus().isSuccessful()) - { - metadata = ev.getDBR(); - logger.log(Level.FINE, "{0} received new meta data: {1}", new Object[] { getName(), metadata }); - monitorChanged(ev); - } - }; - - /** Value update subscription. - * Non-zero value also used to indicate access right change subscription. - */ - private AtomicReference value_monitor = new AtomicReference<>(); - - /** Metadata update subscription */ - private AtomicReference metadata_monitor = new AtomicReference<>(); - - - /** Initialize - * @param name Full name, may include "ca://" - * @param base_name Base name without optional prefix - * @throws Exception on error - */ - public JCA_PV(final String name, String base_name) throws Exception - { - super(name); - logger.fine("JCA PV " + base_name); - // Read-only until connected and we learn otherwise - notifyListenersOfPermissions(true); - base_name = base_name.trim(); - if (base_name.isEmpty()) - throw new Exception("Empty PV name '" + name + "'"); - // .RTYP does not provide meta data - plain_dbr = base_name.endsWith(".RTYP"); - createChannel(base_name); - } - - private void createChannel(final String base_name) throws Exception - { - final short priority = is_large_array - ? base_priority - : (short) (base_priority + 1); - channel = JCAContext.getInstance().getContext().createChannel(base_name, this, priority); - channel.getContext().flushIO(); - } - - /** JCA connection listener */ - @Override - public void connectionChanged(final ConnectionEvent ev) - { - if (ev.isConnected()) - { - logger.log(Level.FINE, "{0} connected", getName()); - - // Connection handler may be called during 'create' when 'channel' has not been set, - // so use channel from event. - final Channel safe_channel = (Channel) ev.getSource(); - // Sanity check in case this.channel is already set - if (channel != null && channel != safe_channel) - throw new IllegalStateException("Expecting " + channel + ", got " + safe_channel); - - final int elements = safe_channel.getElementCount(); - is_array = elements != 1; - if (elements > LARGE_ARRAY_THRESHOLD && ! is_large_array) - { - is_large_array = true; - final String name = safe_channel.getName(); - safe_channel.dispose(); - channel = null; - logger.log(Level.FINE, "Reconnecting large array {0} at lower priority", name); - try - { - createChannel(name); - } - catch (Exception ex) - { - logger.log(Level.SEVERE, "Cannot re-create channel for large array", ex); - } - return; - } - - final boolean is_readonly = ! safe_channel.getWriteAccess(); - notifyListenersOfPermissions(is_readonly); - // .. and start subscription. - // When called from within the callback in createChannel() doing this: - // channel = CAContext....createChannel(.., this, ..), - // this.channel may not be assigned, yet, so pass the safe_channel - getMetaData(safe_channel); - } - else - { - logger.fine(getName() + " disconnected"); - notifyListenersOfDisconnect(); - // On re-connect, fetch meta data - // and maybe re-subscribe (possibly for changed type after IOC reboot) - } - } - - private void getMetaData(final Channel safe_channel) - { - try - { - logger.log(Level.FINE, () -> getName() + " get meta data"); - // With very old IOCs, could only get one element for Ctrl type. - // With R3.15.5, fetching just one element for a record.INP$ - // (i.e. fetching the string as a BYTE[]) - // crashed the IOC, i.e. had to use same request count as for the subscription, - // request_count = JCAContext.getInstance().getRequestCount(channel); - // But that bug has been fixed in 3.15.6 - // (https://bugs.launchpad.net/epics-base/+bug/1678494). - // so to optimize, only fetch one value element for the meta data. - safe_channel.get(DBRHelper.getCtrlType(plain_dbr, safe_channel.getFieldType()), 1, meta_get_listener); - safe_channel.getContext().flushIO(); - } - catch (Exception ex) - { - logger.log(Level.WARNING, getName() + " cannot get meta data", ex); - } - } - - /** Subscribe to updates. - * NOP if already subscribed. - * @param safe_channel Channel that should match `this.channel` - */ - private void subscribe(final Channel safe_channel) - { - // Avoid double-subscription - if (value_monitor.get() != null) - return; - - // Log if called while inside createChannel and channel not set, yet - if (safe_channel != channel) - logger.log(Level.WARNING, "Subscription uses " + safe_channel + " while channel is " + channel, new Exception("Stack trace")); - - try - { - final int mask = JCA_Preferences.getInstance().getMonitorMask(); - final int request_count = JCAContext.getInstance().getRequestCount(safe_channel); - logger.log(Level.FINE, getName() + " subscribes with count = " + request_count); - final Monitor new_monitor = safe_channel.addMonitor(DBRHelper.getTimeType(plain_dbr, safe_channel.getFieldType()), request_count, mask, this); - - final Monitor old_monitor = value_monitor.getAndSet(new_monitor); - // Could there have been another subscription while we established this one? - if (old_monitor != null) - { - logger.log(Level.FINE, getName() + " already had a subscription"); - try - { // Try to clear old monitor and access rights list.. - old_monitor.clear(); - safe_channel.removeAccessRightsListener(this); - } - catch (Throwable ex) - { // .. and log errors, but allow to continue - // with new rights listener and flush - logger.log(Level.WARNING, getName() + " cannot clear old monitor", ex); - } - } - - // Subscribe to metadata changes (DBE_PROPERTY) - final DBRType meta_request = getRequestForMetadata(metadata); - if (JCA_Preferences.getInstance().isDbePropertySupported() && meta_request != null) - { - Monitor old_metadata_monitor = null; - try - { - logger.log(Level.FINE, getName() + " subscribes to 'property' changes"); - old_metadata_monitor = metadata_monitor.getAndSet( - safe_channel.addMonitor(meta_request, request_count, Monitor.PROPERTY, meta_change_listener)); - - } - catch (Throwable ex) - { - logger.log(Level.WARNING, getName() + " cannot create metadata monitor", ex); - } - if (old_metadata_monitor != null) - { - try - { - old_metadata_monitor.clear(); - } - catch (Throwable ex) - { - logger.log(Level.WARNING, getName() + " cannot clear old metadata monitor", ex); - } - } - } - safe_channel.addAccessRightsListener(this); - safe_channel.getContext().flushIO(); - } - catch (Exception ex) - { - logger.log(Level.WARNING, getName() + " cannot subscribe", ex); - } - } - - private DBRType getRequestForMetadata(final DBR metadata) - { - if (metadata.isCTRL()) - return metadata.getType(); - if (metadata.isENUM()) - return DBRType.CTRL_ENUM; - return null; - } - - /** Cancel subscriptions. - * NOP if not subscribed. - * @param safe_channel Channel that should match `this.channel` - */ - private void unsubscribe(final Channel safe_channel) - { - Monitor old_monitor = value_monitor.getAndSet(null); - if (old_monitor != null) - { - // Log if called while inside createChannel and channel not set, yet - if (safe_channel != channel) - logger.log(Level.WARNING, "Unsubscription uses " + safe_channel + " while channel is " + channel, new Exception("Stack trace")); - - logger.log(Level.FINE, getName() + " unsubscribes"); - try - { - safe_channel.removeAccessRightsListener(this); - old_monitor.clear(); - } - catch (Exception ex) - { // This is 'normal', log only on FINE: - // When the channel is disconnected, CAJ cannot send - // an un-subscribe request to the client - logger.log(Level.FINE, getName() + " cannot unsubscribe", ex); - } - } - - old_monitor = metadata_monitor.getAndSet(null); - if (old_monitor != null) - { - try - { - old_monitor.clear(); - } - catch (Throwable ex) - { - logger.log(Level.FINE, getName() + " cannot unsubscribe metadata", ex); - } - } - } - - @Override - public void accessRightsChanged(final AccessRightsEvent ev) - { - final boolean readonly = ! ev.getWriteAccess(); - logger.fine(getName() + (readonly ? " is read-only" : " is writeable")); - notifyListenersOfPermissions(readonly); - } - - @Override - public void monitorChanged(final MonitorEvent ev) - { - try - { // May receive event with null status when 'disconnected' - final CAStatus status = ev.getStatus(); - if (status != null && status.isSuccessful()) - { - final VType value = DBRHelper.decodeValue(is_array, metadata, ev.getDBR()); - logger.log(Level.FINE, "{0} = {1}", new Object[] { getName(), value }); - notifyListenersOfValue(value); - } - } - catch (Exception ex) - { - logger.log(Level.WARNING, getName() + " monitor error", ex); - ex.printStackTrace(); - } - } - - /** {@link Future} that acts as JCA {@link GetListener} - * and provides the value or error to user of the {@link Future} - */ - private class GetCallbackFuture extends CompletableFuture implements GetListener - { - @Override - public void getCompleted(final GetEvent ev) - { - try - { - if (ev.getStatus().isSuccessful()) - { - final VType value = DBRHelper.decodeValue(is_array, metadata, ev.getDBR()); - logger.log(Level.FINE, "{0} get-callback {1}", new Object[] { getName(), value }); - complete(value); - } - else - { - notifyListenersOfDisconnect(); - completeExceptionally(new Exception(ev.getStatus().getMessage())); - } - } - catch (Exception ex) - { - completeExceptionally(ex); - } - } - } - - @Override - public CompletableFuture asyncRead() throws Exception - { - final DBRType type = channel.getFieldType(); - if (type == null || type == DBRType.UNKNOWN) - throw new Exception(getName() + " is not connected"); - final GetCallbackFuture result = new GetCallbackFuture(); - channel.get(DBRHelper.getTimeType(plain_dbr, type), channel.getElementCount(), result); - channel.getContext().flushIO(); - return result; - } - - /** {@link Future} that acts as JCA {@link PutListener} - * and provides error to user of the {@link Future} - */ - private class PutCallbackFuture extends CompletableFuture implements PutListener - { - @Override - public void putCompleted(final PutEvent ev) - { - if (ev.getStatus().isSuccessful()) - complete(null); - else - completeExceptionally(new Exception(getName() + " write failed: " + ev.getStatus().getMessage())); - } - } - - @Override - public void write(final Object new_value) throws Exception - { - performWrite(new_value, null); - } - - @Override - public CompletableFuture asyncWrite(final Object new_value) throws Exception - { - final PutCallbackFuture result = new PutCallbackFuture(); - performWrite(new_value, result); - return result; - } - - private void performWrite(final Object newvalue, final PutListener put_listener) throws Exception - { - //Manage type of PV to convert the value in good format - VType vType = read(); - Object new_value = newvalue; - if(vType instanceof VString) { - new_value = newvalue.toString(); - } - else if(vType instanceof VDouble) { - new_value = Double.valueOf(new_value.toString()); - } - else if(vType instanceof VLong) { - new_value = Double.valueOf(new_value.toString()).longValue(); - } - else if(vType instanceof VFloat) { - new_value = Double.valueOf(new_value.toString()).floatValue(); - } - else if(vType instanceof VInt) { - new_value = Double.valueOf(new_value.toString()).intValue(); - } - else if(vType instanceof VShort) { - new_value = Double.valueOf(new_value.toString()).shortValue(); - } - else if(vType instanceof VEnum) { - new_value = Double.valueOf(new_value.toString()).intValue(); - } - else if(vType instanceof VBoolean) { - new_value = Boolean.parseBoolean(new_value.toString()); - } - - if (new_value instanceof String) - { - if (channel.getFieldType().isBYTE() && channel.getElementCount() > 1) - { // Long string support: Write characters of string as DBF_CHAR array - final char[] chars = ((String) new_value).toCharArray(); - final int[] codes = new int[chars.length+1]; - for (int i=0; i Date: Wed, 22 Oct 2025 11:15:13 +0200 Subject: [PATCH 2/3] Rollback PV_CA --- .../main/java/org/phoebus/pv/ca/JCA_PV.java | 636 ++++++++++++++++++ 1 file changed, 636 insertions(+) create mode 100644 core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java diff --git a/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java b/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java new file mode 100644 index 0000000000..a43480db21 --- /dev/null +++ b/core/pv-ca/src/main/java/org/phoebus/pv/ca/JCA_PV.java @@ -0,0 +1,636 @@ +/******************************************************************************* + * Copyright (c) 2017-2023 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.pv.ca; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; + +import org.epics.vtype.VType; +import org.phoebus.pv.PV; + +import gov.aps.jca.CAStatus; +import gov.aps.jca.Channel; +import gov.aps.jca.Monitor; +import gov.aps.jca.dbr.DBR; +import gov.aps.jca.dbr.DBRType; +import gov.aps.jca.event.AccessRightsEvent; +import gov.aps.jca.event.AccessRightsListener; +import gov.aps.jca.event.ConnectionEvent; +import gov.aps.jca.event.ConnectionListener; +import gov.aps.jca.event.GetEvent; +import gov.aps.jca.event.GetListener; +import gov.aps.jca.event.MonitorEvent; +import gov.aps.jca.event.MonitorListener; +import gov.aps.jca.event.PutEvent; +import gov.aps.jca.event.PutListener; + +/** Channel Access {@link PV} + * @author Kay Kasemir + */ +@SuppressWarnings("nls") +public class JCA_PV extends PV implements ConnectionListener, MonitorListener, AccessRightsListener +{ + /** Threshold above which arrays use a lower channel priority + * (idea from PVManager) + */ + private static final int LARGE_ARRAY_THRESHOLD = JCA_Preferences.getInstance().largeArrayThreshold(); + + /** Priority to use for channel */ + private static final short base_priority = ((JCA_Preferences.getInstance().getMonitorMask() & Monitor.VALUE) == Monitor.VALUE) + ? Channel.PRIORITY_OPI + : Channel.PRIORITY_ARCHIVE; + + /** Request plain DBR type or ..TIME..? */ + private final boolean plain_dbr; + + /** Channel Access does not really distinguish between array and scalar. + * An array may at times only have one value, like a scalar. + * To get more consistent decoding, channels with a max. element count other + * than 1 are considered arrays. + */ + private volatile boolean is_array = false; + + /** Array with more than LARGE_ARRAY_THRESHOLD elements? */ + private volatile boolean is_large_array = false; + + /** JCA Channel */ + private volatile Channel channel; + + /** Meta data. + * + *

May be + *

    + *
  • null + *
  • DBR_CTRL_Double, DBR_CTRL_INT, ..BYTE, which all implement CTRL and TIME + *
  • DBR_CTRL_String, DBR_CTRL_Enum which are each different + *
+ */ + private volatile DBR metadata = null; + + /** Listener to initial get-callback for meta data */ + final private GetListener meta_get_listener = (GetEvent ev) -> + { + try + { + // Use channel from the event, not the volatile channel + if (! (ev.getSource() instanceof Channel)) + throw new Exception("Missing channel"); + final Channel safe_channel = (Channel) ev.getSource(); + + final DBR old_metadata = metadata; + final Class old_type = old_metadata == null ? null : old_metadata.getClass(); + // Channels from CAS, not based on records, may fail + // to provide meta data + if (ev.getStatus().isSuccessful()) + { + metadata = ev.getDBR(); + logger.log(Level.FINE, "{0} received meta data: {1}", new Object[] { getName(), metadata }); + } + else + { + metadata = null; + logger.log(Level.FINE, "{0} has no meta data: {1}", new Object[] { getName(), ev.getStatus() }); + } + + // If channel changed its type, cancel potentially existing subscription + final Class new_type = metadata == null ? null : metadata.getClass(); + if (old_type != new_type) + unsubscribe(safe_channel); + // Subscribe, either for the first time or because type changed requires new one. + // NOP if channel is already subscribed. + subscribe(safe_channel); + } + catch (Throwable ex) + { + // One scenario: Channel was closed while the metadata arrived, + // so subscription will fail. + logger.log(Level.WARNING, "Error handling metadata for channel " + getName(), ex); + } + }; + + /** Listener to meta data changes */ + final private MonitorListener meta_change_listener = (MonitorEvent ev) -> + { + if (ev.getStatus().isSuccessful()) + { + metadata = ev.getDBR(); + logger.log(Level.FINE, "{0} received new meta data: {1}", new Object[] { getName(), metadata }); + monitorChanged(ev); + } + }; + + /** Value update subscription. + * Non-zero value also used to indicate access right change subscription. + */ + private AtomicReference value_monitor = new AtomicReference<>(); + + /** Metadata update subscription */ + private AtomicReference metadata_monitor = new AtomicReference<>(); + + + /** Initialize + * @param name Full name, may include "ca://" + * @param base_name Base name without optional prefix + * @throws Exception on error + */ + public JCA_PV(final String name, String base_name) throws Exception + { + super(name); + logger.fine("JCA PV " + base_name); + // Read-only until connected and we learn otherwise + notifyListenersOfPermissions(true); + base_name = base_name.trim(); + if (base_name.isEmpty()) + throw new Exception("Empty PV name '" + name + "'"); + // .RTYP does not provide meta data + plain_dbr = base_name.endsWith(".RTYP"); + createChannel(base_name); + } + + private void createChannel(final String base_name) throws Exception + { + final short priority = is_large_array + ? base_priority + : (short) (base_priority + 1); + channel = JCAContext.getInstance().getContext().createChannel(base_name, this, priority); + channel.getContext().flushIO(); + } + + /** JCA connection listener */ + @Override + public void connectionChanged(final ConnectionEvent ev) + { + if (ev.isConnected()) + { + logger.log(Level.FINE, "{0} connected", getName()); + + // Connection handler may be called during 'create' when 'channel' has not been set, + // so use channel from event. + final Channel safe_channel = (Channel) ev.getSource(); + // Sanity check in case this.channel is already set + if (channel != null && channel != safe_channel) + throw new IllegalStateException("Expecting " + channel + ", got " + safe_channel); + + final int elements = safe_channel.getElementCount(); + is_array = elements != 1; + if (elements > LARGE_ARRAY_THRESHOLD && ! is_large_array) + { + is_large_array = true; + final String name = safe_channel.getName(); + safe_channel.dispose(); + channel = null; + logger.log(Level.FINE, "Reconnecting large array {0} at lower priority", name); + try + { + createChannel(name); + } + catch (Exception ex) + { + logger.log(Level.SEVERE, "Cannot re-create channel for large array", ex); + } + return; + } + + final boolean is_readonly = ! safe_channel.getWriteAccess(); + notifyListenersOfPermissions(is_readonly); + // .. and start subscription. + // When called from within the callback in createChannel() doing this: + // channel = CAContext....createChannel(.., this, ..), + // this.channel may not be assigned, yet, so pass the safe_channel + getMetaData(safe_channel); + } + else + { + logger.fine(getName() + " disconnected"); + notifyListenersOfDisconnect(); + // On re-connect, fetch meta data + // and maybe re-subscribe (possibly for changed type after IOC reboot) + } + } + + private void getMetaData(final Channel safe_channel) + { + try + { + logger.log(Level.FINE, () -> getName() + " get meta data"); + // With very old IOCs, could only get one element for Ctrl type. + // With R3.15.5, fetching just one element for a record.INP$ + // (i.e. fetching the string as a BYTE[]) + // crashed the IOC, i.e. had to use same request count as for the subscription, + // request_count = JCAContext.getInstance().getRequestCount(channel); + // But that bug has been fixed in 3.15.6 + // (https://bugs.launchpad.net/epics-base/+bug/1678494). + // so to optimize, only fetch one value element for the meta data. + safe_channel.get(DBRHelper.getCtrlType(plain_dbr, safe_channel.getFieldType()), 1, meta_get_listener); + safe_channel.getContext().flushIO(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, getName() + " cannot get meta data", ex); + } + } + + /** Subscribe to updates. + * NOP if already subscribed. + * @param safe_channel Channel that should match `this.channel` + */ + private void subscribe(final Channel safe_channel) + { + // Avoid double-subscription + if (value_monitor.get() != null) + return; + + // Log if called while inside createChannel and channel not set, yet + if (safe_channel != channel) + logger.log(Level.WARNING, "Subscription uses " + safe_channel + " while channel is " + channel, new Exception("Stack trace")); + + try + { + final int mask = JCA_Preferences.getInstance().getMonitorMask(); + final int request_count = JCAContext.getInstance().getRequestCount(safe_channel); + logger.log(Level.FINE, getName() + " subscribes with count = " + request_count); + final Monitor new_monitor = safe_channel.addMonitor(DBRHelper.getTimeType(plain_dbr, safe_channel.getFieldType()), request_count, mask, this); + + final Monitor old_monitor = value_monitor.getAndSet(new_monitor); + // Could there have been another subscription while we established this one? + if (old_monitor != null) + { + logger.log(Level.FINE, getName() + " already had a subscription"); + try + { // Try to clear old monitor and access rights list.. + old_monitor.clear(); + safe_channel.removeAccessRightsListener(this); + } + catch (Throwable ex) + { // .. and log errors, but allow to continue + // with new rights listener and flush + logger.log(Level.WARNING, getName() + " cannot clear old monitor", ex); + } + } + + // Subscribe to metadata changes (DBE_PROPERTY) + final DBRType meta_request = getRequestForMetadata(metadata); + if (JCA_Preferences.getInstance().isDbePropertySupported() && meta_request != null) + { + Monitor old_metadata_monitor = null; + try + { + logger.log(Level.FINE, getName() + " subscribes to 'property' changes"); + old_metadata_monitor = metadata_monitor.getAndSet( + safe_channel.addMonitor(meta_request, request_count, Monitor.PROPERTY, meta_change_listener)); + + } + catch (Throwable ex) + { + logger.log(Level.WARNING, getName() + " cannot create metadata monitor", ex); + } + if (old_metadata_monitor != null) + { + try + { + old_metadata_monitor.clear(); + } + catch (Throwable ex) + { + logger.log(Level.WARNING, getName() + " cannot clear old metadata monitor", ex); + } + } + } + safe_channel.addAccessRightsListener(this); + safe_channel.getContext().flushIO(); + } + catch (Exception ex) + { + logger.log(Level.WARNING, getName() + " cannot subscribe", ex); + } + } + + private DBRType getRequestForMetadata(final DBR metadata) + { + if (metadata.isCTRL()) + return metadata.getType(); + if (metadata.isENUM()) + return DBRType.CTRL_ENUM; + return null; + } + + /** Cancel subscriptions. + * NOP if not subscribed. + * @param safe_channel Channel that should match `this.channel` + */ + private void unsubscribe(final Channel safe_channel) + { + Monitor old_monitor = value_monitor.getAndSet(null); + if (old_monitor != null) + { + // Log if called while inside createChannel and channel not set, yet + if (safe_channel != channel) + logger.log(Level.WARNING, "Unsubscription uses " + safe_channel + " while channel is " + channel, new Exception("Stack trace")); + + logger.log(Level.FINE, getName() + " unsubscribes"); + try + { + safe_channel.removeAccessRightsListener(this); + old_monitor.clear(); + } + catch (Exception ex) + { // This is 'normal', log only on FINE: + // When the channel is disconnected, CAJ cannot send + // an un-subscribe request to the client + logger.log(Level.FINE, getName() + " cannot unsubscribe", ex); + } + } + + old_monitor = metadata_monitor.getAndSet(null); + if (old_monitor != null) + { + try + { + old_monitor.clear(); + } + catch (Throwable ex) + { + logger.log(Level.FINE, getName() + " cannot unsubscribe metadata", ex); + } + } + } + + @Override + public void accessRightsChanged(final AccessRightsEvent ev) + { + final boolean readonly = ! ev.getWriteAccess(); + logger.fine(getName() + (readonly ? " is read-only" : " is writeable")); + notifyListenersOfPermissions(readonly); + } + + @Override + public void monitorChanged(final MonitorEvent ev) + { + try + { // May receive event with null status when 'disconnected' + final CAStatus status = ev.getStatus(); + if (status != null && status.isSuccessful()) + { + final VType value = DBRHelper.decodeValue(is_array, metadata, ev.getDBR()); + logger.log(Level.FINE, "{0} = {1}", new Object[] { getName(), value }); + notifyListenersOfValue(value); + } + } + catch (Exception ex) + { + logger.log(Level.WARNING, getName() + " monitor error", ex); + ex.printStackTrace(); + } + } + + /** {@link Future} that acts as JCA {@link GetListener} + * and provides the value or error to user of the {@link Future} + */ + private class GetCallbackFuture extends CompletableFuture implements GetListener + { + @Override + public void getCompleted(final GetEvent ev) + { + try + { + if (ev.getStatus().isSuccessful()) + { + final VType value = DBRHelper.decodeValue(is_array, metadata, ev.getDBR()); + logger.log(Level.FINE, "{0} get-callback {1}", new Object[] { getName(), value }); + complete(value); + } + else + { + notifyListenersOfDisconnect(); + completeExceptionally(new Exception(ev.getStatus().getMessage())); + } + } + catch (Exception ex) + { + completeExceptionally(ex); + } + } + } + + @Override + public CompletableFuture asyncRead() throws Exception + { + final DBRType type = channel.getFieldType(); + if (type == null || type == DBRType.UNKNOWN) + throw new Exception(getName() + " is not connected"); + final GetCallbackFuture result = new GetCallbackFuture(); + channel.get(DBRHelper.getTimeType(plain_dbr, type), channel.getElementCount(), result); + channel.getContext().flushIO(); + return result; + } + + /** {@link Future} that acts as JCA {@link PutListener} + * and provides error to user of the {@link Future} + */ + private class PutCallbackFuture extends CompletableFuture implements PutListener + { + @Override + public void putCompleted(final PutEvent ev) + { + if (ev.getStatus().isSuccessful()) + complete(null); + else + completeExceptionally(new Exception(getName() + " write failed: " + ev.getStatus().getMessage())); + } + } + + @Override + public void write(final Object new_value) throws Exception + { + performWrite(new_value, null); + } + + @Override + public CompletableFuture asyncWrite(final Object new_value) throws Exception + { + final PutCallbackFuture result = new PutCallbackFuture(); + performWrite(new_value, result); + return result; + } + + private void performWrite(final Object new_value, final PutListener put_listener) throws Exception + { + if (new_value instanceof String) + { + if (channel.getFieldType().isBYTE() && channel.getElementCount() > 1) + { // Long string support: Write characters of string as DBF_CHAR array + final char[] chars = ((String) new_value).toCharArray(); + final int[] codes = new int[chars.length+1]; + for (int i=0; i Date: Tue, 28 Oct 2025 11:17:29 +0100 Subject: [PATCH 3/3] Solve write action on PV that named different from given name see PR #3412 --- .../builder/runtime/WidgetRuntime.java | 46 +++++++++---------- .../runtime/test/WidgetRuntimeTest.java | 7 ++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/WidgetRuntime.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/WidgetRuntime.java index b1767f8783..b065d77d28 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/WidgetRuntime.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/WidgetRuntime.java @@ -26,7 +26,6 @@ import org.epics.vtype.VType; import org.phoebus.framework.macros.MacroHandler; import org.phoebus.framework.macros.MacroValueProvider; -import org.phoebus.pv.PVPool.TypedName; import java.util.ArrayList; import java.util.Collection; @@ -107,7 +106,8 @@ public class WidgetRuntime { */ // This is empty for most widgets, or contains very few PVs, // so using List with linear lookup by name and not a HashMap - private volatile List writable_pvs = null; + // Set pv_name as Key to find the corresponding RuntimePV + private volatile Map writable_pvs = null; /** * Handlers for widget's behaviorScripts property, @@ -230,16 +230,14 @@ public void start() { // Prepare action-related PVs final List actions = widget.propActions().getValue().getActions(); if (actions.size() > 0) { - final List action_pvs = new ArrayList<>(); + final Map action_pvs = new HashMap<>(); for (final ActionInfo action : actions) { if (action instanceof WritePVAction) { final String pv_name = ((WritePVAction) action).getPV(); try { final String expanded = MacroHandler.replace(widget.getMacrosOrProperties(), pv_name); - // Manage default datasource if not ca - final TypedName type_name = TypedName.analyze(expanded); - final RuntimePV pv = PVFactory.getPV(type_name.toString()); - action_pvs.add(pv); + final RuntimePV pv = PVFactory.getPV(expanded); + action_pvs.put(expanded, pv); addPV(pv, true); } catch (Exception ex) { logger.log(Level.WARNING, widget + " cannot start action to write PV '" + pv_name + "'", ex); @@ -398,12 +396,8 @@ public void writePrimaryPV(final Object value) { public void writePV(final String pv_name, final Object value) throws Exception { final String expanded = MacroHandler.replace(widget.getMacrosOrProperties(), pv_name); String name_to_check = expanded; - // Check for default datasource to manage custom datasource - final TypedName type_name = TypedName.analyze(name_to_check); - name_to_check = type_name != null ? type_name.toString() : name_to_check; - String dataType = type_name != null ? type_name.type : null; // For local PV, - if (dataType != null && dataType.equals("loc")) { + if (name_to_check.startsWith("loc://")) { // strip optional data type ... int sep = name_to_check.indexOf('<'); if (sep > 0) @@ -414,18 +408,20 @@ public void writePV(final String pv_name, final Object value) throws Exception { name_to_check = name_to_check.substring(0, sep); } awaitStartup(); - final List safe_pvs = writable_pvs; - if (safe_pvs != null) - for (final RuntimePV pv : safe_pvs) - if (pv.getName().equals(name_to_check)) { + final Map safe_pvs = writable_pvs; + if (safe_pvs != null) { + final RuntimePV pv = safe_pvs.get(name_to_check); + if(pv != null) { try { pv.write(value); } catch (final Exception ex) { throw new Exception("Failed to write " + value + " to PV " + name_to_check, ex); } - return; } - throw new Exception("Unknown PV '" + pv_name + "' (expanded: '" + name_to_check + "')"); + else { + throw new Exception("Unknown PV '" + pv_name + "' (expanded: '" + name_to_check + "')"); + } + } } /** @@ -448,13 +444,15 @@ public void stop() { awaitStartup(); widget.propClass().removePropertyListener(update_widget_class); - final List safe_pvs = writable_pvs; - if (safe_pvs != null) { - for (final RuntimePV pv : safe_pvs) { - removePV(pv); - PVFactory.releasePV(pv); + if(writable_pvs != null && !writable_pvs.isEmpty()) { + final Collection safe_pvs = writable_pvs.values(); + if (safe_pvs != null) { + for (final RuntimePV pv : safe_pvs) { + removePV(pv); + PVFactory.releasePV(pv); + } + writable_pvs = null; } - writable_pvs = null; } final PVNameToValueBinding binding = pv_name_binding.getAndSet(null); diff --git a/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/WidgetRuntimeTest.java b/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/WidgetRuntimeTest.java index 7c7b0b7025..4d52b2fd81 100644 --- a/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/WidgetRuntimeTest.java +++ b/app/display/runtime/src/test/java/org/csstudio/display/builder/runtime/test/WidgetRuntimeTest.java @@ -66,14 +66,13 @@ public void testWriteAction() //Test if the new value is ok assertThat(readValue, equalTo(newValue)); + //Generate a stacktrace to fix in LocalPVFactory + ofWidget.stop(); + } catch (Exception e) { e.printStackTrace(); fail(e); } - - - - } }