diff --git a/core/pv/doc/index.rst b/core/pv/doc/index.rst index dcd3ee3c1a..e1cf9197ae 100644 --- a/core/pv/doc/index.rst +++ b/core/pv/doc/index.rst @@ -151,3 +151,16 @@ Examples :: sys://timeOffset(12 hours) sys://timeOffset(1hour, time, 1) + + +Tango +------ +Tango is different from EPICS, the smallest unit is Device, which includes the commands, states, and attributes. +The command and the attribute has been implemented, add prefix to PV Name in editing interface to distinguish, and the command usually has a return value, so need to use *Text Entry* or a combination of *Action button* and *Text Update* components to achieve it. +Currently, all types of scalars are supported, but SPECTRUM and IMAGE are not yet supported. + +Examples :: + + tga://device/attribute + tgc://device/command + diff --git a/core/pv/pom.xml b/core/pv/pom.xml index 0c51f64f02..52e5e2286f 100644 --- a/core/pv/pom.xml +++ b/core/pv/pom.xml @@ -71,5 +71,11 @@ org.eclipse.paho.client.mqttv3 1.2.2 + + org.tango-controls + JTango + 9.7.0 + pom + diff --git a/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttrContext.java b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttrContext.java new file mode 100644 index 0000000000..644a72b416 --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttrContext.java @@ -0,0 +1,162 @@ +package org.phoebus.pv.tga; + +import fr.esrf.Tango.DevFailed; +import fr.esrf.Tango.EventProperties; +import fr.esrf.TangoApi.AttributeInfoEx; +import fr.esrf.TangoApi.AttributeProxy; +import fr.esrf.TangoApi.DeviceAttribute; +import org.epics.vtype.*; +import org.phoebus.pv.PV; +import org.tango.attribute.AttributeTangoType; +import org.tango.server.events.EventType; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +; + +public class TangoAttrContext { + private static TangoAttrContext instance; + private final ConcurrentHashMap attributeProxys; + private final ConcurrentHashMap events; + private final ConcurrentHashMap types; + + + + private TangoAttrContext() { + events = new ConcurrentHashMap<>(); + attributeProxys = new ConcurrentHashMap<>(); + types = new ConcurrentHashMap<>(); + } + + public static synchronized TangoAttrContext getInstance() throws Exception { + if (instance == null) + instance = new TangoAttrContext(); + return instance; + } + + public void subscribeAttributeEvent(String deviceName, String attributeName, String baseName, TangoAttr_PV pv) throws DevFailed { + String name = deviceName +"/" + attributeName; + AttributeProxy attributeProxy; + if (attributeProxys.get(baseName) == null) { + attributeProxy = new AttributeProxy(name); + subscribeAttributeEvent(baseName, attributeProxy, pv); + attributeProxys.put(baseName,attributeProxy); + }else { + //nothing to do + } + + } + + private void subscribeAttributeEvent(String baseName, AttributeProxy attributeProxy, TangoAttr_PV pv) throws DevFailed { + AttributeInfoEx attribute_info_ex; + try { + attribute_info_ex = attributeProxy.get_info_ex(); + } catch (DevFailed e) { + throw new RuntimeException(e); + } + + //obtain the type of attribute's value. + AttributeTangoType type = AttributeTangoType.getTypeFromTango(attribute_info_ex.data_type); + + types.putIfAbsent(baseName, type); + + + //obtain the tango event type. + EventType eventType = EventType.CHANGE_EVENT; + EventProperties tangoObj = attribute_info_ex.events.getTangoObj(); + if (tangoObj.ch_event.abs_change.equals("Not specified") && tangoObj.ch_event.rel_change.equals("Not specified")) + eventType = EventType.PERIODIC_EVENT; + + //subscribe the tango event. + int event_id; + try { + event_id = attributeProxy.subscribe_event(eventType.getValue(), pv.new TangoCallBack(type), new String[]{}); + } catch (DevFailed e) { + throw new RuntimeException(e); + } + events.put(baseName, event_id); + + } + + public void unSubscribeAttributeEvent(String baseName) throws Exception { + if (!attributeProxys.containsKey(baseName)){ + PV.logger.log(Level.WARNING, "Could not unsubscribe Tango attribute \"" + baseName + + "\" due to no Attribute Proxy."); + throw new Exception("Tango attribute unsubscribe failed: no Attribute proxy connection."); + } + + AttributeProxy attributeProxy = attributeProxys.get(baseName); + Integer event_id = events.get(baseName); + if (event_id == null){ + PV.logger.log(Level.WARNING, "Could not unsubscribe Tango attribute \"" + baseName + + "\" due to no internal record of attribute"); + throw new Exception("Tango attribute unsubscribe failed: no attribute record."); + } + attributeProxy.getDeviceProxy().unsubscribe_event(event_id); + attributeProxys.remove(baseName); + events.remove(baseName); + types.remove(baseName); + } + + + public void writeAttribute(String baseName, String attributeName, Object new_value) throws Exception { + AttributeProxy attributeProxy = attributeProxys.get(baseName); + AttributeTangoType type = types.get(baseName); + if (type == null){ + PV.logger.log(Level.WARNING, "Could not find type of attribute :" + baseName); + throw new Exception("Tango attribute write failed: attribute type not found."); + } + VType vType; + String value; + switch (type){ + case DEVBOOLEAN: + vType = TangoTypeUtil.convert(new_value, VBoolean.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Boolean.parseBoolean(value))); + break; + case DEVLONG64: + case DEVULONG64: + vType = TangoTypeUtil.convert(new_value, VLong.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Long.parseLong(value))); + break; + case DEVSHORT: + case DEVUSHORT: + vType = TangoTypeUtil.convert(new_value, VShort.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Short.parseShort(value))); + break; + case DEVLONG: + case DEVULONG: + vType = TangoTypeUtil.convert(new_value, VInt.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Integer.parseInt(value))); + break; + case DEVFLOAT: + vType = TangoTypeUtil.convert(new_value, VFloat.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Float.parseFloat(value))); + break; + case DEVDOUBLE: + vType = TangoTypeUtil.convert(new_value, VDouble.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Double.parseDouble(value))); + break; + case DEVSTRING: + vType = TangoTypeUtil.convert(new_value, VString.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, value)); + break; + case DEVUCHAR: + vType = TangoTypeUtil.convert(new_value, VByte.class); + value = TangoTypeUtil.ToString(vType); + attributeProxy.write(new DeviceAttribute(attributeName, Byte.parseByte(value))); + break; + default: + throw new IllegalArgumentException("Value " + new_value + " cannot be converted."); + } + + } + +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PV.java b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PV.java new file mode 100644 index 0000000000..2632863e1b --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PV.java @@ -0,0 +1,128 @@ +package org.phoebus.pv.tga; + +import fr.esrf.Tango.DevFailed; +import fr.esrf.TangoApi.CallBack; +import fr.esrf.TangoApi.DeviceAttribute; +import fr.esrf.TangoApi.events.EventData; +import org.epics.vtype.*; +import org.phoebus.pv.PV; +import org.tango.attribute.AttributeTangoType; + +import java.time.Instant; +import java.util.logging.Level; + +public class TangoAttr_PV extends PV { + + private final String baseName; + private String device; + private String attribute; + + + public TangoAttr_PV(String name, String baseName) throws Exception { + super(name); + this.baseName = baseName; + parseRawName(baseName); + TangoAttrContext.getInstance().subscribeAttributeEvent(device, attribute, baseName,this); + } + + + private void parseRawName(final String name) throws Exception { + int pos = name.lastIndexOf('/'); + if (pos <= 0) + throw new Exception("Invalid input:" + name); + //Locate device name + device = name.substring(0,pos); + //Locate tango attribute + attribute = name.substring(pos+1); + } + + + @Override + protected void close() + { + try + { + TangoAttrContext.getInstance().unSubscribeAttributeEvent(baseName); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Failed to unsubscribe Tango Attribute from base name " + baseName); + ex.printStackTrace(); + } + } + + @Override + public void write(final Object new_value) throws Exception{ + if (new_value == null) + throw new Exception(getName() + " got null"); + TangoAttrContext.getInstance().writeAttribute(baseName, attribute, new_value); + } + + + class TangoCallBack extends CallBack { + private final AttributeTangoType type; + + public TangoCallBack(AttributeTangoType type) { + this.type = type; + } + + @Override + public void push_event(EventData evt) { + try { + VType value; + DeviceAttribute attr_value = evt.attr_value; + Time time = Time.of(Instant.ofEpochMilli(attr_value.getTime())); + switch (type){ + case DEVBOOLEAN: + value = VBoolean.of(attr_value.extractBoolean(), Alarm.none(), time); + notifyListenersOfValue(value); + break; + case DEVLONG64: + value = VLong.of(attr_value.extractLong64(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVULONG64: + value = VLong.of(attr_value.extractULong64(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVSHORT: + value = VShort.of(attr_value.extractShort(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVUSHORT: + value = VInt.of(attr_value.extractUShort(),Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVLONG: + value = VInt.of(attr_value.extractLong(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVULONG: + value = VLong.of(attr_value.extractULong(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVFLOAT: + value = VFloat.of(attr_value.extractFloat(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVDOUBLE: + value = VDouble.of(attr_value.extractDouble(), Alarm.none(), time, Display.none()); + notifyListenersOfValue(value); + break; + case DEVSTRING: + value = VString.of(attr_value.extractString(), Alarm.none(), time); + notifyListenersOfValue(value); + break; + case DEVUCHAR: + value = VShort.of(attr_value.extractUChar(), Alarm.none(), time,Display.none()); + notifyListenersOfValue(value); + break; + default: + throw new IllegalArgumentException("Value " + evt.attr_value + " cannot be converted."); + } + }catch (DevFailed e){ + throw new RuntimeException(e); + } + } + } +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PVFactory.java b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PVFactory.java new file mode 100644 index 0000000000..1661955be9 --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tga/TangoAttr_PVFactory.java @@ -0,0 +1,23 @@ +package org.phoebus.pv.tga; + +import org.phoebus.pv.PV; +import org.phoebus.pv.PVFactory; + + +public class TangoAttr_PVFactory implements PVFactory { + + /** PV type implemented by this factory */ + final public static String TYPE = "tga"; + + @Override + public String getType() { + return TYPE; + } + + @Override + public PV createPV(String name, String base_name) throws Exception { + return new TangoAttr_PV(name, base_name); + } + + +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tga/TangoTypeUtil.java b/core/pv/src/main/java/org/phoebus/pv/tga/TangoTypeUtil.java new file mode 100644 index 0000000000..32ab39fc01 --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tga/TangoTypeUtil.java @@ -0,0 +1,172 @@ +package org.phoebus.pv.tga; + + +import org.epics.vtype.*; + +import java.util.Objects; + +public class TangoTypeUtil { + + public static VType convert(final Object value, Class type) throws Exception + { + + if (type.isInstance(value)) + return (VType) value; + + if (value instanceof VType) + return (VType) value; + + if (type == VBoolean.class){ + return VBoolean.of((Boolean) value, Alarm.none(), Time.now()); + } + + if (type == VLong.class) + { + if (value instanceof Number) + return VLong.of(((Number)value).longValue(), Alarm.none(), Time.now(), Display.none()); + return parseStringToVLong(Objects.toString(value)); + } + + if (type == VShort.class) + { + if (value instanceof Number) + return VShort.of(((Number)value).longValue(), Alarm.none(), Time.now(), Display.none()); + return parseStringToVShort(Objects.toString(value)); + } + + if (type == VInt.class) + { + if (value instanceof Number) + return VInt.of(((Number)value).longValue(), Alarm.none(), Time.now(), Display.none()); + return parseStringToVInt(Objects.toString(value)); + } + + if (type == VFloat.class) + { + if (value instanceof Number) + return VFloat.of(((Number)value).longValue(), Alarm.none(), Time.now(), Display.none()); + return parseStringToVFloat(Objects.toString(value)); + } + if (type == VDouble.class) + { + if (value instanceof Number) + return VDouble.of((Number)value, Alarm.none(), Time.now(), Display.none()); + + return parseStringToVDouble(Objects.toString(value)); + } + + if (type == VString.class) + return parseStringToVString(Objects.toString(value)); + + if (type == VByte.class){ + if (value instanceof Number) + return VByte.of(((Number)value).longValue(), Alarm.none(), Time.now(), Display.none()); + return parseStringToVByte(Objects.toString(value)); + } + throw new Exception("Expected type " + type.getSimpleName() + " but got " + value.getClass().getName()); + } + + private static VType parseStringToVByte(String value) throws Exception { + try + { + return VByte.of(Double.parseDouble(value), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VByte from '" + value + "'"); + } + } + + private static VType parseStringToVString(String value) { + return VString.of(stripQuotes(value), Alarm.none(), Time.now()); + } + + + private static String stripQuotes(final String text) + { + if (text.length() < 2) + return text; + + if ((text.charAt(0) == '"') && (text.charAt(text.length()-1) == '"')) + return text.substring(1,text.length()-1); + + return text; + } + + private static VType parseStringToVFloat(String value) throws Exception { + try + { + return VFloat.of(Double.parseDouble(value), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VFloat from '" + value + "'"); + } + } + + private static VType parseStringToVDouble(String value) throws Exception { + try + { + return VDouble.of(Double.parseDouble(value), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VDouble from '" + value + "'"); + } + } + + private static VType parseStringToVInt(String value) throws Exception { + try + { + return VInt.of(Double.parseDouble(value), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VInt from '" + value + "'"); + } + + } + + private static VType parseStringToVShort(String value) throws Exception { + try + { + return VShort.of(Double.parseDouble(value), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VShort from '" + value + "'"); + } + } + + private static VType parseStringToVLong(String value) throws Exception { + try + { + return VLong.of(Double.valueOf(value).longValue(), Alarm.none(), Time.now(), Display.none()); + } + catch (NumberFormatException ex) + { + throw new Exception("Cannot parse VLong from '" + value + "'"); + } + } + + public static String ToString( Object value ) throws Exception{ + //VType vType = convert(value, type); + StringBuilder sb = new StringBuilder(); + + if (value instanceof VString) + { + return sb.append("\"").append(((VString)value).getValue()).append("\"").toString(); + } + if (value instanceof VBoolean) + { + return sb.append(((VBoolean)value).getValue()).toString(); + } + if (value instanceof VNumber) + { + return sb.append(((VNumber)value).getValue()).toString(); + } + + throw new Exception ("Cannot change unknown type to String " + value.getClass().getName()); + } + +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmdContext.java b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmdContext.java new file mode 100644 index 0000000000..373f52afb7 --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmdContext.java @@ -0,0 +1,109 @@ +package org.phoebus.pv.tgc; + +import fr.esrf.Tango.DevFailed; +import fr.soleil.tango.clientapi.TangoCommand; +import org.epics.vtype.*; +import org.phoebus.pv.PV; +import org.phoebus.pv.tga.TangoTypeUtil; +import org.tango.command.CommandTangoType; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +public class TangoCmdContext { + + private static TangoCmdContext instance; + private final ConcurrentHashMap commands; + + private TangoCmdContext() { + commands = new ConcurrentHashMap<>(); + } + + public static synchronized TangoCmdContext getInstance() throws Exception { + if (instance == null) + instance = new TangoCmdContext(); + return instance; + } + + public void createTangoCommand(String deviceName, String commandName, String baseName, TangoCmd_PV pv) throws DevFailed { + TangoCommand tangoCommand = commands.get(baseName); + if ( tangoCommand == null ){ + tangoCommand = new TangoCommand(deviceName, commandName); + commands.put(baseName, tangoCommand); + } + pv.StartCommand(tangoCommand.getCommandName()); + } + + public void removeTangoCommand(String baseName) throws Exception { + TangoCommand tangoCommand = commands.get(baseName); + if (tangoCommand == null){ + PV.logger.log(Level.WARNING, "Could not remove Tango command \"" + baseName + + "\" due to no internal record of command"); + throw new Exception("Tango command remove failed: no command record."); + } + commands.remove(baseName, tangoCommand); + } + + public void executeTangoCommand(String baseName, Object new_value, TangoCmd_PV pv) throws Exception { + TangoCommand tangoCommand = commands.get(baseName); + if (tangoCommand == null){ + PV.logger.log(Level.WARNING, "Could not find Tango command \"" + baseName + + "\" due to no internal record of command"); + throw new Exception("Tango command execute failed: no command record."); + } + + CommandTangoType typeFromTango = CommandTangoType.getTypeFromTango(tangoCommand.getArginType()); + Object res; + VType value; + switch (typeFromTango){ + case DEVBOOLEAN: + res = tangoCommand.execute(Boolean.class, new_value); + value = TangoTypeUtil.convert(res, VBoolean.class); + pv.endCommand(value); + break; + case DEVSHORT: + res = tangoCommand.execute(Short.class, new_value); + value = TangoTypeUtil.convert(res, VShort.class); + pv.endCommand(value); + break; + case DEVLONG64: + res = tangoCommand.execute(Long.class, new_value); + value = TangoTypeUtil.convert(res, VLong.class); + pv.endCommand(value); + break; + case DEVFLOAT: + res = tangoCommand.execute(Float.class, new_value); + value = TangoTypeUtil.convert(res, VFloat.class); + pv.endCommand(value); + break; + case DEVDOUBLE: + res = tangoCommand.execute(Double.class,new_value); + value = TangoTypeUtil.convert(res, VDouble.class); + pv.endCommand(value); + break; + case DEVSTRING: + res = tangoCommand.execute(String.class,new_value); + value = TangoTypeUtil.convert(res, VString.class); + pv.endCommand(value); + break; + case DEVLONG: + res = tangoCommand.execute(Integer.class,new_value); + value = TangoTypeUtil.convert(res, VInt.class); + pv.endCommand(value); + break; + case DEVUCHAR: + res = tangoCommand.execute(Byte.class,new_value); + value = TangoTypeUtil.convert(res, VByte.class); + pv.endCommand(value); + break; + default: + throw new IllegalArgumentException("Value " + new_value + " cannot be converted."); + } + + + + } + + + +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PV.java b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PV.java new file mode 100644 index 0000000000..ad58a0cabf --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PV.java @@ -0,0 +1,64 @@ +package org.phoebus.pv.tgc; + +import org.epics.vtype.VType; +import org.phoebus.pv.PV; + +import java.util.logging.Level; + +public class TangoCmd_PV extends PV { + private final String baseName; + private String device; + private String command; + public TangoCmd_PV(String name, String baseName) throws Exception { + super(name); + this.baseName = baseName; + parseRawName(baseName); + TangoCmdContext.getInstance().createTangoCommand(device, command, baseName, this); + } + + private void parseRawName(final String name) throws Exception { + int pos = name.lastIndexOf('/'); + if (pos <= 0) + throw new Exception("Invalid input:" + name); + //Locate device name + device = name.substring(0,pos); + //Locate tango command + command = name.substring(pos+1); + } + + @Override + protected void close() + { + try + { + TangoCmdContext.getInstance().removeTangoCommand(baseName); + } + catch (Exception ex) + { + logger.log(Level.WARNING, "Failed to unsubscribe Tango Command from base name " + baseName); + ex.printStackTrace(); + } + } + + @Override + public void write(final Object new_value) throws Exception{ + if (new_value == null) + throw new Exception(getName() + " got null"); + TangoCmdContext.getInstance().executeTangoCommand(baseName, new_value, this); + } + + + + + public void StartCommand(final String commandName) { + notifyListenersOfValue(VType.toVType(commandName)); + } + + /** + Return the result after the command is executed。 + */ + public void endCommand(final VType value) { + notifyListenersOfValue(value); + } + +} diff --git a/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PVFactory.java b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PVFactory.java new file mode 100644 index 0000000000..d22668b1be --- /dev/null +++ b/core/pv/src/main/java/org/phoebus/pv/tgc/TangoCmd_PVFactory.java @@ -0,0 +1,21 @@ +package org.phoebus.pv.tgc; + +import org.phoebus.pv.PV; +import org.phoebus.pv.PVFactory; + +public class TangoCmd_PVFactory implements PVFactory { + + /** PV type implemented by this factory */ + final public static String TYPE = "tgc"; + + @Override + public String getType() { + return TYPE; + } + + @Override + public PV createPV(String name, String base_name) throws Exception { + return new TangoCmd_PV(name, base_name); + } + +} diff --git a/core/pv/src/main/resources/META-INF/services/org.phoebus.pv.PVFactory b/core/pv/src/main/resources/META-INF/services/org.phoebus.pv.PVFactory index 85c53bf83e..a63b206b0a 100644 --- a/core/pv/src/main/resources/META-INF/services/org.phoebus.pv.PVFactory +++ b/core/pv/src/main/resources/META-INF/services/org.phoebus.pv.PVFactory @@ -5,4 +5,6 @@ org.phoebus.pv.loc.LocalPVFactory org.phoebus.pv.pva.PVA_PVFactory org.phoebus.pv.opva.PVA_PVFactory org.phoebus.pv.mqtt.MQTT_PVFactory -org.phoebus.pv.formula.FormulaPVFactory \ No newline at end of file +org.phoebus.pv.formula.FormulaPVFactory +org.phoebus.pv.tga.TangoAttr_PVFactory +org.phoebus.pv.tgc.TangoCmd_PVFactory \ No newline at end of file diff --git a/dependencies/phoebus-target/pom.xml b/dependencies/phoebus-target/pom.xml index 8c85b84c3d..f0c52a9441 100644 --- a/dependencies/phoebus-target/pom.xml +++ b/dependencies/phoebus-target/pom.xml @@ -546,6 +546,13 @@ 1.7.28 + + + org.tango-controls + JTango + 9.7.0 + pom +