API guide¶
How to do the common jobs with the driver: identify, read, command, tare, set up a bus and handle errors.
Every snippet on this page is compiled by the host CI from
tests/docs/api_tasks.cpp, with Driver standing for
alicat_basis2::Driver<MyUart> and al for alicat_basis2. The API reference lists every
member.
The shape of the API¶
One Driver<UartT> object per instrument. Its calls fall into a few groups:
| Group | Calls | Instruments |
|---|---|---|
| Live data | ReadInstantaneous() |
All |
| Identity | ReadIdentity(); the free function GetDriverVersion() for the driver |
All |
| Setpoint | SetSetpoint(), SetSetpointSource(), SetCommWatchdogMs(), SetMaxSetpointRamp() |
Controllers |
| Tare | Tare(), SetAutotareEnabled() (controllers) |
All |
| Gas and flow processing | SetGas(), SetFlowAveragingMs(), SetReferenceTemperatureC() |
All |
| Totalizer | ResetTotalizer(), SetTotalizerLimitMode(), SetTotalizerBatch() (controllers) |
All |
| Measurement window | ConfigureMeasurementTrigger() (controllers), StartMeasurementSamples(), ReadMeasurement() |
All |
| Bus configuration | SetModbusAddress(), SetAsciiUnitId(), SetBaudRate(), FactoryRestore(), DiscoverPresentAddresses() |
All |
| Raw access | ReadHoldingRegisters(), WriteSingleRegister(), WriteMultipleRegisters() |
All |
| Driver settings | SetAddress(), GetAddress(), SetTimeoutMs(), GetTimeoutMs(), SetBusBaudRate(), GetBusBaudRate() |
Every call is noexcept and returns DriverResult<T> (.ok(), .value, .error) or, for the raw read, a bare
DriverError. Check it: a failed call leaves value default-constructed.
Talk to several instruments¶
Driver flow_in(uart, 1); // one Driver per instrument, all on the same port
Driver flow_out(uart, 2, 100); // a 100 ms response timeout for this one
flow_out.SetTimeoutMs(250); // change it later
The constructor sends nothing. The port must outlive every driver that uses it. Drivers that share a port must not run at the same time (Porting).
Identify the instrument¶
const auto r = meter.ReadIdentity(); // registers 25, 26..31 and 45..49: three requests
if (!r.ok()) {
return false;
}
id = r.value;
const double full_scale = id.full_scale_flow_raw / 1000.0; // registers 47-48: value / 1000
std::printf("FW %u.%u.%u SN %s full scale %.3f %s\n", id.fw_major, id.fw_minor, id.fw_patch,
id.serial_number, full_scale, al::ToString(id.flow_units).data());
ReadIdentity() costs three requests. Read it once at start-up and keep the result: the flow units and full
scale tell you how to interpret every flow value.
Read live data¶
const auto r = meter.ReadInstantaneous(3); // 3: the instrument's factory-set flow decimals
if (r.ok()) {
const al::InstantaneousData& d = r.value;
std::printf("%s %.2f C flow %.3f total %.3f setpoint %.3f valve %.1f %%\n",
al::GasShortName(d.gas).data(), static_cast<double>(d.temperature_c),
static_cast<double>(d.flow), static_cast<double>(d.total_volume),
static_cast<double>(d.setpoint), static_cast<double>(d.valve_drive_pct));
if (d.mass_flow_overrange || d.temperature_overrange || d.totalizer_overrange) {
std::printf("over range\n");
}
}
One request reads registers 2100 to 2109. Temperature and valve drive are always value / 100. Flow, total,
setpoint and batch are divided by 10decimals, where decimals (default 3) is the factory-set number of
decimal places of the instrument. The manual names no register that reports it: check one reading against the
ASCII data frame or the calibration sheet and fix the value in your code. The raw register values are in the
raw_* fields for logging.
Command a setpoint¶
// Controllers only. Order: source, watchdog, setpoint.
if (!controller.SetSetpointSource(al::SetpointSource::DigitalUnsaved).ok()) {
return false; // a meter answers ModbusException
}
if (!controller.SetCommWatchdogMs(1000).ok()) { // zero setpoint after 1 s without traffic
return false;
}
return controller.SetSetpoint(2.5f).ok(); // 2.5 in the instrument's flow units
| Step | Why |
|---|---|
SetSetpointSource(DigitalUnsaved) |
With Analog the instrument ignores digital setpoints. DigitalSaved stores every setpoint in flash; the manual warns that changing it more often than every few minutes can wear the memory out |
SetCommWatchdogMs(ms) |
0 to 5000 ms. The controller forces a zero setpoint and closes the valve when no serial traffic arrives in time. It works only with DigitalUnsaved |
SetSetpoint(value) |
Writes value × 1000 as a signed 32-bit integer to 2053-2054. The instrument rounds to its own resolution |
ReadInstantaneous().value.setpoint reads back the setpoint in use, which lags the command while a ramp
(SetMaxSetpointRamp()) is active.
Tare and the totalizer¶
// With the process gas in the line and no flow:
if (const auto r = meter.Tare(); !r.ok()) {
std::printf("tare: %s\n", al::ToString(r.error).data());
}
meter.ResetTotalizer(); // total volume back to 0
meter.SetTotalizerLimitMode(al::TotalizerLimitMode::SaturateWithOvr);
Tare with the process gas in the line and no flow: the manual says BASIS instruments reach their accuracy only
when tared on pure process gas. A tare interrupts a running measurement window. The totalizer is lost when power
is lost. SetTotalizerBatch() takes the volume already scaled to the total-volume decimals.
Measure over a window¶
controller.ConfigureMeasurementTrigger(al::MeasurementTrigger::OnDigitalSetpointChange);
controller.StartMeasurementSamples(400); // 400 samples at about 2.5 ms: about 1 s
// ... wait at least as long as the window ...
const auto m = controller.ReadMeasurement(3); // 3: the same flow decimals as live data
if (m.ok()) {
std::printf("%u samples: min %d max %d (raw flow), avg flow %.3f, avg %.2f C\n",
m.value.num_samples_collected, m.value.min_flow_raw, m.value.max_flow_raw,
static_cast<double>(m.value.avg_flow),
static_cast<double>(m.value.avg_temperature_c));
std::printf("previous window: avg flow %ld (raw)\n",
static_cast<long>(m.value.prev_avg_flow_raw));
}
The instrument samples about every 2.5 ms. Writing the sample count starts a new window and moves the running one
into the "previous" registers. ReadMeasurement(decimals) reads registers 4201 to 4215 in one request and scales
avg_flow by the same decimals as live flow; scale the raw minimum, maximum and previous average flow
(prev_avg_flow_raw, 32-bit at 4214-4215) the same way.
Find and readdress instruments¶
std::array<uint8_t, 32> present{}; // bit N set: an instrument answered at address N
const auto r = probe.DiscoverPresentAddresses(present.data(), present.size());
if (r.ok()) {
for (unsigned a = 1; a <= al::kMaxModbusAddress; ++a) {
if (present[a / 8] & (1U << (a % 8))) {
std::printf("instrument at %u\n", a);
}
}
}
The sweep reads register 25 at each address with a 30 ms timeout (by default): a full sweep of 247 empty addresses takes about 7.5 s. It restores the driver's address and timeout afterwards. It finds instruments at the host's current baud rate only.
// Only this instrument on the bus, or only one at the old address.
if (!meter.SetModbusAddress(12).ok()) {
return false;
}
return meter.GetAddress() == 12; // the driver now talks to address 12
Give each instrument a unique address before you connect the next one: two instruments at one address answer on top of each other.
Change the baud rate¶
// The instrument switches as soon as it receives the write, so its reply may be garbled.
meter.SetBaudRate(al::BaudRate::Bps_115200);
set_host_baud(al::BaudRateToBps(al::BaudRate::Bps_115200));
return meter.ReadIdentity().ok(); // confirm at the new rate
The instrument switches before it answers
The manual warns that the host must change its own rate in time to receive the confirmation, and may not.
Treat the result of SetBaudRate() as unreliable, switch the host UART, then confirm with a read at the new
rate. If that fails, sweep the six rates to find the instrument again. SetBaudRate() also switches the
driver's inter-frame gap to the new rate; call SetBusBaudRate() on the other drivers that share the port.
Use any register directly¶
uint16_t full_scale_sccm[2]{}; // registers 35-36: full scale in SCCM, unsigned 32-bit
if (meter.ReadHoldingRegisters(al::reg::FullScaleFlowSccm, full_scale_sccm, 2) == al::DriverError::None) {
const uint32_t sccm = (static_cast<uint32_t>(full_scale_sccm[0]) << 16) | full_scale_sccm[1];
std::printf("full scale %lu SCCM\n", static_cast<unsigned long>(sccm));
}
meter.WriteSingleRegister(al::reg::TareSampleCount, 400); // register 51: samples per tare
The register constants are in alicat_basis2::reg; the Modbus-RTU protocol page lists them
with their scaling. ReadHoldingRegisters() reads up to 32 registers; WriteMultipleRegisters() writes up to 32.
Handle errors¶
const auto r = meter.ReadInstantaneous();
if (!r.ok()) {
switch (r.error) {
case al::DriverError::Timeout: // no reply: wiring, baud rate, address, timeout
case al::DriverError::BadCrc: // noise, termination, a second slave answering
break;
case al::DriverError::ModbusException: // the instrument refused: register, value, meter vs controller
std::printf("exception %s\n", al::ToString(meter.GetLastException()).data());
break;
default:
std::printf("%s\n", al::ToString(r.error).data());
break;
}
}
DriverError |
Meaning | Where to look |
|---|---|---|
Timeout |
No byte arrived within the timeout | Wiring, baud rate, address, DE/RE, the timeout |
InvalidFrame |
Too short, wrong byte count | Line noise, a read() that returns early |
BadCrc |
CRC-16 mismatch | Ground, termination, two instruments at one address |
ModbusException |
The instrument returned an exception reply; GetLastException() has the code |
Register not in this model or firmware (IllegalDataAddress), a meter asked for a controller setting |
ResponseMismatch |
Reply from another address, or an echo that differs from the request | Address clash, a late reply from an earlier request |
InvalidParameter, InvalidAddress |
The driver refused the argument; nothing was sent | Ranges on the Configuration page |
BufferTooSmall |
A request longer than the driver's 32-register buffers | Split the access |
Exception replies come back at once
An exception reply is 5 bytes: the function code with bit 7 set, the exception code and the CRC. The driver
reads the first 3 bytes of every reply, sees bit 7 and reads only the CRC, so a rejected read or write (for
example SetSetpointSource() on a meter) returns ModbusException as soon as the reply has arrived, not after
the timeout. GetLastException() returns the code (ModbusExceptionCode, ToString() names it) until the
next request.
The status bits in InstantaneousData (mass_flow_overrange, temperature_overrange, totalizer_overrange,
valve_held, valve_thermal_management) report instrument conditions, not transport errors.
Troubleshooting maps symptoms to causes.