diff --git a/.vscode/settings.json b/.vscode/settings.json index 4b82f36..e3d255f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -110,7 +110,8 @@ "sfhip.h": "c", "string.h": "c", "math.h": "c", - "cctype": "c" + "cctype": "c", + "stdlib.h": "c" }, "cmake.sourceDirectory": "/home/mira/src/embedded/ch32v208_sens/lwip" } \ No newline at end of file diff --git a/main.c b/main.c index c35f119..1b4eb5d 100644 --- a/main.c +++ b/main.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -24,9 +25,10 @@ // Timing Config #define USB_DEBOUNCE_CONNECT_MS 50 #define USB_DEBOUNCE_DISCONNECT_MS 200 +#define USB_SEND_TIMEOUT_MS 50 #define ENV_SENSOR_READ_INTERVAL_MS 1000 -#define GPIB_TIMEOUT_MS 100 +#define DEFAULT_GPIB_TIMEOUT_MS 3000 // Menu Animation Timings #define MENU_DOT_INTERVAL_MS 500 // Speed of "..." addition @@ -39,41 +41,121 @@ #define DMM_RECOVERY_DELAY_MS 1000 // Backoff if DMM vanishes // Diode sound -#define DIODE_TH_SHORT 0.050f // Volts (below this = SHORT) -#define DIODE_TH_OPEN 2.500f // Volts (above this = OPEN/OL) -#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle +#define DIODE_TH_SHORT 0.050 // Volts (below this = SHORT) +#define DIODE_TH_OPEN 2.500 // Volts (above this = OPEN/OL) +#define DIODE_STABLE_MS 20 // wait X ms for voltage to settle #define DIODE_CHIRP_MS 50 // PT1000 (DIN 43760 / IEC 751 Standard) -#define RTD_A 3.9083e-3f -#define RTD_B -5.775e-7f -#define RTD_R0 1000.0f +#define RTD_A 3.9083e-3 +#define RTD_B -5.775e-7 +#define RTD_R0 1000.0 // Thermistor // TODO: different ranges? 5k, 10k etc.? -// This is a 5k NTC!!! kinda useless here -#define THERM_A 0.001286f -#define THERM_B 0.00023595f -#define THERM_C 0.0000000941f +// This is a 5k NTC! kinda useless here +#define THERM_A 0.001286 +#define THERM_B 0.00023595 +#define THERM_C 0.0000000941 // Thermocouple -#define CJC_FALLBACK_TEMP 22.0f // used if !app.env_sensor_present +#define CJC_FALLBACK_TEMP 22.0 // used if !app.env_sensor_present // this is just cursed but.. yeah, the PCB is near a transformer // ideally, the temp should be measured right at the binding posts.. -#define CJC_SELF_HEATING_OFFSET 4.0f -#define TYPE_K_SCALE 24390.24f // 1 / 41uV +#define CJC_SELF_HEATING_OFFSET 4.0 +#define TYPE_K_SCALE 24390.24 // 1 / 41uV // dBm -#define DBM_REF_Z 50.0f +#define DBM_REF_Z 50.0 // Stats #define STATS_CYCLE_TIME_MS 3000 // time per screen (Live -> Avg -> Min...) -#define STATS_INIT_MIN_VAL 1.0e9f -#define STATS_INIT_MAX_VAL -1.0e9f +#define STATS_INIT_MIN_VAL 1.0e9 +#define STATS_INIT_MAX_VAL -1.0e9 // HP3478A -#define HP_DISP_LEN 13 // 13 chars -#define CONT_THRESHOLD_OHMS 10.0f // continuity beep threshold -#define HP_OVERLOAD_VAL 9.0e9f // HP sends +9.9999E+9 on overload -#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL +#define HP_DISP_LEN 12 // 12 chars +// max str length required to fill 12 segments. +// Worst case: 12 chars + 12 dots/commas + 1 Null terminator = 25 bytes +#define HP_DISP_BUF_SIZE ((HP_DISP_LEN * 2) + 1) +#define CONT_THRESHOLD_OHMS 10.0 // continuity beep threshold +#define CONT_DISP_UPDATE_MS 250 // display throttling +#define DMM_OL_THRESHOLD 9.0e9 // HP sends +9.9999E+9 on overload +#define DMM_OL_NEG_THRESHOLD -9.0e9 +#define REL_STABLE_SAMPLES 3 // filter depth for Relative NULL + +typedef enum { + CMD_UNKNOWN = 0, + // Prologix / Standard GPIB + CMD_ADDR, + CMD_AUTO, + CMD_READ, + CMD_WRITE, + CMD_TRG, + CMD_CLR, + CMD_DCL, + CMD_SPOLL, + CMD_LOC, + CMD_GTL, + CMD_LLO, + CMD_REN, + CMD_IFC, + CMD_STAT, + CMD_RST, + CMD_VER, + CMD_HELP, + // HP3478A Features + CMD_CONT, + CMD_TEMP, + CMD_REL, + CMD_XOHM, + CMD_DBM, + CMD_DIODE, + CMD_MATH, + CMD_NORM, + CMD_DISP, + CMD_ENV, + CMD_TIMEOUT +} cmd_id_t; + +typedef struct { + const char* name; + cmd_id_t id; +} cmd_entry_t; + +static const cmd_entry_t COMMAND_TABLE[] = { + // common + {"read", CMD_READ}, + {"write", CMD_WRITE}, + {"addr", CMD_ADDR}, + {"trg", CMD_TRG}, + {"auto", CMD_AUTO}, + {"stat", CMD_STAT}, + // feats + {"cont", CMD_CONT}, + {"temp", CMD_TEMP}, + {"rel", CMD_REL}, + {"xohm", CMD_XOHM}, + {"dbm", CMD_DBM}, + {"diode", CMD_DIODE}, + {"math", CMD_MATH}, + {"norm", CMD_NORM}, + {"disp", CMD_DISP}, + {"env", CMD_ENV}, + // GPIB mgmt + {"read_tmo_ms", CMD_TIMEOUT}, + {"tmo", CMD_TIMEOUT}, + {"clr", CMD_CLR}, + {"dcl", CMD_DCL}, + {"spoll", CMD_SPOLL}, + {"loc", CMD_LOC}, + {"gtl", CMD_GTL}, + {"llo", CMD_LLO}, + {"ren", CMD_REN}, + {"ifc", CMD_IFC}, + {"rst", CMD_RST}, + {"ver", CMD_VER}, + {"help", CMD_HELP}, + {"?", CMD_HELP}, + {NULL, CMD_UNKNOWN}}; typedef enum { MODE_PASSTHROUGH = 0, // Standard USB-GPIB bridge @@ -99,13 +181,6 @@ typedef enum { MENU_MAX_ITEMS } menu_item_t; -typedef struct { - const char* name; - float a; - float b; - float c; -} thermistor_def_t; - // UI Strings static const char* MENU_NAMES[] = {"REL", "TEMP", "DBM", "CONT", "DIODE", "XOHM", "STATS", "EXIT"}; @@ -129,7 +204,6 @@ typedef enum { SENS_MAX_ITEMS } temp_sensor_t; typedef enum { WIRE_2W = 0, WIRE_4W, WIRE_MAX_ITEMS } wire_mode_t; -typedef enum { CONT_SHORT = 0, CONT_OPEN } cont_state_t; typedef enum { DIODE_STATE_OPEN = 0, // probes open @@ -139,36 +213,47 @@ typedef enum { DIODE_STATE_DONE // chirped, latched silent } diode_state_t; +typedef struct { + const char* cmd_func; + const char* cmd_range; + const char* cmd_digits; + const char* cmd_az; + char unit_str[5]; // "VDC", "OHM", etc. +} dmm_decoded_state_t; + typedef union { struct { - float offset; + double offset; uint8_t stable_count; + char unit[5]; } rel; struct { - float min; - float max; - float sum; + double min; + double max; + double sum; uint32_t count; uint32_t disp_timer; uint8_t view_mode; + char unit[5]; } stats; struct { - float r1; + double r1; uint8_t calibrated; } xohm; - struct { - uint32_t disp_timer; - int last_state; - } cont; - struct { uint8_t connected; // touchy? uint32_t chirp_start; // when we began the touchy } diode; + struct { + // we're using very fast ADC mode for cont, so throttle disp update + // although.. ideally this should be done globally somehow + uint32_t last_disp_update; + } cont; + // menu state (only used when in MODE_MENU) struct { uint32_t timer; @@ -191,6 +276,7 @@ typedef struct { // Addresses uint8_t target_addr; uint8_t dmm_addr; + uint32_t gpib_timeout_ms; // Timers uint32_t usb_ts; @@ -213,7 +299,7 @@ typedef struct { uint8_t saved_state_bytes[5]; // Display shadow buffer - char last_disp_sent[HP_DISP_LEN + 1]; + char last_disp_sent[HP_DISP_BUF_SIZE]; // Unionized Mode Data mode_data_t data; @@ -221,15 +307,22 @@ typedef struct { static app_state_t app = {.dmm_addr = DEFAULT_DMM_ADDR, .target_addr = DEFAULT_DMM_ADDR, - .poll_interval = POLL_INTERVAL_MS}; + .poll_interval = POLL_INTERVAL_MS, + .gpib_timeout_ms = DEFAULT_GPIB_TIMEOUT_MS}; // Buffers +// TODO: buffer usage is very inconsistent +// cmd and resp can probably be a union, and what do we even use tmp_buffer +// for?.. static char cmd_buffer[128]; static char resp_buffer[256]; static char tmp_buffer[128]; -static char disp_buffer[HP_DISP_LEN + 1]; -// Size: "D3"(2) + Text(12) + "\n"(1) + Null(1) = 16 bytes -static char disp_cmd_buffer[2 + HP_DISP_LEN + 1 + 1]; +// Max theoretic bytes for 12 visual chars is 24 +static char disp_buffer[HP_DISP_BUF_SIZE]; +// Display Command Buffer +// Size: "D3"(2) + Text(HP_DISP_BUF_SIZE) + "\n"(1) + Null(1) +// "D3" + "1.2....2." + "\n" + \0 +static char disp_cmd_buffer[2 + HP_DISP_BUF_SIZE + 2]; // USB Ring Buffer #define USB_RX_BUF_SIZE 512 @@ -244,7 +337,6 @@ static uint32_t current_buzz_freq = 0; extern volatile uint8_t usb_debug; // helpers - static int starts_with_nocase(const char* str, const char* prefix) { while (*prefix) { if (tolower((unsigned char)*str) != tolower((unsigned char)*prefix)) { @@ -263,154 +355,341 @@ static char* skip_spaces(char* str) { return str; } -void fmt_float(char* buf, size_t size, float val, int precision) { - if (val != val) { - snprintf(buf, size, "NAN"); - return; - } - if (val > 3.4e38f) { - snprintf(buf, size, "INF"); - return; - } - - if (val < 0.0f) { - *buf++ = '-'; - val = -val; - size--; - } - - float rounder = 0.5f; - for (int i = 0; i < precision; i++) rounder *= 0.1f; - val += rounder; - - uint32_t int_part = (uint32_t)val; - float remainder = val - (float)int_part; - - int len = snprintf(buf, size, "%lu", int_part); - if (len < 0 || (size_t)len >= size) return; - - buf += len; - size -= len; - - if (precision > 0 && size > 1) { - *buf++ = '.'; - size--; - while (precision-- > 0 && size > 1) { - remainder *= 10.0f; - int digit = (int)remainder; - if (digit > 9) digit = 9; - *buf++ = '0' + digit; - remainder -= digit; - size--; +static cmd_id_t parse_command_id(const char* cmd) { + for (int i = 0; COMMAND_TABLE[i].name != NULL; i++) { + if (starts_with_nocase(cmd, COMMAND_TABLE[i].name)) { + int len = strlen(COMMAND_TABLE[i].name); + char next = cmd[len]; + if (next == 0 || isspace((unsigned char)next)) { + return COMMAND_TABLE[i].id; + } } - *buf = 0; } + return CMD_UNKNOWN; } -// [NUMBER] + [SPACES] + [UNIT] -// assumes buffer is at least HP_DISP_LEN + 1b -void format_aligned_display(char* buffer, float value, int precision, - const char* suffix) { - char num_str[16]; +void double_to_str(char* buf, size_t buf_size, double val, int prec) { + if (buf_size == 0) return; + buf[0] = '\0'; + size_t offset = 0; - // fmt number to string - fmt_float(num_str, sizeof(num_str), value, precision); + // NaN/Inf + if (val != val) { + if (buf_size > 3) strcpy(buf, "NAN"); + return; + } - int num_len = strlen(num_str); - int suf_len = strlen(suffix); + // negative + if (val < 0.0) { + if (offset < buf_size - 1) buf[offset++] = '-'; + val = -val; + } + + // 3. Multiplier (Integer Powers of 10) + if (prec < 0) prec = 0; + if (prec > 9) prec = 9; // Limit precision to prevent overflow + + unsigned long long multiplier = 1; + for (int i = 0; i < prec; i++) multiplier *= 10; + + // scale and round + val = (val * (double)multiplier) + 0.5; + + // 5. split integer and fractional parts + unsigned long long full_scaled = (unsigned long long)val; + unsigned long int_part = (unsigned long)(full_scaled / multiplier); + unsigned long long frac_part = full_scaled % multiplier; + + // print integer part + int res = snprintf(buf + offset, buf_size - offset, "%lu", int_part); + + if (res < 0 || (size_t)res >= buf_size - offset) { + buf[buf_size - 1] = '\0'; + return; + } + offset += (size_t)res; + + // print decimal point and fraction + if (prec > 0 && offset < buf_size - 1) { + buf[offset++] = '.'; + + unsigned long long divider = multiplier / 10; + + while (divider > 0 && offset < buf_size - 1) { + unsigned int digit = (unsigned int)(frac_part / divider); + buf[offset++] = (char)('0' + digit); + + frac_part %= divider; + divider /= 10; + } + } + + buf[offset] = '\0'; +} + +// "Note that period, comma, and semicolon go between characters" +int count_non_visual_chars(const char* s) { + int c = 0; + while (*s) { + if (*s == '.' || *s == ',' || *s == ';') c++; + s++; + } + return c; +} + +void format_metric_value(char* buffer, size_t buf_len, double val, + const char* unit, int auto_scale) { + // must hold 12 visible chars + 1 null. + if (buf_len <= HP_DISP_LEN) return; - // reset buffer to spaces memset(buffer, ' ', HP_DISP_LEN); buffer[HP_DISP_LEN] = '\0'; - // write number (left aligned) - // clamp to max display length - int copy_len = (num_len > HP_DISP_LEN) ? HP_DISP_LEN : num_len; - memcpy(buffer, num_str, copy_len); + // scale + double scaled = val; + double abs_val = fabs(val); + char suffix = 0; - // write suffix (right aligned) - int suf_start = HP_DISP_LEN - suf_len; - if (suf_start < 0) suf_start = 0; + if (auto_scale) { + if (abs_val >= 1.0e9) { + scaled *= 1.0e-9; + suffix = 'G'; + } else if (abs_val >= 1.0e6) { + scaled *= 1.0e-6; + suffix = 'M'; + } else if (abs_val >= 1.0e3) { + scaled *= 1.0e-3; + suffix = 'K'; + } + } + double abs_s = fabs(scaled); - memcpy(&buffer[suf_start], suffix, suf_len); -} + // unit + suffix + size_t unit_len = strlen(unit); + size_t suffix_len = (suffix != 0) ? 1 : 0; + size_t meta_vis_len = unit_len + suffix_len; -void format_resistance(char* buffer, size_t buf_len, float val) { - if (buf_len < HP_DISP_LEN + 1) return; - - float scaled_val; - const char* suffix; - int prec; - - // determine logic - if (val >= 1e9f) { - scaled_val = val / 1e9f; - suffix = " G"; - prec = 4; // "1.1234 G" - } else if (val >= 1e6f) { - scaled_val = val / 1e6f; - suffix = " M"; - prec = 4; // "10.1234 M" - } else if (val >= 1e3f) { - scaled_val = val / 1e3f; - suffix = " K"; - prec = 4; // "100.1234 K" - } else { - scaled_val = val; - suffix = " OHM"; - prec = 2; // "100.55 OHM" + if (meta_vis_len > (HP_DISP_LEN - 3)) { + // truncate unit? } - format_aligned_display(buffer, scaled_val, prec, suffix); + int int_digits = 1; + if (abs_s >= 1.0) { + double t = abs_s; + while (t >= 10.0) { + t /= 10.0; + int_digits++; + } + } + + int is_neg = (scaled < 0.0); + // visual slots needed for non-decimal part (sign + ints + space separator) + int separator = (meta_vis_len > 0) ? 1 : 0; + int reserved_vis = is_neg + int_digits + separator + meta_vis_len; + + // calculate allowed decimals (visual) + int allowed_prec = HP_DISP_LEN - reserved_vis; + if (allowed_prec < 0) allowed_prec = 0; + + int desired_prec = 5; + if (abs_s >= 10000.0) + desired_prec = 1; + else if (abs_s >= 1000.0) + desired_prec = 2; + else if (abs_s >= 100.0) + desired_prec = 3; + else if (abs_s >= 10.0) + desired_prec = 4; + + int final_prec = (desired_prec < allowed_prec) ? desired_prec : allowed_prec; + + // render num + char num_buf[32]; + double_to_str(num_buf, sizeof(num_buf), scaled, final_prec); + + // calc padding + int num_bytes = strlen(num_buf); + int num_dots = count_non_visual_chars(num_buf); + int num_vis_len = num_bytes - num_dots; + + // total visual slots used so far + int total_vis_used = num_vis_len + meta_vis_len; + + // to push unit to the right edge + int spaces_needed = HP_DISP_LEN - total_vis_used; + if (spaces_needed < 1 && meta_vis_len > 0) + spaces_needed = 1; // enforce 1 space if possible? + if (total_vis_used + spaces_needed > HP_DISP_LEN) { + // crunch: if overflow (very large number), reduce spaces + spaces_needed = HP_DISP_LEN - total_vis_used; + if (spaces_needed < 0) spaces_needed = 0; + } + + // [number] [spaces] [suffix] [unit] + // num + strcpy(buffer, num_buf); + // spaces + int current_len = strlen(buffer); + for (int i = 0; i < spaces_needed; i++) { + buffer[current_len++] = ' '; + } + buffer[current_len] = '\0'; + // suffix + if (suffix) { + buffer[current_len++] = suffix; + buffer[current_len] = '\0'; + } + // unit + if (unit_len > 0) { + strcat(buffer, unit); + } } -float parse_float(const char* s) { - float res = 0.0f; - float fact = 1.0f; +double parse_double(const char* s) { + double mantissa = 0.0; + int exponent = 0; int sign = 1; - int point_seen = 0; + int decimal_seen = 0; + int decimal_counts = 0; + // skip whitespace while (*s == ' ') s++; - if (*s == '+') + // handle sign + if (*s == '+') { s++; - else if (*s == '-') { + } else if (*s == '-') { sign = -1; s++; } - // parse mantissa + // parse Mantissa as pure int while (*s) { - if (*s == '.') { - point_seen = 1; - } else if (*s >= '0' && *s <= '9') { - if (point_seen) { - fact /= 10.0f; - res += (*s - '0') * fact; - } else { - res = res * 10.0f + (*s - '0'); + if (*s >= '0' && *s <= '9') { + mantissa = (mantissa * 10.0) + (*s - '0'); + if (decimal_seen) { + decimal_counts++; } + } else if (*s == '.') { + decimal_seen = 1; } else if (*s == 'E' || *s == 'e') { - s++; // skip 'E' - int exp = atoi(s); - - // apply exponent - float power = 1.0f; - int exp_abs = abs(exp); - while (exp_abs--) power *= 10.0f; - - if (exp > 0) - res *= power; - else - res /= power; - + s++; + exponent = atoi(s); break; } else { break; } s++; } - return res * sign; + + exponent -= decimal_counts; + + double power = 1.0; + int e_abs = abs(exponent); + while (e_abs-- > 0) power *= 10.0; + + if (exponent > 0) { + mantissa *= power; + } else { + mantissa /= power; + } + + return mantissa * sign; +} + +void decode_dmm_state_bytes(const uint8_t* state_bytes, + dmm_decoded_state_t* out) { + uint8_t b1 = state_bytes[0]; + uint8_t b2 = state_bytes[1]; + + // decode function & unit + switch ((b1 >> 5) & 0x07) { + case 2: + out->cmd_func = HP3478A_FUNC_AC_VOLTS; + strcpy(out->unit_str, "VAC"); + break; + case 3: + out->cmd_func = HP3478A_FUNC_OHMS_2WIRE; + strcpy(out->unit_str, "OHM"); + break; + case 4: + out->cmd_func = HP3478A_FUNC_OHMS_4WIRE; + strcpy(out->unit_str, "OHM"); + break; + case 5: + out->cmd_func = HP3478A_FUNC_DC_CURRENT; + strcpy(out->unit_str, "ADC"); + break; + case 6: + out->cmd_func = HP3478A_FUNC_AC_CURRENT; + strcpy(out->unit_str, "AAC"); + break; + case 7: + out->cmd_func = HP3478A_FUNC_OHMS_EXT; + strcpy(out->unit_str, "OHM"); + break; + default: // case 1 or anything else + out->cmd_func = HP3478A_FUNC_DC_VOLTS; + strcpy(out->unit_str, "VDC"); + break; + } + + // decode range (bits 4-2) + autorange (byte 2 bit 1) + if (b2 & 0x02) { + out->cmd_range = HP3478A_RANGE_AUTO; + } else { + switch ((b1 >> 2) & 0x07) { + case 1: + out->cmd_range = HP3478A_RANGE_NEG_2; + break; + case 2: + out->cmd_range = HP3478A_RANGE_NEG_1; + break; + case 3: + out->cmd_range = HP3478A_RANGE_0; + break; + case 4: + out->cmd_range = HP3478A_RANGE_1; + break; + case 5: + out->cmd_range = HP3478A_RANGE_2; + break; + case 6: + out->cmd_range = HP3478A_RANGE_3; + break; + case 7: + out->cmd_range = HP3478A_RANGE_4; + break; + default: + out->cmd_range = HP3478A_RANGE_0; + break; + } + } + + // decode digits (bits 1-0) + switch (b1 & 0x03) { + case 2: + out->cmd_digits = HP3478A_DIGITS_4_5; + break; + case 3: + out->cmd_digits = HP3478A_DIGITS_3_5; + break; + default: + out->cmd_digits = HP3478A_DIGITS_5_5; + break; + } + + // decode autozero (byte 2 bit 2) + out->cmd_az = (b2 & 0x04) ? HP3478A_AUTOZERO_ON : HP3478A_AUTOZERO_OFF; +} + +// constructs the base configuration string (F R N Z) from decoded state +void build_restoration_string(char* buffer, const dmm_decoded_state_t* state) { + strcpy(buffer, HP3478A_DISP_NORMAL); + strcat(buffer, state->cmd_func); + strcat(buffer, state->cmd_range); + strcat(buffer, state->cmd_digits); + strcat(buffer, state->cmd_az); } #ifdef GPIB_DEBUG @@ -497,7 +776,7 @@ static int gpib_wait_pin(int pin, int expected_state) { uint32_t start = millis(); while (GPIB_READ(pin) != expected_state) { - if ((millis() - start) > GPIB_TIMEOUT_MS) { + if ((millis() - start) > app.gpib_timeout_ms) { #ifdef GPIB_DEBUG // Print which specific pin failed char* pin_name = "UNKNOWN"; @@ -782,7 +1061,7 @@ err: // Data transfer -// Send string to device (auto-handles CRLF escape sequences) +// Send string to device (handles CRLF escape sequences) int gpib_send(uint8_t addr, const char* str) { if (gpib_start_session(addr, SESSION_WRITE) < 0) return -1; @@ -1066,14 +1345,31 @@ void HandleDataOut(struct _USBState* ctx, int endp, uint8_t* data, int len) { } static void usb_send_text(const char* str) { + if (!app.usb_online) return; + int len = strlen(str); int pos = 0; + while (pos < len) { int chunk = len - pos; if (chunk > 64) chunk = 64; - USBFS_SendEndpointNEW(3, (uint8_t*)(str + pos), chunk, 1); - Delay_Us(250); // yikes + uint32_t start_time = millis(); + int sent = 0; + + while ((millis() - start_time) < USB_SEND_TIMEOUT_MS) { + int result = USBFS_SendEndpointNEW(3, (uint8_t*)(str + pos), chunk, 1); + + if (result == 0) { + sent = 1; + break; + } + } + + if (!sent) { + return; + } + pos += chunk; } } @@ -1165,23 +1461,22 @@ static void handle_env_sensor(void) { } } -// Helper to write text to HP3478A Display -// CMD: D2 = Full alphanumeric message -void dmm_display(const char* text) { - if (strncmp(app.last_disp_sent, text, HP_DISP_LEN) == 0) { - return; // text hasn't changed +// helper to write text to HP3478A display +void dmm_display(const char* text, const char* mode) { + // cmp vs shadow buf + if (strncmp(app.last_disp_sent, text, HP_DISP_BUF_SIZE) == 0) { + return; } - // upd cache - strncpy(app.last_disp_sent, text, HP_DISP_LEN); - app.last_disp_sent[HP_DISP_LEN] = 0; + // printf("Updating Display: %s\n", text); - // copy command - strcpy(disp_cmd_buffer, HP3478A_DISP_TEXT_FAST); - // append text - strncat(disp_cmd_buffer, text, HP_DISP_LEN); - // newline require for D2 and D3 - strcat(disp_cmd_buffer, "\n"); + // update shadow buf + strncpy(app.last_disp_sent, text, HP_DISP_BUF_SIZE - 1); + app.last_disp_sent[HP_DISP_BUF_SIZE - 1] = '\0'; + // "D[23]" + Text + "\n" + snprintf(disp_cmd_buffer, sizeof(disp_cmd_buffer), "%s%s\n", mode, + app.last_disp_sent); + // send it gpib_send(app.dmm_addr, disp_cmd_buffer); } static inline void dmm_display_normal(void) { @@ -1200,6 +1495,7 @@ void save_dmm_state(void) { void restore_dmm_state(void) { if (!app.has_saved_state) { + // default fallback if no state saved gpib_send(app.dmm_addr, HP3478A_DISP_NORMAL HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL @@ -1207,94 +1503,11 @@ void restore_dmm_state(void) { return; } - uint8_t b1 = app.saved_state_bytes[0]; - uint8_t b2 = app.saved_state_bytes[1]; + dmm_decoded_state_t state; + decode_dmm_state_bytes(app.saved_state_bytes, &state); - // ptrs to string literals - const char* cmd_func; - const char* cmd_range; - const char* cmd_dig; - const char* cmd_az; - - switch ((b1 >> 5) & 0x07) { - case 2: - cmd_func = HP3478A_FUNC_AC_VOLTS; - break; - case 3: - cmd_func = HP3478A_FUNC_OHMS_2WIRE; - break; - case 4: - cmd_func = HP3478A_FUNC_OHMS_4WIRE; - break; - case 5: - cmd_func = HP3478A_FUNC_DC_CURRENT; - break; - case 6: - cmd_func = HP3478A_FUNC_AC_CURRENT; - break; - case 7: - cmd_func = HP3478A_FUNC_OHMS_EXT; - break; - default: - cmd_func = HP3478A_FUNC_DC_VOLTS; - break; - } - - // Decode Range (Bits 4-2) + AutoRange (Byte 2 Bit 1) - if (b2 & 0x02) { - cmd_range = HP3478A_RANGE_AUTO; - } else { - // Octal 1=R-2 ... Octal 7=R4 - switch ((b1 >> 2) & 0x07) { - case 1: - cmd_range = HP3478A_RANGE_NEG_2; - break; - case 2: - cmd_range = HP3478A_RANGE_NEG_1; - break; - case 3: - cmd_range = HP3478A_RANGE_0; - break; - case 4: - cmd_range = HP3478A_RANGE_1; - break; - case 5: - cmd_range = HP3478A_RANGE_2; - break; - case 6: - cmd_range = HP3478A_RANGE_3; - break; - case 7: - cmd_range = HP3478A_RANGE_4; - break; - default: - cmd_range = HP3478A_RANGE_0; - break; - } - } - - // Decode Digits (Bits 1-0) - switch (b1 & 0x03) { - case 2: - cmd_dig = HP3478A_DIGITS_4_5; - break; - case 3: - cmd_dig = HP3478A_DIGITS_3_5; - break; - default: - cmd_dig = HP3478A_DIGITS_5_5; - break; - } - - // Decode AutoZero (Byte 2 Bit 2) - cmd_az = (b2 & 0x04) ? HP3478A_AUTOZERO_ON : HP3478A_AUTOZERO_OFF; - - // "D1 Fx Rx Nx Zx T1 M20" - strcpy(cmd_buffer, HP3478A_DISP_NORMAL); - strcat(cmd_buffer, cmd_func); - strcat(cmd_buffer, cmd_range); - strcat(cmd_buffer, cmd_dig); - strcat(cmd_buffer, cmd_az); + // "D1 Fx Rx Nx Zx" + "T1 M20" + build_restoration_string(cmd_buffer, &state); strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_ONLY); gpib_send(app.dmm_addr, cmd_buffer); @@ -1316,18 +1529,33 @@ void exit_to_passthrough(void) { void enter_feature_mode(menu_item_t item) { // force display refresh app.last_disp_sent[0] = '\0'; + // clean buffer for cmds + cmd_buffer[0] = '\0'; gpib_remote_enable(1); // assert REN Delay_Ms(20); + // we will hold the decoded state here for REL/STATS + dmm_decoded_state_t saved_cfg; + switch (item) { case MENU_REL: - gpib_send(app.dmm_addr, - HP3478A_FUNC_OHMS_2WIRE HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL - HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); + if (app.has_saved_state) { + decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); + build_restoration_string(cmd_buffer, &saved_cfg); + strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA + HP3478A_CMD_SRQ_CLEAR); + strcpy(app.data.rel.unit, saved_cfg.unit_str); + } else { + // we have no saved state? don't know relative to WHAT + dmm_display("ERR NO STATE", HP3478A_DISP_TEXT_FAST); + } + + gpib_send(app.dmm_addr, cmd_buffer); + app.current_mode = MODE_FEAT_REL; - app.data.rel.offset = 0.0f; - dmm_display("REL MODE"); + app.data.rel.offset = 0.0; + dmm_display("REL MODE", HP3478A_DISP_TEXT_FAST); break; case MENU_DBM: @@ -1336,7 +1564,7 @@ void enter_feature_mode(menu_item_t item) { HP3478A_MEAS_AC_VOLTS HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); app.current_mode = MODE_FEAT_DBM; - dmm_display("DBM 50 OHM"); + dmm_display("DBM 50 OHM", HP3478A_DISP_TEXT_FAST); break; case MENU_TEMP: @@ -1350,7 +1578,7 @@ void enter_feature_mode(menu_item_t item) { HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_NEG_2 HP3478A_DIGITS_5_5 HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); - dmm_display("TEMP TYPE-K"); + dmm_display("TEMP TYPE-K", HP3478A_DISP_TEXT_FAST); } else { // Resistor Setup (PT1000/Therm): // F3=2W / F4=4W @@ -1367,12 +1595,10 @@ void enter_feature_mode(menu_item_t item) { HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); } - gpib_send(app.dmm_addr, tmp_buffer); - if (app.temp_sensor == SENS_PT1000) - dmm_display("TEMP PT1000"); + dmm_display("TEMP PT1000", HP3478A_DISP_TEXT_FAST); else - dmm_display("TEMP THERM"); + dmm_display("TEMP THERM", HP3478A_DISP_TEXT_FAST); } break; @@ -1383,9 +1609,7 @@ void enter_feature_mode(menu_item_t item) { HP3478A_AUTOZERO_OFF HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); app.current_mode = MODE_FEAT_CONT; - app.data.cont.last_state = -1; - app.data.cont.disp_timer = millis(); - dmm_display("CONT MODE"); + dmm_display("CONT MODE", HP3478A_DISP_TEXT_FAST); break; case MENU_DIODE: @@ -1403,7 +1627,7 @@ void enter_feature_mode(menu_item_t item) { app.data.diode.connected = 0; app.data.diode.chirp_start = millis() - DIODE_CHIRP_MS - 1; - dmm_display("DIODE TEST"); + dmm_display("DIODE TEST", HP3478A_DISP_TEXT_FAST); break; case MENU_XOHM: @@ -1412,34 +1636,41 @@ void enter_feature_mode(menu_item_t item) { gpib_send(app.dmm_addr, HP3478A_MEAS_OHMS_EXT HP3478A_DIGITS_5_5 HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); - app.data.xohm.r1 = 0.0f; + app.data.xohm.r1 = 0.0; app.data.xohm.calibrated = 0; app.current_mode = MODE_FEAT_XOHM; - dmm_display("XOHM 10M REF"); + dmm_display("XOHM 10M REF", HP3478A_DISP_TEXT_FAST); break; case MENU_STATS: - // F1: DC Volts - // A1: Auto Range - // N5: 5.5 Digit - // Z1: Auto Zero ON - // T1: Internal Trigger - gpib_send(app.dmm_addr, - HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 - HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL - HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); + if (app.has_saved_state) { + decode_dmm_state_bytes(app.saved_state_bytes, &saved_cfg); + build_restoration_string(cmd_buffer, &saved_cfg); + strcat(cmd_buffer, HP3478A_TRIG_INTERNAL HP3478A_CMD_MASK_BTN_DATA + HP3478A_CMD_SRQ_CLEAR); + strcpy(app.data.stats.unit, saved_cfg.unit_str); + } else { + // fallback + strcpy(cmd_buffer, + HP3478A_FUNC_DC_VOLTS HP3478A_RANGE_AUTO HP3478A_DIGITS_5_5 + HP3478A_AUTOZERO_ON HP3478A_TRIG_INTERNAL + HP3478A_CMD_MASK_BTN_DATA HP3478A_CMD_SRQ_CLEAR); + strcpy(app.data.stats.unit, "VDC"); + } + + gpib_send(app.dmm_addr, cmd_buffer); app.current_mode = MODE_FEAT_STATS; // Initialize Stats - app.data.stats.min = STATS_INIT_MIN_VAL; // start impossible high - app.data.stats.max = STATS_INIT_MAX_VAL; // start impossible low - app.data.stats.sum = 0.0f; + app.data.stats.min = DBL_MAX; // start impossible high + app.data.stats.max = -DBL_MAX; // start impossible low + app.data.stats.sum = 0.0; app.data.stats.count = 0; app.data.stats.view_mode = 0; app.data.stats.disp_timer = millis(); - dmm_display("STATS INIT"); + dmm_display("STATS INIT", HP3478A_DISP_TEXT_FAST); break; case MENU_EXIT: @@ -1455,12 +1686,11 @@ void enter_menu_mode(void) { save_dmm_state(); - uint32_t now = millis(); app.current_mode = MODE_MENU; app.menu_pos = MENU_REL; - app.data.menu.timer = now; + app.data.menu.timer = millis(); - dmm_display("M: REL"); + dmm_display("M: REL", HP3478A_DISP_TEXT_FAST); gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); Delay_Ms(200); } @@ -1489,94 +1719,96 @@ void handle_feature_logic(void) { return; } - float val = parse_float(resp_buffer); + double val = parse_double(resp_buffer); // overload (HP 3478A sends +9.99990E+9 for OL) - int is_overload = (val > HP_OVERLOAD_VAL); + int is_overload = 0; + if (val < DMM_OL_NEG_THRESHOLD || val > DMM_OL_THRESHOLD) is_overload = 1; // RELATIVE MODE if (app.current_mode == MODE_FEAT_REL) { - if (app.data.rel.offset == 0.0f) { + if (app.data.rel.offset == 0.0) { // waiting to capture the NULL value if (is_overload) { app.data.rel.stable_count = 0; // reset counter if probes are open - dmm_display("O.VLD"); + dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { // valid reading app.data.rel.stable_count++; if (app.data.rel.stable_count >= REL_STABLE_SAMPLES) { app.data.rel.offset = val; - dmm_display("NULL SET"); + dmm_display("NULL SET", HP3478A_DISP_TEXT_FAST); tone(3000, 50); app.data.rel.stable_count = 0; } else { - dmm_display("LOCKING..."); + dmm_display("LOCKING...", HP3478A_DISP_TEXT_FAST); } } } else { // offset is already set if (is_overload) { - dmm_display("O.VLD"); + dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { - float diff = val - app.data.rel.offset; - fmt_float(disp_buffer, sizeof(disp_buffer), diff, 4); - // Display: "1.2345 D" - format_aligned_display(disp_buffer, diff, 4, " D"); - dmm_display(disp_buffer); + double diff = val - app.data.rel.offset; + char eff_unit[5]; + // prepend 'D' + snprintf(eff_unit, sizeof(eff_unit), "D%s", app.data.rel.unit); + + format_metric_value(disp_buffer, sizeof(disp_buffer), diff, eff_unit, + 1); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } } } // dBm MODE else if (app.current_mode == MODE_FEAT_DBM) { if (is_overload) { - dmm_display("O.VLD"); + dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { // P(mW) = (V^2 / 50) * 1000 = V^2 * 20 - float p_mw = (val * val * 20.0f); + double p_mw = (val * val * 20.0f); if (p_mw < 1e-9f) { // Align -INF to look consistent // Display: "-INF DBM" - memset(disp_buffer, ' ', 12); - disp_buffer[12] = '\0'; + memset(disp_buffer, ' ', HP_DISP_LEN); + disp_buffer[HP_DISP_LEN] = '\0'; memcpy(disp_buffer, "-INF", 4); memcpy(&disp_buffer[8], " DBM", 4); - dmm_display(disp_buffer); + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); } else { - float dbm = 10.0f * log10f(p_mw); - // Display: "-14.20 DBM" - format_aligned_display(disp_buffer, dbm, 2, " DBM"); - dmm_display(disp_buffer); + double dbm = 10.0 * log10(p_mw); + format_metric_value(disp_buffer, sizeof(disp_buffer), dbm, "DBM", 00); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } } } // TEMP MODE else if (app.current_mode == MODE_FEAT_TEMP) { if (is_overload && app.temp_sensor != SENS_TYPE_K) { - dmm_display("OPEN / ERR"); + dmm_display("OPEN / ERR", HP3478A_DISP_TEXT_FAST); } else { - float temp_c = 0.0f; + double temp_c = 0.0; const char* unit_str = " C"; // 1. TYPE K THERMOCOUPLE if (app.temp_sensor == SENS_TYPE_K) { // 30mV range safety check (Floating input > 50mV) - if (fabsf(val) > 0.05f) { - dmm_display("CHECK PROBE"); + if (fabs(val) > 0.050) { + dmm_display("CHECK PROBE", HP3478A_DISP_TEXT_FAST); return; } else { - float t_amb; + double t_amb; // Cold Junction Compensation (CJC) if (app.env_sensor_present) { - // Convert int(2450) to 24.50 - t_amb = (float)app.current_env.temp_c_x100 / 100.0f; + t_amb = app.current_env.temp_c_x100 / 100.0; t_amb -= CJC_SELF_HEATING_OFFSET; - unit_str = " C (K)"; + unit_str = "C (K)"; } else { t_amb = CJC_FALLBACK_TEMP; - unit_str = " C (K*)"; // '*' means fallback CJC used + unit_str = "C (K*)"; // '*' means fallback CJC used } // Type K response is actually NOT linear, this should be a LUT or a @@ -1588,17 +1820,18 @@ void handle_feature_logic(void) { // 2. PT1000 RTD (Callendar-Van Dusen) else if (app.temp_sensor == SENS_PT1000) { if (val < 10.0f) { - dmm_display("SHORT"); + dmm_display("SHORT", HP3478A_DISP_TEXT_FAST); + return; } else { - float c = RTD_R0 - val; - float b = RTD_R0 * RTD_A; - float a = RTD_R0 * RTD_B; - float disc = (b * b) - (4 * a * c); + double c = RTD_R0 - val; + double b = RTD_R0 * RTD_A; + double a = RTD_R0 * RTD_B; + double disc = (b * b) - (4 * a * c); if (disc >= 0) temp_c = (-b + sqrtf(disc)) / (2 * a); else { - dmm_display("RANGE ERR"); + dmm_display("RANGE ERR", HP3478A_DISP_TEXT_FAST); return; } } @@ -1606,54 +1839,41 @@ void handle_feature_logic(void) { // 3. THERMISTOR (Steinhart-Hart) else { if (val < 10.0f) { - dmm_display("SHORT"); + dmm_display("SHORT", HP3478A_DISP_TEXT_FAST); + return; } else { - // protect against log(0) - float r = (val < 1.0f) ? 1.0f : val; - float lr = logf(r); - float lr3 = lr * lr * lr; - float inv_t = THERM_A + (THERM_B * lr) + (THERM_C * lr3); + // log(0) ? :) + double r = (val < 1.0f) ? 1.0f : val; + double lr = log(r); + double lr3 = lr * lr * lr; + double inv_t = THERM_A + (THERM_B * lr) + (THERM_C * lr3); temp_c = (1.0f / inv_t) - 273.15f; } } // Display: "24.5 C" (or "24.5 C (K)") - format_aligned_display(disp_buffer, temp_c, 1, unit_str); - dmm_display(disp_buffer); + format_metric_value(disp_buffer, sizeof(disp_buffer), temp_c, unit_str, + 0); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } } // CONT MODE else if (app.current_mode == MODE_FEAT_CONT) { int is_short = (!is_overload && val < CONT_THRESHOLD_OHMS); - // beep - if (is_short) { - buzzer_set(2000); // 2kHz tone - } else { - buzzer_set(0); - } + // instant beep + buzzer_set(is_short ? 2500 : 0); - // display logic uint32_t now = millis(); - // force update if state changed or timeout - if ((is_short != app.data.cont.last_state) || - (now - app.data.cont.disp_timer > 200)) { - if (is_overload) { - dmm_display("OPEN"); - } else { - if (val < 1000.0f) { - // Normalize low ohms to look like standard ohms mode - // "10.5 OHM" - format_aligned_display(disp_buffer, val, 1, " OHM"); - } else { - // shouldn't happen in 300 range :) - format_resistance(disp_buffer, sizeof(disp_buffer), val); - } - dmm_display(disp_buffer); - } + if (now - app.data.cont.last_disp_update > CONT_DISP_UPDATE_MS) { + app.data.cont.last_disp_update = now; - app.data.cont.last_state = is_short; - app.data.cont.disp_timer = now; + if (is_overload) { + dmm_display("OPEN", HP3478A_DISP_TEXT_FAST); + } else { + format_metric_value(disp_buffer, sizeof(disp_buffer), val, "OHM", 1); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); + } } } @@ -1664,10 +1884,10 @@ void handle_feature_logic(void) { (voltage > DIODE_TH_SHORT && voltage < DIODE_TH_OPEN); if (voltage < DIODE_TH_OPEN) { - format_aligned_display(disp_buffer, voltage, 4, "VDC"); - dmm_display(disp_buffer); + format_metric_value(disp_buffer, sizeof(disp_buffer), voltage, "VDC", 1); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } else { - dmm_display("OPEN"); + dmm_display("OPEN", HP3478A_DISP_TEXT); } diode_state_t next_state = app.data.diode.connected; @@ -1724,14 +1944,14 @@ void handle_feature_logic(void) { // cal phase, measure the internal 10M resistor if (app.data.xohm.calibrated == 0) { // need the probes to be open. Internal R is ~10M - if (val > 8.0e6f && val < 12.0e6f) { + if (val > 8.0e6 && val < 12.0e6) { app.data.xohm.r1 = val; // Store R1 app.data.xohm.calibrated = 1; tone(3000, 100); - dmm_display("READY"); + dmm_display("READY", HP3478A_DISP_TEXT_FAST); Delay_Ms(500); } else { - dmm_display("OPEN PROBES"); + dmm_display("OPEN PROBES", HP3478A_DISP_TEXT_FAST); } } // Rx = (R1 * R2) / (R1 - R2) @@ -1739,24 +1959,24 @@ void handle_feature_logic(void) { // R2 = val (Measured Parallel) else { if (is_overload || val >= (app.data.xohm.r1 - 1000.0f)) { - dmm_display("OPEN"); + dmm_display("OPEN", HP3478A_DISP_TEXT_FAST); } else { - float r1 = app.data.xohm.r1; - float r2 = val; - float rx = (r1 * r2) / (r1 - r2); + double r1 = app.data.xohm.r1; + double r2 = val; + double rx = (r1 * r2) / (r1 - r2); - format_resistance(disp_buffer, sizeof(disp_buffer), rx); - dmm_display(disp_buffer); + format_metric_value(disp_buffer, sizeof(disp_buffer), rx, "OHM", 1); + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } } } else if (app.current_mode == MODE_FEAT_STATS) { if (is_overload) { - dmm_display("O.VLD"); + dmm_display("O.VLD", HP3478A_DISP_TEXT_FAST); } else { // accumulate math if (val < app.data.stats.min) app.data.stats.min = val; if (val > app.data.stats.max) app.data.stats.max = val; - app.data.stats.sum += (double)val; + app.data.stats.sum += val; app.data.stats.count++; // rotate display @@ -1768,40 +1988,62 @@ void handle_feature_logic(void) { } // render - char prefix[5]; // 3-char prefix + space - float val_to_show = 0.0f; + const char* prefix_str = ""; + double val_to_show = val; switch (app.data.stats.view_mode) { - case 0: // Live Value - strcpy(prefix, ""); + case 0: // Live val_to_show = val; break; - case 1: // Average - strcpy(prefix, "AVG "); + case 1: // Avg + prefix_str = "AVG "; val_to_show = app.data.stats.sum / app.data.stats.count; break; - case 2: // Minimum - strcpy(prefix, "MIN "); + case 2: // Min + prefix_str = "MIN "; val_to_show = app.data.stats.min; break; - case 3: // Maximum - strcpy(prefix, "MAX "); + case 3: // Max + prefix_str = "MAX "; val_to_show = app.data.stats.max; break; default: - strcpy(prefix, "ERR "); - val_to_show = val; app.data.stats.view_mode = 0; break; } - int offset = snprintf(disp_buffer, sizeof(disp_buffer), "%s", prefix); - if (offset >= 0 && offset < (int)sizeof(disp_buffer)) { - fmt_float(disp_buffer + offset, sizeof(disp_buffer) - offset, - val_to_show, 4); + // render + if (app.data.stats.view_mode == 0) { + // live mode + format_metric_value(disp_buffer, sizeof(disp_buffer), val_to_show, + app.data.stats.unit, 1); + } else { + // stats mode: prefix (4) + number (8) + double abs_v = fabs(val_to_show); + double scaled = val_to_show; + char suffix = 0; + + if (abs_v >= 1.0e9) { + scaled *= 1.0e-9; + suffix = 'G'; + } else if (abs_v >= 1.0e6) { + scaled *= 1.0e-6; + suffix = 'M'; + } else if (abs_v >= 1.0e3) { + scaled *= 1.0e-3; + suffix = 'K'; + } + + char num_buf[16]; + double_to_str(num_buf, sizeof(num_buf), scaled, 4); + + // combine: prefix + number + suffix + snprintf(disp_buffer, sizeof(disp_buffer), "%s%s%c ", prefix_str, + num_buf, suffix ? suffix : ' '); } - dmm_display(disp_buffer); + // send it + dmm_display(disp_buffer, HP3478A_DISP_TEXT); } } } @@ -1856,7 +2098,7 @@ void handle_menu_navigation(void) { // update display immediately prepare_menu_base_string(); - dmm_display(disp_buffer); + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); // re-arm SRQ gpib_send(app.dmm_addr, HP3478A_CMD_MASK_BTN_ONLY HP3478A_CMD_SRQ_CLEAR); @@ -1875,7 +2117,7 @@ void handle_menu_navigation(void) { for (int i = 0; i < dots; i++) strcat(disp_buffer, "."); } - dmm_display(disp_buffer); + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); if (elapsed > MENU_COMMIT_DELAY_MS) { // L0: main menu @@ -1887,7 +2129,7 @@ void handle_menu_navigation(void) { app.data.menu.timer = now; prepare_menu_base_string(); - dmm_display(disp_buffer); + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); return; } // enter standard modes @@ -1909,7 +2151,7 @@ void handle_menu_navigation(void) { app.data.menu.timer = now; prepare_menu_base_string(); - dmm_display(disp_buffer); + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); } return; } @@ -2050,11 +2292,12 @@ static void cmd_status(void) { "Stat:\r\n" " Target Addr: %d\r\n" " Internal DMM: %d\r\n" + " Timeout: %lu ms\r\n" " Auto Read: %s\r\n" " Current Mode: %d\r\n" " FW: " FW_VERSION "\r\n", - app.target_addr, app.dmm_addr, app.auto_read ? "ON" : "OFF", - app.current_mode); + app.target_addr, app.dmm_addr, app.gpib_timeout_ms, + app.auto_read ? "ON" : "OFF", app.current_mode); usb_send_text(tmp_buffer); } @@ -2063,21 +2306,12 @@ static void process_command(void) { return; } - int is_query = 0; - char* p_cmd = skip_spaces(cmd_buffer); int is_cpp_cmd = (strncmp(p_cmd, "++", 2) == 0); - - if (app.current_mode != MODE_PASSTHROUGH) { - buzzer_set(0); - app.current_mode = MODE_PASSTHROUGH; - dmm_display_normal(); - gpib_remote_enable(1); // ensure REN matches state - } + int is_query = (strchr(p_cmd, '?') != NULL); if (is_cpp_cmd) { - // move past "++" - p_cmd += 2; + p_cmd += 2; // skip "++" // 'p_args' will point to the first non-space char after the command word char* p_args = p_cmd; @@ -2085,214 +2319,230 @@ static void process_command(void) { p_args++; // find end of word p_args = skip_spaces(p_args); // find start of args - // CMD: ADDR - if (starts_with_nocase(p_cmd, "addr")) { - if (*p_args) { - int addr = atoi(p_args); - if (addr >= 0 && addr <= 30) { - app.target_addr = addr; + cmd_id_t cmd_id = parse_command_id(p_cmd); + + switch (cmd_id) { + // config + case CMD_ADDR: + if (*p_args) { + int addr = atoi(p_args); + if (addr >= 0 && addr <= 30) { + app.target_addr = addr; + usb_send_text("OK\r\n"); + } else + usb_send_text("ERR: Invalid Addr\r\n"); + } else { + // if no arg provided, show current + snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", app.target_addr); + usb_send_text(tmp_buffer); + } + break; + + case CMD_AUTO: + if (*p_args) { + app.auto_read = atoi(p_args) ? 1 : 0; usb_send_text("OK\r\n"); } else - usb_send_text("ERR: Invalid Addr\r\n"); - } else { - // if no arg provided, show current - snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", app.target_addr); - usb_send_text(tmp_buffer); - } - } - // CMD: WRITE - else if (starts_with_nocase(p_cmd, "write")) { - // sends the rest of the string (p_args) to GPIB - if (*p_args) { - gpib_send(app.target_addr, p_args); - if (app.auto_read) goto do_read_operation; // jmp to read block - } - } - // CMD: READ - else if (starts_with_nocase(p_cmd, "read")) { - goto do_read_operation; - } - // CMD: AUTO - else if (starts_with_nocase(p_cmd, "auto")) { - if (*p_args) { - app.auto_read = atoi(p_args) ? 1 : 0; - usb_send_text("OK\r\n"); - } else { - usb_send_text(app.auto_read ? "1\r\n" : "0\r\n"); - } - } - // CMD: TRG - else if (starts_with_nocase(p_cmd, "trg")) { - gpib_trigger(app.target_addr); - usb_send_text("OK\r\n"); - } - // CMD: STATUS / STAT - else if (starts_with_nocase(p_cmd, "stat")) { - cmd_status(); - } - // CMD: CLR - else if (starts_with_nocase(p_cmd, "clr")) { - gpib_device_clear(app.target_addr); - usb_send_text("OK\r\n"); - } - // CMD: REN (Remote Enable) - else if (starts_with_nocase(p_cmd, "ren")) { - if (*p_args) { - int state = atoi(p_args); - gpib_remote_enable(state); - usb_send_text("OK\r\n"); - } else { - usb_send_text("usage: ++ren 1|0\r\n"); - } - } - // CMD: IFC (Interface Clear) - else if (starts_with_nocase(p_cmd, "ifc")) { - gpib_interface_clear(); - usb_send_text("OK\r\n"); - } + usb_send_text(app.auto_read ? "1\r\n" : "0\r\n"); + break; - // CMD: SPOLL (Serial Poll) - else if (starts_with_nocase(p_cmd, "spoll")) { - uint8_t poll_addr = app.target_addr; + case CMD_VER: + usb_send_text("HP3478A Internal USB-GPIB " FW_VERSION "\r\n"); + break; + case CMD_STAT: + cmd_status(); + break; + case CMD_HELP: + cmd_help(); + break; + case CMD_RST: + NVIC_SystemReset(); + break; - if (*p_args) { - int arg = atoi(p_args); - if (arg >= 0 && arg <= 30) poll_addr = arg; - } - - uint8_t stb; - if (gpib_serial_poll(poll_addr, &stb) == 0) { - // print status byte as dec - snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", stb); - usb_send_text(tmp_buffer); - } else { - usb_send_text("ERR: Bus\r\n"); - } - } - // CMD: LLO (Local Lockout) - else if (starts_with_nocase(p_cmd, "llo")) { - gpib_local_lockout(); - usb_send_text("OK\r\n"); - } - // CMD: DCL (Universal Clear) - else if (starts_with_nocase(p_cmd, "dcl")) { - gpib_universal_clear(); - usb_send_text("OK\r\n"); - } - // CMD: GTL (Go To Local) - else if (starts_with_nocase(p_cmd, "gtl")) { - gpib_go_to_local(app.target_addr); - usb_send_text("OK\r\n"); - } - // HP3478A Internal - else if (starts_with_nocase(p_cmd, "cont")) - enter_feature_mode(MENU_CONT); - else if (starts_with_nocase(p_cmd, "temp")) - enter_feature_mode(MENU_TEMP); - else if (starts_with_nocase(p_cmd, "rel")) - enter_feature_mode(MENU_REL); - else if (starts_with_nocase(p_cmd, "xohm")) - enter_feature_mode(MENU_XOHM); - else if (starts_with_nocase(p_cmd, "dbm")) - enter_feature_mode(MENU_DBM); - else if (starts_with_nocase(p_cmd, "diode")) - enter_feature_mode(MENU_DIODE); - else if (starts_with_nocase(p_cmd, "math")) - enter_feature_mode(MENU_STATS); - else if (starts_with_nocase(p_cmd, "norm")) - exit_to_passthrough(); - else if (starts_with_nocase(p_cmd, "disp")) { - int i = 0; - - while (p_args[i] != 0 && i < 12) { - char c = p_args[i]; - if (c >= 'a' && c <= 'z') { - c -= 32; + // data + case CMD_READ: + goto do_read_operation; + case CMD_WRITE: + if (*p_args) { + gpib_send(app.target_addr, p_args); + if (app.auto_read) goto do_read_operation; } + break; - disp_buffer[i] = c; - i++; + // GPIB Bus control + case CMD_TIMEOUT: + if (*p_args) { + int val = atoi(p_args); + // min 1ms, max 60s + if (val > 0 && val < 60000) { + app.gpib_timeout_ms = val; + usb_send_text("OK\r\n"); + } else { + usb_send_text("ERR: Range 1-60000\r\n"); + } + } else { + snprintf(tmp_buffer, sizeof(tmp_buffer), "%lu\r\n", + app.gpib_timeout_ms); + usb_send_text(tmp_buffer); + } + break; + case CMD_TRG: + gpib_trigger(app.target_addr); + usb_send_text("OK\r\n"); + break; + case CMD_CLR: + gpib_device_clear(app.target_addr); + usb_send_text("OK\r\n"); + break; + case CMD_DCL: + gpib_universal_clear(); + usb_send_text("OK\r\n"); + break; + case CMD_IFC: + gpib_interface_clear(); + usb_send_text("OK\r\n"); + break; + case CMD_LLO: + gpib_local_lockout(); + usb_send_text("OK\r\n"); + break; + case CMD_GTL: + gpib_go_to_local(app.target_addr); + usb_send_text("OK\r\n"); + break; + case CMD_LOC: + gpib_remote_enable(0); + usb_send_text("OK\r\n"); + break; + + case CMD_REN: + if (*p_args) { + gpib_remote_enable(atoi(p_args)); + usb_send_text("OK\r\n"); + } else + usb_send_text("ERR: Usage ++ren 1|0\r\n"); + break; + + case CMD_SPOLL: { + uint8_t poll_addr = (*p_args) ? atoi(p_args) : app.target_addr; + uint8_t stb; + if (gpib_serial_poll(poll_addr, &stb) == 0) { + snprintf(tmp_buffer, sizeof(tmp_buffer), "%d\r\n", stb); + usb_send_text(tmp_buffer); + } else + usb_send_text("ERR: Bus\r\n"); + break; } - disp_buffer[i] = 0; - dmm_display(disp_buffer); - usb_send_text("OK\r\n"); + + // HP3478A Internal Features + case CMD_NORM: + exit_to_passthrough(); + // ensure REN is back up for normal use + gpib_remote_enable(1); + usb_send_text("OK\r\n"); + break; + + case CMD_DISP: { + int i = 0; + while (p_args[i] != 0 && i < HP_DISP_LEN) { + char c = p_args[i]; + if (c >= 'a' && c <= 'z') c -= 32; + disp_buffer[i++] = c; + } + disp_buffer[i] = 0; + dmm_display(disp_buffer, HP3478A_DISP_TEXT_FAST); + usb_send_text("OK\r\n"); + break; + } + + case CMD_ENV: { + if (!app.env_sensor_present) { + usb_send_text("ERR: No Sensor\r\n"); + return; + } + double t = app.current_env.temp_c_x100 / 100.0; + double h = app.current_env.hum_p_x100 / 100.0; + char* arg = skip_spaces(p_args); + char out_buf[32]; + + // temp only + if (starts_with_nocase(arg, "temp")) { + double_to_str(out_buf, sizeof(out_buf), t, 2); + usb_send_text(out_buf); + usb_send_text("\r\n"); + } + // hum only + else if (starts_with_nocase(arg, "hum")) { + double_to_str(out_buf, sizeof(out_buf), h, 2); + usb_send_text(out_buf); + usb_send_text("\r\n"); + } + // CSV format (temp,hum) + else { + double_to_str(out_buf, 16, t, 2); + strcat(out_buf, ","); + // fmt humidity into a temp buffer and append + char h_buf[16]; + double_to_str(h_buf, sizeof(h_buf), h, 2); + strcat(out_buf, h_buf); + strcat(out_buf, "\r\n"); + + usb_send_text(out_buf); + } + break; + } + + // feat entry + case CMD_CONT: + case CMD_TEMP: + case CMD_REL: + case CMD_XOHM: + case CMD_DBM: + case CMD_DIODE: + case CMD_MATH: { + menu_item_t item = MENU_EXIT; + if (cmd_id == CMD_CONT) + item = MENU_CONT; + else if (cmd_id == CMD_TEMP) + item = MENU_TEMP; + else if (cmd_id == CMD_REL) + item = MENU_REL; + else if (cmd_id == CMD_XOHM) + item = MENU_XOHM; + else if (cmd_id == CMD_DBM) + item = MENU_DBM; + else if (cmd_id == CMD_DIODE) + item = MENU_DIODE; + else if (cmd_id == CMD_MATH) + item = MENU_STATS; + + save_dmm_state(); // save before changing + enter_feature_mode(item); + usb_send_text("OK\r\n"); + break; + } + + default: + usb_send_text("ERR: Unknown cmd\r\n"); + break; } - // SYSTEM - else if (starts_with_nocase(p_cmd, "loc")) { - gpib_remote_enable(0); - usb_send_text("OK\r\n"); - } else if (starts_with_nocase(p_cmd, "rst")) { - usb_send_text("Rebooting...\r\n"); - Delay_Ms(100); - NVIC_SystemReset(); - } else if (starts_with_nocase(p_cmd, "ver")) { - usb_send_text("HP3478A Internal GPIB " FW_VERSION "\r\n"); - } else if (starts_with_nocase(p_cmd, "help") || - starts_with_nocase(p_cmd, "?")) { - cmd_help(); - } else if (starts_with_nocase(p_cmd, "env")) { - if (!app.env_sensor_present) { - usb_send_text("ERR: No Sensor\r\n"); - return; - } - float t = (float)app.current_env.temp_c_x100 / 100.0f; - float h = (float)app.current_env.hum_p_x100 / 100.0f; - char* arg = skip_spaces(p_args); - char out_buf[32]; - - // temp only - if (starts_with_nocase(arg, "temp")) { - fmt_float(out_buf, sizeof(out_buf), t, 2); - usb_send_text(out_buf); - usb_send_text("\r\n"); - } - // hum only - else if (starts_with_nocase(arg, "hum")) { - fmt_float(out_buf, sizeof(out_buf), h, 2); - usb_send_text(out_buf); - usb_send_text("\r\n"); - } - // CSV format (temp,hum) - else { - fmt_float(out_buf, 16, t, 2); - strcat(out_buf, ","); - // fmt humidity into a temp buffer and append - char h_buf[16]; - fmt_float(h_buf, sizeof(h_buf), h, 2); - strcat(out_buf, h_buf); - strcat(out_buf, "\r\n"); - - usb_send_text(out_buf); - } - } else { - usb_send_text("ERR: Unknown Command\r\n"); - } - return; // end of ++ + return; } - // PASSTHROUGH MODE - // check for query '?' to trigger implicit read - is_query = (strchr(p_cmd, '?') != NULL); - + // passthrough mode (not "++") if (gpib_send(app.target_addr, p_cmd) < 0) { usb_send_text("ERR: Send Fail\r\n"); return; } - - // check if we should read back - if (is_query || app.auto_read) { - goto do_read_operation; - } + if (is_query || app.auto_read) goto do_read_operation; return; do_read_operation: { int len = gpib_receive(app.target_addr, resp_buffer, sizeof(resp_buffer)); if (len > 0) { usb_send_text(resp_buffer); - } else { - if (is_cpp_cmd || is_query) { - usb_send_text("ERR: Read Timeout\r\n"); - } + } else if (is_cpp_cmd || is_query) { + usb_send_text("ERR: Read Timeout\r\n"); } } }