summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2020-10-18 23:45:08 +0300
committerEvgeny Zinoviev <me@ch1p.com>2020-10-24 02:38:24 +0300
commit557a8f9de0961c5ad26d8a84950d67f3cae3bf1a (patch)
treec3299aa624c52669de4f37f147c163a5e49e1058
initial commit
-rw-r--r--.gitignore3
-rw-r--r--Makefile41
-rw-r--r--README.md312
-rw-r--r--isv.c861
-rw-r--r--libvoltronic/voltronic_crc.c99
-rw-r--r--libvoltronic/voltronic_crc.h71
-rw-r--r--libvoltronic/voltronic_dev.c430
-rw-r--r--libvoltronic/voltronic_dev.h109
-rw-r--r--libvoltronic/voltronic_dev_impl.h125
-rw-r--r--libvoltronic/voltronic_dev_serial.h78
-rw-r--r--libvoltronic/voltronic_dev_serial_libserialport.c196
-rw-r--r--libvoltronic/voltronic_dev_usb.h37
-rw-r--r--libvoltronic/voltronic_dev_usb_hidapi.c106
-rw-r--r--p18.c1559
-rw-r--r--p18.h505
-rw-r--r--print.c1089
-rw-r--r--print.h82
-rw-r--r--util.c115
-rw-r--r--util.h57
-rw-r--r--variant.c86
-rw-r--r--variant.h54
21 files changed, 6015 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9e22d16
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.o
+isv
+.idea
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a90f12f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+CC := gcc
+
+PROGRAM = isv
+
+OS = $(shell uname -s)
+ifeq ($(OS),Linux)
+ HIDAPI = hidapi-hidraw
+endif
+ifeq ($(OS),Darwin)
+ HIDAPI = hidapi
+endif
+
+CFLAGS = -O2 -std=c99
+CFLAGS += -Wall -W
+CFLAGS += `pkg-config --cflags $(HIDAPI)`
+LDFLAGS = -lm
+LDFLAGS += `pkg-config --libs $(HIDAPI)`
+
+INSTALL = /usr/bin/env install
+PREFIX = /usr/local
+
+OBJS = isv.o util.o p18.o print.o variant.o
+OBJS += libvoltronic/voltronic_dev_usb_hidapi.o
+OBJS += libvoltronic/voltronic_crc.o
+OBJS += libvoltronic/voltronic_dev.o
+
+all: $(PROGRAM)
+
+$(PROGRAM): $(OBJS)
+ $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
+
+install: $(PROGRAM)
+ $(INSTALL) $(PROGRAM) $(PREFIX)/bin
+
+clean:
+ rm -f $(OBJS) $(PROGRAM)
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $^ -I. -o $@
+
+.PHONY: all install clean distclean \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e5ce232
--- /dev/null
+++ b/README.md
@@ -0,0 +1,312 @@
+# isv
+
+**isv** is utility for controlling Voltronic hybrid solar inverters that use P18 protocol. **isv** has full P18
+support with all known methods implemented. It was written for use with InfiniSolar V 5kW inverter and it's the only
+inverter it has been tested with so far, but it should work with other inverters using P18 protocol as well. Adding
+support for other protocols (such as P16 or P17) by splitting them into separate modules is possible in future.
+
+For now, only USB connection is supported and tested (I just don't have the RS-232 cable with this weird RJ-style plug
+lol), but RS-232 support will be added eventually.
+
+It's written in pure C99 with almost zero dependencies. It uses [libvoltronic](https://github.com/jvandervyver/libvoltronic)
+for underlying device interaction. but you don't need to download and build it separately as **isv** comes with its own
+slightly modified libvoltronic version.
+
+It can output data in different formats (human-readable tables, conveniently-parsable tables and even JSON) so you can
+easily integrate it in your project.
+
+For now only Linux and macOS are supported and tested. Other operating systems will be supported later.
+
+## Requirements
+
+- `pkg-config`
+- `hidapi`
+ - On Linux, you should be able to install it from your distro's package manager
+ - On macOS, `brew install hidapi`
+
+## Building
+
+Just run `make`. If you want to install it, `make install` will do the job.
+
+## Usage
+
+Run `isv` without arguments to see the full options list. For the sake of good readmes it's also written here.
+
+### Common options
+
+- **`-r`** `COMMAND`<br>
+ **`--raw`** `COMMAND` - execute arbitrary command and print inverter's response.<br>
+ Command example: `^P005PI`
+
+- **`-t `** `TIMEOUT`<br>
+ **`--timeout`** `TIMEOUT` - device read timeout, in milliseconds.<br>Example: `-t 5000`
+
+- **`-v`**, **`--verbose`** - print debug information, like hexdumps of communication traffic with inverter
+
+- **`-p`**, **`--pretend`** - do not actually execute anything on inverter, but output some debug info. Little use for
+ normal people. Doesn't work with `--raw`.
+
+- **`-f`** `FORMAT`<br>
+ **`--format`** `FORMAT` - output format for `--get-*` and `--set-*` options, you can find list of supported
+ formats below.
+
+### Get options
+
+- **`--get-protocol-id`** - returns protocol id. Should be always `18` as it's the only one supported.
+
+- **`--get-date-time`** - returns date and time from inverter
+
+- **`--get-total-generated`** - returns total generated energy, in kWatts (or Watts?). The documentation says it should be
+ kilowatts, but my inverter says that we generated almost 200,000 for the last two months... It's must be Watts. My
+ guess is that it reports Watts first and when it reaches some kind of integer limit it switches to kWatts.
+
+- **`--get-year-generated`** `YYYY` - returns generated energy for specified year, in kWatts (or Watts? see above)
+
+- **`--get-month-generated`** `YYYY` `MM` - returns generated energy for specified month, in kWatts (or Watts? see above)
+
+- **`--get-day-generated`** `YYYY` `MM` `DD` - returns generated energy for specified day, in Watts.
+
+- **`--get-series-number`** - returns series number. Or maybe serial number. The documentation is written by Chinese in
+ bad english.
+
+- **`--get-cpu-version`** - returns main and slave CPU versions.
+
+- **`--get-rated-information`** - returns rated information.
+
+- **`--get-general-status`** - returns general status, many cool stuff here. Usually this is what you want to read.
+
+- **`--get-working-mode`** - returns working mode.
+
+- **`--get-faults-warnings`** - returns fault and warning status.
+
+- **`--get-flags`** - returns state of a set of flags, or toggles, like backlight or buzzer ON or OFF, etc.
+
+- **`--get-defaults`** - returns default values of some changeable parameters and default flags values.
+
+- **`--get-max-charging-current-selectable-values`**
+
+- **`--get-max-ac-charging-current-selectable-values`**
+
+- **`--get-parallel-rated-information`** `ID`<br>
+ `ID` - parallel machine ID
+
+- **`--get-parallel-general-status`** `ID`<br>
+ `ID` - parallel machine ID
+
+- **`--get-ac-charge-time-bucket`**
+
+- **`--get-ac-supply-load-time-bucket`**
+
+### Set options
+
+- **`--set-loads-supply`** `0|1`
+
+- **`--set-flag`** `FLAG` `0|1`
+
+ List of flags:
+
+ - `BUZZ` - Silence buzzer or open buzzer
+ - `OLBP` - Overload bypass function
+ - `LCDE` - LCD display escape to default page after 1min timeout
+ - `OLRS` - Overload restart
+ - `OTRS` - Overload temperature restart
+ - `BLON` - Backlight on
+ - `ALRM` - Alarm on primary source interrupt
+ - `FTCR` - Fault code record
+ - `MTYP` - Machine type (1=Grid-Tie, 0=Off-Grid-Tie)
+
+- **`--set-defaults`**<br>
+ Reset changeable parameters to their default values.
+
+- **`--set-battery-max-charging-current`** `ID` `AMPS`<br>
+ `ID` - parallel machine ID (use 0 for a single model)<br>
+ `AMPS` - use `--get-max-charging-current-selectable-values` to see a list of allowed currents
+
+- **`--set-battery-max-ac-charging-current`** `ID` `AMPS`<br>
+ `ID` - parallel machine ID (use 0 for a single model)<br>
+ `AMPS` - use `--get-max-ac-charging-current-selectable-values` to see a list of allowed currents
+
+- **`--set-ac-output-freq`** `50|60`
+
+- **`--set-battery-max-charging-voltage`** `CV` `FV`<br>
+ `CV` - constant voltage (48.0 ~ 58.4)<br>
+ `FV` - float voltage (48.0 ~ 58.4)
+
+- **`--set-ac-output-rated-voltage`** `V`<br>
+ `V` - voltage. Allowed voltages are `202`, `208`, `220`, `230` and `240`
+
+- **`--set-output-source-priority`** `PRIORITY`
+
+ List of priorities:
+
+ - `SUB` is for *Solar-Utility-Battery*<br>
+ - `SBU` is for *Solar-Battery-Utility*
+
+- **`--set-battery-charging-thresholds`** `CV` `DV`<br>
+ Sets battery re-charging and re-discharigng voltages when utility is available.
+
+ `CV` - re-charging voltage<br>
+ *For 12V unit:* `11`, `11.3`, `11.5`, `11.8`, `12`, `12.3`, `12.5` or `12.8`<br>
+ *For 24V unit:* `22`, `22.5`, `23`, `23.5`, `24`, `24.5`, `25` or `25.5`<br>
+ *For 48V unit:* `44`, `45`, `46`, `47`, `48`, `49`, `50` or `51`
+
+ `DV` - re-discharging voltage<br>
+ *For 12V unit:* `0`, `12`, `12.3`, `12.5`, `12.8`, `13`, `13.3`, `13.5`, `13.8`, `14`, `14.3` or `14.5`<br>
+ *For 24V unit:* `0`, `24`, `24.5`, `25`, `25.5`, `26`, `26.5`, `27`, `27.5`, `28`, `28.5` or `29`<br>
+ *For 48V unit:* `0`, `48`, `49`, `50`, `51`, `52`, `53`, `54`, `55`, `56`, `57` or `58`<br>
+
+- **`--set-charging-source-priority`** `ID` `PRIORITY`<br>
+ `ID` - parallel machine ID (use 0 for a single model).
+
+ List of priorities:
+
+ - `SF` for *Solar-First*<br>
+ - `SU` for *Solar-and-Utility*<br>
+ - `S` for *Solar-Only*
+
+- **`--set-solar-power-priority`** `PRIORITY`
+
+ List of priorities:
+
+ - `BLU` for *Battery-Load-Utility*<br>
+ - `LBU` for *Load-Battery-Utility*
+
+- **`--set-ac-input-voltage-range`** `RANGE`
+
+ List of ranges:
+
+ - `APPLIANCE`
+ - `UPS`
+
+- **`--set-battery-type`** `AGM|FLOODED|USER`
+
+- **`--set-output-model`** `ID` `MODEL`<br>
+ `ID` - parallel machine ID (use 0 for a single model).
+
+ List of allowed models:
+
+ - `SM` - Single module
+ - `P` - Parallel output
+ - `P1` - Phase 1 of three phase output
+ - `P2` - Phase 2 of three phase output
+ - `P3` - Phase 3 of three phase
+
+- **`--set-battery-cutoff-voltage`** `V`<br>
+ `V` - cut-off voltage (40.0 ~ 48.0)
+
+- **`--set-solar-configuration`** `ID`<br>
+ `ID` - serial number
+
+- **`--clear-generated-data`**<br>
+ Clears all data of generated energy.
+
+- **`--set-date-time`** `YYYY` `MM` `DD` `hh` `mm` `ss`<br>
+ `YYYY` - year<br>
+ `MM` - month<br>
+ `DD` - day<br>
+ `hh` - hours<br>
+ `mm` - minutes<br>
+ `ss` - seconds
+
+- **`--set-ac-charge-time-bucket`** `START` `END`<br>
+ `START` - starting time, `hh:mm` format<br>
+ `END` - ending time, `hh:mm` format
+
+- **`--set-ac-supply-load-time-bucket`** `START` `END`<br>
+ `START` - starting time, `hh:mm` format<br>
+ `END` - ending time, `hh:mm` format
+
+### Formats
+- `table` - human-readable table. This is used by default.
+
+ Output example:
+ ```
+ Grid voltage: 0.0 V
+ Grid frequency: 0.0 Hz
+ AC output voltage: 230.1 V
+ AC output frequency: 50.0 Hz
+ AC output apparent power: 114 VA
+ AC output active power: 69 Wh
+ Output load percent: 2%
+ Battery voltage: 49.5 V
+ Battery voltage from SCC: 0.0 V
+ Battery voltage from SCC2: 0.0 V
+ Battery discharge current: 1 A
+ Battery charging current: 0 A
+ Battery capacity: 73%
+ Inverter heat sink temperature: 32 °C
+ MPPT1 charger temperature: 0 °C
+ MPPT2 charger temperature: 0 °C
+ PV1 Input power: 0.00 Wh
+ PV2 Input power: 0.00 Wh
+ PV1 Input voltage: 0.0 V
+ PV2 Input voltage: 0.0 V
+ Setting value configuration state: Something changed
+ MPPT1 charger status: Abnormal
+ MPPT2 charger status: Abnormal
+ Load connection: Connected
+ Battery power direction: Discharge
+ DC/AC power direction: DC/AC
+ Line power direction: Do nothing
+ Local parallel ID: 0
+ ```
+
+- `parsable-table`
+
+ Output example:
+
+ ```
+ grid_voltage 0.0 V
+ grid_freq 0.0 Hz
+ ac_output_voltage 230.0 V
+ ac_output_freq 50.0 Hz
+ ac_output_apparent_power 92 VA
+ ac_output_active_power 52 Wh
+ output_load_percent 1 %
+ battery_voltage 49.5 V
+ battery_voltage_scc 0.0 V
+ battery_voltage_scc2 0.0 V
+ battery_discharge_current 1 A
+ battery_charging_current 0 A
+ battery_capacity 73 %
+ inverter_heat_sink_temp 32 °C
+ mppt1_charger_temp 0 °C
+ mppt2_charger_temp 0 °C
+ pv1_input_power 0.00 Wh
+ pv2_input_power 0.00 Wh
+ pv1_input_voltage 0.0 V
+ pv2_input_voltage 0.0 V
+ settings_values_changed "Something changed"
+ mppt1_charger_status Abnormal
+ mppt2_charger_status Abnormal
+ load_connected Connected
+ battery_power_direction Discharge
+ dc_ac_power_direction DC/AC
+ line_power_direction "Do nothing"
+ local_parallel_id 0
+ ```
+
+- `json` - JSON.
+
+ Output example:
+
+ ```
+ {"grid_voltage":0.00,"grid_freq":0.00,"ac_output_voltage":229.90,"ac_output_freq":49.90,"ac_output_apparent_power":91,"ac_output_active_power":47,"output_load_percent":1,"battery_voltage":49.50,"battery_voltage_scc":0.00,"battery_voltage_scc2":0.00,"battery_discharge_current":1,"battery_charging_current":0,"battery_capacity":73,"inverter_heat_sink_temp":32,"mppt1_charger_temp":0,"mppt2_charger_temp":0,"pv1_input_power":0.00,"pv2_input_power":0.00,"pv1_input_voltage":0.00,"pv2_input_voltage":0.00,"settings_values_changed":"Something changed","mppt1_charger_status":"Abnormal","mppt2_charger_status":"Abnormal","load_connected":"Connected","battery_power_direction":"Discharge","dc_ac_power_direction":"DC/AC","line_power_direction":"Do nothing","local_parallel_id":0}
+ ```
+
+- `json-w-units` - JSON with units.
+
+ Output example:
+
+ ```
+ {"grid_voltage":[0.00,"V"],"grid_freq":[0.00,"Hz"],"ac_output_voltage":[230.10,"V"],"ac_output_freq":[50.00,"Hz"],"ac_output_apparent_power":[92,"VA"],"ac_output_active_power":[53,"Wh"],"output_load_percent":[1,"%"],"battery_voltage":[49.50,"V"],"battery_voltage_scc":[0.00,"V"],"battery_voltage_scc2":[0.00,"V"],"battery_discharge_current":[1,"A"],"battery_charging_current":[0,"A"],"battery_capacity":[73,"%"],"inverter_heat_sink_temp":[32,"°C"],"mppt1_charger_temp":[0,"°C"],"mppt2_charger_temp":[0,"°C"],"pv1_input_power":[0.00,"Wh"],"pv2_input_power":[0.00,"Wh"],"pv1_input_voltage":[0.00,"V"],"pv2_input_voltage":[0.00,"V"],"settings_values_changed":"Something changed","mppt1_charger_status":"Abnormal","mppt2_charger_status":"Abnormal","load_connected":"Connected","battery_power_direction":"Discharge","dc_ac_power_direction":"DC/AC","line_power_direction":"Do nothing","local_parallel_id":0}
+ ```
+
+### Return codes
+
+**isv** returns `0` on success, `1` on some input error (e.g. invalid argument) and `2` on communication failure (e.g.
+failed to read response from inverter, or response is invalid).
+
+## License
+
+GPLv3 \ No newline at end of file
diff --git a/isv.c b/isv.c
new file mode 100644
index 0000000..f8603a7
--- /dev/null
+++ b/isv.c
@@ -0,0 +1,861 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <getopt.h>
+#include <stdarg.h>
+#include <assert.h>
+
+#include "variant.h"
+#include "p18.h"
+#include "util.h"
+#include "print.h"
+#include "libvoltronic/voltronic_dev_usb.h"
+
+#define COMMAND_BUF_LENGTH 128
+#define RESPONSE_BUF_LENGTH 128
+
+#define PRINT(msg_type) \
+ { \
+ P18_MSG_T(msg_type) m = P18_UNPACK_FN_NAME(msg_type)(buffer+5); \
+ PRINT_FN_NAME(msg_type)(&m, g_format); \
+ }
+
+#define GET_ARGS(len) \
+ get_args(argc, (const char **)argv, a, (len))
+
+bool g_verbose = false;
+print_format_t g_format = PRINT_FORMAT_TABLE;
+
+static void usageintlist(const int *list, size_t size)
+{
+ for (size_t i = 0; i < size; i++) {
+ printf("%d", list[i]);
+ if (i < size-1)
+ printf(", ");
+ }
+}
+
+static void usagestrlist(const char **list, size_t size)
+{
+ for (size_t i = 0; i < size; i++) {
+ printf("%s", list[i]);
+ if (i < size-1)
+ printf(", ");
+ }
+}
+
+static void write_num(char *buf, int n)
+{
+ assert(n <= 9);
+ *buf++ = n + '0';
+ *buf++ = '\0';
+}
+
+static void usage(const char *progname)
+{
+ printf("Usage: %s OPTIONS\n", progname);
+ printf("\n"
+ "Options:\n"
+ " -h, --help: print this help\n"
+ " -r <COMMAND>,\n"
+ " --raw <COMMAND>: execute arbitrary command and print inverter's\n"
+ " response. Command example: ^P005PI\n"
+ " -t <TIMEOUT>,\n"
+ " --timeout <TIMEOUT>: device read timeout, in milliseconds\n"
+ " -v, --verbose: print debug information, like hexdumps of\n"
+ " communication traffic with inverter\n"
+ " -p, --pretend: do not actually execute command on inverter,\n"
+ " but output some debug info\n"
+ " -f <FORMAT>,\n"
+ " --format <FORMAT>: output format for --get and --set options, see below\n"
+ "\n"
+ "Options to get data from inverter:\n"
+ " --get-protocol-id\n"
+ " --get-date-time\n"
+ " --get-total-generated\n"
+ " --get-year-generated <YYYY>\n"
+ " --get-month-generated <YYYY> <MM>\n"
+ " --get-day-generated <YYYY> <MM> <DD>\n"
+ " --get-series-number\n"
+ " --get-cpu-version\n"
+ " --get-rated-information\n"
+ " --get-general-status\n"
+ " --get-working-mode\n"
+ " --get-faults-warnings\n"
+ " --get-flags\n"
+ " --get-defaults\n"
+ " --get-max-charging-current-selectable-values\n"
+ " --get-max-ac-charging-current-selectable-values\n"
+ " --get-parallel-rated-information <ID>\n"
+ " ID: parallel machine ID\n"
+ "\n"
+ " --get-parallel-general-status <ID>\n"
+ " ID: parallel machine ID\n"
+ "\n"
+ " --get-ac-charge-time-bucket\n"
+ " --get-ac-supply-load-time-bucket\n"
+ "\n"
+ "Options to set inverter's configuration:\n"
+ " --set-loads-supply 0|1\n"
+ " --set-flag <FLAG> 0|1\n"
+ " --set-defaults\n"
+ " --set-battery-max-charging-current <ID> <AMPS>\n"
+ " ID: parallel machine ID (use 0 for a single model)\n"
+ " AMPS: use --get-max-charging-current-selectable-values\n"
+ " to see a list of allowed current values\n"
+ "\n"
+ " --set-battery-max-ac-charging-current <ID> <AMPS>\n"
+ " ID: parallel machine ID (use 0 for a single model)\n"
+ " AMPS: use --get-max-ac-charging-current-selectable-values\n"
+ " to see a list of allowed current values\n"
+ "\n"
+ " --set-ac-output-freq 50|60\n"
+ " --set-battery-max-charging-voltage <CV> <FV>\n"
+ " CV: constant voltage (48.0~58.4)\n"
+ " FV: float voltage (48.0~58.4)\n"
+ "\n"
+ " --set-ac-output-rated-voltage <V>\n"
+ " V: one of: "
+ );
+ usageintlist(p18_ac_output_rated_voltages,
+ ARRAY_SIZE(p18_ac_output_rated_voltages));
+ printf("\n\n"
+ " --set-output-source-priority SUB|SBU\n"
+ " SUB for %s\n"
+ " SBU for %s\n",
+ p18_output_source_priority_label(P18_OSP_SOLAR_UTILITY_BATTERY),
+ p18_output_source_priority_label(P18_OSP_SOLAR_BATTERY_UTILITY));
+ printf("\n"
+ " --set-battery-charging-thresholds <CV> <DV>\n"
+ " Sets battery re-charging and re-discharigng voltages when\n"
+ " utility is available.\n"
+ "\n"
+ " CV: re-charging voltage\n"
+ " for 12V unit, one of: ");
+ usagestrlist(p18_battery_util_recharging_voltages_12v_unit,
+ ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit));
+ printf("\n"
+ " for 24V unit, one of: ");
+ usagestrlist(p18_battery_util_recharging_voltages_24v_unit,
+ ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit));
+ printf("\n"
+ " for 48V unit, one of: ");
+ usagestrlist(p18_battery_util_recharging_voltages_48v_unit,
+ ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit));
+ printf("\n"
+ " DV: re-discharging voltage\n"
+ " for 12V unit, one of: ");
+ usagestrlist(p18_battery_util_redischarging_voltages_12v_unit,
+ ARRAY_SIZE(p18_battery_util_redischarging_voltages_12v_unit));
+ printf("\n"
+ " for 24V unit, one of: ");
+ usagestrlist(p18_battery_util_redischarging_voltages_24v_unit,
+ ARRAY_SIZE(p18_battery_util_redischarging_voltages_24v_unit));
+ printf("\n"
+ " for 48V unit, one of: ");
+ usagestrlist(p18_battery_util_redischarging_voltages_48v_unit,
+ ARRAY_SIZE(p18_battery_util_redischarging_voltages_48v_unit));
+ printf("\n\n"
+ " --set-charging-source-priority <ID> <PRIORITY>\n"
+ " ID: parallel machine ID (use 0 for a single model)\n"
+ " PRIORITY:\n"
+ " SF: %s,\n"
+ " SU: %s,\n"
+ " S: %s\n",
+ p18_charge_source_priority_label(P18_CSP_SOLAR_FIRST),
+ p18_charge_source_priority_label(P18_CSP_SOLAR_AND_UTILITY),
+ p18_charge_source_priority_label(P18_CSP_SOLAR_ONLY));
+ printf("\n"
+ " --set-solar-power-priority BLU|LBU\n"
+ " BLU: %s\n"
+ " LBU: %s\n",
+ p18_solar_power_priority_label(P18_SPP_BATTERY_LOAD_UTILITY),
+ p18_solar_power_priority_label(P18_SPP_LOAD_BATTERY_UTILITY));
+ printf("\n"
+ " --set-ac-input-voltage-range APPLIANCE|UPS\n"
+ " --set-battery-type AGM|FLOODED|USER\n"
+ " --set-output-model <ID> <MODEL>\n"
+ " ID: parallel machine ID (use 0 for a single model)\n"
+ " MODEL:\n"
+ " SM: %s\n"
+ " P: %s\n"
+ " P1: %s\n"
+ " P2: %s\n"
+ " P3: %s\n",
+ p18_output_model_setting_label(P18_OMS_SINGLE_MODULE),
+ p18_output_model_setting_label(P18_OMS_PARALLEL_OUTPUT),
+ p18_output_model_setting_label(P18_OMS_PHASE1_OF_3PHASE_OUTPUT),
+ p18_output_model_setting_label(P18_OMS_PHASE2_OF_3PHASE_OUTPUT),
+ p18_output_model_setting_label(P18_OMS_PHASE3_OF_3PHASE_OUTPUT));
+ printf("\n"
+ " --set-battery-cutoff-voltage <V>\n"
+ " V: cut-off voltage (40.0~48.0)\n"
+ "\n"
+ " --set-solar-configuration <ID>\n"
+ " ID: serial number\n"
+ "\n"
+ " --clear-generated-data\n"
+ " Clears all data of generated energy.\n"
+ "\n"
+ " --set-date-time <YYYY> <MM> <DD> <hh> <mm> <ss>\n"
+ " YYYY: year\n"
+ " MM: month\n"
+ " DD: day\n"
+ " hh: hours\n"
+ " mm: minutes\n"
+ " ss: seconds\n"
+ "\n"
+ " --set-ac-charge-time-bucket <START> <END>\n"
+ " START: starting time, hh:mm format\n"
+ " END: ending time, hh:mm format\n"
+ "\n"
+ " --set-ac-supply-load-time-bucket <START> <END>\n"
+ " START: starting time, hh:mm format\n"
+ " END: ending time, hh:mm format\n"
+ "\n"
+ );
+ printf("Flags:\n");
+
+ size_t len = ARRAY_SIZE(p18_flags_printable_list);
+ for (size_t i = 0; i < len; i++) {
+ p18_flag_printable_list_item_t item = p18_flags_printable_list[i];
+ printf(" %s: %s\n", item.key, item.title);
+ }
+
+ printf("\n"
+ "Formats:\n"
+ " table human-readable table\n"
+ " parsable-table conveniently-parsable table\n"
+ " json JSON object, like {\"ac_output_voltage\":230}\n"
+ " json-w-units JSON object with units, like:\n"
+ " {\"ac_output_voltage\":[230,\"V\"]}\n"
+ );
+
+ exit(1);
+}
+
+static void exit_with_error(int code, char *fmt, ...)
+{
+ static const size_t buf_size = 256;
+ char buf[buf_size];
+ va_list args;
+ va_start(args, fmt);
+ size_t len = vsnprintf(buf, buf_size, fmt, args);
+ va_end(args);
+ buf[MIN(len, buf_size-1)] = '\0';
+ ERROR("error: %s\n", buf);
+ if (g_format == PRINT_FORMAT_JSON || g_format == PRINT_FORMAT_JSON_W_UNITS) {
+ print_item_t items[] = {
+ {.key= "error", .value= variant_string(buf)}
+ };
+ print_json(items, 1, false);
+ }
+ exit(code);
+}
+
+static void get_args(int argc, const char **argv, const char **arguments_dst, size_t count)
+{
+ size_t i = 0;
+ arguments_dst[i++] = optarg;
+ for (; i < count; i++) {
+ if (optind < argc && *argv[optind] != '-')
+ arguments_dst[i] = argv[optind++];
+ else
+ exit_with_error(1, "option %s requires %zu arguments\n",
+ argv[optind-i-1], count);
+ }
+}
+
+static void execute_raw(voltronic_dev_t dev, const char *command, int timeout)
+{
+ char buffer[RESPONSE_BUF_LENGTH];
+ int result = voltronic_dev_execute(dev, 0, command, strlen(command),
+ buffer, sizeof(buffer), NULL, timeout);
+ if (result <= 0)
+ exit_with_error(2, "failed to execute %s: %s\n", command, strerror(errno));
+ printf("%s\n", buffer);
+}
+
+static void query(voltronic_dev_t dev,
+ int command_key,
+ int timeout,
+ const char **args,
+ size_t args_size,
+ bool pretend)
+{
+ char buffer[RESPONSE_BUF_LENGTH];
+ char command[COMMAND_BUF_LENGTH];
+
+ if (!p18_build_command(command_key, args, args_size, command))
+ exit_with_error(1, "invalid query command %d\n", command_key);
+
+ if (pretend) {
+ size_t command_len = strlen(command);
+ LOG("would write %zu+3 %s:\n",
+ command_len, (command_len > 1 ? "bytes" : "byte"));
+ HEXDUMP(command, command_len);
+ return;
+ }
+
+ size_t received;
+ int result = voltronic_dev_execute(dev, 0, command, strlen(command),
+ buffer, sizeof(buffer), &received,
+ timeout);
+ if (result <= 0)
+ exit_with_error(2, "failed to execute %s: %s\n", command, strerror(errno));
+
+ if (command_key < P18_SET_CMDS_ENUM_OFFSET) {
+ size_t data_size;
+ if (!p18_validate_query_response(buffer, received, &data_size))
+ exit_with_error(2, "invalid response\n");
+
+ if (command_key == P18_QUERY_PROTOCOL_ID)
+ PRINT(protocol_id)
+ else if (command_key == P18_QUERY_CURRENT_TIME)
+ PRINT(current_time)
+ else if (command_key == P18_QUERY_TOTAL_GENERATED)
+ PRINT(total_generated)
+ else if (command_key == P18_QUERY_YEAR_GENERATED)
+ PRINT(year_generated)
+ else if (command_key == P18_QUERY_MONTH_GENERATED)
+ PRINT(month_generated)
+ else if (command_key == P18_QUERY_DAY_GENERATED)
+ PRINT(day_generated)
+ else if (command_key == P18_QUERY_SERIES_NUMBER)
+ PRINT(series_number)
+ else if (command_key == P18_QUERY_CPU_VERSION)
+ PRINT(cpu_version)
+ else if (command_key == P18_QUERY_RATED_INFORMATION)
+ PRINT(rated_information)
+ else if (command_key == P18_QUERY_GENERAL_STATUS)
+ PRINT(general_status)
+ else if (command_key == P18_QUERY_WORKING_MODE)
+ PRINT(working_mode)
+ else if (command_key == P18_QUERY_FAULTS_WARNINGS)
+ PRINT(faults_warnings)
+ else if (command_key == P18_QUERY_FLAGS_STATUSES)
+ PRINT(flags_statuses)
+ else if (command_key == P18_QUERY_DEFAULTS)
+ PRINT(defaults)
+ else if (command_key == P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES)
+ PRINT(max_charging_current_selectable_values)
+ else if (command_key == P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES)
+ PRINT(max_ac_charging_current_selectable_values)
+ else if (command_key == P18_QUERY_PARALLEL_RATED_INFORMATION)
+ PRINT(parallel_rated_information)
+ else if (command_key == P18_QUERY_PARALLEL_GENERAL_STATUS)
+ PRINT(parallel_general_status)
+ else if (command_key == P18_QUERY_AC_CHARGE_TIME_BUCKET)
+ PRINT(ac_charge_time_bucket)
+ else if (command_key == P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET)
+ PRINT(ac_supply_load_time_bucket)
+ } else {
+ bool success = p18_set_result(buffer, received);
+ print_set_result(success, g_format);
+ if (!success)
+ exit(2);
+ }
+}
+
+static void validate_date_args(const char *ys, const char *ms, const char *ds)
+{
+ static char *err_year = "invalid year";
+ static char *err_month = "invalid month";
+ static char *err_day = "invalid day";
+
+ int y, m = 0, d = 0;
+
+ /* validate year */
+ if (!isnumeric(ys) || strlen(ys) != 4)
+ exit_with_error(1, err_year);
+ y = (int)strtoul(ys, NULL, 10);
+ if (y < 2000 || y > 2099) {
+ ERROR("%s\n", err_year);
+ exit(1);
+ }
+
+ /* validate month */
+ if (ms != NULL) {
+ if (!isnumeric(ms) || strlen(ms) > 2)
+ exit_with_error(1, err_month);
+ m = (int)strtoul(ms, NULL, 10);
+ if (m < 1 || m > 12)
+ exit_with_error(1, err_month);
+ }
+
+ /* validate day */
+ if (ds != NULL) {
+ if (!isnumeric(ds) || strlen(ds) > 2)
+ exit_with_error(1, err_day);
+ d = (int) strtoul(ds, NULL, 10);
+ if (d < 1 || d > 31)
+ exit_with_error(1, err_day);
+ }
+
+ if (y != 0 && m != 0 && d != 0) {
+ if (!isdatevalid(y, m, d))
+ exit_with_error(1, "invalid date");
+ }
+}
+
+static void validate_time_args(const char *hs, const char *ms, const char *ss)
+{
+ static char *err_hour = "invalid hour";
+ static char *err_minute = "invalid minute";
+ static char *err_second = "invalid second";
+
+ unsigned int h, m, s;
+
+ if (!isnumeric(hs) || strlen(hs) > 2)
+ exit_with_error(1, err_hour);
+ h = (unsigned int)strtoul(hs, NULL, 10);
+ if (h > 23)
+ exit_with_error(1, err_hour);
+
+ if (!isnumeric(ms) || strlen(ms) > 2)
+ exit_with_error(1, err_minute);
+ m = (unsigned int)strtoul(ms, NULL, 10);
+ if (m > 59)
+ exit_with_error(1, err_minute);
+
+ if (!isnumeric(ss) || strlen(ss) > 2)
+ exit_with_error(1, err_second);
+ s = (unsigned int)strtoul(ss, NULL, 10);
+ if (s > 59)
+ exit_with_error(1, err_second);
+}
+
+static bool get_float(const char *s, float *fptr)
+{
+ char *endptr;
+ float f = strtof(s, &endptr);
+ if (endptr == s)
+ return false;
+ if (fptr != NULL)
+ *fptr = f;
+ return true;
+}
+
+static bool get_uint(const char *s, unsigned int *iptr)
+{
+ char *endptr;
+ unsigned int i = (unsigned int)strtoul(s, &endptr, 10);
+ if (endptr == s)
+ return false;
+ if (iptr != NULL)
+ *iptr = i;
+ return true;
+}
+
+static bool is_valid_parallel_id(const char *s)
+{
+ return isnumeric(s) && strlen(s) == 1;
+}
+
+enum action {
+ ACTION_HELP,
+ ACTION_DUMP,
+ ACTION_EXECUTE,
+ ACTION_QUERY,
+};
+
+enum {
+ OPT_HELP = 'h',
+ OPT_DUMP = 'd',
+ OPT_VERBOSE = 'v',
+ OPT_RAW = 'r',
+ OPT_PREDENT = 'p',
+ OPT_TIMEOUT = 't',
+ OPT_FORMAT = 'f',
+};
+
+int main(int argc, char *argv[])
+{
+ if (argv[1] == NULL)
+ usage(argv[0]);
+
+ enum action act = ACTION_HELP;
+ int opt;
+ int command_no = 0, timeout = 1000;
+ bool pretend = false;
+ const char *a[6] = {0}; /* p18 command arguments */
+ static struct option long_options[] = {
+ {"help", no_argument, 0, OPT_HELP},
+ {"dump", no_argument, 0, OPT_DUMP},
+ {"verbose", no_argument, 0, OPT_VERBOSE},
+ {"raw", required_argument, 0, OPT_RAW},
+ {"pretend", required_argument, 0, OPT_PREDENT},
+ {"timeout", required_argument, 0, OPT_TIMEOUT},
+ {"format", required_argument, 0, OPT_FORMAT},
+
+ /* get queries */
+ {"get-protocol-id", no_argument, 0, P18_QUERY_PROTOCOL_ID},
+ {"get-date-time", no_argument, 0, P18_QUERY_CURRENT_TIME},
+ {"get-total-generated", no_argument, 0, P18_QUERY_TOTAL_GENERATED},
+ {"get-year-generated", required_argument, 0, P18_QUERY_YEAR_GENERATED},
+ {"get-month-generated", required_argument, 0, P18_QUERY_MONTH_GENERATED},
+ {"get-day-generated", required_argument, 0, P18_QUERY_DAY_GENERATED},
+ {"get-series-number", no_argument, 0, P18_QUERY_SERIES_NUMBER},
+ {"get-cpu-version", no_argument, 0, P18_QUERY_CPU_VERSION},
+ {"get-rated-information", no_argument, 0, P18_QUERY_RATED_INFORMATION},
+ {"get-general-status", no_argument, 0, P18_QUERY_GENERAL_STATUS},
+ {"get-working-mode", no_argument, 0, P18_QUERY_WORKING_MODE},
+ {"get-faults-warnings", no_argument, 0, P18_QUERY_FAULTS_WARNINGS},
+ {"get-flags", no_argument, 0, P18_QUERY_FLAGS_STATUSES},
+ {"get-defaults", no_argument, 0, P18_QUERY_DEFAULTS},
+ {"get-max-charging-current-selectable-values", no_argument, 0, P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES},
+ {"get-max-ac-charging-current-selectable-values", no_argument, 0, P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES},
+ {"get-parallel-rated-information", required_argument, 0, P18_QUERY_PARALLEL_RATED_INFORMATION},
+ {"get-parallel-general-status", required_argument, 0, P18_QUERY_PARALLEL_GENERAL_STATUS},
+ {"get-ac-charge-time-bucket", no_argument, 0, P18_QUERY_AC_CHARGE_TIME_BUCKET},
+ {"get-ac-supply-load-time-bucket", no_argument, 0, P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET},
+
+ /* set queries */
+ {"set-loads-supply", required_argument, 0, P18_SET_LOADS},
+ {"set-flag", required_argument, 0, P18_SET_FLAG},
+ {"set-defaults", no_argument, 0, P18_SET_DEFAULTS},
+ {"set-battery-max-charging-current", required_argument, 0, P18_SET_BAT_MAX_CHARGE_CURRENT},
+ {"set-battery-max-ac-charging-current", required_argument, 0, P18_SET_BAT_MAX_AC_CHARGE_CURRENT},
+ {"set-ac-output-freq", required_argument, 0, P18_SET_AC_OUTPUT_FREQ},
+ {"set-battery-max-charging-voltage", required_argument, 0, P18_SET_BAT_MAX_CHARGE_VOLTAGE},
+ {"set-ac-output-rated-voltage", required_argument, 0, P18_SET_AC_OUTPUT_RATED_VOLTAGE},
+ {"set-output-source-priority", required_argument, 0, P18_SET_OUTPUT_SOURCE_PRIORITY},
+ {"set-battery-charging-thresholds", required_argument, 0, P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL},
+ {"set-charging-source-priority", required_argument, 0, P18_SET_CHARGING_SOURCE_PRIORITY},
+ {"set-solar-power-priority", required_argument, 0, P18_SET_SOLAR_POWER_PRIORITY},
+ {"set-ac-input-voltage-range", required_argument, 0, P18_SET_AC_INPUT_VOLTAGE_RANGE},
+ {"set-battery-type", required_argument, 0, P18_SET_BAT_TYPE},
+ {"set-output-model", required_argument, 0, P18_SET_OUTPUT_MODEL},
+ {"set-battery-cutoff-voltage", required_argument, 0, P18_SET_BAT_CUTOFF_VOLTAGE},
+ {"set-solar-configuration", required_argument, 0, P18_SET_SOLAR_CONFIG},
+ {"clear-generated-data", no_argument , 0, P18_SET_CLEAR_GENERATED},
+ {"set-date-time", required_argument, 0, P18_SET_DATE_TIME},
+ {"set-ac-charge-time-bucket", required_argument, 0, P18_SET_AC_CHARGE_TIME_BUCKET},
+ {"set-ac-supply-load-time-bucket", required_argument, 0, P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET},
+
+ {0, 0, 0, 0}
+ };
+
+ bool getopt_err = false;
+ while ((opt = getopt_long(argc, argv, "hdvr:pt:f:",
+ long_options, NULL)) != EOF) {
+ if (opt == '?') {
+ getopt_err = true;
+ break;
+ }
+
+ if (opt == OPT_HELP)
+ act = ACTION_HELP;
+
+ else if (opt == OPT_DUMP)
+ act = ACTION_DUMP;
+
+ else if (opt == OPT_VERBOSE)
+ g_verbose = true;
+
+ else if (opt == OPT_PREDENT)
+ pretend = true;
+
+ else if (opt == OPT_FORMAT) {
+ if (!strcmp(optarg, "json"))
+ g_format = PRINT_FORMAT_JSON;
+ else if (!strcmp(optarg, "json-w-units"))
+ g_format = PRINT_FORMAT_JSON_W_UNITS;
+ else if (!strcmp(optarg, "table"))
+ g_format = PRINT_FORMAT_TABLE;
+ else if (!strcmp(optarg, "parsable-table"))
+ g_format = PRINT_FORMAT_PARSABLE_TABLE;
+ else
+ exit_with_error(1, "invalid format");
+ }
+
+ else if (opt == OPT_RAW) {
+ if (strlen(optarg) > COMMAND_BUF_LENGTH - 1)
+ exit_with_error(1, "command is too long");
+ a[0] = optarg;
+ act = ACTION_EXECUTE;
+ }
+
+ else if (opt == OPT_TIMEOUT) {
+ timeout = atoi(optarg);
+ if (timeout <= 0 || timeout > 60000)
+ exit_with_error(1, "invalid timeout");
+ }
+
+ else if (opt >= P18_QUERY_CMDS_ENUM_OFFSET) {
+ if (act == ACTION_QUERY)
+ exit_with_error(1, "one query at a time, please");
+
+ if (opt >= P18_QUERY_CMDS_ENUM_OFFSET) {
+ act = ACTION_QUERY;
+ command_no = opt;
+ }
+
+ switch (opt) {
+ case P18_QUERY_YEAR_GENERATED:
+ GET_ARGS(1);
+ validate_date_args(a[0], NULL, NULL);
+ break;
+
+ case P18_QUERY_MONTH_GENERATED:
+ GET_ARGS(2);
+ validate_date_args(a[0], a[1], NULL);
+ break;
+
+ case P18_QUERY_DAY_GENERATED:
+ GET_ARGS(3);
+ validate_date_args(a[0], a[1], a[2]);
+ break;
+
+ case P18_QUERY_PARALLEL_RATED_INFORMATION:
+ case P18_QUERY_PARALLEL_GENERAL_STATUS:
+ GET_ARGS(1);
+ if (!isnumeric(a[0]) || strlen(a[0]) > 1)
+ exit_with_error(1, "invalid argument");
+ break;
+
+ case P18_SET_LOADS:
+ GET_ARGS(1);
+ if (strcmp(a[0], "0") != 0 && strcmp(a[0], "1") != 0)
+ exit_with_error(1, "invalid argument, only 0 or 1 allowed");
+ break;
+
+ case P18_SET_FLAG: {
+ GET_ARGS(2);
+ bool matchfound = false;
+ FOREACH (const p18_flag_printable_list_item_t *item,
+ p18_flags_printable_list) {
+ if (!strcmp(item->key, a[0])) {
+ a[0] = item->p18_key;
+ matchfound = true;
+ break;
+ }
+ }
+ if (!matchfound)
+ exit_with_error(1, "invalid flag");
+ if (strcmp(a[1], "0") != 0 &&
+ strcmp(a[1], "1") != 0)
+ exit_with_error(1, "invalid flag state, only 0 or 1 allowed");
+ break;
+ }
+
+ case P18_SET_BAT_MAX_CHARGE_CURRENT:
+ case P18_SET_BAT_MAX_AC_CHARGE_CURRENT:
+ GET_ARGS(2);
+ if (!is_valid_parallel_id(a[0]))
+ exit_with_error(1, "invalid id");
+ if (!get_uint(a[1], NULL) || strlen(a[1]) > 3)
+ exit_with_error(1, "invalid argument");
+ break;
+
+ case P18_SET_AC_OUTPUT_FREQ:
+ GET_ARGS(1);
+ if (strcmp(a[0], "50") != 0 && strcmp(a[0], "60") != 0)
+ exit_with_error(1, "invalid frequency, only 50 or 60 allowed");
+ break;
+
+ case P18_SET_BAT_MAX_CHARGE_VOLTAGE:
+ GET_ARGS(2);
+ float cv, fv;
+ bool cvr = get_float(a[0], &cv);
+ bool fvr = get_float(a[1], &fv);
+ if (!cvr || cv < 48.0 || cv > 58.4)
+ exit_with_error(1, "invalid CV");
+ if (!fvr || fv < 48.0 || fv > 58.4)
+ exit_with_error(1, "invalid FV");
+ break;
+
+ case P18_SET_AC_OUTPUT_RATED_VOLTAGE: {
+ GET_ARGS(1);
+ unsigned int v;
+ if (!get_uint(a[0], &v))
+ exit_with_error(1, "invalid argument");
+ bool matchfound = false;
+ FOREACH (const int *allowed_v, p18_ac_output_rated_voltages) {
+ if ((unsigned int)*allowed_v == v) {
+ matchfound = true;
+ break;
+ }
+ }
+ if (!matchfound)
+ exit_with_error(1, "invalid voltage");
+ break;
+
+ case P18_SET_OUTPUT_SOURCE_PRIORITY:
+ GET_ARGS(1);
+ const char *allowed[] = {"SUB", "SBU"};
+ int index;
+ if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid argument");
+ write_num((char *)a[0], index);
+ break;
+
+ case P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL:
+ GET_ARGS(2);
+ if ( !instrarray(a[0], p18_battery_util_recharging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit), NULL)
+ && !instrarray(a[0], p18_battery_util_recharging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit), NULL)
+ && !instrarray(a[0], p18_battery_util_recharging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit), NULL))
+ exit_with_error(1, "invalid CV");
+ if ( !instrarray(a[1], p18_battery_util_redischarging_voltages_12v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_12v_unit), NULL)
+ && !instrarray(a[1], p18_battery_util_redischarging_voltages_24v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_24v_unit), NULL)
+ && !instrarray(a[1], p18_battery_util_redischarging_voltages_48v_unit, ARRAY_SIZE(p18_battery_util_recharging_voltages_48v_unit), NULL))
+ exit_with_error(1, "invalid DV");
+ break;
+ }
+
+ case P18_SET_CHARGING_SOURCE_PRIORITY: {
+ GET_ARGS(2);
+ if (!is_valid_parallel_id(a[0]))
+ exit_with_error(1, "invalid id");
+ const char *allowed[] = {"SF", "SU", "S"};
+ int index;
+ if (!instrarray(a[1], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid priority");
+ write_num((char *)a[1], index);
+ break;
+ }
+
+ case P18_SET_SOLAR_POWER_PRIORITY: {
+ GET_ARGS(1);
+ const char *allowed[] = {"BLU", "LBU"};
+ int index;
+ if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid priority");
+ write_num((char *)a[0], index);
+ break;
+ }
+
+ case P18_SET_AC_INPUT_VOLTAGE_RANGE: {
+ GET_ARGS(1);
+ const char *allowed[] = {"APPLIANCE", "UPS"};
+ int index;
+ if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid argument");
+ write_num((char *)a[0], index);
+ break;
+ }
+
+ case P18_SET_BAT_TYPE: {
+ GET_ARGS(1);
+ const char *allowed[] = {"AGM", "FLOODED", "USER"};
+ int index;
+ if (!instrarray(a[0], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid type");
+ write_num((char *)a[0], index);
+ break;
+ }
+
+ case P18_SET_OUTPUT_MODEL: {
+ GET_ARGS(2);
+ if (!is_valid_parallel_id(a[0]))
+ exit_with_error(1, "invalid id");
+ const char *allowed[] = {"SM", "P", "P1", "P2", "P3"};
+ int index;
+ if (!instrarray(a[1], allowed, ARRAY_SIZE(allowed), &index))
+ exit_with_error(1, "invalid model");
+ write_num((char *)a[1], index);
+ break;
+ }
+
+ case P18_SET_BAT_CUTOFF_VOLTAGE:
+ GET_ARGS(1);
+ float v;
+ bool vr = get_float(a[0], &v);
+ if (!vr || v < 40.0 || v > 48.0)
+ exit_with_error(1, "invalid voltage");
+ break;
+
+ case P18_SET_SOLAR_CONFIG:
+ GET_ARGS(1);
+ if (!isnumeric(a[0]) || strlen(a[0]) > 20)
+ exit_with_error(1, "invalid argument");
+ break;
+
+ case P18_SET_DATE_TIME:
+ GET_ARGS(6);
+ validate_date_args(a[0], a[1], a[2]);
+ validate_time_args(a[3], a[4], a[5]);
+ break;
+
+ case P18_SET_AC_CHARGE_TIME_BUCKET:
+ case P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET:
+ GET_ARGS(2);
+ unsigned short start_h, start_m, end_h, end_m;
+ int results;
+
+ results = sscanf(a[0], "%hu:%hu", &start_h, &start_m);
+ if (results != 2 || start_h > 23 || start_m > 59)
+ exit_with_error(1, "invalid start time");
+
+ results = sscanf(a[1], "%hu:%hu", &end_h, &end_m);
+ if (results != 2 || end_h > 23 || end_m > 59)
+ exit_with_error(1, "invalid end time");
+
+ char *start_col = strchr(a[0], ':');
+ char *end_col = strchr(a[1], ':');
+ *start_col = '\0';
+ *end_col = '\0';
+
+ char *start_m_ptr = start_col+1;
+ char *end_h_ptr = (char *)a[1];
+ char *end_m_ptr = end_col+1;
+
+ a[1] = start_m_ptr;
+ a[2] = end_h_ptr;
+ a[3] = end_m_ptr;
+ break;
+ }
+ }
+ }
+
+ if (optind < argc)
+ exit_with_error(1, "extra parameter found");
+
+ if (getopt_err)
+ exit(1);
+
+ if (act == ACTION_HELP)
+ usage(argv[0]);
+
+ voltronic_dev_t dev = voltronic_usb_create(0x0665, 0x5161);
+
+ if (!pretend && !dev)
+ exit_with_error(1, "could not open USB device: %s", strerror(errno));
+
+ switch (act) {
+ case ACTION_EXECUTE:
+ execute_raw(dev, a[0], timeout);
+ break;
+
+ case ACTION_QUERY:
+ query(dev, command_no, timeout, a, sizeof(a), pretend);
+ break;
+
+ default:
+ exit_with_error(1, "unexpected act %d", act);
+ }
+
+ if (dev)
+ voltronic_dev_close(dev);
+
+ return 0;
+} \ No newline at end of file
diff --git a/libvoltronic/voltronic_crc.c b/libvoltronic/voltronic_crc.c
new file mode 100644
index 0000000..3f40be9
--- /dev/null
+++ b/libvoltronic/voltronic_crc.c
@@ -0,0 +1,99 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "voltronic_crc.h"
+
+#define IS_INTEGER_EQUAL(_ch__a_, _ch__b_) \
+ ((_ch__a_) == (_ch__b_))
+
+#define IS_RESERVED_BYTE(_ch_) ( \
+ IS_INTEGER_EQUAL((_ch_), 0x28) || \
+ IS_INTEGER_EQUAL((_ch_), 0x0D) || \
+ IS_INTEGER_EQUAL((_ch_), 0x0A))
+
+#define CRC_SIZE \
+ (sizeof(voltronic_crc_t))
+
+int write_voltronic_crc(
+ const voltronic_crc_t crc,
+ char* cstring_buffer) {
+
+ if (cstring_buffer != 0) {
+ unsigned char* buffer =
+ (unsigned char*) cstring_buffer;
+
+ buffer[0] = (crc >> 8) & 0xFF;
+ buffer[1] = crc & 0xFF;
+ }
+
+ return CRC_SIZE;
+}
+
+voltronic_crc_t read_voltronic_crc(
+ const char* cstring_buffer) {
+
+ const unsigned char* buffer =
+ (const unsigned char*) cstring_buffer;
+
+ voltronic_crc_t crc = 0;
+
+ crc |= (voltronic_crc_t) buffer[0] << 8;
+ crc |= (voltronic_crc_t) buffer[1];
+
+ return crc;
+}
+
+voltronic_crc_t calculate_voltronic_crc(
+ const char* cstring_buffer,
+ size_t buffer_length) {
+
+ voltronic_crc_t crc = 0;
+ if (buffer_length > 0) {
+
+ static const voltronic_crc_t crc_table[16] = {
+ 0x0000, 0x1021, 0x2042, 0x3063,
+ 0x4084, 0x50A5, 0x60C6, 0x70E7,
+ 0x8108, 0x9129, 0xA14A, 0xB16B,
+ 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF
+ };
+
+ const unsigned char* buffer =
+ (const unsigned char*) cstring_buffer;
+
+ unsigned char byte;
+ do {
+ byte = *buffer;
+
+ crc = crc_table[(crc >> 12) ^ (byte >> 4)] ^ (crc << 4);
+ crc = crc_table[(crc >> 12) ^ (byte & 0x0F)] ^ (crc << 4);
+
+ buffer += sizeof(unsigned char);
+ } while(--buffer_length);
+
+ byte = crc;
+ if (IS_RESERVED_BYTE(byte)) {
+ crc += 1;
+ }
+
+ byte = crc >> 8;
+ if (IS_RESERVED_BYTE(byte)) {
+ crc += 1 << 8;
+ }
+ }
+
+ return crc;
+}
diff --git a/libvoltronic/voltronic_crc.h b/libvoltronic/voltronic_crc.h
new file mode 100644
index 0000000..750f048
--- /dev/null
+++ b/libvoltronic/voltronic_crc.h
@@ -0,0 +1,71 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __VOLTRONIC__CRC__H__
+#define __VOLTRONIC__CRC__H__
+
+ #include <stddef.h>
+
+ /**
+ * The underlying numeric type used to store the CRC
+ */
+ #if defined(_WIN32) || defined(WIN32)
+ #include "windows.h"
+
+ typedef unsigned __int16 voltronic_crc_t;
+ #else
+ #include <stdint.h>
+
+ typedef uint16_t voltronic_crc_t;
+ #endif
+
+ /**
+ * Write a voltronic_crc_t to a buffer
+ *
+ * crc - CRC to write
+ * buffer - Buffer to write CRC to or NULL/0
+ *
+ * Returns the size of the CRC
+ */
+ int write_voltronic_crc(
+ const voltronic_crc_t crc,
+ char* buffer);
+
+ /**
+ * Read a voltronic_crc_t from a buffer
+ *
+ * buffer - Buffer to read CRC from;
+ * If the buffer is smaller than the size
+ * returned by write_voltronic_crc behavoir is undefined
+ *
+ * Returns the CRC read from the buffer
+ */
+ voltronic_crc_t read_voltronic_crc(const char* buffer);
+
+ /**
+ * Calculate the Voltronic CRC by reading a buffer and calculating the CRC from the bytes
+ *
+ * buffer - Buffer to read from
+ * buffer_length - Number of bytes in the buffer
+ *
+ * Returns the CRC created from the bytes in the buffer
+ */
+ voltronic_crc_t calculate_voltronic_crc(
+ const char* buffer,
+ size_t buffer_length);
+
+#endif
diff --git a/libvoltronic/voltronic_dev.c b/libvoltronic/voltronic_dev.c
new file mode 100644
index 0000000..d217b22
--- /dev/null
+++ b/libvoltronic/voltronic_dev.c
@@ -0,0 +1,430 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ * Copyright (C) 2020 Evgeny Zinoviev
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "voltronic_dev.h"
+#include "voltronic_dev_impl.h"
+#include "voltronic_crc.h"
+#include "../util.h"
+
+#if defined(_WIN32) || defined(WIN32)
+
+ #include "windows.h"
+
+ typedef DWORD millisecond_timestamp_t;
+
+#elif defined(__APPLE__)
+
+ #include <mach/mach_time.h>
+
+ typedef uint64_t millisecond_timestamp_t;
+
+#elif defined(ARDUINO)
+
+ #include <Arduino.h>
+
+ typedef unsigned long millisecond_timestamp_t;
+
+#else
+
+ #include <time.h>
+ #include <sys/time.h>
+ #include <stdint.h>
+ #include <unistd.h>
+
+ typedef uint64_t millisecond_timestamp_t;
+
+#endif
+
+#define END_OF_INPUT '\r'
+#define END_OF_INPUT_SIZE sizeof(char)
+#define NON_DATA_SIZE (sizeof(voltronic_crc_t) + END_OF_INPUT_SIZE)
+
+#define GET_IMPL_DEV(_voltronic_dev_t_) \
+ ((void*) (_voltronic_dev_t_))
+
+#if defined(_WIN32) || defined(WIN32)
+
+ #define SET_TIMEOUT_REACHED() SET_LAST_ERROR(WAIT_TIMEOUT)
+ #define SET_BUFFER_OVERFLOW() SET_LAST_ERROR(ERROR_INSUFFICIENT_BUFFER)
+ #define SET_CRC_ERROR() SET_LAST_ERROR(ERROR_CRC)
+ #define SYSTEM_NOT_SUPPORTED() SET_LAST_ERROR(ERROR_CALL_NOT_IMPLEMENTED)
+
+#else
+
+ #define SET_TIMEOUT_REACHED() SET_LAST_ERROR(ETIMEDOUT)
+ #define SET_BUFFER_OVERFLOW() SET_LAST_ERROR(ENOBUFS)
+ #define SET_CRC_ERROR() SET_LAST_ERROR(EBADMSG)
+ #define SYSTEM_NOT_SUPPORTED() SET_LAST_ERROR(ENOSYS)
+
+#endif
+
+static millisecond_timestamp_t get_millisecond_timestamp(void);
+static int is_platform_supported_by_libvoltronic(void);
+
+int voltronic_dev_read(
+ const voltronic_dev_t dev,
+ char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds) {
+
+ if (buffer_size > 0) {
+ const int result = voltronic_dev_impl_read(
+ GET_IMPL_DEV(dev),
+ buffer,
+ buffer_size,
+ timeout_milliseconds);
+
+ return result >= 0 ? result : -1;
+ } else {
+ return 0;
+ }
+}
+
+int voltronic_dev_write(
+ const voltronic_dev_t dev,
+ const char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds) {
+
+ if (buffer_size > 0) {
+ const int result = voltronic_dev_impl_write(
+ GET_IMPL_DEV(dev),
+ buffer,
+ buffer_size,
+ timeout_milliseconds);
+
+ return result >= 0 ? result : -1;
+ } else {
+ return 0;
+ }
+}
+
+int voltronic_dev_close(voltronic_dev_t dev) {
+ if (dev != 0) {
+ const int result = voltronic_dev_impl_close(GET_IMPL_DEV(dev));
+ return result > 0 ? 1 : 0;
+ } else {
+ SET_INVALID_INPUT();
+ return 0;
+ }
+}
+
+static int voltronic_read_data_loop(
+ const voltronic_dev_t dev,
+ char* buffer,
+ size_t buffer_length,
+ const unsigned int timeout_milliseconds) {
+
+ unsigned int size = 0;
+
+ const millisecond_timestamp_t start_time = get_millisecond_timestamp();
+ millisecond_timestamp_t elapsed = 0;
+
+ while(1) {
+ int bytes_read = voltronic_dev_read(
+ dev,
+ buffer,
+ buffer_length,
+ timeout_milliseconds - elapsed);
+
+ if (bytes_read >= 0) {
+ while(bytes_read) {
+ --bytes_read;
+ ++size;
+
+ if (*buffer == END_OF_INPUT) {
+ return size;
+ }
+
+ buffer += sizeof(char);
+ --buffer_length;
+ }
+
+ elapsed = get_millisecond_timestamp() - start_time;
+ if (elapsed >= timeout_milliseconds) {
+ SET_TIMEOUT_REACHED();
+ return -1;
+ }
+
+ if (buffer_length <= 0) {
+ SET_BUFFER_OVERFLOW();
+ return -1;
+ }
+ } else {
+ return bytes_read;
+ }
+ }
+}
+
+static int voltronic_receive_data(
+ const voltronic_dev_t dev,
+ const unsigned int options,
+ char *buffer,
+ const size_t buffer_length,
+ size_t *received,
+ const unsigned int timeout_milliseconds) {
+
+ const int result = voltronic_read_data_loop(
+ dev,
+ buffer,
+ buffer_length,
+ timeout_milliseconds);
+
+ if (result >= 0) {
+ if (received)
+ *received = result;
+
+ LOG("%s: got %d %s:\n",
+ __func__, result, (result > 1 ? "bytes" : "byte"));
+ HEXDUMP(buffer, result);
+
+ if ((options & DISABLE_PARSE_VOLTRONIC_CRC) == 0) {
+ if (((size_t) result) >= NON_DATA_SIZE) {
+ const size_t data_size = result - NON_DATA_SIZE;
+ const voltronic_crc_t read_crc = read_voltronic_crc(&buffer[data_size]);
+ const voltronic_crc_t calculated_crc = calculate_voltronic_crc(buffer, data_size);
+ buffer[data_size] = 0;
+
+ if (((options & DISABLE_VERIFY_VOLTRONIC_CRC) == 0) ||
+ (read_crc == calculated_crc)) {
+
+ return data_size;
+ }
+ }
+
+ SET_CRC_ERROR();
+ } else {
+ if (((size_t) result) >= END_OF_INPUT_SIZE) {
+ const size_t data_size = result - END_OF_INPUT_SIZE;
+ buffer[data_size] = 0;
+ return data_size;
+ }
+ }
+ } else {
+ if (received)
+ *received = 0;
+
+ return result;
+ }
+
+ return -1;
+}
+
+static int voltronic_write_data_loop(
+ const voltronic_dev_t dev,
+ const char* buffer,
+ size_t buffer_length,
+ const unsigned int timeout_milliseconds) {
+
+ const millisecond_timestamp_t start_time = get_millisecond_timestamp();
+ millisecond_timestamp_t elapsed = 0;
+
+ int bytes_left = buffer_length;
+ while(1) {
+ const int write_result = voltronic_dev_write(dev, buffer, bytes_left, timeout_milliseconds);
+
+ if (write_result >= 0) {
+ bytes_left -= write_result;
+ if (bytes_left > 0) {
+ buffer = &buffer[write_result];
+ } else {
+ return buffer_length;
+ }
+
+ elapsed = get_millisecond_timestamp() - start_time;
+ if (elapsed >= timeout_milliseconds) {
+ SET_TIMEOUT_REACHED();
+ return -1;
+ }
+ } else {
+ return write_result;
+ }
+ }
+}
+
+static int voltronic_send_data(
+ const voltronic_dev_t dev,
+ const unsigned int options,
+ const char* buffer,
+ const size_t buffer_length,
+ const unsigned int timeout_milliseconds) {
+
+ size_t copy_length;
+ char* copy;
+ if ((options & DISABLE_WRITE_VOLTRONIC_CRC) == 0) {
+ const voltronic_crc_t crc = calculate_voltronic_crc(buffer, buffer_length);
+
+ copy_length = buffer_length + NON_DATA_SIZE;
+ copy = (char*) ALLOCATE_MEMORY(copy_length * sizeof(char));
+
+ write_voltronic_crc(crc, &copy[buffer_length]);
+ } else {
+ copy_length = buffer_length + END_OF_INPUT_SIZE;
+ copy = (char*) ALLOCATE_MEMORY(copy_length * sizeof(char));
+ }
+
+ COPY_MEMORY(copy, buffer, buffer_length * sizeof(char));
+ copy[copy_length - 1] = END_OF_INPUT;
+
+ LOG("%s: writing %zu %s:\n",
+ __func__, copy_length, (copy_length > 1 ? "bytes" : "byte"));
+ HEXDUMP(copy, copy_length);
+
+ const int result = voltronic_write_data_loop(
+ dev,
+ copy,
+ copy_length,
+ timeout_milliseconds);
+
+ FREE_MEMORY(copy);
+
+ return result;
+
+}
+
+int voltronic_dev_execute(
+ const voltronic_dev_t dev,
+ const unsigned int options,
+ const char *send_buffer,
+ size_t send_buffer_length,
+ char *receive_buffer,
+ size_t receive_buffer_length,
+ size_t *received,
+ const unsigned int timeout_milliseconds) {
+
+ const millisecond_timestamp_t start_time = get_millisecond_timestamp();
+ millisecond_timestamp_t elapsed = 0;
+ int result;
+
+ result = voltronic_send_data(
+ dev,
+ options,
+ send_buffer,
+ send_buffer_length,
+ timeout_milliseconds);
+
+ if (result > 0) {
+ elapsed = get_millisecond_timestamp() - start_time;
+ if (elapsed < timeout_milliseconds) {
+ result = voltronic_receive_data(
+ dev,
+ options,
+ receive_buffer,
+ receive_buffer_length,
+ received,
+ timeout_milliseconds - elapsed);
+
+ if (result > 0) {
+ return result;
+ }
+ } else {
+ SET_TIMEOUT_REACHED();
+ }
+ }
+
+ return 0;
+}
+
+voltronic_dev_t voltronic_dev_internal_create(void* impl_ptr) {
+ if (is_platform_supported_by_libvoltronic()) {
+ if (impl_ptr != 0) {
+ return ((voltronic_dev_t) (impl_ptr));
+ }
+ }
+
+ if (impl_ptr != 0) {
+ voltronic_dev_impl_close(impl_ptr);
+ }
+
+ return 0;
+}
+
+#if defined(_WIN32) || defined(WIN32)
+
+ static millisecond_timestamp_t get_millisecond_timestamp(void) {
+ return (millisecond_timestamp_t) GetTickCount();
+ }
+
+#elif defined(__APPLE__)
+
+ static millisecond_timestamp_t get_millisecond_timestamp(void) {
+ return (millisecond_timestamp_t) mach_absolute_time();
+ }
+
+#elif defined(ARDUINO)
+
+ static millisecond_timestamp_t get_millisecond_timestamp(void) {
+ return (millisecond_timestamp_t) millis();
+ }
+
+#else
+
+ static millisecond_timestamp_t get_millisecond_timestamp(void) {
+ millisecond_timestamp_t milliseconds = 0;
+
+ #if defined(CLOCK_MONOTONIC)
+
+ static int monotonic_clock_error = 0;
+ if (monotonic_clock_error == 0) {
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
+ milliseconds = (millisecond_timestamp_t) ts.tv_sec;
+ milliseconds *= 1000;
+ milliseconds += (millisecond_timestamp_t) (ts.tv_nsec / 1000000);
+ return milliseconds;
+ } else {
+ monotonic_clock_error = 1;
+ }
+ }
+
+ #endif
+
+ struct timeval tv;
+ if (gettimeofday(&tv, 0) == 0) {
+ milliseconds = (millisecond_timestamp_t) tv.tv_sec;
+ milliseconds *= 1000;
+ milliseconds += (millisecond_timestamp_t) (tv.tv_usec / 1000);
+ }
+
+ return milliseconds;
+ }
+
+#endif
+
+static int is_platform_supported_by_libvoltronic(void) {
+ /**
+ * Operating system/cpu architecture validations
+ * If any of these fail, things don't behave the code expects
+ */
+ if ((sizeof(char) == sizeof(unsigned char)) &&
+ (sizeof(unsigned char) == 1) &&
+ (sizeof(int) >= 2) &&
+ (sizeof(unsigned int) >= 2) &&
+ (sizeof(voltronic_crc_t) == 2) &&
+ (sizeof(millisecond_timestamp_t) >= 4)) {
+
+ return 1;
+ } else {
+ SYSTEM_NOT_SUPPORTED();
+ return 0;
+ }
+}
diff --git a/libvoltronic/voltronic_dev.h b/libvoltronic/voltronic_dev.h
new file mode 100644
index 0000000..be301b8
--- /dev/null
+++ b/libvoltronic/voltronic_dev.h
@@ -0,0 +1,109 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __VOLTRONIC__DEV__H__
+#define __VOLTRONIC__DEV__H__
+
+#include <stddef.h>
+
+/**
+ * Opaque pointer to a voltronic device
+ */
+typedef void* voltronic_dev_t;
+
+/**
+ * Read bytes from a voltronic device
+ *
+ * dev -> Opaque device pointer
+ * buffer -> Buffer where read bytes are written to
+ * buffer_size -> Maximum number of bytes to read
+ * timeout_milliseconds -> Number of milliseconds to wait for a single byte before giving up
+ *
+ * Returns the number of bytes read
+ * Returns 0 on timeout
+ * Returns -1 on error
+ *
+ * Function sets errno (POSIX)/LastError (Windows) to approriate error on failure
+ */
+int voltronic_dev_read(
+ const voltronic_dev_t dev,
+ char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds);
+
+/**
+ * Write bytes to a voltronic device
+ *
+ * dev -> Opaque device pointer
+ * buffer -> Buffer of bytes to write to device
+ * buffer_size -> Number of bytes to write
+ * timeout_milliseconds -> Number of milliseconds to wait before giving up
+ *
+ * Returns the number of bytes written
+ * Returns 0 on timeout
+ * Returns -1 on error
+ *
+ * Function sets errno (POSIX)/LastError (Windows) to approriate error on failure
+ */
+int voltronic_dev_write(
+ const voltronic_dev_t dev,
+ const char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds);
+
+/**
+ * Options for voltronic_dev_execute
+ */
+#define VOLTRONIC_EXECUTE_DEFAULT_OPTIONS (0)
+#define DISABLE_WRITE_VOLTRONIC_CRC (1 << 0)
+#define DISABLE_PARSE_VOLTRONIC_CRC (1 << 1)
+#define DISABLE_VERIFY_VOLTRONIC_CRC (1 << 2)
+
+/**
+ * Write a command to the device and wait for a response from the device
+ *
+ * dev -> Opaque device pointer
+ * send_buffer -> Buffer of bytes to write to device
+ * send_buffer_length -> Number of bytes to write
+ * receive_buffer -> Buffer where read bytes are written to
+ * receive_buffer_length -> Maximum number of bytes to read
+ * timeout_milliseconds -> Number of milliseconds to wait before giving up
+ * options -> See options above
+ *
+ * Returns 1 on success and 0 on failure
+ *
+ * Function sets errno (POSIX)/LastError (Windows) to approriate error on failure
+ */
+int voltronic_dev_execute(
+ const voltronic_dev_t dev,
+ const unsigned int options,
+ const char *send_buffer,
+ size_t send_buffer_length,
+ char *receive_buffer,
+ size_t receive_buffer_length,
+ size_t *received,
+ const unsigned int timeout_milliseconds);
+
+/**
+ * Close the connection to the device
+ *
+ * dev -> Opaque device pointer
+ */
+int voltronic_dev_close(
+ voltronic_dev_t dev);
+
+#endif
diff --git a/libvoltronic/voltronic_dev_impl.h b/libvoltronic/voltronic_dev_impl.h
new file mode 100644
index 0000000..287734d
--- /dev/null
+++ b/libvoltronic/voltronic_dev_impl.h
@@ -0,0 +1,125 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __VOLTRONIC__DEV__IMPL__H__
+#define __VOLTRONIC__DEV__IMPL__H__
+
+ /**
+ * ------------------------------------------------------------------
+ * ------------------------------------------------------------------
+ * Used internally by implementations of voltronic_dev.h
+ * Don't include unless you are building an implementation
+ * ------------------------------------------------------------------
+ * ------------------------------------------------------------------
+ */
+
+ #include "voltronic_dev.h"
+
+ /**
+ * Read up to buffer_size bytes from the device
+ *
+ * impl_ptr -> The underlying implementation's device pointer
+ * buffer -> The buffer to store data from the device
+ * buffer_size -> The maximum number of bytes to read
+ * timeout_milliseconds -> Number of milliseconds before giving up
+ *
+ * Return the number of bytes successfully read from the device.
+ * On failure returns < 0
+ *
+ * On failure set the appropriate error using SET_LAST_ERROR
+ */
+ int voltronic_dev_impl_read(
+ void* impl_ptr,
+ char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds);
+
+ /**
+ * Write the provided buffer data to the device
+ *
+ * impl_ptr -> The underlying implementation's device pointer
+ * buffer -> The data to write to the device
+ * buffer_size -> Number of bytes to write to the device
+ * timeout_milliseconds -> Number of milliseconds before giving up
+ *
+ * Return the number of bytes successfully written to the device.
+ * On failure returns < 0
+ *
+ * On failure set the appropriate error using SET_LAST_ERROR
+ */
+ int voltronic_dev_impl_write(
+ void* impl_ptr,
+ const char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds);
+
+ /**
+ * Accept the implementation pointer and close the underlying device connection
+ *
+ * Returns 0 on failure, anything else is considered success
+ *
+ * On failure set the appropriate error using SET_LAST_ERROR
+ */
+ int voltronic_dev_impl_close(
+ void* impl_ptr);
+
+ /**
+ * Create the opaque pointer representing a connection to a physical voltronic device
+ *
+ * impl_ptr -> The underlying implementation's device pointer
+ *
+ * On failure sets the appropriate error using SET_LAST_ERROR
+ */
+ voltronic_dev_t voltronic_dev_internal_create(
+ void* impl_ptr);
+
+ /**
+ * May change if operating system requires it.
+ */
+ #define ALLOCATE_MEMORY(__size__) \
+ malloc(((size_t) (__size__)))
+
+ #define COPY_MEMORY(__destination__, __source__, __size__) \
+ memcpy(((void*) (__destination__)), \
+ ((const void*) (__source__)), \
+ ((size_t) (__size__)))
+
+ #define FREE_MEMORY(__ptr__) \
+ free(((void*) (__ptr__)))
+
+ #if defined(_WIN32) || defined(WIN32)
+ #include "windows.h"
+
+ typedef DWORD last_error_t;
+
+ #define SET_LAST_ERROR(_last_error_value_) SetLastError((_last_error_value_))
+ #define GET_LAST_ERROR() GetLastError()
+ #define SET_INVALID_INPUT() SET_LAST_ERROR(ERROR_INVALID_DATA)
+
+ #else
+
+ #include <errno.h>
+
+ typedef int last_error_t;
+
+ #define SET_LAST_ERROR(_errno__value_) errno = (_errno__value_)
+ #define GET_LAST_ERROR() (errno)
+ #define SET_INVALID_INPUT() SET_LAST_ERROR(EINVAL)
+
+ #endif
+
+#endif
diff --git a/libvoltronic/voltronic_dev_serial.h b/libvoltronic/voltronic_dev_serial.h
new file mode 100644
index 0000000..ddf6562
--- /dev/null
+++ b/libvoltronic/voltronic_dev_serial.h
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __VOLTRONIC__DEV__SERIAL__H__
+#define __VOLTRONIC__DEV__SERIAL__H__
+
+ #include "voltronic_dev.h"
+
+ /**
+ * The serial port baud rate configuration
+ */
+ typedef unsigned int baud_rate_t;
+
+ /**
+ * The serial port data bits configuration
+ */
+ typedef enum {
+ DATA_BITS_FIVE,
+ DATA_BITS_SIX,
+ DATA_BITS_SEVEN,
+ DATA_BITS_EIGHT
+ } data_bits_t;
+
+ /**
+ * The serial port stop bits configuration
+ */
+ typedef enum {
+ STOP_BITS_ONE,
+ STOP_BITS_ONE_AND_ONE_HALF,
+ STOP_BITS_TWO
+ } stop_bits_t;
+
+ /**
+ * The serial port parity configuration
+ */
+ typedef enum {
+ SERIAL_PARITY_NONE,
+ SERIAL_PARITY_ODD,
+ SERIAL_PARITY_EVEN,
+ SERIAL_PARITY_MARK,
+ SERIAL_PARITY_SPACE
+ } serial_parity_t;
+
+ /**
+ * Create an opaque pointer to a voltronic device connected over serial
+ *
+ * name - The device name, ie. COM1; /dev/usb.serial; etc.
+ * baud_rate - Baud rate configuration, ie. 2400
+ * data_bits - Data bits configuration, ie. DATA_BITS_EIGHT
+ * stop_bits - Stop bits configuration, ie. STOP_BITS_ONE
+ * parity - Parity configuration, ie. SERIAL_PARITY_NONE
+ *
+ * Returns an opaque pointer to a voltronic device or 0 if an error occurred
+ *
+ * Function sets errno (POSIX)/LastError (Windows) to approriate error on failure
+ */
+ voltronic_dev_t voltronic_serial_create(
+ const char* name,
+ const baud_rate_t baud_rate,
+ const data_bits_t data_bits,
+ const stop_bits_t stop_bits,
+ const serial_parity_t parity);
+
+#endif
diff --git a/libvoltronic/voltronic_dev_serial_libserialport.c b/libvoltronic/voltronic_dev_serial_libserialport.c
new file mode 100644
index 0000000..c388346
--- /dev/null
+++ b/libvoltronic/voltronic_dev_serial_libserialport.c
@@ -0,0 +1,196 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "voltronic_dev_impl.h"
+#include "voltronic_dev_serial.h"
+#include <libserialport.h>
+
+#define VOLTRONIC_DEV_SP(_impl_ptr_) ((struct sp_port*) (_impl_ptr_))
+
+static int voltronic_dev_serial_configure(
+ struct sp_port* port,
+ const baud_rate_t baud_rate,
+ const data_bits_t data_bits,
+ const stop_bits_t stop_bits,
+ const serial_parity_t parity);
+
+voltronic_dev_t voltronic_serial_create(
+ const char* name,
+ const baud_rate_t baud_rate,
+ const data_bits_t data_bits,
+ const stop_bits_t stop_bits,
+ const serial_parity_t parity) {
+
+ enum sp_return sp_result;
+ struct sp_port* port = 0;
+ if (name != 0) {
+ struct sp_port* tmp_port = { 0 };
+ SET_LAST_ERROR(0);
+ if ((sp_result = sp_get_port_by_name(name, &tmp_port)) == SP_OK) {
+ port = tmp_port;
+ }
+ }
+
+ if (port != 0) {
+ SET_LAST_ERROR(0);
+ if ((sp_result = sp_open(port, SP_MODE_READ_WRITE)) == SP_OK) {
+ SET_LAST_ERROR(0);
+ if (voltronic_dev_serial_configure(port, baud_rate, data_bits, stop_bits, parity) > 0) {
+ sp_flush(port, SP_BUF_BOTH);
+ SET_LAST_ERROR(0);
+ return voltronic_dev_internal_create((void*) port);
+ }
+ }
+
+ const last_error_t last_error = GET_LAST_ERROR();
+ sp_free_port(port);
+ SET_LAST_ERROR(last_error);
+ }
+
+ return 0;
+}
+
+inline int voltronic_dev_impl_read(
+ void* impl_ptr,
+ char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds) {
+
+ SET_LAST_ERROR(0);
+ return (int) sp_blocking_read_next(
+ VOLTRONIC_DEV_SP(impl_ptr),
+ (void*) buffer,
+ buffer_size,
+ (unsigned int) timeout_milliseconds);
+}
+
+inline int voltronic_dev_impl_write(
+ void* impl_ptr,
+ const char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds) {
+
+ SET_LAST_ERROR(0);
+ return (int) sp_blocking_write(
+ VOLTRONIC_DEV_SP(impl_ptr),
+ (const void*) buffer,
+ buffer_size,
+ (unsigned int) timeout_milliseconds);
+}
+
+inline int voltronic_dev_impl_close(void* impl_ptr) {
+ struct sp_port* sp_port = VOLTRONIC_DEV_SP(impl_ptr);
+ SET_LAST_ERROR(0);
+ const enum sp_return result = sp_close(sp_port);
+ if (result == SP_OK) {
+ sp_free_port(sp_port);
+ return 1;
+ } else {
+ return 0;
+ }
+}
+
+static inline int voltronic_dev_baud_rate(
+ const baud_rate_t baud_rate) {
+
+ return (int) baud_rate;
+}
+
+static inline int voltronic_dev_data_bits(
+ const data_bits_t data_bits) {
+
+ switch(data_bits){
+ case DATA_BITS_FIVE: return 5;
+ case DATA_BITS_SIX: return 6;
+ case DATA_BITS_SEVEN: return 7;
+ case DATA_BITS_EIGHT: return 8;
+ default: return -1;
+ }
+}
+
+static inline int voltronic_dev_stop_bits(
+ const stop_bits_t stop_bits) {
+
+ switch(stop_bits){
+ case STOP_BITS_ONE: return 1;
+ case STOP_BITS_TWO: return 2;
+ case STOP_BITS_ONE_AND_ONE_HALF: return 3;
+ default: return -1;
+ }
+}
+
+static inline enum sp_parity voltronic_dev_serial_parity(
+ const serial_parity_t parity) {
+
+ switch(parity){
+ case SERIAL_PARITY_NONE: return SP_PARITY_NONE;
+ case SERIAL_PARITY_ODD: return SP_PARITY_ODD;
+ case SERIAL_PARITY_EVEN: return SP_PARITY_EVEN;
+ case SERIAL_PARITY_MARK: return SP_PARITY_MARK;
+ case SERIAL_PARITY_SPACE: return SP_PARITY_SPACE;
+ default: return SP_PARITY_INVALID;
+ }
+}
+
+static int voltronic_dev_serial_configure(
+ struct sp_port* port,
+ const baud_rate_t baud_rate,
+ const data_bits_t data_bits,
+ const stop_bits_t stop_bits,
+ const serial_parity_t parity) {
+
+ const int sp_baud_rate = voltronic_dev_baud_rate(baud_rate);
+ const int sp_data_bits = voltronic_dev_data_bits(data_bits);
+ const int sp_stop_bits = voltronic_dev_stop_bits(stop_bits);
+ const enum sp_parity sp_sp_parity = voltronic_dev_serial_parity(parity);
+
+ if ((sp_data_bits == -1) ||
+ (sp_data_bits == -1) ||
+ (sp_stop_bits == -1) ||
+ (sp_sp_parity == SP_PARITY_INVALID)) {
+
+ SET_INVALID_INPUT();
+ return 0;
+ }
+
+ int result = 0;
+ struct sp_port_config *config;
+
+ SET_LAST_ERROR(0);
+ if (sp_new_config(&config) == SP_OK) {
+ SET_LAST_ERROR(0);
+ if (sp_get_config(port, config) == SP_OK) {
+ SET_LAST_ERROR(0);
+ if ((sp_set_config_baudrate (config, sp_baud_rate) == SP_OK) &&
+ (sp_set_config_bits(config, sp_data_bits) == SP_OK) &&
+ (sp_set_config_stopbits(config, sp_stop_bits) == SP_OK) &&
+ (sp_set_config_parity(config, sp_sp_parity) == SP_OK)) {
+
+ SET_LAST_ERROR(0);
+ if (sp_set_config(port, config) == SP_OK) {
+ result = 1;
+ }
+ }
+ }
+
+ const last_error_t last_error = GET_LAST_ERROR();
+ sp_free_config(config);
+ SET_LAST_ERROR(last_error);
+ }
+
+ return result;
+}
diff --git a/libvoltronic/voltronic_dev_usb.h b/libvoltronic/voltronic_dev_usb.h
new file mode 100644
index 0000000..19b336e
--- /dev/null
+++ b/libvoltronic/voltronic_dev_usb.h
@@ -0,0 +1,37 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __VOLTRONIC__DEV__USB__H__
+#define __VOLTRONIC__DEV__USB__H__
+
+ #include "voltronic_dev.h"
+
+ /**
+ * Create an opaque pointer to a voltronic device connected over USB
+ *
+ * vendor_id - Device vendor id to search for. ie. 0x0665
+ * product_id - Device product id to search for. ie. 0x5161
+ *
+ * Returns an opaque pointer to a voltronic device or 0 if an error occurred
+ *
+ * Function sets errno (POSIX)/LastError (Windows) to approriate error on failure
+ */
+ voltronic_dev_t voltronic_usb_create(
+ const unsigned int vendor_id,
+ const unsigned int product_id);
+
+#endif
diff --git a/libvoltronic/voltronic_dev_usb_hidapi.c b/libvoltronic/voltronic_dev_usb_hidapi.c
new file mode 100644
index 0000000..05d9a83
--- /dev/null
+++ b/libvoltronic/voltronic_dev_usb_hidapi.c
@@ -0,0 +1,106 @@
+/**
+ * Copyright (C) 2019 Johan van der Vyver
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include "voltronic_dev_impl.h"
+#include "voltronic_dev_usb.h"
+#include "hidapi.h"
+
+#define HID_REPORT_SIZE 8
+
+#define VOLTRONIC_DEV_USB(_impl_ptr_) \
+ ((hid_device*) (_impl_ptr_))
+
+#define GET_REPORT_SIZE(_val_) \
+ (((_val_) > HID_REPORT_SIZE) ? HID_REPORT_SIZE : (_val_))
+
+static inline void voltronic_usb_init_hidapi(void);
+
+voltronic_dev_t voltronic_usb_create(
+ const unsigned int vendor_id,
+ const unsigned int product_id) {
+
+ voltronic_usb_init_hidapi();
+
+ SET_LAST_ERROR(0);
+ hid_device* dev = hid_open(
+ (unsigned short) vendor_id,
+ (unsigned short) product_id,
+ (const wchar_t*) 0);
+
+ if (dev != 0) {
+ SET_LAST_ERROR(0);
+ return voltronic_dev_internal_create((void*) dev);
+ }
+
+ return 0;
+}
+
+inline int voltronic_dev_impl_read(
+ void* impl_ptr,
+ char* buffer,
+ const size_t buffer_size,
+ const unsigned int timeout_milliseconds) {
+
+ SET_LAST_ERROR(0);
+ return hid_read_timeout(
+ VOLTRONIC_DEV_USB(impl_ptr),
+ (unsigned char*) buffer,
+ GET_REPORT_SIZE(buffer_size),
+ (int) timeout_milliseconds);
+}
+
+inline int voltronic_dev_impl_write(
+ void* impl_ptr,
+ const char* buffer,
+ const size_t buffer_size,
+ unsigned int timeout_milliseconds) {
+
+ ++timeout_milliseconds; // stop unused warnings
+ const int write_size = GET_REPORT_SIZE(buffer_size);
+ unsigned char write_buffer[HID_REPORT_SIZE + 1] = { 0 };
+ COPY_MEMORY(&write_buffer[1], buffer, write_size);
+
+ SET_LAST_ERROR(0);
+ const int bytes_written = hid_write(
+ VOLTRONIC_DEV_USB(impl_ptr),
+ (const unsigned char*) write_buffer,
+ (size_t) (HID_REPORT_SIZE + 1));
+
+ return GET_REPORT_SIZE(bytes_written);
+}
+
+inline int voltronic_dev_impl_close(void* impl_ptr) {
+ hid_close(VOLTRONIC_DEV_USB(impl_ptr));
+ return 1;
+}
+
+static inline void voltronic_usb_exit_hidapi(void) {
+ hid_exit();
+}
+
+static inline void voltronic_usb_init_hidapi(void) {
+ static int hidapi_init_complete = 0;
+
+ if (hidapi_init_complete == 0) {
+ if (hid_init() == 0) {
+ atexit(voltronic_usb_exit_hidapi);
+ hidapi_init_complete = 1;
+ }
+ }
+}
diff --git a/p18.c b/p18.c
new file mode 100644
index 0000000..5ca71f2
--- /dev/null
+++ b/p18.c
@@ -0,0 +1,1559 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+
+#include "p18.h"
+#include "util.h"
+
+const char *p18_query_cmds[] = {
+ "PI", /* Protocol ID */
+ "T", /* Current time */
+ "ET", /* Total generated energy */
+ "EY", /* Year generated energy */
+ "EM", /* Month generated energy */
+ "ED", /* Day generated energy */
+ "ID", /* Series number */
+ "VFW", /* CPU version */
+ "PIRI", /* Rated information */
+ "GS", /* General status */
+ "MOD", /* Working mode */
+ "FWS", /* Fault and warning status */
+ "FLAG", /* Enable/Disable flags statuses */
+ "DI", /* Default values of changeable parameters */
+ "MCHGCR", /* Max charging current selectable (allowed) values */
+ "MUCHGCR", /* Max AC charging current selectable (allowed) values */
+ "PRI", /* Parallel system rated information */
+ "PGS", /* Parallel system general status */
+ "ACCT", /* AC charge time bucket */
+ "ACLT", /* AC supply load time bucket */
+};
+
+const char *p18_set_cmds[] = {
+ "LON", /* Enable/disable machine supply power to the loads */
+ "P", /* Enable/disable flags */
+ "PF", /* Reset changeable params */
+ "MCHGC", /* Battery maximum charge current */
+ "MUCHGC", /* Battery maximum AC charge current */
+ /* The protocol documentation defines two commands, "F50" and "F60",
+ but it's identical as if there were just one "F" command with an argument. */
+ "F", /* Set AC output frequency to be 50Hz or 60Hz */
+ "MCHGV", /* Battery maximum charge voltage */
+ "V", /* AC output rated voltage */
+ "POP", /* Output source priority */
+ "BUCD", /* Battery re-charged and re-discharged voltage when utility is available */
+ "PCP", /* Charging source priority */
+ "PSP", /* Solar power priority */
+ "PGR", /* AC input voltage range */
+ "PBT", /* Battery type */
+ "POPM", /* Output model */
+ "PSDV", /* Battery cut-off voltage */
+ "ID", /* Set solar configuration */
+ "CLE", /* Clear all data of generated energy */
+ "DAT", /* Set date and time */
+ "ACCT", /* AC charge time bucket */
+ "ACLT", /* AC supply load time bucket */
+};
+
+const p18_fault_code_list_item_t fault_codes[] = {
+ {1, "Fan is locked"},
+ {2, "Over temperature"},
+ {3, "Battery voltage is too high"},
+ {4, "Battery voltage is too low"},
+ {5, "Output short circuited or Over temperature"},
+ {6, "Output voltage is too high"},
+ {7, "Over load time out"},
+ {8, "Bus voltage is too high"},
+ {9, "Bus soft start failed"},
+ {11, "Main relay failed"},
+ {51, "Over current inverter"},
+ {52, "Bus soft start failed"},
+ {53, "Inverter soft start failed"},
+ {54, "Self-test failed"},
+ {55, "Over DC voltage on output of inverter"},
+ {56, "Battery connection is open"},
+ {57, "Current sensor failed"},
+ {58, "Output voltage is too low"},
+ {60, "Inverter negative power"},
+ {71, "Parallel version different"},
+ {72, "Output circuit failed"},
+ {80, "CAN communication failed"},
+ {81, "Parallel host line lost"},
+ {82, "Parallel synchronized signal lost"},
+ {83, "Parallel battery voltage detect different"},
+ {84, "Parallel Line voltage or frequency detect different"},
+ {85, "Parallel Line input current unbalanced"},
+ {86, "Parallel output setting different"},
+};
+
+const p18_flag_printable_list_item_t p18_flags_printable_list[9] = {
+ {"BUZZ", "A", "Silence buzzer or open buzzer"},
+ {"OLBP", "B", "Overload bypass function"},
+ {"LCDE", "C", "LCD display escape to default page after 1min timeout"},
+ {"OLRS", "D", "Overload restart"},
+ {"OTRS", "E", "Overload temperature restart"},
+ {"BLON", "F", "Backlight on"},
+ {"ALRM", "G", "Alarm on primary source interrupt"},
+ {"FTCR", "H", "Fault code record"},
+ {"MTYP", "I", "Machine type (1=Grid-Tie, 0=Off-Grid-Tie)"},
+};
+
+const int p18_ac_output_rated_voltages[5] = {
+ 202,
+ 208,
+ 220,
+ 230,
+ 240,
+};
+
+const char *p18_battery_util_recharging_voltages_12v_unit[8] = {
+ "11",
+ "11.3",
+ "11.5",
+ "11.8",
+ "12",
+ "12.3",
+ "12.5",
+ "12.8",
+};
+
+const char *p18_battery_util_recharging_voltages_24v_unit[8] = {
+ "22",
+ "22.5",
+ "23",
+ "23.5",
+ "24",
+ "24.5",
+ "25",
+ "25.5",
+};
+const char *p18_battery_util_recharging_voltages_48v_unit[8] = {
+ "44",
+ "45",
+ "46",
+ "47",
+ "48",
+ "49",
+ "50",
+ "51",
+};
+
+const char *p18_battery_util_redischarging_voltages_12v_unit[12] = {
+ "0",
+ "12",
+ "12.3",
+ "12.5",
+ "12.8",
+ "13",
+ "13.3",
+ "13.5",
+ "13.8",
+ "14",
+ "14.3",
+ "14.5",
+};
+const char *p18_battery_util_redischarging_voltages_24v_unit[12] = {
+ "0",
+ "24",
+ "24.5",
+ "25",
+ "25.5",
+ "26",
+ "26.5",
+ "27",
+ "27.5",
+ "28",
+ "28.5",
+ "29"
+};
+const char *p18_battery_util_redischarging_voltages_48v_unit[12] = {
+ "0",
+ "48",
+ "49",
+ "50",
+ "51",
+ "52",
+ "53",
+ "54",
+ "55",
+ "56",
+ "57",
+ "58",
+};
+
+const char *p18_battery_types[] = {
+ "AGM",
+ "Flooded",
+ "User",
+};
+
+const char *p18_voltage_ranges[] = {
+ "Appliance",
+ "UPS"
+};
+
+const char *p18_output_source_priorities[] = {
+ "Solar-Utility-Battery",
+ "Solar-Battery-Utility"
+};
+
+const char *p18_charger_source_priorities[] = {
+ "Solar-First",
+ "Solar-and-Utility",
+ "Solar-Only"
+};
+
+const char *p18_machine_types[] = {
+ "Off-Grid-Tie",
+ "Grid-Tie"
+};
+
+const char *p18_topologies[] = {
+ "Transformerless",
+ "Transformer"
+};
+
+const char *p18_output_model_settings[] = {
+ "Single module",
+ "Parallel output",
+ "Phase 1 of three phase output",
+ "Phase 2 of three phase output",
+ "Phase 3 of three phase",
+};
+
+const char *p18_solar_power_priorities[] = {
+ "Battery-Load-Utility",
+ "Load-Battery-Utility"
+};
+
+const char *p18_mppt_charger_statuses[] = {
+ "Abnormal",
+ "Not charging",
+ "Charging"
+};
+
+const char *p18_battery_power_directions[] = {
+ "Do nothing",
+ "Charge",
+ "Discharge"
+};
+
+const char *p18_dc_ac_power_directions[] = {
+ "Do nothing",
+ "AC/DC",
+ "DC/AC"
+};
+
+const char *p18_line_power_directions[] = {
+ "Do nothing",
+ "Input",
+ "Output",
+};
+
+const char *p18_working_modes[] = {
+ "Power on mode",
+ "Standby mode",
+ "Bypass mode",
+ "Battery mode",
+ "Fault mode",
+ "Hybrid mode",
+};
+
+const char *p18_parallel_connection_statuses[] = {
+ "Non-existent",
+ "Existent"
+};
+
+const char *p18_unknown = "Unknown";
+
+/* ------------------------------------------ */
+
+static char *p18_append_arguments(int command, char *buf, const char **a)
+{
+ switch (command) {
+ case P18_QUERY_YEAR_GENERATED:
+ buf += sprintf(buf, "%s", a[0]);
+ break;
+
+ case P18_QUERY_MONTH_GENERATED:
+ buf += sprintf(buf, "%s%02d", a[0], atoi(a[1]));
+ break;
+
+ case P18_QUERY_DAY_GENERATED:
+ buf += sprintf(buf, "%s%02d%02d",
+ a[0], atoi(a[1]), atoi(a[2]));
+ break;
+
+ case P18_QUERY_PARALLEL_RATED_INFORMATION:
+ case P18_QUERY_PARALLEL_GENERAL_STATUS:
+ buf += sprintf(buf, "%d", atoi(a[0]));
+ break;
+
+ case P18_SET_LOADS:
+ buf += sprintf(buf, "%s", a[0]);
+ break;
+
+ case P18_SET_FLAG:
+ buf += sprintf(buf, "%c%s",
+ *a[1] == '1' ? 'E' : 'D',
+ a[0]);
+ break;
+
+ case P18_SET_BAT_MAX_CHARGE_CURRENT:
+ case P18_SET_BAT_MAX_AC_CHARGE_CURRENT:
+ buf += sprintf(buf, "%c,%03d", *a[0], atoi(a[1]));
+ break;
+
+ case P18_SET_AC_OUTPUT_FREQ:
+ buf += sprintf(buf, "%02d", atoi(a[0]));
+ break;
+
+ case P18_SET_BAT_MAX_CHARGE_VOLTAGE: {
+ double cv = atof(a[0]);
+ double fv = atof(a[1]);
+ buf += sprintf(buf, "%03d,%03d", (int)round(cv*10), (int)round(fv*10));
+ break;
+ }
+
+ case P18_SET_AC_OUTPUT_RATED_VOLTAGE: {
+ int v = atoi(a[0]);
+ buf += sprintf(buf, "%04d", v*10);
+ break;
+ }
+
+ case P18_SET_OUTPUT_SOURCE_PRIORITY:
+ case P18_SET_SOLAR_POWER_PRIORITY:
+ case P18_SET_AC_INPUT_VOLTAGE_RANGE:
+ case P18_SET_BAT_TYPE:
+ buf += sprintf(buf, "%c", *a[0]);
+ break;
+
+ case P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL: {
+ double cv = atof(a[0]);
+ double dv = atof(a[1]);
+ buf += sprintf(buf, "%03d,%03d", (int)round(cv*10), (int)round(dv*10));
+ break;
+ }
+
+ case P18_SET_CHARGING_SOURCE_PRIORITY:
+ case P18_SET_OUTPUT_MODEL:
+ buf += sprintf(buf, "%c,%c", *a[0], *a[1]);
+ break;
+
+ case P18_SET_BAT_CUTOFF_VOLTAGE: {
+ double v = atof(a[0]);
+ buf += sprintf(buf, "%03d", (int)round(v*10));
+ break;
+ }
+
+ case P18_SET_SOLAR_CONFIG: {
+ int len = (int)strlen(a[0]);
+ buf += sprintf(buf, "%02d", len);
+ buf += sprintf(buf, "%s", a[0]);
+ if (len < 20) {
+ for (int i = 0; i < 20-len; i++)
+ buf += sprintf(buf, "%c", '0');
+ }
+ break;
+ }
+
+ case P18_SET_DATE_TIME: {
+ int Y = atoi(a[0]) - 2000;
+ int M = atoi(a[1]);
+ int D = atoi(a[2]);
+ int h = atoi(a[3]);
+ int m = atoi(a[4]);
+ int s = atoi(a[5]);
+ buf += sprintf(buf, "%02d%02d%02d%02d%02d%02d", Y, M, D, h, m, s);
+ break;
+ }
+
+ case P18_SET_AC_CHARGE_TIME_BUCKET:
+ case P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET:
+ buf += sprintf(buf, "%02d%02d,%02d%02d",
+ atoi(a[0]), atoi(a[1]), atoi(a[2]), atoi(a[3]));
+ break;
+ }
+
+ return buf;
+}
+
+bool p18_build_command(int command, const char **args, size_t args_size, char *buf)
+{
+ UNUSED(args_size);
+
+ bool set = command >= P18_SET_CMDS_ENUM_OFFSET;
+ int offset;
+ const char **list;
+ size_t list_size;
+ if (!set) {
+ list = p18_query_cmds;
+ list_size = ARRAY_SIZE(p18_query_cmds);
+ offset = P18_QUERY_CMDS_ENUM_OFFSET;
+ } else {
+ list = p18_set_cmds;
+ list_size = ARRAY_SIZE(p18_set_cmds);
+ offset = P18_SET_CMDS_ENUM_OFFSET;
+ }
+
+ int cmd_index = command - offset;
+ if (cmd_index < 0 || cmd_index >= (int)list_size) {
+ LOG("%s: invalid cmd_index %d\n", __func__, cmd_index);
+ return false;
+ }
+
+ (*buf++) = '^';
+ (*buf++) = set ? 'S' : 'P';
+
+ char *len_p = buf;
+ buf += sprintf(buf, "000");
+ buf += sprintf(buf, "%s", list[cmd_index]);
+
+ buf = p18_append_arguments(command, buf, args);
+
+ char len_buf[4];
+ sprintf(len_buf, "%03zu", (size_t)(buf-len_p));
+ memcpy(len_p, len_buf, 3);
+
+ return true;
+}
+
+bool p18_validate_query_response(const char *buf, size_t size, size_t *data_size)
+{
+ if (buf[0] != '^' || buf[1] != 'D')
+ return false;
+
+ char lenbuf[4];
+ memcpy(lenbuf, &buf[2], 3);
+ lenbuf[3] = '\0';
+
+ unsigned int len = atoi(lenbuf);
+ if (size < len+4)
+ return false;
+
+ if (data_size)
+ *data_size = len-3;
+ return true;
+}
+
+bool p18_set_result(const char *buf, size_t size)
+{
+ if (size < 2) {
+ ERROR("%s: response is too small\n", __func__);
+ return false;
+ }
+ if (buf[0] != '^') {
+ ERROR("%s: invalid character %c, ^ expected\n", __func__, *buf);
+ return false;
+ }
+ return buf[1] == '1';
+}
+
+static void p18_parse_list(
+ const char *data,
+ void *message_ptr,
+ int expected_items_count, /* -1 means no fixed limit */
+ p18_parse_cb_t callback)
+{
+ int data_size = (int)strlen(data);
+ static const int buf_size = 64;
+ char buf[buf_size];
+ char *c = (char *)data;
+
+ for (int i = 0, char_index = 0, item_index = 0;
+ i < data_size;
+ i++) {
+
+ if (*c != ',' && i < data_size-1) {
+ if (char_index < buf_size-1)
+ buf[char_index++] = *c;
+ c++;
+ continue;
+ }
+
+ /* last character */
+ if (*c != ',' && char_index < buf_size-1)
+ buf[char_index++] = *c;
+
+ if (expected_items_count != -1 && item_index >= expected_items_count)
+ ERROR("warning: item %d is not expected\n", item_index);
+ else {
+ buf[char_index] = '\0';
+ callback((const char *)buf, char_index, item_index, message_ptr);
+ }
+
+ char_index = 0;
+ c++;
+ item_index++;
+ }
+}
+
+static void p18_expect_listitem_length(const char *f,
+ int index,
+ size_t expected_len,
+ size_t actual_len) {
+ if (actual_len != expected_len) {
+ LOG("%s: length of item %d is %zu != %zu\n",
+ f, index, actual_len, expected_len);
+ }
+}
+
+/* ------------------------------------------ */
+/* Command-specific methods */
+
+P18_UNPACK_FN(protocol_id)
+{
+ char s[4];
+ strncpy(s, data, 2);
+ s[2] = '\0';
+
+ p18_protocol_id_msg_t m;
+ m.id = atoi(s);
+
+ return m;
+}
+
+P18_UNPACK_FN(current_time)
+{
+ char s[4];
+ int n;
+ p18_current_time_msg_t m;
+
+ substr_copy(s, data, 4);
+ m.year = atoi(s);
+
+ for (int i = 0; i < 5; i++) {
+ substr_copy(s, data + 4 + (i * 2), 2);
+ n = atoi(s);
+ switch (i) {
+ case 0: m.month = n; break;
+ case 1: m.day = n; break;
+ case 2: m.hour = n; break;
+ case 3: m.minute = n; break;
+ case 4: m.second = n; break;
+ default:
+ ERROR("%s: warning: unexpected code path, i=%d\n",
+ __func__, i);
+ break;
+ }
+ }
+
+ return m;
+}
+
+static long parse_generated(const char *buf)
+{
+ char s[16];
+ strncpy(s, buf, 8);
+ s[8] = '\0';
+ return atol(s);
+}
+
+P18_UNPACK_FN(total_generated)
+{
+ p18_total_generated_msg_t m = {
+ .kwh = parse_generated(data)
+ };
+ return m;
+}
+
+P18_UNPACK_FN(year_generated)
+{
+ p18_year_generated_msg_t m = {
+ .kwh = parse_generated(data)
+ };
+ return m;
+}
+
+P18_UNPACK_FN(month_generated)
+{
+ p18_month_generated_msg_t m = {
+ .kwh = parse_generated(data)
+ };
+ return m;
+}
+
+P18_UNPACK_FN(day_generated)
+{
+ p18_day_generated_msg_t m = {
+ .kwh = parse_generated(data)
+ };
+ return m;
+}
+
+P18_UNPACK_FN(series_number)
+{
+ p18_series_number_msg_t m;
+
+ /* read length, reuse the already available id datafer */
+ substr_copy(m.id, data, 2);
+ m.length = atoi(m.id);
+
+ /* read id of given length */
+ substr_copy(m.id, data + 2, m.length);
+
+ return m;
+}
+
+P18_PARSE_CB_FN(cpu_version)
+{
+ //LOG("%s: value=%s, value_len=%d\n", __func__, value, value_len);
+
+ p18_cpu_version_msg_t *m = (p18_cpu_version_msg_t *)message_ptr;
+ P18_EXPECT_LISTITEM_LENGTH(5);
+
+ switch (index) {
+ case 0:
+ substr_copy(m->main_cpu_version, value, 5);
+ break;
+
+ case 1:
+ substr_copy(m->slave1_cpu_version, value, 5);
+ break;
+
+ case 2:
+ substr_copy(m->slave2_cpu_version, value, 5);
+ break;
+ }
+}
+P18_UNPACK_FN(cpu_version)
+{
+ P18_PARSE_LIST_AND_RETURN(cpu_version, 3);
+}
+
+P18_PARSE_CB_FN(rated_information)
+{
+ p18_rated_information_msg_t *m = (p18_rated_information_msg_t *)message_ptr;
+ int num = atoi(value);
+ switch (index) {
+ case 0: /* AAAA */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_input_rating_voltage = num;
+ break;
+ case 1: /* BBB */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_input_rating_current = num;
+ break;
+ case 2: /* CCCC */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_rating_voltage = num;
+ break;
+ case 3: /* DDD */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_output_rating_freq = num;
+ break;
+ case 4: /* EEE */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_output_rating_current = num;
+ break;
+ case 5: /* FFFF */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_rating_apparent_power = num;
+ break;
+ case 6: /* GGGG */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_rating_active_power = num;
+ break;
+ case 7: /* HHH */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_rating_voltage = num;
+ break;
+ case 8: /* III */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_recharge_voltage = num;
+ break;
+ case 9: /* JJJ */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_redischarge_voltage = num;
+ break;
+ case 10: /* KKK */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_under_voltage = num;
+ break;
+ case 11: /* LLL */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_bulk_voltage = num;
+ break;
+ case 12: /* MMM */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_float_voltage = num;
+ break;
+ case 13: /* N */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->battery_type = num;
+ break;
+ case 14: /* OO */
+ P18_EXPECT_LISTITEM_LENGTH(2);
+ m->max_ac_charging_current = num;
+ break;
+ case 15: /* PPP */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->max_charging_current = num;
+ break;
+ case 16: /* Q */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->input_voltage_range = num;
+ break;
+ case 17: /* R */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->output_source_priority = num;
+ break;
+ case 18: /* S */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->charger_source_priority = num;
+ break;
+ case 19: /* T */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->parallel_max_num = num;
+ break;
+ case 20: /* U */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->machine_type = num;
+ break;
+ case 21: /* V */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->topology = num;
+ break;
+ case 22: /* W */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->output_model_setting = num;
+ break;
+ case 23: /* Z */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->solar_power_priority = num;
+ break;
+ case 24: /* a */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ substr_copy(m->mppt, value, 1);
+ break;
+ }
+}
+P18_UNPACK_FN(rated_information)
+{
+ P18_PARSE_LIST_AND_RETURN(rated_information, 25)
+}
+
+P18_PARSE_CB_FN(general_status)
+{
+ p18_general_status_msg_t *m = (p18_general_status_msg_t *)message_ptr;
+ int num = atoi(value);
+ switch (index) {
+ case 0: /* AAAA */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->grid_voltage = num;
+ break;
+ case 1: /* BBB */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->grid_freq = num;
+ break;
+ case 2: /* CCCC */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_voltage = num;
+ break;
+ case 3: /* DDD */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_output_freq = num;
+ break;
+ case 4: /* EEEE */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_apparent_power = num;
+ break;
+ case 5: /* FFFF */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_active_power = num;
+ break;
+ case 6: /* GGG */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->output_load_percent = num;
+ break;
+ case 7: /* HHH */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_voltage = num;
+ break;
+ case 8: /* III */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_voltage_scc = num;
+ break;
+ case 9: /* JJJ */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_voltage_scc2 = num;
+ break;
+ case 10: /* KKK */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_discharge_current = num;
+ break;
+ case 11: /* LLL */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_charging_current = num;
+ break;
+ case 12: /* MMM */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_capacity = num;
+ break;
+ case 13: /* NNN */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->inverter_heat_sink_temp = num;
+ break;
+ case 14: /* OOO */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->mppt1_charger_temp = num;
+ break;
+ case 15: /* PPP */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->mppt2_charger_temp = num;
+ break;
+ case 16: /* QQQQ */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv1_input_power = num;
+ break;
+ case 17: /* RRRR */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv2_input_power = num;
+ break;
+ case 18: /* SSSS */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv1_input_voltage = num;
+ break;
+ case 19: /* TTTT */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv2_input_voltage = num;
+ break;
+ case 20: /* U */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->settings_values_changed = num == 1;
+ break;
+ case 21: /* V */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->mppt1_charger_status = num;
+ break;
+ case 22: /* W */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->mppt2_charger_status = num;
+ break;
+ case 23: /* X */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->load_connected = num == 1;
+ break;
+ case 24: /* Y */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->battery_power_direction = num;
+ break;
+ case 25: /* Z */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->dc_ac_power_direction = num;
+ break;
+ case 26: /* a */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->line_power_direction = num;
+ break;
+ case 27: /* b */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->local_parallel_id = num;
+ break;
+ }
+}
+P18_UNPACK_FN(general_status)
+{
+ P18_PARSE_LIST_AND_RETURN(general_status, 28)
+}
+
+P18_UNPACK_FN(working_mode)
+{
+ char s[4];
+ strncpy(s, data, 2);
+ s[2] = '\0';
+
+ p18_working_mode_msg_t m;
+ m.mode = atoi(s);
+
+ return m;
+}
+
+P18_PARSE_CB_FN(faults_warnings)
+{
+ p18_faults_warnings_msg_t *m = (p18_faults_warnings_msg_t *)message_ptr;
+ int num = atoi(value);
+ P18_EXPECT_LISTITEM_LENGTH(index == 0 ? 2 : 1);
+ switch (index) {
+ case 0:
+ m->fault_code = num;
+ break;
+ case 1:
+ m->line_fail = num > 0;
+ break;
+ case 2:
+ m->output_circuit_short = num > 0;
+ break;
+ case 3:
+ m->inverter_over_temperature = num > 0;
+ break;
+ case 4:
+ m->fan_lock = num > 0;
+ break;
+ case 5:
+ m->battery_voltage_high = num > 0;
+ break;
+ case 6:
+ m->battery_low = num > 0;
+ break;
+ case 7:
+ m->battery_under = num > 0;
+ break;
+ case 8:
+ m->over_load = num > 0;
+ break;
+ case 9:
+ m->eeprom_fail = num > 0;
+ break;
+ case 10:
+ m->power_limit = num > 0;
+ break;
+ case 11:
+ m->pv1_voltage_high = num > 0;
+ break;
+ case 12:
+ m->pv2_voltage_high = num > 0;
+ break;
+ case 13:
+ m->mppt1_overload_warning = num > 0;
+ break;
+ case 14:
+ m->mppt2_overload_warning = num > 0;
+ break;
+ case 15:
+ m->battery_too_low_to_charge_for_scc1 = num > 0;
+ break;
+ case 16:
+ m->battery_too_low_to_charge_for_scc2 = num > 0;
+ break;
+ }
+}
+P18_UNPACK_FN(faults_warnings)
+{
+ P18_PARSE_LIST_AND_RETURN(faults_warnings, 17)
+}
+
+P18_PARSE_CB_FN(flags_statuses)
+{
+ p18_flags_statuses_msg_t *m = (p18_flags_statuses_msg_t *)message_ptr;
+ int num = atoi(value);
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ switch (index) {
+ case 0:
+ m->buzzer = num > 0;
+ break;
+ case 1:
+ m->overload_bypass = num > 0;
+ break;
+ case 2:
+ m->lcd_escape_to_default_page_after_1min_timeout = num > 0;
+ break;
+ case 3:
+ m->overload_restart = num > 0;
+ break;
+ case 4:
+ m->over_temp_restart = num > 0;
+ break;
+ case 5:
+ m->backlight_on = num > 0;
+ break;
+ case 6:
+ m->alarm_on_primary_source_interrupt = num > 0;
+ break;
+ case 7:
+ m->fault_code_record = num > 0;
+ break;
+ }
+}
+P18_UNPACK_FN(flags_statuses)
+{
+ P18_PARSE_LIST_AND_RETURN(flags_statuses, 9)
+}
+
+P18_PARSE_CB_FN(defaults)
+{
+ p18_defaults_msg_t *m = (p18_defaults_msg_t *)message_ptr;
+ int num = atoi(value);
+ switch (index) {
+ case 0: /* AAAA */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_voltage = num;
+ break;
+ case 1: /* BBB */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_output_freq = num;
+ break;
+ case 2: /* C */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->ac_input_voltage_range = num;
+ break;
+ case 3: /* DDD */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_under_voltage = num;
+ break;
+ case 4: /* EEE */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->charging_float_voltage = num;
+ break;
+ case 5: /* FFF */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->charging_bulk_voltage = num;
+ break;
+ case 6: /* GGG */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_recharge_voltage = num;
+ break;
+ case 7: /* HHH */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_redischarge_voltage = num;
+ break;
+ case 8: /* III */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->max_charging_current = num;
+ break;
+ case 9: /* JJ */
+ P18_EXPECT_LISTITEM_LENGTH(2);
+ m->max_ac_charging_current = num;
+ break;
+ case 10: /* K */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->battery_type = num;
+ break;
+ case 11: /* L */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->output_source_priority = num;
+ break;
+ case 12: /* M */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->charger_source_priority = num;
+ break;
+ case 13: /* N */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->solar_power_priority = num;
+ break;
+ case 14: /* O */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->machine_type = num;
+ break;
+ case 15: /* P */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->output_model_setting = num;
+ break;
+ case 16: /* S */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_buzzer = num > 0;
+ break;
+ case 17: /* T */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_overload_restart = num > 0;
+ break;
+ case 18: /* U */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_over_temp_restart = num > 0;
+ break;
+ case 19: /* V */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_backlight_on = num > 0;
+ break;
+ case 20: /* W */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_alarm_on_primary_source_interrupt = num > 0;
+ break;
+ case 21: /* X */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_fault_code_record = num > 0;
+ break;
+ case 22: /* Y */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_overload_bypass = num > 0;
+ break;
+ case 23: /* Z */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->flag_lcd_escape_to_default_page_after_1min_timeout = num > 0;
+ break;
+ }
+}
+P18_UNPACK_FN(defaults)
+{
+ P18_PARSE_LIST_AND_RETURN(defaults, 24)
+}
+
+P18_PARSE_CB_FN(max_charging_current_selectable_values)
+{
+ UNUSED(value_len);
+ p18_max_charging_current_selectable_values_msg_t *m =
+ (p18_max_charging_current_selectable_values_msg_t *)message_ptr;
+ if (m->len >= ARRAY_SIZE(m->amps))
+ ERROR("warning: %dth item, ignoring as we accepting up to %zu values\n",
+ index, ARRAY_SIZE(m->amps));
+ else
+ m->amps[m->len++] = atoi(value);
+}
+P18_UNPACK_FN(max_charging_current_selectable_values)
+{
+ P18_PARSE_LIST_AND_RETURN(max_charging_current_selectable_values, -1)
+}
+
+P18_PARSE_CB_FN(max_ac_charging_current_selectable_values)
+{
+ UNUSED(value_len);
+ p18_max_ac_charging_current_selectable_values_msg_t *m =
+ (p18_max_ac_charging_current_selectable_values_msg_t *)message_ptr;
+ if (m->len >= ARRAY_SIZE(m->amps))
+ ERROR("warning: %dth item, ignoring as we accepting up to %zu values\n",
+ index, ARRAY_SIZE(m->amps));
+ else
+ m->amps[m->len++] = atoi(value);
+}
+P18_UNPACK_FN(max_ac_charging_current_selectable_values)
+{
+ P18_PARSE_LIST_AND_RETURN(max_ac_charging_current_selectable_values, -1)
+}
+
+P18_PARSE_CB_FN(parallel_rated_information)
+{
+ p18_parallel_rated_information_msg_t *m = (p18_parallel_rated_information_msg_t *)message_ptr;
+ switch (index) {
+ case 0: /* A */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->parallel_id_connection_status = atoi(value);
+ break;
+ case 1: /* BB */
+ P18_EXPECT_LISTITEM_LENGTH(2);
+ m->serial_number_valid_length = atoi(value);
+ break;
+ case 2: /* CCCCCCCCCCCCCCCCCCCC */
+ P18_EXPECT_LISTITEM_LENGTH(20);
+ substr_copy(m->serial_number, value, MIN(m->serial_number_valid_length, (int)ARRAY_SIZE(m->serial_number)-1));
+ break;
+ case 3: /* D */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->charger_source_priority = atoi(value);
+ break;
+ case 4: /* EEE */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->max_charging_current = atoi(value);
+ break;
+ case 5: /* FF */
+ P18_EXPECT_LISTITEM_LENGTH(2);
+ m->max_ac_charging_current = atoi(value);
+ break;
+ case 6: /* G */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->output_model_setting = atoi(value);
+ break;
+ }
+}
+P18_UNPACK_FN(parallel_rated_information)
+{
+ P18_PARSE_LIST_AND_RETURN(parallel_rated_information, 7)
+}
+
+P18_PARSE_CB_FN(parallel_general_status)
+{
+ p18_parallel_general_status_msg_t *m = (p18_parallel_general_status_msg_t *)message_ptr;
+ int num = atoi(value);
+ switch (index) {
+ case 0: /* A */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->parallel_id_connection_status = num;
+ break;
+ case 1: /* B */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->work_mode = num;
+ break;
+ case 2: /* CC */
+ P18_EXPECT_LISTITEM_LENGTH(2);
+ m->fault_code = num;
+ break;
+ case 3: /* DDDD */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->grid_voltage = num;
+ break;
+ case 4: /* EEE */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->grid_freq = num;
+ break;
+ case 5: /* FFFF */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_voltage = num;
+ break;
+ case 6: /* GGG */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->ac_output_freq = num;
+ break;
+ case 7: /* HHHH */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_apparent_power = num;
+ break;
+ case 8: /* IIII */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->ac_output_active_power = num;
+ break;
+ case 9: /* JJJJJ */
+ P18_EXPECT_LISTITEM_LENGTH(5);
+ m->total_ac_output_apparent_power = num;
+ break;
+ case 10: /* KKKKK */
+ P18_EXPECT_LISTITEM_LENGTH(5);
+ m->total_ac_output_active_power = num;
+ break;
+ case 11: /* LLL */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->output_load_percent = num;
+ break;
+ case 12: /* MMM */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->total_output_load_percent = num;
+ break;
+ case 13: /* NNN */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_voltage = num;
+ break;
+ case 14: /* OOO */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_discharge_current = num;
+ break;
+ case 15: /* PPP */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_charging_current = num;
+ break;
+ case 16: /* QQQ */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->total_battery_charging_current = num;
+ break;
+ case 17: /* MMM. It's not my mistake, it's as per the doc. */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->battery_capacity = num;
+ break;
+ case 18: /* RRRR */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv1_input_power = num;
+ break;
+ case 19: /* SSSS */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv2_input_power = num;
+ break;
+ case 20: /* TTTT */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv1_input_voltage = num == 1;
+ break;
+ case 21: /* UUUU */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+ m->pv2_input_voltage = num;
+ break;
+ case 22: /* V */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->mppt1_charger_status = num;
+ break;
+ case 23: /* W */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->mppt2_charger_status = num;
+ break;
+ case 24: /* X */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->load_connected = num == 1;
+ break;
+ case 25: /* Y */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->battery_power_direction = num;
+ break;
+ case 26: /* Z */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->dc_ac_power_direction = num;
+ break;
+ case 27: /* a */
+ P18_EXPECT_LISTITEM_LENGTH(1);
+ m->line_power_direction = num;
+ break;
+ case 28: /* bbb. It's marked in red in the doc, idk what it means. */
+ /* My guess is that this elem is not always present. */
+ P18_EXPECT_LISTITEM_LENGTH(3);
+ m->max_temp = num;
+ break;
+ }
+}
+P18_UNPACK_FN(parallel_general_status)
+{
+ P18_PARSE_LIST_AND_RETURN(parallel_general_status, 29)
+}
+
+P18_PARSE_CB_FN(ac_charge_time_bucket)
+{
+ p18_ac_charge_time_bucket_msg_t *m = (p18_ac_charge_time_bucket_msg_t *)message_ptr;
+ char buf[4];
+ switch (index) {
+ case 0: /* AAAA */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+
+ substr_copy(buf, value, 2);
+ m->start_h = atoi(buf);
+
+ substr_copy(buf, value+2, 2);
+ m->start_m = atoi(buf);
+
+ break;
+
+ case 1: /* BBBB */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+
+ substr_copy(buf, value, 2);
+ m->end_h = atoi(buf);
+
+ substr_copy(buf, value+2, 2);
+ m->end_m = atoi(buf);
+
+ break;
+ }
+}
+P18_UNPACK_FN(ac_charge_time_bucket)
+{
+ P18_PARSE_LIST_AND_RETURN(ac_charge_time_bucket, 2)
+}
+
+P18_PARSE_CB_FN(ac_supply_load_time_bucket)
+{
+ p18_ac_supply_load_time_bucket_msg_t *m = (p18_ac_supply_load_time_bucket_msg_t *)message_ptr;
+ char buf[4];
+ switch (index) {
+ case 0: /* AAAA */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+
+ substr_copy(buf, value, 2);
+ m->start_h = atoi(buf);
+
+ substr_copy(buf, value+2, 2);
+ m->start_m = atoi(buf);
+
+ break;
+
+ case 1: /* BBBB */
+ P18_EXPECT_LISTITEM_LENGTH(4);
+
+ substr_copy(buf, value, 2);
+ m->end_h = atoi(buf);
+
+ substr_copy(buf, value+2, 2);
+ m->end_m = atoi(buf);
+
+ break;
+ }
+}
+P18_UNPACK_FN(ac_supply_load_time_bucket)
+{
+ P18_PARSE_LIST_AND_RETURN(ac_supply_load_time_bucket, 2)
+}
+
+/* ------------------------------------------ */
+/* Label getters */
+
+const char *p18_battery_type_label(p18_battery_type_t type)
+{
+ switch (type) {
+ case P18_BT_AGM:
+ case P18_BT_USER:
+ case P18_BT_FLOODED:
+ return p18_battery_types[type];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_input_voltage_range_label(p18_input_voltage_range_t range)
+{
+ switch (range) {
+ case P18_IVR_APPLIANCE:
+ case P18_IVR_UPS:
+ return p18_voltage_ranges[range];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_output_source_priority_label(p18_output_source_priority_t priority)
+{
+ switch (priority) {
+ case P18_OSP_SOLAR_BATTERY_UTILITY:
+ case P18_OSP_SOLAR_UTILITY_BATTERY:
+ return p18_output_source_priorities[priority];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_charge_source_priority_label(p18_charger_source_priority_t priority)
+{
+ switch (priority) {
+ case P18_CSP_SOLAR_FIRST:
+ case P18_CSP_SOLAR_AND_UTILITY:
+ case P18_CSP_SOLAR_ONLY:
+ return p18_charger_source_priorities[priority];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_machine_type_label(p18_machine_type_t type)
+{
+ switch (type) {
+ case P18_MT_GRID_TIE:
+ case P18_MT_OFF_GRID_TIE:
+ return p18_machine_types[type];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_topology_label(p18_topology_t topology)
+{
+ switch (topology) {
+ case P18_TRANSFORMER:
+ case P18_TRANSFORMERLESS:
+ return p18_topologies[topology];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_output_model_setting_label(p18_output_model_setting_t setting)
+{
+ switch (setting) {
+ case P18_OMS_PARALLEL_OUTPUT:
+ case P18_OMS_PHASE1_OF_3PHASE_OUTPUT:
+ case P18_OMS_PHASE2_OF_3PHASE_OUTPUT:
+ case P18_OMS_PHASE3_OF_3PHASE_OUTPUT:
+ case P18_OMS_SINGLE_MODULE:
+ return p18_output_model_settings[setting];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_solar_power_priority_label(p18_solar_power_priority_t priority)
+{
+ switch (priority) {
+ case P18_SPP_BATTERY_LOAD_UTILITY:
+ case P18_SPP_LOAD_BATTERY_UTILITY:
+ return p18_solar_power_priorities[priority];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_mppt_charger_status_label(p18_mppt_charger_status_t status)
+{
+ switch (status) {
+ case P18_MPPT_CS_ABNORMAL:
+ case P18_MPPT_CS_CHARGED:
+ case P18_MPPT_CS_NOT_CHARGED:
+ return p18_mppt_charger_statuses[status];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_battery_power_direction_label(p18_battery_power_direction_t direction)
+{
+ switch (direction) {
+ case P18_BPD_CHARGE:
+ case P18_BPD_DISCHARGE:
+ case P18_BPD_DONOTHING:
+ return p18_battery_power_directions[direction];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_dc_ac_power_direction_label(p18_dc_ac_power_direction_t direction)
+{
+ switch (direction) {
+ case P18_DAPD_AC_DC:
+ case P18_DAPD_DC_AC:
+ case P18_DAPD_DONOTHING:
+ return p18_dc_ac_power_directions[direction];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_line_power_direction_label(p18_line_power_direction_t direction)
+{
+ switch (direction) {
+ case P18_LPD_DONOTHING:
+ case P18_LPD_INPUT:
+ case P18_LPD_OUTPUT:
+ return p18_line_power_directions[direction];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_working_mode_label(p18_working_mode_t mode)
+{
+ switch (mode) {
+ case P18_WM_BATTERY_MODE:
+ case P18_WM_BYPASS_MODE:
+ case P18_WM_FAULT_MODE:
+ case P18_WM_HYBRID_MODE:
+ case P18_WM_POWER_ON_MODE:
+ case P18_WM_STANDBY_MODE:
+ return p18_working_modes[mode];
+
+ default:
+ return p18_unknown;
+ }
+}
+
+const char *p18_fault_code_label(unsigned int code)
+{
+ int len = (int)ARRAY_SIZE(fault_codes);
+ for (int i = 0; i < len; i++) {
+ const p18_fault_code_list_item_t *fc = &fault_codes[i];
+ if (fc->id == code)
+ return fc->message;
+ }
+ return p18_unknown;
+}
+
+const char *p18_parallel_connection_status_label(p18_parallel_id_connection_status_t status)
+{
+ switch (status) {
+ case P18_PCS_NON_EXISTENT:
+ case P18_PCS_EXISTENT:
+ return p18_parallel_connection_statuses[status];
+
+ default:
+ return p18_unknown;
+ }
+} \ No newline at end of file
diff --git a/p18.h b/p18.h
new file mode 100644
index 0000000..604dddc
--- /dev/null
+++ b/p18.h
@@ -0,0 +1,505 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ISV_P18_H
+#define ISV_P18_H
+
+#include <stdbool.h>
+#include <string.h>
+#include "util.h"
+
+#define P18_QUERY_CMDS_ENUM_OFFSET 1000
+#define P18_SET_CMDS_ENUM_OFFSET 1100
+
+#define MK_P18_UNPACK_FN_NAME(msg_type) p18_unpack_ ## msg_type ## _msg
+#define MK_P18_PARSE_CB_FN_NAME(msg_type) p18_parse_cb_ ## msg_type ## _msg
+#define MK_P18_MSG_T(msg_type) p18_ ## msg_type ## _msg_t
+
+#define P18_MSG_T(msg_type) MK_P18_MSG_T(msg_type)
+#define P18_UNPACK_FN_NAME(msg_type) MK_P18_UNPACK_FN_NAME(msg_type)
+#define P18_PARSE_CB_FN_NAME(msg_type) MK_P18_PARSE_CB_FN_NAME(msg_type)
+
+#define P18_UNPACK_FN(msg_type) \
+ P18_MSG_T(msg_type) P18_UNPACK_FN_NAME(msg_type)(const char *data)
+
+#define P18_PARSE_CB_FN(msg_type) \
+ static void P18_PARSE_CB_FN_NAME(msg_type)(const char *value, \
+ size_t value_len, \
+ unsigned int index, \
+ void *message_ptr)
+
+#define P18_PARSE_LIST_AND_RETURN(msg_type, exp_items_count) \
+ P18_MSG_T(msg_type) m = {0}; \
+ p18_parse_list(data, &m, (exp_items_count), P18_PARSE_CB_FN_NAME(msg_type)); \
+ return m;
+
+#define P18_EXPECT_LISTITEM_LENGTH(len) \
+ p18_expect_listitem_length((const char *)__func__, index, (len), value_len)
+
+typedef void (*p18_parse_cb_t)(const char *, size_t, unsigned int, void *);
+
+/* ------------------------------------------ */
+/* Commands list */
+
+enum p18_query_command {
+ P18_QUERY_PROTOCOL_ID = P18_QUERY_CMDS_ENUM_OFFSET,
+ P18_QUERY_CURRENT_TIME,
+ P18_QUERY_TOTAL_GENERATED,
+ P18_QUERY_YEAR_GENERATED,
+ P18_QUERY_MONTH_GENERATED,
+ P18_QUERY_DAY_GENERATED,
+ P18_QUERY_SERIES_NUMBER,
+ P18_QUERY_CPU_VERSION,
+ P18_QUERY_RATED_INFORMATION,
+ P18_QUERY_GENERAL_STATUS,
+ P18_QUERY_WORKING_MODE,
+ P18_QUERY_FAULTS_WARNINGS,
+ P18_QUERY_FLAGS_STATUSES,
+ P18_QUERY_DEFAULTS,
+ P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES,
+ P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES,
+ P18_QUERY_PARALLEL_RATED_INFORMATION,
+ P18_QUERY_PARALLEL_GENERAL_STATUS,
+ P18_QUERY_AC_CHARGE_TIME_BUCKET,
+ P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET,
+};
+
+enum p18_set_command {
+ P18_SET_LOADS = P18_SET_CMDS_ENUM_OFFSET,
+ P18_SET_FLAG,
+ P18_SET_DEFAULTS,
+ P18_SET_BAT_MAX_CHARGE_CURRENT,
+ P18_SET_BAT_MAX_AC_CHARGE_CURRENT,
+ P18_SET_AC_OUTPUT_FREQ,
+ P18_SET_BAT_MAX_CHARGE_VOLTAGE,
+ P18_SET_AC_OUTPUT_RATED_VOLTAGE,
+ P18_SET_OUTPUT_SOURCE_PRIORITY,
+ P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL, /* Battery re-charging and re-discharing voltage when utility is available */
+ P18_SET_CHARGING_SOURCE_PRIORITY,
+ P18_SET_SOLAR_POWER_PRIORITY,
+ P18_SET_AC_INPUT_VOLTAGE_RANGE,
+ P18_SET_BAT_TYPE,
+ P18_SET_OUTPUT_MODEL,
+ P18_SET_BAT_CUTOFF_VOLTAGE,
+ P18_SET_SOLAR_CONFIG,
+ P18_SET_CLEAR_GENERATED,
+ P18_SET_DATE_TIME,
+ P18_SET_AC_CHARGE_TIME_BUCKET,
+ P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET,
+};
+
+typedef enum {
+ P18_BT_AGM = 0,
+ P18_BT_FLOODED = 1,
+ P18_BT_USER = 2,
+} p18_battery_type_t;
+
+typedef enum {
+ P18_IVR_APPLIANCE = 0,
+ P18_IVR_UPS = 1,
+} p18_input_voltage_range_t;
+
+typedef enum {
+ P18_OSP_SOLAR_UTILITY_BATTERY = 0,
+ P18_OSP_SOLAR_BATTERY_UTILITY = 1,
+} p18_output_source_priority_t;
+
+typedef enum {
+ P18_CSP_SOLAR_FIRST = 0,
+ P18_CSP_SOLAR_AND_UTILITY = 1,
+ P18_CSP_SOLAR_ONLY = 2,
+} p18_charger_source_priority_t;
+
+typedef enum {
+ P18_MT_OFF_GRID_TIE = 0,
+ P18_MT_GRID_TIE = 1,
+} p18_machine_type_t;
+
+typedef enum {
+ P18_TRANSFORMERLESS = 0,
+ P18_TRANSFORMER = 1,
+} p18_topology_t;
+
+typedef enum {
+ P18_OMS_SINGLE_MODULE = 0,
+ P18_OMS_PARALLEL_OUTPUT = 1,
+ P18_OMS_PHASE1_OF_3PHASE_OUTPUT = 2,
+ P18_OMS_PHASE2_OF_3PHASE_OUTPUT = 3,
+ P18_OMS_PHASE3_OF_3PHASE_OUTPUT = 4,
+} p18_output_model_setting_t;
+
+typedef enum {
+ P18_SPP_BATTERY_LOAD_UTILITY = 0,
+ P18_SPP_LOAD_BATTERY_UTILITY = 1,
+} p18_solar_power_priority_t;
+
+typedef enum {
+ P18_MPPT_CS_ABNORMAL = 0,
+ P18_MPPT_CS_NOT_CHARGED = 1,
+ P18_MPPT_CS_CHARGED = 2,
+} p18_mppt_charger_status_t;
+
+typedef enum {
+ P18_BPD_DONOTHING = 0,
+ P18_BPD_CHARGE = 1,
+ P18_BPD_DISCHARGE = 2,
+} p18_battery_power_direction_t;
+
+typedef enum {
+ P18_DAPD_DONOTHING = 0,
+ P18_DAPD_AC_DC = 1,
+ P18_DAPD_DC_AC = 2,
+} p18_dc_ac_power_direction_t;
+
+typedef enum {
+ P18_LPD_DONOTHING = 0,
+ P18_LPD_INPUT = 1,
+ P18_LPD_OUTPUT = 2,
+} p18_line_power_direction_t;
+
+typedef enum {
+ P18_WM_POWER_ON_MODE = 0,
+ P18_WM_STANDBY_MODE = 1,
+ P18_WM_BYPASS_MODE = 2,
+ P18_WM_BATTERY_MODE = 3,
+ P18_WM_FAULT_MODE = 4,
+ P18_WM_HYBRID_MODE = 5,
+} p18_working_mode_t;
+
+typedef enum {
+ P18_PCS_NON_EXISTENT = 0,
+ P18_PCS_EXISTENT = 1,
+} p18_parallel_id_connection_status_t;
+
+
+/* ------------------------------------------ */
+/* Message structs */
+
+typedef struct {
+ unsigned int id;
+} p18_protocol_id_msg_t;
+
+typedef struct {
+ unsigned int year;
+ unsigned short month;
+ unsigned short day;
+ unsigned short hour;
+ unsigned short minute;
+ unsigned short second;
+} p18_current_time_msg_t;
+
+typedef struct {
+ unsigned long kwh;
+} p18_total_generated_msg_t;
+
+typedef struct {
+ unsigned long kwh;
+} p18_year_generated_msg_t;
+
+typedef struct {
+ unsigned long kwh;
+} p18_month_generated_msg_t;
+
+typedef struct {
+ unsigned long kwh;
+} p18_day_generated_msg_t;
+
+typedef struct {
+ short length;
+ char id[32];
+} p18_series_number_msg_t;
+
+typedef struct {
+ char main_cpu_version[6];
+ char slave1_cpu_version[6];
+ char slave2_cpu_version[6];
+} p18_cpu_version_msg_t;
+
+typedef struct {
+ unsigned int ac_input_rating_voltage; /* unit: 0.1V */
+ unsigned int ac_input_rating_current; /* unit: 0.1A */
+ unsigned int ac_output_rating_voltage; /* unit: 0.1A */
+ unsigned int ac_output_rating_freq; /* unit: 0.1Hz */
+ unsigned int ac_output_rating_current; /* unit: 0.1A */
+ unsigned int ac_output_rating_apparent_power; /* unit: VA */
+ unsigned int ac_output_rating_active_power; /* unit: W */
+ unsigned int battery_rating_voltage; /* unit: 0.1V */
+ unsigned int battery_recharge_voltage; /* unit: 0.1V */
+ unsigned int battery_redischarge_voltage; /* unit: 0.1V */
+ unsigned int battery_under_voltage; /* unit: 0.1V */
+ unsigned int battery_bulk_voltage; /* unit: 0.1V */
+ unsigned int battery_float_voltage; /* unit: 0.1V */
+ p18_battery_type_t battery_type;
+ unsigned int max_ac_charging_current; /* unit: A */
+ unsigned int max_charging_current; /* unit: A */
+ p18_input_voltage_range_t input_voltage_range;
+ p18_output_source_priority_t output_source_priority;
+ p18_charger_source_priority_t charger_source_priority;
+ unsigned int parallel_max_num;
+ p18_machine_type_t machine_type;
+ p18_topology_t topology;
+ p18_output_model_setting_t output_model_setting;
+ p18_solar_power_priority_t solar_power_priority;
+ char mppt[4];
+} p18_rated_information_msg_t;
+
+typedef struct {
+ unsigned int grid_voltage; /* unit: 0.1V */
+ unsigned int grid_freq; /* unit: 0.1Hz */
+ unsigned int ac_output_voltage; /* unit: 0.1V */
+ unsigned int ac_output_freq; /* unit: 0.1Hz */
+ unsigned int ac_output_apparent_power; /* unit: VA */
+ unsigned int ac_output_active_power; /* unit: W */
+ unsigned int output_load_percent; /* unit: % */
+ unsigned int battery_voltage; /* unit: 0.1V */
+ unsigned int battery_voltage_scc; /* unit: 0.1V */
+ unsigned int battery_voltage_scc2; /* unit: 0.1V */
+ unsigned int battery_discharge_current; /* unit: A */
+ unsigned int battery_charging_current; /* unit: A */
+ unsigned int battery_capacity; /* unit: % */
+ unsigned int inverter_heat_sink_temp; /* unit: C */
+ unsigned int mppt1_charger_temp; /* unit: C */
+ unsigned int mppt2_charger_temp; /* unit: C */
+ unsigned int pv1_input_power; /* unit: W */
+ unsigned int pv2_input_power; /* unit: W */
+ unsigned int pv1_input_voltage; /* unit: 0.1V */
+ unsigned int pv2_input_voltage; /* unit: 0.1V */
+ bool settings_values_changed; /* inverter returns:
+ 0: nothing changed
+ 1: something changed */
+ p18_mppt_charger_status_t mppt1_charger_status;
+ p18_mppt_charger_status_t mppt2_charger_status;
+ bool load_connected; /* inverter returns:
+ 0: disconnected
+ 1: connected */
+ p18_battery_power_direction_t battery_power_direction;
+ p18_dc_ac_power_direction_t dc_ac_power_direction;
+ p18_line_power_direction_t line_power_direction;
+ unsigned int local_parallel_id; /* 0 .. (parallel number - 1) */
+} p18_general_status_msg_t;
+
+typedef struct {
+ p18_working_mode_t mode;
+} p18_working_mode_msg_t;
+
+typedef struct {
+ unsigned int fault_code;
+ bool line_fail;
+ bool output_circuit_short;
+ bool inverter_over_temperature;
+ bool fan_lock;
+ bool battery_voltage_high;
+ bool battery_low;
+ bool battery_under;
+ bool over_load;
+ bool eeprom_fail;
+ bool power_limit;
+ bool pv1_voltage_high;
+ bool pv2_voltage_high;
+ bool mppt1_overload_warning;
+ bool mppt2_overload_warning;
+ bool battery_too_low_to_charge_for_scc1;
+ bool battery_too_low_to_charge_for_scc2;
+} p18_faults_warnings_msg_t;
+
+typedef struct {
+ bool buzzer;
+ bool overload_bypass;
+ bool lcd_escape_to_default_page_after_1min_timeout;
+ bool overload_restart;
+ bool over_temp_restart;
+ bool backlight_on;
+ bool alarm_on_primary_source_interrupt;
+ bool fault_code_record;
+ char reserved;
+} p18_flags_statuses_msg_t;
+
+typedef struct {
+ unsigned int ac_output_voltage; /* unit: 0.1V */
+ unsigned int ac_output_freq;
+ p18_input_voltage_range_t ac_input_voltage_range;
+ unsigned int battery_under_voltage;
+ unsigned int charging_float_voltage;
+ unsigned int charging_bulk_voltage;
+ unsigned int battery_recharge_voltage;
+ unsigned int battery_redischarge_voltage;
+ unsigned int max_charging_current;
+ unsigned int max_ac_charging_current;
+ p18_battery_type_t battery_type;
+ p18_output_source_priority_t output_source_priority;
+ p18_charger_source_priority_t charger_source_priority;
+ p18_solar_power_priority_t solar_power_priority;
+ p18_machine_type_t machine_type;
+ p18_output_model_setting_t output_model_setting;
+ bool flag_buzzer;
+ bool flag_overload_restart;
+ bool flag_over_temp_restart;
+ bool flag_backlight_on;
+ bool flag_alarm_on_primary_source_interrupt;
+ bool flag_fault_code_record;
+ bool flag_overload_bypass;
+ bool flag_lcd_escape_to_default_page_after_1min_timeout;
+} p18_defaults_msg_t;
+
+typedef struct {
+ size_t len;
+ int amps[32];
+} p18_max_charging_current_selectable_values_msg_t;
+
+typedef struct {
+ size_t len;
+ int amps[32];
+} p18_max_ac_charging_current_selectable_values_msg_t;
+
+typedef struct {
+ p18_parallel_id_connection_status_t parallel_id_connection_status;
+ int serial_number_valid_length;
+ char serial_number[32];
+ p18_charger_source_priority_t charger_source_priority;
+ unsigned int max_ac_charging_current; /* unit: A */
+ unsigned int max_charging_current; /* unit: A */
+ p18_output_model_setting_t output_model_setting;
+} p18_parallel_rated_information_msg_t;
+
+typedef struct {
+ p18_parallel_id_connection_status_t parallel_id_connection_status;
+ p18_working_mode_t work_mode;
+ unsigned int fault_code;
+ unsigned int grid_voltage; /* unit: 0.1V */
+ unsigned int grid_freq; /* unit: 0.1Hz */
+ unsigned int ac_output_voltage; /* unit: 0.1V */
+ unsigned int ac_output_freq; /* unit: 0.1Hz */
+ unsigned int ac_output_apparent_power; /* unit: VA */
+ unsigned int ac_output_active_power; /* unit: W */
+ unsigned int total_ac_output_apparent_power; /* unit: VA */
+ unsigned int total_ac_output_active_power; /* unit: W */
+ unsigned int output_load_percent; /* unit: % */
+ unsigned int total_output_load_percent; /* unit: % */
+ unsigned int battery_voltage; /* unit: 0.1V */
+ unsigned int battery_discharge_current; /* unit: A */
+ unsigned int battery_charging_current; /* unit: A */
+ unsigned int total_battery_charging_current; /* unit: A */
+ unsigned int battery_capacity; /* unit: % */
+ unsigned int pv1_input_power; /* unit: W */
+ unsigned int pv2_input_power; /* unit: W */
+ unsigned int pv1_input_voltage; /* unit: 0.1V */
+ unsigned int pv2_input_voltage; /* unit: 0.1V */
+ p18_mppt_charger_status_t mppt1_charger_status;
+ p18_mppt_charger_status_t mppt2_charger_status;
+ bool load_connected; /* inverter returns:
+ 0: disconnected
+ 1: connected */
+ p18_battery_power_direction_t battery_power_direction;
+ p18_dc_ac_power_direction_t dc_ac_power_direction;
+ p18_line_power_direction_t line_power_direction;
+ unsigned int max_temp; /* unit: C */
+} p18_parallel_general_status_msg_t;
+
+typedef struct {
+ unsigned short start_h;
+ unsigned short start_m;
+ unsigned short end_h;
+ unsigned short end_m;
+} p18_ac_charge_time_bucket_msg_t;
+
+typedef struct {
+ unsigned short start_h;
+ unsigned short start_m;
+ unsigned short end_h;
+ unsigned short end_m;
+} p18_ac_supply_load_time_bucket_msg_t;
+
+
+/* ------------------------------------------ */
+/* Some constants and allowed values */
+
+typedef struct {
+ unsigned int id;
+ char message[64];
+} p18_fault_code_list_item_t;
+
+typedef struct {
+ char *key;
+ char *p18_key;
+ char *title;
+} p18_flag_printable_list_item_t;
+extern const p18_flag_printable_list_item_t p18_flags_printable_list[9];
+
+extern const int p18_ac_output_rated_voltages[5];
+
+extern const char *p18_battery_util_recharging_voltages_12v_unit[8];
+extern const char *p18_battery_util_recharging_voltages_24v_unit[8];
+extern const char *p18_battery_util_recharging_voltages_48v_unit[8];
+
+extern const char *p18_battery_util_redischarging_voltages_12v_unit[12];
+extern const char *p18_battery_util_redischarging_voltages_24v_unit[12];
+extern const char *p18_battery_util_redischarging_voltages_48v_unit[12];
+
+
+/* ------------------------------------------ */
+/* Common methods */
+
+bool p18_build_command(int command, const char **args, size_t args_size, char *buf);
+bool p18_validate_query_response(const char *buf, size_t size, size_t *data_size);
+bool p18_set_result(const char *buf, size_t size);
+
+/* ------------------------------------------ */
+/* Command-specific methods */
+
+P18_UNPACK_FN(protocol_id);
+P18_UNPACK_FN(current_time);
+P18_UNPACK_FN(total_generated);
+P18_UNPACK_FN(year_generated);
+P18_UNPACK_FN(month_generated);
+P18_UNPACK_FN(day_generated);
+P18_UNPACK_FN(series_number);
+P18_UNPACK_FN(cpu_version);
+P18_UNPACK_FN(rated_information);
+P18_UNPACK_FN(general_status);
+P18_UNPACK_FN(working_mode);
+P18_UNPACK_FN(faults_warnings);
+P18_UNPACK_FN(flags_statuses);
+P18_UNPACK_FN(defaults);
+P18_UNPACK_FN(max_charging_current_selectable_values);
+P18_UNPACK_FN(max_ac_charging_current_selectable_values);
+P18_UNPACK_FN(parallel_rated_information);
+P18_UNPACK_FN(parallel_general_status);
+P18_UNPACK_FN(ac_charge_time_bucket);
+P18_UNPACK_FN(ac_supply_load_time_bucket);
+
+
+/* ------------------------------------------ */
+/* Label getters */
+
+const char *p18_battery_type_label(p18_battery_type_t type);
+const char *p18_input_voltage_range_label(p18_input_voltage_range_t range);
+const char *p18_output_source_priority_label(p18_output_source_priority_t priority);
+const char *p18_charge_source_priority_label(p18_charger_source_priority_t priority);
+const char *p18_machine_type_label(p18_machine_type_t type);
+const char *p18_topology_label(p18_topology_t topology);
+const char *p18_output_model_setting_label(p18_output_model_setting_t setting);
+const char *p18_solar_power_priority_label(p18_solar_power_priority_t priority);
+const char *p18_mppt_charger_status_label(p18_mppt_charger_status_t status);
+const char *p18_battery_power_direction_label(p18_battery_power_direction_t direction);
+const char *p18_dc_ac_power_direction_label(p18_dc_ac_power_direction_t direction);
+const char *p18_line_power_direction_label(p18_line_power_direction_t direction);
+const char *p18_working_mode_label(p18_working_mode_t mode);
+const char *p18_fault_code_label(unsigned int code);
+const char *p18_parallel_connection_status_label(p18_parallel_id_connection_status_t status);
+
+#endif //ISV_P18_H \ No newline at end of file
diff --git a/print.c b/print.c
new file mode 100644
index 0000000..3ab61bf
--- /dev/null
+++ b/print.c
@@ -0,0 +1,1089 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include "print.h"
+#include "util.h"
+
+#define PRINT_AUTO(items) \
+ if (print_is_table_format(format)) \
+ print_table((items), ARRAY_SIZE(items), format == PRINT_FORMAT_PARSABLE_TABLE); \
+ else \
+ print_json((items), ARRAY_SIZE(items), format == PRINT_FORMAT_JSON_W_UNITS);
+
+const short default_precision = 2;
+const char *units[] = {
+ " V",
+ " A",
+ " Wh",
+ " kWh",
+ " VA",
+ " Hz",
+ "%",
+ " °C",
+};
+const char *unknown_unit = " ?";
+const char *yes = "Yes";
+const char *no = "No";
+const char *true_s = "true";
+const char *false_s = "false";
+const char *enabled = "Enabled";
+const char *disabled = "Disabled";
+
+static const char* print_unit_label(print_unit_t unit)
+{
+ switch (unit) {
+ case PRINT_UNIT_A:
+ case PRINT_UNIT_V:
+ case PRINT_UNIT_KWH:
+ case PRINT_UNIT_WH:
+ case PRINT_UNIT_VA:
+ case PRINT_UNIT_HZ:
+ case PRINT_UNIT_CELSIUS:
+ case PRINT_UNIT_PERCENTAGE:
+ return units[unit-1];
+
+ default:
+ return unknown_unit;
+ }
+}
+
+static void print_table(print_item_t *items, size_t size, bool parsable)
+{
+ print_item_t item;
+ char fmt[32], doublefmt[16];
+ char k[64], v[64];
+
+ if (!parsable) {
+ size_t len, max_title_len = 0;
+ for (size_t i = 0; i < size; i++) {
+ item = items[i];
+ len = strlen(item.title) + 1 /* for colon */;
+ if (len > max_title_len)
+ max_title_len = len;
+ }
+ sprintf(fmt, "%%-%zus %%s", max_title_len);
+ } else {
+ sprintf(fmt, "%%s %%s");
+ }
+
+ const char *unit;
+ for (size_t i = 0; i < size; i++) {
+ item = items[i];
+
+ strcpy(k, parsable ? item.key : item.title);
+ if (!parsable)
+ strcat(k, ":");
+
+ if (variant_is_double(item.value)) {
+ sprintf(doublefmt, "%%2.%dlf",
+ item.precision ? item.precision : default_precision);
+ snprintf(v, 32, doublefmt, item.value.d);
+ } else if (variant_is_long(item.value))
+ snprintf(v, 32, "%ld", item.value.l);
+ else if (variant_is_string(item.value)) {
+ char *pos = strchr(item.value.s, ' ');
+ if (parsable && pos != NULL && pos != item.value.s)
+ snprintf(v, 32, "\"%s\"", item.value.s);
+ else
+ snprintf(v, 32, "%s", item.value.s);
+ } else if (variant_is_bool(item.value))
+ snprintf(v, 32, "%s", item.value.b ? yes : no);
+ else if (variant_is_flag(item.value))
+ snprintf(v, 32, "%s", item.value.b ? enabled : disabled);
+
+ printf(fmt, k, v);
+ if (item.unit) {
+ unit = print_unit_label(item.unit);
+ if (parsable && *unit != ' ')
+ putchar(' ');
+ printf("%s", print_unit_label(item.unit));
+ }
+
+ putchar('\n');
+ }
+}
+
+void print_json(print_item_t *items, size_t size, bool with_units)
+{
+ print_item_t item;
+ putchar('{');
+ for (size_t i = 0; i < size; i++) {
+ item = items[i];
+ printf("\"%s\":", item.key);
+
+ if (item.unit && with_units)
+ putchar('[');
+
+ if (variant_is_string(item.value))
+ printf("\"%s\"", item.value.s);
+ else if (variant_is_double(item.value))
+ printf("%2.2lf", item.value.d);
+ else if (variant_is_long(item.value))
+ printf("%ld", item.value.l);
+ else if (variant_is_bool(item.value) || variant_is_flag(item.value))
+ printf("%s", item.value.b ? true_s : false_s);
+
+ if (item.unit && with_units) {
+ const char *unit_label = print_unit_label(item.unit);
+ if (unit_label[0] == ' ')
+ unit_label++;
+ printf(",\"%s\"]", unit_label);
+ }
+
+ if (i < size-1)
+ putchar(',');
+ }
+ putchar('}');
+ putchar('\n');
+}
+
+bool print_is_json_format(print_format_t f)
+{
+ return f == PRINT_FORMAT_JSON_W_UNITS || f == PRINT_FORMAT_JSON;
+}
+
+static bool print_is_table_format(print_format_t f)
+{
+ return f == PRINT_FORMAT_TABLE || f == PRINT_FORMAT_PARSABLE_TABLE;
+}
+
+void print_set_result(bool success, print_format_t format) {
+ if (print_is_table_format(format))
+ printf("%s\n", success ? "OK" : "Failure");
+ else {
+ print_item_t items[] = {
+ {
+ .key = (success ? "ok" : "error"),
+ .value = (success ? variant_long(1) : variant_string("failure"))
+ }
+ };
+ print_json(items, ARRAY_SIZE(items), false);
+ }
+}
+
+static void print_table_list(const int *items, size_t size)
+{
+ for (size_t i = 0; i < size; i++)
+ printf("%d\n", items[i]);
+}
+
+static void print_json_list(const int *items, size_t size)
+{
+ putchar('[');
+ for (size_t i = 0; i < size; i++) {
+ printf("%d", items[i]);
+ if (i < size-1)
+ putchar(',');
+ }
+ putchar(']');
+ putchar('\n');
+}
+
+
+/* ------------------------------------------ */
+
+PRINT_FN(protocol_id)
+{
+ print_item_t items[] = {
+ {.key= "id", .title= "Protocol ID", .value= variant_long(m->id)}
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(current_time)
+{
+ print_item_t items[] = {
+ {.key= "year", .title= "Year", .value= variant_long(m->year) },
+ {.key= "month", .title= "Month", .value= variant_long(m->month) },
+ {.key= "day", .title= "Day", .value= variant_long(m->day) },
+ {.key= "hour", .title= "Hour", .value= variant_long(m->hour) },
+ {.key= "minute", .title= "Minute", .value= variant_long(m->minute)},
+ {.key= "second", .title= "Second", .value= variant_long(m->second)},
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(total_generated)
+{
+ print_item_t items[] = {
+ {
+ .key = "kwh",
+ .title = "kWh",
+ .value = variant_long((long)m->kwh)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(year_generated)
+{
+ print_item_t items[] = {
+ {
+ .key = "kwh",
+ .title = "kWh",
+ .value = variant_long((long)m->kwh)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(month_generated)
+{
+ print_item_t items[] = {
+ {
+ .key = "kwh",
+ .title = "kWh",
+ .value = variant_long((long)m->kwh)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(day_generated)
+{
+ print_item_t items[] = {
+ {
+ .key = "wh",
+ .title = "Wh",
+ .value = variant_long((long)m->kwh)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(series_number)
+{
+ print_item_t items[] = {
+ {.key= "sn", .title= "Series number", .value= variant_string(m->id)}
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(cpu_version)
+{
+ print_item_t items[] = {
+ {.key= "main_v", .title= "Main CPU version", .value= variant_string((m->main_cpu_version))},
+ {.key= "slave1_v", .title= "Slave 1 CPU version", .value= variant_string(m->slave1_cpu_version)},
+ {.key= "slave2_v", .title= "Slave 2 CPU version", .value= variant_string(m->slave2_cpu_version)},
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(rated_information)
+{
+ print_item_t items[] = {
+ {
+ .key = "ac_input_rating_voltage",
+ .title = "AC input rating voltage",
+ .value = variant_double(m->ac_input_rating_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "ac_input_rating_current",
+ .title = "AC input rating current",
+ .value = variant_double(m->ac_input_rating_current/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "ac_output_rating_voltage",
+ .title = "AC output rating voltage",
+ .value = variant_double(m->ac_output_rating_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "ac_output_rating_freq",
+ .title = "AC output rating frequency",
+ .value = variant_double(m->ac_output_rating_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ
+ },
+ {
+ .key = "ac_output_rating_current",
+ .title = "AC output rating current",
+ .value = variant_double(m->ac_output_rating_current/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "ac_output_rating_apparent_power",
+ .title = "AC output rating apparent power",
+ .value = variant_long(m->ac_output_rating_apparent_power),
+ .unit = PRINT_UNIT_VA,
+ },
+ {
+ .key = "ac_output_rating_active_power",
+ .title = "AC output rating active power",
+ .value = variant_long(m->ac_output_rating_active_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "battery_rating_voltage",
+ .title = "Battery rating voltage",
+ .value = variant_double(m->battery_rating_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_recharge_voltage",
+ .title = "Battery re-charge voltage",
+ .value = variant_double(m->battery_recharge_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_redischarge_voltage",
+ .title = "Battery re-discharge voltage",
+ .value = variant_double(m->battery_redischarge_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_under_voltage",
+ .title = "Battery under voltage",
+ .value = variant_double(m->battery_under_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_bulk_voltage",
+ .title = "Battery bulk voltage",
+ .value = variant_double(m->battery_bulk_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_float_voltage",
+ .title = "Battery float voltage",
+ .value = variant_double(m->battery_float_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_type",
+ .title = "Battery type",
+ .value = variant_string(p18_battery_type_label(m->battery_type))
+ },
+ {
+ .key = "max_charging_current",
+ .title = "Max charging current",
+ .value = variant_long(m->max_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "max_ac_charging_current",
+ .title = "Max AC charging current",
+ .value = variant_long(m->max_ac_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "input_voltage_range",
+ .title = "Input voltage range",
+ .value = variant_string(p18_input_voltage_range_label(m->input_voltage_range))
+ },
+ {
+ .key = "output_source_priority",
+ .title = "Output source priority",
+ .value = variant_string(p18_output_source_priority_label(m->output_source_priority))
+ },
+ {
+ .key = "charger_source_priority",
+ .title = "Charger source priority",
+ .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority))
+ },
+ {
+ .key = "parallel_max_num",
+ .title = "Parallel max num",
+ .value = variant_long(m->parallel_max_num)
+ },
+ {
+ .key = "machine_type",
+ .title = "Machine type",
+ .value = variant_string(p18_machine_type_label(m->machine_type))
+ },
+ {
+ .key = "topology",
+ .title = "Topology",
+ .value = variant_string(p18_topology_label(m->topology))
+ },
+ {
+ .key = "output_model_setting",
+ .title = "Output model setting",
+ .value = variant_string(p18_output_model_setting_label(m->output_model_setting))
+ },
+ {
+ .key = "solar_power_priority",
+ .title = "Solar power priority",
+ .value = variant_string(p18_solar_power_priority_label(m->solar_power_priority))
+ },
+ {
+ .key = "mppt",
+ .title = "MPPT string",
+ .value = variant_string(m->mppt)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(general_status)
+{
+ print_item_t items[] = {
+ {
+ .key = "grid_voltage",
+ .title = "Grid voltage",
+ .value = variant_double(m->grid_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "grid_freq",
+ .title = "Grid frequency",
+ .value = variant_double(m->grid_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ,
+ },
+ {
+ .key = "ac_output_voltage",
+ .title = "AC output voltage",
+ .value = variant_double(m->ac_output_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "ac_output_freq",
+ .title = "AC output frequency",
+ .value = variant_double(m->ac_output_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ,
+ },
+ {
+ .key = "ac_output_apparent_power",
+ .title = "AC output apparent power",
+ .value = variant_long(m->ac_output_apparent_power),
+ .unit = PRINT_UNIT_VA,
+ },
+ {
+ .key = "ac_output_active_power",
+ .title = "AC output active power",
+ .value = variant_long(m->ac_output_active_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "output_load_percent",
+ .title = "Output load percent",
+ .value = variant_long(m->output_load_percent),
+ .unit = PRINT_UNIT_PERCENTAGE,
+ },
+ {
+ .key = "battery_voltage",
+ .title = "Battery voltage",
+ .value = variant_double(m->battery_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_voltage_scc",
+ .title = "Battery voltage from SCC",
+ .value = variant_double(m->battery_voltage_scc/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_voltage_scc2",
+ .title = "Battery voltage from SCC2",
+ .value = variant_double(m->battery_voltage_scc2/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_discharge_current",
+ .title = "Battery discharge current",
+ .value = variant_long(m->battery_discharge_current),
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "battery_charging_current",
+ .title = "Battery charging current",
+ .value = variant_long(m->battery_charging_current),
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "battery_capacity",
+ .title = "Battery capacity",
+ .value = variant_long(m->battery_capacity),
+ .unit = PRINT_UNIT_PERCENTAGE,
+ },
+ {
+ .key = "inverter_heat_sink_temp",
+ .title = "Inverter heat sink temperature",
+ .value = variant_long(m->inverter_heat_sink_temp),
+ .unit = PRINT_UNIT_CELSIUS,
+ },
+ {
+ .key = "mppt1_charger_temp",
+ .title = "MPPT1 charger temperature",
+ .value = variant_long(m->mppt1_charger_temp),
+ .unit = PRINT_UNIT_CELSIUS,
+ },
+ {
+ .key = "mppt2_charger_temp",
+ .title = "MPPT2 charger temperature",
+ .value = variant_long(m->mppt2_charger_temp),
+ .unit = PRINT_UNIT_CELSIUS,
+ },
+ {
+ .key = "pv1_input_power",
+ .title = "PV1 Input power",
+ .value = variant_double(m->pv1_input_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "pv2_input_power",
+ .title = "PV2 Input power",
+ .value = variant_double(m->pv2_input_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "pv1_input_voltage",
+ .title = "PV1 Input voltage",
+ .value = variant_double(m->pv1_input_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "pv2_input_voltage",
+ .title = "PV2 Input voltage",
+ .value = variant_double(m->pv2_input_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "settings_values_changed",
+ .title = "Setting value configuration state",
+ .value = variant_string(m->settings_values_changed ? "Nothing changed" : "Something changed"),
+ },
+ {
+ .key = "mppt1_charger_status",
+ .title = "MPPT1 charger status",
+ .value = variant_string(p18_mppt_charger_status_label(m->mppt1_charger_status)),
+ },
+ {
+ .key = "mppt2_charger_status",
+ .title = "MPPT2 charger status",
+ .value = variant_string(p18_mppt_charger_status_label(m->mppt2_charger_status)),
+ },
+ {
+ .key = "load_connected",
+ .title = "Load connection",
+ .value = variant_string(m->load_connected ? "Connected" : "Disconnected"),
+ },
+ {
+ .key = "battery_power_direction",
+ .title = "Battery power direction",
+ .value = variant_string(p18_battery_power_direction_label(m->battery_power_direction)),
+ },
+ {
+ .key = "dc_ac_power_direction",
+ .title = "DC/AC power direction",
+ .value = variant_string(p18_dc_ac_power_direction_label(m->dc_ac_power_direction)),
+ },
+ {
+ .key = "line_power_direction",
+ .title = "Line power direction",
+ .value = variant_string(p18_line_power_direction_label(m->line_power_direction)),
+ },
+ {
+ .key = "local_parallel_id",
+ .title = "Local parallel ID",
+ .value = variant_long(m->local_parallel_id),
+ }
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(working_mode)
+{
+ print_item_t items[] = {
+ {.key= "mode", .title= "Working mode", .value= variant_string(p18_working_mode_label(m->mode))}
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(faults_warnings)
+{
+ print_item_t items[] = {
+ {.key= "fault_code", .title= "Fault code", .value= variant_string(p18_fault_code_label(m->fault_code))},
+ {.key= "line_fail", .title= "Line fail", .value= variant_bool(m->line_fail)},
+ {.key= "output_circuit_short", .title= "Output circuit short", .value= variant_bool(m->output_circuit_short)},
+ {.key= "inverter_over_temperature", .title= "Inverter over temperature",.value= variant_bool(m->inverter_over_temperature)},
+ {.key= "fan_lock", .title= "Fan lock", .value= variant_bool(m->fan_lock)},
+ {.key= "battery_voltage_high", .title= "Battery voltage high", .value= variant_bool(m->battery_voltage_high)},
+ {.key= "battery_low", .title= "Battery low", .value= variant_bool(m->battery_low)},
+ {.key= "battery_under", .title= "Battery under", .value= variant_bool(m->battery_under)},
+ {.key= "over_load", .title= "Over load", .value= variant_bool(m->over_load)},
+ {.key= "eeprom_fail", .title= "EEPROM fail", .value= variant_bool(m->eeprom_fail)},
+ {.key= "power_limit", .title= "Power limit", .value= variant_bool(m->power_limit)},
+ {.key= "pv1_voltage_high", .title= "PV1 voltage high", .value= variant_bool(m->pv1_voltage_high)},
+ {.key= "pv2_voltage_high", .title= "PV2 voltage high", .value= variant_bool(m->pv2_voltage_high)},
+ {.key= "mppt1_overload_warning", .title= "MPPT1 overload warning", .value= variant_bool(m->mppt1_overload_warning)},
+ {.key= "mppt2_overload_warning", .title= "MPPT2 overload warning", .value= variant_bool(m->mppt2_overload_warning)},
+ {.key= "battery_too_low_to_charge_for_scc1", .title= "Battery too low to charge for SCC1", .value= variant_bool(m->battery_too_low_to_charge_for_scc1)},
+ {.key= "battery_too_low_to_charge_for_scc2", .title= "Battery too low to charge for SCC2", .value= variant_bool(m->battery_too_low_to_charge_for_scc2)},
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(flags_statuses)
+{
+ print_item_t items[] = {
+ {
+ .key = "buzzer",
+ .title = "Buzzer",
+ .value = variant_flag(m->buzzer)
+ },
+ {
+ .key = "overload_bypass",
+ .title = "Overload bypass function",
+ .value = variant_flag(m->overload_bypass)
+ },
+ {
+ .key = "lcd_escape_to_default_page_after_1min_timeout",
+ .title = "Escape to default page after 1min timeout",
+ .value = variant_flag(m->lcd_escape_to_default_page_after_1min_timeout)
+ },
+ {
+ .key = "overload_restart",
+ .title = "Overload restart",
+ .value = variant_flag(m->overload_restart)
+ },
+ {
+ .key = "over_temp_restart",
+ .title = "Over temperature restart",
+ .value = variant_flag(m->over_temp_restart)
+ },
+ {
+ .key = "backlight_on",
+ .title = "Backlight on",
+ .value = variant_flag(m->backlight_on)
+ },
+ {
+ .key = "alarm_on_primary_source_interrupt",
+ .title = "Alarm on when primary source interrupt",
+ .value = variant_flag(m->alarm_on_primary_source_interrupt)
+ },
+ {
+ .key = "fault_code_record",
+ .title = "Fault code record",
+ .value = variant_flag(m->fault_code_record)
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(defaults)
+{
+ print_item_t items[] = {
+ {
+ .key = "ac_output_voltage",
+ .title = "AC output voltage",
+ .value = variant_double(m->ac_output_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "ac_output_freq",
+ .title = "AC output frequency",
+ .value = variant_double(m->ac_output_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ,
+ },
+ {
+ .key = "ac_input_voltage_range",
+ .title = "AC input voltage range",
+ .value = variant_string(p18_input_voltage_range_label(m->ac_input_voltage_range)),
+ },
+ {
+ .key = "battery_under_voltage",
+ .title = "Battery under voltage",
+ .value = variant_double(m->battery_under_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_bulk_voltage",
+ .title = "Charging bulk voltage",
+ .value = variant_double(m->charging_bulk_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_float_voltage",
+ .title = "Charging float voltage",
+ .value = variant_double(m->charging_float_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_recharge_voltage",
+ .title = "Battery re-charge voltage",
+ .value = variant_double(m->battery_recharge_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_redischarge_voltage",
+ .title = "Battery re-discharge voltage",
+ .value = variant_double(m->battery_redischarge_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "max_ac_charging_current",
+ .title = "Max AC charging current",
+ .value = variant_long(m->max_ac_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "max_charging_current",
+ .title = "Max charging current",
+ .value = variant_long(m->max_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "battery_type",
+ .title = "Battery type",
+ .value = variant_string(p18_battery_type_label(m->battery_type))
+ },
+ {
+ .key = "output_source_priority",
+ .title = "Output source priority",
+ .value = variant_string(p18_output_source_priority_label(m->output_source_priority))
+ },
+ {
+ .key = "charger_source_priority",
+ .title = "Charger source priority",
+ .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority))
+ },
+ {
+ .key = "solar_power_priority",
+ .title = "Solar power priority",
+ .value = variant_string(p18_solar_power_priority_label(m->solar_power_priority))
+ },
+ {
+ .key = "machine_type",
+ .title = "Machine type",
+ .value = variant_string(p18_machine_type_label(m->machine_type))
+ },
+ {
+ .key = "output_model_setting",
+ .title = "Output model setting",
+ .value = variant_string(p18_output_model_setting_label(m->output_model_setting))
+ },
+ {
+ .key = "buzzer_flag",
+ .title = "Buzzer flag",
+ .value = variant_flag(m->flag_buzzer)
+ },
+ {
+ .key = "overload_bypass_flag",
+ .title = "Overload bypass function flag",
+ .value = variant_flag(m->flag_overload_bypass)
+ },
+ {
+ .key = "lcd_escape_to_default_page_after_1min_timeout_flag",
+ .title = "Escape to default page after 1min timeout flag",
+ .value = variant_flag(m->flag_lcd_escape_to_default_page_after_1min_timeout)
+ },
+ {
+ .key = "overload_restart_flag",
+ .title = "Overload restart flag",
+ .value = variant_flag(m->flag_overload_restart)
+ },
+ {
+ .key = "over_temp_restart_flag",
+ .title = "Over temperature restart flag",
+ .value = variant_flag(m->flag_over_temp_restart)
+ },
+ {
+ .key = "backlight_on_flag",
+ .title = "Backlight on flag",
+ .value = variant_flag(m->flag_backlight_on)
+ },
+ {
+ .key = "alarm_on_primary_source_interrupt_flag",
+ .title = "Alarm on when primary source interrupt flag",
+ .value = variant_flag(m->flag_alarm_on_primary_source_interrupt)
+ },
+ {
+ .key = "fault_code_record_flag",
+ .title = "Fault code record flag",
+ .value = variant_flag(m->flag_fault_code_record)
+ }
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(max_charging_current_selectable_values)
+{
+ if (print_is_json_format(format))
+ print_json_list(m->amps, m->len);
+ else if (print_is_table_format(format))
+ print_table_list(m->amps, m->len);
+}
+
+PRINT_FN(max_ac_charging_current_selectable_values)
+{
+ if (print_is_json_format(format))
+ print_json_list(m->amps, m->len);
+ else if (print_is_table_format(format))
+ print_table_list(m->amps, m->len);
+}
+
+PRINT_FN(parallel_rated_information)
+{
+ print_item_t items[] = {
+ {
+ .key = "parallel_id_connection_status",
+ .title = "Parallel ID connection status",
+ .value = variant_string(p18_parallel_connection_status_label(m->parallel_id_connection_status)),
+ },
+ {
+ .key = "serial_number",
+ .title = "Serial number",
+ .value = variant_string(m->serial_number),
+ },
+ {
+ .key = "charger_source_priority",
+ .title = "Charger source priority",
+ .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority))
+ },
+ {
+ .key = "max_charging_current",
+ .title = "Max charging current",
+ .value = variant_long(m->max_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "max_ac_charging_current",
+ .title = "Max AC charging current",
+ .value = variant_long(m->max_ac_charging_current),
+ .unit = PRINT_UNIT_A
+ },
+ {
+ .key = "output_model_setting",
+ .title = "Output model setting",
+ .value = variant_string(p18_output_model_setting_label(m->output_model_setting))
+ },
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(parallel_general_status)
+{
+ print_item_t items[] = {
+ {
+ .key = "parallel_id_connection_status",
+ .title = "Parallel ID connection status",
+ .value = variant_string(p18_parallel_connection_status_label(m->parallel_id_connection_status)),
+ },
+ {
+ .key = "mode",
+ .title = "Working mode",
+ .value = variant_string(p18_working_mode_label(m->work_mode))
+ },
+ {
+ .key = "fault_code",
+ .title = "Fault code",
+ .value = variant_string(p18_fault_code_label(m->fault_code))
+ },
+ {
+ .key = "grid_voltage",
+ .title = "Grid voltage",
+ .value = variant_double(m->grid_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "grid_freq",
+ .title = "Grid frequency",
+ .value = variant_double(m->grid_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ,
+ },
+ {
+ .key = "ac_output_voltage",
+ .title = "AC output voltage",
+ .value = variant_double(m->ac_output_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "ac_output_freq",
+ .title = "AC output frequency",
+ .value = variant_double(m->ac_output_freq/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_HZ,
+ },
+ {
+ .key = "ac_output_apparent_power",
+ .title = "AC output apparent power",
+ .value = variant_long(m->ac_output_apparent_power),
+ .unit = PRINT_UNIT_VA,
+ },
+ {
+ .key = "ac_output_active_power",
+ .title = "AC output active power",
+ .value = variant_long(m->ac_output_active_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "total_ac_output_apparent_power",
+ .title = "Total AC output apparent power",
+ .value = variant_long(m->total_ac_output_apparent_power),
+ .unit = PRINT_UNIT_VA,
+ },
+ {
+ .key = "total_ac_output_active_power",
+ .title = "Total AC output active power",
+ .value = variant_long(m->total_ac_output_active_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "output_load_percent",
+ .title = "Output load percent",
+ .value = variant_long(m->output_load_percent),
+ .unit = PRINT_UNIT_PERCENTAGE,
+ },
+ {
+ .key = "total_output_load_percent",
+ .title = "Total output load percent",
+ .value = variant_long(m->total_output_load_percent),
+ .unit = PRINT_UNIT_PERCENTAGE,
+ },
+ {
+ .key = "battery_voltage",
+ .title = "Battery voltage",
+ .value = variant_double(m->battery_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "battery_discharge_current",
+ .title = "Battery discharge current",
+ .value = variant_long(m->battery_discharge_current),
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "battery_charging_current",
+ .title = "Battery charging current",
+ .value = variant_long(m->battery_charging_current),
+ .unit = PRINT_UNIT_A,
+ },
+ {
+ .key = "pv1_input_power",
+ .title = "PV1 Input power",
+ .value = variant_double(m->pv1_input_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "pv2_input_power",
+ .title = "PV2 Input power",
+ .value = variant_double(m->pv2_input_power),
+ .unit = PRINT_UNIT_WH,
+ },
+ {
+ .key = "pv1_input_voltage",
+ .title = "PV1 Input voltage",
+ .value = variant_double(m->pv1_input_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "pv2_input_voltage",
+ .title = "PV2 Input voltage",
+ .value = variant_double(m->pv2_input_voltage/10.0),
+ .precision = 1,
+ .unit = PRINT_UNIT_V,
+ },
+ {
+ .key = "mppt1_charger_status",
+ .title = "MPPT1 charger status",
+ .value = variant_string(p18_mppt_charger_status_label(m->mppt1_charger_status)),
+ },
+ {
+ .key = "mppt2_charger_status",
+ .title = "MPPT2 charger status",
+ .value = variant_string(p18_mppt_charger_status_label(m->mppt2_charger_status)),
+ },
+ {
+ .key = "load_connected",
+ .title = "Load connection",
+ .value = variant_string(m->load_connected ? "Connected" : "Disconnected"),
+ },
+ {
+ .key = "battery_power_direction",
+ .title = "Battery power direction",
+ .value = variant_string(p18_battery_power_direction_label(m->battery_power_direction)),
+ },
+ {
+ .key = "dc_ac_power_direction",
+ .title = "DC/AC power direction",
+ .value = variant_string(p18_dc_ac_power_direction_label(m->dc_ac_power_direction)),
+ },
+ {
+ .key = "line_power_direction",
+ .title = "Line power direction",
+ .value = variant_string(p18_line_power_direction_label(m->line_power_direction)),
+ },
+ {
+ .key = "max_temp",
+ .title = "Max. temperature",
+ .value = variant_long(m->max_temp),
+ }
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(ac_charge_time_bucket)
+{
+ static const int buf_size = 16;
+ char start_time[buf_size], end_time[buf_size];
+ snprintf(start_time, buf_size, "%02d:%02d", m->start_h, m->start_m);
+ snprintf(end_time, buf_size, "%02d:%02d", m->end_h, m->end_m);
+ print_item_t items[] = {
+ {.key= "start_time", .title= "Start time", .value= variant_string(start_time)},
+ {.key= "end_time", .title= "End time", .value= variant_string(end_time)},
+ };
+ PRINT_AUTO(items)
+}
+
+PRINT_FN(ac_supply_load_time_bucket)
+{
+ static const int buf_size = 16;
+ char start_time[buf_size], end_time[buf_size];
+ snprintf(start_time, buf_size, "%02d:%02d", m->start_h, m->start_m);
+ snprintf(end_time, buf_size, "%02d:%02d", m->end_h, m->end_m);
+ print_item_t items[] = {
+ {.key= "start_time", .title= "Start time", .value= variant_string(start_time)},
+ {.key= "end_time", .title= "End time", .value= variant_string(end_time)},
+ };
+ PRINT_AUTO(items)
+} \ No newline at end of file
diff --git a/print.h b/print.h
new file mode 100644
index 0000000..bb85269
--- /dev/null
+++ b/print.h
@@ -0,0 +1,82 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ISV_PRINT_H
+#define ISV_PRINT_H
+
+#include "p18.h"
+#include "variant.h"
+
+#define MAKE_PRINT_FN_NAME(msg_type) print_ ## msg_type ## _msg
+#define PRINT_FN_NAME(msg_type) MAKE_PRINT_FN_NAME(msg_type)
+
+#define PRINT_FN(msg_type) \
+ void PRINT_FN_NAME(msg_type)(const P18_MSG_T(msg_type) *m, print_format_t format)
+
+typedef enum {
+ PRINT_UNIT_V = 1,
+ PRINT_UNIT_A,
+ PRINT_UNIT_WH,
+ PRINT_UNIT_KWH,
+ PRINT_UNIT_VA,
+ PRINT_UNIT_HZ,
+ PRINT_UNIT_PERCENTAGE,
+ PRINT_UNIT_CELSIUS,
+} print_unit_t;
+
+typedef enum {
+ PRINT_FORMAT_TABLE,
+ PRINT_FORMAT_PARSABLE_TABLE,
+ PRINT_FORMAT_JSON,
+ PRINT_FORMAT_JSON_W_UNITS,
+} print_format_t;
+
+typedef struct {
+ char *key;
+ char *title;
+ variant_t value;
+ short precision;
+ print_unit_t unit;
+} print_item_t;
+
+void print_json(print_item_t *items, size_t size, bool with_units);
+void print_set_result(bool success, print_format_t format);
+bool print_is_json_format(print_format_t f);
+
+PRINT_FN(protocol_id);
+PRINT_FN(current_time);
+PRINT_FN(total_generated);
+PRINT_FN(year_generated);
+PRINT_FN(month_generated);
+PRINT_FN(day_generated);
+PRINT_FN(series_number);
+PRINT_FN(cpu_version);
+PRINT_FN(rated_information);
+PRINT_FN(general_status);
+PRINT_FN(working_mode);
+PRINT_FN(faults_warnings);
+PRINT_FN(flags_statuses);
+PRINT_FN(defaults);
+PRINT_FN(max_charging_current_selectable_values);
+PRINT_FN(max_ac_charging_current_selectable_values);
+PRINT_FN(parallel_rated_information);
+PRINT_FN(parallel_general_status);
+PRINT_FN(ac_charge_time_bucket);
+PRINT_FN(ac_supply_load_time_bucket);
+
+#endif //ISV_PRINT_H
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..856b2af
--- /dev/null
+++ b/util.c
@@ -0,0 +1,115 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdbool.h>
+
+#include "util.h"
+
+#define HEXDUMP_COLS 8
+
+/* based on: https://gist.github.com/richinseattle/c527a3acb6f152796a580401057c78b4 */
+void hexdump(void *mem, unsigned int len)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) {
+ /* Print offset */
+ if (i % HEXDUMP_COLS == 0)
+ printf("0x%06x: ", i);
+
+ if (i < len)
+ /* Print hex data */
+ printf("%02x ", 0xFF & ((char *) mem)[i]);
+ else
+ /* End of block, just aligning for ASCII dump */
+ printf(" ");
+
+ /* Print ASCII dump */
+ if (i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
+ for (j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
+ if (j >= len)
+ /* end of block, not really printing */
+ putchar(' ');
+ else if (isprint(((char *) mem)[j]))
+ /* printable char */
+ putchar(0xFF & ((char *) mem)[j]);
+ else
+ /* other char */
+ putchar('.');
+ }
+
+ putchar('\n');
+ }
+ }
+}
+
+/* dst buffer's length must be at least len+1 */
+void substr_copy(char *dst, const char *src, int len)
+{
+ strncpy(dst, src, len);
+ dst[len] = '\0';
+}
+
+bool isnumeric(const char *s)
+{
+ size_t len = strlen(s);
+ for (size_t i = 0; i < len; i++) {
+ if (!isdigit(s[i]))
+ return false;
+ }
+ return true;
+}
+
+bool isdatevalid(const int y, const int m, const int d)
+{
+ /* primitive out of range checks */
+ if (y < 2000 || y > 2099)
+ return false;
+
+ if (d < 1 || d > 31)
+ return false;
+
+ if (m < 1 || m > 12)
+ return false;
+
+ /* some more clever date validity checks */
+ if ((m == 4 || m == 6 || m == 9 || m == 11) && d == 31)
+ return false;
+
+ /* and finally a february check */
+ /* i always wondered, when do people born at feb 29 celebrate their bday? */
+ return m != 2 || ((y % 4 != 0 && d <= 28) || (y % 4 == 0 && d <= 29));
+}
+
+bool instrarray(const char *needle, const char **list, size_t list_size, int *index)
+{
+ bool found = false;
+ for (size_t i = 0; i < list_size; i++) {
+ /* LOG("%s: comparing %s == %s\n", __func__, list[i], needle); */
+ if (!strcmp(list[i], needle)) {
+ found = true;
+ if (index != NULL)
+ *index = (int)i;
+ break;
+ }
+ }
+ return found;
+}
diff --git a/util.h b/util.h
new file mode 100644
index 0000000..64db4bf
--- /dev/null
+++ b/util.h
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ISV_UTIL_H
+#define ISV_UTIL_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "util.h"
+
+extern bool g_verbose;
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+#define UNUSED(x) (void)(x)
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+
+#define LOG(f_, ...) \
+ if (g_verbose) fprintf(stderr, (f_), ##__VA_ARGS__)
+
+#define ERROR(f_, ...) \
+ fprintf(stderr, (f_), ##__VA_ARGS__)
+
+#define HEXDUMP(mem, len) \
+ if (g_verbose) hexdump((mem), (len))
+
+#define FOREACH_OFSIZE(item, array, len) \
+ for (int loop_keep = 1, loop_count = 0; \
+ loop_keep && loop_count != (int)(len); \
+ loop_keep = !loop_keep, loop_count++) \
+ for (item = (array) + loop_count; loop_keep; loop_keep = !loop_keep)
+
+#define FOREACH(item, array) \
+ FOREACH_OFSIZE(item, array, ARRAY_SIZE(array))
+
+void hexdump(void *mem, unsigned int len);
+void substr_copy(char *dst, const char *src, int len);
+bool isnumeric(const char *s);
+bool isdatevalid(int y, int m, int d);
+bool instrarray(const char *needle, const char **list, size_t list_size, int *index);
+
+#endif //ISV_UTIL_H
diff --git a/variant.c b/variant.c
new file mode 100644
index 0000000..aefece8
--- /dev/null
+++ b/variant.c
@@ -0,0 +1,86 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include "variant.h"
+
+variant_t variant_double(double d)
+{
+ variant_t v;
+ v.type = VARIANT_TYPE_DOUBLE;
+ v.d = d;
+ return v;
+}
+
+variant_t variant_long(long l)
+{
+ variant_t v;
+ v.type = VARIANT_TYPE_LONG;
+ v.l = l;
+ return v;
+}
+
+variant_t variant_bool(bool b)
+{
+ variant_t v;
+ v.type = VARIANT_TYPE_BOOL;
+ v.b = b;
+ return v;
+}
+
+variant_t variant_flag(bool b)
+{
+ variant_t v;
+ v.type = VARIANT_TYPE_FLAG;
+ v.b = b;
+ return v;
+}
+
+/* this just stores a pointer to a string, it doesn't copy the string itself */
+variant_t variant_string(const char *s)
+{
+ variant_t v;
+ v.type = VARIANT_TYPE_STRING;
+ v.s = s;
+ return v;
+}
+
+inline bool variant_is_string(variant_t v)
+{
+ return v.type == VARIANT_TYPE_STRING;
+}
+
+inline bool variant_is_long(variant_t v)
+{
+ return v.type == VARIANT_TYPE_LONG;
+}
+
+inline bool variant_is_bool(variant_t v)
+{
+ return v.type == VARIANT_TYPE_BOOL;
+}
+
+inline bool variant_is_flag(variant_t v)
+{
+ return v.type == VARIANT_TYPE_FLAG;
+}
+
+inline bool variant_is_double(variant_t v)
+{
+ return v.type == VARIANT_TYPE_DOUBLE;
+} \ No newline at end of file
diff --git a/variant.h b/variant.h
new file mode 100644
index 0000000..6aaf47f
--- /dev/null
+++ b/variant.h
@@ -0,0 +1,54 @@
+/**
+ * Copyright (C) 2020 Evgeny Zinoviev
+ * This file is part of isv <https://github.com/gch1p/isv>.
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ISV_VARIANT_H
+#define ISV_VARIANT_H
+
+#include <stdbool.h>
+
+typedef enum {
+ VARIANT_TYPE_STRING,
+ VARIANT_TYPE_LONG,
+ VARIANT_TYPE_DOUBLE,
+ VARIANT_TYPE_BOOL,
+ VARIANT_TYPE_FLAG,
+} variant_type_t;
+
+typedef struct {
+ variant_type_t type;
+ double d;
+ long l;
+ bool b;
+ const char *s;
+} variant_t;
+
+variant_t variant_double(double d);
+variant_t variant_long(long l);
+variant_t variant_bool(bool b);
+variant_t variant_flag(bool b);
+variant_t variant_string(const char *s);
+
+void variant_to_string(variant_t v, char *buf, size_t bufsize);
+
+bool variant_is_string(variant_t v);
+bool variant_is_long(variant_t v);
+bool variant_is_bool(variant_t v);
+bool variant_is_flag(variant_t v);
+bool variant_is_double(variant_t v);
+
+#endif //ISV_VARIANT_H