I work with electronics test equipment. I like to be able to automate tests using their remote control interfaces. I have built a pattern, a few base classes, that I can apply to all devices that follow the SCPI protocol. I would like to avoid half-assing the implementation like every time before. I wan't to bite the bullet and do this the right way once and for all. I do not use LabView so I can't just download their drivers.
I pounded out something that got me close, but it's not all the way there. If it looks like mad ramblings just ignore it. Any pointers on the right design pattern will really help!
I have designed it the way you see it because I want the usability to be nice and clean like:
double instantaneousCurrent = instrument.Measurement.Current.Instantaneous.Get();
or
double powerSetPoint = double.Parse(Console.ReadLine());
instrument.Load.ConstantPower.Set(powerSetPoint);
Each node in the would ideally put your statement another "." deeper down the instrument object.
Code:
SCPI Baseclass, Subsystem BaseClass, and SystemProperty BaseClass:
/// <summary>
/// Provides serial object creation services and ensures the correct device is on the other end.
/// </summary>
public abstract class ScpiDeviceBaseClass : IDisposable
{
private List<int> PossibleBaudRates = new List<int>() { 115200, 57600, 38400, 19200, 9600, 4800};
protected SerialPort serialPort;
protected string DeviceName;
#region Constructors
/// <summary>
/// Takes the device name and provided serial port and ensures the port is open and the device on the other end responds with the correct identifier.
/// </summary>
/// <param name="deviceName">A string that can be used to verify the connected device is the expected one by checking if <paramref name="deviceName"/> is contained in the response to an *IDN? request.</param>
/// <param name="port">A preconfigured <see cref="SerialPort"/>.</param>
public ScpiDeviceBaseClass(string deviceName, SerialPort port)
{
this.DeviceName = deviceName;
serialPort = port;
if (!serialPort.IsOpen)
{
try
{
serialPort.Open();
}
catch (UnauthorizedAccessException)
{
throw new Exception($"Failed to open {serialPort.PortName} because it was already open elsewhere.");
}
catch (Exception ex)
{
throw new Exception($"Failed to open {serialPort.PortName}. Reason: \r\n{ex.Message}");
}
}
serialPort.ReadTimeout = 2000;
CheckIdn();
}
/// <summary>
/// Takes the device name and provided serial port name (COM Port), creates a <see cref="SerialPort"/> using all possible baud rates until the device responds with a correct identifier.
/// </summary>
/// <param name="deviceName">A string that can be used to verify the connected device is the expected one by checking if <paramref name="deviceName"/> is contained in the response to an *IDN? request.</param>
/// <param name="portName">A string with the name of the COM port to be used i.e. "COM12".</param>
public ScpiDeviceBaseClass(string deviceName, string portName)
{
this.DeviceName = deviceName;
// Make sure the port name given looks valid.
if (portName == null || portName == "" || !portName.StartsWith("COM"))
{
if (portName == null)
{
portName = "null";
}
else if (portName == "")
{
portName = "blank";
}
throw new Exception($"Unable to connect to {DeviceName}. Bad port provided. Port provided was {portName}.");
}
ScanSelectedPortWithAllBaudRates(portName);
}
/// <summary>
/// Takes the device name and checks all available COM ports with all possible baud rates until the device responds with the correct identifier.
/// </summary>
/// <param name="deviceName">A string that can be used to verify the connected device is the expected one by checking if <paramref name="deviceName"/> is contained in the response to an *IDN? request.</param>
/// <param name="baudRate">An optional parameter that can be used to shorten the list of possible combinations to check.</param>
public ScpiDeviceBaseClass(string deviceName, int baudRate = -1)
{
this.DeviceName = deviceName;
if (baudRate != -1)
{
ScanPortsWithSelectedBaudRate(baudRate);
}
else
{
ScanAllPortsWithAllBaudRates();
}
}
/// <summary>
/// Ensures the device can respond with the current serial port configuration and that the response contains the expected response.
/// </summary>
private void CheckIdn()
{
int tries = 0;
// Try to read the identification of the device connected and ensure it is the device expected.
try
{
while (true)
{
try
{
ReadUntilInBufferEmpty();
serialPort.WriteLine("*IDN?");
string idn = serialPort.ReadLine();
if (!idn.Contains(DeviceName))
{
throw new Exception($"Identification request returned \"{idn}\". Are you connected to the correct com port?");
}
return;
}
catch (Exception)
{
if (tries > 3)
{
throw;
}
}
tries++;
}
}
catch (Exception ex)
{
if (ex is TimeoutException)
{
throw new Exception($"Connection to {DeviceName} failed due to read timeout.");
}
else if (ex is InvalidOperationException)
{
throw new Exception($"Error connecting to {DeviceName}. Error: " + ex.Message);
}
else
{
throw;
}
}
}
/// <summary>
/// Loops through all available COM ports and tries to connect using the specified baud rate.
/// </summary>
/// <param name="baudRate">The baud rate to check each COM port with.</param>
private void ScanPortsWithSelectedBaudRate(int baudRate)
{
string[] ports = SerialPort.GetPortNames();
if (ports.Count() == 0)
{
throw new Exception($"No com ports found to connect to {DeviceName}.");
}
// Try all ports using the specified baud rate.
for (int i = 0; i < ports.Count(); i++)
{
try
{
serialPort = new SerialPort(ports[0], baudRate);
serialPort.DtrEnable = true;
serialPort.RtsEnable = true;
serialPort.Open();
serialPort.ReadTimeout = 2000;
CheckIdn();
return ;
}
catch (Exception)
{
if (i == ports.Count() - 1)
{
throw;
}
}
}
}
/// <summary>
/// Loops through all available COM ports and tries to connect with all possible baud rates.
/// </summary>
private void ScanAllPortsWithAllBaudRates()
{
string[] ports = SerialPort.GetPortNames();
if (ports.Count() == 0)
{
throw new Exception($"No com ports found to connect to {DeviceName}.");
}
// Try all ports with any baud rate.
for (int i = 0; i < ports.Count(); i++)
{
try
{
// Try all possible baud rates on this port.
for (int j = 0; j < PossibleBaudRates.Count; j++)
{
try
{
int baud = PossibleBaudRates[j];
serialPort = new SerialPort(ports[0], baud);
serialPort.DtrEnable = true;
serialPort.RtsEnable = true;
serialPort.Open();
serialPort.ReadTimeout = 2000;
CheckIdn();
break;
}
catch (Exception)
{
serialPort.Dispose();
serialPort = null;
}
}
if (serialPort == null)
{
throw new Exception("Failed to connect to device.");
}
break;
}
catch (Exception)
{
if (i == ports.Count() - 1)
{
throw;
}
}
}
}
/// <summary>
/// Loops through all possible baud rates and tries to connect on the specified COM port.
/// </summary>
/// <param name="portName"></param>
private void ScanSelectedPortWithAllBaudRates(string portName)
{
// Try all possible baud rates on this port.
for (int j = 0; j < PossibleBaudRates.Count; j++)
{
try
{
int baud = PossibleBaudRates[j];
serialPort = new SerialPort(portName, baud);
serialPort.DtrEnable = true;
serialPort.RtsEnable = true;
serialPort.Open();
serialPort.ReadTimeout = 2000;
CheckIdn();
break;
}
catch (Exception)
{
serialPort.Dispose();
serialPort = null;
}
}
if (serialPort == null)
{
throw new Exception("Failed to connect to device.");
}
}
#endregion Constructors
/// <summary>
/// Read the comport until we are sure it is empty.
/// By reading the com port until we get a read timeout three times in a row we can feel confident there is no more data left to be read.
/// </summary>
private void ReadUntilInBufferEmpty()
{
int previousReadTimeout = serialPort.ReadTimeout;
serialPort.ReadTimeout = 100;
int timeoutExceptions = 0;
while (timeoutExceptions < 3)
{
try
{
serialPort.ReadLine();
// If the read does not fail we reset the counter.
timeoutExceptions = 0;
}
catch (TimeoutException)
{
// If the exception thrown is a timeout exception we increment the exception counter and then read again if we are not over the try limit of 3.
timeoutExceptions++;
}
catch (Exception)
{
serialPort.ReadTimeout = previousReadTimeout;
// If the exception thrown is not a read timeout exception we want to throw it up.
throw;
}
}
serialPort.ReadTimeout = previousReadTimeout;
}
public void Dispose()
{
if (this.serialPort != null)
{
this.serialPort.Dispose();
this.serialPort = null;
}
}
/// <summary>
/// Used to represent all nodes in an SCPI command tree that are not leafs.
/// This makes the case where a non-leaf node is a request clunky. How can I fix this?
/// </summary>
public abstract class DeviceSubsystem
{
// Used to quickly identify a root node.
protected bool IsRoot
{
get
{
if (this.Parent == null)
{
return true;
}
else
{
return false;
}
}
}
// The parent of this node.
protected DeviceSubsystem Parent;
// This nodes children.
protected List<DeviceSubsystem> Children;
// This node's name.
protected string NodeName;
// The serial port to use.
protected SerialPort ComPort;
public DeviceSubsystem(string nodeName, DeviceSubsystem parent, SerialPort comPort)
{
NodeName = nodeName;
Parent = parent;
ComPort = comPort;
Children = new List<DeviceSubsystem>();
}
/// <summary>
/// Used to represent all nodes in the SCPI command tree that are leafs.
/// This leaf can be written to and read from.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public abstract class SubSystemProperty<PropertyType> : SubSystemPropertyBase<PropertyType>, SubSystemReader<PropertyType>, SubSystemWritter<PropertyType>
{
/// <summary>
/// Creates a leaf node that can read and write.
/// </summary>
/// <param name="name">The name of the node. This is used to create commands so it must match the exact text the device expects at this node.</param>
/// <param name="parent">This nodes <see cref="DeviceSubsystem"/> parent.</param>
/// <param name="isRootResquest"><para>Indicates that this is not really a leaf, but rather the request implementation of a <see cref="DeviceSubsystem"/>. Some non-leaf nodes are also requests. The current implementation does not handle this case gracefully. This is a hack.</para></param>
public SubSystemProperty(string name, DeviceSubsystem parent, bool isRootResquest = false) : base(name, parent, isRootResquest)
{
}
public PropertyType Get()
{
string response = GetString(Name);
return ConvertToType(response);
}
public void Set(PropertyType value)
{
string valueString = value.ToString();
// If the value is a bool we need to represent it differently.
if (value.GetType() == typeof(bool))
{
bool boolValue = bool.Parse(valueString);
if (boolValue == true)
{
valueString = "1";
}
else
{
valueString = "0";
}
}
SetString(this.Name, valueString);
}
}
/// <summary>
/// Used to represent all nodes in the SCPI command tree that are leafs.
/// This leaf can only be read from.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public abstract class SubsytemPropertyReadonly<PropertyType> : SubSystemPropertyBase<PropertyType>, SubSystemReader<PropertyType>
{
/// <summary>
/// Creates a leaf node that can read.
/// </summary>
/// <param name="name">The name of the node. This is used to create commands so it must match the exact text the device expects at this node.</param>
/// <param name="parent">This nodes <see cref="DeviceSubsystem"/> parent.</param>
/// <param name="isRootResquest"><para>Indicates that this is not really a leaf, but rather the request implementation of a <see cref="DeviceSubsystem"/>. Some non-leaf nodes are also requests. The current implementation does not handle this case gracefully. This is a hack.</para></param>
public SubsytemPropertyReadonly(string name, DeviceSubsystem parent, bool isRootResquest = false) : base(name, parent, isRootResquest)
{
}
public PropertyType Get()
{
string response = GetString(Name);
return ConvertToType(response);
}
}
/// <summary>
/// Used to represent all nodes in the SCPI command tree that are leafs.
/// This leaf can only be written to.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public abstract class SubSystemPropertyWriteOnly<PropertyType> : SubSystemPropertyBase<PropertyType>, SubSystemWritter<PropertyType>
{
/// <summary>
/// Creates a leaf node that can write.
/// </summary>
/// <param name="name">The name of the node.</param>
/// <param name="parent">This nodes <see cref="DeviceSubsystem"/> parent.</param>
/// <param name="isRootResquest"><para>Indicates that this is not really a leaf, but rather the request implementation of a <see cref="DeviceSubsystem"/>. Some non-leaf nodes are also requests. The current implementation does not handle this case gracefully. This is a hack.</para></param>
public SubSystemPropertyWriteOnly(string name, DeviceSubsystem parent, bool isRootResquest = false) : base(name, parent, isRootResquest)
{
}
public void Set(PropertyType value)
{
string valueString = value.ToString();
if (value.GetType() == typeof(bool))
{
bool boolValue = bool.Parse(valueString);
if (boolValue == true)
{
valueString = "1";
}
else
{
valueString = "0";
}
}
SetString(this.Name, valueString);
}
}
/// <summary>
/// Provides the ability to write to leafs.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public interface SubSystemWritter<PropertyType>
{
void Set(PropertyType value);
}
/// <summary>
/// Provides the ability to read from leafs.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public interface SubSystemReader<PropertyType>
{
PropertyType Get();
}
/// <summary>
/// Provides the basic leaf functionality.
/// </summary>
/// <typeparam name="PropertyType">The <see cref="Type"/> of this leaf.</typeparam>
public abstract class SubSystemPropertyBase<PropertyType>
{
protected string Name;
protected DeviceSubsystem Parent;
/// <summary>
/// Creates the base leaf node. It can neither read nor write.
/// </summary>
/// <param name="name">The name of the node. This is used to create commands so it must match the exact text the device expects at this node.</param>
/// <param name="parent">This nodes <see cref="DeviceSubsystem"/> parent.</param>
/// <param name="isRootResquest"><para>Indicates that this is not really a leaf, but rather the request implementation of a <see cref="DeviceSubsystem"/>. Some non-leaf nodes are also requests. The current implementation does not handle this case gracefully. This is a hack.</para></param>
public SubSystemPropertyBase(string name, DeviceSubsystem parent, bool isRootResquest = false)
{
Parent = parent;
if (!isRootResquest)
{
// If isRootRequest is false, we just use the provided name.
Name = name;
}
else
{
// If isRootRequest is true, we use an empty name. This fixes the case where a non-leaf node can make requests. This is a hack.
Name = "";
}
}
#region ResponseConversions
protected PropertyType ConvertToType(string response)
{
switch (Type.GetTypeCode(typeof(PropertyType)))
{
case TypeCode.Boolean:
{
return (PropertyType)Convert.ChangeType(ToBool(response), typeof(PropertyType));
}
case TypeCode.Int16:
{
return (PropertyType)Convert.ChangeType(ToInt(response), typeof(PropertyType));
}
case TypeCode.UInt16:
{
return (PropertyType)Convert.ChangeType(ToUint(response), typeof(PropertyType));
}
case TypeCode.Int32:
{
return (PropertyType)Convert.ChangeType(ToInt(response), typeof(PropertyType));
}
case TypeCode.UInt32:
{
return (PropertyType)Convert.ChangeType(ToUint(response), typeof(PropertyType));
}
case TypeCode.Double:
{
return (PropertyType)Convert.ChangeType(ToDouble(response), typeof(PropertyType));
}
case TypeCode.String:
{
return (PropertyType)Convert.ChangeType(response, typeof(PropertyType));
}
default:
throw new Exception($"Invalid type of '{typeof(PropertyType).Name}' specified in SubsytemProperty.");
}
}
private bool ToBool(string response)
{
if (response == null || response == "")
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Null or empty value passed to {method.Name}.");
}
if (response == "0")
{
return false;
}
else if (response == "1")
{
return true;
}
else
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Invalid value of '{response}' passed to {method.Name}.");
}
}
private double ToDouble(string response)
{
if (response == null || response == "")
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Null or empty value passed to {method.Name}.");
}
if (double.TryParse(response, out double value))
{
return value;
}
else
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Invalid value of '{response}' passed to {method.Name}.");
}
}
private int ToInt(string response)
{
if (response == null || response == "")
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Null or empty value passed to {method.Name}.");
}
if (int.TryParse(response, out int value))
{
return value;
}
else
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Invalid value of '{response}' passed to {method.Name}.");
}
}
private uint ToUint(string response)
{
if (response == null || response == "")
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Null or empty value passed to {method.Name}.");
}
if (uint.TryParse(response, out uint value))
{
return value;
}
else
{
MethodBase method = System.Reflection.MethodBase.GetCurrentMethod();
throw new ArgumentException($"Invalid value of '{response}' passed to {method.Name}.");
}
}
#endregion ResponseConversions
#region Messaging
protected string GetString(string command)
{
string fullCommand = CreateFullCommand(command);
Write(fullCommand);
string response = Read();
return response;
}
protected void SetString(string command, string value)
{
string fullCommand = CreateFullCommand(command, value);
Write(fullCommand);
}
private string ConvertBoolToValue(bool boolValue)
{
if (boolValue == true)
{
return "1";
}
else
{
return "0";
}
}
private string CreateFullCommand(string command, string value = null)
{
StringBuilder sb = new StringBuilder();
// Get the path from this subsystem to the root. This is used to create the full command string.
List<DeviceSubsystem> pathToRoot = new List<DeviceSubsystem>();
DeviceSubsystem currentSubSystem = this.Parent;
// starting from the subsystem we are in check to see if we are at the root, if no look at our parent.
while (true)
{
pathToRoot.Add(currentSubSystem);
if (currentSubSystem.IsRoot)
{
break;
}
else
{
currentSubSystem = currentSubSystem.Parent;
}
}
// Reverse the list so we go from root to the node of interest.
pathToRoot.Reverse();
foreach (DeviceSubsystem subsystem in pathToRoot)
{
sb.Append($":{subsystem.NodeName}");
}
// Add our command to the end of our request.
if (value == null)
{
// if the value param is null this is a request for data
sb.Append($":{command}?");
}
else
{
// if the value param is not null we are sending information to be set
sb.Append($":{command} {value}");
}
return sb.ToString();
}
private void Write(string message)
{
int tries = 0;
while (true)
{
try
{
this.Parent.ComPort.WriteLine(message);
break;
}
catch (Exception)
{
tries++;
if (tries > 3)
{
throw;
}
}
}
}
private string Read()
{
int tries = 0;
while (true)
{
try
{
string response = this.Parent.ComPort.ReadLine();
return response;
}
catch (Exception)
{
tries++;
if (tries > 3)
{
throw;
}
}
}
}
#endregion Messaging
}
}
}
Implementation:
public class TestInstrument : ScpiDeviceBaseClass
{
#region Constructors
public MeasurmentsSubSystem Measurement;
public LoadSubSystem Load;
public TestInstrument(SerialPort port) : base("TestInstrumentIdentifier", port)
{
ConstructSystemRootNodes();
}
public TestInstrument(string portName) : base("TestInstrumentIdentifier", portName)
{
ConstructSystemRootNodes();
}
public TestInstrument() : base("TestInstrumentIdentifier", 57600)
{
ConstructSystemRootNodes();
}
/// <summary>
/// Handles the construction of all root subsystem nodes.
/// </summary>
private void ConstructSystemRootNodes()
{
Measurement = new MeasurmentsSubSystem("Measure", null, this.serialPort);
Load = new LoadSubSystem("Load", null, this.serialPort);
}
#endregion Constructors
/// <summary>
/// Represents the measurements subsystem.
/// </summary>
public class MeasurmentsSubSystem : DeviceSubsystem
{
#region Nodes
/// <summary>
/// The sub-subsystem of measurements, the current measurement subsystem.
/// </summary>
public CurrentMeasurements Current;
#endregion Nodes
#region Constructors
/// <summary>
/// Constructs this subsystem.
/// </summary>
/// <param name="nodeName">The node name is used to construct commands sent to the device.</param>
/// <param name="parent">This subsystem's parent subsystem. Null if this is a root node.</param>
/// <param name="serialPort">The serial port used to communicate with the device.</param>
public MeasurmentsSubSystem(string nodeName, DeviceSubsystem parent, SerialPort serialPort) : base(nodeName, parent, serialPort)
{
ConstructChildren();
ConstructLeafs();
}
/// <summary>
/// Used to construct all non-leaf nodes below this subsystem.
/// </summary>
protected override void ConstructChildren()
{
this.Children.Add(Current = new CurrentMeasurements("Current", this, this.serialPort));
}
/// <summary>
/// Used to construct all leaf nodes directly below this subsystem.
/// </summary>
protected override void ConstructLeafs()
{
}
#endregion Constructors
#region DeviceSubsystem Node Definitions
/// <summary>
/// Represents the subsystem responsible for current measurement.
/// </summary>
public class CurrentMeasurements : DeviceSubsystem
{
#region Nodes
/// <summary>
/// The Amplitude measurement subsystem.
/// </summary>
public AmplitudeMeasurements Amplitude;
/// <summary>
/// Used to access the instantaneous current measurement.
/// </summary>
public Instantaneous instantaneous;
#endregion Nodes
#region Constructors
/// <summary>
/// Constructs this subsystem.
/// </summary>
/// <param name="nodeName">The node name is used to construct commands sent to the device.</param>
/// <param name="parent">This subsystem's parent subsystem. Null if this is a root node.</param>
/// <param name="serialPort">The serial port used to communicate with the device.</param>
public CurrentMeasurements(string nodeName, DeviceSubsystem parent, SerialPort serialPort) : base(nodeName, parent, serialPort)
{
ConstructChildren();
ConstructLeafs();
}
/// <summary>
/// Used to construct all non-leaf nodes below this subsystem.
/// </summary>
protected override void ConstructChildren()
{
this.Children.Add(Amplitude = new AmplitudeMeasurements("Amplitude", this, this.serialPort));
}
/// <summary>
/// Used to construct all leaf nodes directly below this subsystem.
/// </summary>
protected override void ConstructLeafs()
{
instantaneous = new Instantaneous(this);
}
#endregion Constructors
#region DeviceSubsystem Node Definitions
public class AmplitudeMeasurements : DeviceSubsystem
{
#region Nodes
public Hold hold;
public Max max;
#endregion Nodes
#region Constructors
public AmplitudeMeasurements(string nodeName, DeviceSubsystem parent, SerialPort comPort) : base(nodeName, parent, comPort)
{
ConstructChildren();
ConstructLeafs();
}
/// <summary>
/// Used to construct all non-leaf nodes below this subsystem.
/// </summary>
protected override void ConstructChildren()
{
}
/// <summary>
/// Used to construct all leaf nodes directly below this subsystem.
/// </summary>
protected override void ConstructLeafs()
{
hold = new Hold(this);
max = new Max(this);
}
#endregion Constructors
#region SubSystemProperty Node Definitions
public class Hold : SubSystemProperty<bool>
{
public Hold(DeviceSubsystem parent) : base(typeof(Hold).Name, parent) { }
}
public class Max : SubSystemProperty<double>
{
public Max(DeviceSubsystem parent) : base(typeof(Max).Name, parent) { }
}
#endregion SubSystemProperty Node Definitions
}
#endregion DeviceSubsystem Node Definitions
#region SubsytemProperty Node Definitions
public class Instantaneous : SubsytemPropertyReadonly<double>
{
public Instantaneous(DeviceSubsystem parent) : base(typeof(Instantaneous).Name, parent, true) { }
}
#endregion SubsytemProperty Node Definitions
}
#endregion DeviceSubsystem Node Definitions
}
public class LoadSubSystem : DeviceSubsystem
{
#region Nodes
public Status status;
#endregion Nodes
#region Constructors
public LoadSubSystem(string nodeName, DeviceSubsystem parent, SerialPort comPort) : base(nodeName, parent, comPort)
{
ConstructChildren();
ConstructLeafs();
}
/// <summary>
/// Used to construct all non-leaf nodes below this subsystem.
/// </summary>
protected override void ConstructChildren()
{
}
/// <summary>
/// Used to construct all leaf nodes directly below this subsystem.
/// </summary>
protected override void ConstructLeafs()
{
status = new Status(this);
}
#endregion Constructors
#region SubSystemProperty Node Definitions
public class Status : SubSystemProperty<bool>
{
public Status(DeviceSubsystem parent) : base(typeof(Status).Name, parent) { }
}
#endregion SubSystemProperty Node Definitions
}
}