From 557a8f9de0961c5ad26d8a84950d67f3cae3bf1a Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 18 Oct 2020 23:45:08 +0300 Subject: initial commit --- .gitignore | 3 + Makefile | 41 + README.md | 312 +++++ isv.c | 861 ++++++++++++ libvoltronic/voltronic_crc.c | 99 ++ libvoltronic/voltronic_crc.h | 71 + libvoltronic/voltronic_dev.c | 430 ++++++ libvoltronic/voltronic_dev.h | 109 ++ libvoltronic/voltronic_dev_impl.h | 125 ++ libvoltronic/voltronic_dev_serial.h | 78 ++ libvoltronic/voltronic_dev_serial_libserialport.c | 196 +++ libvoltronic/voltronic_dev_usb.h | 37 + libvoltronic/voltronic_dev_usb_hidapi.c | 106 ++ p18.c | 1559 +++++++++++++++++++++ p18.h | 505 +++++++ print.c | 1089 ++++++++++++++ print.h | 82 ++ util.c | 115 ++ util.h | 57 + variant.c | 86 ++ variant.h | 54 + 21 files changed, 6015 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 isv.c create mode 100644 libvoltronic/voltronic_crc.c create mode 100644 libvoltronic/voltronic_crc.h create mode 100644 libvoltronic/voltronic_dev.c create mode 100644 libvoltronic/voltronic_dev.h create mode 100644 libvoltronic/voltronic_dev_impl.h create mode 100644 libvoltronic/voltronic_dev_serial.h create mode 100644 libvoltronic/voltronic_dev_serial_libserialport.c create mode 100644 libvoltronic/voltronic_dev_usb.h create mode 100644 libvoltronic/voltronic_dev_usb_hidapi.c create mode 100644 p18.c create mode 100644 p18.h create mode 100644 print.c create mode 100644 print.h create mode 100644 util.c create mode 100644 util.h create mode 100644 variant.c create mode 100644 variant.h 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`
+ **`--raw`** `COMMAND` - execute arbitrary command and print inverter's response.
+ Command example: `^P005PI` + +- **`-t `** `TIMEOUT`
+ **`--timeout`** `TIMEOUT` - device read timeout, in milliseconds.
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`
+ **`--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`
+ `ID` - parallel machine ID + +- **`--get-parallel-general-status`** `ID`
+ `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`**
+ Reset changeable parameters to their default values. + +- **`--set-battery-max-charging-current`** `ID` `AMPS`
+ `ID` - parallel machine ID (use 0 for a single model)
+ `AMPS` - use `--get-max-charging-current-selectable-values` to see a list of allowed currents + +- **`--set-battery-max-ac-charging-current`** `ID` `AMPS`
+ `ID` - parallel machine ID (use 0 for a single model)
+ `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`
+ `CV` - constant voltage (48.0 ~ 58.4)
+ `FV` - float voltage (48.0 ~ 58.4) + +- **`--set-ac-output-rated-voltage`** `V`
+ `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*
+ - `SBU` is for *Solar-Battery-Utility* + +- **`--set-battery-charging-thresholds`** `CV` `DV`
+ Sets battery re-charging and re-discharigng voltages when utility is available. + + `CV` - re-charging voltage
+ *For 12V unit:* `11`, `11.3`, `11.5`, `11.8`, `12`, `12.3`, `12.5` or `12.8`
+ *For 24V unit:* `22`, `22.5`, `23`, `23.5`, `24`, `24.5`, `25` or `25.5`
+ *For 48V unit:* `44`, `45`, `46`, `47`, `48`, `49`, `50` or `51` + + `DV` - re-discharging voltage
+ *For 12V unit:* `0`, `12`, `12.3`, `12.5`, `12.8`, `13`, `13.3`, `13.5`, `13.8`, `14`, `14.3` or `14.5`
+ *For 24V unit:* `0`, `24`, `24.5`, `25`, `25.5`, `26`, `26.5`, `27`, `27.5`, `28`, `28.5` or `29`
+ *For 48V unit:* `0`, `48`, `49`, `50`, `51`, `52`, `53`, `54`, `55`, `56`, `57` or `58`
+ +- **`--set-charging-source-priority`** `ID` `PRIORITY`
+ `ID` - parallel machine ID (use 0 for a single model). + + List of priorities: + + - `SF` for *Solar-First*
+ - `SU` for *Solar-and-Utility*
+ - `S` for *Solar-Only* + +- **`--set-solar-power-priority`** `PRIORITY` + + List of priorities: + + - `BLU` for *Battery-Load-Utility*
+ - `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`
+ `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`
+ `V` - cut-off voltage (40.0 ~ 48.0) + +- **`--set-solar-configuration`** `ID`
+ `ID` - serial number + +- **`--clear-generated-data`**
+ Clears all data of generated energy. + +- **`--set-date-time`** `YYYY` `MM` `DD` `hh` `mm` `ss`
+ `YYYY` - year
+ `MM` - month
+ `DD` - day
+ `hh` - hours
+ `mm` - minutes
+ `ss` - seconds + +- **`--set-ac-charge-time-bucket`** `START` `END`
+ `START` - starting time, `hh:mm` format
+ `END` - ending time, `hh:mm` format + +- **`--set-ac-supply-load-time-bucket`** `START` `END`
+ `START` - starting time, `hh:mm` format
+ `END` - ending time, `hh:mm` format + +### Formats +- `table` - human-readable table. This is used by default. + + Output example: + ``` + Grid voltage: 0.0 V + Grid frequency: 0.0 Hz + AC output voltage: 230.1 V + AC output frequency: 50.0 Hz + AC output apparent power: 114 VA + AC output active power: 69 Wh + Output load percent: 2% + Battery voltage: 49.5 V + Battery voltage from SCC: 0.0 V + Battery voltage from SCC2: 0.0 V + Battery discharge current: 1 A + Battery charging current: 0 A + Battery capacity: 73% + Inverter heat sink temperature: 32 °C + MPPT1 charger temperature: 0 °C + MPPT2 charger temperature: 0 °C + PV1 Input power: 0.00 Wh + PV2 Input power: 0.00 Wh + PV1 Input voltage: 0.0 V + PV2 Input voltage: 0.0 V + Setting value configuration state: Something changed + MPPT1 charger status: Abnormal + MPPT2 charger status: Abnormal + Load connection: Connected + Battery power direction: Discharge + DC/AC power direction: DC/AC + Line power direction: Do nothing + Local parallel ID: 0 + ``` + +- `parsable-table` + + Output example: + + ``` + grid_voltage 0.0 V + grid_freq 0.0 Hz + ac_output_voltage 230.0 V + ac_output_freq 50.0 Hz + ac_output_apparent_power 92 VA + ac_output_active_power 52 Wh + output_load_percent 1 % + battery_voltage 49.5 V + battery_voltage_scc 0.0 V + battery_voltage_scc2 0.0 V + battery_discharge_current 1 A + battery_charging_current 0 A + battery_capacity 73 % + inverter_heat_sink_temp 32 °C + mppt1_charger_temp 0 °C + mppt2_charger_temp 0 °C + pv1_input_power 0.00 Wh + pv2_input_power 0.00 Wh + pv1_input_voltage 0.0 V + pv2_input_voltage 0.0 V + settings_values_changed "Something changed" + mppt1_charger_status Abnormal + mppt2_charger_status Abnormal + load_connected Connected + battery_power_direction Discharge + dc_ac_power_direction DC/AC + line_power_direction "Do nothing" + local_parallel_id 0 + ``` + +- `json` - JSON. + + Output example: + + ``` + {"grid_voltage":0.00,"grid_freq":0.00,"ac_output_voltage":229.90,"ac_output_freq":49.90,"ac_output_apparent_power":91,"ac_output_active_power":47,"output_load_percent":1,"battery_voltage":49.50,"battery_voltage_scc":0.00,"battery_voltage_scc2":0.00,"battery_discharge_current":1,"battery_charging_current":0,"battery_capacity":73,"inverter_heat_sink_temp":32,"mppt1_charger_temp":0,"mppt2_charger_temp":0,"pv1_input_power":0.00,"pv2_input_power":0.00,"pv1_input_voltage":0.00,"pv2_input_voltage":0.00,"settings_values_changed":"Something changed","mppt1_charger_status":"Abnormal","mppt2_charger_status":"Abnormal","load_connected":"Connected","battery_power_direction":"Discharge","dc_ac_power_direction":"DC/AC","line_power_direction":"Do nothing","local_parallel_id":0} + ``` + +- `json-w-units` - JSON with units. + + Output example: + + ``` + {"grid_voltage":[0.00,"V"],"grid_freq":[0.00,"Hz"],"ac_output_voltage":[230.10,"V"],"ac_output_freq":[50.00,"Hz"],"ac_output_apparent_power":[92,"VA"],"ac_output_active_power":[53,"Wh"],"output_load_percent":[1,"%"],"battery_voltage":[49.50,"V"],"battery_voltage_scc":[0.00,"V"],"battery_voltage_scc2":[0.00,"V"],"battery_discharge_current":[1,"A"],"battery_charging_current":[0,"A"],"battery_capacity":[73,"%"],"inverter_heat_sink_temp":[32,"°C"],"mppt1_charger_temp":[0,"°C"],"mppt2_charger_temp":[0,"°C"],"pv1_input_power":[0.00,"Wh"],"pv2_input_power":[0.00,"Wh"],"pv1_input_voltage":[0.00,"V"],"pv2_input_voltage":[0.00,"V"],"settings_values_changed":"Something changed","mppt1_charger_status":"Abnormal","mppt2_charger_status":"Abnormal","load_connected":"Connected","battery_power_direction":"Discharge","dc_ac_power_direction":"DC/AC","line_power_direction":"Do nothing","local_parallel_id":0} + ``` + +### Return codes + +**isv** returns `0` on success, `1` on some input error (e.g. invalid argument) and `2` on communication failure (e.g. +failed to read response from inverter, or response is invalid). + +## License + +GPLv3 \ No newline at end of file diff --git a/isv.c b/isv.c new file mode 100644 index 0000000..f8603a7 --- /dev/null +++ b/isv.c @@ -0,0 +1,861 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#include + +#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 ,\n" + " --raw : execute arbitrary command and print inverter's\n" + " response. Command example: ^P005PI\n" + " -t ,\n" + " --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 ,\n" + " --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 \n" + " --get-month-generated \n" + " --get-day-generated
\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 \n" + " ID: parallel machine ID\n" + "\n" + " --get-parallel-general-status \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 0|1\n" + " --set-defaults\n" + " --set-battery-max-charging-current \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 \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 \n" + " CV: constant voltage (48.0~58.4)\n" + " FV: float voltage (48.0~58.4)\n" + "\n" + " --set-ac-output-rated-voltage \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 \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 \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 \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 \n" + " V: cut-off voltage (40.0~48.0)\n" + "\n" + " --set-solar-configuration \n" + " ID: serial number\n" + "\n" + " --clear-generated-data\n" + " Clears all data of generated energy.\n" + "\n" + " --set-date-time
\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 \n" + " START: starting time, hh:mm format\n" + " END: ending time, hh:mm format\n" + "\n" + " --set-ac-supply-load-time-bucket \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 . + */ + +#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 . + */ + +#ifndef __VOLTRONIC__CRC__H__ +#define __VOLTRONIC__CRC__H__ + + #include + + /** + * 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 + + 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 . + */ + +#include +#include +#include + +#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 + + typedef uint64_t millisecond_timestamp_t; + +#elif defined(ARDUINO) + + #include + + typedef unsigned long millisecond_timestamp_t; + +#else + + #include + #include + #include + #include + + 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 . + */ + +#ifndef __VOLTRONIC__DEV__H__ +#define __VOLTRONIC__DEV__H__ + +#include + +/** + * 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 . + */ + +#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 + + 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 . + */ + +#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 . + */ + +#include "voltronic_dev_impl.h" +#include "voltronic_dev_serial.h" +#include + +#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 . + */ + +#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 . + */ + +#include +#include +#include "voltronic_dev_impl.h" +#include "voltronic_dev_usb.h" +#include "hidapi.h" + +#define HID_REPORT_SIZE 8 + +#define VOLTRONIC_DEV_USB(_impl_ptr_) \ + ((hid_device*) (_impl_ptr_)) + +#define GET_REPORT_SIZE(_val_) \ + (((_val_) > HID_REPORT_SIZE) ? HID_REPORT_SIZE : (_val_)) + +static inline void voltronic_usb_init_hidapi(void); + +voltronic_dev_t voltronic_usb_create( + const unsigned int vendor_id, + const unsigned int product_id) { + + voltronic_usb_init_hidapi(); + + SET_LAST_ERROR(0); + hid_device* dev = hid_open( + (unsigned short) vendor_id, + (unsigned short) product_id, + (const wchar_t*) 0); + + if (dev != 0) { + SET_LAST_ERROR(0); + return voltronic_dev_internal_create((void*) dev); + } + + return 0; +} + +inline int voltronic_dev_impl_read( + void* impl_ptr, + char* buffer, + const size_t buffer_size, + const unsigned int timeout_milliseconds) { + + SET_LAST_ERROR(0); + return hid_read_timeout( + VOLTRONIC_DEV_USB(impl_ptr), + (unsigned char*) buffer, + GET_REPORT_SIZE(buffer_size), + (int) timeout_milliseconds); +} + +inline int voltronic_dev_impl_write( + void* impl_ptr, + const char* buffer, + const size_t buffer_size, + unsigned int timeout_milliseconds) { + + ++timeout_milliseconds; // stop unused warnings + const int write_size = GET_REPORT_SIZE(buffer_size); + unsigned char write_buffer[HID_REPORT_SIZE + 1] = { 0 }; + COPY_MEMORY(&write_buffer[1], buffer, write_size); + + SET_LAST_ERROR(0); + const int bytes_written = hid_write( + VOLTRONIC_DEV_USB(impl_ptr), + (const unsigned char*) write_buffer, + (size_t) (HID_REPORT_SIZE + 1)); + + return GET_REPORT_SIZE(bytes_written); +} + +inline int voltronic_dev_impl_close(void* impl_ptr) { + hid_close(VOLTRONIC_DEV_USB(impl_ptr)); + return 1; +} + +static inline void voltronic_usb_exit_hidapi(void) { + hid_exit(); +} + +static inline void voltronic_usb_init_hidapi(void) { + static int hidapi_init_complete = 0; + + if (hidapi_init_complete == 0) { + if (hid_init() == 0) { + atexit(voltronic_usb_exit_hidapi); + hidapi_init_complete = 1; + } + } +} diff --git a/p18.c b/p18.c new file mode 100644 index 0000000..5ca71f2 --- /dev/null +++ b/p18.c @@ -0,0 +1,1559 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#include +#include +#include +#include +#include + +#include "p18.h" +#include "util.h" + +const char *p18_query_cmds[] = { + "PI", /* Protocol ID */ + "T", /* Current time */ + "ET", /* Total generated energy */ + "EY", /* Year generated energy */ + "EM", /* Month generated energy */ + "ED", /* Day generated energy */ + "ID", /* Series number */ + "VFW", /* CPU version */ + "PIRI", /* Rated information */ + "GS", /* General status */ + "MOD", /* Working mode */ + "FWS", /* Fault and warning status */ + "FLAG", /* Enable/Disable flags statuses */ + "DI", /* Default values of changeable parameters */ + "MCHGCR", /* Max charging current selectable (allowed) values */ + "MUCHGCR", /* Max AC charging current selectable (allowed) values */ + "PRI", /* Parallel system rated information */ + "PGS", /* Parallel system general status */ + "ACCT", /* AC charge time bucket */ + "ACLT", /* AC supply load time bucket */ +}; + +const char *p18_set_cmds[] = { + "LON", /* Enable/disable machine supply power to the loads */ + "P", /* Enable/disable flags */ + "PF", /* Reset changeable params */ + "MCHGC", /* Battery maximum charge current */ + "MUCHGC", /* Battery maximum AC charge current */ + /* The protocol documentation defines two commands, "F50" and "F60", + but it's identical as if there were just one "F" command with an argument. */ + "F", /* Set AC output frequency to be 50Hz or 60Hz */ + "MCHGV", /* Battery maximum charge voltage */ + "V", /* AC output rated voltage */ + "POP", /* Output source priority */ + "BUCD", /* Battery re-charged and re-discharged voltage when utility is available */ + "PCP", /* Charging source priority */ + "PSP", /* Solar power priority */ + "PGR", /* AC input voltage range */ + "PBT", /* Battery type */ + "POPM", /* Output model */ + "PSDV", /* Battery cut-off voltage */ + "ID", /* Set solar configuration */ + "CLE", /* Clear all data of generated energy */ + "DAT", /* Set date and time */ + "ACCT", /* AC charge time bucket */ + "ACLT", /* AC supply load time bucket */ +}; + +const p18_fault_code_list_item_t fault_codes[] = { + {1, "Fan is locked"}, + {2, "Over temperature"}, + {3, "Battery voltage is too high"}, + {4, "Battery voltage is too low"}, + {5, "Output short circuited or Over temperature"}, + {6, "Output voltage is too high"}, + {7, "Over load time out"}, + {8, "Bus voltage is too high"}, + {9, "Bus soft start failed"}, + {11, "Main relay failed"}, + {51, "Over current inverter"}, + {52, "Bus soft start failed"}, + {53, "Inverter soft start failed"}, + {54, "Self-test failed"}, + {55, "Over DC voltage on output of inverter"}, + {56, "Battery connection is open"}, + {57, "Current sensor failed"}, + {58, "Output voltage is too low"}, + {60, "Inverter negative power"}, + {71, "Parallel version different"}, + {72, "Output circuit failed"}, + {80, "CAN communication failed"}, + {81, "Parallel host line lost"}, + {82, "Parallel synchronized signal lost"}, + {83, "Parallel battery voltage detect different"}, + {84, "Parallel Line voltage or frequency detect different"}, + {85, "Parallel Line input current unbalanced"}, + {86, "Parallel output setting different"}, +}; + +const p18_flag_printable_list_item_t p18_flags_printable_list[9] = { + {"BUZZ", "A", "Silence buzzer or open buzzer"}, + {"OLBP", "B", "Overload bypass function"}, + {"LCDE", "C", "LCD display escape to default page after 1min timeout"}, + {"OLRS", "D", "Overload restart"}, + {"OTRS", "E", "Overload temperature restart"}, + {"BLON", "F", "Backlight on"}, + {"ALRM", "G", "Alarm on primary source interrupt"}, + {"FTCR", "H", "Fault code record"}, + {"MTYP", "I", "Machine type (1=Grid-Tie, 0=Off-Grid-Tie)"}, +}; + +const int p18_ac_output_rated_voltages[5] = { + 202, + 208, + 220, + 230, + 240, +}; + +const char *p18_battery_util_recharging_voltages_12v_unit[8] = { + "11", + "11.3", + "11.5", + "11.8", + "12", + "12.3", + "12.5", + "12.8", +}; + +const char *p18_battery_util_recharging_voltages_24v_unit[8] = { + "22", + "22.5", + "23", + "23.5", + "24", + "24.5", + "25", + "25.5", +}; +const char *p18_battery_util_recharging_voltages_48v_unit[8] = { + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "51", +}; + +const char *p18_battery_util_redischarging_voltages_12v_unit[12] = { + "0", + "12", + "12.3", + "12.5", + "12.8", + "13", + "13.3", + "13.5", + "13.8", + "14", + "14.3", + "14.5", +}; +const char *p18_battery_util_redischarging_voltages_24v_unit[12] = { + "0", + "24", + "24.5", + "25", + "25.5", + "26", + "26.5", + "27", + "27.5", + "28", + "28.5", + "29" +}; +const char *p18_battery_util_redischarging_voltages_48v_unit[12] = { + "0", + "48", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", +}; + +const char *p18_battery_types[] = { + "AGM", + "Flooded", + "User", +}; + +const char *p18_voltage_ranges[] = { + "Appliance", + "UPS" +}; + +const char *p18_output_source_priorities[] = { + "Solar-Utility-Battery", + "Solar-Battery-Utility" +}; + +const char *p18_charger_source_priorities[] = { + "Solar-First", + "Solar-and-Utility", + "Solar-Only" +}; + +const char *p18_machine_types[] = { + "Off-Grid-Tie", + "Grid-Tie" +}; + +const char *p18_topologies[] = { + "Transformerless", + "Transformer" +}; + +const char *p18_output_model_settings[] = { + "Single module", + "Parallel output", + "Phase 1 of three phase output", + "Phase 2 of three phase output", + "Phase 3 of three phase", +}; + +const char *p18_solar_power_priorities[] = { + "Battery-Load-Utility", + "Load-Battery-Utility" +}; + +const char *p18_mppt_charger_statuses[] = { + "Abnormal", + "Not charging", + "Charging" +}; + +const char *p18_battery_power_directions[] = { + "Do nothing", + "Charge", + "Discharge" +}; + +const char *p18_dc_ac_power_directions[] = { + "Do nothing", + "AC/DC", + "DC/AC" +}; + +const char *p18_line_power_directions[] = { + "Do nothing", + "Input", + "Output", +}; + +const char *p18_working_modes[] = { + "Power on mode", + "Standby mode", + "Bypass mode", + "Battery mode", + "Fault mode", + "Hybrid mode", +}; + +const char *p18_parallel_connection_statuses[] = { + "Non-existent", + "Existent" +}; + +const char *p18_unknown = "Unknown"; + +/* ------------------------------------------ */ + +static char *p18_append_arguments(int command, char *buf, const char **a) +{ + switch (command) { + case P18_QUERY_YEAR_GENERATED: + buf += sprintf(buf, "%s", a[0]); + break; + + case P18_QUERY_MONTH_GENERATED: + buf += sprintf(buf, "%s%02d", a[0], atoi(a[1])); + break; + + case P18_QUERY_DAY_GENERATED: + buf += sprintf(buf, "%s%02d%02d", + a[0], atoi(a[1]), atoi(a[2])); + break; + + case P18_QUERY_PARALLEL_RATED_INFORMATION: + case P18_QUERY_PARALLEL_GENERAL_STATUS: + buf += sprintf(buf, "%d", atoi(a[0])); + break; + + case P18_SET_LOADS: + buf += sprintf(buf, "%s", a[0]); + break; + + case P18_SET_FLAG: + buf += sprintf(buf, "%c%s", + *a[1] == '1' ? 'E' : 'D', + a[0]); + break; + + case P18_SET_BAT_MAX_CHARGE_CURRENT: + case P18_SET_BAT_MAX_AC_CHARGE_CURRENT: + buf += sprintf(buf, "%c,%03d", *a[0], atoi(a[1])); + break; + + case P18_SET_AC_OUTPUT_FREQ: + buf += sprintf(buf, "%02d", atoi(a[0])); + break; + + case P18_SET_BAT_MAX_CHARGE_VOLTAGE: { + double cv = atof(a[0]); + double fv = atof(a[1]); + buf += sprintf(buf, "%03d,%03d", (int)round(cv*10), (int)round(fv*10)); + break; + } + + case P18_SET_AC_OUTPUT_RATED_VOLTAGE: { + int v = atoi(a[0]); + buf += sprintf(buf, "%04d", v*10); + break; + } + + case P18_SET_OUTPUT_SOURCE_PRIORITY: + case P18_SET_SOLAR_POWER_PRIORITY: + case P18_SET_AC_INPUT_VOLTAGE_RANGE: + case P18_SET_BAT_TYPE: + buf += sprintf(buf, "%c", *a[0]); + break; + + case P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL: { + double cv = atof(a[0]); + double dv = atof(a[1]); + buf += sprintf(buf, "%03d,%03d", (int)round(cv*10), (int)round(dv*10)); + break; + } + + case P18_SET_CHARGING_SOURCE_PRIORITY: + case P18_SET_OUTPUT_MODEL: + buf += sprintf(buf, "%c,%c", *a[0], *a[1]); + break; + + case P18_SET_BAT_CUTOFF_VOLTAGE: { + double v = atof(a[0]); + buf += sprintf(buf, "%03d", (int)round(v*10)); + break; + } + + case P18_SET_SOLAR_CONFIG: { + int len = (int)strlen(a[0]); + buf += sprintf(buf, "%02d", len); + buf += sprintf(buf, "%s", a[0]); + if (len < 20) { + for (int i = 0; i < 20-len; i++) + buf += sprintf(buf, "%c", '0'); + } + break; + } + + case P18_SET_DATE_TIME: { + int Y = atoi(a[0]) - 2000; + int M = atoi(a[1]); + int D = atoi(a[2]); + int h = atoi(a[3]); + int m = atoi(a[4]); + int s = atoi(a[5]); + buf += sprintf(buf, "%02d%02d%02d%02d%02d%02d", Y, M, D, h, m, s); + break; + } + + case P18_SET_AC_CHARGE_TIME_BUCKET: + case P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET: + buf += sprintf(buf, "%02d%02d,%02d%02d", + atoi(a[0]), atoi(a[1]), atoi(a[2]), atoi(a[3])); + break; + } + + return buf; +} + +bool p18_build_command(int command, const char **args, size_t args_size, char *buf) +{ + UNUSED(args_size); + + bool set = command >= P18_SET_CMDS_ENUM_OFFSET; + int offset; + const char **list; + size_t list_size; + if (!set) { + list = p18_query_cmds; + list_size = ARRAY_SIZE(p18_query_cmds); + offset = P18_QUERY_CMDS_ENUM_OFFSET; + } else { + list = p18_set_cmds; + list_size = ARRAY_SIZE(p18_set_cmds); + offset = P18_SET_CMDS_ENUM_OFFSET; + } + + int cmd_index = command - offset; + if (cmd_index < 0 || cmd_index >= (int)list_size) { + LOG("%s: invalid cmd_index %d\n", __func__, cmd_index); + return false; + } + + (*buf++) = '^'; + (*buf++) = set ? 'S' : 'P'; + + char *len_p = buf; + buf += sprintf(buf, "000"); + buf += sprintf(buf, "%s", list[cmd_index]); + + buf = p18_append_arguments(command, buf, args); + + char len_buf[4]; + sprintf(len_buf, "%03zu", (size_t)(buf-len_p)); + memcpy(len_p, len_buf, 3); + + return true; +} + +bool p18_validate_query_response(const char *buf, size_t size, size_t *data_size) +{ + if (buf[0] != '^' || buf[1] != 'D') + return false; + + char lenbuf[4]; + memcpy(lenbuf, &buf[2], 3); + lenbuf[3] = '\0'; + + unsigned int len = atoi(lenbuf); + if (size < len+4) + return false; + + if (data_size) + *data_size = len-3; + return true; +} + +bool p18_set_result(const char *buf, size_t size) +{ + if (size < 2) { + ERROR("%s: response is too small\n", __func__); + return false; + } + if (buf[0] != '^') { + ERROR("%s: invalid character %c, ^ expected\n", __func__, *buf); + return false; + } + return buf[1] == '1'; +} + +static void p18_parse_list( + const char *data, + void *message_ptr, + int expected_items_count, /* -1 means no fixed limit */ + p18_parse_cb_t callback) +{ + int data_size = (int)strlen(data); + static const int buf_size = 64; + char buf[buf_size]; + char *c = (char *)data; + + for (int i = 0, char_index = 0, item_index = 0; + i < data_size; + i++) { + + if (*c != ',' && i < data_size-1) { + if (char_index < buf_size-1) + buf[char_index++] = *c; + c++; + continue; + } + + /* last character */ + if (*c != ',' && char_index < buf_size-1) + buf[char_index++] = *c; + + if (expected_items_count != -1 && item_index >= expected_items_count) + ERROR("warning: item %d is not expected\n", item_index); + else { + buf[char_index] = '\0'; + callback((const char *)buf, char_index, item_index, message_ptr); + } + + char_index = 0; + c++; + item_index++; + } +} + +static void p18_expect_listitem_length(const char *f, + int index, + size_t expected_len, + size_t actual_len) { + if (actual_len != expected_len) { + LOG("%s: length of item %d is %zu != %zu\n", + f, index, actual_len, expected_len); + } +} + +/* ------------------------------------------ */ +/* Command-specific methods */ + +P18_UNPACK_FN(protocol_id) +{ + char s[4]; + strncpy(s, data, 2); + s[2] = '\0'; + + p18_protocol_id_msg_t m; + m.id = atoi(s); + + return m; +} + +P18_UNPACK_FN(current_time) +{ + char s[4]; + int n; + p18_current_time_msg_t m; + + substr_copy(s, data, 4); + m.year = atoi(s); + + for (int i = 0; i < 5; i++) { + substr_copy(s, data + 4 + (i * 2), 2); + n = atoi(s); + switch (i) { + case 0: m.month = n; break; + case 1: m.day = n; break; + case 2: m.hour = n; break; + case 3: m.minute = n; break; + case 4: m.second = n; break; + default: + ERROR("%s: warning: unexpected code path, i=%d\n", + __func__, i); + break; + } + } + + return m; +} + +static long parse_generated(const char *buf) +{ + char s[16]; + strncpy(s, buf, 8); + s[8] = '\0'; + return atol(s); +} + +P18_UNPACK_FN(total_generated) +{ + p18_total_generated_msg_t m = { + .kwh = parse_generated(data) + }; + return m; +} + +P18_UNPACK_FN(year_generated) +{ + p18_year_generated_msg_t m = { + .kwh = parse_generated(data) + }; + return m; +} + +P18_UNPACK_FN(month_generated) +{ + p18_month_generated_msg_t m = { + .kwh = parse_generated(data) + }; + return m; +} + +P18_UNPACK_FN(day_generated) +{ + p18_day_generated_msg_t m = { + .kwh = parse_generated(data) + }; + return m; +} + +P18_UNPACK_FN(series_number) +{ + p18_series_number_msg_t m; + + /* read length, reuse the already available id datafer */ + substr_copy(m.id, data, 2); + m.length = atoi(m.id); + + /* read id of given length */ + substr_copy(m.id, data + 2, m.length); + + return m; +} + +P18_PARSE_CB_FN(cpu_version) +{ + //LOG("%s: value=%s, value_len=%d\n", __func__, value, value_len); + + p18_cpu_version_msg_t *m = (p18_cpu_version_msg_t *)message_ptr; + P18_EXPECT_LISTITEM_LENGTH(5); + + switch (index) { + case 0: + substr_copy(m->main_cpu_version, value, 5); + break; + + case 1: + substr_copy(m->slave1_cpu_version, value, 5); + break; + + case 2: + substr_copy(m->slave2_cpu_version, value, 5); + break; + } +} +P18_UNPACK_FN(cpu_version) +{ + P18_PARSE_LIST_AND_RETURN(cpu_version, 3); +} + +P18_PARSE_CB_FN(rated_information) +{ + p18_rated_information_msg_t *m = (p18_rated_information_msg_t *)message_ptr; + int num = atoi(value); + switch (index) { + case 0: /* AAAA */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_input_rating_voltage = num; + break; + case 1: /* BBB */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_input_rating_current = num; + break; + case 2: /* CCCC */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_rating_voltage = num; + break; + case 3: /* DDD */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_output_rating_freq = num; + break; + case 4: /* EEE */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_output_rating_current = num; + break; + case 5: /* FFFF */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_rating_apparent_power = num; + break; + case 6: /* GGGG */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_rating_active_power = num; + break; + case 7: /* HHH */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_rating_voltage = num; + break; + case 8: /* III */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_recharge_voltage = num; + break; + case 9: /* JJJ */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_redischarge_voltage = num; + break; + case 10: /* KKK */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_under_voltage = num; + break; + case 11: /* LLL */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_bulk_voltage = num; + break; + case 12: /* MMM */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_float_voltage = num; + break; + case 13: /* N */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->battery_type = num; + break; + case 14: /* OO */ + P18_EXPECT_LISTITEM_LENGTH(2); + m->max_ac_charging_current = num; + break; + case 15: /* PPP */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->max_charging_current = num; + break; + case 16: /* Q */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->input_voltage_range = num; + break; + case 17: /* R */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->output_source_priority = num; + break; + case 18: /* S */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->charger_source_priority = num; + break; + case 19: /* T */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->parallel_max_num = num; + break; + case 20: /* U */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->machine_type = num; + break; + case 21: /* V */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->topology = num; + break; + case 22: /* W */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->output_model_setting = num; + break; + case 23: /* Z */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->solar_power_priority = num; + break; + case 24: /* a */ + P18_EXPECT_LISTITEM_LENGTH(1); + substr_copy(m->mppt, value, 1); + break; + } +} +P18_UNPACK_FN(rated_information) +{ + P18_PARSE_LIST_AND_RETURN(rated_information, 25) +} + +P18_PARSE_CB_FN(general_status) +{ + p18_general_status_msg_t *m = (p18_general_status_msg_t *)message_ptr; + int num = atoi(value); + switch (index) { + case 0: /* AAAA */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->grid_voltage = num; + break; + case 1: /* BBB */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->grid_freq = num; + break; + case 2: /* CCCC */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_voltage = num; + break; + case 3: /* DDD */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_output_freq = num; + break; + case 4: /* EEEE */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_apparent_power = num; + break; + case 5: /* FFFF */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_active_power = num; + break; + case 6: /* GGG */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->output_load_percent = num; + break; + case 7: /* HHH */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_voltage = num; + break; + case 8: /* III */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_voltage_scc = num; + break; + case 9: /* JJJ */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_voltage_scc2 = num; + break; + case 10: /* KKK */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_discharge_current = num; + break; + case 11: /* LLL */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_charging_current = num; + break; + case 12: /* MMM */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_capacity = num; + break; + case 13: /* NNN */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->inverter_heat_sink_temp = num; + break; + case 14: /* OOO */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->mppt1_charger_temp = num; + break; + case 15: /* PPP */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->mppt2_charger_temp = num; + break; + case 16: /* QQQQ */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv1_input_power = num; + break; + case 17: /* RRRR */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv2_input_power = num; + break; + case 18: /* SSSS */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv1_input_voltage = num; + break; + case 19: /* TTTT */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv2_input_voltage = num; + break; + case 20: /* U */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->settings_values_changed = num == 1; + break; + case 21: /* V */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->mppt1_charger_status = num; + break; + case 22: /* W */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->mppt2_charger_status = num; + break; + case 23: /* X */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->load_connected = num == 1; + break; + case 24: /* Y */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->battery_power_direction = num; + break; + case 25: /* Z */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->dc_ac_power_direction = num; + break; + case 26: /* a */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->line_power_direction = num; + break; + case 27: /* b */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->local_parallel_id = num; + break; + } +} +P18_UNPACK_FN(general_status) +{ + P18_PARSE_LIST_AND_RETURN(general_status, 28) +} + +P18_UNPACK_FN(working_mode) +{ + char s[4]; + strncpy(s, data, 2); + s[2] = '\0'; + + p18_working_mode_msg_t m; + m.mode = atoi(s); + + return m; +} + +P18_PARSE_CB_FN(faults_warnings) +{ + p18_faults_warnings_msg_t *m = (p18_faults_warnings_msg_t *)message_ptr; + int num = atoi(value); + P18_EXPECT_LISTITEM_LENGTH(index == 0 ? 2 : 1); + switch (index) { + case 0: + m->fault_code = num; + break; + case 1: + m->line_fail = num > 0; + break; + case 2: + m->output_circuit_short = num > 0; + break; + case 3: + m->inverter_over_temperature = num > 0; + break; + case 4: + m->fan_lock = num > 0; + break; + case 5: + m->battery_voltage_high = num > 0; + break; + case 6: + m->battery_low = num > 0; + break; + case 7: + m->battery_under = num > 0; + break; + case 8: + m->over_load = num > 0; + break; + case 9: + m->eeprom_fail = num > 0; + break; + case 10: + m->power_limit = num > 0; + break; + case 11: + m->pv1_voltage_high = num > 0; + break; + case 12: + m->pv2_voltage_high = num > 0; + break; + case 13: + m->mppt1_overload_warning = num > 0; + break; + case 14: + m->mppt2_overload_warning = num > 0; + break; + case 15: + m->battery_too_low_to_charge_for_scc1 = num > 0; + break; + case 16: + m->battery_too_low_to_charge_for_scc2 = num > 0; + break; + } +} +P18_UNPACK_FN(faults_warnings) +{ + P18_PARSE_LIST_AND_RETURN(faults_warnings, 17) +} + +P18_PARSE_CB_FN(flags_statuses) +{ + p18_flags_statuses_msg_t *m = (p18_flags_statuses_msg_t *)message_ptr; + int num = atoi(value); + P18_EXPECT_LISTITEM_LENGTH(1); + switch (index) { + case 0: + m->buzzer = num > 0; + break; + case 1: + m->overload_bypass = num > 0; + break; + case 2: + m->lcd_escape_to_default_page_after_1min_timeout = num > 0; + break; + case 3: + m->overload_restart = num > 0; + break; + case 4: + m->over_temp_restart = num > 0; + break; + case 5: + m->backlight_on = num > 0; + break; + case 6: + m->alarm_on_primary_source_interrupt = num > 0; + break; + case 7: + m->fault_code_record = num > 0; + break; + } +} +P18_UNPACK_FN(flags_statuses) +{ + P18_PARSE_LIST_AND_RETURN(flags_statuses, 9) +} + +P18_PARSE_CB_FN(defaults) +{ + p18_defaults_msg_t *m = (p18_defaults_msg_t *)message_ptr; + int num = atoi(value); + switch (index) { + case 0: /* AAAA */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_voltage = num; + break; + case 1: /* BBB */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_output_freq = num; + break; + case 2: /* C */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->ac_input_voltage_range = num; + break; + case 3: /* DDD */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_under_voltage = num; + break; + case 4: /* EEE */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->charging_float_voltage = num; + break; + case 5: /* FFF */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->charging_bulk_voltage = num; + break; + case 6: /* GGG */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_recharge_voltage = num; + break; + case 7: /* HHH */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_redischarge_voltage = num; + break; + case 8: /* III */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->max_charging_current = num; + break; + case 9: /* JJ */ + P18_EXPECT_LISTITEM_LENGTH(2); + m->max_ac_charging_current = num; + break; + case 10: /* K */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->battery_type = num; + break; + case 11: /* L */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->output_source_priority = num; + break; + case 12: /* M */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->charger_source_priority = num; + break; + case 13: /* N */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->solar_power_priority = num; + break; + case 14: /* O */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->machine_type = num; + break; + case 15: /* P */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->output_model_setting = num; + break; + case 16: /* S */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_buzzer = num > 0; + break; + case 17: /* T */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_overload_restart = num > 0; + break; + case 18: /* U */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_over_temp_restart = num > 0; + break; + case 19: /* V */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_backlight_on = num > 0; + break; + case 20: /* W */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_alarm_on_primary_source_interrupt = num > 0; + break; + case 21: /* X */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_fault_code_record = num > 0; + break; + case 22: /* Y */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_overload_bypass = num > 0; + break; + case 23: /* Z */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->flag_lcd_escape_to_default_page_after_1min_timeout = num > 0; + break; + } +} +P18_UNPACK_FN(defaults) +{ + P18_PARSE_LIST_AND_RETURN(defaults, 24) +} + +P18_PARSE_CB_FN(max_charging_current_selectable_values) +{ + UNUSED(value_len); + p18_max_charging_current_selectable_values_msg_t *m = + (p18_max_charging_current_selectable_values_msg_t *)message_ptr; + if (m->len >= ARRAY_SIZE(m->amps)) + ERROR("warning: %dth item, ignoring as we accepting up to %zu values\n", + index, ARRAY_SIZE(m->amps)); + else + m->amps[m->len++] = atoi(value); +} +P18_UNPACK_FN(max_charging_current_selectable_values) +{ + P18_PARSE_LIST_AND_RETURN(max_charging_current_selectable_values, -1) +} + +P18_PARSE_CB_FN(max_ac_charging_current_selectable_values) +{ + UNUSED(value_len); + p18_max_ac_charging_current_selectable_values_msg_t *m = + (p18_max_ac_charging_current_selectable_values_msg_t *)message_ptr; + if (m->len >= ARRAY_SIZE(m->amps)) + ERROR("warning: %dth item, ignoring as we accepting up to %zu values\n", + index, ARRAY_SIZE(m->amps)); + else + m->amps[m->len++] = atoi(value); +} +P18_UNPACK_FN(max_ac_charging_current_selectable_values) +{ + P18_PARSE_LIST_AND_RETURN(max_ac_charging_current_selectable_values, -1) +} + +P18_PARSE_CB_FN(parallel_rated_information) +{ + p18_parallel_rated_information_msg_t *m = (p18_parallel_rated_information_msg_t *)message_ptr; + switch (index) { + case 0: /* A */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->parallel_id_connection_status = atoi(value); + break; + case 1: /* BB */ + P18_EXPECT_LISTITEM_LENGTH(2); + m->serial_number_valid_length = atoi(value); + break; + case 2: /* CCCCCCCCCCCCCCCCCCCC */ + P18_EXPECT_LISTITEM_LENGTH(20); + substr_copy(m->serial_number, value, MIN(m->serial_number_valid_length, (int)ARRAY_SIZE(m->serial_number)-1)); + break; + case 3: /* D */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->charger_source_priority = atoi(value); + break; + case 4: /* EEE */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->max_charging_current = atoi(value); + break; + case 5: /* FF */ + P18_EXPECT_LISTITEM_LENGTH(2); + m->max_ac_charging_current = atoi(value); + break; + case 6: /* G */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->output_model_setting = atoi(value); + break; + } +} +P18_UNPACK_FN(parallel_rated_information) +{ + P18_PARSE_LIST_AND_RETURN(parallel_rated_information, 7) +} + +P18_PARSE_CB_FN(parallel_general_status) +{ + p18_parallel_general_status_msg_t *m = (p18_parallel_general_status_msg_t *)message_ptr; + int num = atoi(value); + switch (index) { + case 0: /* A */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->parallel_id_connection_status = num; + break; + case 1: /* B */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->work_mode = num; + break; + case 2: /* CC */ + P18_EXPECT_LISTITEM_LENGTH(2); + m->fault_code = num; + break; + case 3: /* DDDD */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->grid_voltage = num; + break; + case 4: /* EEE */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->grid_freq = num; + break; + case 5: /* FFFF */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_voltage = num; + break; + case 6: /* GGG */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->ac_output_freq = num; + break; + case 7: /* HHHH */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_apparent_power = num; + break; + case 8: /* IIII */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->ac_output_active_power = num; + break; + case 9: /* JJJJJ */ + P18_EXPECT_LISTITEM_LENGTH(5); + m->total_ac_output_apparent_power = num; + break; + case 10: /* KKKKK */ + P18_EXPECT_LISTITEM_LENGTH(5); + m->total_ac_output_active_power = num; + break; + case 11: /* LLL */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->output_load_percent = num; + break; + case 12: /* MMM */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->total_output_load_percent = num; + break; + case 13: /* NNN */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_voltage = num; + break; + case 14: /* OOO */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_discharge_current = num; + break; + case 15: /* PPP */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_charging_current = num; + break; + case 16: /* QQQ */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->total_battery_charging_current = num; + break; + case 17: /* MMM. It's not my mistake, it's as per the doc. */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->battery_capacity = num; + break; + case 18: /* RRRR */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv1_input_power = num; + break; + case 19: /* SSSS */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv2_input_power = num; + break; + case 20: /* TTTT */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv1_input_voltage = num == 1; + break; + case 21: /* UUUU */ + P18_EXPECT_LISTITEM_LENGTH(4); + m->pv2_input_voltage = num; + break; + case 22: /* V */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->mppt1_charger_status = num; + break; + case 23: /* W */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->mppt2_charger_status = num; + break; + case 24: /* X */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->load_connected = num == 1; + break; + case 25: /* Y */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->battery_power_direction = num; + break; + case 26: /* Z */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->dc_ac_power_direction = num; + break; + case 27: /* a */ + P18_EXPECT_LISTITEM_LENGTH(1); + m->line_power_direction = num; + break; + case 28: /* bbb. It's marked in red in the doc, idk what it means. */ + /* My guess is that this elem is not always present. */ + P18_EXPECT_LISTITEM_LENGTH(3); + m->max_temp = num; + break; + } +} +P18_UNPACK_FN(parallel_general_status) +{ + P18_PARSE_LIST_AND_RETURN(parallel_general_status, 29) +} + +P18_PARSE_CB_FN(ac_charge_time_bucket) +{ + p18_ac_charge_time_bucket_msg_t *m = (p18_ac_charge_time_bucket_msg_t *)message_ptr; + char buf[4]; + switch (index) { + case 0: /* AAAA */ + P18_EXPECT_LISTITEM_LENGTH(4); + + substr_copy(buf, value, 2); + m->start_h = atoi(buf); + + substr_copy(buf, value+2, 2); + m->start_m = atoi(buf); + + break; + + case 1: /* BBBB */ + P18_EXPECT_LISTITEM_LENGTH(4); + + substr_copy(buf, value, 2); + m->end_h = atoi(buf); + + substr_copy(buf, value+2, 2); + m->end_m = atoi(buf); + + break; + } +} +P18_UNPACK_FN(ac_charge_time_bucket) +{ + P18_PARSE_LIST_AND_RETURN(ac_charge_time_bucket, 2) +} + +P18_PARSE_CB_FN(ac_supply_load_time_bucket) +{ + p18_ac_supply_load_time_bucket_msg_t *m = (p18_ac_supply_load_time_bucket_msg_t *)message_ptr; + char buf[4]; + switch (index) { + case 0: /* AAAA */ + P18_EXPECT_LISTITEM_LENGTH(4); + + substr_copy(buf, value, 2); + m->start_h = atoi(buf); + + substr_copy(buf, value+2, 2); + m->start_m = atoi(buf); + + break; + + case 1: /* BBBB */ + P18_EXPECT_LISTITEM_LENGTH(4); + + substr_copy(buf, value, 2); + m->end_h = atoi(buf); + + substr_copy(buf, value+2, 2); + m->end_m = atoi(buf); + + break; + } +} +P18_UNPACK_FN(ac_supply_load_time_bucket) +{ + P18_PARSE_LIST_AND_RETURN(ac_supply_load_time_bucket, 2) +} + +/* ------------------------------------------ */ +/* Label getters */ + +const char *p18_battery_type_label(p18_battery_type_t type) +{ + switch (type) { + case P18_BT_AGM: + case P18_BT_USER: + case P18_BT_FLOODED: + return p18_battery_types[type]; + + default: + return p18_unknown; + } +} + +const char *p18_input_voltage_range_label(p18_input_voltage_range_t range) +{ + switch (range) { + case P18_IVR_APPLIANCE: + case P18_IVR_UPS: + return p18_voltage_ranges[range]; + + default: + return p18_unknown; + } +} + +const char *p18_output_source_priority_label(p18_output_source_priority_t priority) +{ + switch (priority) { + case P18_OSP_SOLAR_BATTERY_UTILITY: + case P18_OSP_SOLAR_UTILITY_BATTERY: + return p18_output_source_priorities[priority]; + + default: + return p18_unknown; + } +} + +const char *p18_charge_source_priority_label(p18_charger_source_priority_t priority) +{ + switch (priority) { + case P18_CSP_SOLAR_FIRST: + case P18_CSP_SOLAR_AND_UTILITY: + case P18_CSP_SOLAR_ONLY: + return p18_charger_source_priorities[priority]; + + default: + return p18_unknown; + } +} + +const char *p18_machine_type_label(p18_machine_type_t type) +{ + switch (type) { + case P18_MT_GRID_TIE: + case P18_MT_OFF_GRID_TIE: + return p18_machine_types[type]; + + default: + return p18_unknown; + } +} + +const char *p18_topology_label(p18_topology_t topology) +{ + switch (topology) { + case P18_TRANSFORMER: + case P18_TRANSFORMERLESS: + return p18_topologies[topology]; + + default: + return p18_unknown; + } +} + +const char *p18_output_model_setting_label(p18_output_model_setting_t setting) +{ + switch (setting) { + case P18_OMS_PARALLEL_OUTPUT: + case P18_OMS_PHASE1_OF_3PHASE_OUTPUT: + case P18_OMS_PHASE2_OF_3PHASE_OUTPUT: + case P18_OMS_PHASE3_OF_3PHASE_OUTPUT: + case P18_OMS_SINGLE_MODULE: + return p18_output_model_settings[setting]; + + default: + return p18_unknown; + } +} + +const char *p18_solar_power_priority_label(p18_solar_power_priority_t priority) +{ + switch (priority) { + case P18_SPP_BATTERY_LOAD_UTILITY: + case P18_SPP_LOAD_BATTERY_UTILITY: + return p18_solar_power_priorities[priority]; + + default: + return p18_unknown; + } +} + +const char *p18_mppt_charger_status_label(p18_mppt_charger_status_t status) +{ + switch (status) { + case P18_MPPT_CS_ABNORMAL: + case P18_MPPT_CS_CHARGED: + case P18_MPPT_CS_NOT_CHARGED: + return p18_mppt_charger_statuses[status]; + + default: + return p18_unknown; + } +} + +const char *p18_battery_power_direction_label(p18_battery_power_direction_t direction) +{ + switch (direction) { + case P18_BPD_CHARGE: + case P18_BPD_DISCHARGE: + case P18_BPD_DONOTHING: + return p18_battery_power_directions[direction]; + + default: + return p18_unknown; + } +} + +const char *p18_dc_ac_power_direction_label(p18_dc_ac_power_direction_t direction) +{ + switch (direction) { + case P18_DAPD_AC_DC: + case P18_DAPD_DC_AC: + case P18_DAPD_DONOTHING: + return p18_dc_ac_power_directions[direction]; + + default: + return p18_unknown; + } +} + +const char *p18_line_power_direction_label(p18_line_power_direction_t direction) +{ + switch (direction) { + case P18_LPD_DONOTHING: + case P18_LPD_INPUT: + case P18_LPD_OUTPUT: + return p18_line_power_directions[direction]; + + default: + return p18_unknown; + } +} + +const char *p18_working_mode_label(p18_working_mode_t mode) +{ + switch (mode) { + case P18_WM_BATTERY_MODE: + case P18_WM_BYPASS_MODE: + case P18_WM_FAULT_MODE: + case P18_WM_HYBRID_MODE: + case P18_WM_POWER_ON_MODE: + case P18_WM_STANDBY_MODE: + return p18_working_modes[mode]; + + default: + return p18_unknown; + } +} + +const char *p18_fault_code_label(unsigned int code) +{ + int len = (int)ARRAY_SIZE(fault_codes); + for (int i = 0; i < len; i++) { + const p18_fault_code_list_item_t *fc = &fault_codes[i]; + if (fc->id == code) + return fc->message; + } + return p18_unknown; +} + +const char *p18_parallel_connection_status_label(p18_parallel_id_connection_status_t status) +{ + switch (status) { + case P18_PCS_NON_EXISTENT: + case P18_PCS_EXISTENT: + return p18_parallel_connection_statuses[status]; + + default: + return p18_unknown; + } +} \ No newline at end of file diff --git a/p18.h b/p18.h new file mode 100644 index 0000000..604dddc --- /dev/null +++ b/p18.h @@ -0,0 +1,505 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#ifndef ISV_P18_H +#define ISV_P18_H + +#include +#include +#include "util.h" + +#define P18_QUERY_CMDS_ENUM_OFFSET 1000 +#define P18_SET_CMDS_ENUM_OFFSET 1100 + +#define MK_P18_UNPACK_FN_NAME(msg_type) p18_unpack_ ## msg_type ## _msg +#define MK_P18_PARSE_CB_FN_NAME(msg_type) p18_parse_cb_ ## msg_type ## _msg +#define MK_P18_MSG_T(msg_type) p18_ ## msg_type ## _msg_t + +#define P18_MSG_T(msg_type) MK_P18_MSG_T(msg_type) +#define P18_UNPACK_FN_NAME(msg_type) MK_P18_UNPACK_FN_NAME(msg_type) +#define P18_PARSE_CB_FN_NAME(msg_type) MK_P18_PARSE_CB_FN_NAME(msg_type) + +#define P18_UNPACK_FN(msg_type) \ + P18_MSG_T(msg_type) P18_UNPACK_FN_NAME(msg_type)(const char *data) + +#define P18_PARSE_CB_FN(msg_type) \ + static void P18_PARSE_CB_FN_NAME(msg_type)(const char *value, \ + size_t value_len, \ + unsigned int index, \ + void *message_ptr) + +#define P18_PARSE_LIST_AND_RETURN(msg_type, exp_items_count) \ + P18_MSG_T(msg_type) m = {0}; \ + p18_parse_list(data, &m, (exp_items_count), P18_PARSE_CB_FN_NAME(msg_type)); \ + return m; + +#define P18_EXPECT_LISTITEM_LENGTH(len) \ + p18_expect_listitem_length((const char *)__func__, index, (len), value_len) + +typedef void (*p18_parse_cb_t)(const char *, size_t, unsigned int, void *); + +/* ------------------------------------------ */ +/* Commands list */ + +enum p18_query_command { + P18_QUERY_PROTOCOL_ID = P18_QUERY_CMDS_ENUM_OFFSET, + P18_QUERY_CURRENT_TIME, + P18_QUERY_TOTAL_GENERATED, + P18_QUERY_YEAR_GENERATED, + P18_QUERY_MONTH_GENERATED, + P18_QUERY_DAY_GENERATED, + P18_QUERY_SERIES_NUMBER, + P18_QUERY_CPU_VERSION, + P18_QUERY_RATED_INFORMATION, + P18_QUERY_GENERAL_STATUS, + P18_QUERY_WORKING_MODE, + P18_QUERY_FAULTS_WARNINGS, + P18_QUERY_FLAGS_STATUSES, + P18_QUERY_DEFAULTS, + P18_QUERY_MAX_CHARGING_CURRENT_SELECTABLE_VALUES, + P18_QUERY_MAX_AC_CHARGING_CURRENT_SELECTABLE_VALUES, + P18_QUERY_PARALLEL_RATED_INFORMATION, + P18_QUERY_PARALLEL_GENERAL_STATUS, + P18_QUERY_AC_CHARGE_TIME_BUCKET, + P18_QUERY_AC_SUPPLY_LOAD_TIME_BUCKET, +}; + +enum p18_set_command { + P18_SET_LOADS = P18_SET_CMDS_ENUM_OFFSET, + P18_SET_FLAG, + P18_SET_DEFAULTS, + P18_SET_BAT_MAX_CHARGE_CURRENT, + P18_SET_BAT_MAX_AC_CHARGE_CURRENT, + P18_SET_AC_OUTPUT_FREQ, + P18_SET_BAT_MAX_CHARGE_VOLTAGE, + P18_SET_AC_OUTPUT_RATED_VOLTAGE, + P18_SET_OUTPUT_SOURCE_PRIORITY, + P18_SET_BAT_CHARGING_THRESHOLDS_WHEN_UTILITY_AVAIL, /* Battery re-charging and re-discharing voltage when utility is available */ + P18_SET_CHARGING_SOURCE_PRIORITY, + P18_SET_SOLAR_POWER_PRIORITY, + P18_SET_AC_INPUT_VOLTAGE_RANGE, + P18_SET_BAT_TYPE, + P18_SET_OUTPUT_MODEL, + P18_SET_BAT_CUTOFF_VOLTAGE, + P18_SET_SOLAR_CONFIG, + P18_SET_CLEAR_GENERATED, + P18_SET_DATE_TIME, + P18_SET_AC_CHARGE_TIME_BUCKET, + P18_SET_AC_SUPPLY_LOAD_TIME_BUCKET, +}; + +typedef enum { + P18_BT_AGM = 0, + P18_BT_FLOODED = 1, + P18_BT_USER = 2, +} p18_battery_type_t; + +typedef enum { + P18_IVR_APPLIANCE = 0, + P18_IVR_UPS = 1, +} p18_input_voltage_range_t; + +typedef enum { + P18_OSP_SOLAR_UTILITY_BATTERY = 0, + P18_OSP_SOLAR_BATTERY_UTILITY = 1, +} p18_output_source_priority_t; + +typedef enum { + P18_CSP_SOLAR_FIRST = 0, + P18_CSP_SOLAR_AND_UTILITY = 1, + P18_CSP_SOLAR_ONLY = 2, +} p18_charger_source_priority_t; + +typedef enum { + P18_MT_OFF_GRID_TIE = 0, + P18_MT_GRID_TIE = 1, +} p18_machine_type_t; + +typedef enum { + P18_TRANSFORMERLESS = 0, + P18_TRANSFORMER = 1, +} p18_topology_t; + +typedef enum { + P18_OMS_SINGLE_MODULE = 0, + P18_OMS_PARALLEL_OUTPUT = 1, + P18_OMS_PHASE1_OF_3PHASE_OUTPUT = 2, + P18_OMS_PHASE2_OF_3PHASE_OUTPUT = 3, + P18_OMS_PHASE3_OF_3PHASE_OUTPUT = 4, +} p18_output_model_setting_t; + +typedef enum { + P18_SPP_BATTERY_LOAD_UTILITY = 0, + P18_SPP_LOAD_BATTERY_UTILITY = 1, +} p18_solar_power_priority_t; + +typedef enum { + P18_MPPT_CS_ABNORMAL = 0, + P18_MPPT_CS_NOT_CHARGED = 1, + P18_MPPT_CS_CHARGED = 2, +} p18_mppt_charger_status_t; + +typedef enum { + P18_BPD_DONOTHING = 0, + P18_BPD_CHARGE = 1, + P18_BPD_DISCHARGE = 2, +} p18_battery_power_direction_t; + +typedef enum { + P18_DAPD_DONOTHING = 0, + P18_DAPD_AC_DC = 1, + P18_DAPD_DC_AC = 2, +} p18_dc_ac_power_direction_t; + +typedef enum { + P18_LPD_DONOTHING = 0, + P18_LPD_INPUT = 1, + P18_LPD_OUTPUT = 2, +} p18_line_power_direction_t; + +typedef enum { + P18_WM_POWER_ON_MODE = 0, + P18_WM_STANDBY_MODE = 1, + P18_WM_BYPASS_MODE = 2, + P18_WM_BATTERY_MODE = 3, + P18_WM_FAULT_MODE = 4, + P18_WM_HYBRID_MODE = 5, +} p18_working_mode_t; + +typedef enum { + P18_PCS_NON_EXISTENT = 0, + P18_PCS_EXISTENT = 1, +} p18_parallel_id_connection_status_t; + + +/* ------------------------------------------ */ +/* Message structs */ + +typedef struct { + unsigned int id; +} p18_protocol_id_msg_t; + +typedef struct { + unsigned int year; + unsigned short month; + unsigned short day; + unsigned short hour; + unsigned short minute; + unsigned short second; +} p18_current_time_msg_t; + +typedef struct { + unsigned long kwh; +} p18_total_generated_msg_t; + +typedef struct { + unsigned long kwh; +} p18_year_generated_msg_t; + +typedef struct { + unsigned long kwh; +} p18_month_generated_msg_t; + +typedef struct { + unsigned long kwh; +} p18_day_generated_msg_t; + +typedef struct { + short length; + char id[32]; +} p18_series_number_msg_t; + +typedef struct { + char main_cpu_version[6]; + char slave1_cpu_version[6]; + char slave2_cpu_version[6]; +} p18_cpu_version_msg_t; + +typedef struct { + unsigned int ac_input_rating_voltage; /* unit: 0.1V */ + unsigned int ac_input_rating_current; /* unit: 0.1A */ + unsigned int ac_output_rating_voltage; /* unit: 0.1A */ + unsigned int ac_output_rating_freq; /* unit: 0.1Hz */ + unsigned int ac_output_rating_current; /* unit: 0.1A */ + unsigned int ac_output_rating_apparent_power; /* unit: VA */ + unsigned int ac_output_rating_active_power; /* unit: W */ + unsigned int battery_rating_voltage; /* unit: 0.1V */ + unsigned int battery_recharge_voltage; /* unit: 0.1V */ + unsigned int battery_redischarge_voltage; /* unit: 0.1V */ + unsigned int battery_under_voltage; /* unit: 0.1V */ + unsigned int battery_bulk_voltage; /* unit: 0.1V */ + unsigned int battery_float_voltage; /* unit: 0.1V */ + p18_battery_type_t battery_type; + unsigned int max_ac_charging_current; /* unit: A */ + unsigned int max_charging_current; /* unit: A */ + p18_input_voltage_range_t input_voltage_range; + p18_output_source_priority_t output_source_priority; + p18_charger_source_priority_t charger_source_priority; + unsigned int parallel_max_num; + p18_machine_type_t machine_type; + p18_topology_t topology; + p18_output_model_setting_t output_model_setting; + p18_solar_power_priority_t solar_power_priority; + char mppt[4]; +} p18_rated_information_msg_t; + +typedef struct { + unsigned int grid_voltage; /* unit: 0.1V */ + unsigned int grid_freq; /* unit: 0.1Hz */ + unsigned int ac_output_voltage; /* unit: 0.1V */ + unsigned int ac_output_freq; /* unit: 0.1Hz */ + unsigned int ac_output_apparent_power; /* unit: VA */ + unsigned int ac_output_active_power; /* unit: W */ + unsigned int output_load_percent; /* unit: % */ + unsigned int battery_voltage; /* unit: 0.1V */ + unsigned int battery_voltage_scc; /* unit: 0.1V */ + unsigned int battery_voltage_scc2; /* unit: 0.1V */ + unsigned int battery_discharge_current; /* unit: A */ + unsigned int battery_charging_current; /* unit: A */ + unsigned int battery_capacity; /* unit: % */ + unsigned int inverter_heat_sink_temp; /* unit: C */ + unsigned int mppt1_charger_temp; /* unit: C */ + unsigned int mppt2_charger_temp; /* unit: C */ + unsigned int pv1_input_power; /* unit: W */ + unsigned int pv2_input_power; /* unit: W */ + unsigned int pv1_input_voltage; /* unit: 0.1V */ + unsigned int pv2_input_voltage; /* unit: 0.1V */ + bool settings_values_changed; /* inverter returns: + 0: nothing changed + 1: something changed */ + p18_mppt_charger_status_t mppt1_charger_status; + p18_mppt_charger_status_t mppt2_charger_status; + bool load_connected; /* inverter returns: + 0: disconnected + 1: connected */ + p18_battery_power_direction_t battery_power_direction; + p18_dc_ac_power_direction_t dc_ac_power_direction; + p18_line_power_direction_t line_power_direction; + unsigned int local_parallel_id; /* 0 .. (parallel number - 1) */ +} p18_general_status_msg_t; + +typedef struct { + p18_working_mode_t mode; +} p18_working_mode_msg_t; + +typedef struct { + unsigned int fault_code; + bool line_fail; + bool output_circuit_short; + bool inverter_over_temperature; + bool fan_lock; + bool battery_voltage_high; + bool battery_low; + bool battery_under; + bool over_load; + bool eeprom_fail; + bool power_limit; + bool pv1_voltage_high; + bool pv2_voltage_high; + bool mppt1_overload_warning; + bool mppt2_overload_warning; + bool battery_too_low_to_charge_for_scc1; + bool battery_too_low_to_charge_for_scc2; +} p18_faults_warnings_msg_t; + +typedef struct { + bool buzzer; + bool overload_bypass; + bool lcd_escape_to_default_page_after_1min_timeout; + bool overload_restart; + bool over_temp_restart; + bool backlight_on; + bool alarm_on_primary_source_interrupt; + bool fault_code_record; + char reserved; +} p18_flags_statuses_msg_t; + +typedef struct { + unsigned int ac_output_voltage; /* unit: 0.1V */ + unsigned int ac_output_freq; + p18_input_voltage_range_t ac_input_voltage_range; + unsigned int battery_under_voltage; + unsigned int charging_float_voltage; + unsigned int charging_bulk_voltage; + unsigned int battery_recharge_voltage; + unsigned int battery_redischarge_voltage; + unsigned int max_charging_current; + unsigned int max_ac_charging_current; + p18_battery_type_t battery_type; + p18_output_source_priority_t output_source_priority; + p18_charger_source_priority_t charger_source_priority; + p18_solar_power_priority_t solar_power_priority; + p18_machine_type_t machine_type; + p18_output_model_setting_t output_model_setting; + bool flag_buzzer; + bool flag_overload_restart; + bool flag_over_temp_restart; + bool flag_backlight_on; + bool flag_alarm_on_primary_source_interrupt; + bool flag_fault_code_record; + bool flag_overload_bypass; + bool flag_lcd_escape_to_default_page_after_1min_timeout; +} p18_defaults_msg_t; + +typedef struct { + size_t len; + int amps[32]; +} p18_max_charging_current_selectable_values_msg_t; + +typedef struct { + size_t len; + int amps[32]; +} p18_max_ac_charging_current_selectable_values_msg_t; + +typedef struct { + p18_parallel_id_connection_status_t parallel_id_connection_status; + int serial_number_valid_length; + char serial_number[32]; + p18_charger_source_priority_t charger_source_priority; + unsigned int max_ac_charging_current; /* unit: A */ + unsigned int max_charging_current; /* unit: A */ + p18_output_model_setting_t output_model_setting; +} p18_parallel_rated_information_msg_t; + +typedef struct { + p18_parallel_id_connection_status_t parallel_id_connection_status; + p18_working_mode_t work_mode; + unsigned int fault_code; + unsigned int grid_voltage; /* unit: 0.1V */ + unsigned int grid_freq; /* unit: 0.1Hz */ + unsigned int ac_output_voltage; /* unit: 0.1V */ + unsigned int ac_output_freq; /* unit: 0.1Hz */ + unsigned int ac_output_apparent_power; /* unit: VA */ + unsigned int ac_output_active_power; /* unit: W */ + unsigned int total_ac_output_apparent_power; /* unit: VA */ + unsigned int total_ac_output_active_power; /* unit: W */ + unsigned int output_load_percent; /* unit: % */ + unsigned int total_output_load_percent; /* unit: % */ + unsigned int battery_voltage; /* unit: 0.1V */ + unsigned int battery_discharge_current; /* unit: A */ + unsigned int battery_charging_current; /* unit: A */ + unsigned int total_battery_charging_current; /* unit: A */ + unsigned int battery_capacity; /* unit: % */ + unsigned int pv1_input_power; /* unit: W */ + unsigned int pv2_input_power; /* unit: W */ + unsigned int pv1_input_voltage; /* unit: 0.1V */ + unsigned int pv2_input_voltage; /* unit: 0.1V */ + p18_mppt_charger_status_t mppt1_charger_status; + p18_mppt_charger_status_t mppt2_charger_status; + bool load_connected; /* inverter returns: + 0: disconnected + 1: connected */ + p18_battery_power_direction_t battery_power_direction; + p18_dc_ac_power_direction_t dc_ac_power_direction; + p18_line_power_direction_t line_power_direction; + unsigned int max_temp; /* unit: C */ +} p18_parallel_general_status_msg_t; + +typedef struct { + unsigned short start_h; + unsigned short start_m; + unsigned short end_h; + unsigned short end_m; +} p18_ac_charge_time_bucket_msg_t; + +typedef struct { + unsigned short start_h; + unsigned short start_m; + unsigned short end_h; + unsigned short end_m; +} p18_ac_supply_load_time_bucket_msg_t; + + +/* ------------------------------------------ */ +/* Some constants and allowed values */ + +typedef struct { + unsigned int id; + char message[64]; +} p18_fault_code_list_item_t; + +typedef struct { + char *key; + char *p18_key; + char *title; +} p18_flag_printable_list_item_t; +extern const p18_flag_printable_list_item_t p18_flags_printable_list[9]; + +extern const int p18_ac_output_rated_voltages[5]; + +extern const char *p18_battery_util_recharging_voltages_12v_unit[8]; +extern const char *p18_battery_util_recharging_voltages_24v_unit[8]; +extern const char *p18_battery_util_recharging_voltages_48v_unit[8]; + +extern const char *p18_battery_util_redischarging_voltages_12v_unit[12]; +extern const char *p18_battery_util_redischarging_voltages_24v_unit[12]; +extern const char *p18_battery_util_redischarging_voltages_48v_unit[12]; + + +/* ------------------------------------------ */ +/* Common methods */ + +bool p18_build_command(int command, const char **args, size_t args_size, char *buf); +bool p18_validate_query_response(const char *buf, size_t size, size_t *data_size); +bool p18_set_result(const char *buf, size_t size); + +/* ------------------------------------------ */ +/* Command-specific methods */ + +P18_UNPACK_FN(protocol_id); +P18_UNPACK_FN(current_time); +P18_UNPACK_FN(total_generated); +P18_UNPACK_FN(year_generated); +P18_UNPACK_FN(month_generated); +P18_UNPACK_FN(day_generated); +P18_UNPACK_FN(series_number); +P18_UNPACK_FN(cpu_version); +P18_UNPACK_FN(rated_information); +P18_UNPACK_FN(general_status); +P18_UNPACK_FN(working_mode); +P18_UNPACK_FN(faults_warnings); +P18_UNPACK_FN(flags_statuses); +P18_UNPACK_FN(defaults); +P18_UNPACK_FN(max_charging_current_selectable_values); +P18_UNPACK_FN(max_ac_charging_current_selectable_values); +P18_UNPACK_FN(parallel_rated_information); +P18_UNPACK_FN(parallel_general_status); +P18_UNPACK_FN(ac_charge_time_bucket); +P18_UNPACK_FN(ac_supply_load_time_bucket); + + +/* ------------------------------------------ */ +/* Label getters */ + +const char *p18_battery_type_label(p18_battery_type_t type); +const char *p18_input_voltage_range_label(p18_input_voltage_range_t range); +const char *p18_output_source_priority_label(p18_output_source_priority_t priority); +const char *p18_charge_source_priority_label(p18_charger_source_priority_t priority); +const char *p18_machine_type_label(p18_machine_type_t type); +const char *p18_topology_label(p18_topology_t topology); +const char *p18_output_model_setting_label(p18_output_model_setting_t setting); +const char *p18_solar_power_priority_label(p18_solar_power_priority_t priority); +const char *p18_mppt_charger_status_label(p18_mppt_charger_status_t status); +const char *p18_battery_power_direction_label(p18_battery_power_direction_t direction); +const char *p18_dc_ac_power_direction_label(p18_dc_ac_power_direction_t direction); +const char *p18_line_power_direction_label(p18_line_power_direction_t direction); +const char *p18_working_mode_label(p18_working_mode_t mode); +const char *p18_fault_code_label(unsigned int code); +const char *p18_parallel_connection_status_label(p18_parallel_id_connection_status_t status); + +#endif //ISV_P18_H \ No newline at end of file diff --git a/print.c b/print.c new file mode 100644 index 0000000..3ab61bf --- /dev/null +++ b/print.c @@ -0,0 +1,1089 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#include +#include "print.h" +#include "util.h" + +#define PRINT_AUTO(items) \ + if (print_is_table_format(format)) \ + print_table((items), ARRAY_SIZE(items), format == PRINT_FORMAT_PARSABLE_TABLE); \ + else \ + print_json((items), ARRAY_SIZE(items), format == PRINT_FORMAT_JSON_W_UNITS); + +const short default_precision = 2; +const char *units[] = { + " V", + " A", + " Wh", + " kWh", + " VA", + " Hz", + "%", + " °C", +}; +const char *unknown_unit = " ?"; +const char *yes = "Yes"; +const char *no = "No"; +const char *true_s = "true"; +const char *false_s = "false"; +const char *enabled = "Enabled"; +const char *disabled = "Disabled"; + +static const char* print_unit_label(print_unit_t unit) +{ + switch (unit) { + case PRINT_UNIT_A: + case PRINT_UNIT_V: + case PRINT_UNIT_KWH: + case PRINT_UNIT_WH: + case PRINT_UNIT_VA: + case PRINT_UNIT_HZ: + case PRINT_UNIT_CELSIUS: + case PRINT_UNIT_PERCENTAGE: + return units[unit-1]; + + default: + return unknown_unit; + } +} + +static void print_table(print_item_t *items, size_t size, bool parsable) +{ + print_item_t item; + char fmt[32], doublefmt[16]; + char k[64], v[64]; + + if (!parsable) { + size_t len, max_title_len = 0; + for (size_t i = 0; i < size; i++) { + item = items[i]; + len = strlen(item.title) + 1 /* for colon */; + if (len > max_title_len) + max_title_len = len; + } + sprintf(fmt, "%%-%zus %%s", max_title_len); + } else { + sprintf(fmt, "%%s %%s"); + } + + const char *unit; + for (size_t i = 0; i < size; i++) { + item = items[i]; + + strcpy(k, parsable ? item.key : item.title); + if (!parsable) + strcat(k, ":"); + + if (variant_is_double(item.value)) { + sprintf(doublefmt, "%%2.%dlf", + item.precision ? item.precision : default_precision); + snprintf(v, 32, doublefmt, item.value.d); + } else if (variant_is_long(item.value)) + snprintf(v, 32, "%ld", item.value.l); + else if (variant_is_string(item.value)) { + char *pos = strchr(item.value.s, ' '); + if (parsable && pos != NULL && pos != item.value.s) + snprintf(v, 32, "\"%s\"", item.value.s); + else + snprintf(v, 32, "%s", item.value.s); + } else if (variant_is_bool(item.value)) + snprintf(v, 32, "%s", item.value.b ? yes : no); + else if (variant_is_flag(item.value)) + snprintf(v, 32, "%s", item.value.b ? enabled : disabled); + + printf(fmt, k, v); + if (item.unit) { + unit = print_unit_label(item.unit); + if (parsable && *unit != ' ') + putchar(' '); + printf("%s", print_unit_label(item.unit)); + } + + putchar('\n'); + } +} + +void print_json(print_item_t *items, size_t size, bool with_units) +{ + print_item_t item; + putchar('{'); + for (size_t i = 0; i < size; i++) { + item = items[i]; + printf("\"%s\":", item.key); + + if (item.unit && with_units) + putchar('['); + + if (variant_is_string(item.value)) + printf("\"%s\"", item.value.s); + else if (variant_is_double(item.value)) + printf("%2.2lf", item.value.d); + else if (variant_is_long(item.value)) + printf("%ld", item.value.l); + else if (variant_is_bool(item.value) || variant_is_flag(item.value)) + printf("%s", item.value.b ? true_s : false_s); + + if (item.unit && with_units) { + const char *unit_label = print_unit_label(item.unit); + if (unit_label[0] == ' ') + unit_label++; + printf(",\"%s\"]", unit_label); + } + + if (i < size-1) + putchar(','); + } + putchar('}'); + putchar('\n'); +} + +bool print_is_json_format(print_format_t f) +{ + return f == PRINT_FORMAT_JSON_W_UNITS || f == PRINT_FORMAT_JSON; +} + +static bool print_is_table_format(print_format_t f) +{ + return f == PRINT_FORMAT_TABLE || f == PRINT_FORMAT_PARSABLE_TABLE; +} + +void print_set_result(bool success, print_format_t format) { + if (print_is_table_format(format)) + printf("%s\n", success ? "OK" : "Failure"); + else { + print_item_t items[] = { + { + .key = (success ? "ok" : "error"), + .value = (success ? variant_long(1) : variant_string("failure")) + } + }; + print_json(items, ARRAY_SIZE(items), false); + } +} + +static void print_table_list(const int *items, size_t size) +{ + for (size_t i = 0; i < size; i++) + printf("%d\n", items[i]); +} + +static void print_json_list(const int *items, size_t size) +{ + putchar('['); + for (size_t i = 0; i < size; i++) { + printf("%d", items[i]); + if (i < size-1) + putchar(','); + } + putchar(']'); + putchar('\n'); +} + + +/* ------------------------------------------ */ + +PRINT_FN(protocol_id) +{ + print_item_t items[] = { + {.key= "id", .title= "Protocol ID", .value= variant_long(m->id)} + }; + PRINT_AUTO(items) +} + +PRINT_FN(current_time) +{ + print_item_t items[] = { + {.key= "year", .title= "Year", .value= variant_long(m->year) }, + {.key= "month", .title= "Month", .value= variant_long(m->month) }, + {.key= "day", .title= "Day", .value= variant_long(m->day) }, + {.key= "hour", .title= "Hour", .value= variant_long(m->hour) }, + {.key= "minute", .title= "Minute", .value= variant_long(m->minute)}, + {.key= "second", .title= "Second", .value= variant_long(m->second)}, + }; + PRINT_AUTO(items) +} + +PRINT_FN(total_generated) +{ + print_item_t items[] = { + { + .key = "kwh", + .title = "kWh", + .value = variant_long((long)m->kwh) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(year_generated) +{ + print_item_t items[] = { + { + .key = "kwh", + .title = "kWh", + .value = variant_long((long)m->kwh) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(month_generated) +{ + print_item_t items[] = { + { + .key = "kwh", + .title = "kWh", + .value = variant_long((long)m->kwh) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(day_generated) +{ + print_item_t items[] = { + { + .key = "wh", + .title = "Wh", + .value = variant_long((long)m->kwh) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(series_number) +{ + print_item_t items[] = { + {.key= "sn", .title= "Series number", .value= variant_string(m->id)} + }; + PRINT_AUTO(items) +} + +PRINT_FN(cpu_version) +{ + print_item_t items[] = { + {.key= "main_v", .title= "Main CPU version", .value= variant_string((m->main_cpu_version))}, + {.key= "slave1_v", .title= "Slave 1 CPU version", .value= variant_string(m->slave1_cpu_version)}, + {.key= "slave2_v", .title= "Slave 2 CPU version", .value= variant_string(m->slave2_cpu_version)}, + }; + PRINT_AUTO(items) +} + +PRINT_FN(rated_information) +{ + print_item_t items[] = { + { + .key = "ac_input_rating_voltage", + .title = "AC input rating voltage", + .value = variant_double(m->ac_input_rating_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "ac_input_rating_current", + .title = "AC input rating current", + .value = variant_double(m->ac_input_rating_current/10.0), + .precision = 1, + .unit = PRINT_UNIT_A, + }, + { + .key = "ac_output_rating_voltage", + .title = "AC output rating voltage", + .value = variant_double(m->ac_output_rating_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "ac_output_rating_freq", + .title = "AC output rating frequency", + .value = variant_double(m->ac_output_rating_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ + }, + { + .key = "ac_output_rating_current", + .title = "AC output rating current", + .value = variant_double(m->ac_output_rating_current/10.0), + .precision = 1, + .unit = PRINT_UNIT_A, + }, + { + .key = "ac_output_rating_apparent_power", + .title = "AC output rating apparent power", + .value = variant_long(m->ac_output_rating_apparent_power), + .unit = PRINT_UNIT_VA, + }, + { + .key = "ac_output_rating_active_power", + .title = "AC output rating active power", + .value = variant_long(m->ac_output_rating_active_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "battery_rating_voltage", + .title = "Battery rating voltage", + .value = variant_double(m->battery_rating_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_recharge_voltage", + .title = "Battery re-charge voltage", + .value = variant_double(m->battery_recharge_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_redischarge_voltage", + .title = "Battery re-discharge voltage", + .value = variant_double(m->battery_redischarge_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_under_voltage", + .title = "Battery under voltage", + .value = variant_double(m->battery_under_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_bulk_voltage", + .title = "Battery bulk voltage", + .value = variant_double(m->battery_bulk_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_float_voltage", + .title = "Battery float voltage", + .value = variant_double(m->battery_float_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_type", + .title = "Battery type", + .value = variant_string(p18_battery_type_label(m->battery_type)) + }, + { + .key = "max_charging_current", + .title = "Max charging current", + .value = variant_long(m->max_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "max_ac_charging_current", + .title = "Max AC charging current", + .value = variant_long(m->max_ac_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "input_voltage_range", + .title = "Input voltage range", + .value = variant_string(p18_input_voltage_range_label(m->input_voltage_range)) + }, + { + .key = "output_source_priority", + .title = "Output source priority", + .value = variant_string(p18_output_source_priority_label(m->output_source_priority)) + }, + { + .key = "charger_source_priority", + .title = "Charger source priority", + .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority)) + }, + { + .key = "parallel_max_num", + .title = "Parallel max num", + .value = variant_long(m->parallel_max_num) + }, + { + .key = "machine_type", + .title = "Machine type", + .value = variant_string(p18_machine_type_label(m->machine_type)) + }, + { + .key = "topology", + .title = "Topology", + .value = variant_string(p18_topology_label(m->topology)) + }, + { + .key = "output_model_setting", + .title = "Output model setting", + .value = variant_string(p18_output_model_setting_label(m->output_model_setting)) + }, + { + .key = "solar_power_priority", + .title = "Solar power priority", + .value = variant_string(p18_solar_power_priority_label(m->solar_power_priority)) + }, + { + .key = "mppt", + .title = "MPPT string", + .value = variant_string(m->mppt) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(general_status) +{ + print_item_t items[] = { + { + .key = "grid_voltage", + .title = "Grid voltage", + .value = variant_double(m->grid_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "grid_freq", + .title = "Grid frequency", + .value = variant_double(m->grid_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ, + }, + { + .key = "ac_output_voltage", + .title = "AC output voltage", + .value = variant_double(m->ac_output_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "ac_output_freq", + .title = "AC output frequency", + .value = variant_double(m->ac_output_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ, + }, + { + .key = "ac_output_apparent_power", + .title = "AC output apparent power", + .value = variant_long(m->ac_output_apparent_power), + .unit = PRINT_UNIT_VA, + }, + { + .key = "ac_output_active_power", + .title = "AC output active power", + .value = variant_long(m->ac_output_active_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "output_load_percent", + .title = "Output load percent", + .value = variant_long(m->output_load_percent), + .unit = PRINT_UNIT_PERCENTAGE, + }, + { + .key = "battery_voltage", + .title = "Battery voltage", + .value = variant_double(m->battery_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_voltage_scc", + .title = "Battery voltage from SCC", + .value = variant_double(m->battery_voltage_scc/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_voltage_scc2", + .title = "Battery voltage from SCC2", + .value = variant_double(m->battery_voltage_scc2/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_discharge_current", + .title = "Battery discharge current", + .value = variant_long(m->battery_discharge_current), + .unit = PRINT_UNIT_A, + }, + { + .key = "battery_charging_current", + .title = "Battery charging current", + .value = variant_long(m->battery_charging_current), + .unit = PRINT_UNIT_A, + }, + { + .key = "battery_capacity", + .title = "Battery capacity", + .value = variant_long(m->battery_capacity), + .unit = PRINT_UNIT_PERCENTAGE, + }, + { + .key = "inverter_heat_sink_temp", + .title = "Inverter heat sink temperature", + .value = variant_long(m->inverter_heat_sink_temp), + .unit = PRINT_UNIT_CELSIUS, + }, + { + .key = "mppt1_charger_temp", + .title = "MPPT1 charger temperature", + .value = variant_long(m->mppt1_charger_temp), + .unit = PRINT_UNIT_CELSIUS, + }, + { + .key = "mppt2_charger_temp", + .title = "MPPT2 charger temperature", + .value = variant_long(m->mppt2_charger_temp), + .unit = PRINT_UNIT_CELSIUS, + }, + { + .key = "pv1_input_power", + .title = "PV1 Input power", + .value = variant_double(m->pv1_input_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "pv2_input_power", + .title = "PV2 Input power", + .value = variant_double(m->pv2_input_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "pv1_input_voltage", + .title = "PV1 Input voltage", + .value = variant_double(m->pv1_input_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "pv2_input_voltage", + .title = "PV2 Input voltage", + .value = variant_double(m->pv2_input_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "settings_values_changed", + .title = "Setting value configuration state", + .value = variant_string(m->settings_values_changed ? "Nothing changed" : "Something changed"), + }, + { + .key = "mppt1_charger_status", + .title = "MPPT1 charger status", + .value = variant_string(p18_mppt_charger_status_label(m->mppt1_charger_status)), + }, + { + .key = "mppt2_charger_status", + .title = "MPPT2 charger status", + .value = variant_string(p18_mppt_charger_status_label(m->mppt2_charger_status)), + }, + { + .key = "load_connected", + .title = "Load connection", + .value = variant_string(m->load_connected ? "Connected" : "Disconnected"), + }, + { + .key = "battery_power_direction", + .title = "Battery power direction", + .value = variant_string(p18_battery_power_direction_label(m->battery_power_direction)), + }, + { + .key = "dc_ac_power_direction", + .title = "DC/AC power direction", + .value = variant_string(p18_dc_ac_power_direction_label(m->dc_ac_power_direction)), + }, + { + .key = "line_power_direction", + .title = "Line power direction", + .value = variant_string(p18_line_power_direction_label(m->line_power_direction)), + }, + { + .key = "local_parallel_id", + .title = "Local parallel ID", + .value = variant_long(m->local_parallel_id), + } + }; + PRINT_AUTO(items) +} + +PRINT_FN(working_mode) +{ + print_item_t items[] = { + {.key= "mode", .title= "Working mode", .value= variant_string(p18_working_mode_label(m->mode))} + }; + PRINT_AUTO(items) +} + +PRINT_FN(faults_warnings) +{ + print_item_t items[] = { + {.key= "fault_code", .title= "Fault code", .value= variant_string(p18_fault_code_label(m->fault_code))}, + {.key= "line_fail", .title= "Line fail", .value= variant_bool(m->line_fail)}, + {.key= "output_circuit_short", .title= "Output circuit short", .value= variant_bool(m->output_circuit_short)}, + {.key= "inverter_over_temperature", .title= "Inverter over temperature",.value= variant_bool(m->inverter_over_temperature)}, + {.key= "fan_lock", .title= "Fan lock", .value= variant_bool(m->fan_lock)}, + {.key= "battery_voltage_high", .title= "Battery voltage high", .value= variant_bool(m->battery_voltage_high)}, + {.key= "battery_low", .title= "Battery low", .value= variant_bool(m->battery_low)}, + {.key= "battery_under", .title= "Battery under", .value= variant_bool(m->battery_under)}, + {.key= "over_load", .title= "Over load", .value= variant_bool(m->over_load)}, + {.key= "eeprom_fail", .title= "EEPROM fail", .value= variant_bool(m->eeprom_fail)}, + {.key= "power_limit", .title= "Power limit", .value= variant_bool(m->power_limit)}, + {.key= "pv1_voltage_high", .title= "PV1 voltage high", .value= variant_bool(m->pv1_voltage_high)}, + {.key= "pv2_voltage_high", .title= "PV2 voltage high", .value= variant_bool(m->pv2_voltage_high)}, + {.key= "mppt1_overload_warning", .title= "MPPT1 overload warning", .value= variant_bool(m->mppt1_overload_warning)}, + {.key= "mppt2_overload_warning", .title= "MPPT2 overload warning", .value= variant_bool(m->mppt2_overload_warning)}, + {.key= "battery_too_low_to_charge_for_scc1", .title= "Battery too low to charge for SCC1", .value= variant_bool(m->battery_too_low_to_charge_for_scc1)}, + {.key= "battery_too_low_to_charge_for_scc2", .title= "Battery too low to charge for SCC2", .value= variant_bool(m->battery_too_low_to_charge_for_scc2)}, + }; + PRINT_AUTO(items) +} + +PRINT_FN(flags_statuses) +{ + print_item_t items[] = { + { + .key = "buzzer", + .title = "Buzzer", + .value = variant_flag(m->buzzer) + }, + { + .key = "overload_bypass", + .title = "Overload bypass function", + .value = variant_flag(m->overload_bypass) + }, + { + .key = "lcd_escape_to_default_page_after_1min_timeout", + .title = "Escape to default page after 1min timeout", + .value = variant_flag(m->lcd_escape_to_default_page_after_1min_timeout) + }, + { + .key = "overload_restart", + .title = "Overload restart", + .value = variant_flag(m->overload_restart) + }, + { + .key = "over_temp_restart", + .title = "Over temperature restart", + .value = variant_flag(m->over_temp_restart) + }, + { + .key = "backlight_on", + .title = "Backlight on", + .value = variant_flag(m->backlight_on) + }, + { + .key = "alarm_on_primary_source_interrupt", + .title = "Alarm on when primary source interrupt", + .value = variant_flag(m->alarm_on_primary_source_interrupt) + }, + { + .key = "fault_code_record", + .title = "Fault code record", + .value = variant_flag(m->fault_code_record) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(defaults) +{ + print_item_t items[] = { + { + .key = "ac_output_voltage", + .title = "AC output voltage", + .value = variant_double(m->ac_output_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "ac_output_freq", + .title = "AC output frequency", + .value = variant_double(m->ac_output_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ, + }, + { + .key = "ac_input_voltage_range", + .title = "AC input voltage range", + .value = variant_string(p18_input_voltage_range_label(m->ac_input_voltage_range)), + }, + { + .key = "battery_under_voltage", + .title = "Battery under voltage", + .value = variant_double(m->battery_under_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_bulk_voltage", + .title = "Charging bulk voltage", + .value = variant_double(m->charging_bulk_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_float_voltage", + .title = "Charging float voltage", + .value = variant_double(m->charging_float_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_recharge_voltage", + .title = "Battery re-charge voltage", + .value = variant_double(m->battery_recharge_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_redischarge_voltage", + .title = "Battery re-discharge voltage", + .value = variant_double(m->battery_redischarge_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "max_ac_charging_current", + .title = "Max AC charging current", + .value = variant_long(m->max_ac_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "max_charging_current", + .title = "Max charging current", + .value = variant_long(m->max_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "battery_type", + .title = "Battery type", + .value = variant_string(p18_battery_type_label(m->battery_type)) + }, + { + .key = "output_source_priority", + .title = "Output source priority", + .value = variant_string(p18_output_source_priority_label(m->output_source_priority)) + }, + { + .key = "charger_source_priority", + .title = "Charger source priority", + .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority)) + }, + { + .key = "solar_power_priority", + .title = "Solar power priority", + .value = variant_string(p18_solar_power_priority_label(m->solar_power_priority)) + }, + { + .key = "machine_type", + .title = "Machine type", + .value = variant_string(p18_machine_type_label(m->machine_type)) + }, + { + .key = "output_model_setting", + .title = "Output model setting", + .value = variant_string(p18_output_model_setting_label(m->output_model_setting)) + }, + { + .key = "buzzer_flag", + .title = "Buzzer flag", + .value = variant_flag(m->flag_buzzer) + }, + { + .key = "overload_bypass_flag", + .title = "Overload bypass function flag", + .value = variant_flag(m->flag_overload_bypass) + }, + { + .key = "lcd_escape_to_default_page_after_1min_timeout_flag", + .title = "Escape to default page after 1min timeout flag", + .value = variant_flag(m->flag_lcd_escape_to_default_page_after_1min_timeout) + }, + { + .key = "overload_restart_flag", + .title = "Overload restart flag", + .value = variant_flag(m->flag_overload_restart) + }, + { + .key = "over_temp_restart_flag", + .title = "Over temperature restart flag", + .value = variant_flag(m->flag_over_temp_restart) + }, + { + .key = "backlight_on_flag", + .title = "Backlight on flag", + .value = variant_flag(m->flag_backlight_on) + }, + { + .key = "alarm_on_primary_source_interrupt_flag", + .title = "Alarm on when primary source interrupt flag", + .value = variant_flag(m->flag_alarm_on_primary_source_interrupt) + }, + { + .key = "fault_code_record_flag", + .title = "Fault code record flag", + .value = variant_flag(m->flag_fault_code_record) + } + }; + PRINT_AUTO(items) +} + +PRINT_FN(max_charging_current_selectable_values) +{ + if (print_is_json_format(format)) + print_json_list(m->amps, m->len); + else if (print_is_table_format(format)) + print_table_list(m->amps, m->len); +} + +PRINT_FN(max_ac_charging_current_selectable_values) +{ + if (print_is_json_format(format)) + print_json_list(m->amps, m->len); + else if (print_is_table_format(format)) + print_table_list(m->amps, m->len); +} + +PRINT_FN(parallel_rated_information) +{ + print_item_t items[] = { + { + .key = "parallel_id_connection_status", + .title = "Parallel ID connection status", + .value = variant_string(p18_parallel_connection_status_label(m->parallel_id_connection_status)), + }, + { + .key = "serial_number", + .title = "Serial number", + .value = variant_string(m->serial_number), + }, + { + .key = "charger_source_priority", + .title = "Charger source priority", + .value = variant_string(p18_charge_source_priority_label(m->charger_source_priority)) + }, + { + .key = "max_charging_current", + .title = "Max charging current", + .value = variant_long(m->max_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "max_ac_charging_current", + .title = "Max AC charging current", + .value = variant_long(m->max_ac_charging_current), + .unit = PRINT_UNIT_A + }, + { + .key = "output_model_setting", + .title = "Output model setting", + .value = variant_string(p18_output_model_setting_label(m->output_model_setting)) + }, + }; + PRINT_AUTO(items) +} + +PRINT_FN(parallel_general_status) +{ + print_item_t items[] = { + { + .key = "parallel_id_connection_status", + .title = "Parallel ID connection status", + .value = variant_string(p18_parallel_connection_status_label(m->parallel_id_connection_status)), + }, + { + .key = "mode", + .title = "Working mode", + .value = variant_string(p18_working_mode_label(m->work_mode)) + }, + { + .key = "fault_code", + .title = "Fault code", + .value = variant_string(p18_fault_code_label(m->fault_code)) + }, + { + .key = "grid_voltage", + .title = "Grid voltage", + .value = variant_double(m->grid_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "grid_freq", + .title = "Grid frequency", + .value = variant_double(m->grid_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ, + }, + { + .key = "ac_output_voltage", + .title = "AC output voltage", + .value = variant_double(m->ac_output_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "ac_output_freq", + .title = "AC output frequency", + .value = variant_double(m->ac_output_freq/10.0), + .precision = 1, + .unit = PRINT_UNIT_HZ, + }, + { + .key = "ac_output_apparent_power", + .title = "AC output apparent power", + .value = variant_long(m->ac_output_apparent_power), + .unit = PRINT_UNIT_VA, + }, + { + .key = "ac_output_active_power", + .title = "AC output active power", + .value = variant_long(m->ac_output_active_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "total_ac_output_apparent_power", + .title = "Total AC output apparent power", + .value = variant_long(m->total_ac_output_apparent_power), + .unit = PRINT_UNIT_VA, + }, + { + .key = "total_ac_output_active_power", + .title = "Total AC output active power", + .value = variant_long(m->total_ac_output_active_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "output_load_percent", + .title = "Output load percent", + .value = variant_long(m->output_load_percent), + .unit = PRINT_UNIT_PERCENTAGE, + }, + { + .key = "total_output_load_percent", + .title = "Total output load percent", + .value = variant_long(m->total_output_load_percent), + .unit = PRINT_UNIT_PERCENTAGE, + }, + { + .key = "battery_voltage", + .title = "Battery voltage", + .value = variant_double(m->battery_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "battery_discharge_current", + .title = "Battery discharge current", + .value = variant_long(m->battery_discharge_current), + .unit = PRINT_UNIT_A, + }, + { + .key = "battery_charging_current", + .title = "Battery charging current", + .value = variant_long(m->battery_charging_current), + .unit = PRINT_UNIT_A, + }, + { + .key = "pv1_input_power", + .title = "PV1 Input power", + .value = variant_double(m->pv1_input_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "pv2_input_power", + .title = "PV2 Input power", + .value = variant_double(m->pv2_input_power), + .unit = PRINT_UNIT_WH, + }, + { + .key = "pv1_input_voltage", + .title = "PV1 Input voltage", + .value = variant_double(m->pv1_input_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "pv2_input_voltage", + .title = "PV2 Input voltage", + .value = variant_double(m->pv2_input_voltage/10.0), + .precision = 1, + .unit = PRINT_UNIT_V, + }, + { + .key = "mppt1_charger_status", + .title = "MPPT1 charger status", + .value = variant_string(p18_mppt_charger_status_label(m->mppt1_charger_status)), + }, + { + .key = "mppt2_charger_status", + .title = "MPPT2 charger status", + .value = variant_string(p18_mppt_charger_status_label(m->mppt2_charger_status)), + }, + { + .key = "load_connected", + .title = "Load connection", + .value = variant_string(m->load_connected ? "Connected" : "Disconnected"), + }, + { + .key = "battery_power_direction", + .title = "Battery power direction", + .value = variant_string(p18_battery_power_direction_label(m->battery_power_direction)), + }, + { + .key = "dc_ac_power_direction", + .title = "DC/AC power direction", + .value = variant_string(p18_dc_ac_power_direction_label(m->dc_ac_power_direction)), + }, + { + .key = "line_power_direction", + .title = "Line power direction", + .value = variant_string(p18_line_power_direction_label(m->line_power_direction)), + }, + { + .key = "max_temp", + .title = "Max. temperature", + .value = variant_long(m->max_temp), + } + }; + PRINT_AUTO(items) +} + +PRINT_FN(ac_charge_time_bucket) +{ + static const int buf_size = 16; + char start_time[buf_size], end_time[buf_size]; + snprintf(start_time, buf_size, "%02d:%02d", m->start_h, m->start_m); + snprintf(end_time, buf_size, "%02d:%02d", m->end_h, m->end_m); + print_item_t items[] = { + {.key= "start_time", .title= "Start time", .value= variant_string(start_time)}, + {.key= "end_time", .title= "End time", .value= variant_string(end_time)}, + }; + PRINT_AUTO(items) +} + +PRINT_FN(ac_supply_load_time_bucket) +{ + static const int buf_size = 16; + char start_time[buf_size], end_time[buf_size]; + snprintf(start_time, buf_size, "%02d:%02d", m->start_h, m->start_m); + snprintf(end_time, buf_size, "%02d:%02d", m->end_h, m->end_m); + print_item_t items[] = { + {.key= "start_time", .title= "Start time", .value= variant_string(start_time)}, + {.key= "end_time", .title= "End time", .value= variant_string(end_time)}, + }; + PRINT_AUTO(items) +} \ No newline at end of file diff --git a/print.h b/print.h new file mode 100644 index 0000000..bb85269 --- /dev/null +++ b/print.h @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#ifndef ISV_PRINT_H +#define ISV_PRINT_H + +#include "p18.h" +#include "variant.h" + +#define MAKE_PRINT_FN_NAME(msg_type) print_ ## msg_type ## _msg +#define PRINT_FN_NAME(msg_type) MAKE_PRINT_FN_NAME(msg_type) + +#define PRINT_FN(msg_type) \ + void PRINT_FN_NAME(msg_type)(const P18_MSG_T(msg_type) *m, print_format_t format) + +typedef enum { + PRINT_UNIT_V = 1, + PRINT_UNIT_A, + PRINT_UNIT_WH, + PRINT_UNIT_KWH, + PRINT_UNIT_VA, + PRINT_UNIT_HZ, + PRINT_UNIT_PERCENTAGE, + PRINT_UNIT_CELSIUS, +} print_unit_t; + +typedef enum { + PRINT_FORMAT_TABLE, + PRINT_FORMAT_PARSABLE_TABLE, + PRINT_FORMAT_JSON, + PRINT_FORMAT_JSON_W_UNITS, +} print_format_t; + +typedef struct { + char *key; + char *title; + variant_t value; + short precision; + print_unit_t unit; +} print_item_t; + +void print_json(print_item_t *items, size_t size, bool with_units); +void print_set_result(bool success, print_format_t format); +bool print_is_json_format(print_format_t f); + +PRINT_FN(protocol_id); +PRINT_FN(current_time); +PRINT_FN(total_generated); +PRINT_FN(year_generated); +PRINT_FN(month_generated); +PRINT_FN(day_generated); +PRINT_FN(series_number); +PRINT_FN(cpu_version); +PRINT_FN(rated_information); +PRINT_FN(general_status); +PRINT_FN(working_mode); +PRINT_FN(faults_warnings); +PRINT_FN(flags_statuses); +PRINT_FN(defaults); +PRINT_FN(max_charging_current_selectable_values); +PRINT_FN(max_ac_charging_current_selectable_values); +PRINT_FN(parallel_rated_information); +PRINT_FN(parallel_general_status); +PRINT_FN(ac_charge_time_bucket); +PRINT_FN(ac_supply_load_time_bucket); + +#endif //ISV_PRINT_H diff --git a/util.c b/util.c new file mode 100644 index 0000000..856b2af --- /dev/null +++ b/util.c @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#include +#include +#include +#include + +#include "util.h" + +#define HEXDUMP_COLS 8 + +/* based on: https://gist.github.com/richinseattle/c527a3acb6f152796a580401057c78b4 */ +void hexdump(void *mem, unsigned int len) +{ + unsigned int i, j; + + for (i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) { + /* Print offset */ + if (i % HEXDUMP_COLS == 0) + printf("0x%06x: ", i); + + if (i < len) + /* Print hex data */ + printf("%02x ", 0xFF & ((char *) mem)[i]); + else + /* End of block, just aligning for ASCII dump */ + printf(" "); + + /* Print ASCII dump */ + if (i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) { + for (j = i - (HEXDUMP_COLS - 1); j <= i; j++) { + if (j >= len) + /* end of block, not really printing */ + putchar(' '); + else if (isprint(((char *) mem)[j])) + /* printable char */ + putchar(0xFF & ((char *) mem)[j]); + else + /* other char */ + putchar('.'); + } + + putchar('\n'); + } + } +} + +/* dst buffer's length must be at least len+1 */ +void substr_copy(char *dst, const char *src, int len) +{ + strncpy(dst, src, len); + dst[len] = '\0'; +} + +bool isnumeric(const char *s) +{ + size_t len = strlen(s); + for (size_t i = 0; i < len; i++) { + if (!isdigit(s[i])) + return false; + } + return true; +} + +bool isdatevalid(const int y, const int m, const int d) +{ + /* primitive out of range checks */ + if (y < 2000 || y > 2099) + return false; + + if (d < 1 || d > 31) + return false; + + if (m < 1 || m > 12) + return false; + + /* some more clever date validity checks */ + if ((m == 4 || m == 6 || m == 9 || m == 11) && d == 31) + return false; + + /* and finally a february check */ + /* i always wondered, when do people born at feb 29 celebrate their bday? */ + return m != 2 || ((y % 4 != 0 && d <= 28) || (y % 4 == 0 && d <= 29)); +} + +bool instrarray(const char *needle, const char **list, size_t list_size, int *index) +{ + bool found = false; + for (size_t i = 0; i < list_size; i++) { + /* LOG("%s: comparing %s == %s\n", __func__, list[i], needle); */ + if (!strcmp(list[i], needle)) { + found = true; + if (index != NULL) + *index = (int)i; + break; + } + } + return found; +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..64db4bf --- /dev/null +++ b/util.h @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2020 Evgeny Zinoviev + * This file is part of isv . + * + * 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 . + */ + +#ifndef ISV_UTIL_H +#define ISV_UTIL_H + +#include +#include + +#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 . + * + * 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 . + */ + +#include +#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 . + * + * 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 . + */ + +#ifndef ISV_VARIANT_H +#define ISV_VARIANT_H + +#include + +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 -- cgit v1.2.3