Squidstat API User Manual
Loading...
Searching...
No Matches
The Basics of Running Experiments

The basic building block of a custom experiment are the elements. An element is an elementary experiment such as Constant Voltage/Potential (CV) or Constant Current (CC). A custom experiment can have one or more elements. The elements inside could be run one or more times. A custom experiment can also contain another custom experiment as a sub-experiment. The sub-experiment can be run one or more times as well.

We will go through an example of building and running an experiment.

Creating A Custom Experiment

First, we will have some environment setup by creating our application:

#include "AisDeviceTracker.h"
#include "AisCustomExperiment.h"
#include "experiments/builder_elements/AisConstantCurrentElement.h"
#include "experiments/builder_elements/AisConstantPotElement.h"
char** test = nullptr;
int args;
QCoreApplication app(args, test);

To build a custom experiment, we need at least one element. In the example we will build below, we have two elements and a sub-experiment. The sub-experiment has the same two elements only with their parameters changed.

Let us go through it step by step:

We first create a constant voltage element and set its parameters as seen in the following code block. You can see a full list of the available elements in the classes section. For now, we are only setting the required parameters. You can get a complete list of settable parameters for any given element type by examining the corresponding element class.

// constructing a constant potential element with required arguments
5, // voltage: 5v
1, // sampling interval: 1s
10 // duration: 10s
);
an experiment that simulates a constant applied voltage.
Definition: AisConstantPotElement.h:15
Note
for each element you use, you need to include its corresponding header file.

We create another element of a different type.

// constructing a constant current element with required arguments
1, // current: 1A
1, // sampling interval: 1s
60 // duration: 60s
);
an experiment that simulates a constant current flow with more advance options for stopping the exper...
Definition: AisConstantCurrentElement.h:15

We create a custom experiment and add the previously created elements to it.

auto customExperiment = std::make_shared<AisExperiment>(); // at this point, it is an empty custom experiment, so, we add the elements we created to it.
customExperiment->appendElement(ccElement, 1); // append the CC element to the end of the experiment and set it to run 1 time
customExperiment->appendElement(cvElement, 1); // append the CV element to the end of the experiment and set it to run 1 time
Note
Elements are run in the order that they are added to the experiment

Next, we create a second experiment as a sub-experiment i.e. we are going to then add it to the main experiment.

auto subExperiment = std::make_shared<AisExperiment>(); // this line creates a custom experiment, intended to be used as a sub-subExperiment
subExperiment.appendElement(ccElement, 2); // append the CC element to the sub-experiment and set it to run 2 times
subExperiment.appendElement(cvElement, 3); // append the CV element to the sub-experiment and set it to run 3 times
customExperiment->appendSubExperiment(&subExperiment, 2); // append the sub-experiment to the main experiment and set the sub-experiment to run 2 times.

Again, the order adding/appending the elements and the sub-experiment here corresponds to the order at which they will run. The sub-experiment and the elements it contains will be run after the elements already added to the main experiment

We create an additional constant voltage element with a different voltage setpoint.

4, // voltage: 4v
1, // sampling interval: 1s
10 // duration: 10s
);

This concludes creating the experiment. Next is how to control the workflow of the experiment.

Controlling The Experiment

So far, we have only created the experiment. But we need to start it and control it. The next code section employs a callback mechanism specific to Qt, called signals and slots. Callbacks are used to take an action when a specified condition is met i.e. control the workflow. For simplicity, we provided some common slots related to our API with comments inside, on what you can do. You can read more about Qt signals and slots in the following link: https://doc.qt.io/qt-5/signalsandslots.html

Reading this document should still cover most of what is needed. Basically, a signal can be emitted when an event happens. If a slot is connected to that signal, whatever is inside that slot will be executed when the signal is emitted. You can think of a signal as a condition and a slot is what will be executed once a corresponding condition is met. The only difference is the order of execution. Normal execution have sequential order. However, a slot can be emitted at anytime. Whenever that happens, the slot will execute no matter where the connection has been made (as long as a connection has been made prior). That is how we can have extra control on how and when things are executed.

An experiment is run on a specific channel of a device. You may have more than one device connected. A single device has up to 4 channels. Any channel on a specific device can run a single experiment at a time. To start an experiment, we specify the device and the channel and, then start it. To stop or pause that experiment, we need to specify its corresponding device and channel. We need to keep track of the device and channel for each experiment we start so we can control it later.

We can control a device, including starting, pausing and stopping an experiment on a specific channel using an AisInstrumentHandler A device/instrument handler can be created given a device name that we detect.

We have two parts below: one that creates logic using signals and slots. The second part assigns that logic to an instrument handler which will discuss in a bit. The first part below is creating some control-flow logic that we can assign to a handler. We can also create other logics in the same way that can be assigned to different handlers which can be used to control different devices. If we only have one device, all the logic will be handled with one handler. We can then have further control within, based on channels.

