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 extends VType> 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
+