full version of beta including resources and binary

This commit is contained in:
2026-04-12 18:21:05 +02:00
parent ce1d6e52ab
commit 429b28ef67
24 changed files with 4491 additions and 2 deletions

2
.gitignore vendored
View File

@ -1,7 +1,5 @@
# Build outputs
*.o
mwcli
mw_controller_example
# Logs and captures
*.csv

BIN
bin/mw_controller_example Normal file

Binary file not shown.

BIN
bin/mwcli Normal file

Binary file not shown.

View File

@ -0,0 +1,25 @@
{
"name": "battery_discharge_cutoff",
"sample_period_ms": 1000,
"safety": {
"max_voltage": 5.0,
"max_current": 0.5,
"max_power": 2.5,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{ "action": "set_current", "value": 0.20 },
{ "action": "output", "enabled": true },
{
"action": "hold_until",
"timeout_s": 14400,
"condition": { "type": "voltage_below", "value": 3.00 },
"break_if": { "type": "temperature_above", "value": 50.0 }
},
{ "action": "output", "enabled": false }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,28 @@
{
"name": "cc_step_sweep",
"sample_period_ms": 500,
"safety": {
"max_voltage": 6.0,
"max_current": 0.8,
"max_power": 4.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{ "action": "set_current", "value": 0.10 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 10 },
{
"action": "ramp_current",
"start": 0.10,
"stop": 0.50,
"step": 0.10,
"dwell_s": 10,
"break_if": { "type": "temperature_above", "value": 45.0 }
},
{ "action": "output", "enabled": false }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,21 @@
{
"name": "cp_step_sweep",
"sample_period_ms": 500,
"safety": {
"max_voltage": 6.0,
"max_current": 0.8,
"max_power": 3.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CP" },
{ "action": "set_power", "value": 0.50 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 10 },
{ "action": "ramp_power", "start": 0.50, "stop": 2.50, "step": 0.50, "dwell_s": 10 },
{ "action": "output", "enabled": false }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,21 @@
{
"name": "cr_step_sweep",
"sample_period_ms": 500,
"safety": {
"max_voltage": 6.0,
"max_current": 0.8,
"max_power": 4.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CR" },
{ "action": "set_resistance", "value": 50.0 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 10 },
{ "action": "ramp_resistance", "start": 50.0, "stop": 10.0, "step": 10.0, "dwell_s": 10 },
{ "action": "output", "enabled": false }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,21 @@
{
"name": "load_test_cc",
"sample_period_ms": 500,
"safety": {
"max_voltage": 30.0,
"max_current": 2.0,
"max_power": 20.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{ "action": "set_current", "value": 0.20 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 30 },
{ "action": "ramp_current", "start": 0.20, "stop": 1.00, "step": 0.10, "dwell_s": 20 },
{ "action": "hold_until", "timeout_s": 300, "condition": { "type": "voltage_below", "value": 3.00 } }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,103 @@
#define _POSIX_C_SOURCE 200809L
#include "mightywatt_controller.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
static void sleep_ms(int ms) {
struct timespec ts;
if (ms < 0) {
ms = 0;
}
ts.tv_sec = ms / 1000;
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
nanosleep(&ts, NULL);
}
int main(int argc, char **argv) {
mw_controller *controller = NULL;
mw_controller_config config;
mw_controller_snapshot snapshot;
uint64_t cmd_id = 0;
int i;
if (argc < 2) {
fprintf(stderr, "usage: %s /dev/ttyACM0\n", argv[0]);
return 2;
}
mw_controller_config_init(&config);
config.port_path = argv[1];
config.poll_interval_ms = 500;
config.reconnect_ms = 1500;
if (mw_controller_start(&controller, &config) != 0) {
fprintf(stderr, "controller start failed\n");
return 1;
}
for (i = 0; i < 20; ++i) {
if (mw_controller_get_snapshot(controller, &snapshot) != 0) {
fprintf(stderr, "snapshot failed\n");
mw_controller_stop(controller);
return 1;
}
printf("[%02d] state=%s connected=%s pending=%zu error=%s\n",
i,
mw_controller_connection_state_name(snapshot.connection_state),
snapshot.connected ? "yes" : "no",
snapshot.pending_commands,
snapshot.last_error[0] ? snapshot.last_error : "-");
if (snapshot.connected) {
break;
}
sleep_ms(250);
}
if (!snapshot.connected) {
fprintf(stderr, "device did not connect in time\n");
mw_controller_stop(controller);
return 1;
}
if (mw_controller_queue_load_on(controller, MW_MODE_CURRENT, 250, &cmd_id) != 0) {
fprintf(stderr, "queue load-on failed\n");
mw_controller_stop(controller);
return 1;
}
printf("queued command id=%" PRIu64 "\n", cmd_id);
for (;;) {
if (mw_controller_wait_for_update(controller, snapshot.snapshot_seq, 2000, NULL) < 0) {
fprintf(stderr, "wait for update failed\n");
break;
}
if (mw_controller_get_snapshot(controller, &snapshot) != 0) {
fprintf(stderr, "snapshot failed\n");
break;
}
if (snapshot.last_completed_command_id >= cmd_id) {
printf("command result=%d completed=%" PRIu64 " state=%s\n",
snapshot.last_completed_result,
snapshot.last_completed_command_id,
mw_controller_connection_state_name(snapshot.connection_state));
if (snapshot.app_state_valid && snapshot.app_state.report_valid) {
printf("I=%.3f A V=%.3f V remote=%s status=%u\n",
snapshot.app_state.last_report.current_ma / 1000.0,
snapshot.app_state.last_report.voltage_mv / 1000.0,
snapshot.app_state.last_report.remote ? "on" : "off",
(unsigned)snapshot.app_state.last_report.status);
}
break;
}
}
mw_controller_queue_safe(controller, NULL);
sleep_ms(400);
mw_controller_stop(controller);
return 0;
}

View File

@ -0,0 +1,24 @@
{
"name": "quick_cc_test",
"sample_period_ms": 500,
"safety": {
"max_voltage": 6.0,
"max_current": 0.6,
"max_power": 3.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{ "action": "set_current", "value": 0.10 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 5 },
{ "action": "set_current", "value": 0.25 },
{ "action": "hold", "duration_s": 5 },
{ "action": "set_current", "value": 0.50 },
{ "action": "hold", "duration_s": 5 },
{ "action": "output", "enabled": false }
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,30 @@
{
"name": "repeat_n_pulse_test",
"sample_period_ms": 500,
"safety": {
"max_voltage": 6.0,
"max_current": 0.8,
"max_power": 4.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{
"action": "repeat",
"times": 5,
"break_if": { "type": "temperature_above", "value": 45.0 },
"steps": [
{ "action": "set_current", "value": 0.20 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 5 },
{ "action": "set_current", "value": 0.50 },
{ "action": "hold", "duration_s": 5 },
{ "action": "output", "enabled": false },
{ "action": "hold", "duration_s": 2 }
]
}
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,29 @@
{
"name": "repeat_until_cutoff",
"sample_period_ms": 1000,
"safety": {
"max_voltage": 5.0,
"max_current": 0.6,
"max_power": 3.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{
"action": "repeat_until",
"timeout_s": 14400,
"condition": { "type": "voltage_below", "value": 3.20 },
"break_if": { "type": "temperature_above", "value": 45.0 },
"steps": [
{ "action": "set_current", "value": 0.30 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 60 },
{ "action": "output", "enabled": false },
{ "action": "hold", "duration_s": 10 }
]
}
],
"abort_sequence": [
{ "action": "safe" }
]
}

View File

@ -0,0 +1,29 @@
{
"name": "repeat_while_burn_in",
"sample_period_ms": 1000,
"safety": {
"max_voltage": 6.0,
"max_current": 0.5,
"max_power": 3.0,
"abort_on_disconnect": true
},
"steps": [
{ "action": "set_mode", "mode": "CC" },
{
"action": "repeat_while",
"timeout_s": 7200,
"condition": { "type": "temperature_above", "value": 20.0 },
"break_if": { "type": "temperature_above", "value": 50.0 },
"steps": [
{ "action": "set_current", "value": 0.15 },
{ "action": "output", "enabled": true },
{ "action": "hold", "duration_s": 30 },
{ "action": "output", "enabled": false },
{ "action": "hold", "duration_s": 15 }
]
}
],
"abort_sequence": [
{ "action": "safe" }
]
}

76
include/mightywatt.h Normal file
View File

@ -0,0 +1,76 @@
#ifndef MIGHTYWATT_H
#define MIGHTYWATT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MW_FW_VERSION_MAX 31
#define MW_BOARD_REV_MAX 31
typedef struct mw_device mw_device;
typedef enum {
MW_MODE_CURRENT = 0,
MW_MODE_VOLTAGE = 1,
MW_MODE_POWER = 2,
MW_MODE_RESISTANCE = 3,
MW_MODE_VOLTAGE_INVERTED = 4,
} mw_mode;
enum {
MW_STATUS_READY = 0,
MW_STATUS_CURRENT_OVERLOAD = 1 << 0,
MW_STATUS_VOLTAGE_OVERLOAD = 1 << 1,
MW_STATUS_POWER_OVERLOAD = 1 << 2,
MW_STATUS_OVERHEAT = 1 << 3,
};
typedef struct {
char firmware_version[MW_FW_VERSION_MAX + 1];
char board_revision[MW_BOARD_REV_MAX + 1];
uint32_t max_current_dac_ma;
uint32_t max_current_adc_ma;
uint32_t max_voltage_dac_mv;
uint32_t max_voltage_adc_mv;
uint32_t max_power_mw;
uint32_t dvm_input_resistance_ohm;
uint32_t temperature_threshold_c;
} mw_capabilities;
typedef struct {
uint16_t current_ma;
uint16_t voltage_mv;
uint8_t temperature_c;
bool remote;
uint8_t status;
} mw_report;
const char *mw_last_error(const mw_device *dev);
int mw_open(mw_device **out_dev, const char *port_path, int settle_ms);
void mw_close(mw_device *dev);
int mw_identify(mw_device *dev);
int mw_query_capabilities(mw_device *dev, mw_capabilities *caps);
int mw_get_report(mw_device *dev, mw_report *report);
int mw_set(mw_device *dev, mw_mode mode, uint32_t milli_units, mw_report *report);
int mw_set_remote(mw_device *dev, bool enable, mw_report *report);
int mw_set_series_resistance(mw_device *dev, uint16_t milliohm, mw_report *report);
int mw_get_series_resistance(mw_device *dev, uint16_t *milliohm);
size_t mw_status_string(uint8_t status, char *buffer, size_t buffer_size);
const char *mw_mode_name(mw_mode mode);
uint32_t mw_report_power_mw(const mw_report *report);
uint32_t mw_capability_limit_for_mode(const mw_capabilities *caps, mw_mode mode);
int mw_validate_target(const mw_capabilities *caps, mw_mode mode, uint32_t milli_units,
char *buffer, size_t buffer_size);
#ifdef __cplusplus
}
#endif
#endif

45
include/mightywatt_app.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef MIGHTYWATT_APP_H
#define MIGHTYWATT_APP_H
#include "mightywatt.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct mw_app mw_app;
typedef struct {
bool capabilities_valid;
mw_capabilities capabilities;
bool report_valid;
mw_report last_report;
bool target_valid;
mw_mode target_mode;
uint32_t target_milli_units;
bool restore_target_valid;
mw_mode restore_target_mode;
uint32_t restore_target_milli_units;
} mw_app_state;
int mw_app_open(mw_app **out_app, const char *port_path, int settle_ms);
void mw_app_close(mw_app *app);
const char *mw_app_last_error(const mw_app *app);
int mw_app_refresh_capabilities(mw_app *app, mw_capabilities *out_caps);
int mw_app_get_report(mw_app *app, mw_report *out_report);
int mw_app_set_target(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report);
int mw_app_load_off(mw_app *app, mw_report *out_report);
int mw_app_load_on(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report);
int mw_app_restore_target(mw_app *app, mw_report *out_report);
int mw_app_safe(mw_app *app, mw_report *out_report);
int mw_app_set_remote(mw_app *app, bool enable, mw_report *out_report);
int mw_app_get_series_resistance(mw_app *app, uint16_t *milliohm);
int mw_app_set_series_resistance(mw_app *app, uint16_t milliohm, mw_report *out_report);
int mw_app_get_state(const mw_app *app, mw_app_state *out_state);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,113 @@
#ifndef MIGHTYWATT_CONTROLLER_H
#define MIGHTYWATT_CONTROLLER_H
#include "mightywatt_app.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MW_CONTROLLER_PORT_PATH_MAX 255
#define MW_CONTROLLER_ERROR_MAX 255
typedef struct mw_controller mw_controller;
typedef enum {
MW_CONTROLLER_STOPPED = 0,
MW_CONTROLLER_CONNECTING = 1,
MW_CONTROLLER_CONNECTED = 2,
MW_CONTROLLER_RECONNECT_WAIT = 3,
} mw_controller_connection_state;
typedef enum {
MW_CTRL_CMD_NONE = 0,
MW_CTRL_CMD_SET_TARGET,
MW_CTRL_CMD_LOAD_ON,
MW_CTRL_CMD_LOAD_OFF,
MW_CTRL_CMD_RESTORE_TARGET,
MW_CTRL_CMD_SAFE,
MW_CTRL_CMD_SET_REMOTE,
MW_CTRL_CMD_SET_SERIES_RESISTANCE,
MW_CTRL_CMD_REFRESH_CAPABILITIES,
MW_CTRL_CMD_GET_SERIES_RESISTANCE,
} mw_controller_command_kind;
typedef struct {
const char *port_path;
int settle_ms;
int poll_interval_ms;
int reconnect_ms;
size_t queue_capacity;
bool safe_on_shutdown;
} mw_controller_config;
typedef struct {
bool running;
bool connected;
mw_controller_connection_state connection_state;
uint64_t snapshot_seq;
uint64_t connect_attempts;
uint64_t reconnect_count;
uint64_t poll_success_count;
uint64_t poll_error_count;
uint64_t command_success_count;
uint64_t command_error_count;
uint64_t last_queued_command_id;
uint64_t last_completed_command_id;
int last_completed_result;
mw_controller_command_kind last_completed_kind;
size_t pending_commands;
char port_path[MW_CONTROLLER_PORT_PATH_MAX + 1];
char last_error[MW_CONTROLLER_ERROR_MAX + 1];
char last_completed_error[MW_CONTROLLER_ERROR_MAX + 1];
bool app_state_valid;
mw_app_state app_state;
bool series_resistance_valid;
uint16_t series_resistance_milliohm;
} mw_controller_snapshot;
void mw_controller_config_init(mw_controller_config *config);
int mw_controller_start(mw_controller **out_controller, const mw_controller_config *config);
void mw_controller_stop(mw_controller *controller);
int mw_controller_get_snapshot(mw_controller *controller, mw_controller_snapshot *out_snapshot);
int mw_controller_wait_for_update(mw_controller *controller,
uint64_t last_seen_snapshot_seq,
int timeout_ms,
uint64_t *out_snapshot_seq);
int mw_controller_queue_set_target(mw_controller *controller,
mw_mode mode,
uint32_t milli_units,
uint64_t *out_command_id);
int mw_controller_queue_load_on(mw_controller *controller,
mw_mode mode,
uint32_t milli_units,
uint64_t *out_command_id);
int mw_controller_queue_load_off(mw_controller *controller, uint64_t *out_command_id);
int mw_controller_queue_restore_target(mw_controller *controller, uint64_t *out_command_id);
int mw_controller_queue_safe(mw_controller *controller, uint64_t *out_command_id);
int mw_controller_queue_set_remote(mw_controller *controller,
bool enable,
uint64_t *out_command_id);
int mw_controller_queue_set_series_resistance(mw_controller *controller,
uint16_t milliohm,
uint64_t *out_command_id);
int mw_controller_queue_refresh_capabilities(mw_controller *controller,
uint64_t *out_command_id);
int mw_controller_queue_get_series_resistance(mw_controller *controller,
uint64_t *out_command_id);
const char *mw_controller_connection_state_name(mw_controller_connection_state state);
const char *mw_controller_command_kind_name(mw_controller_command_kind kind);
#ifdef __cplusplus
}
#endif
#endif

30
include/mightywatt_log.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef MIGHTYWATT_LOG_H
#define MIGHTYWATT_LOG_H
#include "mightywatt.h"
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct mw_csv_logger mw_csv_logger;
typedef enum {
MW_CSV_UNITS_ENGINEERING = 0,
MW_CSV_UNITS_RAW = 1
} mw_csv_units_mode;
int mw_csv_logger_open(mw_csv_logger **out_logger, const char *path, mw_csv_units_mode units_mode);
void mw_csv_logger_close(mw_csv_logger *logger);
int mw_csv_logger_write(mw_csv_logger *logger,
const char *context,
long step_index,
const mw_report *report);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -0,0 +1,46 @@
#ifndef MIGHTYWATT_SEQUENCE_H
#define MIGHTYWATT_SEQUENCE_H
#include "mightywatt_app.h"
#include "mightywatt_log.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *csv_path;
int sample_period_ms_override;
bool safe_on_abort;
mw_csv_units_mode csv_units_mode;
} mw_sequence_run_options;
typedef struct {
char name[64];
int sample_period_ms;
size_t steps_total;
size_t steps_completed;
size_t samples_written;
bool aborted;
bool abort_sequence_ran;
size_t abort_steps_completed;
bool last_report_valid;
mw_report last_report;
} mw_sequence_result;
int mw_sequence_run_file(mw_app *app,
const char *json_path,
const mw_sequence_run_options *options,
mw_sequence_result *out_result,
char *error_text,
size_t error_text_size);
#ifdef __cplusplus
}
#endif
#endif

549
src/mightywatt.c Normal file
View File

@ -0,0 +1,549 @@
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "mightywatt.h"
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
struct mw_device {
int fd;
char port_path[256];
char last_error[256];
};
enum {
MW_CMD_SERIES_RESISTANCE = 28,
MW_CMD_REMOTE = 29,
MW_CMD_QDC = 30,
MW_CMD_IDN = 31,
MW_REPORT_LEN = 7,
};
static void mw_set_error(mw_device *dev, const char *fmt, ...) {
va_list ap;
if (!dev) {
return;
}
va_start(ap, fmt);
vsnprintf(dev->last_error, sizeof(dev->last_error), fmt, ap);
va_end(ap);
}
const char *mw_last_error(const mw_device *dev) {
return dev ? dev->last_error : "invalid device";
}
static int mw_sleep_ms(int ms) {
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (long)(ms % 1000) * 1000000L;
while (nanosleep(&ts, &ts) == -1) {
if (errno != EINTR) {
return -1;
}
}
return 0;
}
static int mw_write_all(mw_device *dev, const uint8_t *data, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t rc = write(dev->fd, data + sent, len - sent);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
mw_set_error(dev, "write failed: %s", strerror(errno));
return -1;
}
sent += (size_t)rc;
}
return 0;
}
static int mw_read_exact(mw_device *dev, uint8_t *data, size_t len) {
size_t got = 0;
while (got < len) {
ssize_t rc = read(dev->fd, data + got, len - got);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
mw_set_error(dev, "read failed: %s", strerror(errno));
return -1;
}
if (rc == 0) {
mw_set_error(dev, "read timed out");
return -1;
}
got += (size_t)rc;
}
return 0;
}
static int mw_read_line(mw_device *dev, char *buffer, size_t buffer_size) {
size_t pos = 0;
if (!buffer || buffer_size == 0) {
mw_set_error(dev, "internal buffer size error");
return -1;
}
while (pos + 1 < buffer_size) {
unsigned char c;
ssize_t rc = read(dev->fd, &c, 1);
if (rc < 0) {
if (errno == EINTR) {
continue;
}
mw_set_error(dev, "read line failed: %s", strerror(errno));
return -1;
}
if (rc == 0) {
mw_set_error(dev, "read line timed out");
return -1;
}
if (c == '\n') {
break;
}
if (c == '\r') {
continue;
}
buffer[pos++] = (char)c;
}
buffer[pos] = '\0';
if (pos + 1 == buffer_size) {
mw_set_error(dev, "response line too long");
return -1;
}
return 0;
}
static int mw_flush_io(mw_device *dev) {
if (tcflush(dev->fd, TCIOFLUSH) != 0) {
mw_set_error(dev, "tcflush failed: %s", strerror(errno));
return -1;
}
return 0;
}
static int mw_configure_port(mw_device *dev) {
struct termios tio;
if (tcgetattr(dev->fd, &tio) != 0) {
mw_set_error(dev, "tcgetattr failed: %s", strerror(errno));
return -1;
}
cfmakeraw(&tio);
if (cfsetispeed(&tio, B115200) != 0 || cfsetospeed(&tio, B115200) != 0) {
mw_set_error(dev, "failed to set serial speed: %s", strerror(errno));
return -1;
}
tio.c_cflag |= (CLOCAL | CREAD);
tio.c_cflag &= ~PARENB;
tio.c_cflag &= ~CSTOPB;
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
#ifdef CRTSCTS
tio.c_cflag &= ~CRTSCTS;
#endif
#ifdef HUPCL
tio.c_cflag &= ~HUPCL;
#endif
tio.c_iflag &= ~(IXON | IXOFF | IXANY);
tio.c_cc[VMIN] = 0;
tio.c_cc[VTIME] = 3;
if (tcsetattr(dev->fd, TCSANOW, &tio) != 0) {
mw_set_error(dev, "tcsetattr failed: %s", strerror(errno));
return -1;
}
return 0;
}
int mw_open(mw_device **out_dev, const char *port_path, int settle_ms) {
mw_device *dev;
int fd;
if (!out_dev || !port_path) {
return -1;
}
*out_dev = NULL;
dev = calloc(1, sizeof(*dev));
if (!dev) {
return -1;
}
fd = open(port_path, O_RDWR | O_NOCTTY | O_CLOEXEC);
if (fd < 0) {
snprintf(dev->last_error, sizeof(dev->last_error), "open failed: %s", strerror(errno));
free(dev);
return -1;
}
dev->fd = fd;
snprintf(dev->port_path, sizeof(dev->port_path), "%s", port_path);
if (mw_configure_port(dev) != 0) {
close(fd);
free(dev);
return -1;
}
if (settle_ms < 0) {
settle_ms = 0;
}
if (settle_ms > 0 && mw_sleep_ms(settle_ms) != 0) {
mw_set_error(dev, "sleep interrupted unexpectedly");
close(fd);
free(dev);
return -1;
}
if (mw_flush_io(dev) != 0) {
close(fd);
free(dev);
return -1;
}
*out_dev = dev;
return 0;
}
void mw_close(mw_device *dev) {
if (!dev) {
return;
}
if (dev->fd >= 0) {
close(dev->fd);
}
free(dev);
}
int mw_identify(mw_device *dev) {
char line[128];
uint8_t cmd = MW_CMD_IDN;
int tries;
if (!dev) {
return -1;
}
for (tries = 0; tries < 3; ++tries) {
if (mw_flush_io(dev) != 0) {
return -1;
}
if (mw_write_all(dev, &cmd, 1) != 0) {
return -1;
}
if (mw_read_line(dev, line, sizeof(line)) == 0 && strcmp(line, "MightyWatt") == 0) {
return 0;
}
mw_sleep_ms(150);
}
mw_set_error(dev, "device did not identify as MightyWatt");
return -1;
}
static int mw_parse_u32(mw_device *dev, const char *line, uint32_t *out) {
char *end = NULL;
unsigned long value;
errno = 0;
value = strtoul(line, &end, 10);
if (errno != 0 || end == line || *end != '\0') {
mw_set_error(dev, "invalid numeric response: '%s'", line);
return -1;
}
*out = (uint32_t)value;
return 0;
}
int mw_query_capabilities(mw_device *dev, mw_capabilities *caps) {
uint8_t cmd = MW_CMD_QDC;
char line[128];
uint32_t values[7];
int i;
if (!dev || !caps) {
return -1;
}
memset(caps, 0, sizeof(*caps));
if (mw_flush_io(dev) != 0) {
return -1;
}
if (mw_write_all(dev, &cmd, 1) != 0) {
return -1;
}
if (mw_read_line(dev, caps->firmware_version, sizeof(caps->firmware_version)) != 0) {
return -1;
}
if (mw_read_line(dev, caps->board_revision, sizeof(caps->board_revision)) != 0) {
return -1;
}
for (i = 0; i < 7; ++i) {
if (mw_read_line(dev, line, sizeof(line)) != 0) {
return -1;
}
if (mw_parse_u32(dev, line, &values[i]) != 0) {
return -1;
}
}
caps->max_current_dac_ma = values[0];
caps->max_current_adc_ma = values[1];
caps->max_voltage_dac_mv = values[2];
caps->max_voltage_adc_mv = values[3];
caps->max_power_mw = values[4];
caps->dvm_input_resistance_ohm = values[5];
caps->temperature_threshold_c = values[6];
return 0;
}
static void mw_decode_report(const uint8_t raw[MW_REPORT_LEN], mw_report *report) {
report->current_ma = (uint16_t)(((uint16_t)raw[0] << 8) | raw[1]);
report->voltage_mv = (uint16_t)(((uint16_t)raw[2] << 8) | raw[3]);
report->temperature_c = raw[4];
report->remote = raw[5] ? true : false;
report->status = raw[6];
}
int mw_get_report(mw_device *dev, mw_report *report) {
uint8_t cmd = 0;
uint8_t raw[MW_REPORT_LEN];
if (!dev || !report) {
return -1;
}
if (mw_write_all(dev, &cmd, 1) != 0) {
return -1;
}
if (mw_read_exact(dev, raw, sizeof(raw)) != 0) {
return -1;
}
mw_decode_report(raw, report);
return 0;
}
int mw_set(mw_device *dev, mw_mode mode, uint32_t milli_units, mw_report *report) {
uint8_t raw[4];
size_t len;
if (!dev || !report) {
return -1;
}
switch (mode) {
case MW_MODE_CURRENT:
case MW_MODE_VOLTAGE:
case MW_MODE_VOLTAGE_INVERTED:
if (milli_units > 0xFFFFu) {
mw_set_error(dev, "%s target out of 16-bit range", mw_mode_name(mode));
return -1;
}
len = 3;
raw[0] = (uint8_t)(0x80u | (2u << 5) | (uint8_t)mode);
raw[1] = (uint8_t)((milli_units >> 8) & 0xFFu);
raw[2] = (uint8_t)(milli_units & 0xFFu);
break;
case MW_MODE_POWER:
case MW_MODE_RESISTANCE:
if (milli_units > 0xFFFFFFu) {
mw_set_error(dev, "%s target out of 24-bit range", mw_mode_name(mode));
return -1;
}
len = 4;
raw[0] = (uint8_t)(0x80u | (3u << 5) | (uint8_t)mode);
raw[1] = (uint8_t)((milli_units >> 16) & 0xFFu);
raw[2] = (uint8_t)((milli_units >> 8) & 0xFFu);
raw[3] = (uint8_t)(milli_units & 0xFFu);
break;
default:
mw_set_error(dev, "unsupported mode");
return -1;
}
if (mw_write_all(dev, raw, len) != 0) {
return -1;
}
{
uint8_t reply[MW_REPORT_LEN];
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
return -1;
}
mw_decode_report(reply, report);
}
return 0;
}
int mw_set_remote(mw_device *dev, bool enable, mw_report *report) {
uint8_t cmd[2];
uint8_t reply[MW_REPORT_LEN];
if (!dev || !report) {
return -1;
}
cmd[0] = (uint8_t)(0x80u | (1u << 5) | MW_CMD_REMOTE);
cmd[1] = enable ? 1u : 0u;
if (mw_write_all(dev, cmd, sizeof(cmd)) != 0) {
return -1;
}
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
return -1;
}
mw_decode_report(reply, report);
return 0;
}
int mw_set_series_resistance(mw_device *dev, uint16_t milliohm, mw_report *report) {
uint8_t cmd[3];
uint8_t reply[MW_REPORT_LEN];
if (!dev || !report) {
return -1;
}
cmd[0] = (uint8_t)(0x80u | (2u << 5) | MW_CMD_SERIES_RESISTANCE);
cmd[1] = (uint8_t)((milliohm >> 8) & 0xFFu);
cmd[2] = (uint8_t)(milliohm & 0xFFu);
if (mw_write_all(dev, cmd, sizeof(cmd)) != 0) {
return -1;
}
if (mw_read_exact(dev, reply, sizeof(reply)) != 0) {
return -1;
}
mw_decode_report(reply, report);
return 0;
}
int mw_get_series_resistance(mw_device *dev, uint16_t *milliohm) {
uint8_t cmd = MW_CMD_SERIES_RESISTANCE;
uint32_t value;
char line[64];
if (!dev || !milliohm) {
return -1;
}
if (mw_flush_io(dev) != 0) {
return -1;
}
if (mw_write_all(dev, &cmd, 1) != 0) {
return -1;
}
if (mw_read_line(dev, line, sizeof(line)) != 0) {
return -1;
}
if (mw_parse_u32(dev, line, &value) != 0) {
return -1;
}
if (value > UINT16_MAX) {
mw_set_error(dev, "series resistance out of range: %u", value);
return -1;
}
*milliohm = (uint16_t)value;
return 0;
}
size_t mw_status_string(uint8_t status, char *buffer, size_t buffer_size) {
int n = 0;
int first = 1;
if (!buffer || buffer_size == 0) {
return 0;
}
buffer[0] = '\0';
if (status == 0) {
snprintf(buffer, buffer_size, "READY");
return strlen(buffer);
}
#define MW_APPEND(flag, text) \
do { \
if (status & (flag)) { \
n += snprintf(buffer + n, buffer_size > (size_t)n ? buffer_size - (size_t)n : 0, \
"%s%s", first ? "" : "|", (text)); \
first = 0; \
} \
} while (0)
MW_APPEND(MW_STATUS_CURRENT_OVERLOAD, "CURRENT_OVERLOAD");
MW_APPEND(MW_STATUS_VOLTAGE_OVERLOAD, "VOLTAGE_OVERLOAD");
MW_APPEND(MW_STATUS_POWER_OVERLOAD, "POWER_OVERLOAD");
MW_APPEND(MW_STATUS_OVERHEAT, "OVERHEAT");
#undef MW_APPEND
return strlen(buffer);
}
const char *mw_mode_name(mw_mode mode) {
switch (mode) {
case MW_MODE_CURRENT: return "current";
case MW_MODE_VOLTAGE: return "voltage";
case MW_MODE_POWER: return "power";
case MW_MODE_RESISTANCE: return "resistance";
case MW_MODE_VOLTAGE_INVERTED: return "voltage_inverted";
default: return "unknown";
}
}
uint32_t mw_report_power_mw(const mw_report *report) {
if (!report) {
return 0;
}
return ((uint32_t)report->current_ma * (uint32_t)report->voltage_mv) / 1000u;
}
uint32_t mw_capability_limit_for_mode(const mw_capabilities *caps, mw_mode mode) {
if (!caps) {
return 0;
}
switch (mode) {
case MW_MODE_CURRENT:
return (caps->max_current_adc_ma > caps->max_current_dac_ma)
? caps->max_current_adc_ma : caps->max_current_dac_ma;
case MW_MODE_VOLTAGE:
return (caps->max_voltage_adc_mv > caps->max_voltage_dac_mv)
? caps->max_voltage_adc_mv : caps->max_voltage_dac_mv;
case MW_MODE_VOLTAGE_INVERTED:
return caps->max_voltage_adc_mv;
case MW_MODE_POWER:
return caps->max_power_mw;
case MW_MODE_RESISTANCE:
return caps->dvm_input_resistance_ohm * 1000u;
default:
return 0;
}
}
int mw_validate_target(const mw_capabilities *caps, mw_mode mode, uint32_t milli_units,
char *buffer, size_t buffer_size) {
uint32_t limit;
if (buffer && buffer_size > 0) {
buffer[0] = '\0';
}
if (!caps) {
return 0;
}
limit = mw_capability_limit_for_mode(caps, mode);
if (limit == 0 && mode != MW_MODE_CURRENT) {
if (buffer && buffer_size > 0) {
snprintf(buffer, buffer_size, "unknown limit for mode %s", mw_mode_name(mode));
}
return -1;
}
if (milli_units > limit) {
if (buffer && buffer_size > 0) {
snprintf(buffer, buffer_size, "%s target %.3f exceeds device limit %.3f",
mw_mode_name(mode), milli_units / 1000.0, limit / 1000.0);
}
return -1;
}
return 0;
}

252
src/mightywatt_app.c Normal file
View File

@ -0,0 +1,252 @@
#include "mightywatt_app.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct mw_app {
mw_device *dev;
mw_app_state state;
char last_error[256];
};
static void mw_app_set_error(mw_app *app, const char *fmt, ...) {
va_list ap;
if (!app) {
return;
}
va_start(ap, fmt);
vsnprintf(app->last_error, sizeof(app->last_error), fmt, ap);
va_end(ap);
}
static void mw_app_copy_report(mw_app *app, const mw_report *report) {
if (!app || !report) {
return;
}
app->state.last_report = *report;
app->state.report_valid = true;
}
static void mw_app_copy_caps(mw_app *app, const mw_capabilities *caps) {
if (!app || !caps) {
return;
}
app->state.capabilities = *caps;
app->state.capabilities_valid = true;
}
static int mw_app_fail_from_device(mw_app *app, const char *prefix) {
mw_app_set_error(app, "%s: %s", prefix, mw_last_error(app->dev));
return -1;
}
static int mw_app_check_target(mw_app *app, mw_mode mode, uint32_t milli_units) {
char reason[160];
if (mw_validate_target(app->state.capabilities_valid ? &app->state.capabilities : NULL,
mode, milli_units, reason, sizeof(reason)) != 0) {
mw_app_set_error(app, "%s", reason[0] ? reason : "invalid target");
return -1;
}
return 0;
}
static void mw_app_store_target(mw_app *app, mw_mode mode, uint32_t milli_units) {
app->state.target_valid = true;
app->state.target_mode = mode;
app->state.target_milli_units = milli_units;
if (milli_units > 0) {
app->state.restore_target_valid = true;
app->state.restore_target_mode = mode;
app->state.restore_target_milli_units = milli_units;
}
}
int mw_app_open(mw_app **out_app, const char *port_path, int settle_ms) {
mw_app *app;
mw_capabilities caps;
if (!out_app || !port_path) {
return -1;
}
*out_app = NULL;
app = calloc(1, sizeof(*app));
if (!app) {
return -1;
}
if (mw_open(&app->dev, port_path, settle_ms) != 0) {
mw_app_set_error(app, "%s", app->dev ? mw_last_error(app->dev) : "open failed");
mw_close(app->dev);
free(app);
return -1;
}
if (mw_identify(app->dev) != 0) {
mw_app_set_error(app, "identify failed: %s", mw_last_error(app->dev));
mw_close(app->dev);
free(app);
return -1;
}
if (mw_query_capabilities(app->dev, &caps) != 0) {
mw_app_set_error(app, "capabilities failed: %s", mw_last_error(app->dev));
mw_close(app->dev);
free(app);
return -1;
}
mw_app_copy_caps(app, &caps);
*out_app = app;
return 0;
}
void mw_app_close(mw_app *app) {
if (!app) {
return;
}
mw_close(app->dev);
free(app);
}
const char *mw_app_last_error(const mw_app *app) {
return app ? app->last_error : "invalid app";
}
int mw_app_refresh_capabilities(mw_app *app, mw_capabilities *out_caps) {
mw_capabilities caps;
if (!app) {
return -1;
}
if (mw_query_capabilities(app->dev, &caps) != 0) {
return mw_app_fail_from_device(app, "capabilities failed");
}
mw_app_copy_caps(app, &caps);
if (out_caps) {
*out_caps = caps;
}
return 0;
}
int mw_app_get_report(mw_app *app, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_get_report(app->dev, &report) != 0) {
return mw_app_fail_from_device(app, "report failed");
}
mw_app_copy_report(app, &report);
*out_report = report;
return 0;
}
int mw_app_set_target(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_app_check_target(app, mode, milli_units) != 0) {
return -1;
}
if (mw_set(app->dev, mode, milli_units, &report) != 0) {
return mw_app_fail_from_device(app, "set target failed");
}
mw_app_copy_report(app, &report);
mw_app_store_target(app, mode, milli_units);
*out_report = report;
return 0;
}
int mw_app_load_off(mw_app *app, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_set(app->dev, MW_MODE_CURRENT, 0, &report) != 0) {
return mw_app_fail_from_device(app, "load-off failed");
}
mw_app_copy_report(app, &report);
app->state.target_valid = true;
app->state.target_mode = MW_MODE_CURRENT;
app->state.target_milli_units = 0;
*out_report = report;
return 0;
}
int mw_app_load_on(mw_app *app, mw_mode mode, uint32_t milli_units, mw_report *out_report) {
return mw_app_set_target(app, mode, milli_units, out_report);
}
int mw_app_restore_target(mw_app *app, mw_report *out_report) {
if (!app || !out_report) {
return -1;
}
if (!app->state.restore_target_valid) {
mw_app_set_error(app, "no restorable target in current process state");
return -1;
}
return mw_app_set_target(app,
app->state.restore_target_mode,
app->state.restore_target_milli_units,
out_report);
}
int mw_app_safe(mw_app *app, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_app_load_off(app, &report) != 0) {
return -1;
}
if (mw_set_remote(app->dev, false, &report) != 0) {
return mw_app_fail_from_device(app, "safe failed while disabling remote");
}
mw_app_copy_report(app, &report);
*out_report = report;
return 0;
}
int mw_app_set_remote(mw_app *app, bool enable, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_set_remote(app->dev, enable, &report) != 0) {
return mw_app_fail_from_device(app, enable ? "remote on failed" : "remote off failed");
}
mw_app_copy_report(app, &report);
*out_report = report;
return 0;
}
int mw_app_get_series_resistance(mw_app *app, uint16_t *milliohm) {
if (!app || !milliohm) {
return -1;
}
if (mw_get_series_resistance(app->dev, milliohm) != 0) {
return mw_app_fail_from_device(app, "get-series failed");
}
return 0;
}
int mw_app_set_series_resistance(mw_app *app, uint16_t milliohm, mw_report *out_report) {
mw_report report;
if (!app || !out_report) {
return -1;
}
if (mw_set_series_resistance(app->dev, milliohm, &report) != 0) {
return mw_app_fail_from_device(app, "set-series failed");
}
mw_app_copy_report(app, &report);
*out_report = report;
return 0;
}
int mw_app_get_state(const mw_app *app, mw_app_state *out_state) {
if (!app || !out_state) {
return -1;
}
*out_state = app->state;
return 0;
}