Creating Control Flow Logic Specific To A Handler

The first part is a lambda function called "connectHandlerSignals" which takes a handler as an argument and connects some of the handler signals to slots. We have other signals related to a handler you can add, which you can find in the AisInstrumentHandler This example logic has four conditions on which we can perform other tasks. That is, when we assign this logic to a specific handler, this logic will execute for that handler. The four signals and slots below in the first part are examples for you to follow in order to add other connections.

auto connectHandlerSignals = [=](AisInstrumentHandler* handler) {
QObject::connect(handler, &AisInstrumentHandler::activeDCDataReady, [=](uint8_t channel, const AisDCData& data) {
// do something when DC data are received, such as writing to a CSV file output
// THIS IS WHERE YOU RECEIVE DC DATA FROM THE DEVICE
//example: print the data to the standard output as follows:
qDebug() << "channel: " << (int)channel << "current :" << data.current << " voltage: " << data.workingElectrodeVoltage << " counter electrode : " << data.counterElectrodeVoltage << " timestamp : " << data.timestamp;
});
QObject::connect(handler, &AisInstrumentHandler::activeACDataReady, [=](uint8_t channel, const AisACData&) {
// do something when AC data are received
// THIS IS WHERE YOU RECEIVE AC (EIS) DATA FROM THE DEVICE
});
QObject::connect(handler, &AisInstrumentHandler::experimentNewElementStarting, [=](uint8_t channel, const AisExperimentNode&) {
// do something when a new element is starting
// for example, print to the standard output: "New element starting"
qDebug() << "New element starting";
});
QObject::connect(handler, &AisInstrumentHandler::experimentStopped, [=](uint8_t channel) {
// do something when the experiment has stopped or has been stopped. For example, you can invoke starting the next step in your workflow
// print to the standard output: "Experiment stopped Signal "
qDebug() << "Experiment Stopped Signal " << channel;
});
};
this class provides control of the device including starting, pausing, resuming and stopping an exper...
Definition: AisInstrumentHandler.h:24
void activeACDataReady(uint8_t channel, const AisACData &ACData)
a signal that is emitted whenever new AC data for an active experiment are ready.
void experimentStopped(uint8_t channel)
a signal that is emitted whenever an experiment was stopped manually or has completed.
void activeDCDataReady(uint8_t channel, const AisDCData &DCData)
a signal that is emitted whenever new DC data for an active experiment are ready.
void experimentNewElementStarting(uint8_t channel, const AisExperimentNode &stepInfo)
a signal that is emitted whenever a new elemental experiment has started.
a structure containing AC data information.
Definition: AisDataPoints.h:40
a structure containing DC data information.
Definition: AisDataPoints.h:9
double counterElectrodeVoltage
the measured counter electrode voltage in volts.
Definition: AisDataPoints.h:24
double current
the measured electric current value in Amps
Definition: AisDataPoints.h:29
double workingElectrodeVoltage
the measured working electrode voltage in volts.
Definition: AisDataPoints.h:19
double timestamp
the time at which the DC data arrived.
Definition: AisDataPoints.h:14
a structure containing some information regarding the running element.
Definition: AisDataPoints.h:109

For a more complex logic for running a sequence of experiments, please refer to this example

If you would like to output the incoming data to a file such as a CSV file, you may modify the last block to something as follows:

QString filePath;
auto connectHandlerSignals = [=, &filePath](const AisInstrumentHandler* handler) {
QObject::connect(handler, &AisInstrumentHandler::experimentNewElementStarting, [=, &filePath](uint8_t channel, const AisExperimentNode& node) {
auto utcTime = handler->getExperimentUTCStartTime(0);
auto name = "/" + QString::number(node.stepNumber) + " " + node.stepName + " " + QString::number(utcTime) + ".csv";
filePath = (QString(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)) + name);
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) // overwrite existing files with the same name
return;
QTextStream out(&file);
out << "Time Stamp,"
<< "Counter Electrode Voltage,"
<< "Working Electrode Voltage,"
<< "Current"
<< "\n";
file.close();
qDebug() << "New element beginning: " << node.stepName << "step: " << node.stepNumber;
});
QObject::connect(handler, &AisInstrumentHandler::activeDCDataReady, [=, &filePath](uint8_t channel, const AisDCData& data) {
qDebug() << "current :" << data.current << " voltage: " << data.workingElectrodeVoltage << " counter electrode : " << data.counterElectrodeVoltage << " timestamp : " << data.timestamp;
QFile file(filePath);
if (!file.open(QIODevice::Append | QIODevice::WriteOnly | QIODevice::Text))
return;
QTextStream out(&file);
out << data.timestamp << ","
<< data.counterElectrodeVoltage << ","
<< data.workingElectrodeVoltage << ","
<< data.current
<< "\n";
file.close();
});
QObject::connect(handler, &AisInstrumentHandler::activeACDataReady, [=](uint8_t channel, const AisACData& data) {
qDebug() << data.frequency << " " << data.absoluteImpedance << " " << data.phaseAngle;
});
QObject::connect(handler, &AisInstrumentHandler::experimentStopped, [=](uint8_t channel) {
qDebug() << "Experiment Completed Signal " << channel;
});
};
double absoluteImpedance
the magnitude of the complex impedance.
Definition: AisDataPoints.h:55
double frequency
the applied frequency in Hz.
Definition: AisDataPoints.h:50
double phaseAngle
the phase angle between the real and the imaginary parts of the impedance.
Definition: AisDataPoints.h:70
QString stepName
This is the name of the current element running.
Definition: AisDataPoints.h:114
int stepNumber
this number is the order of the element within the custom experiment.
Definition: AisDataPoints.h:119

