From: ettom Date: Tue, 24 Oct 2023 19:43:06 +0000 (+0300) Subject: gwinstek-psp: Initial implementation X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=e938e2d82925d0a1e7df14d7b69dc8e006afbca5;p=libsigrok.git gwinstek-psp: Initial implementation --- diff --git a/src/hardware/gwinstek-psp/api.c b/src/hardware/gwinstek-psp/api.c index c3fa623c..f2773758 100644 --- a/src/hardware/gwinstek-psp/api.c +++ b/src/hardware/gwinstek-psp/api.c @@ -17,125 +17,334 @@ * along with this program. If not, see . */ +#include #include #include "protocol.h" static struct sr_dev_driver gwinstek_psp_driver_info; -static GSList *scan(struct sr_dev_driver *di, GSList *options) +static const uint32_t scanopts[] = { + SR_CONF_CONN, + SR_CONF_SERIALCOMM, + SR_CONF_FORCE_DETECT, +}; + +static const uint32_t drvopts[] = { + SR_CONF_POWER_SUPPLY, +}; + +static const uint32_t devopts[] = { + SR_CONF_CONN | SR_CONF_GET, + SR_CONF_CONTINUOUS, + SR_CONF_LIMIT_SAMPLES | SR_CONF_GET | SR_CONF_SET, + SR_CONF_LIMIT_MSEC | SR_CONF_GET | SR_CONF_SET, + SR_CONF_VOLTAGE | SR_CONF_GET, + SR_CONF_VOLTAGE_TARGET | SR_CONF_GET | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_CURRENT | SR_CONF_GET, + SR_CONF_CURRENT_LIMIT | SR_CONF_GET | SR_CONF_SET | SR_CONF_LIST, + SR_CONF_OVER_TEMPERATURE_PROTECTION_ACTIVE | SR_CONF_GET, + SR_CONF_ENABLED | SR_CONF_GET | SR_CONF_SET, +}; + +/* Voltage and current ranges. Values are: Min, max, step. */ +static const double volts_20[] = { 0, 20, 0.01 }; +static const double volts_40[] = { 0, 40, 0.01 }; +static const double volts_60[] = { 0, 60, 0.01 }; +static const double amps_3_5[] = { 0, 3.5, 0.01 }; +static const double amps_5[] = { 0, 5, 0.01 }; +static const double amps_10[] = { 0, 10, 0.01 }; + +static const struct gwinstek_psp_model models[] = { + { "GW Instek", "PSP-603", volts_60, amps_3_5 }, + { "GW Instek", "PSP-405", volts_40, amps_5 }, + { "GW Instek", "PSP-2010", volts_20, amps_10 } +}; + +static const struct gwinstek_psp_model *model_lookup(const char *id_text) { - struct drv_context *drvc; - GSList *devices; + size_t idx; + const struct gwinstek_psp_model *check; - (void)options; + if (!id_text || !*id_text) + return NULL; - devices = NULL; - drvc = di->context; - drvc->instances = NULL; + for (idx = 0; idx < ARRAY_SIZE(models); idx++) { + check = &models[idx]; + if (!check->name || !check->name[0]) + continue; + if (g_ascii_strncasecmp(id_text, check->name, strlen(check->name)) != 0) + continue; - /* TODO: scan for devices, either based on a SR_CONF_CONN option - * or on a USB scan. */ + return check; + } + sr_dbg("Could not find a matching model for: [%s].", id_text); - return devices; + return NULL; } -static int dev_open(struct sr_dev_inst *sdi) +static GSList *scan(struct sr_dev_driver *di, GSList *options) { - (void)sdi; + struct sr_config *src; + struct sr_dev_inst *sdi; + struct dev_context *devc; + struct sr_serial_dev_inst *serial; + const char *conn, *serialcomm; + const char *force_detect; + const struct gwinstek_psp_model *model; + GSList *l; + + conn = NULL; + serialcomm = NULL; + force_detect = NULL; + + for (l = options; l; l = l->next) { + src = l->data; + switch (src->key) { + case SR_CONF_CONN: + conn = g_variant_get_string(src->data, NULL); + break; + case SR_CONF_SERIALCOMM: + serialcomm = g_variant_get_string(src->data, NULL); + break; + case SR_CONF_FORCE_DETECT: + force_detect = g_variant_get_string(src->data, NULL); + break; + default: + sr_err("Unknown option %d, skipping.", src->key); + break; + } + } - /* TODO: get handle from sdi->conn and open it. */ + if (!conn) + return NULL; + if (!serialcomm) + serialcomm = "2400/8n1"; + if (!force_detect) { + sr_err("The gwinstek-psp driver requires the force_detect parameter"); + return NULL; + } - return SR_OK; + model = model_lookup(force_detect); + + if (!model) { + sr_err("Unsupported model ID '%s', aborting.", force_detect); + return NULL; + } + + serial = sr_serial_dev_inst_new(conn, serialcomm); + if (serial_open(serial, SERIAL_RDWR) != SR_OK) + return NULL; + + sdi = g_malloc0(sizeof(*sdi)); + sdi->status = SR_ST_INACTIVE; + sdi->vendor = g_strdup(model->vendor); + sdi->model = g_strdup(model->name); + sdi->inst_type = SR_INST_SERIAL; + sdi->conn = serial; + sdi->connection_id = g_strdup(conn); + + sr_channel_new(sdi, 0, SR_CHANNEL_ANALOG, TRUE, "V"); + sr_channel_new(sdi, 1, SR_CHANNEL_ANALOG, TRUE, "I"); + + devc = g_malloc0(sizeof(*devc)); + sr_sw_limits_init(&devc->limits); + g_mutex_init(&devc->rw_mutex); + devc->model = model; + devc->msg_terminator_len = 2; + sdi->priv = devc; + + /* Get current status of device. */ + if (gwinstek_psp_get_all_values(serial, devc) < 0) + goto exit_err; + + if (gwinstek_psp_check_terminator(serial, devc) < 0) + goto exit_err; + + if (gwinstek_psp_get_initial_voltage_target(devc) < 0) + goto exit_err; + + serial_close(serial); + + return std_scan_complete(di, g_slist_append(NULL, sdi)); + +exit_err: + sr_dev_inst_free(sdi); + g_free(devc); + sr_dbg("Scan failed."); + + return NULL; } static int dev_close(struct sr_dev_inst *sdi) { - (void)sdi; + struct dev_context *devc; - /* TODO: get handle from sdi->conn and close it. */ + devc = (sdi) ? sdi->priv : NULL; + if (devc) { + serial_flush(sdi->conn); + g_mutex_clear(&devc->rw_mutex); + } - return SR_OK; + return std_serial_dev_close(sdi); } static int config_get(uint32_t key, GVariant **data, const struct sr_dev_inst *sdi, const struct sr_channel_group *cg) { - int ret; + struct dev_context *devc; - (void)sdi; - (void)data; (void)cg; - ret = SR_OK; + if (!sdi || !data) + return SR_ERR_ARG; + + devc = sdi->priv; + + if (key != SR_CONF_CONN && gwinstek_psp_get_all_values(sdi->conn, devc) < 0) + return SR_ERR; + switch (key) { - /* TODO */ + case SR_CONF_CONN: + *data = g_variant_new_string(sdi->connection_id); + break; + case SR_CONF_VOLTAGE: + *data = g_variant_new_double(devc->voltage_or_0); + break; + case SR_CONF_VOLTAGE_TARGET: + *data = g_variant_new_double(devc->voltage_target); + break; + case SR_CONF_CURRENT: + *data = g_variant_new_double(devc->current); + break; + case SR_CONF_CURRENT_LIMIT: + *data = g_variant_new_double(devc->current_limit); + break; + case SR_CONF_ENABLED: + *data = g_variant_new_boolean(devc->output_enabled); + break; + case SR_CONF_OVER_TEMPERATURE_PROTECTION_ACTIVE: + *data = g_variant_new_boolean(devc->otp_active); + break; default: return SR_ERR_NA; } - return ret; + return SR_OK; } static int config_set(uint32_t key, GVariant *data, const struct sr_dev_inst *sdi, const struct sr_channel_group *cg) { - int ret; + struct dev_context *devc; + int voltage_limit; + double dval; + gboolean bval; + char msg[20]; - (void)sdi; - (void)data; (void)cg; - ret = SR_OK; + devc = sdi->priv; + switch (key) { - /* TODO */ + case SR_CONF_LIMIT_MSEC: + case SR_CONF_LIMIT_SAMPLES: + return sr_sw_limits_config_set(&devc->limits, key, data); + case SR_CONF_VOLTAGE_TARGET: + dval = g_variant_get_double(data); + if (dval < devc->model->voltage[0] || dval > devc->model->voltage[1]) + return SR_ERR_ARG; + + /* Set voltage output limit to the next positive integer. No need + * to check against limits, device handles overflow correctly. */ + voltage_limit = (int) ceil(dval); + if (devc->voltage_limit == voltage_limit) { + sr_dbg("Correct limit (%dV) already set", voltage_limit); + } else { + sr_snprintf_ascii(msg, sizeof(msg), "SU %d\r\n", voltage_limit); + if (gwinstek_psp_send_cmd(sdi->conn, devc, msg, TRUE) < 0) + return SR_ERR; + } + + /* Set voltage output level */ + sr_snprintf_ascii(msg, sizeof(msg), "SV %05.2f\r\n", dval); + if (gwinstek_psp_send_cmd(sdi->conn, devc, msg, TRUE) < 0) + return SR_ERR; + devc->set_voltage_target = dval; + devc->set_voltage_target_updated = g_get_monotonic_time(); + break; + case SR_CONF_CURRENT_LIMIT: + dval = g_variant_get_double(data); + if (dval < devc->model->current[0] || dval > devc->model->current[1]) + return SR_ERR_ARG; + + sr_snprintf_ascii(msg, sizeof(msg), "SI %04.2f\r\n", dval); + if (gwinstek_psp_send_cmd(sdi->conn, devc, msg, TRUE) < 0) + return SR_ERR; + break; + case SR_CONF_ENABLED: + bval = g_variant_get_boolean(data); + /* Set always so it is possible turn off with sigrok-cli. */ + if (gwinstek_psp_send_cmd(sdi->conn, devc, + bval ? "KOE\r\n" : "KOD\r\n", TRUE) < 0) + return SR_ERR; + break; default: - ret = SR_ERR_NA; + return SR_ERR_NA; } - return ret; + return SR_OK; } static int config_list(uint32_t key, GVariant **data, const struct sr_dev_inst *sdi, const struct sr_channel_group *cg) { - int ret; + struct dev_context *devc; - (void)sdi; - (void)data; - (void)cg; + devc = (sdi) ? sdi->priv : NULL; - ret = SR_OK; switch (key) { - /* TODO */ + case SR_CONF_SCAN_OPTIONS: + case SR_CONF_DEVICE_OPTIONS: + return STD_CONFIG_LIST(key, data, sdi, cg, scanopts, drvopts, devopts); + case SR_CONF_VOLTAGE_TARGET: + if (!devc || !devc->model) + return SR_ERR_ARG; + *data = std_gvar_min_max_step_array(devc->model->voltage); + break; + case SR_CONF_CURRENT_LIMIT: + if (!devc || !devc->model) + return SR_ERR_ARG; + *data = std_gvar_min_max_step_array(devc->model->current); + break; default: return SR_ERR_NA; } - return ret; + return SR_OK; } static int dev_acquisition_start(const struct sr_dev_inst *sdi) { - /* TODO: configure hardware, reset acquisition state, set up - * callbacks and send header packet. */ + struct dev_context *devc; + struct sr_serial_dev_inst *serial; - (void)sdi; + devc = sdi->priv; - return SR_OK; -} - -static int dev_acquisition_stop(struct sr_dev_inst *sdi) -{ - /* TODO: stop acquisition. */ + sr_sw_limits_acquisition_start(&devc->limits); + std_session_send_df_header(sdi); - (void)sdi; + devc->next_req_time = 0; + serial = sdi->conn; + serial_source_add(sdi->session, serial, G_IO_IN, + GWINSTEK_PSP_PROCESSING_TIME_MS, + gwinstek_psp_receive_data, (void *)sdi); return SR_OK; } static struct sr_dev_driver gwinstek_psp_driver_info = { .name = "gwinstek-psp", - .longname = "GW Instek PSP", + .longname = "GW Instek PSP series", .api_version = 1, .init = std_init, .cleanup = std_cleanup, @@ -145,10 +354,10 @@ static struct sr_dev_driver gwinstek_psp_driver_info = { .config_get = config_get, .config_set = config_set, .config_list = config_list, - .dev_open = dev_open, + .dev_open = std_serial_dev_open, .dev_close = dev_close, .dev_acquisition_start = dev_acquisition_start, - .dev_acquisition_stop = dev_acquisition_stop, + .dev_acquisition_stop = std_serial_dev_acquisition_stop, .context = NULL, }; SR_REGISTER_DEV_DRIVER(gwinstek_psp_driver_info); diff --git a/src/hardware/gwinstek-psp/protocol.c b/src/hardware/gwinstek-psp/protocol.c index 90347869..01ce79c7 100644 --- a/src/hardware/gwinstek-psp/protocol.c +++ b/src/hardware/gwinstek-psp/protocol.c @@ -17,15 +17,189 @@ * along with this program. If not, see . */ -#include #include "protocol.h" +#include +#include + +#define CMD_ALL_QUERY "L\r\n" +#define RESPONSE_ALL_QUERY_LEN 37 + +static void give_device_time_to_process(struct dev_context *devc) +{ + int64_t sleeping_time; + + if (!devc->next_req_time) + return; + + sleeping_time = devc->next_req_time - g_get_monotonic_time(); + if (sleeping_time > 0) { + g_usleep(sleeping_time); + sr_spew("Sleeping %" PRIi64 " us for processing", sleeping_time); + } +} + +SR_PRIV int gwinstek_psp_send_cmd(struct sr_serial_dev_inst *serial, + struct dev_context *devc, const char *cmd, gboolean lock) +{ + int ret; + + if (lock) + g_mutex_lock(&devc->rw_mutex); + + give_device_time_to_process(devc); + + sr_dbg("Sending '%s'.", cmd); + if ((ret = serial_write_blocking(serial, cmd, strlen(cmd), 0)) < 0) { + sr_err("Error sending command: %d.", ret); + } + + devc->next_req_time = + g_get_monotonic_time() + GWINSTEK_PSP_PROCESSING_TIME_MS * 1000; + + if (lock) + g_mutex_unlock(&devc->rw_mutex); + + return ret; +} + +/* + * Check for extra LF or CRLF (depends on whether device is in URPSP1 or URPSP2 mode. + * Must be called right after calling gwinstek_psp_get_all_values. + */ +SR_PRIV int gwinstek_psp_check_terminator(struct sr_serial_dev_inst *serial, + struct dev_context *devc) +{ + int bytes_left, ret; + + g_mutex_lock(&devc->rw_mutex); + /* Sleep for a while to be extra sure the device has sent everything */ + g_usleep(20 * 1000); + + bytes_left = serial_has_receive_data(serial); + sr_dbg("%d bytes left in buffer", bytes_left); + + if (bytes_left == 0) { + /* 2, must already be set if we got here */ + sr_dbg("Device is in URPSP2 mode, terminator is CRLF"); + } else if (bytes_left == 1) { + devc->msg_terminator_len = 3; + sr_dbg("Device is in URPSP1 mode, terminator is CRCRLF"); + } else { + sr_err("Don't know how to deal with %d bytes left", bytes_left); + g_mutex_unlock(&devc->rw_mutex); + return SR_ERR; + } + + ret = serial_flush(serial); + + g_mutex_unlock(&devc->rw_mutex); + return ret; +} + +/* + * Can we trust that the reported voltage is the same as the voltage target? If + * the output is off or the device is in CV mode, the answer is likely yes. + * Only run this once during the initialization, since naively detecting CV + * mode is not terribly reliable, especially when there is an ongoing + * transition from CV to CC or vice-versa. + */ +SR_PRIV int gwinstek_psp_get_initial_voltage_target(struct dev_context *devc) +{ + if (!devc->output_enabled || (fabs(devc->current - devc->current_limit) >= 0.01)) { + devc->voltage_target = devc->voltage; + sr_dbg("Set initial voltage target to %.2f", devc->voltage_target); + } else { + /* Would it be more correct to fail the scan here? */ + sr_warn("Could not determine actual voltage target, falling back to 0"); + devc->voltage_target = 0; + } + + return SR_OK; +} + +SR_PRIV int gwinstek_psp_get_all_values(struct sr_serial_dev_inst *serial, + struct dev_context *devc) +{ + int ret, bytes_to_read; + char buf[50], *b; + uint64_t now, delta; + + g_mutex_lock(&devc->rw_mutex); + + now = g_get_monotonic_time(); + delta = now - devc->last_status_query_time; + if (delta <= GWINSTEK_PSP_STATUS_POLL_TIME_MS * 1000) { + sr_spew("Last status query was only %" PRIu64 "us ago, returning", delta); + g_mutex_unlock(&devc->rw_mutex); + return SR_OK; + } + + ret = gwinstek_psp_send_cmd(serial, devc, CMD_ALL_QUERY, FALSE); + devc->last_status_query_time = now; + + if (ret < 0) { + g_mutex_unlock(&devc->rw_mutex); + return ret; + } + + b = buf; + memset(buf, 0, sizeof(buf)); + bytes_to_read = RESPONSE_ALL_QUERY_LEN + devc->msg_terminator_len; + ret = serial_read_blocking(serial, b, bytes_to_read, 1000); + + if (ret < 0) { + sr_err("Error %d reading from device.", ret); + g_mutex_unlock(&devc->rw_mutex); + return ret; + } + + sr_dbg("Received: '%s'", buf); + + if (ret != bytes_to_read || sscanf(buf, "V%fA%fW%fU%dI%f", &devc->voltage, + &devc->current, &devc->power, + &devc->voltage_limit, &devc->current_limit) != 5) { + sr_err("Parsing status payload failed"); + while (serial_read_blocking(serial, b, 1, 1000) > 0); + g_mutex_unlock(&devc->rw_mutex); + return SR_ERR; + } + + devc->output_enabled = (buf[31] == '1'); + devc->otp_active = (buf[32] == '1'); + + if (devc->output_enabled) { + devc->voltage_or_0 = devc->voltage; + } else { + devc->voltage_target = devc->voltage; + devc->voltage_target_updated = g_get_monotonic_time(); + devc->voltage_or_0 = 0; + } + + sr_spew("Status: voltage_or_0=%.2f, voltage_target=%.2f, current=%.3f, power=%.1f, " + "voltage_limit=%d, current_limit=%.2f", + devc->voltage_or_0, devc->voltage_target, devc->current, devc->power, + devc->voltage_limit, devc->current_limit); + + g_mutex_unlock(&devc->rw_mutex); + return SR_OK; +} SR_PRIV int gwinstek_psp_receive_data(int fd, int revents, void *cb_data) { - const struct sr_dev_inst *sdi; + struct sr_dev_inst *sdi; struct dev_context *devc; + struct sr_serial_dev_inst *serial; + struct sr_datafeed_packet packet; + struct sr_datafeed_analog analog; + struct sr_analog_encoding encoding; + struct sr_analog_meaning meaning; + struct sr_analog_spec spec; + gboolean otp_active_prev; + gboolean output_enabled_prev; + GSList *l; (void)fd; + (void)revents; sdi = cb_data; if (!sdi) @@ -35,9 +209,74 @@ SR_PRIV int gwinstek_psp_receive_data(int fd, int revents, void *cb_data) if (!devc) return TRUE; - if (revents == G_IO_IN) { - /* TODO */ + serial = sdi->conn; + + otp_active_prev = devc->otp_active; + output_enabled_prev = devc->output_enabled; + + gwinstek_psp_get_all_values(serial, devc); + + if (otp_active_prev != devc->otp_active) { + sr_session_send_meta(sdi, SR_CONF_OVER_TEMPERATURE_PROTECTION_ACTIVE, + g_variant_new_boolean(devc->otp_active)); + } + + if (output_enabled_prev != devc->output_enabled) { + sr_session_send_meta(sdi, SR_CONF_ENABLED, + g_variant_new_boolean(devc->output_enabled)); } + if (devc->set_voltage_target != devc->voltage_target && + (devc->set_voltage_target_updated + 1000 * 1000) < + devc->voltage_target_updated) { + /* The device reports a voltage target that is different from + * the one that was last set. Trust the device if the information + * is more recent. */ + sr_dbg("Updating session voltage target to %.2f", + devc->voltage_target); + sr_session_send_meta(sdi, SR_CONF_VOLTAGE_TARGET, + g_variant_new_double(devc->voltage_target)); + devc->set_voltage_target = devc->voltage_target; + devc->set_voltage_target_updated = g_get_monotonic_time(); + } + + /* Note: digits/spec_digits will be overridden later. */ + sr_analog_init(&analog, &encoding, &meaning, &spec, 0); + + packet.type = SR_DF_ANALOG; + packet.payload = &analog; + meaning.mqflags = SR_MQFLAG_DC; + analog.num_samples = 1; + + /* Voltage */ + l = g_slist_copy(sdi->channels); + l = g_slist_remove_link(l, g_slist_nth(l, 1)); + meaning.channels = l; + meaning.mq = SR_MQ_VOLTAGE; + meaning.mqflags = SR_MQFLAG_DC; + meaning.unit = SR_UNIT_VOLT; + encoding.digits = 2; + spec.spec_digits = 2; + analog.data = &devc->voltage_or_0; + sr_session_send(sdi, &packet); + g_slist_free(l); + + /* Current */ + l = g_slist_copy(sdi->channels); + l = g_slist_remove_link(l, g_slist_nth(l, 0)); + meaning.channels = l; + meaning.mq = SR_MQ_CURRENT; + meaning.unit = SR_UNIT_AMPERE; + encoding.digits = 3; + spec.spec_digits = 3; + analog.data = &devc->current; + sr_session_send(sdi, &packet); + g_slist_free(l); + + sr_sw_limits_update_samples_read(&devc->limits, 1); + + if (sr_sw_limits_check(&devc->limits)) + sr_dev_acquisition_stop(sdi); + return TRUE; } diff --git a/src/hardware/gwinstek-psp/protocol.h b/src/hardware/gwinstek-psp/protocol.h index f477c7fc..a76a6c20 100644 --- a/src/hardware/gwinstek-psp/protocol.h +++ b/src/hardware/gwinstek-psp/protocol.h @@ -27,9 +27,54 @@ #define LOG_PREFIX "gwinstek-psp" +#define GWINSTEK_PSP_PROCESSING_TIME_MS 50 +#define GWINSTEK_PSP_STATUS_POLL_TIME_MS 245 /**< 'L' query response time. */ + +/* Information on single model */ +struct gwinstek_psp_model { + const char *vendor; /**< Vendor name */ + const char *name; /**< Model name */ + const double *voltage; /**< References: Min, max, step */ + const double *current; /**< References: Min, max, step */ +}; + struct dev_context { + const struct gwinstek_psp_model *model; /**< Model information. */ + + struct sr_sw_limits limits; + int64_t next_req_time; + int64_t last_status_query_time; + GMutex rw_mutex; + + float power; /**< Last power value [W] read from device. */ + float current; /**< Last current value [A] read from device. */ + float current_limit; /**< Output current set. */ + float voltage; /**< Last voltage value [V] read from device. */ + float voltage_or_0; /**< Same, but 0 if output is off. */ + int voltage_limit; /**< Output voltage limit. */ + + /*< Output voltage target. The device has no means to query this + * directly. It's equal to the voltage if the output is disabled + * (detectable) or the device is in CV mode (undetectable).*/ + float voltage_target; + int64_t voltage_target_updated; /**< When device last reported a voltage target. */ + + float set_voltage_target; /**< The last set output voltage target. */ + int64_t set_voltage_target_updated; /**< When the voltage target was last set. */ + + gboolean output_enabled; /**< Is the output enabled? */ + gboolean otp_active; /**< Is the overtemperature protection active? */ + + int msg_terminator_len; /** < 2 or 3, depending on the URPSP1/2 setting */ }; +SR_PRIV int gwinstek_psp_send_cmd(struct sr_serial_dev_inst *serial, + struct dev_context *devc, const char* cmd, gboolean lock); +SR_PRIV int gwinstek_psp_check_terminator(struct sr_serial_dev_inst *serial, + struct dev_context *devc); +SR_PRIV int gwinstek_psp_get_initial_voltage_target(struct dev_context *devc); +SR_PRIV int gwinstek_psp_get_all_values(struct sr_serial_dev_inst *serial, + struct dev_context *devc); SR_PRIV int gwinstek_psp_receive_data(int fd, int revents, void *cb_data); #endif