diff --git a/NEWS.adoc b/NEWS.adoc index 1b59dc78b9..cb8f41738b 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -44,6 +44,10 @@ https://github.com/networkupstools/nut/milestone/13 - Second-level bullet points listed in this file will now use 4 spaces (not 3 like before) for easier initial indentation of new entries. + - Introduced an experimental `apcmicrolink` driver for devices with + the APC Microlink protocol on serial port connections. Tested against + APC Smart-UPS 750 (SMT750RMI2UC). [PR #3406] + - `apc_modbus` driver updates: * Fixed string join not doing zero termination. [PR #3413] * Decode `RunTimeCalibrationStatus_BF` into diff --git a/data/driver.list.in b/data/driver.list.in index bff5143ae4..463f60aedc 100644 --- a/data/driver.list.in +++ b/data/driver.list.in @@ -80,6 +80,7 @@ "APC" "ups" "1" "Matrix-UPS" "" "apcsmart" "APC" "ups" "1" "Smart-UPS" "" "apcsmart" "APC" "ups" "1" "Smart-UPS SMT/SMX/SURTD" "Microlink models with RJ45 socket - they *require* AP9620 SmartSlot expansion card and smart cable" "apcsmart" +"APC" "ups" "1" "Smart-UPS SMT/SMX Microlink" "Microlink serial driver" "apcmicrolink" "APC" "ups" "3" "Back-UPS Pro USB" "USB" "usbhid-ups" "APC" "ups" "3" "Back-UPS BK650M2-CH" "USB" "usbhid-ups" # https://github.com/networkupstools/nut/issues/1970 "APC" "ups" "3" "Back-UPS (USB)" "USB" "usbhid-ups" diff --git a/docs/man/Makefile.am b/docs/man/Makefile.am index abe8bd0e22..1c5f6986a4 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -983,6 +983,7 @@ endif # (--with-serial) SRC_SERIAL_PAGES = \ al175.txt \ + apcmicrolink.txt \ apcsmart.txt \ apcsmart-old.txt \ bcmxcp.txt \ @@ -1037,6 +1038,7 @@ SRC_SERIAL_PAGES = \ INST_MAN_SERIAL_PAGES = \ al175.$(MAN_SECTION_CMD_SYS) \ + apcmicrolink.$(MAN_SECTION_CMD_SYS) \ apcsmart.$(MAN_SECTION_CMD_SYS) \ apcsmart-old.$(MAN_SECTION_CMD_SYS) \ bcmxcp.$(MAN_SECTION_CMD_SYS) \ @@ -1155,6 +1157,7 @@ INST_HTML_SERIAL_MANS = \ upscode2.html \ ve-direct.html \ victronups.html \ + apcmicrolink.html \ apcupsd-ups.html if HAVE_LINUX_SERIAL_H diff --git a/docs/man/apcmicrolink.txt b/docs/man/apcmicrolink.txt new file mode 100644 index 0000000000..c63ffffa74 --- /dev/null +++ b/docs/man/apcmicrolink.txt @@ -0,0 +1,147 @@ +APCMICROLINK(8) +=============== + +NAME +---- + +apcmicrolink - Driver for APC Smart-UPS units using the Microlink serial protocol + +SYNOPSIS +-------- + +*apcmicrolink* -h + +*apcmicrolink* -a 'UPS_NAME' ['OPTIONS'] + +NOTE: This man page documents the hardware-specific features of the +*apcmicrolink* driver. For general information about NUT drivers, see +linkman:nutupsdrv[8]. + + +DESCRIPTION +----------- + +The *apcmicrolink* driver talks the APC Microlink protocol used by newer +serial-connected Smart-UPS families such as SMT and SMX units with the +Microlink RJ45 serial port. + +This driver is currently experimental. It discovers most values from the +device descriptor blob at runtime and maps supported Microlink objects onto +standard NUT variables where possible. Unknown descriptor fields can also be +published for debugging and reverse-engineering. + + +SUPPORTED HARDWARE +------------------ + +This driver is intended for APC Smart-UPS models that expose the Microlink +serial protocol, notably SMT and SMX units with the vendor Microlink serial +cable. + +Tested support currently targets: + +* APC Smart-UPS SMT/SMX Microlink models + +Other APC Microlink devices may work if they expose a compatible descriptor +layout. + + +CONFIGURATION +------------- + +The driver is configured via linkman:ups.conf[5]. + +A minimal configuration: + +---- +[apc-microlink] + driver = apcmicrolink + port = /dev/ttyUSB0 +---- + +Optional settings +~~~~~~~~~~~~~~~~~ + +*baudrate*='num':: +Set the serial line speed. The default is `9600`. + +*showinternals*='yes|no':: +Publish additional internal Microlink runtime values. By default this follows +the driver debug level and is enabled automatically when debug logging is on. + +*showunmapped*='yes|no':: +Publish descriptor values that do not currently map to a standard NUT variable. +By default this follows the driver debug level and is enabled automatically +when debug logging is on. + +*cmdsrc*='rj45|usb|localuser|smartslot1|internalnetwork1':: +Select the Microlink command source used for outgoing command writes. The +default is `rj45`. + + +IMPLEMENTED FEATURES +-------------------- + +The driver publishes standard identity, status, runtime and outlet-group data +when these objects are present in the Microlink descriptor. Descriptor-backed +values are interpreted as strings, hex identifiers, dates, times, fixed-point +numbers, and enum or bitfield maps depending on the Microlink type reported by +the device. + +Writable descriptor-backed variables are exposed as read-write NUT variables +when the device reports them as modifiable. Depending on the connected model, +this can include values such as: + +* `ups.id` +* `ups.display.language` +* `ups.test.interval` +* `battery.date` +* `input.transfer.delay` +* `input.transfer.high` +* `input.transfer.low` +* `outlet.group.N.timer.shutdown` +* `outlet.group.N.timer.reboot` +* `outlet.group.N.timer.start` +* `outlet.group.N.delay.shutdown` +* `outlet.group.N.delay.reboot` +* `outlet.group.N.delay.start` +* `outlet.group.N.minimumreturnruntime` +* `outlet.group.N.lowruntimewarning` +* `outlet.group.N.name` + +Supported instant commands currently include: + +* `test.battery.start` +* `test.battery.stop` +* `test.panel.start` +* `test.panel.stop` + +Driver-assisted shutdown is not yet implemented. + + +CABLING +------- + +Use the APC Microlink serial cable appropriate for the UPS. USB-to-serial +adapters can work if they present a standard TTY device to the operating +system. + + +AUTHORS +------- + +* Lukas Schmid + + +SEE ALSO +-------- + +The core driver +~~~~~~~~~~~~~~~ + +linkman:nutupsdrv[8], linkman:ups.conf[5] + +Internet resources +~~~~~~~~~~~~~~~~~~ + +The NUT (Network UPS Tools) home page: https://www.networkupstools.org/ diff --git a/docs/nut.dict b/docs/nut.dict index c95c5a555f..73d24ffac0 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3749 utf-8 +personal_ws-1.1 en 3761 utf-8 AAC AAS ABI @@ -708,6 +708,7 @@ LogMin LowBatt Loyer Luca +Lukas Luxeon Lygre Lynge @@ -1117,6 +1118,7 @@ RISC RK RMCARD RMCPplus +RMI RMXL RNF RNG @@ -1270,6 +1272,7 @@ Salvia Santinoli Savia Sawatzky +Schmid Schmier Schoch Schonefeld @@ -1646,6 +1649,7 @@ apc apcc apcd apcevilhack +apcmicrolink apcsmart apctest apcupsd @@ -1763,6 +1767,7 @@ bigups bindir binfmt binutils +bitfield bitmapped bitmask bitness @@ -1882,6 +1887,7 @@ cmdline cmdname cmdparam cmds +cmdsrc cmdvartab cnf codebase @@ -2400,6 +2406,7 @@ integrations intel intelliSenseMode intercharacter +internalnetwork internet interoperability interoperate @@ -2589,6 +2596,7 @@ localhost localip localport localtime +localuser lockf logfacility logfile @@ -3223,6 +3231,8 @@ sha shellcheck shellenv shm +showinternals +showunmapped shutdownArguments shutdowncmd shutdowndelay @@ -3247,6 +3257,7 @@ slaveid slavesync slibtool sm +smartslot smartups smbus sms @@ -3411,6 +3422,7 @@ tempmax tempmin termios testime +testinterval testtime testuser testvar diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 0fdb8666c6..1415d1db21 100644 --- a/drivers/Makefile.am +++ b/drivers/Makefile.am @@ -147,7 +147,7 @@ endif HAVE_LIBREGEX NUTSW_DRIVERLIST_DUMMY_UPS = dummy-ups$(EXEEXT) NUTSW_DRIVERLIST = $(NUTSW_DRIVERLIST_DUMMY_UPS) \ clone clone-outlet failover apcupsd-ups skel -SERIAL_DRIVERLIST = al175 bcmxcp belkin belkinunv bestfcom \ +SERIAL_DRIVERLIST = al175 apcmicrolink bcmxcp belkin belkinunv bestfcom \ bestfortress bestuferrups bestups etapro everups \ gamatronic genericups isbmex liebert liebert-esp2 liebert-gxe masterguard metasys \ mge-utalk microdowell microsol-apc mge-shut nutdrv_hashx oneac optiups powercom powervar_cx_ser rhino \ @@ -234,6 +234,7 @@ upsdrvctl_LDADD = libdummy_upsdrvquery.la $(LDADD_COMMON) # serial drivers: all of them use standard LDADD and CFLAGS al175_SOURCES = al175.c +apcmicrolink_SOURCES = apcmicrolink.c apcmicrolink-maps.c apc_common.c apcsmart_SOURCES = apcsmart.c apcsmart_tabs.c apcsmart_LDADD = $(LDADD_DRIVERS_SERIAL) $(LIBREGEX_LIBS) apcsmart_old_SOURCES = apcsmart-old.c @@ -459,7 +460,7 @@ adelsystem_cbi_LDADD = $(LDADD_DRIVERS) $(LIBMODBUS_LIBS) # APC Modbus driver (with support of modbus over different media) # Note that a version of libmodbus built with USB support is also needed # for USB connections. Legacy versions work for Serial and TCP links. -apc_modbus_SOURCES = apc_modbus.c +apc_modbus_SOURCES = apc_modbus.c apc_common.c apc_modbus_LDADD = $(LDADD_DRIVERS) $(LIBMODBUS_LIBS) if WITH_MODBUS_USB apc_modbus_SOURCES += hidparser.c @@ -539,7 +540,7 @@ dist_noinst_HEADERS = \ powercom.h powerpanel.h powerp-bin.h powerp-txt.h powervar_cx.h raritan-pdu-mib.h \ safenet.h serial.h sms_ser.h snmp-ups.h solis.h tripplite.h tripplite-hid.h \ upshandler.h usb-common.h usbhid-ups.h powercom-hid.h compaq-mib.h idowell-hid.h \ - apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ + apc_common.h apcmicrolink-maps.h apcmicrolink.h apcsmart.h apcsmart_tabs.h apcsmart-old.h apcupsd-ups.h cyberpower-mib.h riello.h openups-hid.h \ delta_ups-mib.h nutdrv_qx.h nutdrv_qx_bestups.h nutdrv_qx_blazer-common.h \ nutdrv_qx_gtec.h nutdrv_qx_innovart31.h nutdrv_qx_innovart33.h nutdrv_qx_innovatae.h \ nutdrv_qx_masterguard.h nutdrv_qx_mecer.h nutdrv_qx_ablerex.h \ diff --git a/drivers/apc_common.c b/drivers/apc_common.c new file mode 100644 index 0000000000..0c4cda6e63 --- /dev/null +++ b/drivers/apc_common.c @@ -0,0 +1,358 @@ +/* apc_common.c - Shared APC driver helpers + * + * Copyright (C) 2023 Axel Gembe + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "main.h" + +#include +#include +#include + +#include "timehead.h" + +#include "apc_common.h" + +const apc_value_map_t apc_input_quality_map[] = { + { (1U << 0), "Acceptable" }, + { (1U << 1), "PendingAcceptable" }, + { (1U << 2), "VoltageTooLow" }, + { (1U << 3), "VoltageTooHigh" }, + { (1U << 4), "Distorted" }, + { (1U << 5), "BOOST" }, + { (1U << 6), "TRIM" }, + { (1U << 7), "FrequencyTooLow" }, + { (1U << 8), "FrequencyTooHigh" }, + { (1U << 9), "FreqAndPhaseNotLocked" }, + { (1U << 10), "PhaseDeltaOutOfRange" }, + { (1U << 11), "NeutralNotConnected" }, + { (1U << 12), "NotAcceptable" }, + { (1U << 13), "PlugRatingExceeded" }, + { (1U << 14), "PhaseBotAcceptable" }, + { (1U << 15), "PoweringLoad" }, + { 0, NULL } +}; + +const apc_value_map_t apc_efficiency_map[] = { + { -1, "NotAvailable" }, + { -2, "LoadTooLow" }, + { -3, "OutputOff" }, + { -4, "OnBattery" }, + { -5, "InBypass" }, + { -6, "BatteryCharging" }, + { -7, "PoorACInput" }, + { -8, "BatteryDisconnected" }, + { 0, NULL } +}; + +const apc_value_map_t apc_test_status_map[] = { + { 0, "None" }, + { (1ULL << 0), "Pending" }, + { (1ULL << 1), "InProgress" }, + { (1ULL << 2), "Passed" }, + { (1ULL << 3), "Failed" }, + { (1ULL << 4), "Refused" }, + { (1ULL << 5), "Aborted" }, + { (1ULL << 7), "LocalUser" }, + { (1ULL << 8), "Internal" }, + { (1ULL << 9), "InvalidState" }, + { (1ULL << 10), "InternalFault" }, + { (1ULL << 11), "StateOfChargeNotAcceptable" }, + { 0, NULL } +}; + +const apc_value_map_t apc_calibration_status_map[] = { + { 0, "None" }, + { (1ULL << 0), "Pending" }, + { (1ULL << 1), "InProgress" }, + { (1ULL << 2), "Passed" }, + { (1ULL << 3), "Failed" }, + { (1ULL << 4), "Refused" }, + { (1ULL << 5), "Aborted" }, + { (1ULL << 9), "InvalidState" }, + { (1ULL << 10), "InternalFault" }, + { (1ULL << 11), "StateOfChargeNotAcceptable" }, + { (1ULL << 12), "LoadChange" }, + { (1ULL << 13), "ACInputNotAcceptable" }, + { (1ULL << 14), "LoadTooLow" }, + { (1ULL << 15), "OverChargeInProgress" }, + { 0, NULL } +}; + +const apc_value_map_t apc_status_map[] = { + { (1ULL << 1), "OL" }, + { (1ULL << 2), "OB" }, + { (1ULL << 3), "BYPASS" }, + { (1ULL << 4), "OFF" }, + { (1ULL << 7), "TEST" }, + { (1ULL << 21), "OVER" }, + { 0, NULL } +}; + +const apc_value_map_t apc_alarm_map[] = { + { (1ULL << 5), "General fault" }, + { (1ULL << 6), "Input not acceptable" }, + { 0, NULL } +}; + +const apc_outlet_command_suffix_t apc_outlet_command_suffixes[] = { + { "load.off", APC_OUTLET_OP_LOAD_OFF }, + { "load.on", APC_OUTLET_OP_LOAD_ON }, + { "load.cycle", APC_OUTLET_OP_LOAD_CYCLE }, + { "load.off.delay", APC_OUTLET_OP_LOAD_OFF_DELAY }, + { "load.on.delay", APC_OUTLET_OP_LOAD_ON_DELAY }, + { "shutdown.return", APC_OUTLET_OP_SHUTDOWN_RETURN }, + { "shutdown.stayoff", APC_OUTLET_OP_SHUTDOWN_STAYOFF }, + { "shutdown.reboot", APC_OUTLET_OP_SHUTDOWN_REBOOT }, + { "shutdown.reboot.graceful", APC_OUTLET_OP_SHUTDOWN_REBOOT_GRACEFUL }, + { NULL, APC_OUTLET_OP_NULL } +}; + +const apc_value_map_t apc_countdown_map[] = { + { -1, "NotActive" }, + { 0, "CountdownExpired" }, + { 0, NULL } +}; + +const apc_value_map_t apc_countdown_setting_map[] = { + { -1, "Disabled" }, + { 0, NULL } +}; + +int apc_format_countdown_value(int64_t value, char *output, size_t output_len) +{ + const char *text; + int res; + + if (output == NULL || output_len == 0) { + return 0; + } + + if (value == -1) { + text = "NotActive"; + } else if (value == 0) { + text = "CountdownExpired"; + } else { + res = snprintf(output, output_len, "%" PRIi64, value); + if (res < 0 || (size_t)res >= output_len) { + return 0; + } + return 1; + } + + res = snprintf(output, output_len, "%s", text); + if (res < 0 || (size_t)res >= output_len) { + return 0; + } + + return 1; +} + +/* Format a day counter as YYYY-MM-DD. Microlink and Modbus both store their + * documented date fields as days since 2000-01-01, so the offset is fixed + * here. + */ +int apc_format_date_from_days_offset(int64_t value, char *output, size_t output_len) +{ + struct tm tm_info; + time_t time_stamp; + int res; + + if (output == NULL || output_len == 0) { + return 0; + } + + time_stamp = ((time_t)value * 86400) + ((time_t)10957 * 86400); + if (gmtime_r(&time_stamp, &tm_info) == NULL) { + return 0; + } + + res = strftime(output, output_len, "%Y-%m-%d", &tm_info); + if (res == 0) { + return 0; + } + + return 1; +} + +/* Parse YYYY-MM-DD into day counts. The on-wire format is a day count since + * 2000-01-01, so the fixed offset is handled here. + */ +int apc_parse_date_to_days_offset(const char *value, uint64_t *days_out) +{ + struct tm tm_struct; + time_t epoch_time; + uint64_t uint_value; + + if (value == NULL || days_out == NULL) { + return 0; + } + + memset(&tm_struct, 0, sizeof(tm_struct)); + if (strptime(value, "%Y-%m-%d", &tm_struct) == NULL) { + return 0; + } + + if ((epoch_time = timegm(&tm_struct)) == (time_t)-1) { + return 0; + } + + uint_value = (uint64_t)((epoch_time - ((time_t)10957 * 86400)) / 86400); + *days_out = uint_value; + return 1; +} + +int apc_format_test_status_value(const apc_value_map_t *result_map, const apc_value_map_t *source_map, + const apc_value_map_t *modifier_map, uint64_t value, char *output, size_t output_len) +{ + const char *parts[3]; + size_t part_count = 0; + size_t i; + int res; + + if (output == NULL || output_len == 0) { + return 0; + } + + output[0] = '\0'; + + if (result_map != NULL) { + parts[part_count] = apc_lookup_first_set_value_map_text(result_map, value); + if (parts[part_count] != NULL) { + part_count++; + } + } + + if (source_map != NULL) { + parts[part_count] = apc_lookup_first_set_value_map_text(source_map, value); + if (parts[part_count] != NULL) { + part_count++; + } + } + + if (modifier_map != NULL) { + parts[part_count] = apc_lookup_first_set_value_map_text(modifier_map, value); + if (parts[part_count] != NULL) { + part_count++; + } + } + + for (i = 0; i < part_count; i++) { + res = snprintf(output + strlen(output), output_len - strlen(output), "%s%s", + (i > 0) ? ", " : "", parts[i]); + if (res < 0 || (size_t)res >= output_len - strlen(output)) { + return 0; + } + } + + return 1; +} + +uint64_t apc_build_outlet_command(apc_outlet_command_type_t type, uint64_t target_bits) +{ + uint64_t cmd = target_bits; + + switch (type) { + case APC_OUTLET_OP_LOAD_OFF: + cmd |= APC_OUTLET_CMD_OUTPUT_OFF; + break; + case APC_OUTLET_OP_LOAD_ON: + cmd |= APC_OUTLET_CMD_OUTPUT_ON; + break; + case APC_OUTLET_OP_LOAD_CYCLE: + cmd |= APC_OUTLET_CMD_OUTPUT_REBOOT; + break; + case APC_OUTLET_OP_LOAD_OFF_DELAY: + cmd |= APC_OUTLET_CMD_OUTPUT_OFF | APC_OUTLET_CMD_USE_OFF_DELAY; + break; + case APC_OUTLET_OP_LOAD_ON_DELAY: + cmd |= APC_OUTLET_CMD_OUTPUT_ON | APC_OUTLET_CMD_USE_ON_DELAY; + break; + case APC_OUTLET_OP_SHUTDOWN_RETURN: + cmd |= APC_OUTLET_CMD_OUTPUT_SHUTDOWN | APC_OUTLET_CMD_USE_OFF_DELAY; + break; + case APC_OUTLET_OP_SHUTDOWN_STAYOFF: + cmd |= APC_OUTLET_CMD_OUTPUT_OFF | APC_OUTLET_CMD_USE_OFF_DELAY; + break; + case APC_OUTLET_OP_SHUTDOWN_REBOOT: + cmd |= APC_OUTLET_CMD_OUTPUT_REBOOT; + break; + case APC_OUTLET_OP_SHUTDOWN_REBOOT_GRACEFUL: + cmd |= APC_OUTLET_CMD_OUTPUT_REBOOT | APC_OUTLET_CMD_USE_OFF_DELAY; + break; +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT +# pragma GCC diagnostic ignored "-Wcovered-switch-default" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +# pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +#endif + case APC_OUTLET_OP_NULL: + default: +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic pop +#endif + /* Must not occur. */ + break; + } + + return cmd; +} + +const char *apc_lookup_value_map_text(const apc_value_map_t *map, int value) +{ + size_t i; + + if (map == NULL) { + return NULL; + } + + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == value) { + return map[i].text; + } + } + + return NULL; +} + +const char *apc_lookup_first_set_value_map_text(const apc_value_map_t *map, uint64_t value) +{ + size_t i; + + if (map == NULL) { + return NULL; + } + + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 0) { + if (value == 0) { + return map[i].text; + } + continue; + } + + if ((value & (uint64_t)map[i].value) != 0) { + return map[i].text; + } + } + + return NULL; +} diff --git a/drivers/apc_common.h b/drivers/apc_common.h new file mode 100644 index 0000000000..7a205574c1 --- /dev/null +++ b/drivers/apc_common.h @@ -0,0 +1,150 @@ +/* apc_common.h - Shared APC driver helpers + * + * Copyright (C) 2023 Axel Gembe + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef APC_COMMON_H +#define APC_COMMON_H + +#include +#include + +typedef struct apc_value_map_s { + int value; + const char *text; +} apc_value_map_t; + +typedef enum apc_test_status_bits_e { + APC_TEST_STATUS_PENDING = (1ULL << 0), + APC_TEST_STATUS_IN_PROGRESS = (1ULL << 1), + APC_TEST_STATUS_PASSED = (1ULL << 2), + APC_TEST_STATUS_FAILED = (1ULL << 3), + APC_TEST_STATUS_REFUSED = (1ULL << 4), + APC_TEST_STATUS_ABORTED = (1ULL << 5), + APC_TEST_STATUS_LOCAL_USER = (1ULL << 7), + APC_TEST_STATUS_INTERNAL = (1ULL << 8), + APC_TEST_STATUS_INVALID_STATE = (1ULL << 9), + APC_TEST_STATUS_INTERNAL_FAULT = (1ULL << 10), + APC_TEST_STATUS_STATE_OF_CHARGE_NOT_ACCEPTABLE = (1ULL << 11) +} apc_test_status_bits_t; + +typedef enum apc_calibration_status_bits_e { + APC_CAL_STATUS_PENDING = (1ULL << 0), + APC_CAL_STATUS_IN_PROGRESS = (1ULL << 1), + APC_CAL_STATUS_PASSED = (1ULL << 2), + APC_CAL_STATUS_FAILED = (1ULL << 3), + APC_CAL_STATUS_REFUSED = (1ULL << 4), + APC_CAL_STATUS_ABORTED = (1ULL << 5), + APC_CAL_STATUS_INVALID_STATE = (1ULL << 9), + APC_CAL_STATUS_INTERNAL_FAULT = (1ULL << 10), + APC_CAL_STATUS_STATE_OF_CHARGE_NOT_ACCEPTABLE = (1ULL << 11), + APC_CAL_STATUS_LOAD_CHANGE = (1ULL << 12), + APC_CAL_STATUS_AC_INPUT_NOT_ACCEPTABLE = (1ULL << 13), + APC_CAL_STATUS_LOAD_TOO_LOW = (1ULL << 14), + APC_CAL_STATUS_OVER_CHARGE_IN_PROGRESS = (1ULL << 15) +} apc_calibration_status_bits_t; + +/* + * APC outlets and command bit definitions are shared across protocol variants. + * Keep these values protocol-agnostic so both drivers can use the same + * semantic names while mapping them to different transport encodings. + */ +typedef enum apc_outlet_command_bits_e { + APC_OUTLET_CMD_CANCEL = (1ULL << 0), + APC_OUTLET_CMD_OUTPUT_ON = (1ULL << 1), + APC_OUTLET_CMD_OUTPUT_OFF = (1ULL << 2), + APC_OUTLET_CMD_OUTPUT_SHUTDOWN = (1ULL << 3), + APC_OUTLET_CMD_OUTPUT_REBOOT = (1ULL << 4), + APC_OUTLET_CMD_COLD_BOOT_ALLOWED = (1ULL << 5), + APC_OUTLET_CMD_USE_ON_DELAY = (1ULL << 6), + APC_OUTLET_CMD_USE_OFF_DELAY = (1ULL << 7), + APC_OUTLET_CMD_TARGET_MAIN = (1ULL << 8), + APC_OUTLET_CMD_TARGET_SWITCHED0 = (1ULL << 9), + APC_OUTLET_CMD_TARGET_SWITCHED1 = (1ULL << 10), + APC_OUTLET_CMD_TARGET_SWITCHED2 = (1ULL << 11), + APC_OUTLET_CMD_SOURCE_USB_PORT = (1ULL << 12), + APC_OUTLET_CMD_SOURCE_LOCAL_USER = (1ULL << 13), + APC_OUTLET_CMD_SOURCE_RJ45_PORT = (1ULL << 14), + APC_OUTLET_CMD_SOURCE_SMART_SLOT_1 = (1ULL << 15), + APC_OUTLET_CMD_SOURCE_SMART_SLOT_2 = (1ULL << 16), + APC_OUTLET_CMD_SOURCE_INTERNAL_NETWORK_1 = (1ULL << 17), + APC_OUTLET_CMD_SOURCE_INTERNAL_NETWORK_2 = (1ULL << 18), + APC_OUTLET_CMD_TARGET_SWITCHED3 = (1ULL << 19) +} apc_outlet_command_bits_t; + +typedef enum apc_ups_command_bits_e { + APC_UPS_CMD_RESTORE_FACTORY_SETTINGS = (1ULL << 3), + APC_UPS_CMD_OUTPUT_INTO_BYPASS = (1ULL << 4), + APC_UPS_CMD_OUTPUT_OUT_OF_BYPASS = (1ULL << 5), + APC_UPS_CMD_CLEAR_FAULTS = (1ULL << 9), + APC_UPS_CMD_RESET_STRINGS = (1ULL << 13), + APC_UPS_CMD_RESET_LOGS = (1ULL << 14), + APC_UPS_CMD_LOCAL_USER = (1ULL << 29), + APC_UPS_CMD_SMART_SLOT_1 = (1ULL << 30) +} apc_ups_command_bits_t; + +typedef enum apc_battery_test_command_bits_e { + APC_BATTERY_TEST_CMD_START = (1ULL << 0), + APC_BATTERY_TEST_CMD_ABORT = (1ULL << 1), + APC_BATTERY_TEST_CMD_LOCAL_USER = (1ULL << 9) +} apc_battery_test_command_bits_t; + +typedef enum apc_runtime_calibration_command_bits_e { + APC_RUNTIME_CAL_CMD_START = (1ULL << 0), + APC_RUNTIME_CAL_CMD_ABORT = (1ULL << 1), + APC_RUNTIME_CAL_CMD_LOCAL_USER = (1ULL << 9) +} apc_runtime_calibration_command_bits_t; + +typedef enum apc_user_interface_command_bits_e { + APC_USER_IF_CMD_SHORT_TEST = (1ULL << 0), + APC_USER_IF_CMD_CONTINUOUS_TEST = (1ULL << 1), + APC_USER_IF_CMD_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS = (1ULL << 2), + APC_USER_IF_CMD_CANCEL_MUTE = (1ULL << 3), + APC_USER_IF_CMD_ACKNOWLEDGE_BATTERY_ALARMS = (1ULL << 5), + APC_USER_IF_CMD_ACKNOWLEDGE_SITE_WIRING_ALARM = (1ULL << 6) +} apc_user_interface_command_bits_t; + +typedef enum apc_outlet_command_type_e { + APC_OUTLET_OP_NULL = 0, + APC_OUTLET_OP_LOAD_OFF, + APC_OUTLET_OP_LOAD_ON, + APC_OUTLET_OP_LOAD_CYCLE, + APC_OUTLET_OP_LOAD_OFF_DELAY, + APC_OUTLET_OP_LOAD_ON_DELAY, + APC_OUTLET_OP_SHUTDOWN_RETURN, + APC_OUTLET_OP_SHUTDOWN_STAYOFF, + APC_OUTLET_OP_SHUTDOWN_REBOOT, + APC_OUTLET_OP_SHUTDOWN_REBOOT_GRACEFUL +} apc_outlet_command_type_t; + +typedef struct apc_outlet_command_suffix_s { + const char *suffix; + apc_outlet_command_type_t type; +} apc_outlet_command_suffix_t; + +extern const apc_value_map_t apc_countdown_map[]; +extern const apc_value_map_t apc_countdown_setting_map[]; +extern const apc_value_map_t apc_status_map[]; +extern const apc_value_map_t apc_alarm_map[]; +extern const apc_value_map_t apc_input_quality_map[]; +extern const apc_value_map_t apc_efficiency_map[]; +extern const apc_value_map_t apc_test_status_map[]; +extern const apc_value_map_t apc_calibration_status_map[]; +extern const apc_outlet_command_suffix_t apc_outlet_command_suffixes[]; + +int apc_format_countdown_value(int64_t value, char *output, size_t output_len); +int apc_format_date_from_days_offset(int64_t value, char *output, size_t output_len); +int apc_parse_date_to_days_offset(const char *value, uint64_t *days_out); +int apc_format_test_status_value(const apc_value_map_t *result_map, const apc_value_map_t *source_map, + const apc_value_map_t *modifier_map, uint64_t value, char *output, size_t output_len); +uint64_t apc_build_outlet_command(apc_outlet_command_type_t type, uint64_t target_bits); +const char *apc_lookup_value_map_text(const apc_value_map_t *map, int value); +const char *apc_lookup_first_set_value_map_text(const apc_value_map_t *map, uint64_t value); + +#endif /* APC_COMMON_H */ diff --git a/drivers/apc_modbus.c b/drivers/apc_modbus.c index 85862a9fbd..42ef2cffb8 100644 --- a/drivers/apc_modbus.c +++ b/drivers/apc_modbus.c @@ -24,7 +24,6 @@ # include "hidparser.h" #endif /* defined NUT_MODBUS_HAS_USB */ -#include "timehead.h" #include "nut_stdint.h" #include "apc_modbus.h" @@ -510,7 +509,7 @@ static apc_modbus_converter_t _apc_modbus_voltage_conversion = { _apc_modbus_vol static int _apc_modbus_efficiency_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - char *cause; + const char *cause; int res; if (value == NULL || output == NULL || output_len == 0) { @@ -522,32 +521,8 @@ static int _apc_modbus_efficiency_to_nut(const apc_modbus_value_t *value, char * return 0; } - switch (value->data.int_value) { - case -1: - cause = "NotAvailable"; - break; - case -2: - cause = "LoadTooLow"; - break; - case -3: - cause = "OutputOff"; - break; - case -4: - cause = "OnBattery"; - break; - case -5: - cause = "InBypass"; - break; - case -6: - cause = "BatteryCharging"; - break; - case -7: - cause = "PoorACInput"; - break; - case -8: - cause = "BatteryDisconnected"; - break; - default: + cause = apc_lookup_value_map_text(apc_efficiency_map, value->data.int_value); + if (cause == NULL) { return _apc_modbus_double_to_nut(value, output, output_len); } @@ -684,45 +659,22 @@ static int _apc_modbus_status_change_cause_to_nut(const apc_modbus_value_t *valu static apc_modbus_converter_t _apc_modbus_status_change_cause_conversion = { _apc_modbus_status_change_cause_to_nut, NULL }; -static int _apc_modbus_string_join(const char *values[], size_t values_len, const char *separator, char *output, size_t output_len) -{ - size_t i; - size_t output_idx; - int res; - - if (values == NULL || values_len == 0 || separator == NULL || output == NULL || output_len == 0) { - /* Invalid parameters */ - return 0; - } - - output_idx = 0; - output[0] = 0; /* Always zero terminate */ - - for (i = 0; i < values_len && output_idx < output_len; i++) { - if (values[i] == NULL) - continue; - - if (i == 0) { - res = snprintf(output + output_idx, output_len - output_idx, "%s", values[i]); - } else { - res = snprintf(output + output_idx, output_len - output_idx, "%s%s", separator, values[i]); - } - - if (res < 0 || (size_t)res >= output_len) { - return 0; - } - - output_idx += res; - } +static const apc_value_map_t apc_modbus_battery_test_source_map[] = { + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_PROTOCOL, "Source: Protocol" }, + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_LOCALUI, "Source: LocalUI" }, + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_INTERNAL, "Source: Internal" }, + { 0, NULL } +}; - return 1; -} +static const apc_value_map_t apc_modbus_battery_test_modifier_map[] = { + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INVALIDSTATE, "Modifier: InvalidState" }, + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INTERNALFAULT, "Modifier: InternalFault" }, + { APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE, "Modifier: StateOfChargeNotAcceptable" }, + { 0, NULL } +}; static int _apc_modbus_battery_test_status_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - const char *result, *source, *modifier; - const char *values[3]; - if (value == NULL || output == NULL || output_len == 0) { /* Invalid parameters */ return 0; @@ -732,52 +684,43 @@ static int _apc_modbus_battery_test_status_to_nut(const apc_modbus_value_t *valu return 0; } - result = NULL; - if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PENDING)) { - result = "Pending"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_INPROGRESS)) { - result = "InProgress"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PASSED)) { - result = "Passed"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_FAILED)) { - result = "Failed"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_REFUSED)) { - result = "Refused"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_ABORTED)) { - result = "Aborted"; - } - - source = NULL; - if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_PROTOCOL)) { - source = "Source: Protocol"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_LOCALUI)) { - source = "Source: LocalUI"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_INTERNAL)) { - source = "Source: Internal"; - } - - modifier = NULL; - if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INVALIDSTATE)) { - modifier = "Modifier: InvalidState"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_INTERNALFAULT)) { - modifier = "Modifier: InternalFault"; - } else if ((value->data.uint_value & APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE)) { - modifier = "Modifier: StateOfChargeNotAcceptable"; - } - - values[0] = result; - values[1] = source; - values[2] = modifier; - return _apc_modbus_string_join(values, SIZEOF_ARRAY(values), ", ", output, output_len); + return apc_format_test_status_value(apc_test_status_map, + apc_modbus_battery_test_source_map, apc_modbus_battery_test_modifier_map, + value->data.uint_value, output, output_len); } static apc_modbus_converter_t _apc_modbus_battery_test_status_conversion = { _apc_modbus_battery_test_status_to_nut, NULL }; +static const apc_value_map_t apc_modbus_runtime_calibration_status_result_map[] = { + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_PENDING, "Pending" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_INPROGRESS, "InProgress" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_PASSED, "Passed" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_FAILED, "Failed" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_REFUSED, "Refused" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_ABORTED, "Aborted" }, + { 0, NULL } +}; + +static const apc_value_map_t apc_modbus_runtime_calibration_status_source_map[] = { + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_PROTOCOL, "Source: Protocol" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_LOCALUI, "Source: LocalUI" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_INTERNAL, "Source: Internal" }, + { 0, NULL } +}; + +static const apc_value_map_t apc_modbus_runtime_calibration_status_modifier_map[] = { + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_INVALIDSTATE, "Modifier: InvalidState" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_INTERNALFAULT, "Modifier: InternalFault" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE, "Modifier: StateOfChargeNotAcceptable" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_LOADCHANGE, "Modifier: LoadChange" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_ACINPUTNOTACCEPTABLE, "Modifier: ACInputNotAcceptable" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_LOADTOOLOW, "Modifier: LoadTooLow" }, + { APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_OVERCHARGEINPROGRESS, "Modifier: OverChargeInProgress" }, + { 0, NULL } +}; + static int _apc_modbus_runtime_calibration_status_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - const char *result, *source, *modifier; - const char *values[3]; - if (value == NULL || output == NULL || output_len == 0) { /* Invalid parameters */ return 0; @@ -787,62 +730,15 @@ static int _apc_modbus_runtime_calibration_status_to_nut(const apc_modbus_value_ return 0; } - result = NULL; - if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_PENDING)) { - result = "Pending"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_INPROGRESS)) { - result = "InProgress"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_PASSED)) { - result = "Passed"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_FAILED)) { - result = "Failed"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_REFUSED)) { - result = "Refused"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_ABORTED)) { - result = "Aborted"; - } - - source = NULL; - if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_PROTOCOL)) { - source = "Source: Protocol"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_LOCALUI)) { - source = "Source: LocalUI"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_SOURCE_INTERNAL)) { - source = "Source: Internal"; - } - - modifier = NULL; - if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_INVALIDSTATE)) { - modifier = "Modifier: InvalidState"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_INTERNALFAULT)) { - modifier = "Modifier: InternalFault"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_STATEOFCHARGENOTACCEPTABLE)) { - modifier = "Modifier: StateOfChargeNotAcceptable"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_LOADCHANGE)) { - modifier = "Modifier: LoadChange"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_ACINPUTNOTACCEPTABLE)) { - modifier = "Modifier: ACInputNotAcceptable"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_LOADTOOLOW)) { - modifier = "Modifier: LoadTooLow"; - } else if ((value->data.uint_value & APC_MODBUS_RUNTIMECALIBRATIONSTATUS_BF_MOD_OVERCHARGEINPROGRESS)) { - modifier = "Modifier: OverChargeInProgress"; - } - - values[0] = result; - values[1] = source; - values[2] = modifier; - return _apc_modbus_string_join(values, SIZEOF_ARRAY(values), ", ", output, output_len); + return apc_format_test_status_value(apc_modbus_runtime_calibration_status_result_map, + apc_modbus_runtime_calibration_status_source_map, apc_modbus_runtime_calibration_status_modifier_map, + value->data.uint_value, output, output_len); } static apc_modbus_converter_t _apc_modbus_runtime_calibration_status_conversion = { _apc_modbus_runtime_calibration_status_to_nut, NULL }; -static const time_t apc_date_start_offset = 946684800; /* 2000-01-01 00:00 */ - static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - struct tm tm_info; - time_t time_stamp; - if (value == NULL || output == NULL || output_len == 0) { /* Invalid parameters */ return 0; @@ -852,17 +748,11 @@ static int _apc_modbus_date_to_nut(const apc_modbus_value_t *value, char *output return 0; } - time_stamp = ((int64_t)value->data.uint_value * 86400) + apc_date_start_offset; - gmtime_r(&time_stamp, &tm_info); - strftime(output, output_len, "%Y-%m-%d", &tm_info); - - return 1; + return apc_format_date_from_days_offset((int64_t)value->data.uint_value, output, output_len); } static int _apc_modbus_date_from_nut(const char *value, uint16_t *output, size_t output_len) { - struct tm tm_struct; - time_t epoch_time; uint64_t uint_value; if (value == NULL || output == NULL || output_len == 0) { @@ -870,17 +760,10 @@ static int _apc_modbus_date_from_nut(const char *value, uint16_t *output, size_t return 0; } - memset(&tm_struct, 0, sizeof(tm_struct)); - if (strptime(value, "%Y-%m-%d", &tm_struct) == NULL) { + if (!apc_parse_date_to_days_offset(value, &uint_value)) { return 0; } - if ((epoch_time = timegm(&tm_struct)) == -1) { - return 0; - } - - uint_value = (epoch_time - apc_date_start_offset) / 86400; - return _apc_modbus_from_uint64(uint_value, output, output_len); } @@ -894,8 +777,6 @@ static apc_modbus_converter_t _apc_modbus_date_conversion = { _apc_modbus_date_t */ static int _apc_modbus_timer_to_nut(const apc_modbus_value_t *value, char *output, size_t output_len) { - int res; - if (value == NULL || output == NULL || output_len == 0) { /* Invalid parameters */ return 0; @@ -905,19 +786,7 @@ static int _apc_modbus_timer_to_nut(const apc_modbus_value_t *value, char *outpu return 0; } - if (value->data.int_value == -1) { - res = snprintf(output, output_len, "NotActive"); - } else if (value->data.int_value == 0) { - res = snprintf(output, output_len, "CountdownExpired"); - } else { - res = snprintf(output, output_len, "%" PRIi64, value->data.int_value); - } - - if (res < 0 || (size_t)res >= output_len) { - return 0; - } - - return 1; + return apc_format_countdown_value(value->data.int_value, output, output_len); } static apc_modbus_converter_t _apc_modbus_timer_conversion = { _apc_modbus_timer_to_nut, NULL }; @@ -1329,101 +1198,6 @@ static apc_modbus_outlet_group_info_t apc_modbus_outlet_group_info[] = { { "SOG2", "Group 3", APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_2_PRESENT, APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2, 0 }, }; -/* Outlet command types for dynamic handling */ -typedef enum { - APC_OC_NULL = 0, - APC_OC_LOAD_OFF, - APC_OC_LOAD_ON, - APC_OC_LOAD_CYCLE, - APC_OC_LOAD_OFF_DELAY, - APC_OC_LOAD_ON_DELAY, - APC_OC_SHUTDOWN_RETURN, - APC_OC_SHUTDOWN_STAYOFF, - APC_OC_SHUTDOWN_REBOOT, - APC_OC_SHUTDOWN_REBOOT_GRACEFUL -} apc_modbus_outlet_cmd_type_t; - -typedef struct { - const char *suffix; - apc_modbus_outlet_cmd_type_t type; -} apc_modbus_outlet_cmd_suffix_t; - -static apc_modbus_outlet_cmd_suffix_t apc_modbus_outlet_cmd_suffixes[] = { - { "load.off", APC_OC_LOAD_OFF }, - { "load.on", APC_OC_LOAD_ON }, - { "load.cycle", APC_OC_LOAD_CYCLE }, - { "load.off.delay", APC_OC_LOAD_OFF_DELAY }, - { "load.on.delay", APC_OC_LOAD_ON_DELAY }, - { "shutdown.return", APC_OC_SHUTDOWN_RETURN }, - { "shutdown.stayoff", APC_OC_SHUTDOWN_STAYOFF }, - { "shutdown.reboot", APC_OC_SHUTDOWN_REBOOT }, - { "shutdown.reboot.graceful", APC_OC_SHUTDOWN_REBOOT_GRACEFUL }, - { NULL, APC_OC_NULL } -}; - -/* Build outlet command value from command type and target bits */ -static uint64_t _apc_modbus_build_outlet_cmd(apc_modbus_outlet_cmd_type_t type, uint64_t target_bits) -{ - uint64_t cmd = target_bits; - - switch (type) { - case APC_OC_LOAD_OFF: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF; - break; - case APC_OC_LOAD_ON: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON; - break; - case APC_OC_LOAD_CYCLE: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT; - break; - case APC_OC_LOAD_OFF_DELAY: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY; - break; - case APC_OC_LOAD_ON_DELAY: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY; - break; - case APC_OC_SHUTDOWN_RETURN: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY; - break; - case APC_OC_SHUTDOWN_STAYOFF: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY; - break; - case APC_OC_SHUTDOWN_REBOOT: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT; - break; - case APC_OC_SHUTDOWN_REBOOT_GRACEFUL: - cmd |= APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT | APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY; - break; -#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) -# pragma GCC diagnostic push -#endif -#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT -# pragma GCC diagnostic ignored "-Wcovered-switch-default" -#endif -#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE -# pragma GCC diagnostic ignored "-Wunreachable-code" -#endif -/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ -#ifdef __clang__ -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wunreachable-code" -# pragma clang diagnostic ignored "-Wcovered-switch-default" -#endif - case APC_OC_NULL: - default: - /* Must not occur. */ - break; -#ifdef __clang__ -# pragma clang diagnostic pop -#endif -#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) -# pragma GCC diagnostic pop -#endif - } - - return cmd; -} - /* Get combined target bits for MOG and all available SOGs (for global commands) */ static uint64_t _apc_modbus_get_all_outlet_targets(void) { @@ -1452,7 +1226,7 @@ static uint64_t _apc_modbus_get_all_outlet_targets(void) static int _apc_modbus_handle_outlet_cmd(const char *nut_cmdname, const char *extra, int *result) { size_t i, group_idx = 0; - apc_modbus_outlet_cmd_type_t cmd_type = APC_OC_LOAD_OFF; + apc_outlet_command_type_t cmd_type = APC_OUTLET_OP_LOAD_OFF; uint64_t target_bits = 0; uint64_t cmd_value; uint16_t value[2]; @@ -1514,9 +1288,9 @@ static int _apc_modbus_handle_outlet_cmd(const char *nut_cmdname, const char *ex } /* Look up command suffix in table */ - for (i = 0; apc_modbus_outlet_cmd_suffixes[i].suffix; i++) { - if (strcmp(suffix, apc_modbus_outlet_cmd_suffixes[i].suffix) == 0) { - cmd_type = apc_modbus_outlet_cmd_suffixes[i].type; + for (i = 0; apc_outlet_command_suffixes[i].suffix; i++) { + if (strcmp(suffix, apc_outlet_command_suffixes[i].suffix) == 0) { + cmd_type = apc_outlet_command_suffixes[i].type; found_suffix = 1; break; } @@ -1527,7 +1301,7 @@ static int _apc_modbus_handle_outlet_cmd(const char *nut_cmdname, const char *ex } /* Build and send the command */ - cmd_value = _apc_modbus_build_outlet_cmd(cmd_type, target_bits); + cmd_value = apc_build_outlet_command(cmd_type, target_bits); if (!_apc_modbus_from_uint64(cmd_value, value, 2)) { upslogx(LOG_ERR, "%s: Failed to convert command value for [%s]", __func__, nut_cmdname); @@ -1576,9 +1350,9 @@ static int _apc_modbus_read_inventory(void) dstate_setinfo(var_name, "%s", apc_modbus_outlet_group_info[i].designator); /* Add all outlet.group commands for available groups */ - for (j = 0; apc_modbus_outlet_cmd_suffixes[j].suffix; j++) { + for (j = 0; apc_outlet_command_suffixes[j].suffix; j++) { snprintf(var_name, sizeof(var_name), "outlet.group.%" PRIuPTR ".%s", - i, apc_modbus_outlet_cmd_suffixes[j].suffix); + i, apc_outlet_command_suffixes[j].suffix); dstate_addcmd(var_name); } } else { diff --git a/drivers/apc_modbus.h b/drivers/apc_modbus.h index 4eb6b42ba6..ff367b40b8 100644 --- a/drivers/apc_modbus.h +++ b/drivers/apc_modbus.h @@ -19,6 +19,8 @@ #ifndef APC_MODBUS_H #define APC_MODBUS_H +#include "apc_common.h" + #define APC_MODBUS_OUTLETSTATUS_BF_STATE_ON (1 << 0) #define APC_MODBUS_OUTLETSTATUS_BF_STATE_OFF (1 << 1) #define APC_MODBUS_OUTLETSTATUS_BF_MOD_PROCESS_REBOOT (1 << 2) @@ -35,12 +37,12 @@ #define APC_MODBUS_OUTLETSTATUS_BF_MOD_LOW_RUNTIME (1 << 14) /* 15 - 31 are reserved */ -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PENDING (1 << 0) -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_INPROGRESS (1 << 1) -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PASSED (1 << 2) -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_FAILED (1 << 3) -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_REFUSED (1 << 4) -#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_ABORTED (1 << 5) +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PENDING APC_TEST_STATUS_PENDING +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_INPROGRESS APC_TEST_STATUS_IN_PROGRESS +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_PASSED APC_TEST_STATUS_PASSED +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_FAILED APC_TEST_STATUS_FAILED +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_REFUSED APC_TEST_STATUS_REFUSED +#define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_ABORTED APC_TEST_STATUS_ABORTED #define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_PROTOCOL (1 << 6) #define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_LOCALUI (1 << 7) #define APC_MODBUS_REPLACEBATTERYTESTSTATUS_BF_SOURCE_INTERNAL (1 << 8) @@ -104,52 +106,49 @@ #define APC_MODBUS_SOGRELAYCONFIGSETTING_BF_SOG_3_PRESENT (1 << 4) #define APC_MODBUS_UPSCOMMAND_BF_REG 1536 -/* 0 - 2 are reserved */ -#define APC_MODBUS_UPSCOMMAND_BF_RESTORE_FACTORY_SETTINGS (1 << 3) -#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_INTO_BYPASS (1 << 4) -#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_OUT_OF_BYPASS (1 << 5) -/* 6 - 8 are reserved */ -#define APC_MODBUS_UPSCOMMAND_BF_CLEAR_FAULTS (1 << 9) -/* 10 - 12 are reserved */ -#define APC_MODBUS_UPSCOMMAND_BF_RESET_STRINGS (1 << 13) -#define APC_MODBUS_UPSCOMMAND_BF_RESET_LOGS (1 << 14) +#define APC_MODBUS_UPSCOMMAND_BF_RESTORE_FACTORY_SETTINGS APC_UPS_CMD_RESTORE_FACTORY_SETTINGS +#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_INTO_BYPASS APC_UPS_CMD_OUTPUT_INTO_BYPASS +#define APC_MODBUS_UPSCOMMAND_BF_OUTPUT_OUT_OF_BYPASS APC_UPS_CMD_OUTPUT_OUT_OF_BYPASS +#define APC_MODBUS_UPSCOMMAND_BF_CLEAR_FAULTS APC_UPS_CMD_CLEAR_FAULTS +#define APC_MODBUS_UPSCOMMAND_BF_RESET_STRINGS APC_UPS_CMD_RESET_STRINGS +#define APC_MODBUS_UPSCOMMAND_BF_RESET_LOGS APC_UPS_CMD_RESET_LOGS #define APC_MODBUS_OUTLETCOMMAND_BF_REG 1538 -#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_CANCEL (1 << 0) -#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON (1 << 1) -#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF (1 << 2) -#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN (1 << 3) -#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT (1 << 4) -#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_COLD_BOOT_ALLOWED (1 << 5) -#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY (1 << 6) -#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY (1 << 7) -#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP (1 << 8) -#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 (1 << 9) -#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 (1 << 10) -#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 (1 << 11) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_USB_PORT (1 << 12) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_LOCAL_USER (1 << 13) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_RJ45_PORT (1 << 14) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_1 (1 << 15) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_2 (1 << 16) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_1 (1 << 17) -#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_2 (1 << 18) +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_CANCEL APC_OUTLET_CMD_CANCEL +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_ON APC_OUTLET_CMD_OUTPUT_ON +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_OFF APC_OUTLET_CMD_OUTPUT_OFF +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_SHUTDOWN APC_OUTLET_CMD_OUTPUT_SHUTDOWN +#define APC_MODBUS_OUTLETCOMMAND_BF_CMD_OUTPUT_REBOOT APC_OUTLET_CMD_OUTPUT_REBOOT +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_COLD_BOOT_ALLOWED APC_OUTLET_CMD_COLD_BOOT_ALLOWED +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_ON_DELAY APC_OUTLET_CMD_USE_ON_DELAY +#define APC_MODBUS_OUTLETCOMMAND_BF_MOD_USE_OFF_DELAY APC_OUTLET_CMD_USE_OFF_DELAY +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_MAIN_OUTLET_GROUP APC_OUTLET_CMD_TARGET_MAIN +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_0 APC_OUTLET_CMD_TARGET_SWITCHED0 +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_1 APC_OUTLET_CMD_TARGET_SWITCHED1 +#define APC_MODBUS_OUTLETCOMMAND_BF_TARGET_SWITCHED_OUTLET_GROUP_2 APC_OUTLET_CMD_TARGET_SWITCHED2 +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_USB_PORT APC_OUTLET_CMD_SOURCE_USB_PORT +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_LOCAL_USER APC_OUTLET_CMD_SOURCE_LOCAL_USER +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_RJ45_PORT APC_OUTLET_CMD_SOURCE_RJ45_PORT +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_1 APC_OUTLET_CMD_SOURCE_SMART_SLOT_1 +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_SMART_SLOT_2 APC_OUTLET_CMD_SOURCE_SMART_SLOT_2 +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_1 APC_OUTLET_CMD_SOURCE_INTERNAL_NETWORK_1 +#define APC_MODBUS_OUTLETCOMMAND_BF_SOURCE_INTERNAL_NETWORK_2 APC_OUTLET_CMD_SOURCE_INTERNAL_NETWORK_2 #define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_REG 1541 -#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_START (1 << 0) -#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_ABORT (1 << 1) +#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_START APC_BATTERY_TEST_CMD_START +#define APC_MODBUS_REPLACEBATTERYTESTCOMMAND_BF_ABORT APC_BATTERY_TEST_CMD_ABORT #define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_REG 1542 -#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_START (1 << 0) -#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_ABORT (1 << 1) +#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_START APC_RUNTIME_CAL_CMD_START +#define APC_MODBUS_RUNTIMECALIBRATIONCOMMAND_BF_ABORT APC_RUNTIME_CAL_CMD_ABORT #define APC_MODBUS_USERINTERFACECOMMAND_BF_REG 1543 -#define APC_MODBUS_USERINTERFACECOMMAND_BF_SHORT_TEST (1 << 0) -#define APC_MODBUS_USERINTERFACECOMMAND_BF_CONTINUOUS_TEST (1 << 1) -#define APC_MODBUS_USERINTERFACECOMMAND_BF_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS (1 << 2) -#define APC_MODBUS_USERINTERFACECOMMAND_BF_CANCEL_MUTE (1 << 3) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_SHORT_TEST APC_USER_IF_CMD_SHORT_TEST +#define APC_MODBUS_USERINTERFACECOMMAND_BF_CONTINUOUS_TEST APC_USER_IF_CMD_CONTINUOUS_TEST +#define APC_MODBUS_USERINTERFACECOMMAND_BF_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS APC_USER_IF_CMD_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS +#define APC_MODBUS_USERINTERFACECOMMAND_BF_CANCEL_MUTE APC_USER_IF_CMD_CANCEL_MUTE /* 4 is reserved */ -#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_BATTERY_ALARMS (1 << 5) -#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_SITE_WIRING_ALARM (1 << 6) +#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_BATTERY_ALARMS APC_USER_IF_CMD_ACKNOWLEDGE_BATTERY_ALARMS +#define APC_MODBUS_USERINTERFACECOMMAND_BF_ACKNOWLEDGE_SITE_WIRING_ALARM APC_USER_IF_CMD_ACKNOWLEDGE_SITE_WIRING_ALARM #endif /* APC_MODBUS_H */ diff --git a/drivers/apcmicrolink-maps.c b/drivers/apcmicrolink-maps.c new file mode 100644 index 0000000000..21a788fd19 --- /dev/null +++ b/drivers/apcmicrolink-maps.c @@ -0,0 +1,281 @@ +/* apcmicrolink-maps.c - APC Microlink descriptor maps + * + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" +#include "main.h" +#include "apc_common.h" + +/* Descriptor names and enumerations were derived from Microlink descriptors + * published by https://ulexplorer-07aa30.gitlab.io/ + */ + +#include "apcmicrolink.h" +#include "apcmicrolink-maps.h" + +static const microlink_value_map_t microlink_outlet_status_map[] = { + { (1U << 0), "StateOn" }, + { (1U << 1), "StateOff" }, + { (1U << 2), "ProcessReboot" }, + { (1U << 3), "ProcessShutdown" }, + { (1U << 4), "ProcessSleep" }, + { (1U << 7), "PendingLoadShed" }, + { (1U << 8), "PendingOnDelay" }, + { (1U << 9), "PendingOffDelay" }, + { (1U << 10), "PendingOnACPresence" }, + { (1U << 11), "PendingOnMinRuntime" }, + { (1U << 12), "MemberGroupProcess1" }, + { (1U << 13), "MemberGroupProcess2" }, + { (1U << 14), "LowRuntime" }, + { 0, NULL } +}; + +const microlink_desc_publish_map_t microlink_desc_publish_map[] = { + { "2:4.A", apc_status_map, apc_alarm_map }, + { "2:13", apc_calibration_status_map, NULL }, + { NULL, NULL, NULL } +}; + +static const microlink_value_map_t outlet_status_change_cause_map[] = { + { (1UL << 0), "SystemInitialization" }, + { (1UL << 1), "UPSStatusChange" }, + { (1UL << 2), "LocalUserCommand" }, + { (1UL << 3), "USBPortCommand" }, + { (1UL << 4), "SmartSlot1Command" }, + { (1UL << 5), "LoadShedCommand" }, + { (1UL << 6), "RJ45PortCommand" }, + { (1UL << 7), "ACInputBad" }, + { (1UL << 8), "UnknownCommand" }, + { (1UL << 9), "ConfigurationChange" }, + { (1UL << 10), "SmartSlot2Command" }, + { (1UL << 11), "InternalNetwork1Command" }, + { (1UL << 12), "InternalNetwork2Command" }, + { (1UL << 13), "LowRuntimeSet" }, + { (1UL << 14), "LowRuntimeClear" }, + { (1UL << 15), "ScheduledCommand" }, + { (1UL << 16), "LoadRebootCommand" }, + { (1UL << 17), "InputContactCommand" }, + { 0, NULL } +}; + +static const microlink_value_map_t retransfer_delay_map[] = { + { 0, "NoDelay" }, + { 0, NULL } +}; + +static const microlink_value_map_t output_voltage_setting_map[] = { + { (1UL << 0), "VAC100" }, + { (1UL << 1), "VAC120" }, + { (1UL << 2), "VAC200" }, + { (1UL << 3), "VAC208" }, + { (1UL << 4), "VAC220" }, + { (1UL << 5), "VAC230" }, + { (1UL << 6), "VAC240" }, + { (1UL << 7), "VAC220_380" }, + { (1UL << 8), "VAC230_400" }, + { (1UL << 9), "VAC240_415" }, + { (1UL << 10), "VAC277_480" }, + { (1UL << 11), "VAC110" }, + { (1UL << 12), "VAC127" }, + { (1UL << 13), "VACAuto120_208or240" }, + { (1UL << 14), "VAC120_208" }, + { (1UL << 15), "VAC120_240" }, + { (1UL << 16), "VAC100_200" }, + { (1UL << 17), "VAC254_440" }, + { (1UL << 18), "VAC115" }, + { (1UL << 19), "VAC125" }, + { 0, NULL } +}; + +static const microlink_value_map_t language_map[] = { + { (1U << 0), "en" }, + { (1U << 1), "fr" }, + { (1U << 2), "it" }, + { (1U << 3), "de" }, + { (1U << 4), "es" }, + { (1U << 5), "pt" }, + { (1U << 6), "ja" }, + { (1U << 7), "ru" }, + { 0, NULL } +}; + +static const microlink_value_map_t battery_test_interval_map[] = { + { (1U << 0), "Never" }, + { (1U << 1), "OnStartUpOnly" }, + { (1U << 2), "OnStartUpPlus7" }, + { (1U << 3), "OnStartUpPlus14" }, + { (1U << 4), "OnStartUp7Since" }, + { (1U << 5), "OnStartUp14Since" }, + { 0, NULL } +}; + +static const microlink_value_map_t battery_lifetime_status_map[] = { + { (1U << 0), "LifeTimeStatusOK" }, + { (1U << 1), "LifeTimeNearEnd" }, + { (1U << 2), "LifeTimeExceeded" }, + { (1U << 3), "LifeTimeNearEndAcknowledged" }, + { (1U << 4), "LifeTimeExceededAcknowledged" }, + { (1U << 5), "MeasuredLifeTimeNearEnd" }, + { (1U << 6), "MeasuredLifeTimeNearEndAcknowledged" }, + { 0, NULL } +}; + +static const microlink_value_map_t ups_status_change_cause_map[] = { + { 0, "SystemInitialization" }, + { 1, "HighInputVoltage" }, + { 2, "LowInputVoltage" }, + { 3, "DistortedInput" }, + { 4, "RapidChangeOfInputVoltage" }, + { 5, "HighInputFrequency" }, + { 6, "LowInputFrequency" }, + { 7, "FreqAndOrPhaseDifference" }, + { 8, "AcceptableInput" }, + { 9, "AutomaticTest" }, + { 10, "TestEnded" }, + { 11, "LocalUICommand" }, + { 12, "ProtocolCommand" }, + { 13, "LowBatteryVoltage" }, + { 14, "GeneralError" }, + { 15, "PowerSystemError" }, + { 16, "BatterySystemError" }, + { 17, "ErrorCleared" }, + { 18, "AutomaticRestart" }, + { 19, "DistortedInverterOutput" }, + { 20, "InverterOutputAcceptable" }, + { 21, "EPOInterface" }, + { 22, "InputPhaseDeltaOutOfRange" }, + { 23, "InputNeutralNotConnected" }, + { 24, "ATSTransfer" }, + { 25, "ConfigurationChange" }, + { 26, "AlertAsserted" }, + { 27, "AlertCleared" }, + { 28, "PlugRatingExceeded" }, + { 29, "OutletGroupStateChange" }, + { 30, "FailureBypassExpired" }, + { 31, "InternalCommand" }, + { 32, "USBCommand" }, + { 33, "SmartSlot1Command" }, + { 34, "InternalNetwork1Command" }, + { 35, "FollowingSystemController" }, + { 0, NULL } +}; + +const microlink_desc_value_map_t microlink_desc_value_map[] = { + /* Inventory / identity */ + { "2:4.9.40", "ups.serial", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.40", "device.serial", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.42", "experimental.device.sku", + MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.44", "ups.model", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.82", "ups.id", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.1B", "ups.productid", MLINK_DESC_HEX, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.1C", "ups.vendorid", MLINK_DESC_HEX, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.9.19", "ups.mfr.date", MLINK_DESC_DATE, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "3:4A", "ups.firmware", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + + /* Status / health */ + { "2:4.A", "experimental.device.status", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_status_map }, + { "2:11", "ups.test.result", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_test_status_map }, + { "2:13", "experimental.ups.calibration.result", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_calibration_status_map }, + { "2:4.5.11", "experimental.battery.test.result", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_test_status_map }, + { "2:4.5.13", "experimental.ups.calibration.result", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_calibration_status_map }, + { "2:B", "input.transfer.reason", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, ups_status_change_cause_map }, + { "2:4.5.18", "ups.test.interval", MLINK_DESC_ENUM_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, battery_test_interval_map }, + { "2:4.5.19", "battery.date.maintenance", + MLINK_DESC_DATE, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.22", "ups.temperature", MLINK_DESC_FIXED_POINT, MLINK_DESC_SIGNED, 7, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.22", "battery.temperature", MLINK_DESC_FIXED_POINT, MLINK_DESC_SIGNED, 7, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.31", "battery.lowruntimewarning", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.42", "experimental.battery.sku", + MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.48", "battery.date", MLINK_DESC_DATE, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.74", "battery.lifetime.status", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, battery_lifetime_status_map }, + { "2:4.5.9F", "battery.runtime", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.20", "battery.charge", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 9, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.5.21", "battery.voltage", MLINK_DESC_FIXED_POINT, MLINK_DESC_SIGNED, 5, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + + /* Input / output */ + { "2:4.6.16", "input.quality", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_input_quality_map }, + { "2:4.6.25", "input.voltage", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 6, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.6.27", "input.frequency", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 7, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.6.BA", "input.transfer.delay", MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, retransfer_delay_map }, + { "2:4.7.D", "input.transfer.high", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.E", "input.transfer.low", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.25", "output.voltage", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 6, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.26", "output.current", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 5, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.27", "output.frequency", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 7, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.28", "ups.realpower", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 8, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.2A", "ups.realpower.nominal", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.2B", "ups.power.nominal", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.2C", "experimental.output.voltage.setting", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, output_voltage_setting_map }, + { "2:4.7.49", "ups.power", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 8, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "3:17", "ups.efficiency", MLINK_DESC_FIXED_POINT_MAP, MLINK_DESC_SIGNED, 7, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, apc_efficiency_map }, + { "3:26", "experimental.output.energy", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + + /* Outlet groups */ + { "2:4.3E.B8", "outlet.group.0.delay.shutdown", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, apc_countdown_setting_map }, + { "2:4.3E.B7", "outlet.group.0.delay.reboot", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, apc_countdown_setting_map }, + { "2:4.3E.B9", "outlet.group.0.delay.start", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, apc_countdown_setting_map }, + { "2:4.3E.30", "outlet.group.0.minimumreturnruntime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.3E.31", "outlet.group.0.lowruntimewarning", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.3E.82", "outlet.group.0.name", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.3E.84", "experimental.outlet.group.0.status.cause", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, outlet_status_change_cause_map }, + { "2:4.3E.B6", "outlet.group.0.status", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, microlink_outlet_status_map }, + { "2:4.3D[%u].2D", "outlet.group.%u.timer.shutdown", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_map }, + { "2:4.3D[%u].B8", "outlet.group.%u.delay.shutdown", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_setting_map }, + { "2:4.3D[%u].2E", "outlet.group.%u.timer.reboot", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_map }, + { "2:4.3D[%u].B7", "outlet.group.%u.delay.reboot", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_setting_map }, + { "2:4.3D[%u].AD", "outlet.group.%u.timer.start", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_map }, + { "2:4.3D[%u].B9", "outlet.group.%u.delay.start", + MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, apc_countdown_setting_map }, + { "2:4.3D[%u].30", "outlet.group.%u.minimumreturnruntime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, NULL }, + { "2:4.3D[%u].31", "outlet.group.%u.lowruntimewarning", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, NULL }, + { "2:4.3D[%u].82", "outlet.group.%u.name", MLINK_DESC_STRING, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_ONE_BASED, NULL }, + { "2:4.3D[%u].84", "experimental.outlet.group.%u.status.cause", + MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_ONE_BASED, outlet_status_change_cause_map }, + { "2:4.3D[%u].B6", "outlet.group.%u.status", MLINK_DESC_BITFIELD_MAP, + MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_ONE_BASED, microlink_outlet_status_map }, + + /* Settings / statistics */ + { "3:2B", "ups.display.language", MLINK_DESC_ENUM_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, language_map }, + { "2:4.5.F.69", "experimental.statistics.battery.totaltime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.6.F.69", "experimental.statistics.input.totaltime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.7.F.69", "experimental.statistics.output.totaltime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "2:4.F.69", "experimental.statistics.ups.totaltime", + MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, +}; + +const size_t microlink_desc_value_map_count = + sizeof(microlink_desc_value_map) / sizeof(microlink_desc_value_map[0]); diff --git a/drivers/apcmicrolink-maps.h b/drivers/apcmicrolink-maps.h new file mode 100644 index 0000000000..9bab2eb610 --- /dev/null +++ b/drivers/apcmicrolink-maps.h @@ -0,0 +1,68 @@ +/* apcmicrolink-maps.h - APC Microlink descriptor maps + * + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef APCMICROLINK_MAPS_H +#define APCMICROLINK_MAPS_H + +#include +#include + +typedef enum microlink_desc_value_type_e { + MLINK_DESC_NONE, + MLINK_DESC_STRING, + MLINK_DESC_HEX, + MLINK_DESC_FIXED_POINT, + MLINK_DESC_FIXED_POINT_MAP, + MLINK_DESC_DATE, + MLINK_DESC_TIME, + MLINK_DESC_BITFIELD_MAP, + MLINK_DESC_ENUM_MAP +} microlink_desc_value_type_t; + +typedef enum microlink_desc_access_e { + MLINK_DESC_RO = 0, + MLINK_DESC_RW = 1 << 0 +} microlink_desc_access_t; + +typedef enum microlink_desc_numeric_sign_e { + MLINK_DESC_UNSIGNED = 0, + MLINK_DESC_SIGNED = 1 +} microlink_desc_numeric_sign_t; + +typedef enum microlink_desc_name_index_e { + MLINK_NAME_INDEX_NONE = 0, + MLINK_NAME_INDEX_ZERO_BASED, + MLINK_NAME_INDEX_ONE_BASED +} microlink_desc_name_index_t; + +typedef apc_value_map_t microlink_value_map_t; + +typedef struct microlink_desc_value_map_s { + const char *path; + const char *upsd_name; + microlink_desc_value_type_t type; + microlink_desc_numeric_sign_t sign; + unsigned int bin_point; + unsigned int access; + microlink_desc_name_index_t name_index; + const microlink_value_map_t *map; +} microlink_desc_value_map_t; + +typedef struct microlink_desc_publish_map_s { + const char *path; + const microlink_value_map_t *status_map; + const microlink_value_map_t *alarm_map; +} microlink_desc_publish_map_t; + +extern const microlink_desc_publish_map_t microlink_desc_publish_map[]; +extern const microlink_desc_value_map_t microlink_desc_value_map[]; +extern const size_t microlink_desc_value_map_count; + +#endif /* APCMICROLINK_MAPS_H */ diff --git a/drivers/apcmicrolink.c b/drivers/apcmicrolink.c new file mode 100644 index 0000000000..d8860ebc6d --- /dev/null +++ b/drivers/apcmicrolink.c @@ -0,0 +1,2877 @@ +/* apcmicrolink.c - APC Microlink protocol driver + * + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" +#include "main.h" + +#include +#include +#include +#include +#include +#include + +#include "serial.h" +#include "nut_stdint.h" + +#include "apc_common.h" +#include "apcmicrolink.h" +#include "apcmicrolink-maps.h" + +#define DRIVER_NAME "APC Microlink protocol driver" +#define DRIVER_VERSION "0.01" + +upsdrv_info_t upsdrv_info = { + DRIVER_NAME, + DRIVER_VERSION, + "Lukas Schmid \n", + DRV_EXPERIMENTAL, + { NULL } +}; + +#define MLINK_DEFAULT_BAUDRATE B9600 +#define MLINK_NEXT_BYTE 0xFE +#define MLINK_INIT_BYTE 0xFD +#define MLINK_HANDSHAKE_RETRIES 3 +#define MLINK_READ_TIMEOUT_USEC 100000 + +#define MLINK_DESC_OP_USAGE_SIZE 0xFC +#define MLINK_DESC_OP_COLLECTION 0xFD +#define MLINK_DESC_OP_CHILD_NEXT 0xFE +#define MLINK_DESC_OP_BLOCK_END 0xFF +#define MLINK_DESC_OP_SKIP_USAGE 0xFB +#define MLINK_DESC_OP_DOUBLE_SKIP 0xFA +#define MLINK_DESC_OP_SKIP_USAGE_ALT 0xF9 +#define MLINK_DESC_OP_ENTER_BLOCK 0xF8 +#define MLINK_DESC_OP_NOOP 0xF7 +#define MLINK_DESC_OP_EXIT_BLOCK 0xF6 +#define MLINK_DESC_OP_SKIP_NEXT 0xF5 +#define MLINK_DESC_OP_RECURSE 0xF4 +#define MLINK_DESC_OP_MIN 0xF4 +#define MLINK_DESC_USAGE_MAX 0xDF + +static const struct { + const char *value; + speed_t speed; +} microlink_speed_table[] = { +#ifdef B1200 + { "1200", B1200 }, +#endif +#ifdef B2400 + { "2400", B2400 }, +#endif +#ifdef B4800 + { "4800", B4800 }, +#endif + { "9600", B9600 }, +#ifdef B19200 + { "19200", B19200 }, +#endif +#ifdef B38400 + { "38400", B38400 }, +#endif +#ifdef B57600 + { "57600", B57600 }, +#endif +#ifdef B115200 + { "115200", B115200 }, +#endif + { NULL, MLINK_DEFAULT_BAUDRATE } +}; + +static microlink_object_t objects[256]; +static speed_t microlink_baudrate = MLINK_DEFAULT_BAUDRATE; +static int session_ready = 0; +static unsigned char rxbuf[MLINK_MAX_FRAME * 2]; +static size_t rxbuf_len = 0; +static unsigned int parsed_frames = 0; +static unsigned int consecutive_timeouts = 0; +static int poll_primed = 0; +static int authentication_sent = 0; +static microlink_page0_state_t page0; +static int descriptor_ready = 0; +static size_t descriptor_usage_count = 0; +static size_t descriptor_blob_len = 0; +static microlink_descriptor_usage_t descriptor_usages[MLINK_DESCRIPTOR_MAX_USAGES]; +static unsigned char descriptor_blob[MLINK_DESCRIPTOR_MAX_BLOB]; +static int show_internals = -1; +static int show_unmapped = -1; +typedef enum microlink_command_source_e { + MLINK_CMD_SOURCE_RJ45 = 0, + MLINK_CMD_SOURCE_USB, + MLINK_CMD_SOURCE_USER, + MLINK_CMD_SOURCE_SMARTSLOT1, + MLINK_CMD_SOURCE_INTERNAL1 +} microlink_command_source_t; + +static microlink_command_source_t microlink_command_source = MLINK_CMD_SOURCE_RJ45; + +typedef enum microlink_command_domain_e { + MLINK_CMD_DOMAIN_OUTLET = 0, + MLINK_CMD_DOMAIN_BATTERY_TEST, + MLINK_CMD_DOMAIN_RUNTIME_CAL, + MLINK_CMD_DOMAIN_UPS +} microlink_command_domain_t; + +static int microlink_send_simple(unsigned char byte); +static int microlink_send_write(unsigned char id, unsigned char offset, + unsigned char len, const unsigned char *data); +static int microlink_update_blob(void); +static int microlink_parse_descriptor(void); +static int microlink_send_descriptor_mask_value(const char *path, uint64_t mask); +static int microlink_send_command_descriptor_mask_value(const char *path, uint64_t mask); +static int microlink_parse_descriptor_string_value(const char *val, size_t size, + unsigned char *payload); +static int microlink_parse_descriptor_fixed_point_value(const microlink_desc_value_map_t *entry, + const char *val, size_t size, unsigned char *payload); +static int microlink_parse_descriptor_hex_value(const char *val, size_t size, + unsigned char *payload); +static int microlink_parse_descriptor_fixed_point_map_value( + const microlink_desc_value_map_t *entry, const char *val, size_t size, + unsigned char *payload); +static int microlink_parse_descriptor_date_value(const char *val, size_t size, + unsigned char *payload); +static int microlink_parse_descriptor_time_value(const char *val, size_t size, + unsigned char *payload); +static int microlink_parse_descriptor_map_value(const microlink_desc_value_map_t *entry, + const char *val, size_t size, unsigned char *payload); +static const unsigned char *microlink_get_descriptor_data(const char *path, size_t size); +static int microlink_receive_once(void); +static const microlink_object_t *microlink_get_object(unsigned int id); +static microlink_object_t *microlink_get_object_mut(unsigned int id); +static size_t microlink_parse_descriptor_block(const unsigned char *blob, size_t blob_len, + size_t pos, size_t *data_offset, const char *path); +static int microlink_set_descriptor_string_info(const char *name, + const unsigned char *data, size_t size); +static int microlink_set_descriptor_hex_info(const char *name, + const unsigned char *data, size_t size); +static int microlink_set_descriptor_fixed_point_info(const char *name, + const unsigned char *data, size_t size, microlink_desc_numeric_sign_t sign, + unsigned int bin_point); +static int microlink_set_descriptor_fixed_point_map_info(const char *name, + const unsigned char *data, size_t size, microlink_desc_numeric_sign_t sign, + unsigned int bin_point, const microlink_value_map_t *map); +static int microlink_set_descriptor_date_info(const char *name, + const unsigned char *data, size_t size); +static int microlink_set_descriptor_time_info(const char *name, + const unsigned char *data, size_t size); +static int microlink_publish_descriptor_entry(const char *name, const char *path, + const microlink_desc_value_map_t *entry); +static size_t microlink_parse_descriptor_collection(const unsigned char *blob, size_t blob_len, + size_t pos, size_t *data_offset, const char *path); + +static int microlink_parse_baudrate(const char *text, speed_t *baudrate) +{ + size_t i; + + if (text == NULL || baudrate == NULL) { + return 0; + } + + for (i = 0; microlink_speed_table[i].value != NULL; i++) { + if (!strcmp(text, microlink_speed_table[i].value)) { + *baudrate = microlink_speed_table[i].speed; + return 1; + } + } + + return 0; +} + +static int microlink_parse_bool(const char *text, int *value) +{ + if (text == NULL || value == NULL) { + return 0; + } + + if (!strcasecmp(text, "true") || !strcasecmp(text, "on") + || !strcasecmp(text, "yes") || !strcmp(text, "1")) { + *value = 1; + return 1; + } + + if (!strcasecmp(text, "false") || !strcasecmp(text, "off") + || !strcasecmp(text, "no") || !strcmp(text, "0")) { + *value = 0; + return 1; + } + + return 0; +} + +static int microlink_show_unmapped(void) +{ + if (show_unmapped >= 0) { + return show_unmapped; + } + + return (nut_debug_level > 0); +} + +static int microlink_show_internals(void) +{ + if (show_internals >= 0) { + return show_internals; + } + + return (nut_debug_level > 0); +} + +static int microlink_parse_command_source(const char *text, microlink_command_source_t *source) +{ + if (text == NULL || source == NULL) { + return 0; + } + + if (!strcasecmp(text, "rj45")) { + *source = MLINK_CMD_SOURCE_RJ45; + return 1; + } + + if (!strcasecmp(text, "usb")) { + *source = MLINK_CMD_SOURCE_USB; + return 1; + } + + if (!strcasecmp(text, "localuser")) { + *source = MLINK_CMD_SOURCE_USER; + return 1; + } + + if (!strcasecmp(text, "smartslot1")) { + *source = MLINK_CMD_SOURCE_SMARTSLOT1; + return 1; + } + + if (!strcasecmp(text, "internalnetwork1")) { + *source = MLINK_CMD_SOURCE_INTERNAL1; + return 1; + } + + return 0; +} + +static uint64_t microlink_command_source_bit(microlink_command_domain_t domain) +{ +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic push +#endif +#pragma GCC diagnostic ignored "-Wswitch-default" +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE_BREAK +#pragma GCC diagnostic ignored "-Wunreachable-code-break" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-default" +#pragma clang diagnostic ignored "-Wunreachable-code" +#endif + switch (domain) { + case MLINK_CMD_DOMAIN_OUTLET: + switch (microlink_command_source) { + case MLINK_CMD_SOURCE_RJ45: + return APC_OUTLET_CMD_SOURCE_RJ45_PORT; + case MLINK_CMD_SOURCE_USB: + return APC_OUTLET_CMD_SOURCE_USB_PORT; + case MLINK_CMD_SOURCE_USER: + return APC_OUTLET_CMD_SOURCE_LOCAL_USER; + case MLINK_CMD_SOURCE_SMARTSLOT1: + return APC_OUTLET_CMD_SOURCE_SMART_SLOT_1; + case MLINK_CMD_SOURCE_INTERNAL1: + return APC_OUTLET_CMD_SOURCE_INTERNAL_NETWORK_1; + } + break; + case MLINK_CMD_DOMAIN_BATTERY_TEST: + case MLINK_CMD_DOMAIN_RUNTIME_CAL: + switch (microlink_command_source) { + case MLINK_CMD_SOURCE_RJ45: + return (1ULL << 10); + case MLINK_CMD_SOURCE_USB: + return (1ULL << 8); + case MLINK_CMD_SOURCE_USER: + return (1ULL << 9); + case MLINK_CMD_SOURCE_SMARTSLOT1: + return (1ULL << 11); + case MLINK_CMD_SOURCE_INTERNAL1: + return (1ULL << 13); + } + break; + case MLINK_CMD_DOMAIN_UPS: + switch (microlink_command_source) { + case MLINK_CMD_SOURCE_RJ45: + return (1ULL << 27); + case MLINK_CMD_SOURCE_USB: + return (1ULL << 28); + case MLINK_CMD_SOURCE_USER: + return (1ULL << 29); + case MLINK_CMD_SOURCE_SMARTSLOT1: + return (1ULL << 30); + case MLINK_CMD_SOURCE_INTERNAL1: + return (1ULL << 31); + } + break; + } + + return 0; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +#pragma GCC diagnostic pop +#endif +} + +static void microlink_read_config(void) +{ + const char *value; + + if (testvar("baudrate")) { + value = getval("baudrate"); + if (!microlink_parse_baudrate(value, µlink_baudrate)) { + fatalx(EXIT_FAILURE, "apcmicrolink: invalid baudrate '%s'", + value ? value : ""); + } + } + + if (testvar("showunmapped")) { + int parsed = 0; + + value = getval("showunmapped"); + if (value == NULL) { + show_unmapped = 1; + } else if (microlink_parse_bool(value, &parsed)) { + show_unmapped = parsed; + } else { + fatalx(EXIT_FAILURE, "apcmicrolink: invalid showunmapped value '%s'", + value); + } + } + + if (testvar("showinternals")) { + int parsed = 0; + + value = getval("showinternals"); + if (value == NULL) { + show_internals = 1; + } else if (microlink_parse_bool(value, &parsed)) { + show_internals = parsed; + } else { + fatalx(EXIT_FAILURE, "apcmicrolink: invalid showinternals value '%s'", + value); + } + } + + if (testvar("cmdsrc")) { + value = getval("cmdsrc"); + if (value == NULL) { + microlink_command_source = MLINK_CMD_SOURCE_RJ45; + } else if (!microlink_parse_command_source(value, µlink_command_source)) { + fatalx(EXIT_FAILURE, "apcmicrolink: invalid cmdsrc value '%s'", + value); + } + } +} + +static int microlink_timeout_expired(const st_tree_timespec_t *start, + time_t d_sec, useconds_t d_usec) +{ + st_tree_timespec_t now; + double timeout = (double)d_sec + ((double)d_usec / 1000000.0); + + state_get_timestamp(&now); + return difftime_st_tree_timespec(now, *start) >= timeout; +} + +static int microlink_prime_poll(void) +{ + if (!microlink_send_simple(MLINK_NEXT_BYTE)) { + ser_comm_fail("microlink: failed to send poll byte"); + poll_primed = 0; + return 0; + } + + poll_primed = 1; + return 1; +} + +static void microlink_trace_frame(int level, const char *label, + const unsigned char *buf, size_t len) +{ + char msg[64]; + + snprintf(msg, sizeof(msg), "microlink %s", label); + upsdebug_hex(level, msg, buf, len); +} + +static void microlink_checksum(const unsigned char *buf, size_t len, + unsigned char *cb0, unsigned char *cb1) +{ + unsigned int c0 = 0; + unsigned int c1 = 0; + size_t i; + + for (i = 0; i < len; i++) { + c0 = (c0 + buf[i]) % 255U; + c1 = (c1 + c0) % 255U; + } + + *cb0 = (unsigned char)(255U - ((c0 + c1) % 255U)); + *cb1 = (unsigned char)(255U - ((c0 + *cb0) % 255U)); +} + +static int microlink_checksum_valid(const unsigned char *frame, size_t len) +{ + unsigned char cb0, cb1; + + if (len < 3) { + return 0; + } + + microlink_checksum(frame, len - 2, &cb0, &cb1); + return (frame[len - 2] == cb0 && frame[len - 1] == cb1); +} + +static void microlink_format_hex(const unsigned char *buf, size_t len, + char *out, size_t outlen) +{ + size_t i; + size_t pos = 0; + + if (outlen == 0) { + return; + } + + out[0] = '\0'; + + for (i = 0; i < len && pos + 3 < outlen; i++) { + pos += snprintf(out + pos, outlen - pos, "%02X", buf[i]); + if (i + 1 < len && pos + 2 < outlen) { + out[pos++] = ' '; + out[pos] = '\0'; + } + } +} + +static void microlink_format_ascii(const unsigned char *buf, size_t len, + char *out, size_t outlen) +{ + size_t i; + size_t pos = 0; + + if (outlen == 0) { + return; + } + + for (i = 0; i < len && pos + 1 < outlen; i++) { + unsigned char ch = buf[i]; + + if (ch == '\0') { + continue; + } + + if (isprint((int)ch)) { + out[pos++] = (char)ch; + } + } + + while (pos > 0 && isspace((unsigned char)out[pos - 1])) { + pos--; + } + + out[pos] = '\0'; +} + +static const microlink_object_t *microlink_get_object(unsigned int id) +{ + return &objects[id & 0xFFU]; +} + +static microlink_object_t *microlink_get_object_mut(unsigned int id) +{ + return &objects[id & 0xFFU]; +} + +static int microlink_is_descriptor_operator(unsigned char token) +{ + return token >= MLINK_DESC_OP_MIN; +} + +static int microlink_path_append(char *buf, size_t buflen, size_t *pos, + const char *fmt, ...) +{ + va_list ap; + int written; + + if (*pos >= buflen) { + return 0; + } + + va_start(ap, fmt); + written = vsnprintf_dynamic(buf + *pos, buflen - *pos, fmt, fmt, ap); + va_end(ap); + + if (written < 0 || (size_t)written >= buflen - *pos) { + return 0; + } + + *pos += (size_t)written; + return 1; +} + +static int microlink_build_usage_path(char *buf, size_t buflen, const char *path, + unsigned char usage_id) +{ + size_t pos = 0; + size_t pathlen = strlen(path); + + buf[0] = '\0'; + + if (!microlink_path_append(buf, buflen, &pos, "%s", path)) { + return 0; + } + + if (pathlen > 0 && path[pathlen - 1] != ':' && path[pathlen - 1] != '.') { + if (!microlink_path_append(buf, buflen, &pos, ".")) { + return 0; + } + } + + return microlink_path_append(buf, buflen, &pos, "%X", usage_id); +} + +static int microlink_build_child_path(char *buf, size_t buflen, const char *path, + unsigned char id, const char *suffix) +{ + size_t pos = 0; + + buf[0] = '\0'; + + return microlink_path_append(buf, buflen, &pos, "%s", path) + && microlink_path_append(buf, buflen, &pos, "%X%s", id, suffix); +} + +static int microlink_build_collection_path(char *buf, size_t buflen, const char *path, + unsigned char collection_id, unsigned int index) +{ + size_t pos = 0; + + buf[0] = '\0'; + + return microlink_path_append(buf, buflen, &pos, "%s", path) + && microlink_path_append(buf, buflen, &pos, "%X[%u].", collection_id, index); +} + +static void microlink_record_descriptor_usage(const char *path, size_t data_offset, + size_t size, int skipped) +{ + microlink_descriptor_usage_t *usage; + + if (descriptor_usage_count >= MLINK_DESCRIPTOR_MAX_USAGES) { + return; + } + + usage = &descriptor_usages[descriptor_usage_count++]; + memset(usage, 0, sizeof(*usage)); + usage->valid = 1; + usage->skipped = skipped; + usage->data_offset = data_offset; + usage->size = size; + snprintf(usage->path, sizeof(usage->path), "%s", path); +} + +static int microlink_match_path_template(const char *templ, const char *path, + unsigned int *index) +{ + const char *slot = strstr(templ, "%u"); + const char *suffix; + char *endptr = NULL; + unsigned long parsed; + size_t prefix_len; + + if (index != NULL) { + *index = 0; + } + + if (slot == NULL) { + return !strcmp(templ, path); + } + + prefix_len = (size_t)(slot - templ); + suffix = slot + 2; + + if (strncmp(templ, path, prefix_len) != 0) { + return 0; + } + + parsed = strtoul(path + prefix_len, &endptr, 10); + if (endptr == path + prefix_len || strcmp(endptr, suffix) != 0) { + return 0; + } + + if (index != NULL) { + *index = (unsigned int)parsed; + } + + return 1; +} + +static void microlink_format_name_template(const char *templ, unsigned int index, + microlink_desc_name_index_t name_index, char *out, size_t outlen) +{ + unsigned int rendered_index = index; + + if (name_index == MLINK_NAME_INDEX_ONE_BASED) { + rendered_index++; + } + + if (strstr(templ, "%u") != NULL) { + snprintf_dynamic(out, outlen, templ, "%u", rendered_index); + } else { + snprintf(out, outlen, "%s", templ); + } +} + +static const microlink_descriptor_usage_t *microlink_find_descriptor_usage(const char *path) +{ + size_t i; + + for (i = 0; i < descriptor_usage_count; i++) { + if (descriptor_usages[i].valid && !strcmp(descriptor_usages[i].path, path)) { + return &descriptor_usages[i]; + } + } + + return NULL; +} + +static const microlink_descriptor_usage_t *microlink_find_descriptor_usage_validated(const char *path) +{ + const microlink_descriptor_usage_t *usage; + + if (!descriptor_ready) { + upsdebugx(1, "descriptor not ready!"); + return NULL; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || + usage->data_offset + usage->size > descriptor_blob_len) { + return NULL; + } + + return usage; +} + +static const microlink_desc_value_map_t *microlink_find_desc_value_by_path(const char *path, + unsigned int *index) +{ + size_t i; + + for (i = 0; i < microlink_desc_value_map_count; i++) { + if (microlink_match_path_template(microlink_desc_value_map[i].path, path, index)) { + return µlink_desc_value_map[i]; + } + } + + return NULL; +} + +static const microlink_desc_value_map_t *microlink_find_desc_value_by_var(const char *varname, + unsigned int *index) +{ + size_t i; + + if (index != NULL) { + *index = 0; + } + + for (i = 0; i < descriptor_usage_count; i++) { + const microlink_descriptor_usage_t *usage = &descriptor_usages[i]; + const microlink_desc_value_map_t *entry; + unsigned int matched_index; + char name[96]; + + if (!usage->valid || usage->skipped) { + continue; + } + + entry = microlink_find_desc_value_by_path(usage->path, &matched_index); + if (entry == NULL || entry->upsd_name == NULL) { + continue; + } + + microlink_format_name_template(entry->upsd_name, matched_index, entry->name_index, + name, sizeof(name)); + if (!strcmp(name, varname)) { + if (index != NULL) { + *index = matched_index; + } + return entry; + } + } + + return NULL; +} + +static size_t microlink_outlet_group_count(void) +{ + size_t group_count = 1; + char path[32]; + size_t i; + + if (microlink_find_descriptor_usage("2:4.3E.B6") == NULL) { + return 0; + } + + for (i = 0; i < 4; i++) { + snprintf(path, sizeof(path), "2:4.3D[%zu].B6", i); + if (microlink_find_descriptor_usage(path) == NULL) { + break; + } + group_count++; + } + + return group_count; +} + +static uint64_t microlink_outlet_target_bits_for_group(size_t group_idx) +{ + switch (group_idx) { + case 0: + return APC_OUTLET_CMD_TARGET_MAIN; + case 1: + return APC_OUTLET_CMD_TARGET_SWITCHED0; + case 2: + return APC_OUTLET_CMD_TARGET_SWITCHED1; + case 3: + return APC_OUTLET_CMD_TARGET_SWITCHED2; + case 4: + return APC_OUTLET_CMD_TARGET_SWITCHED3; + default: + return 0; + } +} + +static uint64_t microlink_outlet_all_targets(size_t group_count) +{ + uint64_t targets = 0; + size_t i; + + for (i = 0; i < group_count; i++) { + targets |= microlink_outlet_target_bits_for_group(i); + } + + return targets; +} + +static int microlink_set_descriptor_string_info(const char *name, + const unsigned char *data, size_t size) +{ + char value[MLINK_MAX_PAYLOAD + 1]; + + if (data == NULL || size == 0 || size > MLINK_MAX_PAYLOAD) { + return 0; + } + + microlink_format_ascii(data, size, value, sizeof(value)); + if (value[0] == '\0') { + return 0; + } + + dstate_setinfo(name, "%s", value); + return 1; +} + +static int microlink_set_descriptor_hex_info(const char *name, + const unsigned char *data, size_t size) +{ + uint64_t raw = 0; + size_t i; + char text[32]; + + if (data == NULL || size == 0) { + return 0; + } + + if (size > sizeof(raw)) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + /* Keep fixed-size identity fields readable and comparable with Modbus. */ + snprintf(text, sizeof(text), "%0*llx", (int)(size * 2), + (unsigned long long)raw); + dstate_setinfo(name, "%s", text); + return 1; +} + +typedef enum microlink_map_mode_e { + MLINK_MAP_BITFIELD, + MLINK_MAP_VALUE +} microlink_map_mode_t; + +static int microlink_set_descriptor_map_info(const char *name, + const unsigned char *data, size_t size, const microlink_value_map_t *map, + microlink_map_mode_t mode) +{ + const char *zero_text = NULL; + uint32_t raw = 0; + int32_t value = 0; + char buf[128]; + size_t i, used = 0; + int matched = 0; + + if (data == NULL || size == 0 || size > sizeof(raw)) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + /* Bitfields concatenate set labels; enums resolve to one label/value. */ + if (mode == MLINK_MAP_BITFIELD) { + buf[0] = '\0'; + + for (i = 0; map[i].text != NULL; i++) { + int ret; + + if (map[i].value == 0) { + zero_text = map[i].text; + continue; + } + + if ((raw & (uint32_t)map[i].value) == 0) { + continue; + } + + matched = 1; + ret = snprintf(buf + used, sizeof(buf) - used, "%s%s", + used ? " " : "", map[i].text); + if (ret < 0) { + return 0; + } + + if ((size_t)ret >= sizeof(buf) - used) { + used = sizeof(buf) - 1; + break; + } + + used += (size_t)ret; + } + + if (used > 0) { + dstate_setinfo(name, "%s", buf); + } else if (!matched && zero_text != NULL) { + dstate_setinfo(name, "%s", zero_text); + } else { + /* Keep the raw value visible when no label matches. */ + snprintf(buf, sizeof(buf), "0x%0*lX", + (int)(size * 2), (unsigned long)raw); + dstate_setinfo(name, "%s", buf); + } + + return 1; + } + + { + /* Only interpret the top bit as sign when the descriptor expects it. */ + uint32_t sign_bit = 1U << ((size * 8U) - 1U); + if (raw & sign_bit) { + uint32_t full_scale = (size >= sizeof(uint32_t)) + ? 0U : (1U << (size * 8U)); + value = (full_scale != 0U) + ? (int32_t)(raw - full_scale) + : (int32_t)raw; + } else { + value = (int32_t)raw; + } + } + + for (i = 0; map[i].text != NULL; i++) { + if (value == map[i].value) { + dstate_setinfo(name, "%s", map[i].text); + return 1; + } + } + + /* Fall back to the numeric value if the map does not know the label. */ + dstate_setinfo(name, "%ld", (long)value); + return 1; +} + +static int microlink_set_descriptor_fixed_point_map_info(const char *name, + const unsigned char *data, size_t size, microlink_desc_numeric_sign_t sign, + unsigned int bin_point, const microlink_value_map_t *map) +{ + uint32_t raw = 0; + int32_t signed_raw = 0; + double value; + char text[64]; + + size_t i; + + if (data == NULL || size == 0) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + if (sign == MLINK_DESC_SIGNED) { + uint32_t sign_bit = 1U << ((size * 8U) - 1U); + if (raw & sign_bit) { + uint32_t full_scale = (size >= sizeof(uint32_t)) + ? 0U : (1U << (size * 8U)); + signed_raw = (full_scale != 0U) + ? (int32_t)(raw - full_scale) + : (int32_t)raw; + } else { + signed_raw = (int32_t)raw; + } + + for (i = 0; map != NULL && map[i].text != NULL; i++) { + if (signed_raw == map[i].value) { + dstate_setinfo(name, "%s", map[i].text); + return 1; + } + } + + value = (double)signed_raw; + } else { + for (i = 0; map != NULL && map[i].text != NULL; i++) { + if ((int32_t)raw == map[i].value) { + dstate_setinfo(name, "%s", map[i].text); + return 1; + } + } + + value = (double)raw; + } + + if (bin_point > 0U) { + value /= (double)(1U << bin_point); + snprintf(text, sizeof(text), "%.6f", value); + for (i = strlen(text); i > 0 && text[i - 1] == '0'; i--) { + text[i - 1] = '\0'; + } + if (i > 0 && text[i - 1] == '.') { + text[i - 1] = '\0'; + } + } else { + if (sign == MLINK_DESC_SIGNED) { + snprintf(text, sizeof(text), "%ld", (long)signed_raw); + } else { + snprintf(text, sizeof(text), "%lu", (unsigned long)raw); + } + } + + dstate_setinfo(name, "%s", text); + return 1; +} + +static int microlink_handle_outlet_cmd(const char *nut_cmdname, const char *extra, int *result) +{ + size_t group_count, group_idx = 0; + uint64_t target_bits = 0; + apc_outlet_command_type_t cmd_type = APC_OUTLET_OP_NULL; + const char *suffix = NULL; + char *endptr = NULL; + size_t i; + + if (nut_cmdname == NULL || result == NULL) { + return 0; + } + + group_count = microlink_outlet_group_count(); + if (group_count == 0) { + return 0; + } + + if (strncmp(nut_cmdname, "load.", 5) == 0 || strncmp(nut_cmdname, "shutdown.", 9) == 0) { + suffix = (strcmp(nut_cmdname, "shutdown.default") == 0) ? "shutdown.return" : nut_cmdname; + target_bits = microlink_outlet_all_targets(group_count); + } else if (strncmp(nut_cmdname, "outlet.group.", 13) == 0) { + const char *p = nut_cmdname + 13; + + group_idx = strtoul(p, &endptr, 10); + if (endptr == p || endptr == NULL || *endptr != '.') { + return 0; + } + + if (group_idx >= group_count) { + upslogx(LOG_ERR, "%s: Invalid outlet group index %zu in command [%s]", + __func__, group_idx, nut_cmdname); + *result = STAT_INSTCMD_INVALID; + return 1; + } + + suffix = endptr + 1; + target_bits = microlink_outlet_target_bits_for_group(group_idx); + if (target_bits == 0) { + upslogx(LOG_ERR, "%s: Outlet group %zu not available for command [%s]", + __func__, group_idx, nut_cmdname); + *result = STAT_INSTCMD_INVALID; + return 1; + } + } else { + return 0; + } + + for (i = 0; apc_outlet_command_suffixes[i].suffix; i++) { + if (strcmp(suffix, apc_outlet_command_suffixes[i].suffix) == 0) { + cmd_type = apc_outlet_command_suffixes[i].type; + break; + } + } + + if (cmd_type == APC_OUTLET_OP_NULL) { + return 0; + } + + upslog_INSTCMD_POWERSTATE_CHECKED(nut_cmdname, extra); + if (!microlink_send_command_descriptor_mask_value("2:4.B5", + apc_build_outlet_command(cmd_type, target_bits) | microlink_command_source_bit(MLINK_CMD_DOMAIN_OUTLET))) { + *result = STAT_INSTCMD_FAILED; + return 1; + } + + *result = STAT_INSTCMD_HANDLED; + return 1; +} + +static int microlink_handle_simple_instcmd(const char *nut_cmdname, const char *extra, int *result) +{ + uint64_t value; + + if (nut_cmdname == NULL || result == NULL) { + return 0; + } + + if (!strcasecmp(nut_cmdname, "test.battery.start")) { + value = APC_BATTERY_TEST_CMD_START; + } else if (!strcasecmp(nut_cmdname, "test.battery.stop")) { + value = APC_BATTERY_TEST_CMD_ABORT; + } else if (!strcasecmp(nut_cmdname, "test.panel.start")) { + value = APC_USER_IF_CMD_SHORT_TEST; + } else if (!strcasecmp(nut_cmdname, "beeper.mute")) { + value = APC_USER_IF_CMD_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS; + } else if (!strcasecmp(nut_cmdname, "calibrate.start")) { + value = APC_RUNTIME_CAL_CMD_START; + } else if (!strcasecmp(nut_cmdname, "calibrate.stop")) { + value = APC_RUNTIME_CAL_CMD_ABORT; + } else if (!strcasecmp(nut_cmdname, "bypass.start")) { + value = APC_UPS_CMD_OUTPUT_INTO_BYPASS; + } else if (!strcasecmp(nut_cmdname, "bypass.stop")) { + value = APC_UPS_CMD_OUTPUT_OUT_OF_BYPASS; + } else { + return 0; + } + + upslog_INSTCMD_POWERSTATE_CHECKED(nut_cmdname, extra); + + if (!strcasecmp(nut_cmdname, "test.battery.start") || !strcasecmp(nut_cmdname, "test.battery.stop")) { + value |= microlink_command_source_bit(MLINK_CMD_DOMAIN_BATTERY_TEST); + *result = microlink_send_command_descriptor_mask_value("2:10", value) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + } else if (!strcasecmp(nut_cmdname, "test.panel.start") || !strcasecmp(nut_cmdname, "beeper.mute")) { + *result = microlink_send_command_descriptor_mask_value("2:4.B.3B", value) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + } else if (!strcasecmp(nut_cmdname, "calibrate.start") || !strcasecmp(nut_cmdname, "calibrate.stop")) { + value |= microlink_command_source_bit(MLINK_CMD_DOMAIN_RUNTIME_CAL); + *result = microlink_send_command_descriptor_mask_value("2:12", value) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + } else { + value |= microlink_command_source_bit(MLINK_CMD_DOMAIN_UPS); + *result = microlink_send_command_descriptor_mask_value("2:14", value) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + } + + return 1; +} + +static int microlink_set_descriptor_fixed_point_info(const char *name, + const unsigned char *data, size_t size, microlink_desc_numeric_sign_t sign, + unsigned int bin_point) +{ + uint32_t raw = 0; + int32_t signed_raw = 0; + double value; + char text[32]; + size_t i; + + if (data == NULL || size == 0) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + if (sign == MLINK_DESC_SIGNED) { + uint32_t sign_bit = 1U << ((size * 8U) - 1U); + if (raw & sign_bit) { + uint32_t full_scale = (size >= sizeof(uint32_t)) + ? 0U : (1U << (size * 8U)); + signed_raw = (full_scale != 0U) + ? (int32_t)(raw - full_scale) + : (int32_t)raw; + } else { + signed_raw = (int32_t)raw; + } + value = (double)signed_raw; + } else { + value = (double)raw; + } + + if (bin_point > 0U) { + value /= (double)(1U << bin_point); + + snprintf(text, sizeof(text), "%.6f", value); + for (i = strlen(text); i > 0 && text[i - 1] == '0'; i--) { + text[i - 1] = '\0'; + } + if (i > 0 && text[i - 1] == '.') { + text[i - 1] = '\0'; + } + } else { + if (sign == MLINK_DESC_SIGNED) { + snprintf(text, sizeof(text), "%ld", (long)signed_raw); + } else { + snprintf(text, sizeof(text), "%lu", (unsigned long)raw); + } + } + + dstate_setinfo(name, "%s", text); + return 1; +} + +static uint64_t microlink_max_unsigned_for_size(size_t size) +{ + if (size >= sizeof(uint64_t)) { + return UINT64_MAX; + } + + return ((uint64_t)1 << (size * 8U)) - 1U; +} + +static int microlink_value_fits_descriptor(int64_t raw, microlink_desc_numeric_sign_t sign, + size_t size) +{ + if (size == 0 || size > 8) { + return 0; + } + + if (sign == MLINK_DESC_SIGNED) { + int64_t min_raw, max_raw; + + if (size >= sizeof(int64_t)) { + min_raw = INT64_MIN; + max_raw = INT64_MAX; + } else { + uint64_t limit = 1ULL << ((size * 8U) - 1U); + + min_raw = -(int64_t)limit; + max_raw = (int64_t)(limit - 1U); + } + + return raw >= min_raw && raw <= max_raw; + } + + return raw >= 0 && (uint64_t)raw <= microlink_max_unsigned_for_size(size); +} + +static int microlink_parse_fixed_point_value(const microlink_desc_value_map_t *entry, + const char *val, int64_t *raw_out) +{ + char *endptr = NULL; + int64_t raw; + + if (entry == NULL || val == NULL || raw_out == NULL) { + return 0; + } + + if (entry->bin_point == 0U) { + long long parsed = strtoll(val, &endptr, 10); + + if (endptr == val || *endptr != '\0') { + return 0; + } + + raw = (int64_t)parsed; + } else { + double numeric = strtod(val, &endptr); + double scaled; + + if (endptr == val || *endptr != '\0') { + return 0; + } + + scaled = numeric * (double)(1U << entry->bin_point); + raw = (int64_t)((scaled >= 0.0) ? (scaled + 0.5) : (scaled - 0.5)); + } + + *raw_out = raw; + return 1; +} + +static int microlink_lookup_value_map(const microlink_value_map_t *map, const char *val, + int64_t *raw_out) +{ + size_t j; + + if (map == NULL || val == NULL || raw_out == NULL) { + return 0; + } + + for (j = 0; map[j].text != NULL; j++) { + if (!strcasecmp(map[j].text, val)) { + *raw_out = map[j].value; + return 1; + } + } + + return 0; +} + +/* Strings are copied into fixed-width payloads and zero-padded if needed. */ +static int microlink_parse_descriptor_string_value(const char *val, size_t size, + unsigned char *payload) +{ + size_t i; + + if (val == NULL || payload == NULL) { + return 0; + } + + memset(payload, 0, size); + for (i = 0; i < size && val[i] != '\0'; i++) { + payload[i] = (unsigned char)val[i]; + } + + return 1; +} + +static int microlink_parse_descriptor_fixed_point_value(const microlink_desc_value_map_t *entry, + const char *val, size_t size, unsigned char *payload) +{ + int64_t raw; + size_t i; + + if (entry == NULL || val == NULL || payload == NULL) { + return 0; + } + + if (size == 0 || size > 8) { + return 0; + } + + if (!microlink_parse_fixed_point_value(entry, val, &raw) + || !microlink_value_fits_descriptor(raw, entry->sign, size)) { + return 0; + } + + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)(((uint64_t)raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_parse_descriptor_hex_value(const char *val, size_t size, + unsigned char *payload) +{ + uint64_t raw; + char *endptr = NULL; + size_t i; + + if (val == NULL || payload == NULL || size == 0 || size > 8) { + return 0; + } + + errno = 0; + raw = strtoull(val, &endptr, 16); + if (endptr == val || *endptr != '\0' || errno > 0 + || raw > microlink_max_unsigned_for_size(size)) { + return 0; + } + + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)((raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_parse_descriptor_fixed_point_map_value( + const microlink_desc_value_map_t *entry, const char *val, size_t size, + unsigned char *payload) +{ + int64_t raw; + size_t i; + + if (entry == NULL || val == NULL || payload == NULL) { + return 0; + } + + if (size == 0 || size > 8) { + return 0; + } + + /* Try the symbolic label first, then fall back to the fixed-point parser. */ + if (!microlink_lookup_value_map(entry->map, val, &raw)) { + if (!microlink_parse_fixed_point_value(entry, val, &raw)) { + return 0; + } + } + + if (!microlink_value_fits_descriptor(raw, entry->sign, size)) { + return 0; + } + + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)(((uint64_t)raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_parse_descriptor_date_value(const char *val, size_t size, + unsigned char *payload) +{ + uint64_t raw; + size_t i; + + if (val == NULL || payload == NULL) { + return 0; + } + + if (size == 0 || size > 8) { + return 0; + } + + if (!apc_parse_date_to_days_offset(val, &raw) + || raw > microlink_max_unsigned_for_size(size)) { + return 0; + } + + /* Date fields are packed as day counts in big-endian byte order. */ + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)((raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_parse_descriptor_time_value(const char *val, size_t size, + unsigned char *payload) +{ + unsigned int hours, minutes, seconds; + uint64_t raw; + size_t i; + + if (val == NULL || payload == NULL) { + return 0; + } + + if (size == 0 || size > 8) { + return 0; + } + + if (sscanf(val, "%u:%u:%u", &hours, &minutes, &seconds) != 3) { + return 0; + } + + if (minutes > 59U || seconds > 59U) { + return 0; + } + + raw = ((uint64_t)hours * 3600U) + ((uint64_t)minutes * 60U) + (uint64_t)seconds; + if (raw > microlink_max_unsigned_for_size(size)) { + return 0; + } + + /* Time fields are packed as elapsed seconds in big-endian byte order. */ + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)((raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_parse_descriptor_map_value(const microlink_desc_value_map_t *entry, + const char *val, size_t size, unsigned char *payload) +{ + int64_t raw = 0; + char *endptr = NULL; + size_t i; + + if (entry == NULL || val == NULL || payload == NULL) { + return 0; + } + + if (!microlink_lookup_value_map(entry->map, val, &raw)) { + if (entry->type == MLINK_DESC_ENUM_MAP) { + raw = (int64_t)strtoll(val, &endptr, 0); + } else { + raw = (int64_t)strtoull(val, &endptr, 0); + } + + if (endptr == val || *endptr != '\0') { + return 0; + } + } + + if (!microlink_value_fits_descriptor(raw, + entry->type == MLINK_DESC_ENUM_MAP ? entry->sign : MLINK_DESC_UNSIGNED, + size)) { + return 0; + } + + /* Enums and bitfields share the same width checks and packing. */ + for (i = 0; i < size; i++) { + size_t shift = (size - 1U - i) * 8U; + payload[i] = (unsigned char)(((uint64_t)raw >> shift) & 0xFFU); + } + + return 1; +} + +static int microlink_publish_descriptor_entry(const char *name, const char *path, + const microlink_desc_value_map_t *entry) +{ + const microlink_descriptor_usage_t *usage; + const unsigned char *data; + size_t size; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped) { + return 0; + } + + size = usage->size; + if (usage->data_offset + size > descriptor_blob_len) { + return 0; + } + + data = microlink_get_descriptor_data(path, size); + if (data == NULL) { + return 0; + } + + /* Keep the per-type export logic centralized but short. */ +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT +# pragma GCC diagnostic ignored "-Wcovered-switch-default" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +# pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wcovered-switch-default" +# pragma clang diagnostic ignored "-Wunreachable-code" +#endif + switch (entry->type) { + case MLINK_DESC_STRING: + return microlink_set_descriptor_string_info(name, data, size); + case MLINK_DESC_HEX: + return microlink_set_descriptor_hex_info(name, data, size); + case MLINK_DESC_FIXED_POINT: + return microlink_set_descriptor_fixed_point_info(name, data, size, + entry->sign, entry->bin_point); + case MLINK_DESC_FIXED_POINT_MAP: + return microlink_set_descriptor_fixed_point_map_info(name, data, size, + entry->sign, entry->bin_point, entry->map); + case MLINK_DESC_DATE: + return microlink_set_descriptor_date_info(name, data, size); + case MLINK_DESC_TIME: + return microlink_set_descriptor_time_info(name, data, size); + case MLINK_DESC_BITFIELD_MAP: + return microlink_set_descriptor_map_info(name, data, size, entry->map, + MLINK_MAP_BITFIELD); + case MLINK_DESC_ENUM_MAP: + return microlink_set_descriptor_map_info(name, data, size, entry->map, + MLINK_MAP_VALUE); + case MLINK_DESC_NONE: + default: + return 0; + } +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic pop +#endif +} + +static size_t microlink_parse_descriptor_collection(const unsigned char *blob, size_t blob_len, + size_t pos, size_t *data_offset, const char *path) +{ + unsigned char collection_id; + unsigned char count; + size_t block_start; + size_t block_end = 0; + unsigned int idx; + + if (pos + 1 >= blob_len) { + return 0; + } + + /* Collections reuse the same payload block for each indexed child entry. */ + collection_id = blob[pos++]; + count = blob[pos++]; + block_start = pos; + + for (idx = 0; idx < count; idx++) { + char child[64]; + size_t sub_pos; + + if (!microlink_build_collection_path(child, sizeof(child), path, + collection_id, idx)) { + return 0; + } + + sub_pos = microlink_parse_descriptor_block(blob, blob_len, block_start, data_offset, child); + if (sub_pos == 0) { + return 0; + } + + block_end = sub_pos; + } + + return block_end; +} + +static int microlink_set_descriptor_date_info(const char *name, + const unsigned char *data, size_t size) +{ + uint64_t raw = 0; + char text[16]; + size_t i; + + if (data == NULL || size == 0 || size > sizeof(raw)) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + return apc_format_date_from_days_offset((int64_t)raw, text, sizeof(text)) && + dstate_setinfo(name, "%s", text); +} + +static int microlink_set_descriptor_time_info(const char *name, + const unsigned char *data, size_t size) +{ + uint64_t raw = 0; + unsigned int hours, minutes, seconds; + char text[16]; + size_t i; + + if (data == NULL || size == 0 || size > sizeof(raw)) { + return 0; + } + + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + hours = (unsigned int)(raw / 3600U); + minutes = (unsigned int)((raw % 3600U) / 60U); + seconds = (unsigned int)(raw % 60U); + snprintf(text, sizeof(text), "%02u:%02u:%02u", hours, minutes, seconds); + dstate_setinfo(name, "%s", text); + return 1; +} + +static int microlink_get_descriptor_map_bits(const char *path, uint32_t *bits) +{ + const microlink_descriptor_usage_t *usage; + const unsigned char *data; + uint32_t raw = 0; + size_t i; + + if (bits == NULL) { + return 0; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || usage->size == 0 + || usage->size > sizeof(raw)) { + return 0; + } + + data = microlink_get_descriptor_data(path, usage->size); + if (data == NULL) { + return 0; + } + + for (i = 0; i < usage->size; i++) { + raw = (raw << 8) | data[i]; + } + + *bits = raw; + return 1; +} + +static int microlink_auth_data_valid(void) +{ + uint32_t bits = 0; + + if (!microlink_get_descriptor_map_bits(MLINK_DESC_AUTH_STATUS, &bits)) { + return 0; + } + + return ((bits & (1U << 0)) != 0); +} + +static int microlink_startup_ready(void) +{ + if (!session_ready || !microlink_get_object(MLINK_OBJ_PROTOCOL)->seen) { + return 0; + } + + if ((page0.flags & (MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT | MLINK_PAGE0_FLAG_AUTH_REQUIRED)) != 0U + && !descriptor_ready) { + return 0; + } + + if ((page0.flags & MLINK_PAGE0_FLAG_AUTH_REQUIRED) == 0U) { + return 1; + } + + if (microlink_auth_data_valid()) { + return 1; + } + + return authentication_sent; +} + +static void microlink_set_alarms_from_descriptor_map(const char *path, + const microlink_value_map_t *map) +{ + uint32_t raw = 0; + size_t i; + int matched = 0; + + if (!microlink_get_descriptor_map_bits(path, &raw)) { + return; + } + + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 0) { + continue; + } + + if ((raw & (uint32_t)map[i].value) != 0) { + if (strcmp(map[i].text, "None") == 0) { + continue; + } + matched = 1; + alarm_set(map[i].text); + } + } + + if (!matched) { + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 0) { + if (strcmp(map[i].text, "None") != 0) { + alarm_set(map[i].text); + } + break; + } + } + } +} + +static void microlink_set_status_from_descriptor_map(const char *path, + const microlink_value_map_t *map) +{ + uint32_t raw = 0; + size_t i; + int matched = 0; + + if (!microlink_get_descriptor_map_bits(path, &raw)) { + return; + } + + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 0) { + continue; + } + + if ((raw & (uint32_t)map[i].value) != 0) { + if (strcmp(map[i].text, "None") == 0) { + continue; + } + matched = 1; + status_set(map[i].text); + } + } + + if (!matched) { + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 0) { + if (strcmp(map[i].text, "None") != 0) { + status_set(map[i].text); + } + break; + } + } + } +} + +static int microlink_send_descriptor_write(const char *path, const unsigned char *payload, + size_t payload_len) +{ + const microlink_descriptor_usage_t *usage; + size_t page; + size_t offset; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || !usage->valid || usage->size == 0 + || payload_len == 0 || payload_len != usage->size || payload_len > MLINK_MAX_PAYLOAD) { + return 0; + } + + if (usage->data_offset + usage->size > descriptor_blob_len || page0.width == 0) { + return 0; + } + + page = usage->data_offset / page0.width; + offset = usage->data_offset % page0.width; + if (page > 0xFFU || offset > 0xFFU) { + return 0; + } + + /* Make the attempted descriptor write visible before the raw frame is sent. */ + upsdebugx(2, "microlink: write %s page=%zu offset=%zu size=%zu", + path, page, offset, usage->size); + + if (!microlink_send_write((unsigned char)page, (unsigned char)offset, + (unsigned char)usage->size, payload)) { + return 0; + } + + memcpy(descriptor_blob + usage->data_offset, payload, usage->size); +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE +# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE +# pragma GCC diagnostic ignored "-Wtautological-type-limit-compare" +#endif +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wtautological-compare" +# pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#endif + if (page <= (size_t)UCHAR_MAX) { + microlink_object_t *obj = microlink_get_object_mut((unsigned int)page); + if (obj->seen && obj->len >= offset + usage->size) { + memcpy(obj->data + offset, payload, usage->size); + } + } +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_CONSTANT_OUT_OF_RANGE_COMPARE) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_TAUTOLOGICAL_TYPE_LIMIT_COMPARE) ) +# pragma GCC diagnostic pop +#endif + + return 1; +} + +static int microlink_send_descriptor_mask_value(const char *path, uint64_t mask) +{ + const microlink_descriptor_usage_t *usage; + unsigned char payload[MLINK_MAX_PAYLOAD]; + size_t i; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || !usage->valid || usage->size == 0 + || usage->size > sizeof(payload) || usage->size > sizeof(mask)) { + return 0; + } + + memset(payload, 0, usage->size); + for (i = 0; i < usage->size; i++) { + size_t shift = (usage->size - 1U - i) * 8U; + payload[i] = (unsigned char)((mask >> shift) & 0xFFU); + } + + return microlink_send_descriptor_write(path, payload, usage->size); +} + +static int microlink_send_command_descriptor_mask_value(const char *path, uint64_t mask) +{ + /* Command writes should happen after the current poll turn has been consumed. */ + if (poll_primed) { + upsdebugx(2, "microlink: draining in-flight poll before command write"); + if (!microlink_receive_once()) { + return 0; + } + poll_primed = 0; + consecutive_timeouts = 0; + } + + return microlink_send_descriptor_mask_value(path, mask); +} + +static int microlink_send_descriptor_typed_value(const microlink_desc_value_map_t *entry, + const char *path, const char *val) +{ + const microlink_descriptor_usage_t *usage; + unsigned char payload[MLINK_MAX_PAYLOAD]; + size_t size = 0; + + if (entry == NULL || path == NULL || val == NULL) { + return 0; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || !usage->valid || usage->size == 0 + || usage->size > sizeof(payload)) { + return 0; + } + + size = usage->size; + +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT +# pragma GCC diagnostic ignored "-Wcovered-switch-default" +#endif +# pragma GCC diagnostic ignored "-Wswitch-enum" +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +# pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wswitch-enum" +# pragma clang diagnostic ignored "-Wunreachable-code" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +#endif + switch (entry->type) { + case MLINK_DESC_STRING: + /* Strings are written as fixed-width payloads. */ + if (!microlink_parse_descriptor_string_value(val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_FIXED_POINT: + /* Fixed-point descriptors use the configured binary scale. */ + if (!microlink_parse_descriptor_fixed_point_value(entry, val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_HEX: + if (!microlink_parse_descriptor_hex_value(val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_FIXED_POINT_MAP: + if (!microlink_parse_descriptor_fixed_point_map_value(entry, val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_DATE: + if (!microlink_parse_descriptor_date_value(val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_TIME: + if (!microlink_parse_descriptor_time_value(val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_ENUM_MAP: + case MLINK_DESC_BITFIELD_MAP: + if (!microlink_parse_descriptor_map_value(entry, val, size, payload)) { + return 0; + } + break; + case MLINK_DESC_NONE: + default: + return 0; + } +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic pop +#endif + + return microlink_send_descriptor_write(path, payload, size); +} + +static int microlink_descriptor_value_is_printable(const unsigned char *buf, size_t len) +{ + size_t i; + + if (len == 0) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!isprint((int)buf[i])) { + return 0; + } + } + + return 1; +} + +static void microlink_publish_descriptor_exports(void) +{ + size_t i; + + if (!descriptor_ready) { + return; + } + + for (i = 0; i < descriptor_usage_count; i++) { + char name[96]; + char value[(MLINK_MAX_PAYLOAD * 3) + 1]; + const unsigned char *data; + const microlink_descriptor_usage_t *usage = &descriptor_usages[i]; + int mapped = 0; + size_t j; + + if (!usage->valid || usage->skipped) { + continue; + } + + if (usage->data_offset + usage->size > descriptor_blob_len) { + continue; + } + + for (j = 0; j < microlink_desc_value_map_count; j++) { + const microlink_desc_value_map_t *entry = µlink_desc_value_map[j]; + unsigned int index = 0; + + if (entry->upsd_name == NULL + || !microlink_match_path_template(entry->path, usage->path, &index)) { + continue; + } + + mapped = 1; + microlink_format_name_template(entry->upsd_name, index, entry->name_index, + name, sizeof(name)); + microlink_publish_descriptor_entry(name, usage->path, entry); + + if (entry->access & MLINK_DESC_RW) { + int flags = ST_FLAG_RW; + + if (entry->type == MLINK_DESC_STRING) { + flags |= ST_FLAG_STRING; + } + + dstate_setflags(name, flags); + if (entry->type == MLINK_DESC_STRING && usage->size > 0 && usage->size < INT_MAX) { + dstate_setaux(name, (int)usage->size); + } + } + } + + if (mapped) { + continue; + } + + if (!microlink_show_unmapped()) { + continue; + } + + data = descriptor_blob + usage->data_offset; + snprintf(name, sizeof(name), "microlink.unmapped.%s", usage->path); + if (microlink_descriptor_value_is_printable(data, usage->size)) { + microlink_format_ascii(data, usage->size, value, sizeof(value)); + } else if (usage->size == 2) { + snprintf(value, sizeof(value), "0x%02X%02X @ %04" PRIxSIZE ":%" PRIuSIZE, + data[0], data[1], usage->data_offset, usage->size); + } else if (usage->size == 4) { + snprintf(value, sizeof(value), "0x%02X%02X%02X%02X @ %04" PRIxSIZE ":%" PRIuSIZE, + data[0], data[1], data[2], data[3], usage->data_offset, usage->size); + } else { + microlink_format_hex(data, usage->size, value, sizeof(value)); + } + dstate_setinfo(name, "%s", value); + } +} + +static size_t microlink_parse_descriptor_usage(const unsigned char *blob, size_t blob_len, + size_t pos, size_t *data_offset, const char *path, unsigned char usage_id, int skipped) +{ + char usage_path[64]; + size_t usage_size = 2; + + if (!microlink_build_usage_path(usage_path, sizeof(usage_path), path, usage_id)) { + return 0; + } + + while (pos < blob_len) { + unsigned char token = blob[pos]; + + if (token == MLINK_DESC_OP_USAGE_SIZE) { + if (pos + 1 >= blob_len) { + return 0; + } + usage_size = blob[pos + 1]; + pos += 2; + continue; + } + + if (token == MLINK_DESC_OP_SKIP_USAGE || token == MLINK_DESC_OP_SKIP_USAGE_ALT) { + pos++; + if (pos + usage_size > blob_len) { + return 0; + } + pos += usage_size; + continue; + } + + if (token == MLINK_DESC_OP_DOUBLE_SKIP) { + pos++; + if (pos + (usage_size * 2U) > blob_len) { + return 0; + } + pos += usage_size * 2U; + continue; + } + + break; + } + + microlink_record_descriptor_usage(usage_path, *data_offset, usage_size, skipped); + *data_offset += usage_size; + return pos; +} + +static size_t microlink_parse_descriptor_block(const unsigned char *blob, size_t blob_len, + size_t pos, size_t *data_offset, const char *path) +{ + int skip_next = 0; + + while (pos < blob_len) { + unsigned char token = blob[pos++]; + + switch (token) { + case MLINK_DESC_OP_RECURSE: + pos = microlink_parse_descriptor_block(blob, blob_len, pos, data_offset, path); + if (pos == 0) { + return 0; + } + break; + case MLINK_DESC_OP_SKIP_NEXT: + skip_next = 1; + break; + case MLINK_DESC_OP_EXIT_BLOCK: + case MLINK_DESC_OP_BLOCK_END: + return pos; + case MLINK_DESC_OP_NOOP: + break; + case MLINK_DESC_OP_ENTER_BLOCK: + { + char child[64]; + if (pos >= blob_len) { + return 0; + } + if (!microlink_build_child_path(child, sizeof(child), "", blob[pos++], ":")) { + return 0; + } + pos = microlink_parse_descriptor_block(blob, blob_len, pos, data_offset, child); + if (pos == 0) { + return 0; + } + break; + } + case MLINK_DESC_OP_CHILD_NEXT: + { + char child[64]; + if (pos >= blob_len) { + return 0; + } + if (!microlink_build_child_path(child, sizeof(child), path, blob[pos++], ".")) { + return 0; + } + pos = microlink_parse_descriptor_block(blob, blob_len, pos, data_offset, child); + if (pos == 0) { + return 0; + } + break; + } + case MLINK_DESC_OP_COLLECTION: + { + pos = microlink_parse_descriptor_collection(blob, blob_len, pos, data_offset, path); + if (pos == 0) { + return 0; + } + break; + } + default: + if (token == 0x00 || microlink_is_descriptor_operator(token) || token > MLINK_DESC_USAGE_MAX) { + return 0; + } + + pos = microlink_parse_descriptor_usage(blob, blob_len, pos, data_offset, path, + token, skip_next); + if (pos == 0) { + return 0; + } + skip_next = 0; + break; + } + } + + return pos; +} + +static int microlink_update_blob(void) +{ + unsigned int row; + + if ((page0.flags & MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT) == 0U + || page0.descriptor_version != 0x01U) { + return 0; + } + + if (page0.width == 0 || page0.count == 0) { + return 0; + } + + descriptor_blob_len = page0.width * page0.count; + if (descriptor_blob_len > sizeof(descriptor_blob)) { + descriptor_blob_len = sizeof(descriptor_blob); + } + memset(descriptor_blob, 0, descriptor_blob_len); + + for (row = 0; row < page0.count; row++) { + const microlink_object_t *obj = microlink_get_object(row); + size_t copylen; + size_t dst; + + dst = ((size_t)row) * page0.width; + if (dst >= descriptor_blob_len) { + break; + } + + if (!obj->seen || obj->len == 0) { + continue; + } + + copylen = obj->len; + if (copylen > page0.width) { + copylen = page0.width; + } + if (dst + copylen > descriptor_blob_len) { + copylen = descriptor_blob_len - dst; + } + + memcpy(descriptor_blob + dst, obj->data, copylen); + } + + return 1; +} + +static int microlink_parse_descriptor(void) +{ + const microlink_object_t *protocol = microlink_get_object(MLINK_OBJ_PROTOCOL); + uint16_t data_ptr; + size_t data_ptr_offset; + size_t data_offset; + + descriptor_ready = 0; + descriptor_usage_count = 0; + descriptor_blob_len = 0; + + if (!protocol->seen || protocol->len < 12) { + return 0; + } + + if (!microlink_update_blob()) { + return 0; + } + + data_ptr = page0.descriptor_ptr; + data_ptr_offset = ((((size_t)data_ptr) >> 8) * page0.width) + (((size_t)data_ptr) & 0xFFU); + if (data_ptr_offset >= descriptor_blob_len || 12 >= descriptor_blob_len) { + return 0; + } + + data_offset = data_ptr_offset; + if (microlink_parse_descriptor_block(descriptor_blob, descriptor_blob_len, 12, &data_offset, "") == 0) { + descriptor_usage_count = 0; + return 0; + } + + descriptor_ready = 1; + return 1; +} + +static void microlink_cache_object(const unsigned char *frame, size_t len) +{ + unsigned int id; + microlink_object_t *obj; + + if (len < 3) { + return; + } + + id = frame[0]; + obj = microlink_get_object_mut(id); + obj->seen = 1; + obj->len = len - 3; + memcpy(obj->data, frame + 1, obj->len); + + if (id == MLINK_OBJ_PROTOCOL && obj->len >= 3) { + page0.version = obj->data[0]; + page0.width = obj->data[1]; + page0.count = obj->data[2]; + page0.series_id = (obj->len >= 5) + ? (uint16_t)(((uint16_t)obj->data[3] << 8) | (uint16_t)obj->data[4]) + : 0; + page0.series_data_version = (obj->len >= 6) ? obj->data[5] : 0; + page0.flags = (obj->len >= 7) ? obj->data[6] : 0; + page0.descriptor_version = (obj->len >= 9) ? obj->data[8] : 0; + page0.descriptor_ptr = (obj->len >= 12) + ? (uint16_t)(((uint16_t)obj->data[10] << 8) | (uint16_t)obj->data[11]) + : 0; + upsdebugx(2, "microlink: page0 version=%u width=%u pages=%u flags=0x%02X", + (unsigned int)page0.version, + (unsigned int)page0.width, + page0.count, + (unsigned int)page0.flags); + } +} + +static int microlink_send_write(unsigned char id, unsigned char offset, + unsigned char len, const unsigned char *data) +{ + unsigned char frame[MLINK_MAX_FRAME]; + unsigned char cb0, cb1; + size_t framelen = 0; + + frame[framelen++] = id; + frame[framelen++] = offset; + frame[framelen++] = len; + memcpy(frame + framelen, data, len); + framelen += len; + microlink_checksum(frame, framelen, &cb0, &cb1); + frame[framelen++] = cb0; + frame[framelen++] = cb1; + microlink_trace_frame(2, "TX write", frame, framelen); + + if (ser_send_buf(upsfd, frame, framelen) != (ssize_t)framelen) { + return 0; + } + + return 1; +} + +static int microlink_send_simple(unsigned char byte) +{ + microlink_trace_frame(2, "TX ctrl", &byte, 1); + return ser_send_buf(upsfd, &byte, 1) == 1; +} + +static int microlink_try_extract_frame_at(unsigned char *frame, size_t *framelen, + const unsigned char *const sourcebuf, const size_t sourcebuf_len) +{ + size_t current_framelen = 0; + + *framelen = 0; + + if (!microlink_get_object(MLINK_OBJ_PROTOCOL)->seen) { + /* If page0 not already seen, get page0.width manually */ + if (sourcebuf_len < 3) { + return 0; + } + + if (sourcebuf[0] != 0x00) { + return 0; + } + + current_framelen = sourcebuf[2] + 3; + } else { + /* Else, use page0 */ + current_framelen = page0.width + 3; + } + + if (sourcebuf_len < current_framelen) { + return 0; + } + + if (current_framelen > MLINK_MAX_FRAME) { + return 0; + } + + if (!microlink_checksum_valid(sourcebuf, current_framelen)) { + return 0; + } + + memcpy(frame, sourcebuf, current_framelen); + *framelen = current_framelen; + + return 1; +} + +static int microlink_try_extract_frame(unsigned char *frame, size_t *framelen) +{ + size_t start; + + *framelen = 0; + + while (rxbuf_len > 0 && rxbuf[0] == MLINK_NEXT_BYTE) { + memmove(rxbuf, rxbuf + 1, --rxbuf_len); + } + + for (start = 0; start < rxbuf_len; start++) { + if (rxbuf[start] == MLINK_NEXT_BYTE) { + continue; + } + + if (microlink_try_extract_frame_at(frame, framelen, rxbuf + start, rxbuf_len - start)) { + if (start > 0) { + upsdebugx(2, "microlink: skipped %u stray byte(s) before record 0x%02X", + (unsigned int)start, rxbuf[start]); + memmove(rxbuf, rxbuf + start, rxbuf_len - start); + rxbuf_len -= start; + } + + memmove(rxbuf, rxbuf + *framelen, rxbuf_len - *framelen); + rxbuf_len -= *framelen; + microlink_trace_frame(2, "RX record", frame, *framelen); + return 1; + } + } + + if (rxbuf_len >= sizeof(rxbuf)) { + upsdebugx(1, "microlink: dropping %u bytes while resynchronizing", + (unsigned int)(rxbuf_len - (MLINK_RECORD_LEN - 1))); + memmove(rxbuf, rxbuf + (rxbuf_len - (MLINK_RECORD_LEN - 1)), MLINK_RECORD_LEN - 1); + rxbuf_len = MLINK_RECORD_LEN - 1; + } + + return 0; +} + +static const unsigned char *microlink_get_descriptor_data(const char *path, size_t size) +{ + const microlink_descriptor_usage_t *usage; + + if (!descriptor_ready) { + upsdebugx(1, "descriptor not ready!"); + return NULL; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || usage->size != size || + usage->data_offset + usage->size > descriptor_blob_len) { + return NULL; + } + + return descriptor_blob + usage->data_offset; +} + +static void microlink_auth_update(unsigned char *s0, unsigned char *s1, + const unsigned char *data, size_t len) +{ + size_t i; + + for (i = 0; i < len; i++) { + *s0 = (unsigned char)((*s0 + data[i]) % 255U); + *s1 = (unsigned char)((*s1 + *s0) % 255U); + } +} + +static int microlink_authenticate(void) +{ + const microlink_object_t *protocol = microlink_get_object(MLINK_OBJ_PROTOCOL); + const microlink_descriptor_usage_t *serial_usage = NULL; + const unsigned char *master_password; + unsigned char s0, s1; + unsigned char payload[4]; + + if (!protocol->seen || protocol->len < 8) { + upsdebugx(1, "microlink: authentication requested before protocol header was cached"); + return 0; + } + + serial_usage = microlink_find_descriptor_usage_validated(MLINK_DESC_SERIALNUMBER); + master_password = microlink_get_descriptor_data(MLINK_DESC_MASTER_PASSWORD, 4); + + if (serial_usage == NULL || master_password == NULL) { + upsdebugx(1, "microlink: authentication requested before required descriptors were cached"); + return 0; + } + + s0 = protocol->data[4]; + s1 = protocol->data[3]; + + microlink_auth_update(&s0, &s1, protocol->data, 8); + microlink_auth_update(&s0, &s1, descriptor_blob + serial_usage->data_offset, serial_usage->size); + microlink_auth_update(&s0, &s1, master_password, 2); + + payload[0] = 0x00; + payload[1] = 0x00; + payload[2] = s0; + payload[3] = s1; + + upsdebugx(2, "microlink: sending slave password %02X %02X", + payload[2], payload[3]); + + return microlink_send_descriptor_write( + MLINK_DESC_SLAVE_PASSWORD, + payload, + sizeof(payload) + ); +} + +static int microlink_process_frame(const unsigned char *frame, size_t framelen) +{ + if (!microlink_checksum_valid(frame, framelen)) { + ser_comm_fail("microlink: checksum failure on object 0x%02X", frame[0]); + return 0; + } + + parsed_frames++; + microlink_cache_object(frame, framelen); + + if (page0.count > 0 && frame[0] == (unsigned char)(page0.count - 1U)) { + if ((page0.flags & MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT) != 0U) { + if (descriptor_ready) { + microlink_update_blob(); + } else { + microlink_parse_descriptor(); + } + } + + if ((page0.flags & MLINK_PAGE0_FLAG_AUTH_REQUIRED) != 0U + && descriptor_ready && !authentication_sent) { + if (!microlink_authenticate()) { + ser_comm_fail("microlink: failed to authenticate"); + return 0; + } + authentication_sent = 1; + } + } + + return 1; +} + +static int microlink_receive_once(void) +{ + unsigned char frame[MLINK_MAX_FRAME]; + size_t framelen = 0; + st_tree_timespec_t start; + + state_get_timestamp(&start); + + for (;;) { + unsigned char ch; + ssize_t ret; + + if (microlink_try_extract_frame(frame, &framelen)) { + return microlink_process_frame(frame, framelen); + } + + ret = ser_get_char(upsfd, &ch, 0, MLINK_READ_TIMEOUT_USEC); + if (ret < 0) { + return 0; + } + + if (ret == 0) { + if (microlink_timeout_expired(&start, 0, MLINK_READ_TIMEOUT_USEC)) { + return 0; + } + continue; + } + + if (rxbuf_len < sizeof(rxbuf)) { + rxbuf[rxbuf_len++] = ch; + upsdebug_hex(5, "microlink RX byte", &ch, 1); + } else { + upsdebugx(1, "microlink: receive buffer overflow, resetting parser"); + rxbuf_len = 0; + } + } +} + +static int microlink_poll_once(void) +{ + if (!poll_primed) { + if (!microlink_prime_poll()) { + return 0; + } + } + + if (microlink_receive_once()) { + consecutive_timeouts = 0; + poll_primed = 0; + return 1; + } + + poll_primed = 0; + consecutive_timeouts++; + return 0; +} + +static int microlink_start_session(void) +{ + unsigned int attempt; + + rxbuf_len = 0; + poll_primed = 0; + authentication_sent = 0; + memset(&page0, 0, sizeof(page0)); + descriptor_ready = 0; + descriptor_usage_count = 0; + descriptor_blob_len = 0; + ser_flush_io(upsfd); + + for (attempt = 0; attempt < MLINK_HANDSHAKE_RETRIES; attempt++) { + if (!microlink_send_simple(MLINK_INIT_BYTE)) { + return 0; + } + + if (microlink_receive_once()) { + consecutive_timeouts = 0; + session_ready = 1; + return microlink_prime_poll(); + } + } + + return 0; +} + +static int microlink_reconnect_session(void) +{ + upsdebugx(1, "microlink: reconnecting session after %u consecutive timeouts", + consecutive_timeouts); + session_ready = 0; + return microlink_start_session(); +} + +static void microlink_publish_identity(void) +{ + dstate_setinfo("ups.mfr", "APC"); + dstate_setinfo("device.mfr", "APC"); + dstate_setinfo("device.type", "ups"); + microlink_publish_descriptor_exports(); +} + +static void microlink_publish_status(void) +{ + size_t i; + + status_init(); + alarm_init(); + + for (i = 0; microlink_desc_publish_map[i].path != NULL; i++) { + if (microlink_desc_publish_map[i].status_map != NULL) { + microlink_set_status_from_descriptor_map( + microlink_desc_publish_map[i].path, + microlink_desc_publish_map[i].status_map); + } + if (microlink_desc_publish_map[i].alarm_map != NULL) { + microlink_set_alarms_from_descriptor_map( + microlink_desc_publish_map[i].path, + microlink_desc_publish_map[i].alarm_map); + } + } + + status_commit(); + alarm_commit(); +} + +static void microlink_publish_runtime(void) +{ + uint16_t descriptor_ptr = 0; + size_t descriptor_data_offset = 0; + char hex[16]; + char flags[16]; + const microlink_object_t *protocol = microlink_get_object(MLINK_OBJ_PROTOCOL); + + if (!microlink_show_internals()) { + return; + } + + if (protocol->seen && protocol->len >= 7) { + dstate_setinfo("microlink.version", "%u", (unsigned int)page0.version); + dstate_setinfo("microlink.series.id", "%u", (unsigned int)page0.series_id); + dstate_setinfo("microlink.series.data.version", "%u", + (unsigned int)page0.series_data_version); + snprintf(flags, sizeof(flags), "0x%02X", page0.flags); + dstate_setinfo("microlink.flags", "%s", flags); + dstate_setinfo("microlink.flag.auth_required", "%u", + (unsigned int)((page0.flags & MLINK_PAGE0_FLAG_AUTH_REQUIRED) != 0U)); + dstate_setinfo("microlink.flag.implicit_stuffing", "%u", + (unsigned int)((page0.flags & MLINK_PAGE0_FLAG_IMPLICIT_STUFFING) != 0U)); + dstate_setinfo("microlink.flag.descriptor_present", "%u", + (unsigned int)((page0.flags & MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT) != 0U)); + dstate_setinfo("microlink.flag.firmware_update_needed", "%u", + (unsigned int)((page0.flags & MLINK_PAGE0_FLAG_FIRMWARE_UPDATE_NEEDED) != 0U)); + } + + if (protocol->seen && protocol->len >= 12 + && (page0.flags & MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT) != 0U) { + dstate_setinfo("microlink.descriptor.version", "%u", + (unsigned int)page0.descriptor_version); + descriptor_ptr = page0.descriptor_ptr; + descriptor_data_offset = ((((size_t)descriptor_ptr) >> 8) * page0.width) + + (((size_t)descriptor_ptr) & 0xFFU); + dstate_setinfo("microlink.descriptor.table_offset", "%u", 12U); + snprintf(hex, sizeof(hex), "0x%04X", descriptor_ptr); + dstate_setinfo("microlink.descriptor.pointer", "%s", hex); + dstate_setinfo("microlink.descriptor.data_offset", "%u", + (unsigned int)descriptor_data_offset); + } + + dstate_setinfo("microlink.session", "%s", session_ready ? "ready" : "syncing"); + dstate_setinfo("microlink.timeouts", "%u", consecutive_timeouts); + dstate_setinfo("microlink.rxbuf", "%u", (unsigned int)rxbuf_len); + dstate_setinfo("microlink.page.width", "%u", (unsigned int)page0.width); + dstate_setinfo("microlink.page.count", "%u", page0.count); + dstate_setinfo("microlink.descriptor.ready", "%u", (unsigned int)descriptor_ready); + dstate_setinfo("microlink.descriptor.usages", "%u", (unsigned int)descriptor_usage_count); +} + +static int setvar(const char *varname, const char *val) +{ + const microlink_desc_value_map_t *entry; + unsigned int index = 0; + char path[64]; + + upsdebug_SET_STARTING(varname, val); + + entry = microlink_find_desc_value_by_var(varname, &index); + if (entry != NULL && (entry->access & MLINK_DESC_RW)) { + microlink_format_name_template(entry->path, index, + MLINK_NAME_INDEX_ZERO_BASED, path, sizeof(path)); + upsdebugx(2, "microlink: setvar %s -> %s via %s", varname, val, path); + if (microlink_send_descriptor_typed_value(entry, path, val)) { + microlink_publish_identity(); + microlink_publish_runtime(); + return STAT_SET_HANDLED; + } + return STAT_SET_FAILED; + } + + upslog_SET_UNKNOWN(varname, val); + return STAT_SET_UNKNOWN; +} + +static int instcmd(const char *cmdname, const char *extra) +{ + int ret = STAT_INSTCMD_INVALID; + + upsdebug_INSTCMD_STARTING(cmdname, extra); + + if (microlink_handle_outlet_cmd(cmdname, extra, &ret)) { + upslog_INSTCMD_RESULT(ret, cmdname, extra); + return ret; + } + + if (microlink_handle_simple_instcmd(cmdname, extra, &ret)) { + upslog_INSTCMD_RESULT(ret, cmdname, extra); + return ret; + } + + upslog_INSTCMD_UNKNOWN(cmdname, extra); + return STAT_INSTCMD_UNKNOWN; +} + +void upsdrv_initups(void) +{ + microlink_read_config(); + upsfd = ser_open(device_path); + ser_set_speed(upsfd, device_path, microlink_baudrate); + ser_set_dtr(upsfd, 1); +} + +void upsdrv_initinfo(void) +{ + int i; + size_t outlet_group_count, g; + static const char *const outlet_suffixes[] = { + "load.off", + "load.on", + "load.cycle", + "load.off.delay", + "load.on.delay", + "shutdown.default", + "shutdown.return", + "shutdown.stayoff", + "shutdown.reboot", + "shutdown.reboot.graceful", + NULL + }; + + memset(objects, 0, sizeof(objects)); + session_ready = 0; + rxbuf_len = 0; + parsed_frames = 0; + consecutive_timeouts = 0; + poll_primed = 0; + authentication_sent = 0; + memset(&page0, 0, sizeof(page0)); + descriptor_ready = 0; + poll_interval = 0; + if (!microlink_start_session()) { + fatalx(EXIT_FAILURE, "apcmicrolink: failed to start Microlink session on %s", device_path); + } + + while (!microlink_startup_ready()) { + if (!microlink_poll_once() && consecutive_timeouts >= MLINK_HANDSHAKE_RETRIES) { + fatalx(EXIT_FAILURE, + "apcmicrolink: timed out waiting for Microlink startup readiness on %s", + device_path); + } + } + + microlink_publish_identity(); + microlink_publish_status(); + microlink_publish_runtime(); + + dstate_addcmd("test.battery.start"); + dstate_addcmd("test.battery.stop"); + dstate_addcmd("test.panel.start"); + dstate_addcmd("beeper.mute"); + dstate_addcmd("calibrate.start"); + dstate_addcmd("calibrate.stop"); + dstate_addcmd("bypass.start"); + dstate_addcmd("bypass.stop"); + + outlet_group_count = microlink_outlet_group_count(); + if (outlet_group_count > 0) { + char cmd[64]; + + dstate_setinfo("outlet.group.count", "%u", (unsigned int)outlet_group_count); + + dstate_addcmd("load.off"); + dstate_addcmd("load.on"); + dstate_addcmd("load.cycle"); + dstate_addcmd("load.off.delay"); + dstate_addcmd("load.on.delay"); + dstate_addcmd("shutdown.default"); + dstate_addcmd("shutdown.return"); + dstate_addcmd("shutdown.stayoff"); + dstate_addcmd("shutdown.reboot"); + dstate_addcmd("shutdown.reboot.graceful"); + + for (g = 0; g < outlet_group_count; g++) { + for (i = 0; outlet_suffixes[i] != NULL; i++) { + snprintf(cmd, sizeof(cmd), "outlet.group.%zu.%s", g, outlet_suffixes[i]); + dstate_addcmd(cmd); + } + } + } + upsh.instcmd = instcmd; + upsh.setvar = setvar; +} + +void upsdrv_updateinfo(void) +{ + int good = 0; + + if (!session_ready && !microlink_start_session()) { + dstate_datastale(); + return; + } + + if (microlink_poll_once()) { + good = 1; + } + + if (!good && consecutive_timeouts >= MLINK_HANDSHAKE_RETRIES) { + if (!microlink_reconnect_session()) { + dstate_datastale(); + return; + } + good = 1; + } + + if (!good) { + if (parsed_frames == 0) { + session_ready = 0; + dstate_datastale(); + return; + } + + microlink_publish_identity(); + microlink_publish_status(); + microlink_publish_runtime(); + dstate_dataok(); + return; + } + + ser_comm_good(); + microlink_publish_identity(); + microlink_publish_status(); + microlink_publish_runtime(); + dstate_dataok(); +} + +void upsdrv_shutdown(void) +{ + int ret; + + ret = instcmd("shutdown.return", NULL); + if (ret != STAT_INSTCMD_HANDLED) { + upslogx(LOG_ERR, "apcmicrolink: failed to issue shutdown.return"); + set_exit_flag(EF_EXIT_FAILURE); + } +} + +void upsdrv_makevartable(void) +{ + addvar(VAR_VALUE, "baudrate", "Serial port baud rate (e.g. 9600, 19200, 38400)"); + addvar(VAR_VALUE, "showinternals", + "Show Microlink internal runtime values (yes/no, default follows debug mode)"); + addvar(VAR_VALUE, "showunmapped", + "Show unmapped Microlink descriptor values (yes/no, default follows debug mode)"); + addvar(VAR_VALUE, "cmdsrc", + "Microlink command source: rj45, usb, localuser, smartslot1, internalnetwork1 (default: rj45)"); +} + +void upsdrv_help(void) +{ +} + +void upsdrv_tweak_prognames(void) +{ +} + +void upsdrv_cleanup(void) +{ + if (VALID_FD(upsfd)) { + ser_close(upsfd, device_path); + upsfd = ERROR_FD; + } +} diff --git a/drivers/apcmicrolink.h b/drivers/apcmicrolink.h new file mode 100644 index 0000000000..0481eb46b4 --- /dev/null +++ b/drivers/apcmicrolink.h @@ -0,0 +1,60 @@ +/* apcmicrolink.h - APC Microlink protocol driver definitions + * + * Copyright (C) 2026 Lukas Schmid + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#ifndef APCMICROLINK_H +#define APCMICROLINK_H + +#include +#include + +#define MLINK_MAX_FRAME 256 +#define MLINK_MAX_PAYLOAD (MLINK_MAX_FRAME - 3) +#define MLINK_RECORD_LEN (MLINK_MAX_FRAME) +#define MLINK_DESCRIPTOR_MAX_BLOB (256 * MLINK_MAX_PAYLOAD) +#define MLINK_DESCRIPTOR_MAX_USAGES 1024 + +#define MLINK_OBJ_PROTOCOL 0x00 + +#define MLINK_DESC_SLAVE_PASSWORD "2:4.8.5" +#define MLINK_DESC_MASTER_PASSWORD "2:4.8.6" +#define MLINK_DESC_AUTH_STATUS "2:4.8.9" +#define MLINK_DESC_SERIALNUMBER "2:4.9.40" + +#define MLINK_PAGE0_FLAG_AUTH_REQUIRED (1U << 0) +#define MLINK_PAGE0_FLAG_IMPLICIT_STUFFING (1U << 1) +#define MLINK_PAGE0_FLAG_DESCRIPTOR_PRESENT (1U << 3) +#define MLINK_PAGE0_FLAG_FIRMWARE_UPDATE_NEEDED (1U << 4) + +typedef struct microlink_object_s { + int seen; + size_t len; + unsigned char data[MLINK_MAX_PAYLOAD]; +} microlink_object_t; + +typedef struct microlink_descriptor_usage_s { + int valid; + int skipped; + char path[64]; + size_t data_offset; + size_t size; +} microlink_descriptor_usage_t; + +typedef struct microlink_page0_state_s { + size_t width; + unsigned int count; + unsigned char version; + unsigned char series_data_version; + unsigned char descriptor_version; + unsigned char flags; + uint16_t series_id; + uint16_t descriptor_ptr; +} microlink_page0_state_t; + +#endif /* APCMICROLINK_H */