Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 429b28ef67 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,5 @@
|
||||
# Build outputs
|
||||
*.o
|
||||
mwcli
|
||||
mw_controller_example
|
||||
|
||||
# Logs and captures
|
||||
*.csv
|
||||
|
||||
BIN
bin/mw_controller_example
Normal file
BIN
bin/mw_controller_example
Normal file
Binary file not shown.
25
examples/battery_discharge_cutoff.json
Normal file
25
examples/battery_discharge_cutoff.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "battery_discharge_cutoff",
|
||||
"sample_period_ms": 1000,
|
||||
"safety": {
|
||||
"max_voltage": 5.0,
|
||||
"max_current": 0.5,
|
||||
"max_power": 2.5,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{ "action": "set_current", "value": 0.20 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{
|
||||
"action": "hold_until",
|
||||
"timeout_s": 14400,
|
||||
"condition": { "type": "voltage_below", "value": 3.00 },
|
||||
"break_if": { "type": "temperature_above", "value": 50.0 }
|
||||
},
|
||||
{ "action": "output", "enabled": false }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
28
examples/cc_step_sweep.json
Normal file
28
examples/cc_step_sweep.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "cc_step_sweep",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.8,
|
||||
"max_power": 4.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{ "action": "set_current", "value": 0.10 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 10 },
|
||||
{
|
||||
"action": "ramp_current",
|
||||
"start": 0.10,
|
||||
"stop": 0.50,
|
||||
"step": 0.10,
|
||||
"dwell_s": 10,
|
||||
"break_if": { "type": "temperature_above", "value": 45.0 }
|
||||
},
|
||||
{ "action": "output", "enabled": false }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
21
examples/cp_step_sweep.json
Normal file
21
examples/cp_step_sweep.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "cp_step_sweep",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.8,
|
||||
"max_power": 3.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CP" },
|
||||
{ "action": "set_power", "value": 0.50 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 10 },
|
||||
{ "action": "ramp_power", "start": 0.50, "stop": 2.50, "step": 0.50, "dwell_s": 10 },
|
||||
{ "action": "output", "enabled": false }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
21
examples/cr_step_sweep.json
Normal file
21
examples/cr_step_sweep.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "cr_step_sweep",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.8,
|
||||
"max_power": 4.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CR" },
|
||||
{ "action": "set_resistance", "value": 50.0 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 10 },
|
||||
{ "action": "ramp_resistance", "start": 50.0, "stop": 10.0, "step": 10.0, "dwell_s": 10 },
|
||||
{ "action": "output", "enabled": false }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
21
examples/load_test_cc.json
Normal file
21
examples/load_test_cc.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "load_test_cc",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 30.0,
|
||||
"max_current": 2.0,
|
||||
"max_power": 20.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{ "action": "set_current", "value": 0.20 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 30 },
|
||||
{ "action": "ramp_current", "start": 0.20, "stop": 1.00, "step": 0.10, "dwell_s": 20 },
|
||||
{ "action": "hold_until", "timeout_s": 300, "condition": { "type": "voltage_below", "value": 3.00 } }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
103
examples/mw_controller_example.c
Normal file
103
examples/mw_controller_example.c
Normal file
@ -0,0 +1,103 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "mightywatt_controller.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static void sleep_ms(int ms) {
|
||||
struct timespec ts;
|
||||
if (ms < 0) {
|
||||
ms = 0;
|
||||
}
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
mw_controller *controller = NULL;
|
||||
mw_controller_config config;
|
||||
mw_controller_snapshot snapshot;
|
||||
uint64_t cmd_id = 0;
|
||||
int i;
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "usage: %s /dev/ttyACM0\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
mw_controller_config_init(&config);
|
||||
config.port_path = argv[1];
|
||||
config.poll_interval_ms = 500;
|
||||
config.reconnect_ms = 1500;
|
||||
|
||||
if (mw_controller_start(&controller, &config) != 0) {
|
||||
fprintf(stderr, "controller start failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (i = 0; i < 20; ++i) {
|
||||
if (mw_controller_get_snapshot(controller, &snapshot) != 0) {
|
||||
fprintf(stderr, "snapshot failed\n");
|
||||
mw_controller_stop(controller);
|
||||
return 1;
|
||||
}
|
||||
printf("[%02d] state=%s connected=%s pending=%zu error=%s\n",
|
||||
i,
|
||||
mw_controller_connection_state_name(snapshot.connection_state),
|
||||
snapshot.connected ? "yes" : "no",
|
||||
snapshot.pending_commands,
|
||||
snapshot.last_error[0] ? snapshot.last_error : "-");
|
||||
if (snapshot.connected) {
|
||||
break;
|
||||
}
|
||||
sleep_ms(250);
|
||||
}
|
||||
|
||||
if (!snapshot.connected) {
|
||||
fprintf(stderr, "device did not connect in time\n");
|
||||
mw_controller_stop(controller);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (mw_controller_queue_load_on(controller, MW_MODE_CURRENT, 250, &cmd_id) != 0) {
|
||||
fprintf(stderr, "queue load-on failed\n");
|
||||
mw_controller_stop(controller);
|
||||
return 1;
|
||||
}
|
||||
printf("queued command id=%" PRIu64 "\n", cmd_id);
|
||||
|
||||
for (;;) {
|
||||
if (mw_controller_wait_for_update(controller, snapshot.snapshot_seq, 2000, NULL) < 0) {
|
||||
fprintf(stderr, "wait for update failed\n");
|
||||
break;
|
||||
}
|
||||
if (mw_controller_get_snapshot(controller, &snapshot) != 0) {
|
||||
fprintf(stderr, "snapshot failed\n");
|
||||
break;
|
||||
}
|
||||
if (snapshot.last_completed_command_id >= cmd_id) {
|
||||
printf("command result=%d completed=%" PRIu64 " state=%s\n",
|
||||
snapshot.last_completed_result,
|
||||
snapshot.last_completed_command_id,
|
||||
mw_controller_connection_state_name(snapshot.connection_state));
|
||||
if (snapshot.app_state_valid && snapshot.app_state.report_valid) {
|
||||
printf("I=%.3f A V=%.3f V remote=%s status=%u\n",
|
||||
snapshot.app_state.last_report.current_ma / 1000.0,
|
||||
snapshot.app_state.last_report.voltage_mv / 1000.0,
|
||||
snapshot.app_state.last_report.remote ? "on" : "off",
|
||||
(unsigned)snapshot.app_state.last_report.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mw_controller_queue_safe(controller, NULL);
|
||||
sleep_ms(400);
|
||||
mw_controller_stop(controller);
|
||||
return 0;
|
||||
}
|
||||
24
examples/quick_cc_test.json
Normal file
24
examples/quick_cc_test.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "quick_cc_test",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.6,
|
||||
"max_power": 3.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{ "action": "set_current", "value": 0.10 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 5 },
|
||||
{ "action": "set_current", "value": 0.25 },
|
||||
{ "action": "hold", "duration_s": 5 },
|
||||
{ "action": "set_current", "value": 0.50 },
|
||||
{ "action": "hold", "duration_s": 5 },
|
||||
{ "action": "output", "enabled": false }
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
30
examples/repeat_n_pulse_test.json
Normal file
30
examples/repeat_n_pulse_test.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "repeat_n_pulse_test",
|
||||
"sample_period_ms": 500,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.8,
|
||||
"max_power": 4.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{
|
||||
"action": "repeat",
|
||||
"times": 5,
|
||||
"break_if": { "type": "temperature_above", "value": 45.0 },
|
||||
"steps": [
|
||||
{ "action": "set_current", "value": 0.20 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 5 },
|
||||
{ "action": "set_current", "value": 0.50 },
|
||||
{ "action": "hold", "duration_s": 5 },
|
||||
{ "action": "output", "enabled": false },
|
||||
{ "action": "hold", "duration_s": 2 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
29
examples/repeat_until_cutoff.json
Normal file
29
examples/repeat_until_cutoff.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "repeat_until_cutoff",
|
||||
"sample_period_ms": 1000,
|
||||
"safety": {
|
||||
"max_voltage": 5.0,
|
||||
"max_current": 0.6,
|
||||
"max_power": 3.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{
|
||||
"action": "repeat_until",
|
||||
"timeout_s": 14400,
|
||||
"condition": { "type": "voltage_below", "value": 3.20 },
|
||||
"break_if": { "type": "temperature_above", "value": 45.0 },
|
||||
"steps": [
|
||||
{ "action": "set_current", "value": 0.30 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 60 },
|
||||
{ "action": "output", "enabled": false },
|
||||
{ "action": "hold", "duration_s": 10 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
29
examples/repeat_while_burn_in.json
Normal file
29
examples/repeat_while_burn_in.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "repeat_while_burn_in",
|
||||
"sample_period_ms": 1000,
|
||||
"safety": {
|
||||
"max_voltage": 6.0,
|
||||
"max_current": 0.5,
|
||||
"max_power": 3.0,
|
||||
"abort_on_disconnect": true
|
||||
},
|
||||
"steps": [
|
||||
{ "action": "set_mode", "mode": "CC" },
|
||||
{
|
||||
"action": "repeat_while",
|
||||
"timeout_s": 7200,
|
||||
"condition": { "type": "temperature_above", "value": 20.0 },
|
||||
"break_if": { "type": "temperature_above", "value": 50.0 },
|
||||
"steps": [
|
||||
{ "action": "set_current", "value": 0.15 },
|
||||
{ "action": "output", "enabled": true },
|
||||
{ "action": "hold", "duration_s": 30 },
|
||||
{ "action": "output", "enabled": false },
|
||||
{ "action": "hold", "duration_s": 15 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"abort_sequence": [
|
||||
{ "action": "safe" }
|
||||
]
|
||||
}
|
||||
76
include/mightywatt.h
Normal file
76
include/mightywatt.h
Normal file
@ -0,0 +1,76 @@
|
||||
#ifndef MIGHTYWATT_H
|
||||
#define MIGHTYWATT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MW_FW_VERSION_MAX 31
|
||||
#define MW_BOARD_REV_MAX 31
|
||||
|
||||
typedef struct mw_device mw_device;
|
||||
|
||||
typedef enum {
|
||||
MW_MODE_CURRENT = 0,
|
||||
MW_MODE_VOLTAGE = 1,
|
||||
MW_MODE_POWER = 2,
|
||||
MW_MODE_RESISTANCE = 3,
|
||||
MW_MODE_VOLTAGE_INVERTED = 4,
|
||||
} mw_mode;
|
||||
|
||||
enum {
|
||||
MW_STATUS_READY = 0,
|
||||
MW_STATUS_CURRENT_OVERLOAD = 1 << 0,
|
||||
MW_STATUS_VOLTAGE_OVERLOAD = 1 << 1,
|
||||
MW_STATUS_POWER_OVERLOAD = 1 << 2,
|
||||
MW_STATUS_OVERHEAT = 1 << 3,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
char firmware_version[MW_FW_VERSION_MAX + 1];
|
||||
char board_revision[MW_BOARD_REV_MAX + 1];
|
||||
uint32_t max_current_dac_ma;
|
||||
uint32_t max_current_adc_ma;
|
||||
uint32_t max_voltage_dac_mv;
|
||||
uint32_t max_voltage_adc_mv;
|
||||
uint32_t max_power_mw;
|
||||
uint32_t dvm_input_resistance_ohm;
|
||||
uint32_t temperature_threshold_c;
|
||||
} mw_capabilities;
|
||||
|
||||
typedef struct {
|
||||
uint16_t current_ma;
|
||||
uint16_t voltage_mv;
|
||||
uint8_t temperature_c;
|
||||
bool remote;
|
||||
uint8_t status;
|
||||
} mw_report;
|
||||
|
||||
const char *mw_last_error(const mw_device *dev);
|
||||
int mw_open(mw_device **out_dev, const char *port_path, int settle_ms);
|
||||
void mw_close(mw_device *dev);
|
||||
|
||||
int mw_identify(mw_device *dev);
|
||||
int mw_query_capabilities(mw_device *dev, mw_capabilities *caps);
|
||||
int mw_get_report(mw_device *dev, mw_report *report);
|
||||
int mw_set(mw_device *dev, mw_mode mode, uint32_t milli_units, mw_report *report);
|
||||
int mw_set_remote(mw_device *dev, bool enable, mw_report *report);
|
||||
int mw_set_series_resistance(mw_device *dev, uint16_t milliohm, mw_report *report);
|
||||
int mw_get_series_resistance(mw_device *dev, uint16_t *milliohm);
|
||||
|
||||
size_t mw_status_string(uint8_t status, char *buffer, size_t buffer_size);
|
||||
const char *mw_mode_name(mw_mode mode);
|
||||
uint32_t mw_report_power_mw(const mw_report *report);
|
||||
uint32_t mw_capability_limit_for_mode(const mw_capabilities *caps, mw_mode mode);
|
||||
int mw_validate_target(const mw_capabilities *caps, mw_mode mode, uint32_t milli_units,
|
||||
char *buffer, size_t buffer_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
45
include/mightywatt_app.h
Normal file
45
include/mightywatt_app.h
Normal file
@ -0,0 +1,45 @@
|
||||
#ifndef MIGHTYWATT_APP_H
|
||||
#define MIGHTYWATT_APP_H
|
||||
|
||||
#include "mightywatt.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct mw_app mw_app;
|
||||
|
||||
typedef struct {
|
||||
bool capabilities_valid;
|
||||
mw_capabilities capabilities;
|
||||
bool report_valid;
|
||||
mw_report last_report;
|
||||
bool target_valid;
|
||||
mw_mode target_mode;
|
||||
uint32_t target_milli_units;
|
||||
bool restore_target_valid;
|
||||
mw_mode restore_target_mode;
|
||||
uint32_t restore_target_milli_units;
|
||||
} mw_app_state;
|
||||
|
||||
int mw_app_open(mw_app **out_app, const char *port_path, int settle_ms);
|
||||
void mw_app_close(mw_app *app);
|
||||
const char *mw_app_last_error(const mw_app *app);
|
||||
|
||||
int mw_app_refresh_capabilities(mw_app *app, mw_capabilities *out_caps);
|
||||
int mw_app_get_report(mw_app *app, mw_report *out_report);
|
||||
int mw_app_set_target(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report);
|
||||
int mw_app_load_off(mw_app *app, mw_report *out_report);
|
||||
int mw_app_load_on(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report);
|
||||
int mw_app_restore_target(mw_app *app, mw_report *out_report);
|
||||
int mw_app_safe(mw_app *app, mw_report *out_report);
|
||||
int mw_app_set_remote(mw_app *app, bool enable, mw_report *out_report);
|
||||
int mw_app_get_series_resistance(mw_app *app, uint16_t *milliohm);
|
||||
int mw_app_set_series_resistance(mw_app *app, uint16_t milliohm, mw_report *out_report);
|
||||
int mw_app_get_state(const mw_app *app, mw_app_state *out_state);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
113
include/mightywatt_controller.h
Normal file
113
include/mightywatt_controller.h
Normal file
@ -0,0 +1,113 @@
|
||||
#ifndef MIGHTYWATT_CONTROLLER_H
|
||||
#define MIGHTYWATT_CONTROLLER_H
|
||||
|
||||
#include "mightywatt_app.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MW_CONTROLLER_PORT_PATH_MAX 255
|
||||
#define MW_CONTROLLER_ERROR_MAX 255
|
||||
|
||||
typedef struct mw_controller mw_controller;
|
||||
|
||||
typedef enum {
|
||||
MW_CONTROLLER_STOPPED = 0,
|
||||
MW_CONTROLLER_CONNECTING = 1,
|
||||
MW_CONTROLLER_CONNECTED = 2,
|
||||
MW_CONTROLLER_RECONNECT_WAIT = 3,
|
||||
} mw_controller_connection_state;
|
||||
|
||||
typedef enum {
|
||||
MW_CTRL_CMD_NONE = 0,
|
||||
MW_CTRL_CMD_SET_TARGET,
|
||||
MW_CTRL_CMD_LOAD_ON,
|
||||
MW_CTRL_CMD_LOAD_OFF,
|
||||
MW_CTRL_CMD_RESTORE_TARGET,
|
||||
MW_CTRL_CMD_SAFE,
|
||||
MW_CTRL_CMD_SET_REMOTE,
|
||||
MW_CTRL_CMD_SET_SERIES_RESISTANCE,
|
||||
MW_CTRL_CMD_REFRESH_CAPABILITIES,
|
||||
MW_CTRL_CMD_GET_SERIES_RESISTANCE,
|
||||
} mw_controller_command_kind;
|
||||
|
||||
typedef struct {
|
||||
const char *port_path;
|
||||
int settle_ms;
|
||||
int poll_interval_ms;
|
||||
int reconnect_ms;
|
||||
size_t queue_capacity;
|
||||
bool safe_on_shutdown;
|
||||
} mw_controller_config;
|
||||
|
||||
typedef struct {
|
||||
bool running;
|
||||
bool connected;
|
||||
mw_controller_connection_state connection_state;
|
||||
uint64_t snapshot_seq;
|
||||
uint64_t connect_attempts;
|
||||
uint64_t reconnect_count;
|
||||
uint64_t poll_success_count;
|
||||
uint64_t poll_error_count;
|
||||
uint64_t command_success_count;
|
||||
uint64_t command_error_count;
|
||||
uint64_t last_queued_command_id;
|
||||
uint64_t last_completed_command_id;
|
||||
int last_completed_result;
|
||||
mw_controller_command_kind last_completed_kind;
|
||||
size_t pending_commands;
|
||||
char port_path[MW_CONTROLLER_PORT_PATH_MAX + 1];
|
||||
char last_error[MW_CONTROLLER_ERROR_MAX + 1];
|
||||
char last_completed_error[MW_CONTROLLER_ERROR_MAX + 1];
|
||||
bool app_state_valid;
|
||||
mw_app_state app_state;
|
||||
bool series_resistance_valid;
|
||||
uint16_t series_resistance_milliohm;
|
||||
} mw_controller_snapshot;
|
||||
|
||||
void mw_controller_config_init(mw_controller_config *config);
|
||||
|
||||
int mw_controller_start(mw_controller **out_controller, const mw_controller_config *config);
|
||||
void mw_controller_stop(mw_controller *controller);
|
||||
|
||||
int mw_controller_get_snapshot(mw_controller *controller, mw_controller_snapshot *out_snapshot);
|
||||
int mw_controller_wait_for_update(mw_controller *controller,
|
||||
uint64_t last_seen_snapshot_seq,
|
||||
int timeout_ms,
|
||||
uint64_t *out_snapshot_seq);
|
||||
|
||||
int mw_controller_queue_set_target(mw_controller *controller,
|
||||
mw_mode mode,
|
||||
uint32_t milli_units,
|
||||
uint64_t *out_command_id);
|
||||
int mw_controller_queue_load_on(mw_controller *controller,
|
||||
mw_mode mode,
|
||||
uint32_t milli_units,
|
||||
uint64_t *out_command_id);
|
||||
int mw_controller_queue_load_off(mw_controller *controller, uint64_t *out_command_id);
|
||||
int mw_controller_queue_restore_target(mw_controller *controller, uint64_t *out_command_id);
|
||||
int mw_controller_queue_safe(mw_controller *controller, uint64_t *out_command_id);
|
||||
int mw_controller_queue_set_remote(mw_controller *controller,
|
||||
bool enable,
|
||||
uint64_t *out_command_id);
|
||||
int mw_controller_queue_set_series_resistance(mw_controller *controller,
|
||||
uint16_t milliohm,
|
||||
uint64_t *out_command_id);
|
||||
int mw_controller_queue_refresh_capabilities(mw_controller *controller,
|
||||
uint64_t *out_command_id);
|
||||
int mw_controller_queue_get_series_resistance(mw_controller *controller,
|
||||
uint64_t *out_command_id);
|
||||
|
||||
const char *mw_controller_connection_state_name(mw_controller_connection_state state);
|
||||
const char *mw_controller_command_kind_name(mw_controller_command_kind kind);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
30
include/mightywatt_log.h
Normal file
30
include/mightywatt_log.h
Normal file
@ -0,0 +1,30 @@
|
||||
#ifndef MIGHTYWATT_LOG_H
|
||||
#define MIGHTYWATT_LOG_H
|
||||
|
||||
#include "mightywatt.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct mw_csv_logger mw_csv_logger;
|
||||
|
||||
typedef enum {
|
||||
MW_CSV_UNITS_ENGINEERING = 0,
|
||||
MW_CSV_UNITS_RAW = 1
|
||||
} mw_csv_units_mode;
|
||||
|
||||
int mw_csv_logger_open(mw_csv_logger **out_logger, const char *path, mw_csv_units_mode units_mode);
|
||||
void mw_csv_logger_close(mw_csv_logger *logger);
|
||||
int mw_csv_logger_write(mw_csv_logger *logger,
|
||||
const char *context,
|
||||
long step_index,
|
||||
const mw_report *report);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
46
include/mightywatt_sequence.h
Normal file
46
include/mightywatt_sequence.h
Normal file
@ -0,0 +1,46 @@
|
||||
#ifndef MIGHTYWATT_SEQUENCE_H
|
||||
#define MIGHTYWATT_SEQUENCE_H
|
||||
|
||||
#include "mightywatt_app.h"
|
||||
#include "mightywatt_log.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
const char *csv_path;
|
||||
int sample_period_ms_override;
|
||||
bool safe_on_abort;
|
||||
mw_csv_units_mode csv_units_mode;
|
||||
} mw_sequence_run_options;
|
||||
|
||||
typedef struct {
|
||||
char name[64];
|
||||
int sample_period_ms;
|
||||
size_t steps_total;
|
||||
size_t steps_completed;
|
||||
size_t samples_written;
|
||||
bool aborted;
|
||||
bool abort_sequence_ran;
|
||||
size_t abort_steps_completed;
|
||||
bool last_report_valid;
|
||||
mw_report last_report;
|
||||
} mw_sequence_result;
|
||||
|
||||
int mw_sequence_run_file(mw_app *app,
|
||||
const char *json_path,
|
||||
const mw_sequence_run_options *options,
|
||||
mw_sequence_result *out_result,
|
||||
char *error_text,
|
||||
size_t error_text_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
549
src/mightywatt.c
Normal file
549
src/mightywatt.c
Normal file
@ -0,0 +1,549 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "mightywatt.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <termios.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct mw_device {
|
||||
int fd;
|
||||
char port_path[256];
|
||||
char last_error[256];
|
||||
};
|
||||
|
||||
enum {
|
||||
MW_CMD_SERIES_RESISTANCE = 28,
|
||||
MW_CMD_REMOTE = 29,
|
||||
MW_CMD_QDC = 30,
|
||||
MW_CMD_IDN = 31,
|
||||
MW_REPORT_LEN = 7,
|
||||
};
|
||||
|
||||
static void mw_set_error(mw_device *dev, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
if (!dev) {
|
||||
return;
|
||||
}
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(dev->last_error, sizeof(dev->last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
const char *mw_last_error(const mw_device *dev) {
|
||||
return dev ? dev->last_error : "invalid device";
|
||||
}
|
||||
|
||||
static int mw_sleep_ms(int ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
|
||||
while (nanosleep(&ts, &ts) == -1) {
|
||||
if (errno != EINTR) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_write_all(mw_device *dev, const uint8_t *data, size_t len) {
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
ssize_t rc = write(dev->fd, data + sent, len - sent);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
mw_set_error(dev, "write failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
sent += (size_t)rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_read_exact(mw_device *dev, uint8_t *data, size_t len) {
|
||||
size_t got = 0;
|
||||
while (got < len) {
|
||||
ssize_t rc = read(dev->fd, data + got, len - got);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
mw_set_error(dev, "read failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
mw_set_error(dev, "read timed out");
|
||||
return -1;
|
||||
}
|
||||
got += (size_t)rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_read_line(mw_device *dev, char *buffer, size_t buffer_size) {
|
||||
size_t pos = 0;
|
||||
if (!buffer || buffer_size == 0) {
|
||||
mw_set_error(dev, "internal buffer size error");
|
||||
return -1;
|
||||
}
|
||||
while (pos + 1 < buffer_size) {
|
||||
unsigned char c;
|
||||
ssize_t rc = read(dev->fd, &c, 1);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
mw_set_error(dev, "read line failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
mw_set_error(dev, "read line timed out");
|
||||
return -1;
|
||||
}
|
||||
if (c == '\n') {
|
||||
break;
|
||||
}
|
||||
if (c == '\r') {
|
||||
continue;
|
||||
}
|
||||
buffer[pos++] = (char)c;
|
||||
}
|
||||
buffer[pos] = '\0';
|
||||
if (pos + 1 == buffer_size) {
|
||||
mw_set_error(dev, "response line too long");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_flush_io(mw_device *dev) {
|
||||
if (tcflush(dev->fd, TCIOFLUSH) != 0) {
|
||||
mw_set_error(dev, "tcflush failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_configure_port(mw_device *dev) {
|
||||
struct termios tio;
|
||||
if (tcgetattr(dev->fd, &tio) != 0) {
|
||||
mw_set_error(dev, "tcgetattr failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
cfmakeraw(&tio);
|
||||
if (cfsetispeed(&tio, B115200) != 0 || cfsetospeed(&tio, B115200) != 0) {
|
||||
mw_set_error(dev, "failed to set serial speed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
tio.c_cflag |= (CLOCAL | CREAD);
|
||||
tio.c_cflag &= ~PARENB;
|
||||
tio.c_cflag &= ~CSTOPB;
|
||||
tio.c_cflag &= ~CSIZE;
|
||||
tio.c_cflag |= CS8;
|
||||
#ifdef CRTSCTS
|
||||
tio.c_cflag &= ~CRTSCTS;
|
||||
#endif
|
||||
#ifdef HUPCL
|
||||
tio.c_cflag &= ~HUPCL;
|
||||
#endif
|
||||
tio.c_iflag &= ~(IXON | IXOFF | IXANY);
|
||||
tio.c_cc[VMIN] = 0;
|
||||
tio.c_cc[VTIME] = 3;
|
||||
|
||||
if (tcsetattr(dev->fd, TCSANOW, &tio) != 0) {
|
||||
mw_set_error(dev, "tcsetattr failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_open(mw_device **out_dev, const char *port_path, int settle_ms) {
|
||||
mw_device *dev;
|
||||
int fd;
|
||||
|
||||
if (!out_dev || !port_path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_dev = NULL;
|
||||
dev = calloc(1, sizeof(*dev));
|
||||
if (!dev) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
fd = open(port_path, O_RDWR | O_NOCTTY | O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
snprintf(dev->last_error, sizeof(dev->last_error), "open failed: %s", strerror(errno));
|
||||
free(dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
dev->fd = fd;
|
||||
snprintf(dev->port_path, sizeof(dev->port_path), "%s", port_path);
|
||||
|
||||
if (mw_configure_port(dev) != 0) {
|
||||
close(fd);
|
||||
free(dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (settle_ms < 0) {
|
||||
settle_ms = 0;
|
||||
}
|
||||
if (settle_ms > 0 && mw_sleep_ms(settle_ms) != 0) {
|
||||
mw_set_error(dev, "sleep interrupted unexpectedly");
|
||||
close(fd);
|
||||
free(dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mw_flush_io(dev) != 0) {
|
||||
close(fd);
|
||||
free(dev);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_dev = dev;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mw_close(mw_device *dev) {
|
||||
if (!dev) {
|
||||
return;
|
||||
}
|
||||
if (dev->fd >= 0) {
|
||||
close(dev->fd);
|
||||
}
|
||||
free(dev);
|
||||
}
|
||||
|
||||
int mw_identify(mw_device *dev) {
|
||||
char line[128];
|
||||
uint8_t cmd = MW_CMD_IDN;
|
||||
int tries;
|
||||
|
||||
if (!dev) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (tries = 0; tries < 3; ++tries) {
|
||||
if (mw_flush_io(dev) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_write_all(dev, &cmd, 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_line(dev, line, sizeof(line)) == 0 && strcmp(line, "MightyWatt") == 0) {
|
||||
return 0;
|
||||
}
|
||||
mw_sleep_ms(150);
|
||||
}
|
||||
|
||||
mw_set_error(dev, "device did not identify as MightyWatt");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int mw_parse_u32(mw_device *dev, const char *line, uint32_t *out) {
|
||||
char *end = NULL;
|
||||
unsigned long value;
|
||||
errno = 0;
|
||||
value = strtoul(line, &end, 10);
|
||||
if (errno != 0 || end == line || *end != '\0') {
|
||||
mw_set_error(dev, "invalid numeric response: '%s'", line);
|
||||
return -1;
|
||||
}
|
||||
*out = (uint32_t)value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_query_capabilities(mw_device *dev, mw_capabilities *caps) {
|
||||
uint8_t cmd = MW_CMD_QDC;
|
||||
char line[128];
|
||||
uint32_t values[7];
|
||||
int i;
|
||||
|
||||
if (!dev || !caps) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(caps, 0, sizeof(*caps));
|
||||
if (mw_flush_io(dev) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_write_all(dev, &cmd, 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mw_read_line(dev, caps->firmware_version, sizeof(caps->firmware_version)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_line(dev, caps->board_revision, sizeof(caps->board_revision)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < 7; ++i) {
|
||||
if (mw_read_line(dev, line, sizeof(line)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_parse_u32(dev, line, &values[i]) != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
caps->max_current_dac_ma = values[0];
|
||||
caps->max_current_adc_ma = values[1];
|
||||
caps->max_voltage_dac_mv = values[2];
|
||||
caps->max_voltage_adc_mv = values[3];
|
||||
caps->max_power_mw = values[4];
|
||||
caps->dvm_input_resistance_ohm = values[5];
|
||||
caps->temperature_threshold_c = values[6];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mw_decode_report(const uint8_t raw[MW_REPORT_LEN], mw_report *report) {
|
||||
report->current_ma = (uint16_t)(((uint16_t)raw[0] << 8) | raw[1]);
|
||||
report->voltage_mv = (uint16_t)(((uint16_t)raw[2] << 8) | raw[3]);
|
||||
report->temperature_c = raw[4];
|
||||
report->remote = raw[5] ? true : false;
|
||||
report->status = raw[6];
|
||||
}
|
||||
|
||||
int mw_get_report(mw_device *dev, mw_report *report) {
|
||||
uint8_t cmd = 0;
|
||||
uint8_t raw[MW_REPORT_LEN];
|
||||
|
||||
if (!dev || !report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_write_all(dev, &cmd, 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_exact(dev, raw, sizeof(raw)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
mw_decode_report(raw, report);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_set(mw_device *dev, mw_mode mode, uint32_t milli_units, mw_report *report) {
|
||||
uint8_t raw[4];
|
||||
size_t len;
|
||||
|
||||
if (!dev || !report) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case MW_MODE_CURRENT:
|
||||
case MW_MODE_VOLTAGE:
|
||||
case MW_MODE_VOLTAGE_INVERTED:
|
||||
if (milli_units > 0xFFFFu) {
|
||||
mw_set_error(dev, "%s target out of 16-bit range", mw_mode_name(mode));
|
||||
return -1;
|
||||
}
|
||||
len = 3;
|
||||
raw[0] = (uint8_t)(0x80u | (2u << 5) | (uint8_t)mode);
|
||||
raw[1] = (uint8_t)((milli_units >> 8) & 0xFFu);
|
||||
raw[2] = (uint8_t)(milli_units & 0xFFu);
|
||||
break;
|
||||
case MW_MODE_POWER:
|
||||
case MW_MODE_RESISTANCE:
|
||||
if (milli_units > 0xFFFFFFu) {
|
||||
mw_set_error(dev, "%s target out of 24-bit range", mw_mode_name(mode));
|
||||
return -1;
|
||||
}
|
||||
len = 4;
|
||||
raw[0] = (uint8_t)(0x80u | (3u << 5) | (uint8_t)mode);
|
||||
raw[1] = (uint8_t)((milli_units >> 16) & 0xFFu);
|
||||
raw[2] = (uint8_t)((milli_units >> 8) & 0xFFu);
|
||||
raw[3] = (uint8_t)(milli_units & 0xFFu);
|
||||
break;
|
||||
default:
|
||||
mw_set_error(dev, "unsupported mode");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mw_write_all(dev, raw, len) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t reply[MW_REPORT_LEN];
|
||||
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
mw_decode_report(reply, report);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_set_remote(mw_device *dev, bool enable, mw_report *report) {
|
||||
uint8_t cmd[2];
|
||||
uint8_t reply[MW_REPORT_LEN];
|
||||
|
||||
if (!dev || !report) {
|
||||
return -1;
|
||||
}
|
||||
cmd[0] = (uint8_t)(0x80u | (1u << 5) | MW_CMD_REMOTE);
|
||||
cmd[1] = enable ? 1u : 0u;
|
||||
if (mw_write_all(dev, cmd, sizeof(cmd)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
mw_decode_report(reply, report);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_set_series_resistance(mw_device *dev, uint16_t milliohm, mw_report *report) {
|
||||
uint8_t cmd[3];
|
||||
uint8_t reply[MW_REPORT_LEN];
|
||||
|
||||
if (!dev || !report) {
|
||||
return -1;
|
||||
}
|
||||
cmd[0] = (uint8_t)(0x80u | (2u << 5) | MW_CMD_SERIES_RESISTANCE);
|
||||
cmd[1] = (uint8_t)((milliohm >> 8) & 0xFFu);
|
||||
cmd[2] = (uint8_t)(milliohm & 0xFFu);
|
||||
if (mw_write_all(dev, cmd, sizeof(cmd)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
mw_decode_report(reply, report);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_get_series_resistance(mw_device *dev, uint16_t *milliohm) {
|
||||
uint8_t cmd = MW_CMD_SERIES_RESISTANCE;
|
||||
uint32_t value;
|
||||
char line[64];
|
||||
|
||||
if (!dev || !milliohm) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_flush_io(dev) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_write_all(dev, &cmd, 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_read_line(dev, line, sizeof(line)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_parse_u32(dev, line, &value) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (value > UINT16_MAX) {
|
||||
mw_set_error(dev, "series resistance out of range: %u", value);
|
||||
return -1;
|
||||
}
|
||||
*milliohm = (uint16_t)value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t mw_status_string(uint8_t status, char *buffer, size_t buffer_size) {
|
||||
int n = 0;
|
||||
int first = 1;
|
||||
if (!buffer || buffer_size == 0) {
|
||||
return 0;
|
||||
}
|
||||
buffer[0] = '\0';
|
||||
if (status == 0) {
|
||||
snprintf(buffer, buffer_size, "READY");
|
||||
return strlen(buffer);
|
||||
}
|
||||
#define MW_APPEND(flag, text) \
|
||||
do { \
|
||||
if (status & (flag)) { \
|
||||
n += snprintf(buffer + n, buffer_size > (size_t)n ? buffer_size - (size_t)n : 0, \
|
||||
"%s%s", first ? "" : "|", (text)); \
|
||||
first = 0; \
|
||||
} \
|
||||
} while (0)
|
||||
MW_APPEND(MW_STATUS_CURRENT_OVERLOAD, "CURRENT_OVERLOAD");
|
||||
MW_APPEND(MW_STATUS_VOLTAGE_OVERLOAD, "VOLTAGE_OVERLOAD");
|
||||
MW_APPEND(MW_STATUS_POWER_OVERLOAD, "POWER_OVERLOAD");
|
||||
MW_APPEND(MW_STATUS_OVERHEAT, "OVERHEAT");
|
||||
#undef MW_APPEND
|
||||
return strlen(buffer);
|
||||
}
|
||||
|
||||
const char *mw_mode_name(mw_mode mode) {
|
||||
switch (mode) {
|
||||
case MW_MODE_CURRENT: return "current";
|
||||
case MW_MODE_VOLTAGE: return "voltage";
|
||||
case MW_MODE_POWER: return "power";
|
||||
case MW_MODE_RESISTANCE: return "resistance";
|
||||
case MW_MODE_VOLTAGE_INVERTED: return "voltage_inverted";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t mw_report_power_mw(const mw_report *report) {
|
||||
if (!report) {
|
||||
return 0;
|
||||
}
|
||||
return ((uint32_t)report->current_ma * (uint32_t)report->voltage_mv) / 1000u;
|
||||
}
|
||||
|
||||
uint32_t mw_capability_limit_for_mode(const mw_capabilities *caps, mw_mode mode) {
|
||||
if (!caps) {
|
||||
return 0;
|
||||
}
|
||||
switch (mode) {
|
||||
case MW_MODE_CURRENT:
|
||||
return (caps->max_current_adc_ma > caps->max_current_dac_ma)
|
||||
? caps->max_current_adc_ma : caps->max_current_dac_ma;
|
||||
case MW_MODE_VOLTAGE:
|
||||
return (caps->max_voltage_adc_mv > caps->max_voltage_dac_mv)
|
||||
? caps->max_voltage_adc_mv : caps->max_voltage_dac_mv;
|
||||
case MW_MODE_VOLTAGE_INVERTED:
|
||||
return caps->max_voltage_adc_mv;
|
||||
case MW_MODE_POWER:
|
||||
return caps->max_power_mw;
|
||||
case MW_MODE_RESISTANCE:
|
||||
return caps->dvm_input_resistance_ohm * 1000u;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int mw_validate_target(const mw_capabilities *caps, mw_mode mode, uint32_t milli_units,
|
||||
char *buffer, size_t buffer_size) {
|
||||
uint32_t limit;
|
||||
|
||||
if (buffer && buffer_size > 0) {
|
||||
buffer[0] = '\0';
|
||||
}
|
||||
if (!caps) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
limit = mw_capability_limit_for_mode(caps, mode);
|
||||
if (limit == 0 && mode != MW_MODE_CURRENT) {
|
||||
if (buffer && buffer_size > 0) {
|
||||
snprintf(buffer, buffer_size, "unknown limit for mode %s", mw_mode_name(mode));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (milli_units > limit) {
|
||||
if (buffer && buffer_size > 0) {
|
||||
snprintf(buffer, buffer_size, "%s target %.3f exceeds device limit %.3f",
|
||||
mw_mode_name(mode), milli_units / 1000.0, limit / 1000.0);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
252
src/mightywatt_app.c
Normal file
252
src/mightywatt_app.c
Normal file
@ -0,0 +1,252 @@
|
||||
#include "mightywatt_app.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct mw_app {
|
||||
mw_device *dev;
|
||||
mw_app_state state;
|
||||
char last_error[256];
|
||||
};
|
||||
|
||||
static void mw_app_set_error(mw_app *app, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(app->last_error, sizeof(app->last_error), fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void mw_app_copy_report(mw_app *app, const mw_report *report) {
|
||||
if (!app || !report) {
|
||||
return;
|
||||
}
|
||||
app->state.last_report = *report;
|
||||
app->state.report_valid = true;
|
||||
}
|
||||
|
||||
static void mw_app_copy_caps(mw_app *app, const mw_capabilities *caps) {
|
||||
if (!app || !caps) {
|
||||
return;
|
||||
}
|
||||
app->state.capabilities = *caps;
|
||||
app->state.capabilities_valid = true;
|
||||
}
|
||||
|
||||
static int mw_app_fail_from_device(mw_app *app, const char *prefix) {
|
||||
mw_app_set_error(app, "%s: %s", prefix, mw_last_error(app->dev));
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int mw_app_check_target(mw_app *app, mw_mode mode, uint32_t milli_units) {
|
||||
char reason[160];
|
||||
if (mw_validate_target(app->state.capabilities_valid ? &app->state.capabilities : NULL,
|
||||
mode, milli_units, reason, sizeof(reason)) != 0) {
|
||||
mw_app_set_error(app, "%s", reason[0] ? reason : "invalid target");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mw_app_store_target(mw_app *app, mw_mode mode, uint32_t milli_units) {
|
||||
app->state.target_valid = true;
|
||||
app->state.target_mode = mode;
|
||||
app->state.target_milli_units = milli_units;
|
||||
if (milli_units > 0) {
|
||||
app->state.restore_target_valid = true;
|
||||
app->state.restore_target_mode = mode;
|
||||
app->state.restore_target_milli_units = milli_units;
|
||||
}
|
||||
}
|
||||
|
||||
int mw_app_open(mw_app **out_app, const char *port_path, int settle_ms) {
|
||||
mw_app *app;
|
||||
mw_capabilities caps;
|
||||
|
||||
if (!out_app || !port_path) {
|
||||
return -1;
|
||||
}
|
||||
*out_app = NULL;
|
||||
|
||||
app = calloc(1, sizeof(*app));
|
||||
if (!app) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mw_open(&app->dev, port_path, settle_ms) != 0) {
|
||||
mw_app_set_error(app, "%s", app->dev ? mw_last_error(app->dev) : "open failed");
|
||||
mw_close(app->dev);
|
||||
free(app);
|
||||
return -1;
|
||||
}
|
||||
if (mw_identify(app->dev) != 0) {
|
||||
mw_app_set_error(app, "identify failed: %s", mw_last_error(app->dev));
|
||||
mw_close(app->dev);
|
||||
free(app);
|
||||
return -1;
|
||||
}
|
||||
if (mw_query_capabilities(app->dev, &caps) != 0) {
|
||||
mw_app_set_error(app, "capabilities failed: %s", mw_last_error(app->dev));
|
||||
mw_close(app->dev);
|
||||
free(app);
|
||||
return -1;
|
||||
}
|
||||
mw_app_copy_caps(app, &caps);
|
||||
*out_app = app;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mw_app_close(mw_app *app) {
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
mw_close(app->dev);
|
||||
free(app);
|
||||
}
|
||||
|
||||
const char *mw_app_last_error(const mw_app *app) {
|
||||
return app ? app->last_error : "invalid app";
|
||||
}
|
||||
|
||||
int mw_app_refresh_capabilities(mw_app *app, mw_capabilities *out_caps) {
|
||||
mw_capabilities caps;
|
||||
if (!app) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_query_capabilities(app->dev, &caps) != 0) {
|
||||
return mw_app_fail_from_device(app, "capabilities failed");
|
||||
}
|
||||
mw_app_copy_caps(app, &caps);
|
||||
if (out_caps) {
|
||||
*out_caps = caps;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_get_report(mw_app *app, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_get_report(app->dev, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, "report failed");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_set_target(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_app_check_target(app, mode, milli_units) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_set(app->dev, mode, milli_units, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, "set target failed");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
mw_app_store_target(app, mode, milli_units);
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_load_off(mw_app *app, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_set(app->dev, MW_MODE_CURRENT, 0, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, "load-off failed");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
app->state.target_valid = true;
|
||||
app->state.target_mode = MW_MODE_CURRENT;
|
||||
app->state.target_milli_units = 0;
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_load_on(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report) {
|
||||
return mw_app_set_target(app, mode, milli_units, out_report);
|
||||
}
|
||||
|
||||
int mw_app_restore_target(mw_app *app, mw_report *out_report) {
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (!app->state.restore_target_valid) {
|
||||
mw_app_set_error(app, "no restorable target in current process state");
|
||||
return -1;
|
||||
}
|
||||
return mw_app_set_target(app,
|
||||
app->state.restore_target_mode,
|
||||
app->state.restore_target_milli_units,
|
||||
out_report);
|
||||
}
|
||||
|
||||
int mw_app_safe(mw_app *app, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_app_load_off(app, &report) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_set_remote(app->dev, false, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, "safe failed while disabling remote");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_set_remote(mw_app *app, bool enable, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_set_remote(app->dev, enable, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, enable ? "remote on failed" : "remote off failed");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_get_series_resistance(mw_app *app, uint16_t *milliohm) {
|
||||
if (!app || !milliohm) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_get_series_resistance(app->dev, milliohm) != 0) {
|
||||
return mw_app_fail_from_device(app, "get-series failed");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_set_series_resistance(mw_app *app, uint16_t milliohm, mw_report *out_report) {
|
||||
mw_report report;
|
||||
if (!app || !out_report) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_set_series_resistance(app->dev, milliohm, &report) != 0) {
|
||||
return mw_app_fail_from_device(app, "set-series failed");
|
||||
}
|
||||
mw_app_copy_report(app, &report);
|
||||
*out_report = report;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_app_get_state(const mw_app *app, mw_app_state *out_state) {
|
||||
if (!app || !out_state) {
|
||||
return -1;
|
||||
}
|
||||
*out_state = app->state;
|
||||
return 0;
|
||||
}
|
||||
723
src/mightywatt_controller.c
Normal file
723
src/mightywatt_controller.c
Normal file
@ -0,0 +1,723 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "mightywatt_controller.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
mw_controller_command_kind kind;
|
||||
mw_mode mode;
|
||||
uint32_t milli_units;
|
||||
bool enable;
|
||||
uint16_t series_milliohm;
|
||||
uint64_t id;
|
||||
} mw_controller_command;
|
||||
|
||||
struct mw_controller {
|
||||
pthread_t thread;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
bool thread_started;
|
||||
bool stop_requested;
|
||||
mw_controller_config config;
|
||||
mw_controller_snapshot snapshot;
|
||||
mw_app *app;
|
||||
mw_controller_command *queue;
|
||||
size_t queue_capacity;
|
||||
size_t queue_head;
|
||||
size_t queue_tail;
|
||||
size_t queue_count;
|
||||
uint64_t next_command_id;
|
||||
uint64_t next_retry_due_ms;
|
||||
uint64_t next_poll_due_ms;
|
||||
};
|
||||
|
||||
static uint64_t mw_now_ms(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000L);
|
||||
}
|
||||
|
||||
static void mw_abs_timespec_from_now(int timeout_ms, struct timespec *out_ts) {
|
||||
uint64_t now_ms;
|
||||
if (!out_ts) {
|
||||
return;
|
||||
}
|
||||
if (timeout_ms < 0) {
|
||||
timeout_ms = 0;
|
||||
}
|
||||
now_ms = mw_now_ms() + (uint64_t)timeout_ms;
|
||||
out_ts->tv_sec = (time_t)(now_ms / 1000ULL);
|
||||
out_ts->tv_nsec = (long)(now_ms % 1000ULL) * 1000000L;
|
||||
}
|
||||
|
||||
static void mw_snapshot_touch_locked(mw_controller *controller) {
|
||||
controller->snapshot.snapshot_seq++;
|
||||
pthread_cond_broadcast(&controller->cond);
|
||||
}
|
||||
|
||||
static void mw_snapshot_set_error_locked(mw_controller *controller, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(controller->snapshot.last_error,
|
||||
sizeof(controller->snapshot.last_error),
|
||||
fmt,
|
||||
ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void mw_snapshot_set_completed_locked(mw_controller *controller,
|
||||
const mw_controller_command *cmd,
|
||||
int result,
|
||||
const char *error_text) {
|
||||
controller->snapshot.last_completed_command_id = cmd ? cmd->id : 0;
|
||||
controller->snapshot.last_completed_kind = cmd ? cmd->kind : MW_CTRL_CMD_NONE;
|
||||
controller->snapshot.last_completed_result = result;
|
||||
snprintf(controller->snapshot.last_completed_error,
|
||||
sizeof(controller->snapshot.last_completed_error),
|
||||
"%s",
|
||||
error_text ? error_text : "");
|
||||
}
|
||||
|
||||
static void mw_snapshot_sync_app_state_locked(mw_controller *controller) {
|
||||
mw_app_state state;
|
||||
if (!controller || !controller->app) {
|
||||
controller->snapshot.app_state_valid = false;
|
||||
return;
|
||||
}
|
||||
if (mw_app_get_state(controller->app, &state) == 0) {
|
||||
controller->snapshot.app_state = state;
|
||||
controller->snapshot.app_state_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void mw_disconnect_locked(mw_controller *controller, const char *reason) {
|
||||
mw_app *app_to_close = controller->app;
|
||||
bool was_connected = controller->snapshot.connected;
|
||||
|
||||
controller->app = NULL;
|
||||
controller->snapshot.connected = false;
|
||||
controller->snapshot.connection_state = controller->stop_requested
|
||||
? MW_CONTROLLER_STOPPED
|
||||
: MW_CONTROLLER_RECONNECT_WAIT;
|
||||
if (reason && reason[0] != '\0') {
|
||||
mw_snapshot_set_error_locked(controller, "%s", reason);
|
||||
}
|
||||
if (was_connected) {
|
||||
controller->snapshot.reconnect_count++;
|
||||
}
|
||||
controller->next_retry_due_ms = mw_now_ms() + (uint64_t)controller->config.reconnect_ms;
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
mw_app_close(app_to_close);
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
}
|
||||
|
||||
static int mw_queue_push_locked(mw_controller *controller,
|
||||
const mw_controller_command *command,
|
||||
uint64_t *out_command_id) {
|
||||
mw_controller_command stored;
|
||||
char validation[160];
|
||||
|
||||
if (!controller || !command) {
|
||||
return -1;
|
||||
}
|
||||
if (controller->stop_requested || !controller->snapshot.running) {
|
||||
mw_snapshot_set_error_locked(controller, "controller is stopping or not running");
|
||||
return -1;
|
||||
}
|
||||
if (controller->queue_count >= controller->queue_capacity) {
|
||||
mw_snapshot_set_error_locked(controller, "command queue is full");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (command->kind == MW_CTRL_CMD_SET_TARGET || command->kind == MW_CTRL_CMD_LOAD_ON) {
|
||||
const mw_capabilities *caps = NULL;
|
||||
if (controller->snapshot.app_state_valid &&
|
||||
controller->snapshot.app_state.capabilities_valid) {
|
||||
caps = &controller->snapshot.app_state.capabilities;
|
||||
}
|
||||
if (mw_validate_target(caps,
|
||||
command->mode,
|
||||
command->milli_units,
|
||||
validation,
|
||||
sizeof(validation)) != 0) {
|
||||
mw_snapshot_set_error_locked(controller,
|
||||
"%s",
|
||||
validation[0] ? validation : "invalid target");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
stored = *command;
|
||||
stored.id = ++controller->next_command_id;
|
||||
controller->queue[controller->queue_tail] = stored;
|
||||
controller->queue_tail = (controller->queue_tail + 1U) % controller->queue_capacity;
|
||||
controller->queue_count++;
|
||||
controller->snapshot.pending_commands = controller->queue_count;
|
||||
controller->snapshot.last_queued_command_id = stored.id;
|
||||
mw_snapshot_touch_locked(controller);
|
||||
if (out_command_id) {
|
||||
*out_command_id = stored.id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_queue_pop_locked(mw_controller *controller, mw_controller_command *out_command) {
|
||||
if (!controller || !out_command || controller->queue_count == 0U) {
|
||||
return -1;
|
||||
}
|
||||
*out_command = controller->queue[controller->queue_head];
|
||||
controller->queue_head = (controller->queue_head + 1U) % controller->queue_capacity;
|
||||
controller->queue_count--;
|
||||
controller->snapshot.pending_commands = controller->queue_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_try_keep_connection(mw_controller *controller) {
|
||||
mw_report report;
|
||||
if (!controller || !controller->app) {
|
||||
return -1;
|
||||
}
|
||||
if (mw_app_get_report(controller->app, &report) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mw_handle_connected_failure_locked(mw_controller *controller,
|
||||
const mw_controller_command *command,
|
||||
const char *error_text,
|
||||
bool may_still_be_connected) {
|
||||
mw_snapshot_set_completed_locked(controller, command, -1, error_text);
|
||||
controller->snapshot.command_error_count++;
|
||||
mw_snapshot_set_error_locked(controller, "%s", error_text ? error_text : "operation failed");
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
|
||||
if (may_still_be_connected && mw_try_keep_connection(controller) == 0) {
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
mw_snapshot_sync_app_state_locked(controller);
|
||||
mw_snapshot_touch_locked(controller);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
mw_disconnect_locked(controller, error_text ? error_text : "device communication failed");
|
||||
}
|
||||
|
||||
static void mw_execute_connect(mw_controller *controller) {
|
||||
mw_app *new_app = NULL;
|
||||
int rc;
|
||||
char error_buffer[MW_CONTROLLER_ERROR_MAX + 1] = "";
|
||||
|
||||
rc = mw_app_open(&new_app,
|
||||
controller->config.port_path,
|
||||
controller->config.settle_ms);
|
||||
if (rc != 0) {
|
||||
if (new_app) {
|
||||
snprintf(error_buffer, sizeof(error_buffer), "%s", mw_app_last_error(new_app));
|
||||
mw_app_close(new_app);
|
||||
} else {
|
||||
snprintf(error_buffer, sizeof(error_buffer), "connect failed");
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->snapshot.connect_attempts++;
|
||||
controller->snapshot.connected = false;
|
||||
controller->snapshot.connection_state = MW_CONTROLLER_RECONNECT_WAIT;
|
||||
controller->next_retry_due_ms = mw_now_ms() + (uint64_t)controller->config.reconnect_ms;
|
||||
mw_snapshot_set_error_locked(controller, "%s", error_buffer);
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->snapshot.connect_attempts++;
|
||||
controller->app = new_app;
|
||||
controller->snapshot.connected = true;
|
||||
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTED;
|
||||
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
|
||||
controller->snapshot.last_error[0] = '\0';
|
||||
mw_snapshot_sync_app_state_locked(controller);
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
}
|
||||
|
||||
static void mw_execute_poll(mw_controller *controller) {
|
||||
mw_report report;
|
||||
(void)report;
|
||||
if (mw_app_get_report(controller->app, &report) != 0) {
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->snapshot.poll_error_count++;
|
||||
mw_disconnect_locked(controller, mw_app_last_error(controller->app));
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->snapshot.poll_success_count++;
|
||||
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
|
||||
controller->snapshot.last_error[0] = '\0';
|
||||
mw_snapshot_sync_app_state_locked(controller);
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
}
|
||||
|
||||
static void mw_execute_command(mw_controller *controller, const mw_controller_command *command) {
|
||||
int rc = -1;
|
||||
mw_report report;
|
||||
uint16_t series_milliohm = 0;
|
||||
char error_text[MW_CONTROLLER_ERROR_MAX + 1] = "";
|
||||
|
||||
switch (command->kind) {
|
||||
case MW_CTRL_CMD_SET_TARGET:
|
||||
rc = mw_app_set_target(controller->app,
|
||||
command->mode,
|
||||
command->milli_units,
|
||||
&report);
|
||||
break;
|
||||
case MW_CTRL_CMD_LOAD_ON:
|
||||
rc = mw_app_load_on(controller->app,
|
||||
command->mode,
|
||||
command->milli_units,
|
||||
&report);
|
||||
break;
|
||||
case MW_CTRL_CMD_LOAD_OFF:
|
||||
rc = mw_app_load_off(controller->app, &report);
|
||||
break;
|
||||
case MW_CTRL_CMD_RESTORE_TARGET:
|
||||
rc = mw_app_restore_target(controller->app, &report);
|
||||
break;
|
||||
case MW_CTRL_CMD_SAFE:
|
||||
rc = mw_app_safe(controller->app, &report);
|
||||
break;
|
||||
case MW_CTRL_CMD_SET_REMOTE:
|
||||
rc = mw_app_set_remote(controller->app, command->enable, &report);
|
||||
break;
|
||||
case MW_CTRL_CMD_SET_SERIES_RESISTANCE:
|
||||
rc = mw_app_set_series_resistance(controller->app,
|
||||
command->series_milliohm,
|
||||
&report);
|
||||
break;
|
||||
case MW_CTRL_CMD_REFRESH_CAPABILITIES:
|
||||
rc = mw_app_refresh_capabilities(controller->app, NULL);
|
||||
break;
|
||||
case MW_CTRL_CMD_GET_SERIES_RESISTANCE:
|
||||
rc = mw_app_get_series_resistance(controller->app, &series_milliohm);
|
||||
break;
|
||||
default:
|
||||
snprintf(error_text, sizeof(error_text), "unsupported command kind");
|
||||
break;
|
||||
}
|
||||
|
||||
if (rc != 0 && error_text[0] == '\0') {
|
||||
snprintf(error_text, sizeof(error_text), "%s", mw_app_last_error(controller->app));
|
||||
}
|
||||
|
||||
if (rc != 0) {
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
mw_handle_connected_failure_locked(controller,
|
||||
command,
|
||||
error_text,
|
||||
true);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->snapshot.command_success_count++;
|
||||
controller->snapshot.last_error[0] = '\0';
|
||||
mw_snapshot_set_completed_locked(controller, command, 0, "");
|
||||
if (command->kind == MW_CTRL_CMD_GET_SERIES_RESISTANCE) {
|
||||
controller->snapshot.series_resistance_valid = true;
|
||||
controller->snapshot.series_resistance_milliohm = series_milliohm;
|
||||
}
|
||||
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
|
||||
mw_snapshot_sync_app_state_locked(controller);
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
}
|
||||
|
||||
static void *mw_controller_thread_main(void *arg) {
|
||||
mw_controller *controller = (mw_controller *)arg;
|
||||
|
||||
for (;;) {
|
||||
bool stop_now = false;
|
||||
bool do_connect = false;
|
||||
bool do_poll = false;
|
||||
mw_controller_command command;
|
||||
bool have_command = false;
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
for (;;) {
|
||||
uint64_t now_ms;
|
||||
struct timespec wake_ts;
|
||||
int wait_ms;
|
||||
|
||||
if (controller->stop_requested) {
|
||||
stop_now = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!controller->app) {
|
||||
now_ms = mw_now_ms();
|
||||
if (now_ms >= controller->next_retry_due_ms) {
|
||||
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTING;
|
||||
controller->snapshot.connected = false;
|
||||
mw_snapshot_touch_locked(controller);
|
||||
do_connect = true;
|
||||
break;
|
||||
}
|
||||
wait_ms = (int)(controller->next_retry_due_ms - now_ms);
|
||||
mw_abs_timespec_from_now(wait_ms, &wake_ts);
|
||||
pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (controller->queue_count > 0U) {
|
||||
if (mw_queue_pop_locked(controller, &command) == 0) {
|
||||
have_command = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
now_ms = mw_now_ms();
|
||||
if (now_ms >= controller->next_poll_due_ms) {
|
||||
do_poll = true;
|
||||
break;
|
||||
}
|
||||
|
||||
wait_ms = (int)(controller->next_poll_due_ms - now_ms);
|
||||
mw_abs_timespec_from_now(wait_ms, &wake_ts);
|
||||
pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
|
||||
}
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
|
||||
if (stop_now) {
|
||||
mw_app *app_to_close = NULL;
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
app_to_close = controller->app;
|
||||
controller->app = NULL;
|
||||
controller->snapshot.running = false;
|
||||
controller->snapshot.connected = false;
|
||||
controller->snapshot.connection_state = MW_CONTROLLER_STOPPED;
|
||||
mw_snapshot_touch_locked(controller);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
|
||||
if (app_to_close) {
|
||||
if (controller->config.safe_on_shutdown) {
|
||||
mw_report dummy_report;
|
||||
(void)mw_app_safe(app_to_close, &dummy_report);
|
||||
}
|
||||
mw_app_close(app_to_close);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (do_connect) {
|
||||
mw_execute_connect(controller);
|
||||
} else if (have_command) {
|
||||
mw_execute_command(controller, &command);
|
||||
} else if (do_poll) {
|
||||
mw_execute_poll(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mw_controller_config_init(mw_controller_config *config) {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
memset(config, 0, sizeof(*config));
|
||||
config->settle_ms = 2200;
|
||||
config->poll_interval_ms = 500;
|
||||
config->reconnect_ms = 2000;
|
||||
config->queue_capacity = 32;
|
||||
config->safe_on_shutdown = true;
|
||||
}
|
||||
|
||||
int mw_controller_start(mw_controller **out_controller, const mw_controller_config *config) {
|
||||
mw_controller *controller;
|
||||
size_t capacity;
|
||||
|
||||
if (!out_controller || !config || !config->port_path || config->port_path[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
*out_controller = NULL;
|
||||
|
||||
controller = calloc(1, sizeof(*controller));
|
||||
if (!controller) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
controller->config = *config;
|
||||
capacity = config->queue_capacity > 0U ? config->queue_capacity : 32U;
|
||||
controller->queue = calloc(capacity, sizeof(*controller->queue));
|
||||
if (!controller->queue) {
|
||||
free(controller);
|
||||
return -1;
|
||||
}
|
||||
controller->queue_capacity = capacity;
|
||||
controller->snapshot.running = true;
|
||||
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTING;
|
||||
snprintf(controller->snapshot.port_path,
|
||||
sizeof(controller->snapshot.port_path),
|
||||
"%s",
|
||||
config->port_path);
|
||||
controller->next_retry_due_ms = mw_now_ms();
|
||||
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
|
||||
|
||||
if (pthread_mutex_init(&controller->mutex, NULL) != 0) {
|
||||
free(controller->queue);
|
||||
free(controller);
|
||||
return -1;
|
||||
}
|
||||
if (pthread_cond_init(&controller->cond, NULL) != 0) {
|
||||
pthread_mutex_destroy(&controller->mutex);
|
||||
free(controller->queue);
|
||||
free(controller);
|
||||
return -1;
|
||||
}
|
||||
if (pthread_create(&controller->thread, NULL, mw_controller_thread_main, controller) != 0) {
|
||||
pthread_cond_destroy(&controller->cond);
|
||||
pthread_mutex_destroy(&controller->mutex);
|
||||
free(controller->queue);
|
||||
free(controller);
|
||||
return -1;
|
||||
}
|
||||
|
||||
controller->thread_started = true;
|
||||
*out_controller = controller;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mw_controller_stop(mw_controller *controller) {
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
controller->stop_requested = true;
|
||||
pthread_cond_broadcast(&controller->cond);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
|
||||
if (controller->thread_started) {
|
||||
pthread_join(controller->thread, NULL);
|
||||
}
|
||||
|
||||
pthread_cond_destroy(&controller->cond);
|
||||
pthread_mutex_destroy(&controller->mutex);
|
||||
free(controller->queue);
|
||||
free(controller);
|
||||
}
|
||||
|
||||
int mw_controller_get_snapshot(mw_controller *controller, mw_controller_snapshot *out_snapshot) {
|
||||
if (!controller || !out_snapshot) {
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
*out_snapshot = controller->snapshot;
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_controller_wait_for_update(mw_controller *controller,
|
||||
uint64_t last_seen_snapshot_seq,
|
||||
int timeout_ms,
|
||||
uint64_t *out_snapshot_seq) {
|
||||
int rc = 0;
|
||||
|
||||
if (!controller) {
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
if (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
|
||||
if (timeout_ms < 0) {
|
||||
while (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
|
||||
pthread_cond_wait(&controller->cond, &controller->mutex);
|
||||
}
|
||||
} else {
|
||||
struct timespec wake_ts;
|
||||
mw_abs_timespec_from_now(timeout_ms, &wake_ts);
|
||||
while (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
|
||||
rc = pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
|
||||
if (rc == ETIMEDOUT) {
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return 1;
|
||||
}
|
||||
if (rc != 0) {
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out_snapshot_seq) {
|
||||
*out_snapshot_seq = controller->snapshot.snapshot_seq;
|
||||
}
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mw_controller_enqueue_simple(mw_controller *controller,
|
||||
mw_controller_command_kind kind,
|
||||
mw_mode mode,
|
||||
uint32_t milli_units,
|
||||
bool enable,
|
||||
uint16_t series_milliohm,
|
||||
uint64_t *out_command_id) {
|
||||
mw_controller_command cmd;
|
||||
|
||||
if (!controller) {
|
||||
return -1;
|
||||
}
|
||||
memset(&cmd, 0, sizeof(cmd));
|
||||
cmd.kind = kind;
|
||||
cmd.mode = mode;
|
||||
cmd.milli_units = milli_units;
|
||||
cmd.enable = enable;
|
||||
cmd.series_milliohm = series_milliohm;
|
||||
|
||||
pthread_mutex_lock(&controller->mutex);
|
||||
if (mw_queue_push_locked(controller, &cmd, out_command_id) != 0) {
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return -1;
|
||||
}
|
||||
pthread_cond_broadcast(&controller->cond);
|
||||
pthread_mutex_unlock(&controller->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mw_controller_queue_set_target(mw_controller *controller,
|
||||
mw_mode mode,
|
||||
uint32_t milli_units,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_SET_TARGET,
|
||||
mode,
|
||||
milli_units,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_load_on(mw_controller *controller,
|
||||
mw_mode mode,
|
||||
uint32_t milli_units,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_LOAD_ON,
|
||||
mode,
|
||||
milli_units,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_load_off(mw_controller *controller, uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_LOAD_OFF,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_restore_target(mw_controller *controller, uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_RESTORE_TARGET,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_safe(mw_controller *controller, uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_SAFE,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_set_remote(mw_controller *controller,
|
||||
bool enable,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_SET_REMOTE,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
enable,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_set_series_resistance(mw_controller *controller,
|
||||
uint16_t milliohm,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_SET_SERIES_RESISTANCE,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
milliohm,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_refresh_capabilities(mw_controller *controller,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_REFRESH_CAPABILITIES,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
int mw_controller_queue_get_series_resistance(mw_controller *controller,
|
||||
uint64_t *out_command_id) {
|
||||
return mw_controller_enqueue_simple(controller,
|
||||
MW_CTRL_CMD_GET_SERIES_RESISTANCE,
|
||||
MW_MODE_CURRENT,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
out_command_id);
|
||||
}
|
||||
|
||||
const char *mw_controller_connection_state_name(mw_controller_connection_state state) {
|
||||
switch (state) {
|
||||
case MW_CONTROLLER_STOPPED: return "stopped";
|
||||
case MW_CONTROLLER_CONNECTING: return "connecting";
|
||||
case MW_CONTROLLER_CONNECTED: return "connected";
|
||||
case MW_CONTROLLER_RECONNECT_WAIT: return "reconnect_wait";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const char *mw_controller_command_kind_name(mw_controller_command_kind kind) {
|
||||
switch (kind) {
|
||||
case MW_CTRL_CMD_NONE: return "none";
|
||||
case MW_CTRL_CMD_SET_TARGET: return "set_target";
|
||||
case MW_CTRL_CMD_LOAD_ON: return "load_on";
|
||||
case MW_CTRL_CMD_LOAD_OFF: return "load_off";
|
||||
case MW_CTRL_CMD_RESTORE_TARGET: return "restore_target";
|
||||
case MW_CTRL_CMD_SAFE: return "safe";
|
||||
case MW_CTRL_CMD_SET_REMOTE: return "set_remote";
|
||||
case MW_CTRL_CMD_SET_SERIES_RESISTANCE: return "set_series_resistance";
|
||||
case MW_CTRL_CMD_REFRESH_CAPABILITIES: return "refresh_capabilities";
|
||||
case MW_CTRL_CMD_GET_SERIES_RESISTANCE: return "get_series_resistance";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
122
src/mightywatt_log.c
Normal file
122
src/mightywatt_log.c
Normal file
@ -0,0 +1,122 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "mightywatt_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
struct mw_csv_logger {
|
||||
FILE *fp;
|
||||
struct timespec t0;
|
||||
mw_csv_units_mode units_mode;
|
||||
};
|
||||
|
||||
static double mw_elapsed_seconds(const struct timespec *t0) {
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return (double)(now.tv_sec - t0->tv_sec) + (double)(now.tv_nsec - t0->tv_nsec) / 1000000000.0;
|
||||
}
|
||||
|
||||
static void mw_timestamp_iso8601(char *buffer, size_t buffer_size) {
|
||||
struct timespec now;
|
||||
struct tm tm_now;
|
||||
clock_gettime(CLOCK_REALTIME, &now);
|
||||
gmtime_r(&now.tv_sec, &tm_now);
|
||||
{
|
||||
int millis = (int)(now.tv_nsec / 1000000L);
|
||||
snprintf(buffer,
|
||||
buffer_size,
|
||||
"%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
|
||||
tm_now.tm_year + 1900,
|
||||
tm_now.tm_mon + 1,
|
||||
tm_now.tm_mday,
|
||||
tm_now.tm_hour,
|
||||
tm_now.tm_min,
|
||||
tm_now.tm_sec,
|
||||
millis);
|
||||
}
|
||||
}
|
||||
|
||||
int mw_csv_logger_open(mw_csv_logger **out_logger, const char *path, mw_csv_units_mode units_mode) {
|
||||
mw_csv_logger *logger;
|
||||
if (!out_logger || !path || !*path) {
|
||||
return -1;
|
||||
}
|
||||
*out_logger = NULL;
|
||||
logger = calloc(1, sizeof(*logger));
|
||||
if (!logger) {
|
||||
return -1;
|
||||
}
|
||||
logger->fp = fopen(path, "w");
|
||||
if (!logger->fp) {
|
||||
free(logger);
|
||||
return -1;
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &logger->t0);
|
||||
logger->units_mode = units_mode;
|
||||
if (logger->units_mode == MW_CSV_UNITS_RAW) {
|
||||
fprintf(logger->fp,
|
||||
"timestamp_utc,elapsed_ms,context,step_index,current_ma,voltage_mv,power_mw,temperature_c,remote,status_bits,status_text\n");
|
||||
} else {
|
||||
fprintf(logger->fp,
|
||||
"timestamp_utc,elapsed_s,context,step_index,current_a,voltage_v,power_w,temperature_c,remote,status_bits,status_text\n");
|
||||
}
|
||||
fflush(logger->fp);
|
||||
*out_logger = logger;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mw_csv_logger_close(mw_csv_logger *logger) {
|
||||
if (!logger) {
|
||||
return;
|
||||
}
|
||||
if (logger->fp) {
|
||||
fclose(logger->fp);
|
||||
}
|
||||
free(logger);
|
||||
}
|
||||
|
||||
int mw_csv_logger_write(mw_csv_logger *logger,
|
||||
const char *context,
|
||||
long step_index,
|
||||
const mw_report *report) {
|
||||
char ts[96];
|
||||
char status[128];
|
||||
if (!logger || !logger->fp || !report) {
|
||||
return -1;
|
||||
}
|
||||
mw_timestamp_iso8601(ts, sizeof(ts));
|
||||
mw_status_string(report->status, status, sizeof(status));
|
||||
if (logger->units_mode == MW_CSV_UNITS_RAW) {
|
||||
fprintf(logger->fp,
|
||||
"%s,%.0f,%s,%ld,%u,%u,%u,%u,%s,%u,%s\n",
|
||||
ts,
|
||||
mw_elapsed_seconds(&logger->t0) * 1000.0,
|
||||
context ? context : "",
|
||||
step_index,
|
||||
(unsigned)report->current_ma,
|
||||
(unsigned)report->voltage_mv,
|
||||
(unsigned)mw_report_power_mw(report),
|
||||
(unsigned)report->temperature_c,
|
||||
report->remote ? "true" : "false",
|
||||
(unsigned)report->status,
|
||||
status);
|
||||
} else {
|
||||
fprintf(logger->fp,
|
||||
"%s,%.3f,%s,%ld,%.3f,%.3f,%.3f,%u,%s,%u,%s\n",
|
||||
ts,
|
||||
mw_elapsed_seconds(&logger->t0),
|
||||
context ? context : "",
|
||||
step_index,
|
||||
report->current_ma / 1000.0,
|
||||
report->voltage_mv / 1000.0,
|
||||
mw_report_power_mw(report) / 1000.0,
|
||||
(unsigned)report->temperature_c,
|
||||
report->remote ? "true" : "false",
|
||||
(unsigned)report->status,
|
||||
status);
|
||||
}
|
||||
fflush(logger->fp);
|
||||
return ferror(logger->fp) ? -1 : 0;
|
||||
}
|
||||
1459
src/mightywatt_sequence.c
Normal file
1459
src/mightywatt_sequence.c
Normal file
File diff suppressed because it is too large
Load Diff
745
src/mwcli.c
Normal file
745
src/mwcli.c
Normal file
@ -0,0 +1,745 @@
|
||||
#define _DEFAULT_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "mightywatt_app.h"
|
||||
#include "mightywatt_log.h"
|
||||
#include "mightywatt_sequence.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
|
||||
static volatile sig_atomic_t g_stop_requested = 0;
|
||||
|
||||
static void handle_signal(int signo) {
|
||||
(void)signo;
|
||||
g_stop_requested = 1;
|
||||
}
|
||||
|
||||
static void usage(FILE *fp) {
|
||||
fprintf(fp,
|
||||
"mwcli - MightyWatt Linux CLI\n"
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
" mwcli -d /dev/ttyACM0 [options] <command> [args]\n"
|
||||
"\n"
|
||||
"Options:\n"
|
||||
" -d, --device PATH Serial device\n"
|
||||
" -s, --settle-ms N Delay after open (default 2200)\n"
|
||||
" -i, --interval-ms N Poll interval for monitor/hold (default 500)\n"
|
||||
" -c, --count N Number of samples for monitor/hold\n"
|
||||
" --sample-period-ms N Override sequence sample period\n"
|
||||
" --csv PATH Write measurement samples as CSV\n"
|
||||
" -j, --json JSON stdout for non-streaming commands\n"
|
||||
" -h, --help Show this help\n"
|
||||
"\n"
|
||||
"Commands:\n"
|
||||
" idn\n"
|
||||
" caps\n"
|
||||
" report\n"
|
||||
" monitor\n"
|
||||
" set-current <amps>\n"
|
||||
" set-voltage <volts>\n"
|
||||
" set-power <watts>\n"
|
||||
" set-resistance <ohms>\n"
|
||||
" set-vinv <volts>\n"
|
||||
" hold <current|voltage|power|resistance|vinv> <value>\n"
|
||||
" load-on <current|voltage|power|resistance|vinv> <value>\n"
|
||||
" load-off\n"
|
||||
" safe\n"
|
||||
" remote on|off\n"
|
||||
" get-series\n"
|
||||
" set-series <ohms>\n"
|
||||
" run-sequence <sequence.json>\n"
|
||||
"\n"
|
||||
"Examples:\n"
|
||||
" mwcli -d /dev/ttyACM0 caps\n"
|
||||
" mwcli -d /dev/ttyACM0 --csv out.csv monitor --count 20\n"
|
||||
" mwcli -d /dev/ttyACM0 hold current 0.500 --interval-ms 500\n"
|
||||
" mwcli -d /dev/ttyACM0 --csv run.csv run-sequence profile.json\n");
|
||||
}
|
||||
|
||||
static int parse_double_arg(const char *text, double *out) {
|
||||
char *end = NULL;
|
||||
double value;
|
||||
errno = 0;
|
||||
value = strtod(text, &end);
|
||||
if (errno != 0 || end == text || *end != '\0') {
|
||||
return -1;
|
||||
}
|
||||
*out = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int round_to_uint32(double input, uint32_t *out) {
|
||||
if (input < 0.0 || input > 4294967295.0) {
|
||||
return -1;
|
||||
}
|
||||
*out = (uint32_t)(input + 0.5);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int unit_text_to_milli(const char *text, uint32_t *out) {
|
||||
double value;
|
||||
if (parse_double_arg(text, &value) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return round_to_uint32(value * 1000.0, out);
|
||||
}
|
||||
|
||||
static int parse_mode_text(const char *text, mw_mode *mode) {
|
||||
if (!text || !mode) {
|
||||
return -1;
|
||||
}
|
||||
if (strcasecmp(text, "current") == 0 || strcasecmp(text, "CC") == 0) {
|
||||
*mode = MW_MODE_CURRENT;
|
||||
} else if (strcasecmp(text, "voltage") == 0 || strcasecmp(text, "CV") == 0) {
|
||||
*mode = MW_MODE_VOLTAGE;
|
||||
} else if (strcasecmp(text, "power") == 0 || strcasecmp(text, "CP") == 0) {
|
||||
*mode = MW_MODE_POWER;
|
||||
} else if (strcasecmp(text, "resistance") == 0 || strcasecmp(text, "CR") == 0) {
|
||||
*mode = MW_MODE_RESISTANCE;
|
||||
} else if (strcasecmp(text, "vinv") == 0 || strcasecmp(text, "voltage_inverted") == 0) {
|
||||
*mode = MW_MODE_VOLTAGE_INVERTED;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void json_print_escaped(const char *text) {
|
||||
const unsigned char *p = (const unsigned char *)text;
|
||||
putchar('"');
|
||||
while (p && *p) {
|
||||
switch (*p) {
|
||||
case '"': fputs("\\\"", stdout); break;
|
||||
case '\\': fputs("\\\\", stdout); break;
|
||||
case '\b': fputs("\\b", stdout); break;
|
||||
case '\f': fputs("\\f", stdout); break;
|
||||
case '\n': fputs("\\n", stdout); break;
|
||||
case '\r': fputs("\\r", stdout); break;
|
||||
case '\t': fputs("\\t", stdout); break;
|
||||
default:
|
||||
if (*p < 0x20u) {
|
||||
printf("\\u%04x", (unsigned)*p);
|
||||
} else {
|
||||
putchar((int)*p);
|
||||
}
|
||||
}
|
||||
++p;
|
||||
}
|
||||
putchar('"');
|
||||
}
|
||||
|
||||
static void print_report_text(const mw_report *r) {
|
||||
char status[128];
|
||||
mw_status_string(r->status, status, sizeof(status));
|
||||
printf("current=%.3f A\n", r->current_ma / 1000.0);
|
||||
printf("voltage=%.3f V\n", r->voltage_mv / 1000.0);
|
||||
printf("power=%.3f W\n", mw_report_power_mw(r) / 1000.0);
|
||||
printf("temperature=%u C\n", (unsigned)r->temperature_c);
|
||||
printf("remote=%s\n", r->remote ? "on" : "off");
|
||||
printf("status=%s\n", status);
|
||||
}
|
||||
|
||||
static void print_report_json(const mw_report *r, const char *command) {
|
||||
char status[128];
|
||||
mw_status_string(r->status, status, sizeof(status));
|
||||
printf("{");
|
||||
printf("\"ok\":true,");
|
||||
printf("\"command\":"); json_print_escaped(command);
|
||||
printf(",\"report\":{");
|
||||
printf("\"current_a\":%.3f,", r->current_ma / 1000.0);
|
||||
printf("\"voltage_v\":%.3f,", r->voltage_mv / 1000.0);
|
||||
printf("\"power_w\":%.3f,", mw_report_power_mw(r) / 1000.0);
|
||||
printf("\"temperature_c\":%u,", (unsigned)r->temperature_c);
|
||||
printf("\"remote\":%s,", r->remote ? "true" : "false");
|
||||
printf("\"status_bits\":%u,", (unsigned)r->status);
|
||||
printf("\"status_text\":"); json_print_escaped(status);
|
||||
printf("}}\n");
|
||||
}
|
||||
|
||||
static void print_caps_text(const mw_capabilities *c) {
|
||||
printf("firmware=%s\n", c->firmware_version);
|
||||
printf("board_revision=%s\n", c->board_revision);
|
||||
printf("max_current_dac=%.3f A\n", c->max_current_dac_ma / 1000.0);
|
||||
printf("max_current_adc=%.3f A\n", c->max_current_adc_ma / 1000.0);
|
||||
printf("max_voltage_dac=%.3f V\n", c->max_voltage_dac_mv / 1000.0);
|
||||
printf("max_voltage_adc=%.3f V\n", c->max_voltage_adc_mv / 1000.0);
|
||||
printf("max_power=%.3f W\n", c->max_power_mw / 1000.0);
|
||||
printf("dvm_input_resistance=%u ohm\n", c->dvm_input_resistance_ohm);
|
||||
printf("temperature_threshold=%u C\n", c->temperature_threshold_c);
|
||||
}
|
||||
|
||||
static void print_caps_json(const mw_capabilities *c) {
|
||||
printf("{");
|
||||
printf("\"ok\":true,");
|
||||
printf("\"command\":\"caps\",");
|
||||
printf("\"capabilities\":{");
|
||||
printf("\"firmware\":"); json_print_escaped(c->firmware_version);
|
||||
printf(",\"board_revision\":"); json_print_escaped(c->board_revision);
|
||||
printf(",\"max_current_dac_a\":%.3f", c->max_current_dac_ma / 1000.0);
|
||||
printf(",\"max_current_adc_a\":%.3f", c->max_current_adc_ma / 1000.0);
|
||||
printf(",\"max_voltage_dac_v\":%.3f", c->max_voltage_dac_mv / 1000.0);
|
||||
printf(",\"max_voltage_adc_v\":%.3f", c->max_voltage_adc_mv / 1000.0);
|
||||
printf(",\"max_power_w\":%.3f", c->max_power_mw / 1000.0);
|
||||
printf(",\"dvm_input_resistance_ohm\":%u", c->dvm_input_resistance_ohm);
|
||||
printf(",\"temperature_threshold_c\":%u", c->temperature_threshold_c);
|
||||
printf("}}\n");
|
||||
}
|
||||
|
||||
static void print_error_json(const char *command, const char *error_text) {
|
||||
printf("{");
|
||||
printf("\"ok\":false,");
|
||||
printf("\"command\":"); json_print_escaped(command ? command : "unknown");
|
||||
printf(",\"error\":"); json_print_escaped(error_text ? error_text : "unknown error");
|
||||
printf("}\n");
|
||||
}
|
||||
|
||||
static int log_report_if_requested(mw_csv_logger *logger, const char *context, const mw_report *report) {
|
||||
if (!logger || !report) {
|
||||
return 0;
|
||||
}
|
||||
return mw_csv_logger_write(logger, context, -1, report);
|
||||
}
|
||||
|
||||
static int stream_reports(mw_app *app,
|
||||
int interval_ms,
|
||||
long count,
|
||||
int json_output,
|
||||
const char *command_name,
|
||||
mw_csv_logger *logger) {
|
||||
long i = 0;
|
||||
const int keepalive_ms = 250;
|
||||
const int maintain_output = (command_name && strcmp(command_name, "hold") == 0);
|
||||
int effective_interval_ms = interval_ms > 0 ? interval_ms : 500;
|
||||
int poll_ms = maintain_output && effective_interval_ms > keepalive_ms ? keepalive_ms : effective_interval_ms;
|
||||
struct timespec t0;
|
||||
double next_print_s = 0.0;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
while (!g_stop_requested && (count < 0 || i < count)) {
|
||||
mw_report r;
|
||||
struct timespec now_mono;
|
||||
double elapsed_s;
|
||||
int should_print;
|
||||
time_t now = time(NULL);
|
||||
struct tm tm_now;
|
||||
char ts[32];
|
||||
|
||||
if (mw_app_get_report(app, &r) != 0) {
|
||||
return -1;
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &now_mono);
|
||||
elapsed_s = (double)(now_mono.tv_sec - t0.tv_sec) + (double)(now_mono.tv_nsec - t0.tv_nsec) / 1000000000.0;
|
||||
should_print = (!maintain_output) || (elapsed_s + 1e-9 >= next_print_s);
|
||||
if (should_print) {
|
||||
if (logger && mw_csv_logger_write(logger, command_name ? command_name : "stream", i, &r) != 0) {
|
||||
return -1;
|
||||
}
|
||||
localtime_r(&now, &tm_now);
|
||||
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now);
|
||||
if (json_output) {
|
||||
char status[128];
|
||||
mw_status_string(r.status, status, sizeof(status));
|
||||
printf("{");
|
||||
printf("\"ok\":true,");
|
||||
printf("\"command\":"); json_print_escaped(command_name ? command_name : "stream");
|
||||
printf(",\"timestamp\":"); json_print_escaped(ts);
|
||||
printf(",\"report\":{");
|
||||
printf("\"current_a\":%.3f,", r.current_ma / 1000.0);
|
||||
printf("\"voltage_v\":%.3f,", r.voltage_mv / 1000.0);
|
||||
printf("\"power_w\":%.3f,", mw_report_power_mw(&r) / 1000.0);
|
||||
printf("\"temperature_c\":%u,", (unsigned)r.temperature_c);
|
||||
printf("\"remote\":%s,", r.remote ? "true" : "false");
|
||||
printf("\"status_bits\":%u,", (unsigned)r.status);
|
||||
printf("\"status_text\":"); json_print_escaped(status);
|
||||
printf("}}\n");
|
||||
} else {
|
||||
printf("[%s] I=%.3f A V=%.3f V P=%.3f W T=%u C remote=%s status=0x%02X\n",
|
||||
ts,
|
||||
r.current_ma / 1000.0,
|
||||
r.voltage_mv / 1000.0,
|
||||
mw_report_power_mw(&r) / 1000.0,
|
||||
(unsigned)r.temperature_c,
|
||||
r.remote ? "on" : "off",
|
||||
(unsigned)r.status);
|
||||
}
|
||||
fflush(stdout);
|
||||
++i;
|
||||
next_print_s += effective_interval_ms / 1000.0;
|
||||
}
|
||||
if (!g_stop_requested && (count < 0 || i < count)) {
|
||||
struct timespec ts_sleep;
|
||||
ts_sleep.tv_sec = poll_ms / 1000;
|
||||
ts_sleep.tv_nsec = (long)(poll_ms % 1000) * 1000000L;
|
||||
nanosleep(&ts_sleep, NULL);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
mw_app *app = NULL;
|
||||
mw_capabilities caps;
|
||||
const char *device = NULL;
|
||||
const char *csv_path = NULL;
|
||||
mw_csv_units_mode csv_units_mode = MW_CSV_UNITS_ENGINEERING;
|
||||
int settle_ms = 2200;
|
||||
int interval_ms = 500;
|
||||
int sequence_sample_period_ms = 0;
|
||||
long count = -1;
|
||||
int json_output = 0;
|
||||
int opt;
|
||||
int long_index = 0;
|
||||
int rc = 1;
|
||||
const char *command_for_error = NULL;
|
||||
mw_csv_logger *logger = NULL;
|
||||
static const struct option options[] = {
|
||||
{"device", required_argument, 0, 'd'},
|
||||
{"settle-ms", required_argument, 0, 's'},
|
||||
{"interval-ms", required_argument, 0, 'i'},
|
||||
{"count", required_argument, 0, 'c'},
|
||||
{"csv", required_argument, 0, 1000},
|
||||
{"sample-period-ms", required_argument, 0, 1001},
|
||||
{"csv-raw", no_argument, 0, 1002},
|
||||
{"json", no_argument, 0, 'j'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{0, 0, 0, 0}
|
||||
};
|
||||
|
||||
signal(SIGINT, handle_signal);
|
||||
signal(SIGTERM, handle_signal);
|
||||
|
||||
while ((opt = getopt_long(argc, argv, "d:s:i:c:jh", options, &long_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'd': device = optarg; break;
|
||||
case 's': settle_ms = atoi(optarg); break;
|
||||
case 'i': interval_ms = atoi(optarg); break;
|
||||
case 'c': count = atol(optarg); break;
|
||||
case 'j': json_output = 1; break;
|
||||
case 'h': usage(stdout); return 0;
|
||||
case 1000: csv_path = optarg; break;
|
||||
case 1001: sequence_sample_period_ms = atoi(optarg); break;
|
||||
case 1002: csv_units_mode = MW_CSV_UNITS_RAW; break;
|
||||
default: usage(stderr); return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!device || optind >= argc) {
|
||||
usage(stderr);
|
||||
return 2;
|
||||
}
|
||||
|
||||
command_for_error = argv[optind];
|
||||
if (mw_app_open(&app, device, settle_ms) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(command_for_error, app ? mw_app_last_error(app) : strerror(errno));
|
||||
} else {
|
||||
fprintf(stderr, "open failed: %s\n", app ? mw_app_last_error(app) : strerror(errno));
|
||||
}
|
||||
mw_app_close(app);
|
||||
return 1;
|
||||
}
|
||||
if (csv_path && *csv_path) {
|
||||
if (mw_csv_logger_open(&logger, csv_path, csv_units_mode) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(command_for_error, "failed to open CSV output file");
|
||||
} else {
|
||||
fprintf(stderr, "failed to open CSV output file: %s\n", csv_path);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
if (mw_app_refresh_capabilities(app, &caps) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(command_for_error, mw_app_last_error(app));
|
||||
} else {
|
||||
fprintf(stderr, "caps refresh failed: %s\n", mw_app_last_error(app));
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
|
||||
{
|
||||
const char *cmd = argv[optind++];
|
||||
command_for_error = cmd;
|
||||
|
||||
if (strcmp(cmd, "idn") == 0) {
|
||||
if (json_output) {
|
||||
printf("{\"ok\":true,\"command\":\"idn\",\"identity\":\"MightyWatt\"}\n");
|
||||
} else {
|
||||
printf("MightyWatt\n");
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "caps") == 0) {
|
||||
if (json_output) {
|
||||
print_caps_json(&caps);
|
||||
} else {
|
||||
print_caps_text(&caps);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "report") == 0) {
|
||||
mw_report report;
|
||||
if (mw_app_get_report(app, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, "report", &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "monitor") == 0) {
|
||||
if (stream_reports(app, interval_ms, count, json_output, "monitor", logger) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "set-current") == 0 || strcmp(cmd, "set-voltage") == 0 ||
|
||||
strcmp(cmd, "set-power") == 0 || strcmp(cmd, "set-resistance") == 0 ||
|
||||
strcmp(cmd, "set-vinv") == 0) {
|
||||
mw_mode mode;
|
||||
uint32_t milli;
|
||||
mw_report report;
|
||||
if (optind >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "command needs a value");
|
||||
} else {
|
||||
fprintf(stderr, "%s needs a value\n", cmd);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
mode = strcmp(cmd, "set-current") == 0 ? MW_MODE_CURRENT :
|
||||
strcmp(cmd, "set-voltage") == 0 ? MW_MODE_VOLTAGE :
|
||||
strcmp(cmd, "set-power") == 0 ? MW_MODE_POWER :
|
||||
strcmp(cmd, "set-resistance") == 0 ? MW_MODE_RESISTANCE : MW_MODE_VOLTAGE_INVERTED;
|
||||
if (unit_text_to_milli(argv[optind], &milli) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid numeric value");
|
||||
} else {
|
||||
fprintf(stderr, "invalid numeric value: %s\n", argv[optind]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (mw_app_set_target(app, mode, milli, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "hold") == 0) {
|
||||
mw_mode mode;
|
||||
uint32_t milli;
|
||||
mw_report report;
|
||||
if (optind + 1 >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "hold needs <mode> <value>");
|
||||
} else {
|
||||
fprintf(stderr, "hold needs <mode> <value>\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (parse_mode_text(argv[optind], &mode) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid mode for hold");
|
||||
} else {
|
||||
fprintf(stderr, "invalid mode for hold: %s\n", argv[optind]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (unit_text_to_milli(argv[optind + 1], &milli) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid numeric value");
|
||||
} else {
|
||||
fprintf(stderr, "invalid numeric value: %s\n", argv[optind + 1]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (mw_app_load_on(app, mode, milli, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, "hold_start", &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
if (stream_reports(app, interval_ms, count, json_output, cmd, logger) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "load-on") == 0) {
|
||||
mw_mode mode;
|
||||
uint32_t milli;
|
||||
mw_report report;
|
||||
if (optind + 1 >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "load-on needs <mode> <value>");
|
||||
} else {
|
||||
fprintf(stderr, "load-on needs <mode> <value>\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (parse_mode_text(argv[optind], &mode) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid mode for load-on");
|
||||
} else {
|
||||
fprintf(stderr, "invalid mode for load-on: %s\n", argv[optind]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (unit_text_to_milli(argv[optind + 1], &milli) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid numeric value");
|
||||
} else {
|
||||
fprintf(stderr, "invalid numeric value: %s\n", argv[optind + 1]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (mw_app_load_on(app, mode, milli, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "load-off") == 0) {
|
||||
mw_report report;
|
||||
if (mw_app_load_off(app, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "safe") == 0) {
|
||||
mw_report report;
|
||||
if (mw_app_safe(app, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "remote") == 0) {
|
||||
mw_report report;
|
||||
if (optind >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "remote needs on or off");
|
||||
} else {
|
||||
fprintf(stderr, "remote needs on or off\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (strcasecmp(argv[optind], "on") == 0) {
|
||||
if (mw_app_set_remote(app, 1, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
} else if (strcasecmp(argv[optind], "off") == 0) {
|
||||
if (mw_app_set_remote(app, 0, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
} else {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "remote expects on or off");
|
||||
} else {
|
||||
fprintf(stderr, "remote expects on or off\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "get-series") == 0) {
|
||||
uint16_t mohm;
|
||||
if (mw_app_get_series_resistance(app, &mohm) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (json_output) {
|
||||
printf("{\"ok\":true,\"command\":\"get-series\",\"series_resistance_ohm\":%.3f}\n", mohm / 1000.0);
|
||||
} else {
|
||||
printf("series_resistance=%.3f ohm\n", mohm / 1000.0);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "set-series") == 0) {
|
||||
mw_report report;
|
||||
uint32_t mohm;
|
||||
if (optind >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "set-series needs an ohm value");
|
||||
} else {
|
||||
fprintf(stderr, "set-series needs an ohm value\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (unit_text_to_milli(argv[optind], &mohm) != 0 || mohm > 65535u) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "invalid series resistance value");
|
||||
} else {
|
||||
fprintf(stderr, "invalid series resistance value: %s\n", argv[optind]);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (mw_app_set_series_resistance(app, (uint16_t)mohm, &report) != 0) {
|
||||
goto fail;
|
||||
}
|
||||
if (log_report_if_requested(logger, cmd, &report) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "failed to write CSV output");
|
||||
} else {
|
||||
fprintf(stderr, "failed to write CSV output\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
print_report_json(&report, cmd);
|
||||
} else {
|
||||
print_report_text(&report);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (strcmp(cmd, "run-sequence") == 0) {
|
||||
mw_sequence_result result;
|
||||
mw_sequence_run_options opts;
|
||||
char error_text[256];
|
||||
if (optind >= argc) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "run-sequence needs a JSON file path");
|
||||
} else {
|
||||
fprintf(stderr, "run-sequence needs a JSON file path\n");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
opts.csv_path = csv_path;
|
||||
opts.sample_period_ms_override = sequence_sample_period_ms;
|
||||
opts.safe_on_abort = true;
|
||||
if (mw_sequence_run_file(app, argv[optind], &opts, &result, error_text, sizeof(error_text)) != 0) {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, error_text[0] ? error_text : "sequence failed");
|
||||
} else {
|
||||
fprintf(stderr, "%s\n", error_text[0] ? error_text : "sequence failed");
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
if (json_output) {
|
||||
printf("{");
|
||||
printf("\"ok\":true,\"command\":\"run-sequence\",");
|
||||
printf("\"name\":"); json_print_escaped(result.name[0] ? result.name : "sequence");
|
||||
printf(",\"sample_period_ms\":%d", result.sample_period_ms);
|
||||
printf(",\"steps_total\":%lu", (unsigned long)result.steps_total);
|
||||
printf(",\"steps_completed\":%lu", (unsigned long)result.steps_completed);
|
||||
printf(",\"samples_written\":%lu", (unsigned long)result.samples_written);
|
||||
if (result.last_report_valid) {
|
||||
printf(",\"last_report\":{");
|
||||
printf("\"current_a\":%.3f,", result.last_report.current_ma / 1000.0);
|
||||
printf("\"voltage_v\":%.3f,", result.last_report.voltage_mv / 1000.0);
|
||||
printf("\"power_w\":%.3f", mw_report_power_mw(&result.last_report) / 1000.0);
|
||||
printf("}");
|
||||
}
|
||||
printf("}\n");
|
||||
} else {
|
||||
printf("sequence=%s\n", result.name[0] ? result.name : "sequence");
|
||||
printf("sample_period_ms=%d\n", result.sample_period_ms);
|
||||
printf("steps_completed=%lu/%lu\n", (unsigned long)result.steps_completed, (unsigned long)result.steps_total);
|
||||
printf("samples_written=%lu\n", (unsigned long)result.samples_written);
|
||||
if (result.last_report_valid) {
|
||||
print_report_text(&result.last_report);
|
||||
}
|
||||
}
|
||||
rc = 0;
|
||||
} else {
|
||||
if (json_output) {
|
||||
print_error_json(cmd, "unknown command");
|
||||
} else {
|
||||
fprintf(stderr, "unknown command: %s\n", cmd);
|
||||
usage(stderr);
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
goto done;
|
||||
|
||||
fail:
|
||||
if (json_output) {
|
||||
print_error_json(command_for_error, mw_app_last_error(app));
|
||||
} else {
|
||||
fprintf(stderr, "%s\n", mw_app_last_error(app));
|
||||
}
|
||||
|
||||
done:
|
||||
mw_csv_logger_close(logger);
|
||||
mw_app_close(app);
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user