diff --git a/.gitignore b/.gitignore index 6a7344a..0afc933 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # Build outputs *.o -mwcli -mw_controller_example # Logs and captures *.csv diff --git a/bin/mw_controller_example b/bin/mw_controller_example new file mode 100644 index 0000000..bb65144 Binary files /dev/null and b/bin/mw_controller_example differ diff --git a/bin/mwcli b/bin/mwcli new file mode 100644 index 0000000..e2972fb Binary files /dev/null and b/bin/mwcli differ diff --git a/examples/battery_discharge_cutoff.json b/examples/battery_discharge_cutoff.json new file mode 100644 index 0000000..ee42579 --- /dev/null +++ b/examples/battery_discharge_cutoff.json @@ -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" } + ] +} diff --git a/examples/cc_step_sweep.json b/examples/cc_step_sweep.json new file mode 100644 index 0000000..ee8b42b --- /dev/null +++ b/examples/cc_step_sweep.json @@ -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" } + ] +} diff --git a/examples/cp_step_sweep.json b/examples/cp_step_sweep.json new file mode 100644 index 0000000..5777f4e --- /dev/null +++ b/examples/cp_step_sweep.json @@ -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" } + ] +} diff --git a/examples/cr_step_sweep.json b/examples/cr_step_sweep.json new file mode 100644 index 0000000..b8e4289 --- /dev/null +++ b/examples/cr_step_sweep.json @@ -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" } + ] +} diff --git a/examples/load_test_cc.json b/examples/load_test_cc.json new file mode 100644 index 0000000..c74d947 --- /dev/null +++ b/examples/load_test_cc.json @@ -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" } + ] +} diff --git a/examples/mw_controller_example.c b/examples/mw_controller_example.c new file mode 100644 index 0000000..52a3cef --- /dev/null +++ b/examples/mw_controller_example.c @@ -0,0 +1,103 @@ +#define _POSIX_C_SOURCE 200809L + +#include "mightywatt_controller.h" + +#include +#include +#include +#include +#include + +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; +} diff --git a/examples/quick_cc_test.json b/examples/quick_cc_test.json new file mode 100644 index 0000000..6badccf --- /dev/null +++ b/examples/quick_cc_test.json @@ -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" } + ] +} diff --git a/examples/repeat_n_pulse_test.json b/examples/repeat_n_pulse_test.json new file mode 100644 index 0000000..8d140c9 --- /dev/null +++ b/examples/repeat_n_pulse_test.json @@ -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" } + ] +} diff --git a/examples/repeat_until_cutoff.json b/examples/repeat_until_cutoff.json new file mode 100644 index 0000000..421d011 --- /dev/null +++ b/examples/repeat_until_cutoff.json @@ -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" } + ] +} diff --git a/examples/repeat_while_burn_in.json b/examples/repeat_while_burn_in.json new file mode 100644 index 0000000..1ef5fa4 --- /dev/null +++ b/examples/repeat_while_burn_in.json @@ -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" } + ] +} diff --git a/include/mightywatt.h b/include/mightywatt.h new file mode 100644 index 0000000..57deb6b --- /dev/null +++ b/include/mightywatt.h @@ -0,0 +1,76 @@ +#ifndef MIGHTYWATT_H +#define MIGHTYWATT_H + +#include +#include +#include + +#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 diff --git a/include/mightywatt_app.h b/include/mightywatt_app.h new file mode 100644 index 0000000..1470692 --- /dev/null +++ b/include/mightywatt_app.h @@ -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 diff --git a/include/mightywatt_controller.h b/include/mightywatt_controller.h new file mode 100644 index 0000000..78e7b6a --- /dev/null +++ b/include/mightywatt_controller.h @@ -0,0 +1,113 @@ +#ifndef MIGHTYWATT_CONTROLLER_H +#define MIGHTYWATT_CONTROLLER_H + +#include "mightywatt_app.h" + +#include +#include +#include + +#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 diff --git a/include/mightywatt_log.h b/include/mightywatt_log.h new file mode 100644 index 0000000..dedd5aa --- /dev/null +++ b/include/mightywatt_log.h @@ -0,0 +1,30 @@ +#ifndef MIGHTYWATT_LOG_H +#define MIGHTYWATT_LOG_H + +#include "mightywatt.h" + +#include + +#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 diff --git a/include/mightywatt_sequence.h b/include/mightywatt_sequence.h new file mode 100644 index 0000000..098c6ec --- /dev/null +++ b/include/mightywatt_sequence.h @@ -0,0 +1,46 @@ +#ifndef MIGHTYWATT_SEQUENCE_H +#define MIGHTYWATT_SEQUENCE_H + +#include "mightywatt_app.h" +#include "mightywatt_log.h" + +#include +#include +#include + +#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 diff --git a/src/mightywatt.c b/src/mightywatt.c new file mode 100644 index 0000000..c6685b1 --- /dev/null +++ b/src/mightywatt.c @@ -0,0 +1,549 @@ +#define _DEFAULT_SOURCE +#define _POSIX_C_SOURCE 200809L +#include "mightywatt.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/src/mightywatt_app.c b/src/mightywatt_app.c new file mode 100644 index 0000000..45cc862 --- /dev/null +++ b/src/mightywatt_app.c @@ -0,0 +1,252 @@ +#include "mightywatt_app.h" + +#include +#include +#include +#include + +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; +} diff --git a/src/mightywatt_controller.c b/src/mightywatt_controller.c new file mode 100644 index 0000000..9c7b0c5 --- /dev/null +++ b/src/mightywatt_controller.c @@ -0,0 +1,723 @@ +#define _POSIX_C_SOURCE 200809L + +#include "mightywatt_controller.h" + +#include +#include +#include +#include +#include +#include +#include + +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"; + } +} diff --git a/src/mightywatt_log.c b/src/mightywatt_log.c new file mode 100644 index 0000000..8c3da61 --- /dev/null +++ b/src/mightywatt_log.c @@ -0,0 +1,122 @@ +#define _POSIX_C_SOURCE 200809L +#include "mightywatt_log.h" + +#include +#include +#include +#include + +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; +} diff --git a/src/mightywatt_sequence.c b/src/mightywatt_sequence.c new file mode 100644 index 0000000..db7fe6b --- /dev/null +++ b/src/mightywatt_sequence.c @@ -0,0 +1,1459 @@ +#define _POSIX_C_SOURCE 200809L +#include "mightywatt_sequence.h" +#include "mightywatt_log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { + JT_UNDEFINED = 0, + JT_OBJECT, + JT_ARRAY, + JT_STRING, + JT_PRIMITIVE, +} jt_type; + +typedef struct { + jt_type type; + int start; + int end; + int size; + int parent; +} jt_token; + +typedef struct { + const char *js; + size_t len; + size_t pos; + int toknext; + int toksuper; +} jt_parser; + +static void jt_init(jt_parser *p, const char *js, size_t len) { + p->js = js; + p->len = len; + p->pos = 0; + p->toknext = 0; + p->toksuper = -1; +} + +static jt_token *jt_alloc_token(jt_parser *p, jt_token *tokens, size_t num_tokens) { + jt_token *tok; + if ((size_t)p->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[p->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; + tok->parent = -1; + tok->type = JT_UNDEFINED; + return tok; +} + +static void jt_fill_token(jt_token *tok, jt_type type, int start, int end) { + tok->type = type; + tok->start = start; + tok->end = end; + tok->size = 0; +} + +static int jt_parse_primitive(jt_parser *p, jt_token *tokens, size_t num_tokens) { + size_t start = p->pos; + jt_token *tok; + for (; p->pos < p->len; ++p->pos) { + char c = p->js[p->pos]; + if (c == '\t' || c == '\r' || c == '\n' || c == ' ' || c == ',' || c == ']' || c == '}') { + break; + } + if ((unsigned char)c < 32 || c == '"') { + return -1; + } + } + tok = jt_alloc_token(p, tokens, num_tokens); + if (!tok) { + return -1; + } + jt_fill_token(tok, JT_PRIMITIVE, (int)start, (int)p->pos); + tok->parent = p->toksuper; + if (p->toksuper != -1) { + tokens[p->toksuper].size++; + } + p->pos--; + return 0; +} + +static int jt_parse_string(jt_parser *p, jt_token *tokens, size_t num_tokens) { + size_t start = ++p->pos; + jt_token *tok; + for (; p->pos < p->len; ++p->pos) { + char c = p->js[p->pos]; + if (c == '"') { + tok = jt_alloc_token(p, tokens, num_tokens); + if (!tok) { + return -1; + } + jt_fill_token(tok, JT_STRING, (int)start, (int)p->pos); + tok->parent = p->toksuper; + if (p->toksuper != -1) { + tokens[p->toksuper].size++; + } + return 0; + } + if (c == '\\') { + ++p->pos; + if (p->pos >= p->len) { + return -1; + } + } + } + return -1; +} + +static int jt_parse(jt_parser *p, jt_token *tokens, size_t num_tokens) { + for (; p->pos < p->len; ++p->pos) { + char c = p->js[p->pos]; + jt_token *tok; + int i; + switch (c) { + case '{': + case '[': + tok = jt_alloc_token(p, tokens, num_tokens); + if (!tok) { + return -1; + } + tok->type = (c == '{') ? JT_OBJECT : JT_ARRAY; + tok->start = (int)p->pos; + tok->parent = p->toksuper; + if (p->toksuper != -1) { + tokens[p->toksuper].size++; + } + p->toksuper = p->toknext - 1; + break; + case '}': + case ']': + for (i = p->toknext - 1; i >= 0; --i) { + tok = &tokens[i]; + if (tok->start != -1 && tok->end == -1) { + if ((tok->type == JT_OBJECT && c == '}') || (tok->type == JT_ARRAY && c == ']')) { + tok->end = (int)p->pos + 1; + p->toksuper = tok->parent; + break; + } + return -1; + } + } + if (i < 0) { + return -1; + } + break; + case '"': + if (jt_parse_string(p, tokens, num_tokens) != 0) { + return -1; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + case ':': + case ',': + break; + default: + if (jt_parse_primitive(p, tokens, num_tokens) != 0) { + return -1; + } + break; + } + } + for (int i = p->toknext - 1; i >= 0; --i) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + return -1; + } + } + return p->toknext; +} + +static int js_token_eq(const char *js, const jt_token *tok, const char *text) { + size_t n; + if (!js || !tok || !text || tok->start < 0 || tok->end < tok->start) { + return 0; + } + n = (size_t)(tok->end - tok->start); + return strlen(text) == n && strncmp(js + tok->start, text, n) == 0; +} + +static int js_token_to_string(const char *js, const jt_token *tok, char *buffer, size_t buffer_size) { + size_t n; + if (!js || !tok || !buffer || buffer_size == 0 || tok->start < 0 || tok->end < tok->start) { + return -1; + } + n = (size_t)(tok->end - tok->start); + if (n + 1 > buffer_size) { + return -1; + } + memcpy(buffer, js + tok->start, n); + buffer[n] = '\0'; + return 0; +} + +static int js_token_to_double(const char *js, const jt_token *tok, double *out) { + char tmp[64]; + char *end = NULL; + if (!out || js_token_to_string(js, tok, tmp, sizeof(tmp)) != 0) { + return -1; + } + errno = 0; + *out = strtod(tmp, &end); + if (errno != 0 || end == tmp || *end != '\0') { + return -1; + } + return 0; +} + +static int js_token_to_int(const char *js, const jt_token *tok, int *out) { + double d; + if (!out || js_token_to_double(js, tok, &d) != 0) { + return -1; + } + *out = (int)d; + return 0; +} + +static int js_token_to_bool(const char *js, const jt_token *tok, int *out) { + if (!out || !js || !tok) { + return -1; + } + if (js_token_eq(js, tok, "true")) { + *out = 1; + return 0; + } + if (js_token_eq(js, tok, "false")) { + *out = 0; + return 0; + } + return -1; +} + +static int js_skip(const jt_token *tokens, int count, int index) { + int end; + if (!tokens || index < 0 || index >= count) { + return index + 1; + } + end = index + 1; + if (tokens[index].type == JT_OBJECT || tokens[index].type == JT_ARRAY) { + for (int i = 0; i < tokens[index].size; ++i) { + end = js_skip(tokens, count, end); + } + } + return end; +} + +static int js_object_get(const char *js, const jt_token *tokens, int count, int object_index, const char *key) { + int i; + int end; + if (!js || !tokens || object_index < 0 || object_index >= count || tokens[object_index].type != JT_OBJECT) { + return -1; + } + i = object_index + 1; + end = js_skip(tokens, count, object_index); + while (i < end) { + int key_index = i; + int value_index = i + 1; + if (value_index >= end) { + return -1; + } + if (js_token_eq(js, &tokens[key_index], key)) { + return value_index; + } + i = js_skip(tokens, count, value_index); + } + return -1; +} + +typedef struct { + uint32_t max_voltage_mv; + uint32_t max_current_ma; + uint32_t max_power_mw; + int abort_on_disconnect; +} mw_seq_safety; + +typedef enum { + MW_SEQ_COND_VOLTAGE_BELOW = 0, + MW_SEQ_COND_VOLTAGE_ABOVE, + MW_SEQ_COND_CURRENT_BELOW, + MW_SEQ_COND_CURRENT_ABOVE, + MW_SEQ_COND_POWER_BELOW, + MW_SEQ_COND_POWER_ABOVE, + MW_SEQ_COND_TEMPERATURE_ABOVE, +} mw_seq_condition_kind; + +typedef struct { + mw_seq_condition_kind kind; + uint32_t milli_units; +} mw_seq_condition; + +typedef enum { + MW_SEQ_ACT_SET_MODE = 0, + MW_SEQ_ACT_SET_TARGET, + MW_SEQ_ACT_OUTPUT, + MW_SEQ_ACT_HOLD, + MW_SEQ_ACT_RAMP, + MW_SEQ_ACT_HOLD_UNTIL, + MW_SEQ_ACT_REPEAT, + MW_SEQ_ACT_REPEAT_UNTIL, + MW_SEQ_ACT_REPEAT_WHILE, + MW_SEQ_ACT_SAFE, + MW_SEQ_ACT_REMOTE, +} mw_seq_action_kind; + +typedef struct mw_seq_step { + mw_seq_action_kind kind; + mw_mode mode; + uint32_t milli_units; + int enabled; + double duration_s; + double timeout_s; + uint32_t ramp_start; + uint32_t ramp_stop; + uint32_t ramp_step; + double dwell_s; + mw_seq_condition condition; + int has_condition; + int repeat_times; + struct mw_seq_step *children; + size_t child_count; + int has_break_if; + mw_seq_condition break_if; +} mw_seq_step; + +typedef struct { + char name[64]; + int sample_period_ms; + mw_seq_safety safety; + mw_seq_step *steps; + size_t step_count; + mw_seq_step *abort_steps; + size_t abort_step_count; +} mw_sequence; + +typedef struct { + mw_app *app; + mw_sequence *seq; + mw_csv_logger *logger; + mw_sequence_result *result; + mw_mode staged_mode; + uint32_t staged_target; + int staged_valid; + int output_enabled; + long exec_step_counter; + int in_abort_sequence; + const mw_seq_condition *break_stack[16]; + size_t break_stack_count; +} mw_sequence_exec; + +enum { + SEQ_EXEC_OK = 0, + SEQ_EXEC_FAIL = -1, + SEQ_EXEC_ABORT = 1, +}; + +static void seq_set_error(char *buffer, size_t buffer_size, const char *fmt, ...) { + va_list ap; + if (!buffer || buffer_size == 0) { + return; + } + va_start(ap, fmt); + vsnprintf(buffer, buffer_size, fmt, ap); + va_end(ap); +} + +static int seq_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; +} + +#define MW_SEQ_KEEPALIVE_MS 250 + +static double seq_elapsed_s(const struct timespec *t0, const struct timespec *now) { + return (double)(now->tv_sec - t0->tv_sec) + (double)(now->tv_nsec - t0->tv_nsec) / 1000000000.0; +} + +static int seq_effective_keepalive_ms(const mw_sequence_exec *exec) { + int sample_ms; + if (!exec || !exec->seq) { + return MW_SEQ_KEEPALIVE_MS; + } + sample_ms = exec->seq->sample_period_ms; + if (sample_ms <= 0) { + sample_ms = 500; + } + return sample_ms < MW_SEQ_KEEPALIVE_MS ? sample_ms : MW_SEQ_KEEPALIVE_MS; +} + +static int seq_double_to_milli(double input, uint32_t *out) { + if (!out || input < 0.0 || input > 4294967.295) { + return -1; + } + *out = (uint32_t)(input * 1000.0 + 0.5); + return 0; +} + +static int seq_parse_mode_string(const char *text, mw_mode *out_mode) { + if (!text || !out_mode) { + return -1; + } + if (strcasecmp(text, "CC") == 0 || strcasecmp(text, "current") == 0) { + *out_mode = MW_MODE_CURRENT; + } else if (strcasecmp(text, "CV") == 0 || strcasecmp(text, "voltage") == 0) { + *out_mode = MW_MODE_VOLTAGE; + } else if (strcasecmp(text, "CP") == 0 || strcasecmp(text, "power") == 0) { + *out_mode = MW_MODE_POWER; + } else if (strcasecmp(text, "CR") == 0 || strcasecmp(text, "resistance") == 0) { + *out_mode = MW_MODE_RESISTANCE; + } else if (strcasecmp(text, "CVINV") == 0 || strcasecmp(text, "vinv") == 0 || + strcasecmp(text, "voltage_inverted") == 0) { + *out_mode = MW_MODE_VOLTAGE_INVERTED; + } else { + return -1; + } + return 0; +} + +static int seq_parse_action_mode_from_name(const char *action, mw_mode *out_mode) { + if (strncmp(action, "set_", 4) == 0) { + return seq_parse_mode_string(action + 4, out_mode); + } + if (strncmp(action, "ramp_", 5) == 0) { + return seq_parse_mode_string(action + 5, out_mode); + } + return -1; +} + +static int seq_condition_met(const mw_seq_condition *cond, const mw_report *report) { + uint32_t power_mw; + if (!cond || !report) { + return 0; + } + power_mw = mw_report_power_mw(report); + switch (cond->kind) { + case MW_SEQ_COND_VOLTAGE_BELOW: return report->voltage_mv < cond->milli_units; + case MW_SEQ_COND_VOLTAGE_ABOVE: return report->voltage_mv > cond->milli_units; + case MW_SEQ_COND_CURRENT_BELOW: return report->current_ma < cond->milli_units; + case MW_SEQ_COND_CURRENT_ABOVE: return report->current_ma > cond->milli_units; + case MW_SEQ_COND_POWER_BELOW: return power_mw < cond->milli_units; + case MW_SEQ_COND_POWER_ABOVE: return power_mw > cond->milli_units; + case MW_SEQ_COND_TEMPERATURE_ABOVE: return (uint32_t)report->temperature_c * 1000u > cond->milli_units; + default: return 0; + } +} + +static void seq_condition_to_text(const mw_seq_condition *cond, char *buf, size_t buf_sz) { + const char *name = "unknown"; + double value = cond ? (double)cond->milli_units / 1000.0 : 0.0; + if (!buf || buf_sz == 0) { + return; + } + if (cond) { + switch (cond->kind) { + case MW_SEQ_COND_VOLTAGE_BELOW: name = "voltage_below"; break; + case MW_SEQ_COND_VOLTAGE_ABOVE: name = "voltage_above"; break; + case MW_SEQ_COND_CURRENT_BELOW: name = "current_below"; break; + case MW_SEQ_COND_CURRENT_ABOVE: name = "current_above"; break; + case MW_SEQ_COND_POWER_BELOW: name = "power_below"; break; + case MW_SEQ_COND_POWER_ABOVE: name = "power_above"; break; + case MW_SEQ_COND_TEMPERATURE_ABOVE: name = "temperature_above"; break; + default: break; + } + } + snprintf(buf, buf_sz, "%s %.3f", name, value); +} + +static void seq_free_steps(mw_seq_step *steps, size_t count) { + if (!steps) { + return; + } + for (size_t i = 0; i < count; ++i) { + seq_free_steps(steps[i].children, steps[i].child_count); + } + free(steps); +} + +static void seq_free(mw_sequence *seq) { + if (!seq) { + return; + } + seq_free_steps(seq->steps, seq->step_count); + seq_free_steps(seq->abort_steps, seq->abort_step_count); + memset(seq, 0, sizeof(*seq)); +} + +static int seq_parse_condition(const char *js, const jt_token *tokens, int count, int cond_index, + mw_seq_condition *out_cond, char *error_text, size_t error_size) { + int type_idx; + int value_idx; + char type_buf[48]; + double value; + if (!out_cond || cond_index < 0 || tokens[cond_index].type != JT_OBJECT) { + seq_set_error(error_text, error_size, "invalid condition object"); + return -1; + } + type_idx = js_object_get(js, tokens, count, cond_index, "type"); + value_idx = js_object_get(js, tokens, count, cond_index, "value"); + if (type_idx < 0 || value_idx < 0) { + seq_set_error(error_text, error_size, "condition needs type and value"); + return -1; + } + if (js_token_to_string(js, &tokens[type_idx], type_buf, sizeof(type_buf)) != 0 || + js_token_to_double(js, &tokens[value_idx], &value) != 0 || + seq_double_to_milli(value, &out_cond->milli_units) != 0) { + seq_set_error(error_text, error_size, "invalid condition fields"); + return -1; + } + if (strcmp(type_buf, "voltage_below") == 0) { + out_cond->kind = MW_SEQ_COND_VOLTAGE_BELOW; + } else if (strcmp(type_buf, "voltage_above") == 0) { + out_cond->kind = MW_SEQ_COND_VOLTAGE_ABOVE; + } else if (strcmp(type_buf, "current_below") == 0) { + out_cond->kind = MW_SEQ_COND_CURRENT_BELOW; + } else if (strcmp(type_buf, "current_above") == 0) { + out_cond->kind = MW_SEQ_COND_CURRENT_ABOVE; + } else if (strcmp(type_buf, "power_below") == 0) { + out_cond->kind = MW_SEQ_COND_POWER_BELOW; + } else if (strcmp(type_buf, "power_above") == 0) { + out_cond->kind = MW_SEQ_COND_POWER_ABOVE; + } else if (strcmp(type_buf, "temperature_above") == 0) { + out_cond->kind = MW_SEQ_COND_TEMPERATURE_ABOVE; + } else { + seq_set_error(error_text, error_size, "unsupported condition type: %s", type_buf); + return -1; + } + return 0; +} + +static int seq_parse_step_array(const char *js, const jt_token *tokens, int count, int array_index, + mw_seq_step **out_steps, size_t *out_step_count, + char *error_text, size_t error_size); + +static int seq_parse_step(const char *js, const jt_token *tokens, int count, int step_index, + mw_seq_step *out_step, char *error_text, size_t error_size) { + int action_idx; + int break_idx; + char action[48]; + memset(out_step, 0, sizeof(*out_step)); + + action_idx = js_object_get(js, tokens, count, step_index, "action"); + if (action_idx < 0 || js_token_to_string(js, &tokens[action_idx], action, sizeof(action)) != 0) { + seq_set_error(error_text, error_size, "step missing action"); + return -1; + } + + break_idx = js_object_get(js, tokens, count, step_index, "break_if"); + if (break_idx >= 0) { + if (seq_parse_condition(js, tokens, count, break_idx, &out_step->break_if, error_text, error_size) != 0) { + return -1; + } + out_step->has_break_if = 1; + } + + if (strcmp(action, "set_mode") == 0) { + int mode_idx = js_object_get(js, tokens, count, step_index, "mode"); + char mode_buf[32]; + if (mode_idx < 0 || js_token_to_string(js, &tokens[mode_idx], mode_buf, sizeof(mode_buf)) != 0 || + seq_parse_mode_string(mode_buf, &out_step->mode) != 0) { + seq_set_error(error_text, error_size, "set_mode needs valid mode"); + return -1; + } + out_step->kind = MW_SEQ_ACT_SET_MODE; + return 0; + } + + if (strcmp(action, "output") == 0) { + int enabled_idx = js_object_get(js, tokens, count, step_index, "enabled"); + int enabled; + if (enabled_idx < 0 || js_token_to_bool(js, &tokens[enabled_idx], &enabled) != 0) { + seq_set_error(error_text, error_size, "output needs enabled boolean"); + return -1; + } + out_step->kind = MW_SEQ_ACT_OUTPUT; + out_step->enabled = enabled; + return 0; + } + + if (strcmp(action, "safe") == 0) { + out_step->kind = MW_SEQ_ACT_SAFE; + return 0; + } + + if (strcmp(action, "remote") == 0) { + int enabled_idx = js_object_get(js, tokens, count, step_index, "enabled"); + int enabled; + if (enabled_idx < 0 || js_token_to_bool(js, &tokens[enabled_idx], &enabled) != 0) { + seq_set_error(error_text, error_size, "remote needs enabled boolean"); + return -1; + } + out_step->kind = MW_SEQ_ACT_REMOTE; + out_step->enabled = enabled; + return 0; + } + + if (strcmp(action, "hold") == 0) { + int duration_idx = js_object_get(js, tokens, count, step_index, "duration_s"); + if (duration_idx < 0 || js_token_to_double(js, &tokens[duration_idx], &out_step->duration_s) != 0 || + out_step->duration_s < 0.0) { + seq_set_error(error_text, error_size, "hold needs duration_s >= 0"); + return -1; + } + out_step->kind = MW_SEQ_ACT_HOLD; + return 0; + } + + if (strcmp(action, "hold_until") == 0) { + int timeout_idx = js_object_get(js, tokens, count, step_index, "timeout_s"); + int cond_idx = js_object_get(js, tokens, count, step_index, "condition"); + if (timeout_idx < 0 || js_token_to_double(js, &tokens[timeout_idx], &out_step->timeout_s) != 0 || + out_step->timeout_s < 0.0) { + seq_set_error(error_text, error_size, "hold_until needs timeout_s >= 0"); + return -1; + } + if (seq_parse_condition(js, tokens, count, cond_idx, &out_step->condition, error_text, error_size) != 0) { + return -1; + } + out_step->has_condition = 1; + out_step->kind = MW_SEQ_ACT_HOLD_UNTIL; + return 0; + } + + if (strncmp(action, "set_", 4) == 0) { + int value_idx = js_object_get(js, tokens, count, step_index, "value"); + double value; + if (seq_parse_action_mode_from_name(action, &out_step->mode) != 0 || + value_idx < 0 || js_token_to_double(js, &tokens[value_idx], &value) != 0 || + seq_double_to_milli(value, &out_step->milli_units) != 0) { + seq_set_error(error_text, error_size, "%s needs numeric value", action); + return -1; + } + out_step->kind = MW_SEQ_ACT_SET_TARGET; + return 0; + } + + if (strncmp(action, "ramp_", 5) == 0) { + int start_idx = js_object_get(js, tokens, count, step_index, "start"); + int stop_idx = js_object_get(js, tokens, count, step_index, "stop"); + int stepv_idx = js_object_get(js, tokens, count, step_index, "step"); + int dwell_idx = js_object_get(js, tokens, count, step_index, "dwell_s"); + double start, stop, stepv; + if (seq_parse_action_mode_from_name(action, &out_step->mode) != 0 || + start_idx < 0 || stop_idx < 0 || stepv_idx < 0 || dwell_idx < 0 || + js_token_to_double(js, &tokens[start_idx], &start) != 0 || + js_token_to_double(js, &tokens[stop_idx], &stop) != 0 || + js_token_to_double(js, &tokens[stepv_idx], &stepv) != 0 || + js_token_to_double(js, &tokens[dwell_idx], &out_step->dwell_s) != 0 || + seq_double_to_milli(start, &out_step->ramp_start) != 0 || + seq_double_to_milli(stop, &out_step->ramp_stop) != 0 || + seq_double_to_milli(stepv, &out_step->ramp_step) != 0 || + out_step->ramp_step == 0 || out_step->dwell_s < 0.0) { + seq_set_error(error_text, error_size, "%s needs start/stop/step/dwell_s", action); + return -1; + } + out_step->kind = MW_SEQ_ACT_RAMP; + return 0; + } + + if (strcmp(action, "repeat") == 0) { + int times_idx = js_object_get(js, tokens, count, step_index, "times"); + int steps_idx = js_object_get(js, tokens, count, step_index, "steps"); + if (times_idx < 0 || js_token_to_int(js, &tokens[times_idx], &out_step->repeat_times) != 0 || + out_step->repeat_times < 0) { + seq_set_error(error_text, error_size, "repeat needs times >= 0"); + return -1; + } + if (seq_parse_step_array(js, tokens, count, steps_idx, &out_step->children, &out_step->child_count, + error_text, error_size) != 0) { + return -1; + } + out_step->kind = MW_SEQ_ACT_REPEAT; + return 0; + } + + if (strcmp(action, "repeat_until") == 0) { + int cond_idx = js_object_get(js, tokens, count, step_index, "condition"); + int steps_idx = js_object_get(js, tokens, count, step_index, "steps"); + int timeout_idx = js_object_get(js, tokens, count, step_index, "timeout_s"); + if (seq_parse_condition(js, tokens, count, cond_idx, &out_step->condition, error_text, error_size) != 0) { + return -1; + } + out_step->has_condition = 1; + if (timeout_idx >= 0 && + (js_token_to_double(js, &tokens[timeout_idx], &out_step->timeout_s) != 0 || out_step->timeout_s < 0.0)) { + seq_set_error(error_text, error_size, "repeat_until timeout_s must be >= 0"); + return -1; + } + if (seq_parse_step_array(js, tokens, count, steps_idx, &out_step->children, &out_step->child_count, + error_text, error_size) != 0) { + return -1; + } + out_step->kind = MW_SEQ_ACT_REPEAT_UNTIL; + return 0; + } + + if (strcmp(action, "repeat_while") == 0) { + int cond_idx = js_object_get(js, tokens, count, step_index, "condition"); + int steps_idx = js_object_get(js, tokens, count, step_index, "steps"); + int timeout_idx = js_object_get(js, tokens, count, step_index, "timeout_s"); + if (seq_parse_condition(js, tokens, count, cond_idx, &out_step->condition, error_text, error_size) != 0) { + return -1; + } + out_step->has_condition = 1; + if (timeout_idx >= 0 && + (js_token_to_double(js, &tokens[timeout_idx], &out_step->timeout_s) != 0 || out_step->timeout_s < 0.0)) { + seq_set_error(error_text, error_size, "repeat_while timeout_s must be >= 0"); + return -1; + } + if (seq_parse_step_array(js, tokens, count, steps_idx, &out_step->children, &out_step->child_count, + error_text, error_size) != 0) { + return -1; + } + out_step->kind = MW_SEQ_ACT_REPEAT_WHILE; + return 0; + } + + seq_set_error(error_text, error_size, "unsupported action: %s", action); + return -1; +} + +static int seq_parse_step_array(const char *js, const jt_token *tokens, int count, int array_index, + mw_seq_step **out_steps, size_t *out_step_count, + char *error_text, size_t error_size) { + int idx; + mw_seq_step *steps; + size_t n; + if (!out_steps || !out_step_count || array_index < 0 || tokens[array_index].type != JT_ARRAY || + tokens[array_index].size <= 0) { + seq_set_error(error_text, error_size, "sequence block needs non-empty steps array"); + return -1; + } + n = (size_t)tokens[array_index].size; + steps = calloc(n, sizeof(*steps)); + if (!steps) { + seq_set_error(error_text, error_size, "out of memory allocating steps"); + return -1; + } + idx = array_index + 1; + for (size_t i = 0; i < n; ++i) { + if (seq_parse_step(js, tokens, count, idx, &steps[i], error_text, error_size) != 0) { + seq_free_steps(steps, n); + return -1; + } + idx = js_skip(tokens, count, idx); + } + *out_steps = steps; + *out_step_count = n; + return 0; +} + +static int seq_parse_json_text(const char *js, mw_sequence *out_seq, char *error_text, size_t error_size) { + jt_parser parser; + jt_token *tokens = NULL; + int token_count; + int root_idx = 0; + int i; + int steps_idx; + int abort_idx; + + memset(out_seq, 0, sizeof(*out_seq)); + out_seq->sample_period_ms = 500; + out_seq->safety.abort_on_disconnect = 1; + + tokens = calloc(2048, sizeof(*tokens)); + if (!tokens) { + seq_set_error(error_text, error_size, "out of memory"); + return -1; + } + jt_init(&parser, js, strlen(js)); + token_count = jt_parse(&parser, tokens, 2048); + if (token_count < 1 || tokens[root_idx].type != JT_OBJECT) { + free(tokens); + seq_set_error(error_text, error_size, "invalid JSON sequence file"); + return -1; + } + + i = js_object_get(js, tokens, token_count, root_idx, "name"); + if (i >= 0) { + js_token_to_string(js, &tokens[i], out_seq->name, sizeof(out_seq->name)); + } + i = js_object_get(js, tokens, token_count, root_idx, "sample_period_ms"); + if (i >= 0) { + js_token_to_int(js, &tokens[i], &out_seq->sample_period_ms); + if (out_seq->sample_period_ms <= 0) { + out_seq->sample_period_ms = 500; + } + } + i = js_object_get(js, tokens, token_count, root_idx, "safety"); + if (i >= 0 && tokens[i].type == JT_OBJECT) { + int v; + int b; + double d; + v = js_object_get(js, tokens, token_count, i, "max_voltage"); + if (v >= 0 && js_token_to_double(js, &tokens[v], &d) == 0) { + seq_double_to_milli(d, &out_seq->safety.max_voltage_mv); + } + v = js_object_get(js, tokens, token_count, i, "max_current"); + if (v >= 0 && js_token_to_double(js, &tokens[v], &d) == 0) { + seq_double_to_milli(d, &out_seq->safety.max_current_ma); + } + v = js_object_get(js, tokens, token_count, i, "max_power"); + if (v >= 0 && js_token_to_double(js, &tokens[v], &d) == 0) { + seq_double_to_milli(d, &out_seq->safety.max_power_mw); + } + v = js_object_get(js, tokens, token_count, i, "abort_on_disconnect"); + if (v >= 0 && js_token_to_bool(js, &tokens[v], &b) == 0) { + out_seq->safety.abort_on_disconnect = b; + } + } + + steps_idx = js_object_get(js, tokens, token_count, root_idx, "steps"); + if (seq_parse_step_array(js, tokens, token_count, steps_idx, &out_seq->steps, &out_seq->step_count, + error_text, error_size) != 0) { + free(tokens); + return -1; + } + + abort_idx = js_object_get(js, tokens, token_count, root_idx, "abort_sequence"); + if (seq_parse_step_array(js, tokens, token_count, abort_idx, &out_seq->abort_steps, &out_seq->abort_step_count, + error_text, error_size) != 0) { + seq_set_error(error_text, error_size, "sequence needs non-empty abort_sequence array"); + free(tokens); + seq_free(out_seq); + return -1; + } + + free(tokens); + return 0; +} + +static int seq_read_text_file(const char *path, char **out_text, char *error_text, size_t error_size) { + FILE *fp; + long size; + char *buf; + size_t got; + if (!path || !out_text) { + seq_set_error(error_text, error_size, "invalid sequence path"); + return -1; + } + *out_text = NULL; + fp = fopen(path, "rb"); + if (!fp) { + seq_set_error(error_text, error_size, "cannot open sequence file: %s", path); + return -1; + } + if (fseek(fp, 0, SEEK_END) != 0 || (size = ftell(fp)) < 0 || fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + seq_set_error(error_text, error_size, "cannot size sequence file: %s", path); + return -1; + } + buf = calloc((size_t)size + 1, 1); + if (!buf) { + fclose(fp); + seq_set_error(error_text, error_size, "out of memory reading sequence file"); + return -1; + } + got = fread(buf, 1, (size_t)size, fp); + fclose(fp); + if (got != (size_t)size) { + free(buf); + seq_set_error(error_text, error_size, "failed to read sequence file: %s", path); + return -1; + } + buf[size] = '\0'; + *out_text = buf; + return 0; +} + +static int seq_check_limits(const mw_seq_safety *safety, const mw_report *report, + char *error_text, size_t error_size) { + uint32_t power_mw; + if (!safety || !report) { + return SEQ_EXEC_OK; + } + power_mw = mw_report_power_mw(report); + if (safety->max_current_ma && report->current_ma > safety->max_current_ma) { + seq_set_error(error_text, error_size, "safety abort: current %.3f A exceeds limit %.3f A", + report->current_ma / 1000.0, safety->max_current_ma / 1000.0); + return SEQ_EXEC_ABORT; + } + if (safety->max_voltage_mv && report->voltage_mv > safety->max_voltage_mv) { + seq_set_error(error_text, error_size, "safety abort: voltage %.3f V exceeds limit %.3f V", + report->voltage_mv / 1000.0, safety->max_voltage_mv / 1000.0); + return SEQ_EXEC_ABORT; + } + if (safety->max_power_mw && power_mw > safety->max_power_mw) { + seq_set_error(error_text, error_size, "safety abort: power %.3f W exceeds limit %.3f W", + power_mw / 1000.0, safety->max_power_mw / 1000.0); + return SEQ_EXEC_ABORT; + } + return SEQ_EXEC_OK; +} + +static int seq_log_report(mw_sequence_exec *exec, const char *context, long step_index, const mw_report *report) { + if (!exec || !report) { + return -1; + } + exec->result->last_report = *report; + exec->result->last_report_valid = true; + if (exec->logger) { + if (mw_csv_logger_write(exec->logger, context, step_index, report) != 0) { + return -1; + } + exec->result->samples_written++; + } + return 0; +} + +static int seq_push_step_break(mw_sequence_exec *exec, const mw_seq_step *step, + char *error_text, size_t error_size) { + if (!exec || !step || !step->has_break_if) { + return SEQ_EXEC_OK; + } + if (exec->break_stack_count >= (sizeof(exec->break_stack) / sizeof(exec->break_stack[0]))) { + seq_set_error(error_text, error_size, "break_if nesting too deep"); + return SEQ_EXEC_FAIL; + } + exec->break_stack[exec->break_stack_count++] = &step->break_if; + return SEQ_EXEC_OK; +} + +static void seq_pop_step_break(mw_sequence_exec *exec, const mw_seq_step *step) { + if (!exec || !step || !step->has_break_if || exec->break_stack_count == 0) { + return; + } + exec->break_stack_count--; +} + +static int seq_check_active_breaks(mw_sequence_exec *exec, const mw_report *report, + char *error_text, size_t error_size) { + char cond_text[80]; + if (!exec || !report || exec->in_abort_sequence) { + return SEQ_EXEC_OK; + } + for (size_t i = 0; i < exec->break_stack_count; ++i) { + const mw_seq_condition *cond = exec->break_stack[i]; + if (cond && seq_condition_met(cond, report)) { + seq_condition_to_text(cond, cond_text, sizeof(cond_text)); + seq_set_error(error_text, error_size, "break_if triggered: %s", cond_text); + return SEQ_EXEC_ABORT; + } + } + return SEQ_EXEC_OK; +} + +static int seq_after_report(mw_sequence_exec *exec, const char *context, long step_index, + const mw_report *report, char *error_text, size_t error_size) { + int rc; + if (seq_log_report(exec, context, step_index, report) != 0) { + seq_set_error(error_text, error_size, "failed to write CSV log"); + return SEQ_EXEC_FAIL; + } + if (exec->in_abort_sequence) { + return SEQ_EXEC_OK; + } + rc = seq_check_limits(&exec->seq->safety, report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + return seq_check_active_breaks(exec, report, error_text, error_size); +} + + +static int seq_after_unlogged_report(mw_sequence_exec *exec, + const mw_report *report, + char *error_text, + size_t error_size) { + int rc; + if (!exec || !report) { + seq_set_error(error_text, error_size, "invalid unlogged report context"); + return SEQ_EXEC_FAIL; + } + exec->result->last_report = *report; + exec->result->last_report_valid = true; + if (exec->in_abort_sequence) { + return SEQ_EXEC_OK; + } + rc = seq_check_limits(&exec->seq->safety, report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + return seq_check_active_breaks(exec, report, error_text, error_size); +} + +static int seq_get_and_log_report(mw_sequence_exec *exec, const char *context, long step_index, + mw_report *out_report, char *error_text, size_t error_size) { + mw_report report; + if (mw_app_get_report(exec->app, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + if (out_report) { + *out_report = report; + } + return seq_after_report(exec, context, step_index, &report, error_text, error_size); +} + +static int seq_apply_output_state(mw_sequence_exec *exec, int enabled, long step_index, + char *error_text, size_t error_size) { + mw_report report; + int rc; + if (enabled) { + if (!exec->staged_valid) { + seq_set_error(error_text, error_size, "output enabled without staged target"); + return SEQ_EXEC_FAIL; + } + if (mw_app_load_on(exec->app, exec->staged_mode, exec->staged_target, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + exec->output_enabled = 1; + rc = seq_after_report(exec, "output_on", step_index, &report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + } else { + if (mw_app_load_off(exec->app, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + exec->output_enabled = 0; + rc = seq_after_report(exec, "output_off", step_index, &report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + } + return SEQ_EXEC_OK; +} + +static int seq_set_staged_target(mw_sequence_exec *exec, mw_mode mode, uint32_t milli_units, + long step_index, char *error_text, size_t error_size) { + mw_report report; + int rc; + exec->staged_mode = mode; + exec->staged_target = milli_units; + exec->staged_valid = 1; + if (exec->output_enabled) { + if (mw_app_load_on(exec->app, mode, milli_units, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + rc = seq_after_report(exec, "set_target", step_index, &report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + } + return SEQ_EXEC_OK; +} + +static int seq_poll_for_duration(mw_sequence_exec *exec, double duration_s, const char *context, + long step_index, char *error_text, size_t error_size) { + struct timespec t0; + double next_log_s = 0.0; + int keepalive_ms; + if (!exec) { + seq_set_error(error_text, error_size, "invalid duration polling context"); + return SEQ_EXEC_FAIL; + } + keepalive_ms = seq_effective_keepalive_ms(exec); + clock_gettime(CLOCK_MONOTONIC, &t0); + for (;;) { + struct timespec now; + double elapsed; + int rc; + int should_log; + mw_report report; + + if (mw_app_get_report(exec->app, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed = seq_elapsed_s(&t0, &now); + should_log = (elapsed + 1e-9 >= next_log_s) || (elapsed + 1e-9 >= duration_s); + if (should_log) { + rc = seq_after_report(exec, context, step_index, &report, error_text, error_size); + next_log_s += exec->seq->sample_period_ms / 1000.0; + } else { + rc = seq_after_unlogged_report(exec, &report, error_text, error_size); + } + if (rc != SEQ_EXEC_OK) { + return rc; + } + if (elapsed >= duration_s) { + break; + } + if (seq_sleep_ms(keepalive_ms) != 0) { + seq_set_error(error_text, error_size, "sleep interrupted"); + return SEQ_EXEC_FAIL; + } + } + return SEQ_EXEC_OK; +} + +static int seq_poll_until(mw_sequence_exec *exec, const mw_seq_condition *cond, double timeout_s, + long step_index, char *error_text, size_t error_size) { + struct timespec t0; + double next_log_s = 0.0; + int keepalive_ms; + if (!exec || !cond) { + seq_set_error(error_text, error_size, "invalid hold_until context"); + return SEQ_EXEC_FAIL; + } + keepalive_ms = seq_effective_keepalive_ms(exec); + clock_gettime(CLOCK_MONOTONIC, &t0); + for (;;) { + struct timespec now; + double elapsed; + int rc; + int should_log; + mw_report report; + + if (mw_app_get_report(exec->app, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + return SEQ_EXEC_FAIL; + } + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed = seq_elapsed_s(&t0, &now); + should_log = (elapsed + 1e-9 >= next_log_s); + if (should_log) { + rc = seq_after_report(exec, "hold_until", step_index, &report, error_text, error_size); + next_log_s += exec->seq->sample_period_ms / 1000.0; + } else { + rc = seq_after_unlogged_report(exec, &report, error_text, error_size); + } + if (rc != SEQ_EXEC_OK) { + return rc; + } + if (seq_condition_met(cond, &report)) { + return SEQ_EXEC_OK; + } + if (timeout_s > 0.0 && elapsed >= timeout_s) { + seq_set_error(error_text, error_size, "hold_until timed out after %.3f s", timeout_s); + return SEQ_EXEC_ABORT; + } + if (seq_sleep_ms(keepalive_ms) != 0) { + seq_set_error(error_text, error_size, "sleep interrupted"); + return SEQ_EXEC_FAIL; + } + } +} + +static int seq_log_loop_condition(mw_sequence_exec *exec, const char *context, long step_index, + mw_report *out_report, char *error_text, size_t error_size) { + return seq_get_and_log_report(exec, context, step_index, out_report, error_text, error_size); +} + +static int seq_run_steps(mw_sequence_exec *exec, const mw_seq_step *steps, size_t count, + size_t *completed_out, char *error_text, size_t error_size); + +static int seq_run_step(mw_sequence_exec *exec, const mw_seq_step *step, + char *error_text, size_t error_size) { + long step_index; + int rc; + if (!exec || !step) { + seq_set_error(error_text, error_size, "invalid step execution context"); + return SEQ_EXEC_FAIL; + } + + step_index = exec->exec_step_counter++; + rc = seq_push_step_break(exec, step, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + return rc; + } + + switch (step->kind) { + case MW_SEQ_ACT_SET_MODE: + exec->staged_mode = step->mode; + rc = SEQ_EXEC_OK; + break; + case MW_SEQ_ACT_SET_TARGET: + rc = seq_set_staged_target(exec, step->mode, step->milli_units, step_index, error_text, error_size); + break; + case MW_SEQ_ACT_OUTPUT: + rc = seq_apply_output_state(exec, step->enabled, step_index, error_text, error_size); + break; + case MW_SEQ_ACT_HOLD: + rc = seq_poll_for_duration(exec, step->duration_s, "hold", step_index, error_text, error_size); + break; + case MW_SEQ_ACT_HOLD_UNTIL: + rc = seq_poll_until(exec, &step->condition, step->timeout_s, step_index, error_text, error_size); + break; + case MW_SEQ_ACT_SAFE: { + mw_report report; + if (mw_app_safe(exec->app, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + rc = SEQ_EXEC_FAIL; + } else { + exec->output_enabled = 0; + exec->staged_mode = MW_MODE_CURRENT; + exec->staged_target = 0; + exec->staged_valid = 1; + rc = seq_after_report(exec, exec->in_abort_sequence ? "abort_safe" : "safe", + step_index, &report, error_text, error_size); + } + break; + } + case MW_SEQ_ACT_REMOTE: { + mw_report report; + if (mw_app_set_remote(exec->app, step->enabled ? true : false, &report) != 0) { + seq_set_error(error_text, error_size, "%s", mw_app_last_error(exec->app)); + rc = SEQ_EXEC_FAIL; + } else { + rc = seq_after_report(exec, step->enabled ? "remote_on" : "remote_off", + step_index, &report, error_text, error_size); + } + break; + } + case MW_SEQ_ACT_RAMP: { + uint32_t value = step->ramp_start; + int ascending = step->ramp_stop >= step->ramp_start; + rc = SEQ_EXEC_OK; + for (;;) { + rc = seq_set_staged_target(exec, step->mode, value, step_index, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + if (!exec->output_enabled) { + rc = seq_apply_output_state(exec, 1, step_index, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + } + rc = seq_poll_for_duration(exec, step->dwell_s, "ramp", step_index, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + if (value == step->ramp_stop) { + break; + } + if (ascending) { + if (value + step->ramp_step >= step->ramp_stop) { + value = step->ramp_stop; + } else { + value += step->ramp_step; + } + } else { + if (value <= step->ramp_step || value - step->ramp_step <= step->ramp_stop) { + value = step->ramp_stop; + } else { + value -= step->ramp_step; + } + } + } + break; + } + case MW_SEQ_ACT_REPEAT: { + rc = SEQ_EXEC_OK; + for (int n = 0; n < step->repeat_times; ++n) { + rc = seq_run_steps(exec, step->children, step->child_count, NULL, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + } + break; + } + case MW_SEQ_ACT_REPEAT_UNTIL: { + struct timespec t0; + rc = SEQ_EXEC_OK; + clock_gettime(CLOCK_MONOTONIC, &t0); + for (;;) { + struct timespec now; + double elapsed; + mw_report report; + rc = seq_run_steps(exec, step->children, step->child_count, NULL, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + rc = seq_log_loop_condition(exec, "repeat_until_check", step_index, &report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + if (seq_condition_met(&step->condition, &report)) { + rc = SEQ_EXEC_OK; + break; + } + if (step->timeout_s > 0.0) { + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed = (double)(now.tv_sec - t0.tv_sec) + (double)(now.tv_nsec - t0.tv_nsec) / 1000000000.0; + if (elapsed >= step->timeout_s) { + seq_set_error(error_text, error_size, "repeat_until timed out after %.3f s", step->timeout_s); + rc = SEQ_EXEC_ABORT; + break; + } + } + } + break; + } + case MW_SEQ_ACT_REPEAT_WHILE: { + struct timespec t0; + rc = SEQ_EXEC_OK; + clock_gettime(CLOCK_MONOTONIC, &t0); + for (;;) { + struct timespec now; + double elapsed; + mw_report report; + rc = seq_log_loop_condition(exec, "repeat_while_check", step_index, &report, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + if (!seq_condition_met(&step->condition, &report)) { + rc = SEQ_EXEC_OK; + break; + } + rc = seq_run_steps(exec, step->children, step->child_count, NULL, error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + if (step->timeout_s > 0.0) { + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed = (double)(now.tv_sec - t0.tv_sec) + (double)(now.tv_nsec - t0.tv_nsec) / 1000000000.0; + if (elapsed >= step->timeout_s) { + seq_set_error(error_text, error_size, "repeat_while timed out after %.3f s", step->timeout_s); + rc = SEQ_EXEC_ABORT; + break; + } + } + } + break; + } + default: + seq_set_error(error_text, error_size, "unsupported step kind"); + rc = SEQ_EXEC_FAIL; + break; + } + + seq_pop_step_break(exec, step); + return rc; +} + +static int seq_run_steps(mw_sequence_exec *exec, const mw_seq_step *steps, size_t count, + size_t *completed_out, char *error_text, size_t error_size) { + size_t completed = 0; + int rc = SEQ_EXEC_OK; + for (size_t i = 0; i < count; ++i) { + rc = seq_run_step(exec, &steps[i], error_text, error_size); + if (rc != SEQ_EXEC_OK) { + break; + } + completed++; + } + if (completed_out) { + *completed_out = completed; + } + return rc; +} + +static int seq_run_abort_sequence(mw_sequence_exec *exec, char *error_text, size_t error_size) { + size_t completed = 0; + int saved_abort = exec->in_abort_sequence; + int rc; + exec->in_abort_sequence = 1; + rc = seq_run_steps(exec, exec->seq->abort_steps, exec->seq->abort_step_count, &completed, error_text, error_size); + exec->in_abort_sequence = saved_abort; + exec->result->abort_sequence_ran = true; + exec->result->abort_steps_completed = completed; + return rc; +} + +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) { + char *json_text = NULL; + mw_sequence seq; + mw_csv_logger *logger = NULL; + mw_sequence_exec exec; + mw_sequence_result result; + int main_rc = SEQ_EXEC_FAIL; + int final_rc = -1; + + if (error_text && error_text_size > 0) { + error_text[0] = '\0'; + } + if (!app || !json_path || !out_result) { + seq_set_error(error_text, error_text_size, "invalid arguments to sequence runner"); + return -1; + } + memset(&result, 0, sizeof(result)); + memset(&seq, 0, sizeof(seq)); + + if (seq_read_text_file(json_path, &json_text, error_text, error_text_size) != 0) { + return -1; + } + if (seq_parse_json_text(json_text, &seq, error_text, error_text_size) != 0) { + goto done; + } + if (options && options->sample_period_ms_override > 0) { + seq.sample_period_ms = options->sample_period_ms_override; + } + if (options && options->csv_path && *options->csv_path) { + if (mw_csv_logger_open(&logger, options->csv_path, options->csv_units_mode) != 0) { + seq_set_error(error_text, error_text_size, "failed to open CSV log: %s", options->csv_path); + goto done; + } + } + + memset(&exec, 0, sizeof(exec)); + exec.app = app; + exec.seq = &seq; + exec.logger = logger; + exec.result = &result; + exec.staged_mode = MW_MODE_CURRENT; + + if (seq.name[0]) { + snprintf(result.name, sizeof(result.name), "%s", seq.name); + } + result.sample_period_ms = seq.sample_period_ms; + result.steps_total = seq.step_count; + + main_rc = seq_run_steps(&exec, seq.steps, seq.step_count, &result.steps_completed, error_text, error_text_size); + if (main_rc != SEQ_EXEC_OK) { + result.aborted = true; + } + + if (seq_run_abort_sequence(&exec, error_text, error_text_size) != SEQ_EXEC_OK) { + if (options && options->safe_on_abort) { + mw_report safe_report; + if (mw_app_safe(app, &safe_report) == 0) { + exec.in_abort_sequence = 1; + seq_log_report(&exec, "safe_fallback", -1, &safe_report); + exec.in_abort_sequence = 0; + } + } + goto done; + } + + if (main_rc == SEQ_EXEC_OK) { + final_rc = 0; + } else { + final_rc = -1; + } + +done: + *out_result = result; + mw_csv_logger_close(logger); + seq_free(&seq); + free(json_text); + return final_rc; +} diff --git a/src/mwcli.c b/src/mwcli.c new file mode 100644 index 0000000..24792b2 --- /dev/null +++ b/src/mwcli.c @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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] [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 \n" + " set-voltage \n" + " set-power \n" + " set-resistance \n" + " set-vinv \n" + " hold \n" + " load-on \n" + " load-off\n" + " safe\n" + " remote on|off\n" + " get-series\n" + " set-series \n" + " run-sequence \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 "); + } else { + fprintf(stderr, "hold needs \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 "); + } else { + fprintf(stderr, "load-on needs \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; +}