Partial implementation of network config, calibration

This commit is contained in:
Joris van Rantwijk 2024-09-29 19:21:51 +02:00
parent 27fc4236e4
commit 67c6a44f22
1 changed files with 321 additions and 85 deletions

View File

@ -14,6 +14,7 @@
#include <stdio.h>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <functional>
#include <istream>
#include <map>
@ -34,6 +35,8 @@ using namespace puzzlefw;
namespace asio = boost::asio;
/* ******** Utility functions ******** */
/** Return true if the string ends with the suffix. */
bool str_ends_with(const std::string& value, const std::string& suffix)
{
@ -102,32 +105,121 @@ bool parse_float(const std::string& s, double& v)
}
// Forward declaration.
class ControlServer;
/* ******** Network configuration ******** */
enum NetworkConfigMode { NETCFG_INVALID = 0, NETCFG_DHCP, NETCFG_STATIC };
/**
* The CommandHandler handles commands from remote clients.
*/
class CommandHandler
{
public:
// IDN response fields.
static constexpr const char * IDN_MANUFACTURER = "Jigsaw";
static constexpr const char * IDN_MODEL = "PuzzleFw";
// Configuration files.
static constexpr const char * CFG_FILE_CALIBRATION =
"/var/lib/puzzlefw/cfg/calibration.conf";
static constexpr const char * CFG_FILE_NETWORK =
"/var/lib/puzzlefw/cfg/network.conf";
enum ExitStatus {
EXIT_ERROR = 1,
EXIT_HALT = 10,
EXIT_REBOOT = 11
struct NetworkConfig {
NetworkConfigMode mode;
asio::ip::address_v4 ipaddr;
asio::ip::address_v4 netmask;
asio::ip::address_v4 gateway;
NetworkConfig() : mode(NETCFG_INVALID) { }
};
/** Read network configuration from file. */
bool read_network_config(const char *filename, NetworkConfig& ipcfg)
{
ipcfg = NetworkConfig();
std::ifstream is(filename);
if (!is) {
log(LOG_ERROR, "Can not read %s", filename);
return false;
}
std::string line;
while (std::getline(is, line)) {
boost::system::error_code ec{};
size_t p = line.find_last_not_of(" \t\n\v\f\r");
if (p != line.npos) {
line.erase(p + 1);
}
if (line == "MODE=dhcp") {
ipcfg.mode = NETCFG_DHCP;
}
if (line == "MODE=static") {
ipcfg.mode = NETCFG_STATIC;
}
if (line.compare(0, 7, "IPADDR=") == 0) {
ipcfg.ipaddr = asio::ip::make_address_v4(line.substr(7), ec);
}
if (line.compare(0, 8, "NETMASK=") == 0) {
ipcfg.netmask = asio::ip::make_address_v4(line.substr(8), ec);
}
if (line.compare(0, 8, "GATEWAY=") == 0 && line.size() > 8) {
ipcfg.gateway = asio::ip::make_address_v4(line.substr(8), ec);
}
if (ec) {
return false;
}
}
return ((ipcfg.mode == NETCFG_DHCP)
|| (ipcfg.mode == NETCFG_STATIC
&& (! ipcfg.ipaddr.is_unspecified())
&& (! ipcfg.netmask.is_unspecified())));
}
/** Parse network configuration arguments. */
bool parse_network_config(const std::vector<std::string>& args,
NetworkConfig& ipcfg,
std::string& errmsg)
{
ipcfg = NetworkConfig{};
errmsg = std::string();
if (args.size() == 1 && str_to_lower(args[0]) == "dhcp") {
ipcfg.mode = NETCFG_DHCP;
return true;
}
if ((args.size() == 3 || args.size() == 4)
&& str_to_lower(args[0]) == "static") {
ipcfg.mode = NETCFG_STATIC;
boost::system::error_code ec{};
ipcfg.ipaddr = asio::ip::make_address_v4(args[1], ec);
if (ec || ipcfg.ipaddr.is_unspecified()) {
errmsg = "Invalid IP address";
return false;
}
ipcfg.netmask = asio::ip::make_address_v4(args[2], ec);
if (ec || ipcfg.netmask.is_unspecified()) {
errmsg = "Invalid netmask";
return false;
}
// Check that netmask describes a valid prefix.
unsigned int mask = ipcfg.netmask.to_uint();
if (mask & ((~mask) >> 1)) {
errmsg = "Invalid netmask";
return false;
}
// Optional gateway address.
if (args.size() == 4) {
ipcfg.gateway = asio::ip::make_address_v4(args[3], ec);
if (ec) {
errmsg = "Invalid gateway";
return false;
}
}
return true;
}
errmsg = "Invalid address mode";
return false;
}
/* ******** Calibration ******** */
enum RangeSpec {
RANGE_NONE = 0,
RANGE_LO = 1,
@ -146,6 +238,149 @@ public:
ChannelCalibration channel_cal[4];
};
/** Read calibration from file. */
void read_calibration_file(const char *filename, Calibration& cal)
{
// Set defaults in case calibration is missing or incomplete.
for (unsigned int channel = 0; channel < 4; channel++) {
cal.channel_cal[channel].range_spec = RANGE_LO;
cal.channel_cal[channel].offset_lo = 8192;
cal.channel_cal[channel].offset_hi = 8192;
cal.channel_cal[channel].gain_lo = -8191;
cal.channel_cal[channel].gain_hi = -409;
}
std::ifstream is(filename);
if (!is) {
log(LOG_ERROR, "Can not read calibration file");
return;
}
std::string line;
while (std::getline(is, line)) {
size_t p = line.find_last_not_of(" \t\n\v\f\r");
if (p != line.npos) {
line.erase(p + 1);
}
p = line.find('=');
if (p == line.npos) {
continue;
}
std::string label = line.substr(0, p);
std::string value = line.substr(p + 1);
if (label.size() < 4
|| label[0] != 'C'
|| label[1] != 'H'
|| label[2] < '1'
|| label[2] > '4'
|| label[3] != '_') {
continue;
}
unsigned int channel = label[2] - '1';
label.erase(0, 4);
if (label == "RANGE") {
if (value == "LO") {
cal.channel_cal[channel].range_spec = RANGE_LO;
}
if (value == "HI") {
cal.channel_cal[channel].range_spec = RANGE_HI;
}
}
if (label == "OFFSET_LO") {
parse_float(value, cal.channel_cal[channel].offset_lo);
}
if (label == "OFFSET_HI") {
parse_float(value, cal.channel_cal[channel].offset_hi);
}
if (label == "GAIN_LO") {
parse_float(value, cal.channel_cal[channel].gain_lo);
}
if (label == "GAIN_HI") {
parse_float(value, cal.channel_cal[channel].gain_hi);
}
}
}
/** Write calibration to file. */
bool write_calibration_file(const char *filename, const Calibration& cal)
{
std::ofstream os(filename);
if (!os) {
log(LOG_ERROR, "Can not write calibration file");
return false;
}
for (unsigned int channel = 0; channel < 4; channel++) {
std::string line;
line = str_format("CH%u_RANGE=%s\n", channel + 1,
(cal.channel_cal[channel].range_spec == RANGE_HI) ? "HI" : "LO");
os << line;
line = str_format("CH%u_OFFSET_LO=%.6f\n", channel + 1,
cal.channel_cal[channel].offset_lo);
os << line;
line = str_format("CH%u_OFFSET_HI=%.6f\n", channel + 1,
cal.channel_cal[channel].offset_hi);
os << line;
line = str_format("CH%u_GAIN_LO=%.6f\n", channel + 1,
cal.channel_cal[channel].gain_lo);
os << line;
line = str_format("CH%u_GAIN_HI=%.6f\n", channel + 1,
cal.channel_cal[channel].gain_hi);
os << line;
}
if (!os) {
log(LOG_ERROR, "Can not write calibration file");
return false;
}
return true;
}
/* ******** class CommandHandler ******** */
// Forward declaration.
class ControlServer;
/**
* The CommandHandler handles commands from remote clients.
*/
class CommandHandler
{
public:
// IDN response fields.
static constexpr const char * IDN_MANUFACTURER = "Jigsaw";
static constexpr const char * IDN_MODEL = "PuzzleFw";
// Configuration files.
static constexpr const char * CFG_FILE_CALIBRATION =
"/var/lib/puzzlefw/cfg/calibration.conf";
static constexpr const char * CFG_FILE_NETWORK_SAVED =
"/var/lib/puzzlefw/cfg/network.conf";
static constexpr const char * CFG_FILE_NETWORK_ACTIVE =
"/var/lib/puzzlefw/cfg/network_active.conf";
enum ExitStatus {
EXIT_ERROR = 1,
EXIT_HALT = 10,
EXIT_REBOOT = 11
};
struct CommandEnvironment {
int channel;
RangeSpec range_spec;
@ -206,7 +441,7 @@ public:
*/
void reset()
{
read_calibration();
read_calibration_file(CFG_FILE_CALIBRATION, m_calibration);
m_device.set_adc_simulation_enabled(false);
m_device.set_digital_simulation_enabled(false);
m_device.set_trigger_mode(TRIG_NONE);
@ -260,10 +495,10 @@ public:
&& action.compare(0, 6, "ain:ch") == 0
&& action[7] == ':') {
char channel_digit = action[6];
if (channel_digit < '0' || channel_digit > '3') {
if (channel_digit < '1' || channel_digit > '4') {
return err_unknown_command();
}
env.channel = channel_digit - '0';
env.channel = channel_digit - '1';
action[6] = 'N'; // mark channel index
}
@ -323,14 +558,14 @@ public:
// Handle command IPCFG.
if (action == "ipcfg" || action == "ipcfg:saved") {
if (tokens.size() < 3) {
if (tokens.size() < 2) {
return err_missing_argument();
}
if (tokens.size() > 4) {
if (tokens.size() > 5) {
return err_unexpected_argument();
}
const std::string& gw = (tokens.size() > 3) ? tokens[3] : "";
return cmd_ipcfg(env, tokens[1], tokens[2], gw);
tokens.erase(tokens.begin());
return cmd_ipcfg(env, tokens);
}
return err_unknown_command();
@ -338,27 +573,13 @@ public:
private:
/** Asynchronously stop control and/or data servers. */
void stop_server(bool stop_control, std::function<void()> handler);
void stop_server(std::function<void()> handler);
void stop_data_servers(unsigned int idx, std::function<void()> handler);
/** Asynchronously start control and/or data servers. */
void start_server(bool start_control);
void start_server();
void start_data_servers(unsigned int idx);
/** Read calibration from file. */
void read_calibration()
{
for (int c = 0; c < 4; c++) {
m_calibration.channel_cal[c].range_spec = RANGE_LO;
m_calibration.channel_cal[c].offset_lo = 8192;
m_calibration.channel_cal[c].offset_hi = 8192;
m_calibration.channel_cal[c].gain_lo = -8191;
m_calibration.channel_cal[c].gain_hi = -409;
}
// TODO - read from file
}
/** Convert a raw ADC sample to Volt. */
double convert_sample_to_volt(unsigned int channel, unsigned int sample)
{
@ -456,7 +677,7 @@ private:
}
/** Handle command AIN:CHn:OFFS[:range]? */
std::string qry_channel_offs(CommandEnvironment env)
std::string qry_channel_offset(CommandEnvironment env)
{
ChannelCalibration& cal = m_calibration.channel_cal[env.channel];
RangeSpec range_spec = env.range_spec;
@ -608,8 +829,25 @@ private:
/** Handle command IPCFG[:SAVED]? */
std::string qry_ipcfg(CommandEnvironment env)
{
// TODO - read network config from file
return "ERROR";
const char *filename = env.saved_flag ? CFG_FILE_NETWORK_SAVED
: CFG_FILE_NETWORK_ACTIVE;
NetworkConfig ipcfg;
if (! read_network_config(filename, ipcfg)) {
return "ERROR Unconfigured";
}
std::string result;
if (ipcfg.mode == NETCFG_DHCP) {
return "DHCP";
}
if (ipcfg.mode == NETCFG_STATIC) {
return "STATIC "
+ ipcfg.ipaddr.to_string()
+ " " + ipcfg.netmask.to_string()
+ " " + ipcfg.gateway.to_string();
}
return "ERROR Unconfigured";
}
/** Handle command RESET */
@ -706,7 +944,7 @@ private:
}
/** Handle command AIN:CHn:OFFS[:range] */
std::string cmd_channel_offs(CommandEnvironment env,
std::string cmd_channel_offset(CommandEnvironment env,
const std::string& arg)
{
double offs;
@ -962,18 +1200,24 @@ private:
/** Handle command AIN:MINMAX:CLEAR */
std::string cmd_cal_save(CommandEnvironment env)
{
// TODO - write calibration to file
if (! write_calibration_file(CFG_FILE_CALIBRATION, m_calibration)) {
return "ERROR Can not write calibration";
}
// TODO - invoke script to save to SD card
return "ERROR";
}
/** Handle command IPCFG */
std::string cmd_ipcfg(CommandEnvironment env,
const std::string& ipaddr,
const std::string& netmask,
const std::string& gateway)
const std::vector<std::string>& args)
{
// TODO -- parse and check address
NetworkConfig ipcfg;
std::string errmsg;
if (! parse_network_config(args, ipcfg, errmsg)) {
return "ERROR " + errmsg;
}
// TODO -- activate or save address, restart networking if needed
return "ERROR";
}
@ -987,8 +1231,8 @@ private:
{ "ain:channels:count?", &CommandHandler::qry_channels_count },
{ "ain:channels:active?", &CommandHandler::qry_channels_active },
{ "ain:chN:range?", &CommandHandler::qry_channel_range },
{ "ain:chN:offs?", &CommandHandler::qry_channel_offs },
{ "ain:chN:offs:RR?", &CommandHandler::qry_channel_offs },
{ "ain:chN:offset?", &CommandHandler::qry_channel_offset },
{ "ain:chN:offset:RR?", &CommandHandler::qry_channel_offset },
{ "ain:chN:gain?", &CommandHandler::qry_channel_gain },
{ "ain:chN:gain:RR?", &CommandHandler::qry_channel_gain },
{ "ain:chN:sample?", &CommandHandler::qry_channel_sample },
@ -1025,8 +1269,8 @@ private:
command_table_one_arg = {
{ "ain:channels:active", &CommandHandler::cmd_channels_active },
{ "ain:chN:range", &CommandHandler::cmd_channel_range },
{ "ain:chN:offs", &CommandHandler::cmd_channel_offs },
{ "ain:chN:offs:RR", &CommandHandler::cmd_channel_offs },
{ "ain:chN:offset", &CommandHandler::cmd_channel_offset },
{ "ain:chN:offset:RR", &CommandHandler::cmd_channel_offset },
{ "ain:chN:gain", &CommandHandler::cmd_channel_gain },
{ "ain:chN:gain:RR", &CommandHandler::cmd_channel_gain },
{ "ain:srate", &CommandHandler::cmd_srate },
@ -1052,6 +1296,8 @@ private:
};
/* ******** class ControlServer ******** */
/**
* Manage TCP connections for remote control.
*/
@ -1374,10 +1620,8 @@ private:
/* ******** Methods for class CommandHandler ******** */
void CommandHandler::stop_server(bool stop_control,
std::function<void()> handler)
void CommandHandler::stop_server(std::function<void()> handler)
{
if (stop_control) {
asio::post(m_control_server->get_executor(),
[this,handler]() {
m_control_server->stop_server();
@ -1386,9 +1630,6 @@ void CommandHandler::stop_server(bool stop_control,
stop_data_servers(0, handler);
});
});
} else {
stop_data_servers(0, handler);
}
}
void CommandHandler::stop_data_servers(unsigned int idx,
@ -1408,9 +1649,8 @@ void CommandHandler::stop_data_servers(unsigned int idx,
}
}
void CommandHandler::start_server(bool start_control)
void CommandHandler::start_server()
{
if (start_control) {
asio::post(m_control_server->get_executor(),
[this]() {
m_control_server->start_server();
@ -1419,9 +1659,6 @@ void CommandHandler::start_server(bool start_control)
start_data_servers(0);
});
});
} else {
start_data_servers(0);
}
}
void CommandHandler::start_data_servers(unsigned int idx)
@ -1522,9 +1759,8 @@ int run_remote_control_server(
io.run();
// TODO -- multi-threading
// TODO -- get exit status from command handler
return 0;
return command_handler.exit_status();
}