diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 41 | ||||
-rw-r--r-- | README.md | 312 | ||||
-rw-r--r-- | isv.c | 861 | ||||
-rw-r--r-- | libvoltronic/voltronic_crc.c | 99 | ||||
-rw-r--r-- | libvoltronic/voltronic_crc.h | 71 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev.c | 430 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev.h | 109 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev_impl.h | 125 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev_serial.h | 78 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev_serial_libserialport.c | 196 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev_usb.h | 37 | ||||
-rw-r--r-- | libvoltronic/voltronic_dev_usb_hidapi.c | 106 | ||||
-rw-r--r-- | p18.c | 1559 | ||||
-rw-r--r-- | p18.h | 505 | ||||
-rw-r--r-- | print.c | 1089 | ||||
-rw-r--r-- | print.h | 82 | ||||
-rw-r--r-- | util.c | 115 | ||||
-rw-r--r-- | util.h | 57 | ||||
-rw-r--r-- | variant.c | 86 | ||||
-rw-r--r-- | variant.h | 54 |
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 @@ -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, ©[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; + } + } +} @@ -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 @@ -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 @@ -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 @@ -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 @@ -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; +} @@ -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 |