723
src/mightywatt_controller.c Normal file
View File

@ -0,0 +1,723 @@
#define _POSIX_C_SOURCE 200809L
#include "mightywatt_controller.h"
#include <errno.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
typedef struct {
mw_controller_command_kind kind;
mw_mode mode;
uint32_t milli_units;
bool enable;
uint16_t series_milliohm;
uint64_t id;
} mw_controller_command;
struct mw_controller {
pthread_t thread;
pthread_mutex_t mutex;
pthread_cond_t cond;
bool thread_started;
bool stop_requested;
mw_controller_config config;
mw_controller_snapshot snapshot;
mw_app *app;
mw_controller_command *queue;
size_t queue_capacity;
size_t queue_head;
size_t queue_tail;
size_t queue_count;
uint64_t next_command_id;
uint64_t next_retry_due_ms;
uint64_t next_poll_due_ms;
};
static uint64_t mw_now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (uint64_t)ts.tv_sec * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000L);
}
static void mw_abs_timespec_from_now(int timeout_ms, struct timespec *out_ts) {
uint64_t now_ms;
if (!out_ts) {
return;
}
if (timeout_ms < 0) {
timeout_ms = 0;
}
now_ms = mw_now_ms() + (uint64_t)timeout_ms;
out_ts->tv_sec = (time_t)(now_ms / 1000ULL);
out_ts->tv_nsec = (long)(now_ms % 1000ULL) * 1000000L;
}
static void mw_snapshot_touch_locked(mw_controller *controller) {
controller->snapshot.snapshot_seq++;
pthread_cond_broadcast(&controller->cond);
}
static void mw_snapshot_set_error_locked(mw_controller *controller, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(controller->snapshot.last_error,
sizeof(controller->snapshot.last_error),
fmt,
ap);
va_end(ap);
}
static void mw_snapshot_set_completed_locked(mw_controller *controller,
const mw_controller_command *cmd,
int result,
const char *error_text) {
controller->snapshot.last_completed_command_id = cmd ? cmd->id : 0;
controller->snapshot.last_completed_kind = cmd ? cmd->kind : MW_CTRL_CMD_NONE;
controller->snapshot.last_completed_result = result;
snprintf(controller->snapshot.last_completed_error,
sizeof(controller->snapshot.last_completed_error),
"%s",
error_text ? error_text : "");
}
static void mw_snapshot_sync_app_state_locked(mw_controller *controller) {
mw_app_state state;
if (!controller || !controller->app) {
controller->snapshot.app_state_valid = false;
return;
}
if (mw_app_get_state(controller->app, &state) == 0) {
controller->snapshot.app_state = state;
controller->snapshot.app_state_valid = true;
}
}
static void mw_disconnect_locked(mw_controller *controller, const char *reason) {
mw_app *app_to_close = controller->app;
bool was_connected = controller->snapshot.connected;
controller->app = NULL;
controller->snapshot.connected = false;
controller->snapshot.connection_state = controller->stop_requested
? MW_CONTROLLER_STOPPED
: MW_CONTROLLER_RECONNECT_WAIT;
if (reason && reason[0] != '\0') {
mw_snapshot_set_error_locked(controller, "%s", reason);
}
if (was_connected) {
controller->snapshot.reconnect_count++;
}
controller->next_retry_due_ms = mw_now_ms() + (uint64_t)controller->config.reconnect_ms;
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
mw_app_close(app_to_close);
pthread_mutex_lock(&controller->mutex);
}
static int mw_queue_push_locked(mw_controller *controller,
const mw_controller_command *command,
uint64_t *out_command_id) {
mw_controller_command stored;
char validation[160];
if (!controller || !command) {
return -1;
}
if (controller->stop_requested || !controller->snapshot.running) {
mw_snapshot_set_error_locked(controller, "controller is stopping or not running");
return -1;
}
if (controller->queue_count >= controller->queue_capacity) {
mw_snapshot_set_error_locked(controller, "command queue is full");
return -1;
}
if (command->kind == MW_CTRL_CMD_SET_TARGET || command->kind == MW_CTRL_CMD_LOAD_ON) {
const mw_capabilities *caps = NULL;
if (controller->snapshot.app_state_valid &&
controller->snapshot.app_state.capabilities_valid) {
caps = &controller->snapshot.app_state.capabilities;
}
if (mw_validate_target(caps,
command->mode,
command->milli_units,
validation,
sizeof(validation)) != 0) {
mw_snapshot_set_error_locked(controller,
"%s",
validation[0] ? validation : "invalid target");
return -1;
}
}
stored = *command;
stored.id = ++controller->next_command_id;
controller->queue[controller->queue_tail] = stored;
controller->queue_tail = (controller->queue_tail + 1U) % controller->queue_capacity;
controller->queue_count++;
controller->snapshot.pending_commands = controller->queue_count;
controller->snapshot.last_queued_command_id = stored.id;
mw_snapshot_touch_locked(controller);
if (out_command_id) {
*out_command_id = stored.id;
}
return 0;
}
static int mw_queue_pop_locked(mw_controller *controller, mw_controller_command *out_command) {
if (!controller || !out_command || controller->queue_count == 0U) {
return -1;
}
*out_command = controller->queue[controller->queue_head];
controller->queue_head = (controller->queue_head + 1U) % controller->queue_capacity;
controller->queue_count--;
controller->snapshot.pending_commands = controller->queue_count;
return 0;
}
static int mw_try_keep_connection(mw_controller *controller) {
mw_report report;
if (!controller || !controller->app) {
return -1;
}
if (mw_app_get_report(controller->app, &report) != 0) {
return -1;
}
return 0;
}
static void mw_handle_connected_failure_locked(mw_controller *controller,
const mw_controller_command *command,
const char *error_text,
bool may_still_be_connected) {
mw_snapshot_set_completed_locked(controller, command, -1, error_text);
controller->snapshot.command_error_count++;
mw_snapshot_set_error_locked(controller, "%s", error_text ? error_text : "operation failed");
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
if (may_still_be_connected && mw_try_keep_connection(controller) == 0) {
pthread_mutex_lock(&controller->mutex);
mw_snapshot_sync_app_state_locked(controller);
mw_snapshot_touch_locked(controller);
return;
}
pthread_mutex_lock(&controller->mutex);
mw_disconnect_locked(controller, error_text ? error_text : "device communication failed");
}
static void mw_execute_connect(mw_controller *controller) {
mw_app *new_app = NULL;
int rc;
char error_buffer[MW_CONTROLLER_ERROR_MAX + 1] = "";
rc = mw_app_open(&new_app,
controller->config.port_path,
controller->config.settle_ms);
if (rc != 0) {
if (new_app) {
snprintf(error_buffer, sizeof(error_buffer), "%s", mw_app_last_error(new_app));
mw_app_close(new_app);
} else {
snprintf(error_buffer, sizeof(error_buffer), "connect failed");
}
pthread_mutex_lock(&controller->mutex);
controller->snapshot.connect_attempts++;
controller->snapshot.connected = false;
controller->snapshot.connection_state = MW_CONTROLLER_RECONNECT_WAIT;
controller->next_retry_due_ms = mw_now_ms() + (uint64_t)controller->config.reconnect_ms;
mw_snapshot_set_error_locked(controller, "%s", error_buffer);
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
return;
}
pthread_mutex_lock(&controller->mutex);
controller->snapshot.connect_attempts++;
controller->app = new_app;
controller->snapshot.connected = true;
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTED;
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
controller->snapshot.last_error[0] = '\0';
mw_snapshot_sync_app_state_locked(controller);
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
}
static void mw_execute_poll(mw_controller *controller) {
mw_report report;
(void)report;
if (mw_app_get_report(controller->app, &report) != 0) {
pthread_mutex_lock(&controller->mutex);
controller->snapshot.poll_error_count++;
mw_disconnect_locked(controller, mw_app_last_error(controller->app));
pthread_mutex_unlock(&controller->mutex);
return;
}
pthread_mutex_lock(&controller->mutex);
controller->snapshot.poll_success_count++;
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
controller->snapshot.last_error[0] = '\0';
mw_snapshot_sync_app_state_locked(controller);
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
}
static void mw_execute_command(mw_controller *controller, const mw_controller_command *command) {
int rc = -1;
mw_report report;
uint16_t series_milliohm = 0;
char error_text[MW_CONTROLLER_ERROR_MAX + 1] = "";
switch (command->kind) {
case MW_CTRL_CMD_SET_TARGET:
rc = mw_app_set_target(controller->app,
command->mode,
command->milli_units,
&report);
break;
case MW_CTRL_CMD_LOAD_ON:
rc = mw_app_load_on(controller->app,
command->mode,
command->milli_units,
&report);
break;
case MW_CTRL_CMD_LOAD_OFF:
rc = mw_app_load_off(controller->app, &report);
break;
case MW_CTRL_CMD_RESTORE_TARGET:
rc = mw_app_restore_target(controller->app, &report);
break;
case MW_CTRL_CMD_SAFE:
rc = mw_app_safe(controller->app, &report);
break;
case MW_CTRL_CMD_SET_REMOTE:
rc = mw_app_set_remote(controller->app, command->enable, &report);
break;
case MW_CTRL_CMD_SET_SERIES_RESISTANCE:
rc = mw_app_set_series_resistance(controller->app,
command->series_milliohm,
&report);
break;
case MW_CTRL_CMD_REFRESH_CAPABILITIES:
rc = mw_app_refresh_capabilities(controller->app, NULL);
break;
case MW_CTRL_CMD_GET_SERIES_RESISTANCE:
rc = mw_app_get_series_resistance(controller->app, &series_milliohm);
break;
default:
snprintf(error_text, sizeof(error_text), "unsupported command kind");
break;
}
if (rc != 0 && error_text[0] == '\0') {
snprintf(error_text, sizeof(error_text), "%s", mw_app_last_error(controller->app));
}
if (rc != 0) {
pthread_mutex_lock(&controller->mutex);
mw_handle_connected_failure_locked(controller,
command,
error_text,
true);
pthread_mutex_unlock(&controller->mutex);
return;
}
pthread_mutex_lock(&controller->mutex);
controller->snapshot.command_success_count++;
controller->snapshot.last_error[0] = '\0';
mw_snapshot_set_completed_locked(controller, command, 0, "");
if (command->kind == MW_CTRL_CMD_GET_SERIES_RESISTANCE) {
controller->snapshot.series_resistance_valid = true;
controller->snapshot.series_resistance_milliohm = series_milliohm;
}
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
mw_snapshot_sync_app_state_locked(controller);
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
}
static void *mw_controller_thread_main(void *arg) {
mw_controller *controller = (mw_controller *)arg;
for (;;) {
bool stop_now = false;
bool do_connect = false;
bool do_poll = false;
mw_controller_command command;
bool have_command = false;
pthread_mutex_lock(&controller->mutex);
for (;;) {
uint64_t now_ms;
struct timespec wake_ts;
int wait_ms;
if (controller->stop_requested) {
stop_now = true;
break;
}
if (!controller->app) {
now_ms = mw_now_ms();
if (now_ms >= controller->next_retry_due_ms) {
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTING;
controller->snapshot.connected = false;
mw_snapshot_touch_locked(controller);
do_connect = true;
break;
}
wait_ms = (int)(controller->next_retry_due_ms - now_ms);
mw_abs_timespec_from_now(wait_ms, &wake_ts);
pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
continue;
}
if (controller->queue_count > 0U) {
if (mw_queue_pop_locked(controller, &command) == 0) {
have_command = true;
break;
}
}
now_ms = mw_now_ms();
if (now_ms >= controller->next_poll_due_ms) {
do_poll = true;
break;
}
wait_ms = (int)(controller->next_poll_due_ms - now_ms);
mw_abs_timespec_from_now(wait_ms, &wake_ts);
pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
}
pthread_mutex_unlock(&controller->mutex);
if (stop_now) {
mw_app *app_to_close = NULL;
pthread_mutex_lock(&controller->mutex);
app_to_close = controller->app;
controller->app = NULL;
controller->snapshot.running = false;
controller->snapshot.connected = false;
controller->snapshot.connection_state = MW_CONTROLLER_STOPPED;
mw_snapshot_touch_locked(controller);
pthread_mutex_unlock(&controller->mutex);
if (app_to_close) {
if (controller->config.safe_on_shutdown) {
mw_report dummy_report;
(void)mw_app_safe(app_to_close, &dummy_report);
}
mw_app_close(app_to_close);
}
return NULL;
}
if (do_connect) {
mw_execute_connect(controller);
} else if (have_command) {
mw_execute_command(controller, &command);
} else if (do_poll) {
mw_execute_poll(controller);
}
}
}
void mw_controller_config_init(mw_controller_config *config) {
if (!config) {
return;
}
memset(config, 0, sizeof(*config));
config->settle_ms = 2200;
config->poll_interval_ms = 500;
config->reconnect_ms = 2000;
config->queue_capacity = 32;
config->safe_on_shutdown = true;
}
int mw_controller_start(mw_controller **out_controller, const mw_controller_config *config) {
mw_controller *controller;
size_t capacity;
if (!out_controller || !config || !config->port_path || config->port_path[0] == '\0') {
return -1;
}
*out_controller = NULL;
controller = calloc(1, sizeof(*controller));
if (!controller) {
return -1;
}
controller->config = *config;
capacity = config->queue_capacity > 0U ? config->queue_capacity : 32U;
controller->queue = calloc(capacity, sizeof(*controller->queue));
if (!controller->queue) {
free(controller);
return -1;
}
controller->queue_capacity = capacity;
controller->snapshot.running = true;
controller->snapshot.connection_state = MW_CONTROLLER_CONNECTING;
snprintf(controller->snapshot.port_path,
sizeof(controller->snapshot.port_path),
"%s",
config->port_path);
controller->next_retry_due_ms = mw_now_ms();
controller->next_poll_due_ms = mw_now_ms() + (uint64_t)controller->config.poll_interval_ms;
if (pthread_mutex_init(&controller->mutex, NULL) != 0) {
free(controller->queue);
free(controller);
return -1;
}
if (pthread_cond_init(&controller->cond, NULL) != 0) {
pthread_mutex_destroy(&controller->mutex);
free(controller->queue);
free(controller);
return -1;
}
if (pthread_create(&controller->thread, NULL, mw_controller_thread_main, controller) != 0) {
pthread_cond_destroy(&controller->cond);
pthread_mutex_destroy(&controller->mutex);
free(controller->queue);
free(controller);
return -1;
}
controller->thread_started = true;
*out_controller = controller;
return 0;
}
void mw_controller_stop(mw_controller *controller) {
if (!controller) {
return;
}
pthread_mutex_lock(&controller->mutex);
controller->stop_requested = true;
pthread_cond_broadcast(&controller->cond);
pthread_mutex_unlock(&controller->mutex);
if (controller->thread_started) {
pthread_join(controller->thread, NULL);
}
pthread_cond_destroy(&controller->cond);
pthread_mutex_destroy(&controller->mutex);
free(controller->queue);
free(controller);
}
int mw_controller_get_snapshot(mw_controller *controller, mw_controller_snapshot *out_snapshot) {
if (!controller || !out_snapshot) {
return -1;
}
pthread_mutex_lock(&controller->mutex);
*out_snapshot = controller->snapshot;
pthread_mutex_unlock(&controller->mutex);
return 0;
}
int mw_controller_wait_for_update(mw_controller *controller,
uint64_t last_seen_snapshot_seq,
int timeout_ms,
uint64_t *out_snapshot_seq) {
int rc = 0;
if (!controller) {
return -1;
}
pthread_mutex_lock(&controller->mutex);
if (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
if (timeout_ms < 0) {
while (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
pthread_cond_wait(&controller->cond, &controller->mutex);
}
} else {
struct timespec wake_ts;
mw_abs_timespec_from_now(timeout_ms, &wake_ts);
while (controller->snapshot.snapshot_seq == last_seen_snapshot_seq) {
rc = pthread_cond_timedwait(&controller->cond, &controller->mutex, &wake_ts);
if (rc == ETIMEDOUT) {
pthread_mutex_unlock(&controller->mutex);
return 1;
}
if (rc != 0) {
pthread_mutex_unlock(&controller->mutex);
return -1;
}
}
}
}
if (out_snapshot_seq) {
*out_snapshot_seq = controller->snapshot.snapshot_seq;
}
pthread_mutex_unlock(&controller->mutex);
return 0;
}
static int mw_controller_enqueue_simple(mw_controller *controller,
mw_controller_command_kind kind,
mw_mode mode,
uint32_t milli_units,
bool enable,
uint16_t series_milliohm,
uint64_t *out_command_id) {
mw_controller_command cmd;
if (!controller) {
return -1;
}
memset(&cmd, 0, sizeof(cmd));
cmd.kind = kind;
cmd.mode = mode;
cmd.milli_units = milli_units;
cmd.enable = enable;
cmd.series_milliohm = series_milliohm;
pthread_mutex_lock(&controller->mutex);
if (mw_queue_push_locked(controller, &cmd, out_command_id) != 0) {
pthread_mutex_unlock(&controller->mutex);
return -1;
}
pthread_cond_broadcast(&controller->cond);
pthread_mutex_unlock(&controller->mutex);
return 0;
}
int mw_controller_queue_set_target(mw_controller *controller,
mw_mode mode,
uint32_t milli_units,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_SET_TARGET,
mode,
milli_units,
false,
0,
out_command_id);
}
int mw_controller_queue_load_on(mw_controller *controller,
mw_mode mode,
uint32_t milli_units,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_LOAD_ON,
mode,
milli_units,
false,
0,
out_command_id);
}
int mw_controller_queue_load_off(mw_controller *controller, uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_LOAD_OFF,
MW_MODE_CURRENT,
0,
false,
0,
out_command_id);
}
int mw_controller_queue_restore_target(mw_controller *controller, uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_RESTORE_TARGET,
MW_MODE_CURRENT,
0,
false,
0,
out_command_id);
}
int mw_controller_queue_safe(mw_controller *controller, uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_SAFE,
MW_MODE_CURRENT,
0,
false,
0,
out_command_id);
}
int mw_controller_queue_set_remote(mw_controller *controller,
bool enable,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_SET_REMOTE,
MW_MODE_CURRENT,
0,
enable,
0,
out_command_id);
}
int mw_controller_queue_set_series_resistance(mw_controller *controller,
uint16_t milliohm,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_SET_SERIES_RESISTANCE,
MW_MODE_CURRENT,
0,
false,
milliohm,
out_command_id);
}
int mw_controller_queue_refresh_capabilities(mw_controller *controller,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_REFRESH_CAPABILITIES,
MW_MODE_CURRENT,
0,
false,
0,
out_command_id);
}
int mw_controller_queue_get_series_resistance(mw_controller *controller,
uint64_t *out_command_id) {
return mw_controller_enqueue_simple(controller,
MW_CTRL_CMD_GET_SERIES_RESISTANCE,
MW_MODE_CURRENT,
0,
false,
0,
out_command_id);
}
const char *mw_controller_connection_state_name(mw_controller_connection_state state) {
switch (state) {
case MW_CONTROLLER_STOPPED: return "stopped";
case MW_CONTROLLER_CONNECTING: return "connecting";
case MW_CONTROLLER_CONNECTED: return "connected";
case MW_CONTROLLER_RECONNECT_WAIT: return "reconnect_wait";
default: return "unknown";
}
}
const char *mw_controller_command_kind_name(mw_controller_command_kind kind) {
switch (kind) {
case MW_CTRL_CMD_NONE: return "none";
case MW_CTRL_CMD_SET_TARGET: return "set_target";
case MW_CTRL_CMD_LOAD_ON: return "load_on";
case MW_CTRL_CMD_LOAD_OFF: return "load_off";
case MW_CTRL_CMD_RESTORE_TARGET: return "restore_target";
case MW_CTRL_CMD_SAFE: return "safe";
case MW_CTRL_CMD_SET_REMOTE: return "set_remote";
case MW_CTRL_CMD_SET_SERIES_RESISTANCE: return "set_series_resistance";
case MW_CTRL_CMD_REFRESH_CAPABILITIES: return "refresh_capabilities";
case MW_CTRL_CMD_GET_SERIES_RESISTANCE: return "get_series_resistance";
default: return "unknown";
}
}

