WIP: Common interface and class registry for all devices in libDevCom
To-Do
-
Tree-like structure for the JSON configuration, to make it simpler to express the device hierarchies -
Update relevant READMEs for describing the labRemote equipment configuration schema -
Get rid of the type
field inIDevice
What
- Adding the IDevice interface for all devices in
libDevCom
- Backwards compatibility
- Changes to EquipConf
- Updates to labRemote JSON schema and equipment configuration
- New examples showing usage
- Related Issues and MR
libDevCom
IDevice Interface for This MR introduces the lightweight IDevice
interface:
class IDevice {
public:
IDevice(const std::string& name, const std::string& type);
// methods for passing child-devices and/or communication devices
virtual void setComList(std::vector<std::shared_ptr<IDevice>> comlist); // for devices that have multiple child devices
virtual void setCom(std::shared_ptr<IDevice> com); // for device that uses a single child device
virtual void setCom(std::shared_ptr<ICom> com); // for devices that take a serial device directly
// method for passing the configuration for this specific device
virtual void setConfiguration(const nlohmann::json& config);
};
The MR updates the following base device classes in libDevCom
(and all devices that inherit from them):
ADCDevice
DACDevice
CalibratedDevice
ClimateSensor
IOExpander
MuxDevice
-
MPSSEChip
(this MR renamesMPSSEChip
toMPSSEDevice
to follow the usual naming convention)
For all devices that inherit from the above device types, new constructors are added to match the following pattern (exampled using ADCDevice
):
// ADCDevice.h
class ADCDevice : public IDevice {
public :
ADCDevice(const std::string& name, const std::string& type)
: IDevice(name, type) {}
};
// ADCDeviceImpl.h
class ADCDeviceImpl : public ADCDevice {
public:
ADCDeviceImpl(const std::string& name);
virtual ~ADCDeviceImpl() = default;
// IDevice interface
virtual void setCom(std::shared_ptr<DeviceCom> com);
virtual void setConfiguration(const nlohmann::json& config);
// ADCDevice interface below
...
private :
std::shared_ptr<I2CCom> m_com;
};
// ADCDeviceImpl.cpp
ADCDeviceImpl::ADCDeviceImpl(const std::string& name)
: ADCDevice(name, "ADCDeviceImpl")
,m_com(nullptr) {}
void ADCDeviceImpl::setCom(std::shared_ptr<DeviceCom> com) {
std::shared_ptr<I2CCom> i2c = std::dynamic_pointer_cast<I2CCom>(com);
if(!i2c) {
throw (...);
}
m_com = i2c;
}
void ADCDeviceImpl::setConfiguration(const nlohmann::json& config) {
// voltage reference
if(config.find("reference") != config.end()) {
m_voltage_reference = config["reference"].get<float>();
}
// retrieve other configuration parameters
// store the config
IDevice::setConfiguration(config);
}
With these changes, all devices appearing under libDevCom
can be entirely retrieved following the usual pattern:
auto device = std::make_shared<...>(name);
device->setCom(...);
device->setConfiguration(...)
Compatibility with Previous DevCom
This MR does not remove the "old-style" constructors, in which each device had it's old, many-parametered custom constructors.
Updates to EquipConf
This MR updates EquipConf
and how equipment of various types are retrieved.
The MR updates the getX(const std::string& name)
methods such that there is one for each of the
libX
's that appear in labRemote and that have support in EquipConf
:
-
libPS
->getPowerSupply
(getPowerSupplyConf
) andgetPowerSupplyChannel
(getPowerSupplyChannelConf
) -
libMeter
->getMeter
(getMeterConf
) -
libChiller
->getChiller
(getChillerConf
) -
libDevCom
->getDevice
(getDeviceConf
)
Before this MR, there was only the singular getDevice
which would handle all cases of equipment but now there is one of these methods for each of the supported device classes in labRemote. This is more explicit and clear from a user side in which
declaring intent is important.
With the devices in libDevCom
having a class registry, we can then do the usual device initialization using EquipConf
:
EquipConf ec;
ec.setHardwareConfig(config);
# get an ADCDevice object by casting the returned std::shared_ptr<IDevice> instance
auto adc = std::dynamic_pointer_cast<ADCDevice>(hw.getDevice(adc_name));
# get a power supply instance
auto power_supply = hw.getPowerSupply(ps_name);
Updates to the labRemote JSON schema for specifying equipment
Changes have been made to the labRemote JSON schema to harmonize the
equipment configuration. Previously there was only the devices
array, under which
all devices existed. Prior to this MR the only devices that could be configured
via EquipConf
were power supplies, meters, and chillers, which all have the same
configuration: name
, hw-type
, hw-model
, and communication
.
With the inclusion of libDevCom
to the list of things retrieved by EquipConf
,
the JSON schema and how a user declares their objects changes as well.
This MR replaces the previous devices
array with arrays for each
of the classes of equipment configured by EquipConf
:
{
"devices":[...],
"power-supplies": [...],
"power-supply-channels": [...],
"chillers": [...],
"meters": [...],
...
}
with the configuration for datasinks
and datastreams
left untouched.
A complete example of a configuration under this new schema is below:
Click to show full JSON example
{
"devices": [
{
"name": "FT232H-0",
"type": "FT232H",
"communication": "",
"config": {
"protocol": "I2C",
"speed": "400kHz",
"endianness": "MSB",
"id": {
"description": "",
"serial": ""
}
}
},
{
"name": "FTDICom-0",
"type": "I2CFTDICom",
"communication": "FT232H-0",
"config": {
"address": 33
}
},
{
"name": "ADC-0",
"type": "AD799X",
"communication": "FTDICom-0",
"config": {
"reference": 5.0,
"model": "AD7998"
}
},
{
"name": "ArduinoADC",
"type": "ADCDevComuino",
"communication": {
"protocol": "TextSerialCom",
"termination": "\n",
"baudrate": "B9600",
"port": "/dev/ttyACM0"
},
"config": {
"reference": 5.0
}
},
{
"name": "NTC-0",
"type": "NTCSensor",
"communication": "ArduinoADC",
"config": {
"channel": 1,
"Vsup": 2.5,
"Tref": 273.15,
"Rref": 10000,
"Rdiv": 10000,
"Bntc": 24913
}
},
{
"name": "EnvSensor",
"type": "HIH4000",
"communication": ["ADC-0", "NTC-0"],
"config": {
"Vsup": 5.0,
"channel": 0,
"calibration": {
"offset": 0.85,
"slope": 0.03
}
}
}
],
"power-supplies": [
{
"name": "PS-0",
"hw-type": "PS",
"hw-model": "RS_HMP4040",
"communication": {
"protocol": "TextSerialCom",
"termination": "\n",
"baudrate": "B9600",
"port": "/dev/ttyACM0"
}
}
],
"power-supply-channels": [
{
"name": "low-voltage",
"hw-type": "PS",
"device": "PS-0",
"channel": 1
}
],
"meters": [
{
"name": "Meter-0",
"hw-type": "Meter",
"hw-model": "DMM6500",
"communication": {
"protocol": "TextSerialCom",
"termination": "\n",
"port": "/dev/ttyACM0"
}
}
],
"chillers": [
]
}
-
name
: A user-provided, arbitrary name -
type
: The name of the C++ class that inherits fromIDevice
that is to be instantiated -
communication
: Can be one of:- A name (
string
) of another device appearing underdevices
that is an instance of a class inheriting fromIDevice
(e.g.DeviceCom
) - An array/list of multiple devices that appear under
devices
, each an instance of a class inheriting fromIDevice
(e.g.DeviceCom
) - A JSON object specifying the configuration of a
libCom
class, in the same way as is done already for power supplies
- A name (
-
config
: An arbitrary JSON object of key-value pairs that this device uses for configuration
If the communication
field of a given device is a name (or list of names) of another device, that device
must exist in the same JSON configuration under devices
. These devices will in turn be initialized (recursively)
by EquipConf
and then passed to the current device's setCom(std::shared_ptr<IDevice> com)
method to do as
needed by the device's implementation.
If the communication
field of a given device is a JSON object specifying {"protocol": ...}
(as for power supplies),
then EquipConf
will call createCommunication
for the given libCom
class and then pass that to the
current device's setCom(std::shared_ptr<ICom> com)
method.
For example, in the above JSON the device NTC-0
the communication
field specifies "ArduinoADC"
,
and in turn the ArduinoADC
device has for communication
and instance of TextSerialCom
. In this case,
when EquipConf::getDevice("NTC-0")
is called, it will first initialize the ArduinoADC
instance of ADCDevComuino
(internally calling EquipConf::getDevice("ArduinoADC")
), and then pass the ArduinoADC
to setCom(...)
of the NTC-0
instance.
Providing a list of device names to a device's communication
field is exampled in the above JSON
by the device named EnvSensor
: "communication": ["ADC-0", "NTC-0"]
. In this case,
calling EquipConf::getDevice("EnvSensor")
will initiate multiple calls of EquipConf::getDevice(...)
for the two devices specified in the communication
field and then pass those
each in turn to the setCom(...)
of the EnvSensor
object, whose implementation will need to know how to handle
the provided objects.
If a given device's communication
field is an empty string, as in the case of the device named FT232H-0
in
the above JSON, then EquipConf::getDevice
will not attempt to call setCom
on the device.
It is assumed in these cases that the device is fully initialized by its configuration parameters
in the config
field. This is the case for the USB-based devices, for example.
With the JSON configuration above, one would be able to do the following:
EquipConf equip_conf;
equip_conf.setHardwareConfig(config);
auto ntc = std::dynamic_pointer_cast<NTCSensor>(equip_conf.getDevice("NTC-0"));
ntc->read();
float temp = ntc->temperatue();
The call to getDevice("NTC-0")
does all of the work of instantiating the child devices needed
by the NTCSensor
instance (in this case, the ADCDevice
named "ArduinoADC"
).
New Examples
This MR adds two pairs of examples showing the usage of EquipConf
to load in devices:
-
NTCSensor
example:- ntc_equipconf_example.cpp (compare to: ntc_example.cpp)
- ntc_equipconf_example.py (compare to: ntc_example.py)
-
ADCDevice
readout viaFT232H
- ft232h_adc_equipconf_example.cpp (compare to: ft232h_adc_example.cpp)
- ft232h_adc_equipconf_example.py (compare to: ft232h_adc_example.py)
The NTCSensor
example shows a semi-complex labRemote configuration, with the NTCSensor
having
a child ADCDevice
device. The ADCDevice+FT232H
example shows an even more complex labRemote
configuration with multiple devices being instantiated by EquipConf
.
However, despite the complexities, the actual user code for retrieving the
devices described by the labRemote JSON configurations is trivial.
What's more, the user code is almost identical between the C++ and Python examples.