Skip to content

WIP: Common interface and class registry for all devices in libDevCom

Daniel Joseph Antrim requested to merge dantrim_devcom_registry into devel

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 in IDevice

What

IDevice Interface for libDevCom

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 renames MPSSEChip to MPSSEDevice 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) and getPowerSupplyChannel (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": [
    ]
}
The configuration of `devices` (the class of objects under `libDevCom`) have the following fields to specify in the JSON configuration:
  • name: A user-provided, arbitrary name
  • type: The name of the C++ class that inherits from IDevice that is to be instantiated
  • communication: Can be one of:
    • A name (string) of another device appearing under devices that is an instance of a class inheriting from IDevice (e.g. DeviceCom)
    • An array/list of multiple devices that appear under devices, each an instance of a class inheriting from IDevice (e.g. DeviceCom)
    • A JSON object specifying the configuration of a libCom class, in the same way as is done already for power supplies
  • 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:

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.

Related MR and Issues

Edited by Daniel Joseph Antrim

Merge request reports