122
src/mightywatt_log.c Normal file
View File

@ -0,0 +1,122 @@
#define _POSIX_C_SOURCE 200809L
#include "mightywatt_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
struct mw_csv_logger {
FILE *fp;
struct timespec t0;
mw_csv_units_mode units_mode;
};
static double mw_elapsed_seconds(const struct timespec *t0) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (double)(now.tv_sec - t0->tv_sec) + (double)(now.tv_nsec - t0->tv_nsec) / 1000000000.0;
}
static void mw_timestamp_iso8601(char *buffer, size_t buffer_size) {
struct timespec now;
struct tm tm_now;
clock_gettime(CLOCK_REALTIME, &now);
gmtime_r(&now.tv_sec, &tm_now);
{
int millis = (int)(now.tv_nsec / 1000000L);
snprintf(buffer,
buffer_size,
"%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
tm_now.tm_year + 1900,
tm_now.tm_mon + 1,
tm_now.tm_mday,
tm_now.tm_hour,
tm_now.tm_min,
tm_now.tm_sec,
millis);
}
}
int mw_csv_logger_open(mw_csv_logger **out_logger, const char *path, mw_csv_units_mode units_mode) {
mw_csv_logger *logger;
if (!out_logger || !path || !*path) {
return -1;
}
*out_logger = NULL;
logger = calloc(1, sizeof(*logger));
if (!logger) {
return -1;
}
logger->fp = fopen(path, "w");
if (!logger->fp) {
free(logger);
return -1;
}
clock_gettime(CLOCK_MONOTONIC, &logger->t0);
logger->units_mode = units_mode;
if (logger->units_mode == MW_CSV_UNITS_RAW) {
fprintf(logger->fp,
"timestamp_utc,elapsed_ms,context,step_index,current_ma,voltage_mv,power_mw,temperature_c,remote,status_bits,status_text\n");
} else {
fprintf(logger->fp,
"timestamp_utc,elapsed_s,context,step_index,current_a,voltage_v,power_w,temperature_c,remote,status_bits,status_text\n");
}
fflush(logger->fp);
*out_logger = logger;
return 0;
}
void mw_csv_logger_close(mw_csv_logger *logger) {
if (!logger) {
return;
}
if (logger->fp) {
fclose(logger->fp);
}
free(logger);
}
int mw_csv_logger_write(mw_csv_logger *logger,
const char *context,
long step_index,
const mw_report *report) {
char ts[96];
char status[128];
if (!logger || !logger->fp || !report) {
return -1;
}
mw_timestamp_iso8601(ts, sizeof(ts));
mw_status_string(report->status, status, sizeof(status));
if (logger->units_mode == MW_CSV_UNITS_RAW) {
fprintf(logger->fp,
"%s,%.0f,%s,%ld,%u,%u,%u,%u,%s,%u,%s\n",
ts,
mw_elapsed_seconds(&logger->t0) * 1000.0,
context ? context : "",
step_index,
(unsigned)report->current_ma,
(unsigned)report->voltage_mv,
(unsigned)mw_report_power_mw(report),
(unsigned)report->temperature_c,
report->remote ? "true" : "false",
(unsigned)report->status,
status);
} else {
fprintf(logger->fp,
"%s,%.3f,%s,%ld,%.3f,%.3f,%.3f,%u,%s,%u,%s\n",
ts,
mw_elapsed_seconds(&logger->t0),
context ? context : "",
step_index,
report->current_ma / 1000.0,
report->voltage_mv / 1000.0,
mw_report_power_mw(report) / 1000.0,
(unsigned)report->temperature_c,
report->remote ? "true" : "false",
(unsigned)report->status,
status);
}
fflush(logger->fp);
return ferror(logger->fp) ? -1 : 0;
}