Here we output the DC data to a CSV file but, you may do that for AC data as well in the same manner.

You may also find it useful to refer to C++ lambdas documentation: https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp

Connecting Slots To Device-Tracker Signals

There are two signals related to a device tracker: when a device is connected and second, when a device is disconnected.

When a Device is Connected

This connects a slot to the device tracker's AisDeviceTracker::newDeviceConnected signal that provides the device name. Because we have the device name, we can create a device handler and do whatever a handler can do. In this slot example, we are creating a handler, assigning the previously created logic to this handler and then starting an experiment.

QObject::connect(tracker, &AisDeviceTracker::newDeviceConnected, &app, [=](const QString& deviceName) {
// Do something when a new device is detected to be connected. The device name is given in the variable 'deviceName'
// The following lines start the experiment that we created
auto handler = tracker->getInstrumentHandler(deviceName); // create a device handler using the given device name
connectHandlerSignals(handler); // connect the signals we created for the device. This is done once per device.
auto error = handler->uploadExperimentToChannel(0, customExperiment); // upload to a specific channel (first arg) an experiment (second arg) on the given
device controlled by the handler.
if (error) {
qDebug() << error.message();
return;
}
auto error = handler->startUploadedExperiment(0); // start the previously uploaded experiment on the given channel.
if (error) {
qDebug() << error.message();
return;
}
});
void newDeviceConnected(const QString &deviceName)
a signal to be emitted whenever a new connection has been successfully established with a device.

Please refer to AisInstrumentHandler for possible errors that may occur when performing operations such as uploading and starting an experiment. For example, when uploading an experiment, AisInstrumentHandler::uploadExperimentToChannel may return an AisErrorCode::InvalidParameters error if the parameters are out of range where you can display the message to check which parameter was not supported for your device.

Note
Specific to the cycler model, before starting an experiment, you have the option of linking channels so that you can share the electric current over multiple channels using AisInstrumentHandler::setLinkedChannels. If using paralleled channels, AisInstrumentHandler::setLinkedChannels MUST be called before each experiment that uses paralleled channels. To link channels on the cycler, you can modify the last code by first linking the channels, and then uploading and starting the experiment on the master channel for the linked channels:
auto masterchannel = handler->setLinkedChannels({ 0, 1 }); // this does two things, first links the given channels and second returns the masterchannel used to control the combined output.
handler->uploadExperimentToChannel(masterchannel, customExperiment);
handler->startUploadedExperiment(masterchannel);

When a Device is Disconnected

The following code connects a slot to the device tracker's AisDeviceTracker::deviceDisconnected signal with the device name.

QObject::connect(tracker, &AisDeviceTracker::deviceDisconnected, &app, [=](const QString& deviceName) {
// do something when a device has been disconnected. The device name is given in the variable 'deviceName'
// for example, print to the standard output that the device given is disconnected
qDebug() << deviceName << "is disconnected ";
});
void deviceDisconnected(const QString &deviceName)
a signal to be emitted whenever a device has been disconnected.

We still have not started the experiment, we've only created an experiment and setup callback functions via signals. When we connect a device using the tracker as shown below, the AisDeviceTracker::newDeviceConnected signal will be emitted with the device name. As a result, the slot we connected earlier to the signal AisDeviceTracker::newDeviceConnected will execute (connecting the other signals and running the experiment).

Note
in the example we showed, the function connectHandlerSignals is intentionally called inside the AisDeviceTracker::newDeviceConnected slot because connectHandlerSignals needs a valid handler. When AisDeviceTracker::newDeviceConnected is emitted, we know we can get a device handler for the newly connected device and then control the device with the handler.

Now to connect the device, the easiest way to connect all plugged-in devices is to call AisDeviceTracker::connectAllPluggedInDevices:

tracker->connectAllPluggedInDevices();

To connect specific devices, you may alternatively call AisDeviceTracker::connectToDeviceOnComPort with a specific COM port.

tracker->connectToDeviceOnComPort("COM3"); // change the port number to yours. For example, in windows, you can find it from the device manager

Finally, we can start the application by calling:

app.exec();

In the next section, we introduce a more advanced control flow.