diff --git a/NEWS.adoc b/NEWS.adoc index 4dd5544251..566a8ca197 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -42,6 +42,10 @@ https://github.com/networkupstools/nut/milestone/13 - (expected) CI automation for use of data points in drivers that conform to patterns defined in link:docs/nut-names.txt[] + - 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] + - Second-level bullet points listed in this file will now use 4 spaces (not 3 like before) for easier initial indentation of new entries. 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 be249f6819..3486af5c14 100644 --- a/docs/man/Makefile.am +++ b/docs/man/Makefile.am @@ -976,6 +976,7 @@ endif # (--with-serial) SRC_SERIAL_PAGES = \ al175.txt \ + apcmicrolink.txt \ apcsmart.txt \ apcsmart-old.txt \ bcmxcp.txt \ @@ -1030,6 +1031,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) \ @@ -1148,6 +1150,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..83f9b40233 --- /dev/null +++ b/docs/man/apcmicrolink.txt @@ -0,0 +1,131 @@ +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. + + +IMPLEMENTED FEATURES +-------------------- + +The driver publishes standard identity, status, runtime and outlet-group data +when these objects are present in the Microlink descriptor. + +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` +* `battery.testinterval` +* `outlet.group.N.oncountdown` +* `outlet.group.N.offcountdown` +* `outlet.group.N.stayoffcountdown` +* `outlet.group.N.minimumreturnruntime` +* `outlet.group.N.lowruntimewarning` + +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 9ff5485410..ea09347522 100644 --- a/docs/nut.dict +++ b/docs/nut.dict @@ -1,4 +1,4 @@ -personal_ws-1.1 en 3734 utf-8 +personal_ws-1.1 en 3741 utf-8 AAC AAS ABI @@ -705,6 +705,7 @@ LogMin LowBatt Loyer Luca +Lukas Luxeon Lygre Lynge @@ -1113,6 +1114,7 @@ RISC RK RMCARD RMCPplus +RMI RMXL RNF RNG @@ -1265,6 +1267,7 @@ Salvia Santinoli Savia Sawatzky +Schmid Schmier Schoch Schonefeld @@ -1640,6 +1643,7 @@ apc apcc apcd apcevilhack +apcmicrolink apcsmart apctest apcupsd @@ -3209,6 +3213,8 @@ sha shellcheck shellenv shm +showinternals +showunmapped shutdownArguments shutdowncmd shutdowndelay @@ -3397,6 +3403,7 @@ tempmax tempmin termios testime +testinterval testtime testuser testvar diff --git a/drivers/Makefile.am b/drivers/Makefile.am index 0fdb8666c6..560d5766ea 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 apcsmart_SOURCES = apcsmart.c apcsmart_tabs.c apcsmart_LDADD = $(LDADD_DRIVERS_SERIAL) $(LIBREGEX_LIBS) apcsmart_old_SOURCES = apcsmart-old.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 \ + 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/apcmicrolink-maps.c b/drivers/apcmicrolink-maps.c new file mode 100644 index 0000000000..a73bd7bcb9 --- /dev/null +++ b/drivers/apcmicrolink-maps.c @@ -0,0 +1,561 @@ +/* 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" + +/* 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 cal_status_map[] = { + { 0, "None" }, + { (1UL << 0), "Pending" }, + { (1UL << 1), "InProgress" }, + { (1UL << 2), "Passed" }, + { (1UL << 3), "Failed" }, + { (1UL << 4), "Refused" }, + { (1UL << 5), "Aborted" }, + { (1UL << 9), "InvalidState" }, + { (1UL << 10), "InternalFault" }, + { (1UL << 11), "StateOfChargeNotAcceptable" }, + { (1UL << 12), "LoadChange" }, + { (1UL << 13), "ACInputNotAcceptable" }, + { (1UL << 14), "LoadTooLow" }, + { (1UL << 15), "OverChargeInProgress" }, + { 0, NULL } +}; + +static const microlink_value_map_t test_status_map[] = { + { 0, "None" }, + { (1U << 0), "Pending" }, + { (1U << 1), "InProgress" }, + { (1U << 2), "Passed" }, + { (1U << 3), "Failed" }, + { (1U << 4), "Refused" }, + { (1U << 5), "Aborted" }, + { (1U << 7), "LocalUser" }, + { (1U << 8), "Internal" }, + { (1U << 9), "InvalidState" }, + { (1U << 10), "InternalFault" }, + { (1U << 11), "StateOfChargeNotAcceptable" }, + { 0, NULL } +}; + +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 } +}; + +static const microlink_value_map_t battery_error_map[] = { + { 0, "None" }, + { (1UL << 0), "Disconnected" }, + { (1UL << 1), "Overvoltage" }, + { (1UL << 2), "NeedsReplacement" }, + { (1UL << 3), "OvertemperatureCritical" }, + { (1UL << 4), "Charger" }, + { (1UL << 5), "TemperatureSensor" }, + { (1UL << 6), "BusSoftStart" }, + { (1UL << 7), "OvertemperatureWarning" }, + { (1UL << 8), "GeneralError" }, + { (1UL << 9), "Communication" }, + { (1UL << 10), "DisconnectedFrame" }, + { (1UL << 11), "FirmwareMismatch" }, + { (1UL << 12), "VoltageSenseError" }, + { (1UL << 13), "IncompatiblePack" }, + { (1UL << 14), "ChemistryMismatch" }, + { (1UL << 16), "PositiveFuseOrRelayError" }, + { (1UL << 17), "NegativeFuseOrRelayError" }, + { 0, NULL } +}; + +static const microlink_value_map_t general_error_map[] = { + { 0, "None" }, + { (1UL << 0), "SiteWiring" }, + { (1UL << 1), "EEPROM" }, + { (1UL << 2), "ADConverter" }, + { (1UL << 3), "LogicPowerSupply" }, + { (1UL << 4), "InternalCommunication" }, + { (1UL << 5), "UIButton" }, + { (1UL << 6), "NeedsFactorySetup" }, + { (1UL << 7), "EPOActive" }, + { (1UL << 8), "FirmwareMismatch" }, + { (1UL << 9), "Oscillator" }, + { (1UL << 10), "MeasurementMismatch" }, + { (1UL << 11), "Subsystem" }, + { (1UL << 12), "LogicPowerSupplyRelay" }, + { (1UL << 13), "NetworkWarning" }, + { (1UL << 14), "InputContactOutputRelay" }, + { (1UL << 15), "AirFilterWarning" }, + { (1UL << 16), "DisplayCommunication" }, + { 0, NULL } +}; + +static const microlink_value_map_t power_error_map[] = { + { 0, "None" }, + { (1UL << 0), "OutputOverload" }, + { (1UL << 1), "OutputShortCircuit" }, + { (1UL << 2), "OutputOvervoltage" }, + { (1UL << 3), "TransformerDCImbalance" }, + { (1UL << 4), "Overtemperature" }, + { (1UL << 5), "BackfeedRelay" }, + { (1UL << 6), "AVRRelay" }, + { (1UL << 7), "PFCInputRelay" }, + { (1UL << 8), "OutputRelay" }, + { (1UL << 9), "BypassRelay" }, + { (1UL << 10), "Fan" }, + { (1UL << 11), "PFC" }, + { (1UL << 12), "DCBusOvervoltage" }, + { (1UL << 13), "Inverter" }, + { (1UL << 14), "OverCurrent" }, + { (1UL << 15), "BypassPFCRelay" }, + { (1UL << 16), "BusSoftStart" }, + { (1UL << 17), "GreenRelay" }, + { (1UL << 18), "DCOutput" }, + { (1UL << 19), "DCBusConverter" }, + { (1UL << 20), "Sensor" }, + { (1UL << 21), "InstallationWiring" }, + { 0, NULL } +}; + +static const microlink_value_map_t ups_status_map[] = { + { (1UL << 0), "StatusChange" }, + { (1UL << 1), "StateOnline" }, + { (1UL << 2), "StateOnBattery" }, + { (1UL << 3), "StateBypass" }, + { (1UL << 4), "StateOutputOff" }, + { (1UL << 5), "Fault" }, + { (1UL << 6), "InputBad" }, + { (1UL << 7), "Test" }, + { (1UL << 8), "PendingOutputOn" }, + { (1UL << 9), "PendingOutputOff" }, + { (1UL << 10), "Commanded" }, + { (1UL << 11), "Maintenance" }, + { (1UL << 12), "Inquiring" }, + { (1UL << 13), "HighEfficiency" }, + { (1UL << 14), "InformationalAlert" }, + { (1UL << 15), "FaultState" }, + { (1UL << 16), "StaticBypassStandby" }, + { (1UL << 17), "InverterStandby" }, + { (1UL << 18), "MegaTie" }, + { (1UL << 19), "MainsBadState" }, + { (1UL << 20), "FaultRecoveryState" }, + { (1UL << 21), "OverloadState" }, + { (1UL << 22), "MaintenanceMode" }, + { (1UL << 23), "EfficiencyTestMode" }, + { (1UL << 24), "ForcedInternal" }, + { 0, NULL } +}; + +static const microlink_value_map_t ups_status_status_map[] = { + { (1UL << 1), "OL" }, + { (1UL << 2), "OB" }, + { (1UL << 3), "BYPASS" }, + { (1UL << 4), "OFF" }, + { (1UL << 7), "CAL" }, + { (1UL << 21), "OVER" }, + { 0, NULL } +}; + +static const microlink_value_map_t ups_status_alarm_map[] = { + { (1UL << 5), "Fault" }, + { (1UL << 6), "InputBad" }, + { (1UL << 14), "InformationalAlert" }, + { 0, NULL } +}; + +static const microlink_value_map_t cal_status_status_map[] = { + { (1UL << 1), "CAL" }, + { 0, NULL } +}; + +static const microlink_value_map_t battery_error_status_map[] = { + { (1UL << 2), "RB" }, + { 0, NULL } +}; + +static const microlink_value_map_t power_error_status_map[] = { + { (1UL << 0), "OVER" }, + { 0, NULL } +}; + +const microlink_desc_publish_map_t microlink_desc_publish_map[] = { + { "2:4.A", ups_status_status_map, ups_status_alarm_map }, + { "2:13", cal_status_status_map, NULL }, + { "2:4.34", battery_error_status_map, battery_error_map }, + { "2:4.38", NULL, general_error_map }, + { "2:4.36", power_error_status_map, power_error_map }, + { 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 input_status_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 } +}; + +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 } +}; + +static const microlink_value_map_t countdown_map[] = { + { -1, "NotActive" }, + { 0, "CountdownExpired" }, + { 0, NULL } +}; + +static const microlink_value_map_t countdown_setting_map[] = { + { -1, "Disabled" }, + { 0, NULL } +}; + +#define MLINK_BATTERY_TEST_CMD_START (1ULL << 0) +#define MLINK_BATTERY_TEST_CMD_ABORT (1ULL << 1) +#define MLINK_BATTERY_TEST_CMD_LOCALUSER (1ULL << 9) + +#define MLINK_RUNTIME_CAL_CMD_START (1ULL << 0) +#define MLINK_RUNTIME_CAL_CMD_ABORT (1ULL << 1) +#define MLINK_RUNTIME_CAL_CMD_LOCALUSER (1ULL << 9) + +#define MLINK_UPS_CMD_RESTORE_FACTORY_SETTINGS (1ULL << 3) +#define MLINK_UPS_CMD_OUTPUT_INTO_BYPASS (1ULL << 4) +#define MLINK_UPS_CMD_OUTPUT_OUT_OF_BYPASS (1ULL << 5) +#define MLINK_UPS_CMD_CLEAR_FAULTS (1ULL << 9) +#define MLINK_UPS_CMD_RESET_STRINGS (1ULL << 13) +#define MLINK_UPS_CMD_RESET_LOGS (1ULL << 14) +#define MLINK_UPS_CMD_LOCALUSER (1ULL << 29) +#define MLINK_UPS_CMD_SMARTSLOT1 (1ULL << 30) + +#define MLINK_UPS_CMD_PANEL_SHORT_TEST (1ULL << 0) +#define MLINK_UPS_CMD_PANEL_CONT_TEST (1ULL << 1) +#define MLINK_UPS_CMD_PANEL_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS (1ULL << 2) +#define MLINK_UPS_CMD_PANEL_CANCEL_MUTE (1ULL << 3) + +#define MLINK_OUTLET_CMD_CANCEL (1ULL << 0) +#define MLINK_OUTLET_CMD_OUTPUT_ON (1ULL << 1) +#define MLINK_OUTLET_CMD_OUTPUT_OFF (1ULL << 2) +#define MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN (1ULL << 3) +#define MLINK_OUTLET_CMD_OUTPUT_REBOOT (1ULL << 4) +#define MLINK_OUTLET_CMD_COLD_BOOT_ALLOWED (1ULL << 5) +#define MLINK_OUTLET_CMD_USE_ON_DELAY (1ULL << 6) +#define MLINK_OUTLET_CMD_USE_OFF_DELAY (1ULL << 7) +#define MLINK_OUTLET_CMD_TARGET_UNSWITCHED (1ULL << 8) +#define MLINK_OUTLET_CMD_TARGET_SWITCHED0 (1ULL << 9) +#define MLINK_OUTLET_CMD_TARGET_SWITCHED1 (1ULL << 10) +#define MLINK_OUTLET_CMD_TARGET_SWITCHED2 (1ULL << 11) +#define MLINK_OUTLET_CMD_USB_PORT (1ULL << 12) +#define MLINK_OUTLET_CMD_LOCALUSER (1ULL << 13) +#define MLINK_OUTLET_CMD_RJ45_PORT (1ULL << 14) +#define MLINK_OUTLET_CMD_SMARTSLOT1 (1ULL << 15) +#define MLINK_OUTLET_CMD_SMARTSLOT2 (1ULL << 16) +#define MLINK_OUTLET_CMD_INTERNAL_NETWORK1 (1ULL << 17) +#define MLINK_OUTLET_CMD_INTERNAL_NETWORK2 (1ULL << 18) +#define MLINK_OUTLET_CMD_TARGET_SWITCHED3 (1ULL << 19) +#define MLINK_OUTLET_CMD_ALL_TARGETS (MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_TARGET_SWITCHED3) + +const microlink_desc_value_map_t microlink_desc_value_map[] = { + { "2:4.9.40", "ups.serial", 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.7.28", "ups.load", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 8, 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:11", "ups.test.result", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, 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, cal_status_map }, + { "2:4.34", "experimental.ups.battery.error", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, battery_error_map }, + { "2:4.38", "experimental.ups.general.error", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, general_error_map }, + { "2:4.36", "experimental.ups.power.error", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, power_error_map }, + { "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.A", "experimental.device.status", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, ups_status_map }, + + /* Switched Outlet Groups */ + { "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, 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, 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, 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, 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, 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, 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 }, + + /* Unswitched Outlet Group */ + { "2:4.3E.2D", "outlet.group.0.timer.shutdown", MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, countdown_map }, + { "2:4.3E.B8", "outlet.group.0.delay.shutdown", MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, countdown_setting_map }, + { "2:4.3E.2E", "outlet.group.0.timer.reboot", MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, countdown_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, countdown_setting_map }, + { "2:4.3E.AD", "outlet.group.0.timer.start", MLINK_DESC_ENUM_MAP, MLINK_DESC_SIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, countdown_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, 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.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 }, + + /* Status */ + { "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 }, + { "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.9F", "battery.runtime", MLINK_DESC_FIXED_POINT, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, NULL }, + { "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.48", "battery.date", MLINK_DESC_DATE, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RW, MLINK_NAME_INDEX_NONE, NULL }, + { "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.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.11", "experimental.battery.test.result", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, test_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 }, + + /* Input */ + { "2:4.6.16", "input.quality", MLINK_DESC_BITFIELD_MAP, MLINK_DESC_UNSIGNED, 0, MLINK_DESC_RO, MLINK_NAME_INDEX_NONE, input_status_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 }, + + /* Output */ + { "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 }, + + /* 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 microlink_desc_command_map_t microlink_desc_command_map[] = { + { "2:10", "test.battery.start", MLINK_DESC_WRITE_BITMASK, MLINK_BATTERY_TEST_CMD_START | MLINK_BATTERY_TEST_CMD_LOCALUSER, NULL, NULL }, + { "2:10", "test.battery.stop", MLINK_DESC_WRITE_BITMASK, MLINK_BATTERY_TEST_CMD_ABORT | MLINK_BATTERY_TEST_CMD_LOCALUSER, NULL, NULL }, + { "2:4.B.3B", "test.panel.start", MLINK_DESC_WRITE_BITMASK, MLINK_UPS_CMD_PANEL_SHORT_TEST, NULL, NULL }, + { "2:4.B.3B", "beeper.mute", MLINK_DESC_WRITE_BITMASK, MLINK_UPS_CMD_PANEL_MUTE_ALL_ACTIVE_AUDIBLE_ALARMS, NULL, NULL }, + { "2:12", "calibrate.start", MLINK_DESC_WRITE_BITMASK, MLINK_RUNTIME_CAL_CMD_START | MLINK_RUNTIME_CAL_CMD_LOCALUSER, NULL, NULL }, + { "2:12", "calibrate.stop", MLINK_DESC_WRITE_BITMASK, MLINK_RUNTIME_CAL_CMD_ABORT | MLINK_RUNTIME_CAL_CMD_LOCALUSER, NULL, NULL }, + { "2:14", "bypass.start", MLINK_DESC_WRITE_BITMASK, MLINK_UPS_CMD_OUTPUT_INTO_BYPASS | MLINK_UPS_CMD_LOCALUSER, NULL, NULL }, + { "2:14", "bypass.stop", MLINK_DESC_WRITE_BITMASK, MLINK_UPS_CMD_OUTPUT_OUT_OF_BYPASS | MLINK_UPS_CMD_LOCALUSER, NULL, NULL }, + + { "2:4.B5", "load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "shutdown.default", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_ALL_TARGETS | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + + { "2:4.B5", "outlet.group.0.load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + { "2:4.B5", "outlet.group.0.shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_UNSWITCHED | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3E.B6" }, + + { "2:4.B5", "outlet.group.1.load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + { "2:4.B5", "outlet.group.1.shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED0 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[0].B6" }, + + { "2:4.B5", "outlet.group.2.load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + { "2:4.B5", "outlet.group.2.shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED1 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[1].B6" }, + + { "2:4.B5", "outlet.group.3.load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + { "2:4.B5", "outlet.group.3.shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED2 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[2].B6" }, + + { "2:4.B5", "outlet.group.4.load.off", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.load.off.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_OFF | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.load.on", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.load.on.delay", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_ON | MLINK_OUTLET_CMD_USE_ON_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.load.cycle", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.shutdown.return", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.shutdown.stayoff", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_SHUTDOWN | MLINK_OUTLET_CMD_USE_OFF_DELAY | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" }, + { "2:4.B5", "outlet.group.4.shutdown.reboot", MLINK_DESC_WRITE_BITMASK, MLINK_OUTLET_CMD_OUTPUT_REBOOT | MLINK_OUTLET_CMD_TARGET_SWITCHED3 | MLINK_OUTLET_CMD_LOCALUSER, NULL, "2:4.3D[3].B6" } +}; + +const size_t microlink_desc_value_map_count = + sizeof(microlink_desc_value_map) / sizeof(microlink_desc_value_map[0]); +const size_t microlink_desc_command_map_count = + sizeof(microlink_desc_command_map) / sizeof(microlink_desc_command_map[0]); diff --git a/drivers/apcmicrolink-maps.h b/drivers/apcmicrolink-maps.h new file mode 100644 index 0000000000..82d3506bc5 --- /dev/null +++ b/drivers/apcmicrolink-maps.h @@ -0,0 +1,91 @@ +/* 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_map_mode_e { + MLINK_MAP_VALUE = 0, + MLINK_MAP_BITFIELD = 1 +} microlink_map_mode_t; + +typedef enum microlink_desc_value_type_e { + MLINK_DESC_NONE, + MLINK_DESC_STRING, + MLINK_DESC_FIXED_POINT, + 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_write_type_e { + MLINK_DESC_WRITE_NONE, + MLINK_DESC_WRITE_TYPED, + MLINK_DESC_WRITE_BITMASK +} microlink_desc_write_type_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 struct microlink_bitfield_map_s { + int value; + const char *text; +} 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_command_map_s { + const char *path; + const char *cmd_name; + microlink_desc_write_type_t write_type; + uint64_t bit_mask; + const char *value; + const char *presence_path; +} microlink_desc_command_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 microlink_desc_command_map_t microlink_desc_command_map[]; +extern const size_t microlink_desc_value_map_count; +extern const size_t microlink_desc_command_map_count; + +#endif /* APCMICROLINK_MAPS_H */ diff --git a/drivers/apcmicrolink.c b/drivers/apcmicrolink.c new file mode 100644 index 0000000000..7a1a27de6f --- /dev/null +++ b/drivers/apcmicrolink.c @@ -0,0 +1,2212 @@ +/* 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 "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 + +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; + +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_command_available(const microlink_desc_command_map_t *entry); +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_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 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); + } + } +} + +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 >= 0xF4; +} + +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_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 const microlink_desc_command_map_t *microlink_find_desc_command_by_name(const char *cmdname) +{ + size_t i; + + for (i = 0; i < microlink_desc_command_map_count; i++) { + if (microlink_desc_command_map[i].cmd_name != NULL + && !strcmp(microlink_desc_command_map[i].cmd_name, cmdname) + && microlink_command_available(µlink_desc_command_map[i])) { + return µlink_desc_command_map[i]; + } + } + + return NULL; +} + +static int microlink_command_available(const microlink_desc_command_map_t *entry) +{ + if (entry == NULL || entry->presence_path == NULL) { + return 1; + } + + return (microlink_find_descriptor_usage(entry->presence_path) != NULL); +} + +static int microlink_set_descriptor_string_info(const char *name, const char *path) +{ + const microlink_descriptor_usage_t *usage; + char value[MLINK_MAX_PAYLOAD + 1]; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped + || usage->data_offset + usage->size > descriptor_blob_len) { + return 0; + } + + microlink_format_ascii(descriptor_blob + usage->data_offset, usage->size, + value, sizeof(value)); + if (value[0] == '\0') { + return 0; + } + + dstate_setinfo(name, "%s", value); + return 1; +} + +static int microlink_set_descriptor_map_info(const char *name, const char *path, + const microlink_value_map_t *map, microlink_map_mode_t mode) +{ + const microlink_descriptor_usage_t *usage; + const unsigned char *data; + const char *zero_text = NULL; + uint32_t raw = 0; + int32_t value = 0; + char buf[128]; + size_t i, size, used = 0; + int matched = 0; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || + usage->data_offset + usage->size > descriptor_blob_len) { + return 0; + } + + size = usage->size; + if (size == 0 || size > sizeof(raw)) { + return 0; + } + + data = descriptor_blob + usage->data_offset; + for (i = 0; i < size; i++) { + raw = (raw << 8) | data[i]; + } + + 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 { + snprintf(buf, sizeof(buf), "0x%0*lX", + (int)(size * 2), (unsigned long)raw); + dstate_setinfo(name, "%s", buf); + } + + return 1; + } + + { + 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; + } + } + + dstate_setinfo(name, "%ld", (long)value); + return 1; +} + +static int microlink_set_descriptor_fixed_point_info(const char *name, const char *path, + microlink_desc_numeric_sign_t sign, unsigned int bin_point) +{ + const microlink_descriptor_usage_t *usage; + const unsigned char *data; + uint32_t raw = 0; + int32_t signed_raw = 0; + double value; + char text[32]; + size_t i, size; + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || usage->size == 0 + || usage->data_offset + usage->size > descriptor_blob_len) { + return 0; + } + + size = usage->size; + data = descriptor_blob + usage->data_offset; + 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 int microlink_get_descriptor_integer(const char *path, size_t max_size, + uint64_t *raw_out, size_t *size_out) +{ + const microlink_descriptor_usage_t *usage; + const unsigned char *data; + uint64_t raw = 0; + size_t i; + + if (raw_out == NULL) { + return 0; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || usage->skipped || usage->size == 0 + || usage->size > max_size + || usage->data_offset + usage->size > descriptor_blob_len) { + return 0; + } + + data = descriptor_blob + usage->data_offset; + for (i = 0; i < usage->size; i++) { + raw = (raw << 8) | data[i]; + } + + *raw_out = raw; + if (size_out != NULL) { + *size_out = usage->size; + } + + return 1; +} + +static void microlink_civil_from_days(int64_t days, int *year, unsigned int *month, + unsigned int *day) +{ + int64_t z = days + 730425; + int64_t era = (z >= 0 ? z : z - 146096) / 146097; + unsigned int doe = (unsigned int)(z - era * 146097); + unsigned int yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + int y = (int)yoe + (int)(era * 400); + unsigned int doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + unsigned int mp = (5 * doy + 2) / 153; + + *day = doy - (153 * mp + 2) / 5 + 1; + *month = mp + (mp < 10 ? 3U : (unsigned int)-9); + *year = y + (*month <= 2U); +} + +static int64_t microlink_days_from_civil(int year, unsigned int month, unsigned int day) +{ + int y = year - (month <= 2U); + int era = (y >= 0 ? y : y - 399) / 400; + unsigned int yoe = (unsigned int)(y - era * 400); + unsigned int doy = (153 * (month + (month > 2U ? (unsigned int)-3 : 9U)) + 2) / 5 + day - 1; + unsigned int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + + return (int64_t)(era * 146097 + (int)doe) - 730425; +} + +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_set_descriptor_date_info(const char *name, const char *path) +{ + uint64_t raw; + int year; + unsigned int month, day; + char text[16]; + + if (!microlink_get_descriptor_integer(path, sizeof(uint32_t), &raw, NULL)) { + return 0; + } + + microlink_civil_from_days((int64_t)raw, &year, &month, &day); + snprintf(text, sizeof(text), "%04d-%02u-%02u", year, month, day); + dstate_setinfo(name, "%s", text); + return 1; +} + +static int microlink_set_descriptor_time_info(const char *name, const char *path) +{ + uint64_t raw; + unsigned int hours, minutes, seconds; + char text[16]; + + if (!microlink_get_descriptor_integer(path, sizeof(uint32_t), &raw, NULL)) { + return 0; + } + + 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) + || usage->data_offset + usage->size > descriptor_blob_len) { + return 0; + } + + data = descriptor_blob + usage->data_offset; + 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.bits.descriptor_present || page0.flags.bits.auth_required) + && !descriptor_ready) { + return 0; + } + + if (!page0.flags.bits.auth_required) { + 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) { + matched = 1; + alarm_set(map[i].text); + } + } + + if (!matched) { + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 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) { + matched = 1; + status_set(map[i].text); + } + } + + if (!matched) { + for (i = 0; map[i].text != NULL; i++) { + if (map[i].value == 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; + } + + 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 (page < 256U) { + 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); + } + } + + 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_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 i; + + if (entry == NULL || path == NULL || val == NULL) { + return 0; + } + + usage = microlink_find_descriptor_usage(path); + if (usage == NULL || !usage->valid || usage->skipped || usage->size == 0 + || usage->size > sizeof(payload)) { + return 0; + } + + switch (entry->type) { + case MLINK_DESC_STRING: + memset(payload, 0, usage->size); + for (i = 0; i < usage->size && val[i] != '\0'; i++) { + payload[i] = (unsigned char)val[i]; + } + return microlink_send_descriptor_write(path, payload, usage->size); + + case MLINK_DESC_FIXED_POINT: + { + char *endptr = NULL; + int64_t raw; + + if (usage->size == 0 || usage->size > 8) { + return 0; + } + + if (entry->bin_point == 0U) { + /* strict integer parsing */ + long long parsed = strtoll(val, &endptr, 10); + + if (endptr == val || *endptr != '\0') { + return 0; + } + + raw = (int64_t)parsed; + } else { + /* fixed-point parsing */ + 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)); + } + + /* shared range + packing logic */ + + { + int64_t min_raw, max_raw; + + if (entry->sign == MLINK_DESC_SIGNED) { + max_raw = ((int64_t)1 << ((usage->size * 8U) - 1U)) - 1; + min_raw = -((int64_t)1 << ((usage->size * 8U) - 1U)); + } else { + min_raw = 0; + max_raw = ((int64_t)1 << (usage->size * 8U)) - 1; + } + + if (raw < min_raw || raw > max_raw) { + 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)(((uint64_t)raw >> shift) & 0xFFU); + } + + return microlink_send_descriptor_write(path, payload, usage->size); + } + + case MLINK_DESC_DATE: + { + int year; + int check_year; + unsigned int month, day; + unsigned int check_month, check_day; + int64_t raw; + + if (usage->size == 0 || usage->size > 8) { + return 0; + } + + if (sscanf(val, "%d-%u-%u", &year, &month, &day) != 3) { + return 0; + } + + if (month < 1U || month > 12U || day < 1U || day > 31U) { + return 0; + } + + raw = microlink_days_from_civil(year, month, day); + if (raw < 0 || (uint64_t)raw > microlink_max_unsigned_for_size(usage->size)) { + return 0; + } + + microlink_civil_from_days(raw, &check_year, &check_month, &check_day); + if (check_year != year || check_month != month || check_day != day) { + 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)(((uint64_t)raw >> shift) & 0xFFU); + } + + return microlink_send_descriptor_write(path, payload, usage->size); + } + + case MLINK_DESC_TIME: + { + unsigned int hours, minutes, seconds; + uint64_t raw; + + if (usage->size == 0 || usage->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(usage->size)) { + 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)((raw >> shift) & 0xFFU); + } + + return microlink_send_descriptor_write(path, payload, usage->size); + } + + case MLINK_DESC_ENUM_MAP: + case MLINK_DESC_BITFIELD_MAP: + { + char *endptr = NULL; + int64_t raw = 0; + size_t j; + + if (usage->size == 0 || usage->size > 8) { + return 0; + } + + if (entry->map != NULL) { + for (j = 0; entry->map[j].text != NULL; j++) { + if (!strcasecmp(entry->map[j].text, val)) { + raw = entry->map[j].value; + break; + } + } + + if (entry->map[j].text == NULL) { + 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; + } + } + } + + { + int64_t min_raw, max_raw; + + if (entry->type == MLINK_DESC_ENUM_MAP && entry->sign == MLINK_DESC_SIGNED) { + max_raw = ((int64_t)1 << ((usage->size * 8U) - 1U)) - 1; + min_raw = -((int64_t)1 << ((usage->size * 8U) - 1U)); + } else { + min_raw = 0; + max_raw = ((int64_t)1 << (usage->size * 8U)) - 1; + } + + if (raw < min_raw || raw > max_raw) { + 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)(((uint64_t)raw >> shift) & 0xFFU); + } + + return microlink_send_descriptor_write(path, payload, 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 +#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 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 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)); + + switch (entry->type) { + case MLINK_DESC_STRING: + microlink_set_descriptor_string_info(name, usage->path); + break; + case MLINK_DESC_FIXED_POINT: + microlink_set_descriptor_fixed_point_info(name, usage->path, + entry->sign, entry->bin_point); + break; + case MLINK_DESC_DATE: + microlink_set_descriptor_date_info(name, usage->path); + break; + case MLINK_DESC_TIME: + microlink_set_descriptor_time_info(name, usage->path); + break; + case MLINK_DESC_BITFIELD_MAP: + microlink_set_descriptor_map_info(name, usage->path, entry->map, MLINK_MAP_BITFIELD); + break; + case MLINK_DESC_ENUM_MAP: + microlink_set_descriptor_map_info(name, usage->path, entry->map, MLINK_MAP_VALUE); + 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 MLINK_DESC_NONE: + default: + 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 + } + + 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 == 0xFC) { + if (pos + 1 >= blob_len) { + return 0; + } + usage_size = blob[pos + 1]; + pos += 2; + continue; + } + + if (token == 0xFB || token == 0xF9) { + pos++; + if (pos + usage_size > blob_len) { + return 0; + } + pos += usage_size; + continue; + } + + if (token == 0xFA) { + 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 0xF4: + pos = microlink_parse_descriptor_block(blob, blob_len, pos, data_offset, path); + if (pos == 0) { + return 0; + } + break; + case 0xF5: + skip_next = 1; + break; + case 0xF6: + case 0xFF: + return pos; + case 0xF7: + break; + case 0xF8: + { + 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 0xFE: + { + 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 0xFD: + { + 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; + } + + 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; + } + + pos = block_end; + break; + } + default: + if (token == 0x00 || microlink_is_descriptor_operator(token) || token > 0xDF) { + 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.bits.descriptor_present || 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.raw = (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.raw); + } +} + +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(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 (rxbuf_len - start >= MLINK_RECORD_LEN + && microlink_checksum_valid(rxbuf + start, MLINK_RECORD_LEN)) { + 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; + } + + memcpy(frame, rxbuf, MLINK_RECORD_LEN); + *framelen = MLINK_RECORD_LEN; + memmove(rxbuf, rxbuf + MLINK_RECORD_LEN, rxbuf_len - MLINK_RECORD_LEN); + rxbuf_len -= MLINK_RECORD_LEN; + microlink_trace_frame(2, "RX record", frame, MLINK_RECORD_LEN); + 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 unsigned char *serial; + 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 = microlink_get_descriptor_data(MLINK_DESC_SERIALNUMBER, 16); + master_password = microlink_get_descriptor_data(MLINK_DESC_MASTER_PASSWORD, 4); + + if (serial == 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, serial, 16); + 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.bits.descriptor_present) { + if (descriptor_ready) { + microlink_update_blob(); + } else { + microlink_parse_descriptor(); + } + } + + if (page0.flags.bits.auth_required && 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; + return microlink_prime_poll(); + } + + 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"); + 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.raw); + dstate_setinfo("microlink.flags", "%s", flags); + dstate_setinfo("microlink.flag.auth_required", "%u", + (unsigned int)page0.flags.bits.auth_required); + dstate_setinfo("microlink.flag.implicit_stuffing", "%u", + (unsigned int)page0.flags.bits.implicit_stuffing); + dstate_setinfo("microlink.flag.descriptor_present", "%u", + (unsigned int)page0.flags.bits.descriptor_present); + dstate_setinfo("microlink.flag.firmware_update_needed", "%u", + (unsigned int)page0.flags.bits.firmware_update_needed); + } + + if (protocol->seen && protocol->len >= 12 && page0.flags.bits.descriptor_present) { + 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)); + 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) +{ + const microlink_desc_command_map_t *entry; + const microlink_desc_value_map_t *value_entry; + int ret = STAT_INSTCMD_INVALID; + + NUT_UNUSED_VARIABLE(extra); + upsdebug_INSTCMD_STARTING(cmdname, extra); + + entry = microlink_find_desc_command_by_name(cmdname); + if (entry != NULL) { + upslog_INSTCMD_POWERSTATE_MAYBE(cmdname, extra); + switch (entry->write_type) { + case MLINK_DESC_WRITE_BITMASK: + ret = microlink_send_descriptor_mask_value(entry->path, entry->bit_mask) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + break; + case MLINK_DESC_WRITE_TYPED: + value_entry = microlink_find_desc_value_by_path(entry->path, NULL); + ret = (value_entry != NULL && entry->value != NULL + && microlink_send_descriptor_typed_value(value_entry, entry->path, entry->value)) + ? STAT_INSTCMD_HANDLED : STAT_INSTCMD_FAILED; + 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 MLINK_DESC_WRITE_NONE: + default: + ret = STAT_INSTCMD_FAILED; + 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 + } + 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; + + 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(); + + for (i = 0; i < (int)microlink_desc_command_map_count; i++) { + if (microlink_command_available(µlink_desc_command_map[i])) { + dstate_addcmd(microlink_desc_command_map[i].cmd_name); + } + } + 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)"); +} + +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..d68c6cc1ea --- /dev/null +++ b/drivers/apcmicrolink.h @@ -0,0 +1,68 @@ +/* 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_FIXED_PAYLOAD_LEN 32 +#define MLINK_RECORD_LEN (1 + MLINK_FIXED_PAYLOAD_LEN + 2) +#define MLINK_DESCRIPTOR_MAX_BLOB (256 * MLINK_FIXED_PAYLOAD_LEN) +#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" + +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 union microlink_page0_flags_u { + unsigned char raw; + struct { + unsigned char auth_required : 1; + unsigned char implicit_stuffing : 1; + unsigned char reserved_2 : 1; + unsigned char descriptor_present : 1; + unsigned char firmware_update_needed : 1; + unsigned char reserved_5_7 : 3; + } bits; +} microlink_page0_flags_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; + microlink_page0_flags_t flags; + uint16_t series_id; + uint16_t descriptor_ptr; +} microlink_page0_state_t; + +#endif /* APCMICROLINK_H */