1459
src/mightywatt_sequence.c Normal file

File diff suppressed because it is too large Load Diff

745
src/mwcli.c Normal file
View File

@ -0,0 +1,745 @@
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "mightywatt_app.h"
#include "mightywatt_log.h"
#include "mightywatt_sequence.h"
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
static volatile sig_atomic_t g_stop_requested = 0;
static void handle_signal(int signo) {
(void)signo;
g_stop_requested = 1;
}
static void usage(FILE *fp) {
fprintf(fp,
"mwcli - MightyWatt Linux CLI\n"
"\n"
"Usage:\n"
" mwcli -d /dev/ttyACM0 [options] <command> [args]\n"
"\n"
"Options:\n"
" -d, --device PATH Serial device\n"
" -s, --settle-ms N Delay after open (default 2200)\n"
" -i, --interval-ms N Poll interval for monitor/hold (default 500)\n"
" -c, --count N Number of samples for monitor/hold\n"
" --sample-period-ms N Override sequence sample period\n"
" --csv PATH Write measurement samples as CSV\n"
" -j, --json JSON stdout for non-streaming commands\n"
" -h, --help Show this help\n"
"\n"
"Commands:\n"
" idn\n"
" caps\n"
" report\n"
" monitor\n"
" set-current <amps>\n"
" set-voltage <volts>\n"
" set-power <watts>\n"
" set-resistance <ohms>\n"
" set-vinv <volts>\n"
" hold <current|voltage|power|resistance|vinv> <value>\n"
" load-on <current|voltage|power|resistance|vinv> <value>\n"
" load-off\n"
" safe\n"
" remote on|off\n"
" get-series\n"
" set-series <ohms>\n"
" run-sequence <sequence.json>\n"
"\n"
"Examples:\n"
" mwcli -d /dev/ttyACM0 caps\n"
" mwcli -d /dev/ttyACM0 --csv out.csv monitor --count 20\n"
" mwcli -d /dev/ttyACM0 hold current 0.500 --interval-ms 500\n"
" mwcli -d /dev/ttyACM0 --csv run.csv run-sequence profile.json\n");
}
static int parse_double_arg(const char *text, double *out) {
char *end = NULL;
double value;
errno = 0;
value = strtod(text, &end);
if (errno != 0 || end == text || *end != '\0') {
return -1;
}
*out = value;
return 0;
}
static int round_to_uint32(double input, uint32_t *out) {
if (input < 0.0 || input > 4294967295.0) {
return -1;
}
*out = (uint32_t)(input + 0.5);
return 0;
}
static int unit_text_to_milli(const char *text, uint32_t *out) {
double value;
if (parse_double_arg(text, &value) != 0) {
return -1;
}
return round_to_uint32(value * 1000.0, out);
}
static int parse_mode_text(const char *text, mw_mode *mode) {
if (!text || !mode) {
return -1;
}
if (strcasecmp(text, "current") == 0 || strcasecmp(text, "CC") == 0) {
*mode = MW_MODE_CURRENT;
} else if (strcasecmp(text, "voltage") == 0 || strcasecmp(text, "CV") == 0) {
*mode = MW_MODE_VOLTAGE;
} else if (strcasecmp(text, "power") == 0 || strcasecmp(text, "CP") == 0) {
*mode = MW_MODE_POWER;
} else if (strcasecmp(text, "resistance") == 0 || strcasecmp(text, "CR") == 0) {
*mode = MW_MODE_RESISTANCE;
} else if (strcasecmp(text, "vinv") == 0 || strcasecmp(text, "voltage_inverted") == 0) {
*mode = MW_MODE_VOLTAGE_INVERTED;
} else {
return -1;
}
return 0;
}
static void json_print_escaped(const char *text) {
const unsigned char *p = (const unsigned char *)text;
putchar('"');
while (p && *p) {
switch (*p) {
case '"': fputs("\\\"", stdout); break;
case '\\': fputs("\\\\", stdout); break;
case '\b': fputs("\\b", stdout); break;
case '\f': fputs("\\f", stdout); break;
case '\n': fputs("\\n", stdout); break;
case '\r': fputs("\\r", stdout); break;
case '\t': fputs("\\t", stdout); break;
default:
if (*p < 0x20u) {
printf("\\u%04x", (unsigned)*p);
} else {
putchar((int)*p);
}
}
++p;
}
putchar('"');
}
static void print_report_text(const mw_report *r) {
char status[128];
mw_status_string(r->status, status, sizeof(status));
printf("current=%.3f A\n", r->current_ma / 1000.0);
printf("voltage=%.3f V\n", r->voltage_mv / 1000.0);
printf("power=%.3f W\n", mw_report_power_mw(r) / 1000.0);
printf("temperature=%u C\n", (unsigned)r->temperature_c);
printf("remote=%s\n", r->remote ? "on" : "off");
printf("status=%s\n", status);
}
static void print_report_json(const mw_report *r, const char *command) {
char status[128];
mw_status_string(r->status, status, sizeof(status));
printf("{");
printf("\"ok\":true,");
printf("\"command\":"); json_print_escaped(command);
printf(",\"report\":{");
printf("\"current_a\":%.3f,", r->current_ma / 1000.0);
printf("\"voltage_v\":%.3f,", r->voltage_mv / 1000.0);
printf("\"power_w\":%.3f,", mw_report_power_mw(r) / 1000.0);
printf("\"temperature_c\":%u,", (unsigned)r->temperature_c);
printf("\"remote\":%s,", r->remote ? "true" : "false");
printf("\"status_bits\":%u,", (unsigned)r->status);
printf("\"status_text\":"); json_print_escaped(status);
printf("}}\n");
}
static void print_caps_text(const mw_capabilities *c) {
printf("firmware=%s\n", c->firmware_version);
printf("board_revision=%s\n", c->board_revision);
printf("max_current_dac=%.3f A\n", c->max_current_dac_ma / 1000.0);
printf("max_current_adc=%.3f A\n", c->max_current_adc_ma / 1000.0);
printf("max_voltage_dac=%.3f V\n", c->max_voltage_dac_mv / 1000.0);
printf("max_voltage_adc=%.3f V\n", c->max_voltage_adc_mv / 1000.0);
printf("max_power=%.3f W\n", c->max_power_mw / 1000.0);
printf("dvm_input_resistance=%u ohm\n", c->dvm_input_resistance_ohm);
printf("temperature_threshold=%u C\n", c->temperature_threshold_c);
}
static void print_caps_json(const mw_capabilities *c) {
printf("{");
printf("\"ok\":true,");
printf("\"command\":\"caps\",");
printf("\"capabilities\":{");
printf("\"firmware\":"); json_print_escaped(c->firmware_version);
printf(",\"board_revision\":"); json_print_escaped(c->board_revision);
printf(",\"max_current_dac_a\":%.3f", c->max_current_dac_ma / 1000.0);
printf(",\"max_current_adc_a\":%.3f", c->max_current_adc_ma / 1000.0);
printf(",\"max_voltage_dac_v\":%.3f", c->max_voltage_dac_mv / 1000.0);
printf(",\"max_voltage_adc_v\":%.3f", c->max_voltage_adc_mv / 1000.0);
printf(",\"max_power_w\":%.3f", c->max_power_mw / 1000.0);
printf(",\"dvm_input_resistance_ohm\":%u", c->dvm_input_resistance_ohm);
printf(",\"temperature_threshold_c\":%u", c->temperature_threshold_c);
printf("}}\n");
}
static void print_error_json(const char *command, const char *error_text) {
printf("{");
printf("\"ok\":false,");
printf("\"command\":"); json_print_escaped(command ? command : "unknown");
printf(",\"error\":"); json_print_escaped(error_text ? error_text : "unknown error");
printf("}\n");
}
static int log_report_if_requested(mw_csv_logger *logger, const char *context, const mw_report *report) {
if (!logger || !report) {
return 0;
}
return mw_csv_logger_write(logger, context, -1, report);
}
static int stream_reports(mw_app *app,
int interval_ms,
long count,
int json_output,
const char *command_name,
mw_csv_logger *logger) {
long i = 0;
const int keepalive_ms = 250;
const int maintain_output = (command_name && strcmp(command_name, "hold") == 0);
int effective_interval_ms = interval_ms > 0 ? interval_ms : 500;
int poll_ms = maintain_output && effective_interval_ms > keepalive_ms ? keepalive_ms : effective_interval_ms;
struct timespec t0;
double next_print_s = 0.0;
clock_gettime(CLOCK_MONOTONIC, &t0);
while (!g_stop_requested && (count < 0 || i < count)) {
mw_report r;
struct timespec now_mono;
double elapsed_s;
int should_print;
time_t now = time(NULL);
struct tm tm_now;
char ts[32];
if (mw_app_get_report(app, &r) != 0) {
return -1;
}
clock_gettime(CLOCK_MONOTONIC, &now_mono);
elapsed_s = (double)(now_mono.tv_sec - t0.tv_sec) + (double)(now_mono.tv_nsec - t0.tv_nsec) / 1000000000.0;
should_print = (!maintain_output) || (elapsed_s + 1e-9 >= next_print_s);
if (should_print) {
if (logger && mw_csv_logger_write(logger, command_name ? command_name : "stream", i, &r) != 0) {
return -1;
}
localtime_r(&now, &tm_now);
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm_now);
if (json_output) {
char status[128];
mw_status_string(r.status, status, sizeof(status));
printf("{");
printf("\"ok\":true,");
printf("\"command\":"); json_print_escaped(command_name ? command_name : "stream");
printf(",\"timestamp\":"); json_print_escaped(ts);
printf(",\"report\":{");
printf("\"current_a\":%.3f,", r.current_ma / 1000.0);
printf("\"voltage_v\":%.3f,", r.voltage_mv / 1000.0);
printf("\"power_w\":%.3f,", mw_report_power_mw(&r) / 1000.0);
printf("\"temperature_c\":%u,", (unsigned)r.temperature_c);
printf("\"remote\":%s,", r.remote ? "true" : "false");
printf("\"status_bits\":%u,", (unsigned)r.status);
printf("\"status_text\":"); json_print_escaped(status);
printf("}}\n");
} else {
printf("[%s] I=%.3f A V=%.3f V P=%.3f W T=%u C remote=%s status=0x%02X\n",
ts,
r.current_ma / 1000.0,
r.voltage_mv / 1000.0,
mw_report_power_mw(&r) / 1000.0,
(unsigned)r.temperature_c,
r.remote ? "on" : "off",
(unsigned)r.status);
}
fflush(stdout);
++i;
next_print_s += effective_interval_ms / 1000.0;
}
if (!g_stop_requested && (count < 0 || i < count)) {
struct timespec ts_sleep;
ts_sleep.tv_sec = poll_ms / 1000;
ts_sleep.tv_nsec = (long)(poll_ms % 1000) * 1000000L;
nanosleep(&ts_sleep, NULL);
}
}
return 0;
}
int main(int argc, char **argv) {
mw_app *app = NULL;
mw_capabilities caps;
const char *device = NULL;
const char *csv_path = NULL;
mw_csv_units_mode csv_units_mode = MW_CSV_UNITS_ENGINEERING;
int settle_ms = 2200;
int interval_ms = 500;
int sequence_sample_period_ms = 0;
long count = -1;
int json_output = 0;
int opt;
int long_index = 0;
int rc = 1;
const char *command_for_error = NULL;
mw_csv_logger *logger = NULL;
static const struct option options[] = {
{"device", required_argument, 0, 'd'},
{"settle-ms", required_argument, 0, 's'},
{"interval-ms", required_argument, 0, 'i'},
{"count", required_argument, 0, 'c'},
{"csv", required_argument, 0, 1000},
{"sample-period-ms", required_argument, 0, 1001},
{"csv-raw", no_argument, 0, 1002},
{"json", no_argument, 0, 'j'},
{"help", no_argument, 0, 'h'},
{0, 0, 0, 0}
};
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
while ((opt = getopt_long(argc, argv, "d:s:i:c:jh", options, &long_index)) != -1) {
switch (opt) {
case 'd': device = optarg; break;
case 's': settle_ms = atoi(optarg); break;
case 'i': interval_ms = atoi(optarg); break;
case 'c': count = atol(optarg); break;
case 'j': json_output = 1; break;
case 'h': usage(stdout); return 0;
case 1000: csv_path = optarg; break;
case 1001: sequence_sample_period_ms = atoi(optarg); break;
case 1002: csv_units_mode = MW_CSV_UNITS_RAW; break;
default: usage(stderr); return 2;
}
}
if (!device || optind >= argc) {
usage(stderr);
return 2;
}
command_for_error = argv[optind];
if (mw_app_open(&app, device, settle_ms) != 0) {
if (json_output) {
print_error_json(command_for_error, app ? mw_app_last_error(app) : strerror(errno));
} else {
fprintf(stderr, "open failed: %s\n", app ? mw_app_last_error(app) : strerror(errno));
}
mw_app_close(app);
return 1;
}
if (csv_path && *csv_path) {
if (mw_csv_logger_open(&logger, csv_path, csv_units_mode) != 0) {
if (json_output) {
print_error_json(command_for_error, "failed to open CSV output file");
} else {
fprintf(stderr, "failed to open CSV output file: %s\n", csv_path);
}
goto done;
}
}
if (mw_app_refresh_capabilities(app, &caps) != 0) {
if (json_output) {
print_error_json(command_for_error, mw_app_last_error(app));
} else {
fprintf(stderr, "caps refresh failed: %s\n", mw_app_last_error(app));
}
goto done;
}
{
const char *cmd = argv[optind++];
command_for_error = cmd;
if (strcmp(cmd, "idn") == 0) {
if (json_output) {
printf("{\"ok\":true,\"command\":\"idn\",\"identity\":\"MightyWatt\"}\n");
} else {
printf("MightyWatt\n");
}
rc = 0;
} else if (strcmp(cmd, "caps") == 0) {
if (json_output) {
print_caps_json(&caps);
} else {
print_caps_text(&caps);
}
rc = 0;
} else if (strcmp(cmd, "report") == 0) {
mw_report report;
if (mw_app_get_report(app, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, "report", &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "monitor") == 0) {
if (stream_reports(app, interval_ms, count, json_output, "monitor", logger) != 0) {
goto fail;
}
rc = 0;
} else if (strcmp(cmd, "set-current") == 0 || strcmp(cmd, "set-voltage") == 0 ||
strcmp(cmd, "set-power") == 0 || strcmp(cmd, "set-resistance") == 0 ||
strcmp(cmd, "set-vinv") == 0) {
mw_mode mode;
uint32_t milli;
mw_report report;
if (optind >= argc) {
if (json_output) {
print_error_json(cmd, "command needs a value");
} else {
fprintf(stderr, "%s needs a value\n", cmd);
}
goto done;
}
mode = strcmp(cmd, "set-current") == 0 ? MW_MODE_CURRENT :
strcmp(cmd, "set-voltage") == 0 ? MW_MODE_VOLTAGE :
strcmp(cmd, "set-power") == 0 ? MW_MODE_POWER :
strcmp(cmd, "set-resistance") == 0 ? MW_MODE_RESISTANCE : MW_MODE_VOLTAGE_INVERTED;
if (unit_text_to_milli(argv[optind], &milli) != 0) {
if (json_output) {
print_error_json(cmd, "invalid numeric value");
} else {
fprintf(stderr, "invalid numeric value: %s\n", argv[optind]);
}
goto done;
}
if (mw_app_set_target(app, mode, milli, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "hold") == 0) {
mw_mode mode;
uint32_t milli;
mw_report report;
if (optind + 1 >= argc) {
if (json_output) {
print_error_json(cmd, "hold needs <mode> <value>");
} else {
fprintf(stderr, "hold needs <mode> <value>\n");
}
goto done;
}
if (parse_mode_text(argv[optind], &mode) != 0) {
if (json_output) {
print_error_json(cmd, "invalid mode for hold");
} else {
fprintf(stderr, "invalid mode for hold: %s\n", argv[optind]);
}
goto done;
}
if (unit_text_to_milli(argv[optind + 1], &milli) != 0) {
if (json_output) {
print_error_json(cmd, "invalid numeric value");
} else {
fprintf(stderr, "invalid numeric value: %s\n", argv[optind + 1]);
}
goto done;
}
if (mw_app_load_on(app, mode, milli, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, "hold_start", &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
if (stream_reports(app, interval_ms, count, json_output, cmd, logger) != 0) {
goto fail;
}
rc = 0;
} else if (strcmp(cmd, "load-on") == 0) {
mw_mode mode;
uint32_t milli;
mw_report report;
if (optind + 1 >= argc) {
if (json_output) {
print_error_json(cmd, "load-on needs <mode> <value>");
} else {
fprintf(stderr, "load-on needs <mode> <value>\n");
}
goto done;
}
if (parse_mode_text(argv[optind], &mode) != 0) {
if (json_output) {
print_error_json(cmd, "invalid mode for load-on");
} else {
fprintf(stderr, "invalid mode for load-on: %s\n", argv[optind]);
}
goto done;
}
if (unit_text_to_milli(argv[optind + 1], &milli) != 0) {
if (json_output) {
print_error_json(cmd, "invalid numeric value");
} else {
fprintf(stderr, "invalid numeric value: %s\n", argv[optind + 1]);
}
goto done;
}
if (mw_app_load_on(app, mode, milli, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "load-off") == 0) {
mw_report report;
if (mw_app_load_off(app, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "safe") == 0) {
mw_report report;
if (mw_app_safe(app, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "remote") == 0) {
mw_report report;
if (optind >= argc) {
if (json_output) {
print_error_json(cmd, "remote needs on or off");
} else {
fprintf(stderr, "remote needs on or off\n");
}
goto done;
}
if (strcasecmp(argv[optind], "on") == 0) {
if (mw_app_set_remote(app, 1, &report) != 0) {
goto fail;
}
} else if (strcasecmp(argv[optind], "off") == 0) {
if (mw_app_set_remote(app, 0, &report) != 0) {
goto fail;
}
} else {
if (json_output) {
print_error_json(cmd, "remote expects on or off");
} else {
fprintf(stderr, "remote expects on or off\n");
}
goto done;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "get-series") == 0) {
uint16_t mohm;
if (mw_app_get_series_resistance(app, &mohm) != 0) {
goto fail;
}
if (json_output) {
printf("{\"ok\":true,\"command\":\"get-series\",\"series_resistance_ohm\":%.3f}\n", mohm / 1000.0);
} else {
printf("series_resistance=%.3f ohm\n", mohm / 1000.0);
}
rc = 0;
} else if (strcmp(cmd, "set-series") == 0) {
mw_report report;
uint32_t mohm;
if (optind >= argc) {
if (json_output) {
print_error_json(cmd, "set-series needs an ohm value");
} else {
fprintf(stderr, "set-series needs an ohm value\n");
}
goto done;
}
if (unit_text_to_milli(argv[optind], &mohm) != 0 || mohm > 65535u) {
if (json_output) {
print_error_json(cmd, "invalid series resistance value");
} else {
fprintf(stderr, "invalid series resistance value: %s\n", argv[optind]);
}
goto done;
}
if (mw_app_set_series_resistance(app, (uint16_t)mohm, &report) != 0) {
goto fail;
}
if (log_report_if_requested(logger, cmd, &report) != 0) {
if (json_output) {
print_error_json(cmd, "failed to write CSV output");
} else {
fprintf(stderr, "failed to write CSV output\n");
}
goto done;
}
if (json_output) {
print_report_json(&report, cmd);
} else {
print_report_text(&report);
}
rc = 0;
} else if (strcmp(cmd, "run-sequence") == 0) {
mw_sequence_result result;
mw_sequence_run_options opts;
char error_text[256];
if (optind >= argc) {
if (json_output) {
print_error_json(cmd, "run-sequence needs a JSON file path");
} else {
fprintf(stderr, "run-sequence needs a JSON file path\n");
}
goto done;
}
memset(&opts, 0, sizeof(opts));
opts.csv_path = csv_path;
opts.sample_period_ms_override = sequence_sample_period_ms;
opts.safe_on_abort = true;
if (mw_sequence_run_file(app, argv[optind], &opts, &result, error_text, sizeof(error_text)) != 0) {
if (json_output) {
print_error_json(cmd, error_text[0] ? error_text : "sequence failed");
} else {
fprintf(stderr, "%s\n", error_text[0] ? error_text : "sequence failed");
}
goto done;
}
if (json_output) {
printf("{");
printf("\"ok\":true,\"command\":\"run-sequence\",");
printf("\"name\":"); json_print_escaped(result.name[0] ? result.name : "sequence");
printf(",\"sample_period_ms\":%d", result.sample_period_ms);
printf(",\"steps_total\":%lu", (unsigned long)result.steps_total);
printf(",\"steps_completed\":%lu", (unsigned long)result.steps_completed);
printf(",\"samples_written\":%lu", (unsigned long)result.samples_written);
if (result.last_report_valid) {
printf(",\"last_report\":{");
printf("\"current_a\":%.3f,", result.last_report.current_ma / 1000.0);
printf("\"voltage_v\":%.3f,", result.last_report.voltage_mv / 1000.0);
printf("\"power_w\":%.3f", mw_report_power_mw(&result.last_report) / 1000.0);
printf("}");
}
printf("}\n");
} else {
printf("sequence=%s\n", result.name[0] ? result.name : "sequence");
printf("sample_period_ms=%d\n", result.sample_period_ms);
printf("steps_completed=%lu/%lu\n", (unsigned long)result.steps_completed, (unsigned long)result.steps_total);
printf("samples_written=%lu\n", (unsigned long)result.samples_written);
if (result.last_report_valid) {
print_report_text(&result.last_report);
}
}
rc = 0;
} else {
if (json_output) {
print_error_json(cmd, "unknown command");
} else {
fprintf(stderr, "unknown command: %s\n", cmd);
usage(stderr);
}
goto done;
}
}
goto done;
fail:
if (json_output) {
print_error_json(command_for_error, mw_app_last_error(app));
} else {
fprintf(stderr, "%s\n", mw_app_last_error(app));
}
done:
mw_csv_logger_close(logger);
mw_app_close(app);
return rc;
}