aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2021-05-07 02:18:07 +0300
committerEvgeny Zinoviev <me@ch1p.io>2021-05-07 02:18:07 +0300
commit7e743b73433475df086fcec81be7b10c1d695a42 (patch)
tree1737c5f9bdad2a40f740e9a655e510641331b9e2
initial
-rw-r--r--.gitignore6
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt106
-rw-r--r--LICENSE27
-rw-r--r--PROTOCOL.md31
-rw-r--r--README.md38
-rw-r--r--src/common.cc15
-rw-r--r--src/common.h36
-rw-r--r--src/formatter/formatter.cc38
-rw-r--r--src/formatter/formatter.h257
-rw-r--r--src/inverterctl.cc490
-rw-r--r--src/inverterd.cc292
-rw-r--r--src/logging.h43
-rw-r--r--src/numeric_types.h10
-rw-r--r--src/p18/client.cc234
-rw-r--r--src/p18/client.h31
-rw-r--r--src/p18/commands.cc455
-rw-r--r--src/p18/commands.h35
-rw-r--r--src/p18/defines.cc246
-rw-r--r--src/p18/defines.h32
-rw-r--r--src/p18/exceptions.h22
-rw-r--r--src/p18/functions.cc12
-rw-r--r--src/p18/functions.h12
-rw-r--r--src/p18/response.cc781
-rw-r--r--src/p18/response.h461
-rw-r--r--src/p18/types.h162
-rw-r--r--src/server/connection.cc258
-rw-r--r--src/server/connection.h70
-rw-r--r--src/server/server.cc143
-rw-r--r--src/server/server.h79
-rw-r--r--src/server/signal.cc20
-rw-r--r--src/server/signal.h16
-rw-r--r--src/testserial.cc73
-rw-r--r--src/util.cc86
-rw-r--r--src/util.h31
-rw-r--r--src/voltronic/crc.cc60
-rw-r--r--src/voltronic/crc.h20
-rw-r--r--src/voltronic/device.cc167
-rw-r--r--src/voltronic/device.h176
-rw-r--r--src/voltronic/exceptions.h27
-rw-r--r--src/voltronic/pseudo_device.cc63
-rw-r--r--src/voltronic/serial_device.cc160
-rw-r--r--src/voltronic/time.cc38
-rw-r--r--src/voltronic/time.h14
-rw-r--r--src/voltronic/usb_device.cc60
-rw-r--r--third_party/hexdump/hexdump.h83
46 files changed, 5519 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0a317a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.o
+isv
+.idea
+cmake-build-debug/
+cmake-build-release/
+build/ \ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..c88fb31
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "third_party/json"]
+ path = third_party/json
+ url = https://github.com/nlohmann/json
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..8cc7583
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,106 @@
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.0)
+set(CMAKE_CXX_STANDARD 17)
+add_compile_options(-Wno-psabi)
+
+project(inverter-tools VERSION 1.0)
+
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX /usr/local/bin)
+endif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+
+
+include(GNUInstallDirs)
+
+
+# find hidapi
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+ find_library(HIDAPI_LIBRARY hidapi-hidraw)
+endif()
+if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+ find_library(HIDAPI_LIBRARY hidapi)
+endif()
+find_path(HIDAPI_INCLUDE_DIR hidapi/hidapi.h)
+
+
+# find libserialport
+find_library(LIBSERIALPORT_LIBRARY serialport)
+find_path(LIBSERIALPORT_INCLUDE_DIR libserialport.h)
+
+
+add_executable(inverterctl
+ src/inverterctl.cc
+ src/p18/defines.cc
+ src/p18/client.cc
+ src/p18/functions.cc
+ src/p18/response.cc
+ src/util.cc
+ src/p18/commands.cc
+ src/common.cc
+ src/formatter/formatter.cc
+ src/voltronic/crc.cc
+ src/voltronic/usb_device.cc
+ src/voltronic/device.cc
+ src/voltronic/time.cc
+ src/voltronic/serial_device.cc
+ src/voltronic/pseudo_device.cc)
+target_include_directories(inverterctl PRIVATE .)
+target_link_libraries(inverterctl m ${HIDAPI_LIBRARY} ${LIBSERIALPORT_LIBRARY})
+target_compile_definitions(inverterctl PUBLIC INVERTERCTL)
+target_include_directories(inverterctl PRIVATE
+ ${HIDAPI_INCLUDE_DIR}
+ ${LIBSERIALPORT_INCLUDE_DIR}
+ third_party
+ third_party/json/single_include)
+install(TARGETS inverterctl
+ RUNTIME DESTINATION bin)
+
+
+add_executable(inverterd
+ src/inverterd.cc
+ src/common.cc
+ src/util.cc
+ src/server/server.cc
+ src/server/connection.cc
+ src/server/signal.cc
+ src/p18/commands.cc
+ src/p18/defines.cc
+ src/p18/client.cc
+ src/p18/functions.cc
+ src/p18/response.cc
+ src/formatter/formatter.cc
+ src/voltronic/crc.cc
+ src/voltronic/usb_device.cc
+ src/voltronic/device.cc
+ src/voltronic/time.cc
+ src/voltronic/serial_device.cc
+ src/voltronic/pseudo_device.cc)
+target_include_directories(inverterd PRIVATE .)
+target_compile_definitions(inverterd PUBLIC INVERTERD)
+target_link_libraries(inverterd
+ m pthread
+ ${HIDAPI_LIBRARY}
+ ${LIBSERIALPORT_LIBRARY})
+target_include_directories(inverterd PRIVATE
+ ${HIDAPI_INCLUDE_DIR}
+ ${LIBSERIALPORT_INCLUDE_DIR}
+ third_party
+ third_party/json/single_include)
+install(TARGETS inverterd
+ RUNTIME DESTINATION bin)
+
+
+add_executable(testserial src/testserial.cc)
+target_include_directories(testserial PRIVATE .)
+target_link_libraries(testserial ${LIBSERIALPORT_LIBRARY})
+target_include_directories(testserial PRIVATE
+ ${LIBSERIALPORT_INCLUDE_DIR}
+ third_party/hexdump)
+
+
+# inverterd
+#add_executable(inverterd
+# src/inverterd.cc)
+#target_link_libraries(inverterd ${HIDAPI_LIBRARY} ${LIBSERIALPORT_LIBRARY} m)
+# TODO install
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dbd2af8
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2021, Evgeny Zinoviev
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file
diff --git a/PROTOCOL.md b/PROTOCOL.md
new file mode 100644
index 0000000..44eb97a
--- /dev/null
+++ b/PROTOCOL.md
@@ -0,0 +1,31 @@
+# inverterd protocol
+
+inverterd implements simple text-based, telnet-compatible protocol.
+
+## Requests
+
+Each request is represented by a single line that ends with `\r\n`, in the
+following format:
+```
+COMMAND [...ARGUMENTS]
+```
+
+Available commands:
+
+- `v` `VERSION`<br>
+ Sets the protocol version, affects subsequents requests. Default version is `1`.
+
+- `format` `FORMAT`<br>
+ Sets the data format for device responses.
+
+- `exec` `COMMAND` `[...ARGUMENTS]`<br>
+ Runs a command.
+
+Sending `EOT` (`0x04`) closes connection.
+
+## Responses
+
+Each response is represented by one or more lines, each ending with `\r\n`, plus
+extra `\r\n` in the end.
+
+First line is always a status, which may be either `ok` or `err`. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ad98f8e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+# inverter-tools
+
+**inverter-tools** is a collection of tools for controlling Voltronic hybrid solar
+inverters. Only P18 protocol is supported at the moment, supporting more hardware
+is planned.
+
+- `inverterctl` is a full-featured command line utility with all P18 commands
+ supported.
+
+- `inverterd` is a daemon that starts TCP server that accepts user requests. It
+ replaces inverterctl for multi-user scenarios, where there may be more than one
+ simultaneous request to device, to avoid errors or lockups.
+
+## Requirements
+
+- Linux (tested on x86_64 and armhf), macOS (tested on aarch64)
+- C++17 compiler
+- CMake
+- HIDAPI
+- libserialport
+
+## Supported devices
+
+As the time of writing, only InfiniSolar V 5KW was tested.
+
+## Supported interfaces
+
+* USB (HIDAPI)
+* RS232 (libserialport)
+
+## Usage
+
+Please use the `--help` option for now. The help message has full description
+for all possible options and commands.
+
+## License
+
+BSD-3-Clause \ No newline at end of file
diff --git a/src/common.cc b/src/common.cc
new file mode 100644
index 0000000..fbd1614
--- /dev/null
+++ b/src/common.cc
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "common.h"
+#include <stdexcept>
+
+formatter::Format format_from_string(std::string& s) {
+ if (s == "json")
+ return formatter::Format::JSON;
+ else if (s == "table")
+ return formatter::Format::Table;
+ else if (s == "simple-table")
+ return formatter::Format::SimpleTable;
+ else
+ throw std::invalid_argument("invalid format");
+} \ No newline at end of file
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..6786b5e
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_COMMON_H
+#define INVERTER_TOOLS_COMMON_H
+
+#include "formatter/formatter.h"
+
+enum class DeviceType {
+ USB,
+ Serial,
+ Pseudo
+};
+
+// long opts
+enum {
+ LO_HELP = 1,
+ LO_VERBOSE,
+ LO_RAW,
+ LO_TIMEOUT,
+ LO_CACHE_TIMEOUT,
+ LO_FORMAT,
+ LO_DEVICE,
+ LO_USB_VENDOR_ID,
+ LO_USB_DEVICE_ID,
+ LO_SERIAL_NAME,
+ LO_SERIAL_BAUD_RATE,
+ LO_SERIAL_DATA_BITS,
+ LO_SERIAL_STOP_BITS,
+ LO_SERIAL_PARITY,
+ LO_HOST,
+ LO_PORT,
+};
+
+formatter::Format format_from_string(std::string& s);
+
+#endif //INVERTER_TOOLS_COMMON_H
diff --git a/src/formatter/formatter.cc b/src/formatter/formatter.cc
new file mode 100644
index 0000000..6fedf8c
--- /dev/null
+++ b/src/formatter/formatter.cc
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "formatter.h"
+
+namespace formatter {
+
+std::ostream& operator<<(std::ostream& os, Unit val) {
+ switch (val) {
+ case Unit::V:
+ return os << "V";
+
+ case Unit::A:
+ return os << "A";
+
+ case Unit::Wh:
+ return os << "Wh";
+
+ case Unit::VA:
+ return os << "VA";
+
+ case Unit::Hz:
+ return os << "Hz";
+
+ case Unit::Percentage:
+ return os << "%";
+
+ case Unit::Celsius:
+ return os << "°C";
+
+ default:
+ break;
+ };
+
+ return os;
+}
+
+
+} \ No newline at end of file
diff --git a/src/formatter/formatter.h b/src/formatter/formatter.h
new file mode 100644
index 0000000..5dfa184
--- /dev/null
+++ b/src/formatter/formatter.h
@@ -0,0 +1,257 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_PRINT_H
+#define INVERTER_TOOLS_PRINT_H
+
+#include <string>
+#include <vector>
+#include <iostream>
+#include <sstream>
+#include <ios>
+#include <iomanip>
+#include <nlohmann/json.hpp>
+
+#include "src/util.h"
+
+namespace formatter {
+
+using nlohmann::json;
+using nlohmann::ordered_json;
+
+
+/**
+ * Enumerations
+ */
+
+enum class Unit {
+ None = 0,
+ V,
+ A,
+ Wh,
+ VA,
+ Hz,
+ Percentage,
+ Celsius,
+};
+
+enum class Format {
+ Table,
+ SimpleTable,
+ JSON,
+};
+std::ostream& operator<<(std::ostream& os, Unit val);
+
+
+/**
+ * Helper functions
+ */
+
+template <typename T>
+std::string to_str(T& v) {
+ std::ostringstream buf;
+ buf << v;
+ return buf.str();
+}
+
+
+/**
+ * Items
+ */
+
+template <typename T>
+struct TableItem {
+ explicit TableItem(std::string key, std::string title, T value, Unit unit = Unit::None, unsigned precision = 0) :
+ key(std::move(key)),
+ title(std::move(title)),
+ value(value),
+ unit(unit) {}
+
+ std::string key;
+ std::string title;
+ T value;
+ Unit unit;
+};
+
+template <typename T>
+struct ListItem {
+ explicit ListItem(T value) : value(value) {}
+ T value;
+};
+
+
+/**
+ * Items holders
+ */
+
+class Formattable {
+protected:
+ Format format_;
+
+public:
+ explicit Formattable(Format format) : format_(format) {}
+ virtual ~Formattable() = default;
+
+ virtual std::ostream& writeJSON(std::ostream& os) const = 0;
+ virtual std::ostream& writeTable(std::ostream& os) const = 0;
+ virtual std::ostream& writeSimpleTable(std::ostream& os) const = 0;
+
+ friend std::ostream& operator<<(std::ostream& os, Formattable const& ref) {
+ switch (ref.format_) {
+ case Format::Table:
+ return ref.writeTable(os);
+
+ case Format::SimpleTable:
+ return ref.writeSimpleTable(os);
+
+ case Format::JSON:
+ return ref.writeJSON(os);
+ }
+
+ return os;
+ }
+};
+
+
+// T must have `operator<<` and `basic_json toJSON()` methods
+template <typename T>
+class Table : public Formattable {
+protected:
+ std::vector<TableItem<T>> v_;
+
+public:
+ explicit Table(Format format, std::vector<TableItem<T>> v)
+ : Formattable(format), v_(v) {}
+
+ std::ostream& writeSimpleTable(std::ostream& os) const override {
+ for (const auto& item: v_) {
+ os << item.key << " ";
+
+ std::string value = to_str(item.value);
+ bool space = string_has(value, ' ');
+ if (space)
+ os << "\"";
+ os << value;
+ if (space)
+ os << "\"";
+
+ if (item.unit != Unit::None)
+ os << " " << item.unit;
+
+ if (&item != &v_.back())
+ os << std::endl;
+ }
+ return os;
+ }
+
+ std::ostream& writeTable(std::ostream& os) const override {
+ int maxWidth = 0;
+ for (const auto& item: v_) {
+ int width = item.title.size()+1 /* colon */;
+ if (width > maxWidth)
+ maxWidth = width;
+ }
+
+ std::ios_base::fmtflags f(os.flags());
+ os << std::left;
+ for (const auto &item: v_) {
+ os << std::setw(maxWidth) << (item.title+":") << " " << item.value;
+
+ if (item.unit != Unit::None)
+ os << " " << item.unit;
+
+ if (&item != &v_.back())
+ os << std::endl;
+ }
+
+ os.flags(f);
+ return os;
+ }
+
+ std::ostream& writeJSON(std::ostream& os) const override {
+ ordered_json j = {
+ {"result", "ok"},
+ {"data", {}}
+ };
+ for (const auto &item: v_) {
+ if (item.unit != Unit::None) {
+ json jval = json::object();
+ jval["value"] = item.value.toJSON();
+ jval["unit"] = to_str(item.unit);
+ j["data"][item.key] = jval;
+ } else {
+ j["data"][item.key] = item.value.toJSON();
+ }
+ }
+ return os << j.dump();
+ }
+};
+
+template <typename T>
+class List : public Formattable {
+protected:
+ std::vector<ListItem<T>> v_;
+
+public:
+ explicit List(Format format, std::vector<ListItem<T>> v)
+ : Formattable(format), v_(v) {}
+
+ std::ostream& writeSimpleTable(std::ostream& os) const override {
+ return writeTable(os);
+ }
+
+ std::ostream& writeTable(std::ostream& os) const override {
+ for (const auto &item: v_) {
+ os << item.value;
+ if (&item != &v_.back())
+ os << std::endl;
+ }
+ return os;
+ }
+
+ std::ostream& writeJSON(std::ostream& os) const override {
+ json data = {};
+ ordered_json j;
+
+ j["result"] = "ok";
+
+ for (const auto &item: v_)
+ data.push_back(item.value.toJSON());
+ j["data"] = data;
+
+ return os << j.dump();
+ }
+};
+
+class Status : public Formattable {
+protected:
+ bool value_;
+ std::string message_;
+
+public:
+ explicit Status(Format format, bool value, std::string message)
+ : Formattable(format), value_(value), message_(std::move(message)) {}
+
+ std::ostream& writeSimpleTable(std::ostream& os) const override {
+ return writeTable(os);
+ }
+
+ std::ostream& writeTable(std::ostream& os) const override {
+ os << (value_ ? "ok" : "error");
+ if (!message_.empty())
+ os << ": " << message_;
+ return os;
+ }
+
+ std::ostream& writeJSON(std::ostream& os) const override {
+ ordered_json j = {
+ {"result", (value_ ? "ok" : "error")}
+ };
+ if (!message_.empty())
+ j["message"] = message_;
+ return os << j.dump();
+ }
+};
+
+}
+
+#endif \ No newline at end of file
diff --git a/src/inverterctl.cc b/src/inverterctl.cc
new file mode 100644
index 0000000..882c425
--- /dev/null
+++ b/src/inverterctl.cc
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <cstdlib>
+#include <string>
+#include <iostream>
+#include <ios>
+#include <iomanip>
+#include <array>
+#include <vector>
+#include <stdexcept>
+#include <getopt.h>
+
+#include "logging.h"
+#include "util.h"
+#include "common.h"
+#include "p18/client.h"
+#include "p18/types.h"
+#include "p18/defines.h"
+#include "p18/exceptions.h"
+#include "p18/commands.h"
+#include "formatter/formatter.h"
+#include "voltronic/device.h"
+#include "voltronic/exceptions.h"
+#include "hexdump/hexdump.h"
+
+const size_t MAX_RAW_COMMAND_LENGTH = 128;
+
+template <typename T, std::size_t N>
+std::ostream& operator<<(std::ostream& os, const std::array<T, N>& P) {
+ for (auto const& item: P) {
+ std::cout << item;
+ if (&item != &P.back())
+ std::cout << "|";
+ }
+ return os;
+}
+
+static void short_usage(const char* progname) {
+ std::cout << "Usage: " << progname << " OPTIONS [COMMAND]\n" <<
+ "\n"
+ "Options:\n"
+ " -h: Show this help\n"
+ " --help: Show full help (with all commands)\n"
+ " --raw <DATA>: Execute arbitrary command and print response\n"
+ " --device <DEVICE>: 'usb' (default), 'serial' or 'pseudo'\n"
+ " --timeout <TIMEOUT>: Timeout in ms (default: " << voltronic::Device::TIMEOUT << ")\n"
+ " --verbose: Be verbose\n"
+ " --format <FORMAT>: 'table' (default), 'simple-table' or 'json'\n"
+ "\n"
+ "To see list of supported commands, use --help.\n";
+ exit(1);
+}
+
+static void usage(const char* progname) {
+ std::ios_base::fmtflags f(std::cout.flags());
+ std::cout << "Usage: " << progname << " OPTIONS [COMMAND]\n" <<
+ "\n"
+ "Options:\n"
+ " -h: Show short help\n"
+ " --help: Show this help\n"
+ " --raw <DATA>: Execute arbitrary command and print response\n"
+ " (example: ^P005PI)\n"
+ " --device <DEVICE>: Device type to use. See below for list of supported\n"
+ " devices\n"
+ " --timeout <TIMEOUT>: Device read/write timeout, in milliseconds\n"
+ " (default: " << voltronic::Device::TIMEOUT << ")\n"
+ " --verbose: Print debug information (including hex dumps of\n"
+ " device traffic)\n"
+ " --format <FORMAT>: Output format for command responses\n"
+ "\n"
+ "Device types:\n"
+ " usb USB device\n"
+ " serial Serial device\n"
+ " pseudo Pseudo device (only useful for development/debugging purposes)\n"
+ "\n";
+ std::cout << std::hex << std::setfill('0') <<
+ "USB device options:\n"
+ " --usb-vendor-id <ID>: Vendor ID (default: " << std::setw(4) << voltronic::USBDevice::VENDOR_ID << ")\n"
+ " --usb-device-id <ID>: Device ID (default: " << std::setw(4) << voltronic::USBDevice::PRODUCT_ID << ")\n"
+ "\n";
+ std::cout.flags(f);
+ std::cout <<
+ "Serial device options:\n"
+ " --serial-name <NAME>: Path to serial device (default: " << voltronic::SerialDevice::DEVICE_NAME << ")\n"
+ " --serial-baud-rate 110|300|1200|2400|4800|9600|19200|38400|57600|115200\n"
+ " --serial-data-bits 5|6|7|8\n"
+ " --serial-stop-bits 1|1.5|2\n"
+ " --serial-parity none|odd|even|mark|space\n"
+ "\n"
+ "Commands:\n"
+ " get-protocol-id\n"
+ " get-date-time\n"
+ " get-total-generated\n"
+ " get-year-generated <yyyy>\n"
+ " get-month-generated <yyyy> <mm>\n"
+ " get-day-generated <yyyy> <mm> <dd>\n"
+ " get-series-number\n"
+ " get-cpu-version\n"
+ " get-rated\n"
+ " get-status\n"
+ " get-p-rated <id>\n"
+ " id: Parallel machine ID\n"
+ "\n"
+ " get-p-status <id>\n"
+ " id: Parallel machine ID\n"
+ "\n"
+ " get-mode\n"
+ " get-errors\n"
+ " get-flags\n"
+ " get-rated-defaults\n"
+ " get-allowed-charging-currents\n"
+ " get-allowed-ac-charging-currents\n"
+ " get-ac-charging-time\n"
+ " get-ac-loads-supply-time\n"
+ " set-loads-supply 0|1\n"
+ " set-flag <flag> 0|1\n"
+ " set-rated-defaults\n"
+ " set-max-charging-current <id> <amps>\n"
+ " id: Parallel machine ID (use 0 for single model)\n"
+ " amps: Use get-allowed-charging-currents\n"
+ " to see a list of allowed values.\n"
+ "\n"
+ " set-max-ac-charging-current <id> <amps>\n"
+ " id: Parallel machine ID (use 0 for single model)\n"
+ " amps: Use get-allowed-ac-charging-currents\n"
+ " to see a list of allowed values.\n"
+ "\n"
+ " set-ac-output-freq 50|60\n"
+ " set-max-charging-voltage <cv> <fv>\n"
+ " cv: Constant voltage (48.0 ~ 58.4).\n"
+ " fv: Float voltage (48.0 ~ 58.4).\n"
+ "\n"
+ " set-ac-output-voltage <v>\n"
+ " v: " << p18::ac_output_rated_voltages << "\n"
+ "\n"
+ " set-output-source-priority SUB|SBU\n"
+ " 'SUB' means " << p18::OutputSourcePriority::SolarUtilityBattery << "\n"
+ " 'SBU' means " << p18::OutputSourcePriority::SolarBatteryUtility << "\n"
+ "\n"
+ " set-charging-thresholds <cv> <dv>\n"
+ " Set battery re-charging and re-discharging voltages when\n"
+ " utility is available.\n"
+ "\n"
+ " cv: re-charging voltage\n"
+ " For 12 V unit: " << p18::bat_ac_recharging_voltages_12v << "\n"
+ " For 24 V unit: " << p18::bat_ac_recharging_voltages_24v << "\n"
+ " For 48 V unit: " << p18::bat_ac_recharging_voltages_48v << "\n"
+ "\n"
+ " dv: re-discharging voltage\n"
+ " For 12 V unit: " << p18::bat_ac_redischarging_voltages_12v << "\n"
+ " For 24 V unit: " << p18::bat_ac_redischarging_voltages_24v << "\n"
+ " For 48 V unit: " << p18::bat_ac_redischarging_voltages_48v << "\n"
+ "\n"
+ " set-charging-source-priority <id> <priority>\n"
+ " id: Parallel machine ID (use 0 for a single model)\n"
+ " priority: SF|SU|S\n"
+ " 'SF' means " << p18::ChargerSourcePriority::SolarFirst << ",\n"
+ " 'SU' means " << p18::ChargerSourcePriority::SolarAndUtility << "\n"
+ " 'S' means " << p18::ChargerSourcePriority::SolarOnly << "\n"
+ "\n"
+ " set-solar-power-priority BLU|LBU\n"
+ " 'BLU' means " << p18::SolarPowerPriority::BatteryLoadUtility << "\n"
+ " 'LBU' means " << p18::SolarPowerPriority::LoadBatteryUtility << "\n"
+ "\n"
+ " set-ac-input-voltage-range APPLIANCE|UPS\n"
+ " set-battery-type AGM|FLOODED|USER\n"
+ " set-output-model <id> <model>\n"
+ " id: Parallel machine ID (use 0 for a single model)\n"
+ " model: SM|P|P1|P2|P3\n"
+ " SM: " << p18::OutputModelSetting::SingleModule << "\n"
+ " P: " << p18::OutputModelSetting::ParallelOutput << "\n"
+ " P1: " << p18::OutputModelSetting::Phase1OfThreePhaseOutput << "\n"
+ " P2: " << p18::OutputModelSetting::Phase2OfThreePhaseOutput << "\n"
+ " P3: " << p18::OutputModelSetting::Phase3OfThreePhaseOutput << "\n"
+ "\n"
+ " set-battery-cut-off-voltage <v>\n"
+ " v: Cut-off voltage (40.0~48.0)\n"
+ "\n"
+ " set-solar-configuration <id>\n"
+ " id: Serial number\n"
+ "\n"
+ " clear-generated-data\n"
+ " Clear all recorded stats about generated energy.\n"
+ "\n"
+ " set-date-time <YYYY> <MM> <DD> <hh> <mm> <ss>\n"
+ " YYYY: Year\n"
+ " MM: Month\n"
+ " DD: Day\n"
+ " hh: Hours\n"
+ " mm: Minutes\n"
+ " ss: Seconds\n"
+ "\n"
+ " set-ac-charging-time <start> <end>\n"
+ " start: Starting time, hh:mm format\n"
+ " end: Ending time, hh:mm format\n"
+ "\n"
+ " set-ac-loads-supply-time <start> <end>\n"
+ " start: Starting time, hh:mm format\n"
+ " end: Ending time, hh:mm format\n"
+ "\n"
+ "Flags:\n";
+ for (const p18::Flag& flag: p18::flags)
+ std::cout << " " << flag.flag << ": " << flag.description << "\n";
+ std::cout <<
+ "\n"
+ "Formats:\n"
+ " table Human-readable table\n"
+ " simple-table Conveniently-parsable table\n"
+ " json JSON object or array\n";
+
+ exit(1);
+}
+
+static void output_formatted_error(formatter::Format format, std::exception& e, std::string s = "") {
+ std::ostringstream buf;
+ if (!s.empty())
+ buf << s << ": ";
+ buf << e.what();
+
+ auto err = p18::response_type::ErrorResponse(buf.str());
+ auto output = err.format(format);
+
+ if (format == formatter::Format::JSON) {
+ std::cout << *output;
+ } else {
+ std::cerr << *output << std::endl;
+ }
+}
+
+
+enum class Action {
+ ShortHelp,
+ FullHelp,
+ Raw,
+ Command,
+};
+
+int main(int argc, char *argv[]) {
+ if (argv[1] == nullptr)
+ short_usage(argv[0]);
+
+ // common params
+ Action action = Action::Command;
+ u64 timeout = voltronic::Device::TIMEOUT;
+ bool verbose = false;
+ p18::CommandType commandType;
+ std::vector<std::string> arguments;
+
+ // format params
+ bool formatChanged = false;
+ formatter::Format format = formatter::Format::Table;
+
+ // raw command param
+ std::string raw;
+
+ // device params
+ DeviceType deviceType = DeviceType::USB;
+
+ u16 usbVendorId = voltronic::USBDevice::VENDOR_ID;
+ u16 usbDeviceId = voltronic::USBDevice::PRODUCT_ID;
+
+ std::string serialDeviceName(voltronic::SerialDevice::DEVICE_NAME);
+ voltronic::SerialBaudRate serialBaudRate = voltronic::SerialDevice::BAUD_RATE;
+ voltronic::SerialDataBits serialDataBits = voltronic::SerialDevice::DATA_BITS;
+ voltronic::SerialStopBits serialStopBits = voltronic::SerialDevice::STOP_BITS;
+ voltronic::SerialParity serialParity = voltronic::SerialDevice::PARITY;
+
+ try {
+ int opt;
+ struct option long_options[] = {
+ {"help", no_argument, nullptr, LO_HELP},
+ {"verbose", no_argument, nullptr, LO_VERBOSE},
+ {"raw", required_argument, nullptr, LO_RAW},
+ {"timeout", required_argument, nullptr, LO_TIMEOUT},
+ {"format", required_argument, nullptr, LO_FORMAT},
+ {"device", required_argument, nullptr, LO_DEVICE},
+ {"usb-vendor-id", required_argument, nullptr, LO_USB_VENDOR_ID},
+ {"usb-device-id", required_argument, nullptr, LO_USB_DEVICE_ID},
+ {"serial-name", required_argument, nullptr, LO_SERIAL_NAME},
+ {"serial-baud-rate", required_argument, nullptr, LO_SERIAL_BAUD_RATE},
+ {"serial-data-bits", required_argument, nullptr, LO_SERIAL_DATA_BITS},
+ {"serial-stop-bits", required_argument, nullptr, LO_SERIAL_STOP_BITS},
+ {"serial-parity", required_argument, nullptr, LO_SERIAL_PARITY},
+ {nullptr, 0, nullptr, 0}
+ };
+
+ bool getoptError = false; // FIXME
+ while ((opt = getopt_long(argc, argv, "h", long_options, nullptr)) != EOF) {
+ if (opt == '?') {
+ getoptError = true;
+ break;
+ }
+
+ // simple options (flags), no arguments
+ switch (opt) {
+ case 'h': action = Action::ShortHelp; continue;
+ case LO_HELP: action = Action::FullHelp; continue;
+ case LO_VERBOSE: verbose = true; continue;
+ default: break;
+ }
+
+ // options with arguments
+ std::string arg;
+ if (optarg)
+ arg = std::string(optarg);
+
+ switch (opt) {
+ case LO_FORMAT:
+ format = format_from_string(arg);
+ formatChanged = true;
+ break;
+
+ case LO_DEVICE:
+ if (arg == "usb")
+ deviceType = DeviceType::USB;
+ else if (arg == "serial")
+ deviceType = DeviceType::Serial;
+ else if (arg == "pseudo")
+ deviceType = DeviceType::Pseudo;
+ else
+ throw std::invalid_argument("invalid device");
+
+ break;
+
+ case LO_RAW:
+ raw = arg;
+ if (raw.size() > MAX_RAW_COMMAND_LENGTH)
+ throw std::invalid_argument("command is too long");
+ action = Action::Raw;
+ break;
+
+ case LO_TIMEOUT:
+ timeout = std::stoull(arg);
+ break;
+
+ case LO_USB_VENDOR_ID:
+ try {
+ if (arg.size() != 4)
+ throw std::invalid_argument("usb-vendor-id: invalid length");
+ usbVendorId = static_cast<u16>(hextoul(arg));
+ } catch (std::invalid_argument& e) {
+ throw std::invalid_argument(std::string("usb-vendor-id: invalid format: ") + e.what());
+ }
+ break;
+
+ case LO_USB_DEVICE_ID:
+ try {
+ if (arg.size() != 4)
+ throw std::invalid_argument("usb-device-id: invalid length");
+ usbDeviceId = static_cast<u16>(hextoul(arg));
+ } catch (std::invalid_argument& e) {
+ throw std::invalid_argument(std::string("usb-device-id: invalid format: ") + e.what());
+ }
+ break;
+
+ case LO_SERIAL_NAME:
+ serialDeviceName = arg;
+ break;
+
+ case LO_SERIAL_BAUD_RATE:
+ serialBaudRate = static_cast<voltronic::SerialBaudRate>(std::stoul(arg));
+ if (!voltronic::is_serial_baud_rate_valid(serialBaudRate))
+ throw std::invalid_argument("invalid serial baud rate");
+ break;
+
+ case LO_SERIAL_DATA_BITS:
+ serialDataBits = static_cast<voltronic::SerialDataBits>(std::stoul(arg));
+ if (voltronic::is_serial_data_bits_valid(serialDataBits))
+ throw std::invalid_argument("invalid serial data bits");
+ break;
+
+ case LO_SERIAL_STOP_BITS:
+ if (arg == "1")
+ serialStopBits = voltronic::SerialStopBits::One;
+ else if (arg == "1.5")
+ serialStopBits = voltronic::SerialStopBits::OneAndHalf;
+ else if (arg == "2")
+ serialStopBits = voltronic::SerialStopBits::Two;
+ else
+ throw std::invalid_argument("invalid serial stop bits");
+ break;
+
+ case LO_SERIAL_PARITY:
+ if (arg == "none")
+ serialParity = voltronic::SerialParity::None;
+ else if (arg == "odd")
+ serialParity = voltronic::SerialParity::Odd;
+ else if (arg == "even")
+ serialParity = voltronic::SerialParity::Even;
+ else if (arg == "mark")
+ serialParity = voltronic::SerialParity::Mark;
+ else if (arg == "space")
+ serialParity = voltronic::SerialParity::Space;
+ else
+ throw std::invalid_argument("invalid serial parity");
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ switch (action) {
+ case Action::ShortHelp:
+ short_usage(argv[0]);
+ break;
+
+ case Action::FullHelp:
+ usage(argv[0]);
+ break;
+
+ case Action::Command: {
+ if (argc <= optind)
+ throw std::invalid_argument("missing command");
+
+ std::string command = argv[optind++];
+
+ p18::CommandInput input{argc, argv};
+ commandType = p18::validate_input(command, arguments, (void*)&input);
+ break;
+ }
+
+ case Action::Raw:
+ if (formatChanged)
+ throw std::invalid_argument("--format is not allowed with --raw");
+ break;
+ }
+
+ if (optind < argc)
+ throw std::invalid_argument("extra parameter found");
+ } catch (std::invalid_argument& e) {
+ output_formatted_error(format, e);
+ return 1;
+ }
+
+ bool success = false;
+ try {
+ std::shared_ptr<voltronic::Device> dev;
+ switch (deviceType) {
+ case DeviceType::USB:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::USBDevice(usbVendorId,
+ usbDeviceId));
+ break;
+
+ case DeviceType::Pseudo:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::PseudoDevice);
+ break;
+
+ case DeviceType::Serial:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::SerialDevice(serialDeviceName,
+ serialBaudRate,
+ serialDataBits,
+ serialStopBits,
+ serialParity));
+ break;
+ }
+
+ dev->setVerbose(verbose);
+ dev->setTimeout(timeout);
+
+ p18::Client client;
+ client.setDevice(dev);
+
+ if (action == Action::Raw) {
+ auto result = client.runOnDevice(raw);
+ if (verbose)
+ std::cerr << hexdump(result.first.get(), result.second);
+ std::cout << std::string(result.first.get(), result.second) << std::endl;
+ } else {
+ auto response = client.execute(commandType, arguments);
+ std::cout << *(response->format(format).get()) << std::endl;
+ }
+
+ success = true;
+ }
+ catch (voltronic::DeviceError& e) {
+ output_formatted_error(format, e, "device error");
+ }
+ catch (voltronic::TimeoutError& e) {
+ output_formatted_error(format, e, "timeout");
+ }
+ catch (voltronic::InvalidDataError& e) {
+ output_formatted_error(format, e, "data is invalid");
+ }
+ catch (p18::InvalidResponseError& e) {
+ output_formatted_error(format, e, "response is invalid");
+ }
+
+ return success ? 1 : 0;
+} \ No newline at end of file
diff --git a/src/inverterd.cc b/src/inverterd.cc
new file mode 100644
index 0000000..1357454
--- /dev/null
+++ b/src/inverterd.cc
@@ -0,0 +1,292 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <iostream>
+#include <string>
+#include <vector>
+#include <iomanip>
+#include <ios>
+#include <getopt.h>
+
+#include "common.h"
+#include "voltronic/device.h"
+#include "voltronic/exceptions.h"
+#include "p18/exceptions.h"
+#include "util.h"
+#include "logging.h"
+#include "server/server.h"
+#include "server/signal.h"
+
+static const char* DEFAULT_HOST = "127.0.0.1";
+static int DEFAULT_PORT = 8305;
+
+static void usage(const char* progname) {
+ std::cout << "Usage: " << progname << " OPTIONS [COMMAND]\n" <<
+ "\n"
+ "Options:\n"
+ " -h, --help: Show this help\n"
+ " --host <HOST>: Server host (default: " << DEFAULT_HOST << ")\n"
+ " --port <PORT> Server port (default: " << DEFAULT_PORT << ")\n"
+ " --device <DEVICE>: 'usb' (default), 'serial' or 'pseudo'\n"
+ " --timeout <TIMEOUT>: Device timeout in ms (default: " << voltronic::Device::TIMEOUT << ")\n"
+ " --cache-timeout <TIMEOUT>"
+ " Cache validity time, in ms (default: " << server::Server::CACHE_TIMEOUT << ")\n"
+ " --verbose: Be verbose\n"
+ "\n";
+
+ std::ios_base::fmtflags f(std::cout.flags());
+ std::cout << std::hex << std::setfill('0') <<
+ "USB device options:\n"
+ " --usb-vendor-id <ID>: Vendor ID (default: " << std::setw(4) << voltronic::USBDevice::VENDOR_ID << ")\n"
+ " --usb-device-id <ID>: Device ID (default: " << std::setw(4) << voltronic::USBDevice::PRODUCT_ID << ")\n";
+ std::cout.flags(f);
+
+ std::cout << "\n"
+ "Serial device options:\n"
+ " --serial-name <NAME>: Path to serial device (default: " << voltronic::SerialDevice::DEVICE_NAME << ")\n"
+ " --serial-baud-rate 110|300|1200|2400|4800|9600|19200|38400|57600|115200\n"
+ " --serial-data-bits 5|6|7|8\n"
+ " --serial-stop-bits 1|1.5|2\n"
+ " --serial-parity none|odd|even|mark|space\n";
+ exit(1);
+}
+
+
+int main(int argc, char *argv[]) {
+ // common params
+ uint64_t timeout = voltronic::Device::TIMEOUT;
+ uint64_t cacheTimeout = server::Server::CACHE_TIMEOUT;
+ bool verbose = false;
+
+ // server params
+ std::string host(DEFAULT_HOST);
+ int port = DEFAULT_PORT;
+
+ // device params
+ DeviceType deviceType = DeviceType::USB;
+
+ unsigned short usbVendorId = voltronic::USBDevice::VENDOR_ID;
+ unsigned short usbDeviceId = voltronic::USBDevice::PRODUCT_ID;
+
+ std::string serialDeviceName(voltronic::SerialDevice::DEVICE_NAME);
+ voltronic::SerialBaudRate serialBaudRate = voltronic::SerialDevice::BAUD_RATE;
+ voltronic::SerialDataBits serialDataBits = voltronic::SerialDevice::DATA_BITS;
+ voltronic::SerialStopBits serialStopBits = voltronic::SerialDevice::STOP_BITS;
+ voltronic::SerialParity serialParity = voltronic::SerialDevice::PARITY;
+
+ try {
+ int opt;
+ struct option long_options[] = {
+ {"help", no_argument, nullptr, 'h'},
+ {"verbose", no_argument, nullptr, LO_VERBOSE},
+ {"timeout", required_argument, nullptr, LO_TIMEOUT},
+ {"cache-timeout", required_argument, nullptr, LO_CACHE_TIMEOUT},
+ {"device", required_argument, nullptr, LO_DEVICE},
+ {"usb-vendor-id", required_argument, nullptr, LO_USB_VENDOR_ID},
+ {"usb-device-id", required_argument, nullptr, LO_USB_DEVICE_ID},
+ {"serial-name", required_argument, nullptr, LO_SERIAL_NAME},
+ {"serial-baud-rate", required_argument, nullptr, LO_SERIAL_BAUD_RATE},
+ {"serial-data-bits", required_argument, nullptr, LO_SERIAL_DATA_BITS},
+ {"serial-stop-bits", required_argument, nullptr, LO_SERIAL_STOP_BITS},
+ {"serial-parity", required_argument, nullptr, LO_SERIAL_PARITY},
+ {"host", required_argument, nullptr, LO_HOST},
+ {"port", required_argument, nullptr, LO_PORT},
+ {nullptr, 0, nullptr, 0}
+ };
+
+ bool getoptError = false; // FIXME
+ while ((opt = getopt_long(argc, argv, "h", long_options, nullptr)) != EOF) {
+ if (opt == '?') {
+ getoptError = true;
+ break;
+ }
+
+ // simple options (flags), no arguments
+ switch (opt) {
+ case 'h':
+ usage(argv[0]);
+
+ case LO_VERBOSE:
+ verbose = true;
+ continue;
+
+ default:
+ break;
+ }
+
+ // options with arguments
+ std::string arg;
+ if (optarg)
+ arg = std::string(optarg);
+
+ switch (opt) {
+ case LO_DEVICE:
+ if (arg == "usb")
+ deviceType = DeviceType::USB;
+ else if (arg == "serial")
+ deviceType = DeviceType::Serial;
+ else if (arg == "pseudo")
+ deviceType = DeviceType::Pseudo;
+ else
+ throw std::invalid_argument("invalid device");
+
+ break;
+
+ case LO_TIMEOUT:
+ timeout = std::stoull(arg);
+ break;
+
+ case LO_CACHE_TIMEOUT:
+ cacheTimeout = std::stoull(arg);
+ break;
+
+ case LO_USB_VENDOR_ID:
+ try {
+ if (arg.size() != 4)
+ throw std::invalid_argument("usb-vendor-id: invalid length");
+ usbVendorId = static_cast<unsigned short>(hextoul(arg));
+ } catch (std::invalid_argument& e) {
+ throw std::invalid_argument(std::string("usb-vendor-id: invalid format: ") + e.what());
+ }
+ break;
+
+ case LO_USB_DEVICE_ID:
+ try {
+ if (arg.size() != 4)
+ throw std::invalid_argument("usb-device-id: invalid length");
+ usbDeviceId = static_cast<unsigned short>(hextoul(arg));
+ } catch (std::invalid_argument& e) {
+ throw std::invalid_argument(std::string("usb-device-id: invalid format: ") + e.what());
+ }
+ break;
+
+ case LO_SERIAL_NAME:
+ serialDeviceName = arg;
+ break;
+
+ case LO_SERIAL_BAUD_RATE:
+ serialBaudRate = static_cast<voltronic::SerialBaudRate>(std::stoul(arg));
+ if (!voltronic::is_serial_baud_rate_valid(serialBaudRate))
+ throw std::invalid_argument("invalid serial baud rate");
+ break;
+
+ case LO_SERIAL_DATA_BITS:
+ serialDataBits = static_cast<voltronic::SerialDataBits>(std::stoul(arg));
+ if (voltronic::is_serial_data_bits_valid(serialDataBits))
+ throw std::invalid_argument("invalid serial data bits");
+ break;
+
+ case LO_SERIAL_STOP_BITS:
+ if (arg == "1")
+ serialStopBits = voltronic::SerialStopBits::One;
+ else if (arg == "1.5")
+ serialStopBits = voltronic::SerialStopBits::OneAndHalf;
+ else if (arg == "2")
+ serialStopBits = voltronic::SerialStopBits::Two;
+ else
+ throw std::invalid_argument("invalid serial stop bits");
+ break;
+
+ case LO_SERIAL_PARITY:
+ if (arg == "none")
+ serialParity = voltronic::SerialParity::None;
+ else if (arg == "odd")
+ serialParity = voltronic::SerialParity::Odd;
+ else if (arg == "even")
+ serialParity = voltronic::SerialParity::Even;
+ else if (arg == "mark")
+ serialParity = voltronic::SerialParity::Mark;
+ else if (arg == "space")
+ serialParity = voltronic::SerialParity::Space;
+ else
+ throw std::invalid_argument("invalid serial parity");
+ break;
+
+ case LO_HOST:
+ host = arg;
+ break;
+
+ case LO_PORT:
+ port = std::stoi(arg);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ if (optind < argc)
+ throw std::invalid_argument("extra parameter found");
+ } catch (std::invalid_argument& e) {
+ myerr << "error: " << e.what();
+ return 1;
+ }
+
+ // open device
+ std::shared_ptr<voltronic::Device> dev;
+ try {
+ switch (deviceType) {
+ case DeviceType::USB:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::USBDevice(usbVendorId, usbDeviceId));
+ break;
+
+ case DeviceType::Pseudo:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::PseudoDevice);
+ break;
+
+ case DeviceType::Serial:
+ dev = std::shared_ptr<voltronic::Device>(new voltronic::SerialDevice(serialDeviceName,
+ serialBaudRate,
+ serialDataBits,
+ serialStopBits,
+ serialParity));
+ break;
+ }
+
+ dev->setTimeout(timeout);
+
+// p18::Client client;
+// client.setDevice(dev);
+
+ /*if (action == Action::Raw) {
+ auto result = client.runOnDevice(raw);
+ if (verbose)
+ std::cerr << hexdump(result.first.get(), result.second);
+ std::cout << std::string(result.first.get(), result.second) << std::endl;
+ } else {
+ auto response = client.execute(commandType, arguments);
+ std::cout << *(response->format(format).get()) << std::endl;
+ }*/
+
+// success = true;
+ }
+ catch (voltronic::DeviceError& e) {
+ myerr << "device error: " << e.what();
+ return 1;
+ }
+ catch (voltronic::TimeoutError& e) {
+ myerr << "timeout error: " << e.what();
+ return 1;
+ }
+ catch (voltronic::InvalidDataError& e) {
+ myerr << "data is invalid: " << e.what();
+ return 1;
+ }
+ catch (p18::InvalidResponseError& e) {
+ myerr << "response is invalid: " << e.what();
+ return 1;
+ }
+
+ // create server
+ server::set_signal_handlers();
+
+ server::Server server(dev);
+ server.setVerbose(verbose);
+ server.setCacheTimeout(cacheTimeout);
+
+ server.start(host, port);
+
+ if (verbose)
+ mylog << "done";
+
+ return 0;
+} \ No newline at end of file
diff --git a/src/logging.h b/src/logging.h
new file mode 100644
index 0000000..2e84198
--- /dev/null
+++ b/src/logging.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_LOGGING_H
+#define INVERTER_TOOLS_LOGGING_H
+
+#include <iostream>
+#include <string>
+#include <string_view>
+
+class custom_log
+{
+private:
+ std::ostream& os_;
+
+public:
+ custom_log(std::ostream& os, const std::string& func) : os_(os) {
+ os_ << func << ": ";
+ }
+
+ template <class T>
+ custom_log &operator<<(const T &v) {
+ os_ << v;
+ return *this;
+ }
+
+ ~custom_log() {
+ os_ << std::endl;
+ }
+};
+
+inline std::string method_name(const std::string& function, const std::string& pretty) {
+ size_t locFunName = pretty.find(function);
+ size_t begin = pretty.rfind(" ", locFunName) + 1;
+ size_t end = pretty.find("(", locFunName + function.length());
+ return pretty.substr(begin, end - begin) + "()";
+ }
+
+#define __METHOD_NAME__ method_name(__FUNCTION__, __PRETTY_FUNCTION__)
+
+#define mylog custom_log(std::cout, __METHOD_NAME__)
+#define myerr custom_log(std::cerr, __METHOD_NAME__)
+
+#endif //INVERTER_TOOLS_LOGGING_H
diff --git a/src/numeric_types.h b/src/numeric_types.h
new file mode 100644
index 0000000..b581228
--- /dev/null
+++ b/src/numeric_types.h
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_NUMERIC_TYPES_H
+#define INVERTER_TOOLS_NUMERIC_TYPES_H
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint64_t u64;
+
+#endif //INVERTER_TOOLS_NUMERIC_TYPES_H
diff --git a/src/p18/client.cc b/src/p18/client.cc
new file mode 100644
index 0000000..9baae1a
--- /dev/null
+++ b/src/p18/client.cc
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <memory>
+#include <utility>
+#include <sstream>
+#include <iomanip>
+#include <cmath>
+#include <stdexcept>
+
+#include "client.h"
+#include "types.h"
+#include "defines.h"
+#include "exceptions.h"
+#include "response.h"
+#include "../voltronic/crc.h"
+
+#define MKRESPONSE(type) std::shared_ptr<response_type::BaseResponse>(new response_type::type(raw, rawSize))
+
+#define RESPONSE_CASE(type) \
+ case CommandType::Get ## type: \
+ response = MKRESPONSE(type); \
+ break; \
+
+
+namespace p18 {
+
+void Client::setDevice(std::shared_ptr<voltronic::Device> device) {
+ device_ = std::move(device);
+}
+
+std::shared_ptr<response_type::BaseResponse> Client::execute(p18::CommandType commandType, std::vector<std::string>& arguments) {
+ std::ostringstream buf;
+ buf << std::setfill('0');
+
+ int iCommandType = static_cast<int>(commandType);
+ bool isSetCommand = iCommandType >= 100;
+
+ auto pos = raw_commands.find(commandType);
+ if (pos == raw_commands.end())
+ throw std::runtime_error("packedCommand " + std::to_string(iCommandType) + " not found");
+
+ std::string packedCommand = pos->second;
+ std::string packedArguments = packArguments(commandType, arguments);
+
+ size_t len = sizeof(voltronic::CRC) + 1 + packedCommand.size() + packedArguments.size();
+
+ buf << "^";
+ buf << (isSetCommand ? "S" : "P");
+ buf << std::setw(3) << len;
+ buf << packedCommand;
+ buf << packedArguments;
+
+ std::string packed = buf.str();
+
+ auto result = runOnDevice(packed);
+ std::shared_ptr<response_type::BaseResponse> response;
+
+ const auto raw = result.first;
+ const auto rawSize = result.second;
+
+ switch (commandType) {
+ RESPONSE_CASE(ProtocolID)
+ RESPONSE_CASE(CurrentTime)
+ RESPONSE_CASE(TotalGenerated)
+ RESPONSE_CASE(YearGenerated)
+ RESPONSE_CASE(MonthGenerated)
+ RESPONSE_CASE(DayGenerated)
+ RESPONSE_CASE(SeriesNumber)
+ RESPONSE_CASE(CPUVersion)
+ RESPONSE_CASE(RatedInformation)
+ RESPONSE_CASE(GeneralStatus)
+ RESPONSE_CASE(WorkingMode)
+ RESPONSE_CASE(FaultsAndWarnings)
+ RESPONSE_CASE(FlagsAndStatuses)
+ RESPONSE_CASE(Defaults)
+ RESPONSE_CASE(AllowedChargingCurrents)
+ RESPONSE_CASE(AllowedACChargingCurrents)
+ RESPONSE_CASE(ParallelRatedInformation)
+ RESPONSE_CASE(ParallelGeneralStatus)
+ RESPONSE_CASE(ACChargingTimeBucket)
+ RESPONSE_CASE(ACLoadsSupplyTimeBucket)
+
+ case CommandType::SetLoads:
+ case CommandType::SetFlag:
+ case CommandType::SetDefaults:
+ case CommandType::SetBatteryMaxChargingCurrent:
+ case CommandType::SetBatteryMaxACChargingCurrent:
+ case CommandType::SetACOutputFreq:
+ case CommandType::SetBatteryMaxChargingVoltage:
+ case CommandType::SetACOutputRatedVoltage:
+ case CommandType::SetOutputSourcePriority:
+ case CommandType::SetBatteryChargingThresholds:
+ case CommandType::SetChargingSourcePriority:
+ case CommandType::SetSolarPowerPriority:
+ case CommandType::SetACInputVoltageRange:
+ case CommandType::SetBatteryType:
+ case CommandType::SetOutputModel:
+ case CommandType::SetBatteryCutOffVoltage:
+ case CommandType::SetSolarConfig:
+ case CommandType::ClearGenerated:
+ case CommandType::SetDateTime:
+ case CommandType::SetACChargingTimeBucket:
+ case CommandType::SetACLoadsSupplyTimeBucket:
+ response = MKRESPONSE(SetResponse);
+ break;
+ }
+
+ try {
+ if (!response->validate())
+ throw InvalidResponseError("validate() failed");
+
+ response->unpack();
+ } catch (InvalidResponseError& e) {
+ return std::make_unique<response_type::ErrorResponse>(e.what());
+ }
+
+ return std::move(response);
+}
+
+std::pair<std::shared_ptr<char>, size_t> Client::runOnDevice(std::string& raw) {
+ size_t bufSize = 256;
+ std::shared_ptr<char> buf(new char[bufSize]);
+ size_t responseSize = device_->run(
+ (const u8*)raw.c_str(), raw.size(),
+ (u8*)buf.get(), bufSize);
+
+ return std::pair<std::shared_ptr<char>, size_t>(buf, responseSize);
+}
+
+std::string Client::packArguments(p18::CommandType commandType, std::vector<std::string>& arguments) {
+ std::ostringstream buf;
+ buf << std::setfill('0');
+
+ switch (commandType) {
+ case CommandType::GetYearGenerated:
+ case CommandType::SetOutputSourcePriority:
+ case CommandType::SetSolarPowerPriority:
+ case CommandType::SetACInputVoltageRange:
+ case CommandType::SetBatteryType:
+ case CommandType::SetLoads:
+ buf << arguments[0];
+ break;
+
+ case CommandType::GetMonthGenerated:
+ case CommandType::GetDayGenerated:
+ buf << arguments[0];
+ for (int i = 1; i <= (commandType == CommandType::GetMonthGenerated ? 1 : 2); i++)
+ buf << std::setw(2) << std::stoi(arguments[i]);
+ break;
+
+ case CommandType::GetParallelGeneralStatus:
+ case CommandType::GetParallelRatedInformation:
+ buf << std::stoi(arguments[0]);
+ break;
+
+ case CommandType::SetFlag:
+ buf << (arguments[1] == "1" ? "E" : "D");
+ buf << arguments[0];
+ break;
+
+ case CommandType::SetBatteryMaxChargingCurrent:
+ case CommandType::SetBatteryMaxACChargingCurrent:
+ buf << arguments[0] << ",";
+ buf << std::setw(3) << std::stoi(arguments[1]);
+ break;
+
+ case CommandType::SetACOutputFreq:
+ buf << std::setw(2) << std::stoi(arguments[0]);
+ break;
+
+ case CommandType::SetBatteryMaxChargingVoltage:
+ case CommandType::SetBatteryChargingThresholds: {
+ for (int i = 0; i < 2; i++) {
+ double val = std::stod(arguments[i]);
+ buf << std::setw(3) << (int)round(val*10);
+ if (i == 0)
+ buf << ",";
+ }
+ break;
+ }
+
+ case CommandType::SetACOutputRatedVoltage: {
+ buf << std::setw(4) << (std::stoi(arguments[0])*10);
+ break;
+ }
+
+ case CommandType::SetChargingSourcePriority:
+ case CommandType::SetOutputModel:
+ buf << arguments[0] << "," << arguments[1];
+ break;
+
+ case CommandType::SetBatteryCutOffVoltage: {
+ double v = std::stod(arguments[0]);
+ buf << std::setw(3) << ((int)round(v*10));
+ break;
+ }
+
+ case CommandType::SetSolarConfig: {
+ size_t len = arguments[0].size();
+ buf << std::setw(2) << len << arguments[0];
+ if (len < 20) {
+ for (int i = 0; i < 20-len; i++)
+ buf << "0";
+ }
+ break;
+ }
+
+ case CommandType::SetDateTime: {
+ for (int i = 0; i < 6; i++) {
+ int val = std::stoi(arguments[0]);
+ if (i == 0)
+ val -= 2000;
+ buf << std::setw(2) << val;
+ }
+ break;
+ }
+
+ case CommandType::SetACChargingTimeBucket:
+ case CommandType::SetACLoadsSupplyTimeBucket:
+ for (int i = 0; i < 4; i++) {
+ buf << std::setw(2) << std::stoi(arguments[i]);
+ if (i == 1)
+ buf << ",";
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return buf.str();
+}
+
+} \ No newline at end of file
diff --git a/src/p18/client.h b/src/p18/client.h
new file mode 100644
index 0000000..8307bbb
--- /dev/null
+++ b/src/p18/client.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_P18_CLIENT_H_
+#define INVERTER_TOOLS_P18_CLIENT_H_
+
+#include "../voltronic/device.h"
+#include "types.h"
+#include "response.h"
+
+#include <memory>
+#include <vector>
+#include <string>
+
+
+namespace p18 {
+
+class Client {
+private:
+ std::shared_ptr<voltronic::Device> device_;
+ static std::string packArguments(p18::CommandType commandType, std::vector<std::string>& arguments);
+
+public:
+ void setDevice(std::shared_ptr<voltronic::Device> device);
+ std::shared_ptr<response_type::BaseResponse> execute(p18::CommandType commandType, std::vector<std::string>& arguments);
+ std::pair<std::shared_ptr<char>, size_t> runOnDevice(std::string& raw);
+};
+
+}
+
+
+#endif //INVERTER_TOOLS_P18_CLIENT_H_
diff --git a/src/p18/commands.cc b/src/p18/commands.cc
new file mode 100644
index 0000000..0d37566
--- /dev/null
+++ b/src/p18/commands.cc
@@ -0,0 +1,455 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <stdexcept>
+#include <sstream>
+#include <vector>
+#include <string>
+
+#ifdef INVERTERCTL
+#include <getopt.h>
+#endif
+
+#include "commands.h"
+#include "defines.h"
+#include "functions.h"
+#include "../util.h"
+#include "../logging.h"
+
+namespace p18 {
+
+const std::map<std::string, p18::CommandType> client_commands = {
+ {"get-protocol-id", p18::CommandType::GetProtocolID},
+ {"get-date-time", p18::CommandType::GetCurrentTime},
+ {"get-total-generated", p18::CommandType::GetTotalGenerated},
+ {"get-year-generated", p18::CommandType::GetYearGenerated},
+ {"get-month-generated", p18::CommandType::GetMonthGenerated},
+ {"get-day-generated", p18::CommandType::GetDayGenerated},
+ {"get-series-number", p18::CommandType::GetSeriesNumber},
+ {"get-cpu-version", p18::CommandType::GetCPUVersion},
+ {"get-rated", p18::CommandType::GetRatedInformation},
+ {"get-status", p18::CommandType::GetGeneralStatus},
+ {"get-mode", p18::CommandType::GetWorkingMode},
+ {"get-errors", p18::CommandType::GetFaultsAndWarnings},
+ {"get-flags", p18::CommandType::GetFlagsAndStatuses},
+ {"get-rated-defaults", p18::CommandType::GetDefaults},
+ {"get-allowed-charging-currents", p18::CommandType::GetAllowedChargingCurrents},
+ {"get-allowed-ac-charging-currents", p18::CommandType::GetAllowedACChargingCurrents},
+ {"get-p-rated", p18::CommandType::GetParallelRatedInformation},
+ {"get-p-status", p18::CommandType::GetParallelGeneralStatus},
+ {"get-ac-charging-time", p18::CommandType::GetACChargingTimeBucket},
+ {"get-ac-loads-supply-time", p18::CommandType::GetACLoadsSupplyTimeBucket},
+ {"set-loads-supply", p18::CommandType::SetLoads},
+ {"set-flag", p18::CommandType::SetFlag},
+ {"set-rated-defaults", p18::CommandType::SetDefaults},
+ {"set-max-charging-current", p18::CommandType::SetBatteryMaxChargingCurrent},
+ {"set-max-ac-charging-current", p18::CommandType::SetBatteryMaxACChargingCurrent},
+ {"set-ac-output-freq", p18::CommandType::SetACOutputFreq},
+ {"set-max-charging-voltage", p18::CommandType::SetBatteryMaxChargingVoltage},
+ {"set-ac-output-voltage", p18::CommandType::SetACOutputRatedVoltage},
+ {"set-output-source-priority", p18::CommandType::SetOutputSourcePriority},
+ {"set-charging-thresholds", p18::CommandType::SetBatteryChargingThresholds}, /* Battery re-charging and re-discharging voltage when utility is available */
+ {"set-charging-source-priority", p18::CommandType::SetChargingSourcePriority},
+ {"set-solar-power-priority", p18::CommandType::SetSolarPowerPriority},
+ {"set-ac-input-voltage-range", p18::CommandType::SetACInputVoltageRange},
+ {"set-battery-type", p18::CommandType::SetBatteryType},
+ {"set-output-model", p18::CommandType::SetOutputModel},
+ {"set-battery-cut-off-voltage", p18::CommandType::SetBatteryCutOffVoltage},
+ {"set-solar-configuration", p18::CommandType::SetSolarConfig},
+ {"clear-generated-data", p18::CommandType::ClearGenerated},
+ {"set-date-time", p18::CommandType::SetDateTime},
+ {"set-ac-charging-time", p18::CommandType::SetACChargingTimeBucket},
+ {"set-ac-loads-supply-time", p18::CommandType::SetACLoadsSupplyTimeBucket},
+};
+
+static void validate_date_args(const std::string* ys, const std::string* ms, const std::string* ds) {
+ static const std::string err_year = "invalid year";
+ static const std::string err_month = "invalid month";
+ static const std::string err_day = "invalid day";
+
+ int y, m = 0, d = 0;
+
+ // validate year
+ if (!is_numeric(*ys) || ys->size() != 4)
+ throw std::invalid_argument(err_year);
+
+ y = std::stoi(*ys);
+ if (y < 2000 || y > 2099)
+ throw std::invalid_argument(err_year);
+
+ // validate month
+ if (ms != nullptr) {
+ if (!is_numeric(*ms) || ms->size() > 2)
+ throw std::invalid_argument(err_month);
+
+ m = std::stoi(*ms);
+ if (m < 1 || m > 12)
+ throw std::invalid_argument(err_month);
+ }
+
+ // validate day
+ if (ds != nullptr) {
+ if (!is_numeric(*ds) || ds->size() > 2)
+ throw std::invalid_argument(err_day);
+
+ d = std::stoi(*ds);
+ if (d < 1 || d > 31)
+ throw std::invalid_argument(err_day);
+ }
+
+ if (y != 0 && m != 0 && d != 0) {
+ if (!is_date_valid(y, m, d))
+ throw std::invalid_argument("invalid date");
+ }
+}
+
+static void validate_time_args(const std::string* hs, const std::string* ms, const std::string* ss) {
+ static const std::string err_hour = "invalid hour";
+ static const std::string err_minute = "invalid minute";
+ static const std::string err_second = "invalid second";
+
+ unsigned h, m, s;
+
+ if (!is_numeric(*hs) || hs->size() > 2)
+ throw std::invalid_argument(err_hour);
+
+ h = static_cast<unsigned>(std::stoul(*hs));
+ if (h > 23)
+ throw std::invalid_argument(err_hour);
+
+ if (!is_numeric(*ms) || ms->size() > 2)
+ throw std::invalid_argument(err_minute);
+
+ m = static_cast<unsigned>(std::stoul(*ms));
+ if (m > 59)
+ throw std::invalid_argument(err_minute);
+
+ if (!is_numeric(*ss) || ss->size() > 2)
+ throw std::invalid_argument(err_second);
+
+ s = static_cast<unsigned>(std::stoul(*ss));
+ if (s > 59)
+ throw std::invalid_argument(err_second);
+}
+
+
+#define GET_ARGS(__len__) get_args((CommandInput*)input, arguments, (__len__))
+
+#ifdef INVERTERCTL
+static void get_args(CommandInput* input,
+ std::vector<std::string>& arguments,
+ size_t count) {
+ for (size_t i = 0; i < count; i++) {
+ if (optind < input->argc && *input->argv[optind] != '-')
+ arguments.emplace_back(input->argv[optind++]);
+ else {
+ std::ostringstream error;
+ error << "this command requires " << count << " argument";
+ if (count > 1)
+ error << "s";
+ throw std::invalid_argument(error.str());
+ }
+ }
+}
+#endif
+
+#ifdef INVERTERD
+static void get_args(CommandInput* input,
+ std::vector<std::string>& arguments,
+ size_t count) {
+ if (input->argv->size() < count) {
+ std::ostringstream error;
+ error << "this command requires " << count << " argument";
+ if (count > 1)
+ error << "s";
+ throw std::invalid_argument(error.str());
+ }
+
+ for (size_t i = 0; i < count; i++)
+ arguments.emplace_back((*input->argv)[i]);
+}
+#endif
+
+p18::CommandType validate_input(std::string& command,
+ std::vector<std::string>& arguments,
+ void* input) {
+ auto it = p18::client_commands.find(command);
+ if (it == p18::client_commands.end())
+ throw std::invalid_argument("invalid command");
+
+ auto commandType = it->second;
+ switch (commandType) {
+ case p18::CommandType::GetYearGenerated:
+ GET_ARGS(1);
+ validate_date_args(&arguments[0], nullptr, nullptr);
+ break;
+
+ case p18::CommandType::GetMonthGenerated:
+ GET_ARGS(2);
+ validate_date_args(&arguments[0], &arguments[1], nullptr);
+ break;
+
+ case p18::CommandType::GetDayGenerated:
+ GET_ARGS(3);
+ validate_date_args(&arguments[0], &arguments[1], &arguments[2]);
+ break;
+
+ case p18::CommandType::GetParallelRatedInformation:
+ case p18::CommandType::GetParallelGeneralStatus:
+ GET_ARGS(1);
+ if (!is_numeric(arguments[0]) || arguments[0].size() > 1)
+ throw std::invalid_argument("invalid argument");
+ break;
+
+ case p18::CommandType::SetLoads: {
+ GET_ARGS(1);
+ std::string &arg = arguments[0];
+ if (arg != "0" && arg != "1")
+ throw std::invalid_argument("invalid argument, only 0 or 1 allowed");
+ break;
+ }
+
+ case p18::CommandType::SetFlag: {
+ GET_ARGS(2);
+
+ bool match_found = false;
+ for (auto const& item: p18::flags) {
+ if (arguments[0] == item.flag) {
+ arguments[0] = item.letter;
+ match_found = true;
+ break;
+ }
+ }
+
+ if (!match_found)
+ throw std::invalid_argument("invalid flag");
+
+ if (arguments[1] != "0" && arguments[1] != "1")
+ throw std::invalid_argument("invalid flag state, only 0 or 1 allowed");
+
+ break;
+ }
+
+ case p18::CommandType::SetBatteryMaxChargingCurrent:
+ case p18::CommandType::SetBatteryMaxACChargingCurrent: {
+ GET_ARGS(2);
+
+ auto id = static_cast<unsigned>(std::stoul(arguments[0]));
+ auto amps = static_cast<unsigned>(std::stoul(arguments[1]));
+
+ if (!p18::is_valid_parallel_id(id))
+ throw std::invalid_argument("invalid id");
+
+ // 3 characters max
+ if (amps > 999)
+ throw std::invalid_argument("invalid amps");
+
+ break;
+ }
+
+ case p18::CommandType::SetACOutputFreq: {
+ GET_ARGS(1);
+ std::string &freq = arguments[0];
+ if (freq != "50" && freq != "60")
+ throw std::invalid_argument("invalid frequency, only 50 or 60 allowed");
+ break;
+ }
+
+ case p18::CommandType::SetBatteryMaxChargingVoltage: {
+ GET_ARGS(2);
+
+ float cv = std::stof(arguments[0]);
+ float fv = std::stof(arguments[1]);
+
+ if (cv < 48.0 || cv > 58.4)
+ throw std::invalid_argument("invalid CV");
+
+ if (fv < 48.0 || fv > 58.4)
+ throw std::invalid_argument("invalid FV");
+
+ break;
+ }
+
+ case p18::CommandType::SetACOutputRatedVoltage: {
+ GET_ARGS(1);
+
+ auto v = static_cast<unsigned>(std::stoul(arguments[0]));
+
+ bool matchFound = false;
+ for (const auto &item: p18::ac_output_rated_voltages) {
+ if (v == item) {
+ matchFound = true;
+ break;
+ }
+ }
+
+ if (!matchFound)
+ throw std::invalid_argument("invalid voltage");
+
+ break;
+ }
+
+ case p18::CommandType::SetOutputSourcePriority: {
+ GET_ARGS(1);
+
+ std::array<std::string, 2> priorities({"SUB", "SBU"});
+
+ long index = index_of(priorities, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid argument");
+
+ arguments[0] = std::to_string(index);
+ break;
+ }
+
+ case p18::CommandType::SetBatteryChargingThresholds: {
+ GET_ARGS(2);
+
+ float cv = std::stof(arguments[0]);
+ float dv = std::stof(arguments[1]);
+
+ if (index_of(p18::bat_ac_recharging_voltages_12v, cv) == -1 ||
+ index_of(p18::bat_ac_recharging_voltages_24v, cv) == -1 ||
+ index_of(p18::bat_ac_recharging_voltages_48v, cv) == -1)
+ throw std::invalid_argument("invalid CV");
+
+ if (index_of(p18::bat_ac_redischarging_voltages_12v, dv) == -1 ||
+ index_of(p18::bat_ac_redischarging_voltages_24v, dv) == -1 ||
+ index_of(p18::bat_ac_redischarging_voltages_48v, dv) == -1)
+ throw std::invalid_argument("invalid DV");
+
+ break;
+ }
+
+ case p18::CommandType::SetChargingSourcePriority: {
+ GET_ARGS(2);
+
+ auto id = static_cast<unsigned>(std::stoul(arguments[0]));
+ if (!p18::is_valid_parallel_id(id))
+ throw std::invalid_argument("invalid id");
+
+ std::array<std::string, 3> priorities({"SF", "SU", "S"});
+ long index = index_of(priorities, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid argument");
+
+ arguments[1] = std::to_string(index);
+ break;
+ }
+
+ case p18::CommandType::SetSolarPowerPriority: {
+ GET_ARGS(1);
+
+ std::array<std::string, 2> allowed({"BLU", "LBU"});
+ long index = index_of(allowed, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid priority");
+
+ arguments[0] = std::to_string(index);
+ break;
+ }
+
+ case p18::CommandType::SetACInputVoltageRange: {
+ GET_ARGS(1);
+ std::array<std::string, 2> allowed({"APPLIANCE", "UPS"});
+ long index = index_of(allowed, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid argument");
+ arguments[0] = std::to_string(index);
+ break;
+ }
+
+ case p18::CommandType::SetBatteryType: {
+ GET_ARGS(1);
+
+ std::array<std::string, 3> allowed({"AGM", "FLOODED", "USER"});
+ long index = index_of(allowed, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid type");
+ arguments[0] = std::to_string(index);
+
+ break;
+ }
+
+ case p18::CommandType::SetOutputModel: {
+ GET_ARGS(2);
+
+ auto id = static_cast<unsigned>(std::stoul(arguments[0]));
+ if (!p18::is_valid_parallel_id(id))
+ throw std::invalid_argument("invalid id");
+
+ std::array<std::string, 5> allowed({"SM", "P", "P1", "P2", "P3"});
+ long index = index_of(allowed, arguments[0]);
+ if (index == -1)
+ throw std::invalid_argument("invalid model");
+ arguments[1] = std::to_string(index);
+
+ break;
+ }
+
+ case p18::CommandType::SetBatteryCutOffVoltage: {
+ GET_ARGS(1);
+
+ float v = std::stof(arguments[0]);
+ if (v < 40.0 || v > 48.0)
+ throw std::invalid_argument("invalid voltage");
+
+ break;
+ }
+
+ case p18::CommandType::SetSolarConfig: {
+ GET_ARGS(1);
+
+ if (!is_numeric(arguments[0]) || arguments[0].size() > 20)
+ throw std::invalid_argument("invalid argument");
+
+ break;
+ }
+
+ case p18::CommandType::SetDateTime: {
+ GET_ARGS(6);
+
+ validate_date_args(&arguments[0], &arguments[1], &arguments[2]);
+ validate_time_args(&arguments[3], &arguments[4], &arguments[5]);
+
+ break;
+ }
+
+ case p18::CommandType::SetACChargingTimeBucket:
+ case p18::CommandType::SetACLoadsSupplyTimeBucket: {
+ GET_ARGS(2);
+
+ std::vector<std::string> start = split(arguments[0], ':');
+ if (start.size() != 2)
+ throw std::invalid_argument("invalid start time");
+
+ std::vector<std::string> end = split(arguments[1], ':');
+ if (end.size() != 2)
+ throw std::invalid_argument("invalid end time");
+
+ auto startHour = static_cast<unsigned short>(std::stoul(start[0]));
+ auto startMinute = static_cast<unsigned short>(std::stoul(start[1]));
+ if (startHour > 23 || startMinute > 59)
+ throw std::invalid_argument("invalid start time");
+
+ auto endHour = static_cast<unsigned short>(std::stoul(end[0]));
+ auto endMinute = static_cast<unsigned short>(std::stoul(end[1]));
+ if (endHour > 23 || endMinute > 59)
+ throw std::invalid_argument("invalid end time");
+
+ arguments[0] = std::to_string(startHour);
+ arguments[1] = std::to_string(startMinute);
+
+ arguments[2] = std::to_string(endHour);
+ arguments[3] = std::to_string(endMinute);
+
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return commandType;
+}
+
+} \ No newline at end of file
diff --git a/src/p18/commands.h b/src/p18/commands.h
new file mode 100644
index 0000000..0b597e9
--- /dev/null
+++ b/src/p18/commands.h
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_P18_COMMANDS_H
+#define INVERTER_TOOLS_P18_COMMANDS_H
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "types.h"
+
+namespace p18 {
+
+#ifdef INVERTERCTL
+struct CommandInput {
+ int argc;
+ char** argv;
+};
+#endif
+
+#ifdef INVERTERD
+struct CommandInput {
+ std::vector<std::string>* argv;
+};
+#endif
+
+extern const std::map<std::string, p18::CommandType> client_commands;
+
+static void validate_date_args(const std::string* ys, const std::string* ms, const std::string* ds);
+static void validate_time_args(const std::string* hs, const std::string* ms, const std::string* ss);
+CommandType validate_input(std::string& command, std::vector<std::string>& arguments, void* input);
+
+}
+
+#endif //INVERTER_TOOLS_P18_COMMANDS_H
diff --git a/src/p18/defines.cc b/src/p18/defines.cc
new file mode 100644
index 0000000..4207bde
--- /dev/null
+++ b/src/p18/defines.cc
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <iostream>
+
+#include "defines.h"
+#include "types.h"
+
+namespace p18 {
+
+const std::map<CommandType, std::string> raw_commands = {
+ {CommandType::GetProtocolID, "PI"},
+ {CommandType::GetCurrentTime, "T"},
+ {CommandType::GetTotalGenerated, "ET"},
+ {CommandType::GetYearGenerated, "EY"},
+ {CommandType::GetMonthGenerated, "EM"},
+ {CommandType::GetDayGenerated, "ED"},
+ {CommandType::GetSeriesNumber, "ID"},
+ {CommandType::GetCPUVersion, "VFW"},
+ {CommandType::GetRatedInformation, "PIRI"},
+ {CommandType::GetGeneralStatus, "GS"},
+ {CommandType::GetWorkingMode, "MOD"},
+ {CommandType::GetFaultsAndWarnings, "FWS"},
+ {CommandType::GetFlagsAndStatuses, "FLAG"},
+ {CommandType::GetDefaults, "DI"},
+ {CommandType::GetAllowedChargingCurrents, "MCHGCR"},
+ {CommandType::GetAllowedACChargingCurrents, "MUCHGCR"},
+ {CommandType::GetParallelRatedInformation, "PRI"},
+ {CommandType::GetParallelGeneralStatus, "PGS"},
+ {CommandType::GetACChargingTimeBucket, "ACCT"},
+ {CommandType::GetACLoadsSupplyTimeBucket, "ACLT"},
+ {CommandType::SetLoads, "LON"},
+ {CommandType::SetFlag, "P"},
+ {CommandType::SetDefaults, "PF"},
+ {CommandType::SetBatteryMaxChargingCurrent, "MCHGC"},
+ {CommandType::SetBatteryMaxACChargingCurrent, "MUCHGC"},
+ /* The protocol documentation defines two commands, "F50" and "F60",
+ but it's identical as if there were just one "F" command with an argument. */
+ {CommandType::SetACOutputFreq, "F"},
+ {CommandType::SetBatteryMaxChargingVoltage, "MCHGV"},
+ {CommandType::SetACOutputRatedVoltage, "V"},
+ {CommandType::SetOutputSourcePriority, "POP"},
+ {CommandType::SetBatteryChargingThresholds, "BUCD"},
+ {CommandType::SetChargingSourcePriority, "PCP"},
+ {CommandType::SetSolarPowerPriority, "PSP"},
+ {CommandType::SetACInputVoltageRange, "PGR"},
+ {CommandType::SetBatteryType, "PBT"},
+ {CommandType::SetOutputModel, "POPM"},
+ {CommandType::SetBatteryCutOffVoltage, "PSDV"},
+ {CommandType::SetSolarConfig, "ID"},
+ {CommandType::ClearGenerated, "CLE"},
+ {CommandType::SetDateTime, "DAT"},
+ {CommandType::SetACChargingTimeBucket, "ACCT"},
+ {CommandType::SetACLoadsSupplyTimeBucket, "ACLT"},
+};
+
+const std::array<int, 5> ac_output_rated_voltages = {202, 208, 220, 230, 240};
+
+const std::array<float, 8> bat_ac_recharging_voltages_12v = {11, 11.3, 11.5, 11.8, 12, 12.3, 12.5, 12.8};
+const std::array<float, 8> bat_ac_recharging_voltages_24v = {22, 22.5, 23, 23.5, 24, 24.5, 25, 25.5};
+const std::array<float, 8> bat_ac_recharging_voltages_48v = {44, 45, 46, 47, 48, 49, 50, 51};
+
+const std::array<float, 12> bat_ac_redischarging_voltages_12v = {0, 12, 12.3, 12.5, 12.8, 13, 13.3, 13.5, 13.8, 14, 14.3, 14.5};
+const std::array<float, 12> bat_ac_redischarging_voltages_24v = {0, 24, 24.5, 25, 25.5, 26, 26.5, 27, 27.5, 28, 28.5, 29};
+const std::array<float, 12> bat_ac_redischarging_voltages_48v = {0, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58};
+
+const std::map<int, std::string> 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 std::array<Flag, 9> flags = {{
+ {"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)"},
+}};
+
+ENUM_STR(BatteryType) {
+ switch (val) {
+ case BatteryType::AGM: return os << "AGM" ;
+ case BatteryType::Flooded: return os << "Flooded";
+ case BatteryType::User: return os << "User";
+ };
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(InputVoltageRange) {
+ switch (val) {
+ case InputVoltageRange::Appliance: return os << "Appliance";
+ case InputVoltageRange::USP: return os << "USP";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(OutputSourcePriority) {
+ switch (val) {
+ case OutputSourcePriority::SolarUtilityBattery:
+ return os << "Solar-Utility-Battery";
+ case OutputSourcePriority::SolarBatteryUtility:
+ return os << "Solar-Battery-Utility";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(ChargerSourcePriority) {
+ switch (val) {
+ case ChargerSourcePriority::SolarFirst:
+ return os << "Solar-First";
+ case ChargerSourcePriority::SolarAndUtility:
+ return os << "Solar-and-Utility";
+ case ChargerSourcePriority::SolarOnly:
+ return os << "Solar-only";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(MachineType) {
+ switch (val) {
+ case MachineType::OffGridTie: return os << "Off-Grid-Tie";
+ case MachineType::GridTie: return os << "Grid-Tie";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(Topology) {
+ switch (val) {
+ case Topology::TransformerLess: return os << "Transformer-less";
+ case Topology::Transformer: return os << "Transformer";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(OutputModelSetting) {
+ switch (val) {
+ case OutputModelSetting::SingleModule:
+ return os << "Single module";
+ case OutputModelSetting::ParallelOutput:
+ return os << "Parallel output";
+ case OutputModelSetting::Phase1OfThreePhaseOutput:
+ return os << "Phase 1 of three phase output";
+ case OutputModelSetting::Phase2OfThreePhaseOutput:
+ return os << "Phase 2 of three phase output";
+ case OutputModelSetting::Phase3OfThreePhaseOutput:
+ return os << "Phase 3 of three phase";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(SolarPowerPriority) {
+ switch (val) {
+ case SolarPowerPriority::BatteryLoadUtility:
+ return os << "Battery-Load-Utility";
+ case SolarPowerPriority::LoadBatteryUtility:
+ return os << "Load-Battery-Utility";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(MPPTChargerStatus) {
+ switch (val) {
+ case MPPTChargerStatus::Abnormal: return os << "Abnormal";
+ case MPPTChargerStatus::NotCharging: return os << "Not charging";
+ case MPPTChargerStatus::Charging: return os << "Charging";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(BatteryPowerDirection) {
+ switch (val) {
+ case BatteryPowerDirection::DoNothing: return os << "Do nothing";
+ case BatteryPowerDirection::Charge: return os << "Charge";
+ case BatteryPowerDirection::Discharge: return os << "Discharge";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(DC_AC_PowerDirection) {
+ switch (val) {
+ case DC_AC_PowerDirection::DoNothing: return os << "Do nothing";
+ case DC_AC_PowerDirection::AC_DC: return os << "AC/DC";
+ case DC_AC_PowerDirection::DC_AC: return os << "DC/AC";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(LinePowerDirection) {
+ switch (val) {
+ case LinePowerDirection::DoNothing: return os << "Do nothing";
+ case LinePowerDirection::Input: return os << "Input";
+ case LinePowerDirection::Output: return os << "Output";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(WorkingMode) {
+ switch (val) {
+ case WorkingMode::PowerOnMode: return os << "Power on mode";
+ case WorkingMode::StandbyMode: return os << "Standby mode";
+ case WorkingMode::BypassMode: return os << "Bypass mode";
+ case WorkingMode::BatteryMode: return os << "Battery mode";
+ case WorkingMode::FaultMode: return os << "Fault mode";
+ case WorkingMode::HybridMode: return os << "Hybrid mode";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+ENUM_STR(ParallelConnectionStatus) {
+ switch (val) {
+ case ParallelConnectionStatus::NotExistent: return os << "Non-existent";
+ case ParallelConnectionStatus::Existent: return os << "Existent";
+ }
+ ENUM_STR_DEFAULT;
+}
+
+} \ No newline at end of file
diff --git a/src/p18/defines.h b/src/p18/defines.h
new file mode 100644
index 0000000..83728f8
--- /dev/null
+++ b/src/p18/defines.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_P18_DEFINES_H
+#define INVERTER_TOOLS_P18_DEFINES_H
+
+#include <map>
+#include <string>
+#include <array>
+
+#include "types.h"
+
+namespace p18 {
+
+extern const std::map<CommandType, std::string> raw_commands;
+
+extern const std::array<int, 5> ac_output_rated_voltages;
+
+extern const std::array<float, 8> bat_ac_recharging_voltages_12v;
+extern const std::array<float, 8> bat_ac_recharging_voltages_24v;
+extern const std::array<float, 8> bat_ac_recharging_voltages_48v;
+
+extern const std::array<float, 12> bat_ac_redischarging_voltages_12v;
+extern const std::array<float, 12> bat_ac_redischarging_voltages_24v;
+extern const std::array<float, 12> bat_ac_redischarging_voltages_48v;
+
+extern const std::map<int, std::string> fault_codes;
+
+extern const std::array<Flag, 9> flags;
+
+}
+
+#endif //INVERTER_TOOLS_P18_DEFINES_H
diff --git a/src/p18/exceptions.h b/src/p18/exceptions.h
new file mode 100644
index 0000000..9b79082
--- /dev/null
+++ b/src/p18/exceptions.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INFINISOLAR_TOOLS_P18_EXCEPTIONS_H
+#define INFINISOLAR_TOOLS_P18_EXCEPTIONS_H
+
+#include <stdexcept>
+
+namespace p18 {
+
+class InvalidResponseError : public std::runtime_error {
+public:
+ using std::runtime_error::runtime_error;
+};
+
+class ParseError : public InvalidResponseError {
+public:
+ using InvalidResponseError::InvalidResponseError;
+};
+
+}
+
+#endif //INFINISOLAR_TOOLS_P18_EXCEPTIONS_H
diff --git a/src/p18/functions.cc b/src/p18/functions.cc
new file mode 100644
index 0000000..9799fc0
--- /dev/null
+++ b/src/p18/functions.cc
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "functions.h"
+
+namespace p18 {
+
+bool is_valid_parallel_id(unsigned id)
+{
+ return id >= 0 && id <= 6;
+}
+
+} \ No newline at end of file
diff --git a/src/p18/functions.h b/src/p18/functions.h
new file mode 100644
index 0000000..c372242
--- /dev/null
+++ b/src/p18/functions.h
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INFINISOLAR_TOOLS_P18_FUNCTIONS_H
+#define INFINISOLAR_TOOLS_P18_FUNCTIONS_H
+
+namespace p18 {
+
+bool is_valid_parallel_id(unsigned id);
+
+}
+
+#endif //INFINISOLAR_TOOLS_P18_FUNCTIONS_H
diff --git a/src/p18/response.cc b/src/p18/response.cc
new file mode 100644
index 0000000..fa3693b
--- /dev/null
+++ b/src/p18/response.cc
@@ -0,0 +1,781 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <utility>
+#include <cstring>
+#include <sstream>
+#include <iomanip>
+#include <typeinfo>
+
+#include "response.h"
+#include "exceptions.h"
+#include "../logging.h"
+
+#define RETURN_TABLE(...) \
+ return std::shared_ptr<formatter::Table<VariantHolder>>( \
+ new formatter::Table<VariantHolder>(format, __VA_ARGS__) \
+ );
+
+#define RETURN_STATUS(...) \
+ return std::shared_ptr<formatter::Status>( \
+ new formatter::Status(format, __VA_ARGS__) \
+ );
+
+
+namespace p18::response_type {
+
+typedef formatter::TableItem<VariantHolder> LINE;
+
+using formatter::Unit;
+
+/**
+ * Base responses
+ */
+
+BaseResponse::BaseResponse(std::shared_ptr<char> raw, size_t rawSize)
+ : raw_(std::move(raw)), rawSize_(rawSize) {}
+
+bool GetResponse::validate() {
+ if (rawSize_ < 5)
+ return false;
+
+ const char* raw = raw_.get();
+ if (raw[0] != '^' || raw[1] != 'D')
+ return false;
+
+ char lenbuf[4];
+ memcpy(lenbuf, &raw[2], 3);
+ lenbuf[3] = '\0';
+
+ auto len = static_cast<size_t>(std::stoul(lenbuf));
+ return rawSize_ >= len-5 /* exclude ^Dxxx*/;
+}
+
+const char* GetResponse::getData() const {
+ return raw_.get() + 5;
+}
+
+size_t GetResponse::getDataSize() const {
+ return rawSize_ - 5;
+}
+
+std::vector<std::string> GetResponse::getList(std::vector<size_t> itemLengths) const {
+ std::string buf(getData(), getDataSize());
+ auto list = ::split(buf, ',');
+
+ if (!itemLengths.empty()) {
+ // check list length
+ if (list.size() < itemLengths.size()) {
+ std::ostringstream error;
+ error << "while parsing " << demangle_type_name(typeid(*this).name());
+ error << ": list is expected to be " << itemLengths.size() << " items long, ";
+ error << "got only " << list.size() << " items";
+ throw ParseError(error.str());
+ }
+
+ // check each item's length
+ for (int i = 0; i < itemLengths.size(); i++) {
+ if (list[i].size() != itemLengths[i]) {
+ std::ostringstream error;
+ error << "while parsing " << demangle_type_name(typeid(*this).name());
+ error << ": item " << i << " is expected to be " << itemLengths[i] << " characters long, ";
+ error << "got " << list[i].size() << " characters";
+ throw ParseError(error.str());
+ }
+ }
+ }
+
+ return list;
+}
+
+bool SetResponse::validate() {
+ if (rawSize_ < 2)
+ return false;
+
+ const char* raw = raw_.get();
+ return raw[0] == '^' && (raw[1] == '0' || raw[1] == '1');
+}
+
+bool SetResponse::get() {
+ return raw_.get()[1] == '1';
+}
+
+void SetResponse::unpack() {}
+
+formattable_ptr SetResponse::format(formatter::Format format) {
+ RETURN_STATUS(get(), "");
+}
+
+formattable_ptr ErrorResponse::format(formatter::Format format) {
+ return std::shared_ptr<formatter::Status>(
+ new formatter::Status(format, false, error_)
+ );
+}
+
+
+/**
+ * Actual typed responses
+ */
+
+void ProtocolID::unpack() {
+ auto data = getData();
+
+ char s[4];
+ strncpy(s, data, 2);
+ s[2] = '\0';
+
+ id = stou(s);
+}
+
+formattable_ptr ProtocolID::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("id", "Protocol ID", id),
+ });
+}
+
+
+void CurrentTime::unpack() {
+ auto data = getData();
+
+ std::string buf;
+ buf = std::string(data, 4);
+
+ year = stou(buf);
+
+ for (int i = 0; i < 5; i++) {
+ buf = std::string(data + 4 + (i * 2), 2);
+ auto n = stou(buf);
+
+ switch (i) {
+ case 0:
+ month = n;
+ break;
+
+ case 1:
+ day = n;
+ break;
+
+ case 2:
+ hour = n;
+ break;
+
+ case 3:
+ minute = n;
+ break;
+
+ case 4:
+ second = n;
+ break;
+
+ default:
+ std::ostringstream error;
+ error << "unexpected value while parsing CurrentTime (i = " << i << ")";
+ throw ParseError(error.str());
+ }
+ }
+}
+
+formattable_ptr CurrentTime::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("year", "Year", year),
+ LINE("month", "Month", month),
+ LINE("day", "Day", day),
+ LINE("hour", "Hour", hour),
+ LINE("minute", "Minute", minute),
+ LINE("second", "Second", second),
+ });
+}
+
+
+void TotalGenerated::unpack() {
+ auto data = getData();
+
+ std::string buf(data, 8);
+ kwh = stou(buf);
+}
+
+formattable_ptr TotalGenerated::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("kwh", "KWh", kwh)
+ });
+}
+
+
+void SeriesNumber::unpack() {
+ auto data = getData();
+
+ std::string buf(data, 2);
+ size_t len = std::stoul(buf);
+
+ id = std::string(data+2, len);
+}
+
+formattable_ptr SeriesNumber::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("sn", "Series number", id)
+ });
+}
+
+
+void CPUVersion::unpack() {
+ auto list = getList({5, 5, 5});
+
+ main_cpu_version = list[0];
+ slave1_cpu_version = list[1];
+ slave2_cpu_version = list[2];
+}
+
+formattable_ptr CPUVersion::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("main_v", "Main CPU version", main_cpu_version),
+ LINE("slave1_v", "Slave 1 CPU version", slave1_cpu_version),
+ LINE("slave2_v", "Slave 2 CPU version", slave2_cpu_version)
+ });
+}
+
+
+void RatedInformation::unpack() {
+ auto list = getList({
+ 4, // AAAA
+ 3, // BBB
+ 4, // CCCC
+ 3, // DDD
+ 3, // EEE
+ 4, // FFFF
+ 4, // GGGG
+ 3, // HHH
+ 3, // III
+ 3, // JJJ
+ 3, // KKK
+ 3, // LLL
+ 3, // MMM
+ 1, // N
+ 2, // OO
+ 3, // PPP
+ 1, // O
+ 1, // R
+ 1, // S
+ 1, // T
+ 1, // U
+ 1, // V
+ 1, // W
+ 1, // Z
+ 1, // a
+ });
+
+ ac_input_rating_voltage = stou(list[0]);
+ ac_input_rating_current = stou(list[1]);
+ ac_output_rating_voltage = stou(list[2]);
+ ac_output_rating_freq = stou(list[3]);
+ ac_output_rating_current = stou(list[4]);
+ ac_output_rating_apparent_power = stou(list[5]);
+ ac_output_rating_active_power = stou(list[6]);
+ battery_rating_voltage = stou(list[7]);
+ battery_recharge_voltage = stou(list[8]);
+ battery_redischarge_voltage = stou(list[9]);
+ battery_under_voltage = stou(list[10]);
+ battery_bulk_voltage = stou(list[11]);
+ battery_float_voltage = stou(list[12]);
+ battery_type = static_cast<BatteryType>(stou(list[13]));
+ max_ac_charging_current = stou(list[14]);
+ max_charging_current = stou(list[15]);
+ input_voltage_range = static_cast<InputVoltageRange>(stou(list[16]));
+ output_source_priority = static_cast<OutputModelSetting>(stou(list[17]));
+ charger_source_priority = static_cast<ChargerSourcePriority>(stou(list[18]));
+ parallel_max_num = stou(list[19]);
+ machine_type = static_cast<MachineType>(stou(list[20]));
+ topology = static_cast<Topology>(stou(list[21]));
+ output_model_setting = static_cast<OutputModelSetting>(stou(list[22]));
+ solar_power_priority = static_cast<SolarPowerPriority>(stou(list[23]));
+ mppt = list[24];
+}
+
+formattable_ptr RatedInformation::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("ac_input_rating_voltage", "AC input rating voltage", ac_input_rating_voltage / 10.0, Unit::V),
+ LINE("ac_input_rating_current", "AC input rating current", ac_input_rating_current / 10.0, Unit::A),
+ LINE("ac_output_rating_voltage", "AC output rating voltage", ac_output_rating_voltage / 10.0, Unit::V),
+ LINE("ac_output_rating_freq", "AC output rating frequency", ac_output_rating_freq / 10.0, Unit::Hz),
+ LINE("ac_output_rating_current", "AC output rating current", ac_output_rating_current / 10.0, Unit::A),
+ LINE("ac_output_rating_apparent_power", "AC output rating apparent power", ac_output_rating_apparent_power, Unit::VA),
+ LINE("ac_output_rating_active_power", "AC output rating active power", ac_output_rating_active_power, Unit::Wh),
+ LINE("battery_rating_voltage", "Battery rating voltage", battery_rating_voltage / 10.0, Unit::V),
+ LINE("battery_recharge_voltage", "Battery re-charge voltage", battery_recharge_voltage / 10.0, Unit::V),
+ LINE("battery_redischarge_voltage", "Battery re-discharge voltage", battery_redischarge_voltage / 10.0, Unit::V),
+ LINE("battery_under_voltage", "Battery under voltage", battery_under_voltage / 10.0, Unit::V),
+ LINE("battery_bulk_voltage", "Battery bulk voltage", battery_bulk_voltage / 10.0, Unit::V),
+ LINE("battery_float_voltage", "Battery float voltage", battery_float_voltage / 10.0, Unit::V),
+ LINE("battery_type", "Battery type", battery_type),
+ LINE("max_charging_current", "Max charging current", max_charging_current, Unit::A),
+ LINE("max_ac_charging_current", "Max AC charging current", max_ac_charging_current, Unit::A),
+ LINE("input_voltage_range", "Input voltage range", input_voltage_range),
+ LINE("output_source_priority", "Output source priority", output_source_priority),
+ LINE("charge_source_priority", "Charge source priority", charger_source_priority),
+ LINE("parallel_max_num", "Parallel max num", parallel_max_num),
+ LINE("machine_type", "Machine type", machine_type),
+ LINE("topology", "Topology", topology),
+ LINE("output_model_setting", "Output model setting", output_model_setting),
+ LINE("solar_power_priority", "Solar power priority", solar_power_priority),
+ LINE("mppt", "MPPT string", mppt)
+ });
+}
+
+
+void GeneralStatus::unpack() {
+ auto list = getList({
+ 4, // AAAA
+ 3, // BBB
+ 4, // CCCC
+ 3, // DDD
+ 4, // EEEE
+ 4, // FFFF
+ 3, // GGG
+ 3, // HHH
+ 3, // III
+ 3, // JJJ
+ 3, // KKK
+ 3, // LLL
+ 3, // MMM
+ 3, // NNN
+ 3, // OOO
+ 3, // PPP
+ 4, // QQQQ
+ 4, // RRRR
+ 4, // SSSS
+ 4, // TTTT
+ 1, // U
+ 1, // V
+ 1, // W
+ 1, // X
+ 1, // Y
+ 1, // Z
+ 1, // a
+ 1, // b
+ });
+
+ grid_voltage = stou(list[0]);
+ grid_freq = stou(list[1]);
+ ac_output_voltage = stou(list[2]);
+ ac_output_freq = stou(list[3]);
+ ac_output_apparent_power = stou(list[4]);
+ ac_output_active_power = stou(list[5]);
+ output_load_percent = stou(list[6]);
+ battery_voltage = stou(list[7]);
+ battery_voltage_scc = stou(list[8]);
+ battery_voltage_scc2 = stou(list[9]);
+ battery_discharge_current = stou(list[10]);
+ battery_charging_current = stou(list[11]);
+ battery_capacity = stou(list[12]);
+ inverter_heat_sink_temp = stou(list[13]);
+ mppt1_charger_temp = stou(list[14]);
+ mppt2_charger_temp = stou(list[15]);
+ pv1_input_power = stou(list[16]);
+ pv2_input_power = stou(list[17]);
+ pv1_input_voltage = stou(list[18]);
+ pv2_input_voltage = stou(list[19]);
+ settings_values_changed = list[20] == "1";
+ mppt1_charger_status = static_cast<MPPTChargerStatus>(stou(list[21]));
+ mppt2_charger_status = static_cast<MPPTChargerStatus>(stou(list[22]));
+ load_connected = list[23] == "1";
+ battery_power_direction = static_cast<BatteryPowerDirection>(stou(list[24]));
+ dc_ac_power_direction = static_cast<DC_AC_PowerDirection>(stou(list[25]));
+ line_power_direction = static_cast<LinePowerDirection>(stou(list[26]));
+ local_parallel_id = stou(list[27]);
+}
+
+formattable_ptr GeneralStatus::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("grid_voltage", "Grid voltage", grid_voltage / 10.0, Unit::V),
+ LINE("grid_freq", "Grid frequency", grid_freq / 10.0, Unit::Hz),
+ LINE("ac_output_voltage", "AC output voltage", ac_output_voltage / 10.0, Unit::V),
+ LINE("ac_output_freq", "AC output frequency", ac_output_freq / 10.0, Unit::Hz),
+ LINE("ac_output_apparent_power", "AC output apparent power", ac_output_apparent_power, Unit::VA),
+ LINE("ac_output_active_power", "AC output active power", ac_output_active_power, Unit::Wh),
+ LINE("output_load_percent", "Output load percent", output_load_percent, Unit::Percentage),
+ LINE("battery_voltage", "Battery voltage", battery_voltage / 10.0, Unit::V),
+ LINE("battery_voltage_scc", "Battery voltage from SCC", battery_voltage_scc / 10.0, Unit::V),
+ LINE("battery_voltage_scc2", "Battery voltage from SCC2", battery_voltage_scc2 / 10.0, Unit::V),
+ LINE("battery_discharging_current", "Battery discharging current", battery_discharge_current, Unit::A),
+ LINE("battery_charging_current", "Battery charging current", battery_charging_current, Unit::A),
+ LINE("battery_capacity", "Battery capacity", battery_capacity, Unit::Percentage),
+ LINE("inverter_heat_sink_temp", "Inverter heat sink temperature", inverter_heat_sink_temp, Unit::Celsius),
+ LINE("mppt1_charger_temp", "MPPT1 charger temperature", mppt1_charger_temp, Unit::Celsius),
+ LINE("mppt2_charger_temp", "MPPT2 charger temperature", mppt2_charger_temp, Unit::Celsius),
+ LINE("pv1_input_power", "PV1 input power", pv1_input_power, Unit::Wh),
+ LINE("pv2_input_power", "PV2 input power", pv2_input_power, Unit::Wh),
+ LINE("pv1_input_voltage", "PV1 input voltage", pv1_input_voltage / 10.0, Unit::V),
+ LINE("pv2_input_voltage", "PV2 input voltage", pv2_input_voltage / 10.0, Unit::V),
+ LINE("settings_values_changed", "Configuration state", std::string(settings_values_changed ? "Default" : "Custom")),
+ LINE("mppt1_charger_status", "MPPT1 charger status", mppt1_charger_status),
+ LINE("mppt2_charger_status", "MPPT2 charger status", mppt2_charger_status),
+ LINE("load_connected", "Load connection", std::string(load_connected ? "Connected" : "Disconnected")),
+ LINE("battery_power_direction", "Battery power direction", battery_power_direction),
+ LINE("dc_ac_power_direction", "DC/AC power direction", dc_ac_power_direction),
+ LINE("line_power_direction", "LINE power direction", line_power_direction),
+ LINE("local_parallel_id", "Local parallel ID", local_parallel_id),
+ });
+}
+
+
+void WorkingMode::unpack() {
+ auto data = getData();
+ mode = static_cast<p18::WorkingMode>(stou(std::string(data, 2)));
+}
+
+formattable_ptr WorkingMode::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("mode", "Working mode", mode)
+ })
+}
+
+
+void FaultsAndWarnings::unpack() {
+ auto list = getList({2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
+
+ fault_code = stou(list[0]);
+ line_fail = stou(list[1]) > 0;
+ output_circuit_short = stou(list[2]) > 0;
+ inverter_over_temperature = stou(list[3]) > 0;
+ fan_lock = stou(list[4]) > 0;
+ battery_voltage_high = stou(list[5]) > 0;
+ battery_low = stou(list[6]) > 0;
+ battery_under = stou(list[7]) > 0;
+ over_load = stou(list[8]) > 0;
+ eeprom_fail = stou(list[9]) > 0;
+ power_limit = stou(list[10]) > 0;
+ pv1_voltage_high = stou(list[11]) > 0;
+ pv2_voltage_high = stou(list[12]) > 0;
+ mppt1_overload_warning = stou(list[13]) > 0;
+ mppt2_overload_warning = stou(list[14]) > 0;
+ battery_too_low_to_charge_for_scc1 = stou(list[15]) > 0;
+ battery_too_low_to_charge_for_scc2 = stou(list[16]) > 0;
+}
+
+formattable_ptr FaultsAndWarnings::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("fault_code", "Fault code", fault_code),
+ LINE("line_fail", "Line fail", line_fail),
+ LINE("output_circuit_short", "Output circuit short", output_circuit_short),
+ LINE("inverter_over_temperature", "Inverter over temperature", inverter_over_temperature),
+ LINE("fan_lock", "Fan lock", fan_lock),
+ LINE("battery_voltage_high", "Battery voltage high", battery_voltage_high),
+ LINE("battery_low", "Battery low", battery_low),
+ LINE("battery_under", "Battery under", battery_under),
+ LINE("over_load", "Over load", over_load),
+ LINE("eeprom_fail", "EEPROM fail", eeprom_fail),
+ LINE("power_limit", "Power limit", power_limit),
+ LINE("pv1_voltage_high", "PV1 voltage high", pv1_voltage_high),
+ LINE("pv2_voltage_high", "PV2 voltage high", pv2_voltage_high),
+ LINE("mppt1_overload_warning", "MPPT1 overload warning", mppt1_overload_warning),
+ LINE("mppt2_overload_warning", "MPPT2 overload warning", mppt2_overload_warning),
+ LINE("battery_too_low_to_charge_for_scc1", "Battery too low to charge for SCC1", battery_too_low_to_charge_for_scc1),
+ LINE("battery_too_low_to_charge_for_scc2", "Battery too low to charge for SCC2", battery_too_low_to_charge_for_scc2),
+ })
+}
+
+
+void FlagsAndStatuses::unpack() {
+ auto list = getList({1, 1, 1, 1, 1, 1, 1, 1, 1});
+
+ buzzer = stou(list[0]) > 0;
+ overload_bypass = stou(list[1]) > 0;
+ lcd_escape_to_default_page_after_1min_timeout = stou(list[2]) > 0;
+ overload_restart = stou(list[3]) > 0;
+ over_temp_restart = stou(list[4]) > 0;
+ backlight_on = stou(list[5]) > 0;
+ alarm_on_primary_source_interrupt = stou(list[6]) > 0;
+ fault_code_record = stou(list[7]) > 0;
+ reserved = *list[8].c_str();
+}
+
+formattable_ptr FlagsAndStatuses::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("buzzer",
+ "Buzzer",
+ buzzer),
+
+ LINE("overload_bypass",
+ "Overload bypass function",
+ overload_bypass),
+
+ LINE("escape_to_default_screen_after_1min_timeout",
+ "Escape to default screen after 1min timeout",
+ lcd_escape_to_default_page_after_1min_timeout),
+
+ LINE("overload_restart",
+ "Overload restart",
+ overload_restart),
+
+ LINE("over_temp_restart",
+ "Over temperature restart",
+ over_temp_restart),
+
+ LINE("backlight_on",
+ "Backlight on",
+ backlight_on),
+
+ LINE("alarm_on_on_primary_source_interrupt",
+ "Alarm on on primary source interrupt",
+ alarm_on_primary_source_interrupt),
+
+ LINE("fault_code_record",
+ "Fault code record",
+ fault_code_record)
+ })
+}
+
+
+void Defaults::unpack() {
+ auto list = getList({
+ 4, // AAAA
+ 3, // BBB
+ 1, // C
+ 3, // DDD
+ 3, // EEE
+ 3, // FFF
+ 3, // GGG
+ 3, // HHH
+ 3, // III
+ 2, // JJ
+ 1, // K
+ 1, // L
+ 1, // M
+ 1, // N
+ 1, // O
+ 1, // P
+ 1, // S
+ 1, // T
+ 1, // U
+ 1, // V
+ 1, // W
+ 1, // X
+ 1, // Y
+ 1, // Z
+ });
+
+ ac_output_voltage = stou(list[0]);
+ ac_output_freq = stou(list[1]);
+ ac_input_voltage_range = static_cast<InputVoltageRange>(stou(list[2]));
+ battery_under_voltage = stou(list[3]);
+ charging_float_voltage = stou(list[4]);
+ charging_bulk_voltage = stou(list[5]);
+ battery_recharge_voltage = stou(list[6]);
+ battery_redischarge_voltage = stou(list[7]);
+ max_charging_current = stou(list[8]);
+ max_ac_charging_current = stou(list[9]);
+ battery_type = static_cast<BatteryType>(stou(list[10]));
+ output_source_priority = static_cast<OutputSourcePriority>(stou(list[11]));
+ charger_source_priority = static_cast<ChargerSourcePriority>(stou(list[12]));
+ solar_power_priority = static_cast<SolarPowerPriority>(stou(list[13]));
+ machine_type = static_cast<MachineType>(stou(list[14]));
+ output_model_setting = static_cast<OutputModelSetting>(stou(list[15]));
+ flag_buzzer = stou(list[16]) > 0;
+ flag_overload_restart = stou(list[17]) > 0;
+ flag_over_temp_restart = stou(list[18]) > 0;
+ flag_backlight_on = stou(list[19]) > 0;
+ flag_alarm_on_primary_source_interrupt = stou(list[20]) > 0;
+ flag_fault_code_record = stou(list[21]) > 0;
+ flag_overload_bypass = stou(list[22]) > 0;
+ flag_lcd_escape_to_default_page_after_1min_timeout = stou(list[23]) > 0;
+}
+
+formattable_ptr Defaults::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("ac_output_voltage", "AC output voltage", ac_output_voltage / 10.0, Unit::V),
+ LINE("ac_output_freq", "AC output frequency", ac_output_freq / 10.0, Unit::Hz),
+ LINE("ac_input_voltage_range", "AC input voltage range", ac_input_voltage_range),
+ LINE("battery_under_voltage", "Battery under voltage", battery_under_voltage / 10.0, Unit::V),
+ LINE("battery_bulk_voltage", "Charging bulk voltage", charging_bulk_voltage / 10.0, Unit::V),
+ LINE("battery_float_voltage", "Charging float voltage", charging_float_voltage / 10.0, Unit::V),
+ LINE("battery_recharging_voltage", "Battery re-charging voltage", battery_recharge_voltage / 10.0, Unit::V),
+ LINE("battery_redischarging_voltage", "Battery re-discharging voltage", battery_redischarge_voltage / 10.0, Unit::V),
+ LINE("max_charging_current", "Max charging current", max_charging_current, Unit::A),
+ LINE("max_ac_charging_current", "Max AC charging current", max_ac_charging_current, Unit::A),
+ LINE("battery_type", "Battery type", battery_type),
+ LINE("output_source_priority", "Output source priority", output_source_priority),
+ LINE("charger_source_priority", "Charger source priority", charger_source_priority),
+ LINE("solar_power_priority", "Solar power priority", solar_power_priority),
+ LINE("machine_type", "Machine type", machine_type),
+ LINE("output_model_setting", "Output model setting", output_model_setting),
+ LINE("buzzer_flag", "Buzzer flag", flag_buzzer),
+ LINE("overload_bypass_flag", "Overload bypass function flag", flag_overload_bypass),
+ LINE("escape_to_default_screen_after_1min_timeout_flag", "Escape to default screen after 1min timeout flag", flag_lcd_escape_to_default_page_after_1min_timeout),
+ LINE("overload_restart_flag", "Overload restart flag", flag_overload_restart),
+ LINE("over_temp_restart_flag", "Over temperature restart flag", flag_over_temp_restart),
+ LINE("backlight_on_flag", "Backlight on flag", flag_backlight_on),
+ LINE("alarm_on_on_primary_source_interrupt_flag", "Alarm on on primary source interrupt flag", flag_alarm_on_primary_source_interrupt),
+ LINE("fault_code_record_flag", "Fault code record flag", flag_fault_code_record),
+ })
+}
+
+void AllowedChargingCurrents::unpack() {
+ auto list = getList({});
+ for (const std::string& i: list) {
+ amps.emplace_back(stou(i));
+ }
+}
+
+formattable_ptr AllowedChargingCurrents::format(formatter::Format format) {
+ std::vector<formatter::ListItem<VariantHolder>> v;
+ for (const auto& n: amps)
+ v.emplace_back(n);
+
+ return std::shared_ptr<formatter::List<VariantHolder>>(
+ new formatter::List<VariantHolder>(format, v)
+ );
+}
+
+
+void ParallelRatedInformation::unpack() {
+ auto list = getList({
+ 1, // A
+ 2, // BB
+ 20, // CCCCCCCCCCCCCCCCCCCC
+ 1, // D
+ 3, // EEE
+ 2, // FF
+ 1 // G
+ });
+
+ parallel_id_connection_status = static_cast<ParallelConnectionStatus>(stou(list[0]));
+ serial_number_valid_length = stou(list[1]);
+ serial_number = std::string(list[2], serial_number_valid_length);
+ charger_source_priority = static_cast<ChargerSourcePriority>(stou(list[3]));
+ max_charging_current = stou(list[4]);
+ max_ac_charging_current = stou(list[5]);
+ output_model_setting = static_cast<OutputModelSetting>(stou(list[6]));
+}
+
+formattable_ptr ParallelRatedInformation::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("parallel_id_connection_status", "Parallel ID connection status", parallel_id_connection_status),
+ LINE("serial_number", "Serial number", serial_number),
+ LINE("charger_source_priority", "Charger source priority", charger_source_priority),
+ LINE("max_charging_current", "Max charging current", max_charging_current, Unit::A),
+ LINE("max_ac_charging_current", "Max AC charging current", max_ac_charging_current, Unit::A),
+ LINE("output_model_setting", "Output model setting", output_model_setting),
+ })
+}
+
+
+void ParallelGeneralStatus::unpack() {
+ auto list = getList({
+ 1, // A
+ 1, // B
+ 2, // CC
+ 4, // DDDD
+ 3, // EEE
+ 4, // FFFF
+ 3, // GGG
+ 4, // HHHH
+ 4, // IIII
+ 5, // JJJJJ
+ 5, // KKKKK
+ 3, // LLL
+ 3, // MMM
+ 3, // NNN
+ 3, // OOO
+ 3, // PPP
+ 3, // QQQ
+ 3, // MMM. It's not my mistake, it's per the doc.
+ 4, // RRRR
+ 4, // SSSS
+ 4, // TTTT
+ 4, // UUUU
+ 1, // V
+ 1, // W
+ 1, // X
+ 1, // Y
+ 1, // Z
+ 1, // a
+ 3, // bbb. Note: this one is marked in red in the doc. I don't know what that means.
+ });
+
+ parallel_id_connection_status = static_cast<ParallelConnectionStatus>(stou(list[0]));
+ work_mode = static_cast<p18::WorkingMode>(stou(list[1]));
+ fault_code = stou(list[2]);
+ grid_voltage = stou(list[3]);
+ grid_freq = stou(list[4]);
+ ac_output_voltage = stou(list[5]);
+ ac_output_freq = stou(list[6]);
+ ac_output_apparent_power = stou(list[7]);
+ ac_output_active_power = stou(list[8]);
+ total_ac_output_apparent_power = stou(list[9]);
+ total_ac_output_active_power = stou(list[10]);
+ output_load_percent = stou(list[11]);
+ total_output_load_percent = stou(list[12]);
+ battery_voltage = stou(list[13]);
+ battery_discharge_current = stou(list[14]);
+ battery_charging_current = stou(list[15]);
+ total_battery_charging_current = stou(list[16]);
+ battery_capacity = stou(list[17]);
+ pv1_input_power = stou(list[18]);
+ pv2_input_power = stou(list[19]);
+ pv1_input_voltage = stou(list[20]);
+ pv2_input_voltage = stou(list[21]);
+ mppt1_charger_status = static_cast<MPPTChargerStatus>(stou(list[22]));
+ mppt2_charger_status = static_cast<MPPTChargerStatus>(stou(list[23]));
+ load_connected = stou(list[24]);
+ battery_power_direction = static_cast<BatteryPowerDirection>(stou(list[25]));
+ dc_ac_power_direction = static_cast<DC_AC_PowerDirection>(stou(list[26]));
+ line_power_direction = static_cast<LinePowerDirection>(stou(list[27]));
+ max_temp = stou(list[28]);
+}
+
+formattable_ptr ParallelGeneralStatus::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("parallel_id_connection_status", "Parallel ID connection status", parallel_id_connection_status),
+ LINE("mode", "Working mode", work_mode),
+ LINE("fault_code", "Fault code", fault_code),
+ LINE("grid_voltage", "Grid voltage", grid_voltage / 10.0, Unit::V),
+ LINE("grid_freq", "Grid frequency", grid_freq / 10.0, Unit::Hz),
+ LINE("ac_output_voltage", "AC output voltage", ac_output_voltage / 10.0, Unit::V),
+ LINE("ac_output_freq", "AC output frequency", ac_output_freq / 10.0, Unit::Hz),
+ LINE("ac_output_apparent_power", "AC output apparent power", ac_output_apparent_power, Unit::VA),
+ LINE("ac_output_active_power", "AC output active power", ac_output_active_power, Unit::Wh),
+ LINE("total_ac_output_apparent_power", "Total AC output apparent power", total_ac_output_apparent_power, Unit::VA),
+ LINE("total_ac_output_active_power", "Total AC output active power", total_ac_output_active_power, Unit::Wh),
+ LINE("output_load_percent", "Output load percent", output_load_percent, Unit::Percentage),
+ LINE("total_output_load_percent", "Total output load percent", total_output_load_percent, Unit::Percentage),
+ LINE("battery_voltage", "Battery voltage", battery_voltage / 10.0, Unit::V),
+ LINE("battery_discharge_current", "Battery discharge current", battery_discharge_current, Unit::A),
+ LINE("battery_charging_current", "Battery charging current", battery_charging_current, Unit::A),
+ LINE("pv1_input_power", "PV1 Input power", pv1_input_power, Unit::Wh),
+ LINE("pv2_input_power", "PV2 Input power", pv2_input_power, Unit::Wh),
+ LINE("pv1_input_voltage", "PV1 Input voltage", pv1_input_voltage / 10.0, Unit::V),
+ LINE("pv2_input_voltage", "PV2 Input voltage", pv2_input_voltage / 10.0, Unit::V),
+ LINE("mppt1_charger_status", "MPPT1 charger status", mppt1_charger_status),
+ LINE("mppt2_charger_status", "MPPT2 charger status", mppt2_charger_status),
+ LINE("load_connected", "Load connection", std::string(load_connected ? "Connected" : "Disconnected")),
+ LINE("battery_power_direction", "Battery power direction", battery_power_direction),
+ LINE("dc_ac_power_direction", "DC/AC power direction", dc_ac_power_direction),
+ LINE("line_power_direction", "Line power direction", line_power_direction),
+ LINE("max_temp", "Max. temperature", max_temp),
+ })
+}
+
+
+void ACChargingTimeBucket::unpack() {
+ auto list = getList({4 /* AAAA */, 4 /* BBBB */});
+
+ start_h = stouh(list[0].substr(0, 2));
+ start_m = stouh(list[0].substr(2, 2));
+
+ end_h = stouh(list[1].substr(0, 2));
+ end_m = stouh(list[1].substr(2, 2));
+}
+
+static inline std::string get_time(unsigned short h, unsigned short m) {
+ std::ostringstream buf;
+ buf << std::setfill('0');
+ buf << std::setw(2) << h << ":" << std::setw(2) << m;
+ return buf.str();
+}
+
+formattable_ptr ACChargingTimeBucket::format(formatter::Format format) {
+ RETURN_TABLE({
+ LINE("start_time", "Start time", get_time(start_h, start_m)),
+ LINE("end_time", "End time", get_time(end_h, end_m)),
+ })
+}
+
+} \ No newline at end of file
diff --git a/src/p18/response.h b/src/p18/response.h
new file mode 100644
index 0000000..f9585d3
--- /dev/null
+++ b/src/p18/response.h
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_P18_RESPONSE_H
+#define INVERTER_TOOLS_P18_RESPONSE_H
+
+#include <string>
+#include <vector>
+#include <memory>
+#include <variant>
+#include <nlohmann/json.hpp>
+
+#include "types.h"
+#include "src/formatter/formatter.h"
+
+namespace p18::response_type {
+
+using nlohmann::json;
+
+typedef std::shared_ptr<formatter::Formattable> formattable_ptr;
+
+
+/**
+ * Value holder for the formatter module
+ */
+
+typedef std::variant<
+ unsigned,
+ unsigned short,
+ unsigned long,
+ bool,
+ double,
+ std::string,
+ p18::BatteryType,
+ p18::BatteryPowerDirection,
+ p18::ChargerSourcePriority,
+ p18::DC_AC_PowerDirection,
+ p18::InputVoltageRange,
+ p18::LinePowerDirection,
+ p18::MachineType,
+ p18::MPPTChargerStatus,
+ p18::Topology,
+ p18::OutputSourcePriority,
+ p18::OutputModelSetting,
+ p18::ParallelConnectionStatus,
+ p18::SolarPowerPriority,
+ p18::WorkingMode
+> Variant;
+
+class VariantHolder {
+private:
+ Variant v_;
+
+public:
+ // implicit conversion constructors
+ VariantHolder(unsigned v) : v_(v) {}
+ VariantHolder(unsigned short v) : v_(v) {}
+ VariantHolder(unsigned long v) : v_(v) {}
+ VariantHolder(bool v) : v_(v) {}
+ VariantHolder(double v) : v_(v) {}
+ VariantHolder(std::string v) : v_(v) {}
+ VariantHolder(p18::BatteryType v) : v_(v) {}
+ VariantHolder(p18::BatteryPowerDirection v) : v_(v) {}
+ VariantHolder(p18::ChargerSourcePriority v) : v_(v) {}
+ VariantHolder(p18::DC_AC_PowerDirection v) : v_(v) {}
+ VariantHolder(p18::InputVoltageRange v) : v_(v) {}
+ VariantHolder(p18::LinePowerDirection v) : v_(v) {}
+ VariantHolder(p18::MachineType v) : v_(v) {}
+ VariantHolder(p18::MPPTChargerStatus v) : v_(v) {}
+ VariantHolder(p18::Topology v) : v_(v) {}
+ VariantHolder(p18::OutputSourcePriority v) : v_(v) {}
+ VariantHolder(p18::OutputModelSetting v) : v_(v) {}
+ VariantHolder(p18::ParallelConnectionStatus v) : v_(v) {}
+ VariantHolder(p18::SolarPowerPriority v) : v_(v) {}
+ VariantHolder(p18::WorkingMode v) : v_(v) {}
+
+ friend std::ostream &operator<<(std::ostream &os, VariantHolder const& ref) {
+ std::visit([&os](const auto& elem) {
+ os << elem;
+ }, ref.v_);
+ return os;
+ }
+
+ inline json toJSON() const {
+ json j;
+ std::visit([&j](const auto& elem) {
+ j = elem;
+ }, v_);
+ return j;
+ }
+};
+
+
+/**
+ * Base responses
+ */
+
+class BaseResponse {
+protected:
+ std::shared_ptr<char> raw_;
+ size_t rawSize_;
+
+public:
+ BaseResponse(std::shared_ptr<char> raw, size_t rawSize);
+ virtual ~BaseResponse() = default;
+ virtual bool validate() = 0;
+ virtual void unpack() = 0;
+ virtual formattable_ptr format(formatter::Format format) = 0;
+};
+
+class GetResponse : public BaseResponse {
+protected:
+ const char* getData() const;
+ size_t getDataSize() const;
+ std::vector<std::string> getList(std::vector<size_t> itemLengths) const;
+
+public:
+ using BaseResponse::BaseResponse;
+ bool validate() override;
+// virtual void output() = 0;
+};
+
+class SetResponse : public BaseResponse {
+public:
+ using BaseResponse::BaseResponse;
+ void unpack() override;
+ bool validate() override;
+ formattable_ptr format(formatter::Format format) override;
+ bool get();
+};
+
+class ErrorResponse : public BaseResponse {
+private:
+ std::string error_;
+
+public:
+ explicit ErrorResponse(std::string error)
+ : BaseResponse(nullptr, 0), error_(std::move(error)) {}
+
+ bool validate() override {
+ return true;
+ }
+ void unpack() override {}
+ formattable_ptr format(formatter::Format format) override;
+};
+
+
+/**
+ * Actual typed responses
+ */
+
+class ProtocolID : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned id = 0;
+};
+
+class CurrentTime : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned year = 0;
+ unsigned short month = 0;
+ unsigned short day = 0;
+ unsigned short hour = 0;
+ unsigned short minute = 0;
+ unsigned short second = 0;
+};
+
+class TotalGenerated : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned long kwh = 0;
+};
+
+class YearGenerated : public TotalGenerated {
+public:
+ using TotalGenerated::TotalGenerated;
+};
+
+class MonthGenerated : public TotalGenerated {
+public:
+ using TotalGenerated::TotalGenerated;
+};
+
+class DayGenerated : public TotalGenerated {
+public:
+ using TotalGenerated::TotalGenerated;
+};
+
+class SeriesNumber : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ std::string id;
+};
+
+class CPUVersion : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ std::string main_cpu_version;
+ std::string slave1_cpu_version;
+ std::string slave2_cpu_version;
+};
+
+class RatedInformation : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned ac_input_rating_voltage; /* unit: 0.1V */
+ unsigned ac_input_rating_current; /* unit: 0.1A */
+ unsigned ac_output_rating_voltage; /* unit: 0.1A */
+ unsigned ac_output_rating_freq; /* unit: 0.1Hz */
+ unsigned ac_output_rating_current; /* unit: 0.1A */
+ unsigned ac_output_rating_apparent_power; /* unit: VA */
+ unsigned ac_output_rating_active_power; /* unit: W */
+ unsigned battery_rating_voltage; /* unit: 0.1V */
+ unsigned battery_recharge_voltage; /* unit: 0.1V */
+ unsigned battery_redischarge_voltage; /* unit: 0.1V */
+ unsigned battery_under_voltage; /* unit: 0.1V */
+ unsigned battery_bulk_voltage; /* unit: 0.1V */
+ unsigned battery_float_voltage; /* unit: 0.1V */
+ p18::BatteryType battery_type;
+ unsigned max_ac_charging_current; /* unit: A */
+ unsigned max_charging_current; /* unit: A */
+ p18::InputVoltageRange input_voltage_range;
+ p18::OutputModelSetting output_source_priority;
+ p18::ChargerSourcePriority charger_source_priority;
+ unsigned parallel_max_num;
+ p18::MachineType machine_type;
+ p18::Topology topology;
+ p18::OutputModelSetting output_model_setting;
+ p18::SolarPowerPriority solar_power_priority;
+ std::string mppt;
+};
+
+class GeneralStatus : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned grid_voltage; /* unit: 0.1V */
+ unsigned grid_freq; /* unit: 0.1Hz */
+ unsigned ac_output_voltage; /* unit: 0.1V */
+ unsigned ac_output_freq; /* unit: 0.1Hz */
+ unsigned ac_output_apparent_power; /* unit: VA */
+ unsigned ac_output_active_power; /* unit: W */
+ unsigned output_load_percent; /* unit: % */
+ unsigned battery_voltage; /* unit: 0.1V */
+ unsigned battery_voltage_scc; /* unit: 0.1V */
+ unsigned battery_voltage_scc2; /* unit: 0.1V */
+ unsigned battery_discharge_current; /* unit: A */
+ unsigned battery_charging_current; /* unit: A */
+ unsigned battery_capacity; /* unit: % */
+ unsigned inverter_heat_sink_temp; /* unit: C */
+ unsigned mppt1_charger_temp; /* unit: C */
+ unsigned mppt2_charger_temp; /* unit: C */
+ unsigned pv1_input_power; /* unit: W */
+ unsigned pv2_input_power; /* unit: W */
+ unsigned pv1_input_voltage; /* unit: 0.1V */
+ unsigned pv2_input_voltage; /* unit: 0.1V */
+ bool settings_values_changed; /* inverter returns:
+ 0: nothing changed
+ 1: something changed */
+ p18::MPPTChargerStatus mppt1_charger_status;
+ p18::MPPTChargerStatus mppt2_charger_status;
+ bool load_connected; /* inverter returns:
+ 0: disconnected
+ 1: connected */
+ p18::BatteryPowerDirection battery_power_direction;
+ p18::DC_AC_PowerDirection dc_ac_power_direction;
+ p18::LinePowerDirection line_power_direction;
+ unsigned local_parallel_id; /* 0 .. (parallel number - 1) */
+};
+
+class WorkingMode : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ p18::WorkingMode mode = static_cast<p18::WorkingMode>(0);
+};
+
+class FaultsAndWarnings : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned fault_code = 0;
+ bool line_fail = false;
+ bool output_circuit_short = false;
+ bool inverter_over_temperature = false;
+ bool fan_lock = false;
+ bool battery_voltage_high = false;
+ bool battery_low = false;
+ bool battery_under = false;
+ bool over_load = false;
+ bool eeprom_fail = false;
+ bool power_limit = false;
+ bool pv1_voltage_high = false;
+ bool pv2_voltage_high = false;
+ bool mppt1_overload_warning = false;
+ bool mppt2_overload_warning = false;
+ bool battery_too_low_to_charge_for_scc1 = false;
+ bool battery_too_low_to_charge_for_scc2 = false;
+};
+
+class FlagsAndStatuses : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ bool buzzer = false;
+ bool overload_bypass = false;
+ bool lcd_escape_to_default_page_after_1min_timeout = false;
+ bool overload_restart = false;
+ bool over_temp_restart = false;
+ bool backlight_on = false;
+ bool alarm_on_primary_source_interrupt = false;
+ bool fault_code_record = false;
+ char reserved = '0';
+};
+
+class Defaults : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned ac_output_voltage = 0; /* unit: 0.1V */
+ unsigned ac_output_freq = 0;
+ p18::InputVoltageRange ac_input_voltage_range = static_cast<InputVoltageRange>(0);
+ unsigned battery_under_voltage = 0;
+ unsigned charging_float_voltage = 0;
+ unsigned charging_bulk_voltage = 0;
+ unsigned battery_recharge_voltage = 0;
+ unsigned battery_redischarge_voltage = 0;
+ unsigned max_charging_current = 0;
+ unsigned max_ac_charging_current = 0;
+ p18::BatteryType battery_type = static_cast<BatteryType>(0);
+ p18::OutputSourcePriority output_source_priority = static_cast<OutputSourcePriority>(0);
+ p18::ChargerSourcePriority charger_source_priority = static_cast<ChargerSourcePriority>(0);
+ p18::SolarPowerPriority solar_power_priority = static_cast<SolarPowerPriority>(0);
+ p18::MachineType machine_type = static_cast<MachineType>(0);
+ p18::OutputModelSetting output_model_setting = static_cast<OutputModelSetting>(0);
+ bool flag_buzzer = false;
+ bool flag_overload_restart = false;
+ bool flag_over_temp_restart = false;
+ bool flag_backlight_on = false;
+ bool flag_alarm_on_primary_source_interrupt = false;
+ bool flag_fault_code_record = false;
+ bool flag_overload_bypass = false;
+ bool flag_lcd_escape_to_default_page_after_1min_timeout = false;
+};
+
+class AllowedChargingCurrents : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ std::vector<unsigned> amps;
+};
+
+class AllowedACChargingCurrents : public AllowedChargingCurrents {
+public:
+ using AllowedChargingCurrents::AllowedChargingCurrents;
+};
+
+class ParallelRatedInformation : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ p18::ParallelConnectionStatus parallel_id_connection_status = static_cast<ParallelConnectionStatus>(0);
+ unsigned serial_number_valid_length = 0;
+ std::string serial_number;
+ p18::ChargerSourcePriority charger_source_priority = static_cast<ChargerSourcePriority>(0);
+ unsigned max_ac_charging_current = 0; // unit: A
+ unsigned max_charging_current = 0; // unit: A
+ p18::OutputModelSetting output_model_setting = static_cast<OutputModelSetting>(0);
+};
+
+class ParallelGeneralStatus : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ p18::ParallelConnectionStatus parallel_id_connection_status;
+ p18::WorkingMode work_mode;
+ unsigned fault_code;
+ unsigned grid_voltage; /* unit: 0.1V */
+ unsigned grid_freq; /* unit: 0.1Hz */
+ unsigned ac_output_voltage; /* unit: 0.1V */
+ unsigned ac_output_freq; /* unit: 0.1Hz */
+ unsigned ac_output_apparent_power; /* unit: VA */
+ unsigned ac_output_active_power; /* unit: W */
+ unsigned total_ac_output_apparent_power; /* unit: VA */
+ unsigned total_ac_output_active_power; /* unit: W */
+ unsigned output_load_percent; /* unit: % */
+ unsigned total_output_load_percent; /* unit: % */
+ unsigned battery_voltage; /* unit: 0.1V */
+ unsigned battery_discharge_current; /* unit: A */
+ unsigned battery_charging_current; /* unit: A */
+ unsigned total_battery_charging_current; /* unit: A */
+ unsigned battery_capacity; /* unit: % */
+ unsigned pv1_input_power; /* unit: W */
+ unsigned pv2_input_power; /* unit: W */
+ unsigned pv1_input_voltage; /* unit: 0.1V */
+ unsigned pv2_input_voltage; /* unit: 0.1V */
+ p18::MPPTChargerStatus mppt1_charger_status;
+ p18::MPPTChargerStatus mppt2_charger_status;
+ bool load_connected; /* inverter returns:
+ 0: disconnected
+ 1: connected */
+ p18::BatteryPowerDirection battery_power_direction;
+ p18::DC_AC_PowerDirection dc_ac_power_direction;
+ p18::LinePowerDirection line_power_direction;
+ unsigned max_temp; /* unit: C */
+};
+
+class ACChargingTimeBucket : public GetResponse {
+public:
+ using GetResponse::GetResponse;
+ void unpack() override;
+ formattable_ptr format(formatter::Format format) override;
+
+ unsigned short start_h = 0;
+ unsigned short start_m = 0;
+ unsigned short end_h = 0;
+ unsigned short end_m = 0;
+};
+
+class ACLoadsSupplyTimeBucket : public ACChargingTimeBucket {
+public:
+ using ACChargingTimeBucket::ACChargingTimeBucket;
+};
+
+} // namespace p18
+
+#endif //INVERTER_TOOLS_P18_RESPONSE_H
diff --git a/src/p18/types.h b/src/p18/types.h
new file mode 100644
index 0000000..69e95e5
--- /dev/null
+++ b/src/p18/types.h
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_P18_TYPES_H
+#define INVERTER_TOOLS_P18_TYPES_H
+
+#include <string>
+
+#define ENUM_STR(enum_type) std::ostream& operator<< (std::ostream& os, enum_type val)
+#define ENUM_STR_DEFAULT return os << val
+
+namespace p18 {
+
+enum class CommandType {
+ GetProtocolID = 0,
+ GetCurrentTime,
+ GetTotalGenerated,
+ GetYearGenerated,
+ GetMonthGenerated,
+ GetDayGenerated,
+ GetSeriesNumber,
+ GetCPUVersion,
+ GetRatedInformation,
+ GetGeneralStatus,
+ GetWorkingMode,
+ GetFaultsAndWarnings,
+ GetFlagsAndStatuses,
+ GetDefaults,
+ GetAllowedChargingCurrents,
+ GetAllowedACChargingCurrents,
+ GetParallelRatedInformation,
+ GetParallelGeneralStatus,
+ GetACChargingTimeBucket,
+ GetACLoadsSupplyTimeBucket,
+ SetLoads = 100,
+ SetFlag,
+ SetDefaults,
+ SetBatteryMaxChargingCurrent,
+ SetBatteryMaxACChargingCurrent,
+ SetACOutputFreq,
+ SetBatteryMaxChargingVoltage,
+ SetACOutputRatedVoltage,
+ SetOutputSourcePriority,
+ SetBatteryChargingThresholds, /* Battery re-charging and re-discharing voltage when utility is available */
+ SetChargingSourcePriority,
+ SetSolarPowerPriority,
+ SetACInputVoltageRange,
+ SetBatteryType,
+ SetOutputModel,
+ SetBatteryCutOffVoltage,
+ SetSolarConfig,
+ ClearGenerated,
+ SetDateTime,
+ SetACChargingTimeBucket,
+ SetACLoadsSupplyTimeBucket,
+};
+
+enum class BatteryType {
+ AGM = 0,
+ Flooded = 1,
+ User = 2,
+};
+ENUM_STR(BatteryType);
+
+enum class InputVoltageRange {
+ Appliance = 0,
+ USP = 1,
+};
+ENUM_STR(InputVoltageRange);
+
+enum class OutputSourcePriority {
+ SolarUtilityBattery = 0,
+ SolarBatteryUtility = 1,
+};
+ENUM_STR(OutputSourcePriority);
+
+enum class ChargerSourcePriority {
+ SolarFirst = 0,
+ SolarAndUtility = 1,
+ SolarOnly = 2,
+};
+ENUM_STR(ChargerSourcePriority);
+
+enum class MachineType {
+ OffGridTie = 0,
+ GridTie = 1,
+};
+ENUM_STR(MachineType);
+
+enum class Topology {
+ TransformerLess = 0,
+ Transformer = 1,
+};
+ENUM_STR(Topology);
+
+enum class OutputModelSetting {
+ SingleModule = 0,
+ ParallelOutput = 1,
+ Phase1OfThreePhaseOutput = 2,
+ Phase2OfThreePhaseOutput = 3,
+ Phase3OfThreePhaseOutput = 4,
+};
+ENUM_STR(OutputModelSetting);
+
+enum class SolarPowerPriority {
+ BatteryLoadUtility = 0,
+ LoadBatteryUtility = 1,
+};
+ENUM_STR(SolarPowerPriority);
+
+enum class MPPTChargerStatus {
+ Abnormal = 0,
+ NotCharging = 1,
+ Charging = 2,
+};
+ENUM_STR(MPPTChargerStatus);
+
+enum class BatteryPowerDirection {
+ DoNothing = 0,
+ Charge = 1,
+ Discharge = 2,
+};
+ENUM_STR(BatteryPowerDirection);
+
+enum class DC_AC_PowerDirection {
+ DoNothing = 0,
+ AC_DC = 1,
+ DC_AC = 2,
+};
+ENUM_STR(DC_AC_PowerDirection);
+
+enum class LinePowerDirection {
+ DoNothing = 0,
+ Input = 1,
+ Output = 2,
+};
+ENUM_STR(LinePowerDirection);
+
+enum class WorkingMode {
+ PowerOnMode = 0,
+ StandbyMode = 1,
+ BypassMode = 2,
+ BatteryMode = 3,
+ FaultMode = 4,
+ HybridMode = 5,
+};
+ENUM_STR(WorkingMode);
+
+enum class ParallelConnectionStatus {
+ NotExistent = 0,
+ Existent = 1,
+};
+ENUM_STR(ParallelConnectionStatus);
+
+struct Flag {
+ std::string flag;
+ char letter;
+ std::string description;
+};
+
+}
+
+#endif //INVERTER_TOOLS_P18_TYPES_H
diff --git a/src/server/connection.cc b/src/server/connection.cc
new file mode 100644
index 0000000..c662d6d
--- /dev/null
+++ b/src/server/connection.cc
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <stdexcept>
+#include <unistd.h>
+#include <ios>
+#include <arpa/inet.h>
+#include <cerrno>
+
+#include "connection.h"
+#include "../p18/commands.h"
+#include "../p18/response.h"
+#include "../logging.h"
+#include "../common.h"
+#include "hexdump/hexdump.h"
+#include "signal.h"
+
+#define CHECK_ARGUMENTS_LENGTH(__size__) \
+ if (arguments.size() != (__size__)) { \
+ std::ostringstream error; \
+ error << "invalid arguments count: expected " << (__size__) << ", got " << arguments.size(); \
+ throw std::invalid_argument(error.str()); \
+ }
+
+#define CHECK_ARGUMENTS_MIN_LENGTH(__size__) \
+ if (arguments.size() < (__size__)) { \
+ std::ostringstream error; \
+ error << "invalid arguments count: expected " << (__size__) << ", got " << arguments.size(); \
+ throw std::invalid_argument(error.str()); \
+ }
+
+
+namespace server {
+
+Connection::Connection(int sock, struct sockaddr_in addr, Server* server)
+ : sock_(sock), addr_(addr), server_(server)
+{
+ if (server_->verbose())
+ mylog << "new connection from " << ipv4();
+
+ thread_ = std::thread(&Connection::run, this);
+ thread_.detach();
+}
+
+Connection::~Connection() {
+ if (server_->verbose())
+ mylog << "closing socket..";
+
+ if (close(sock_) == -1)
+ myerr << ipv4() << ": close: " << strerror(errno);
+
+ server_->removeConnection(this);
+}
+
+void Connection::run() {
+ static int bufSize = 2048;
+ char buf[bufSize];
+
+ while (true) {
+ long rcvd = readLoop(buf, bufSize - 1);
+ if (rcvd == -1) {
+ if (errno != EINTR && server_->verbose())
+ myerr << ipv4() << ": recv: " << std::string(strerror(errno));
+ break;
+ }
+ if (rcvd == 0)
+ break;
+
+ buf[rcvd] = '\0';
+ if (*buf == '\4')
+ break;
+
+ Response resp = processRequest(buf);
+ if (!sendResponse(resp))
+ break;
+ }
+
+ delete this;
+}
+
+int Connection::readLoop(char* buf, size_t bufSize) const {
+ char* bufptr = buf;
+ int left = static_cast<int>(bufSize);
+ int readed = 0;
+
+ while (left > 0) {
+ size_t rcvd = recv(sock_, bufptr, left, 0);
+ if (rcvd == -1)
+ return -1;
+ if (rcvd == 0)
+ break;
+
+ readed += static_cast<int>(rcvd);
+ if (*bufptr == '\4')
+ break;
+
+ left -= static_cast<int>(rcvd);
+ bufptr += rcvd;
+
+ bufptr[rcvd] = '\0';
+ char* ptr = strstr(buf, "\r\n");
+ if (ptr)
+ break;
+ }
+
+ return readed;
+}
+
+bool Connection::writeLoop(const char* buf, size_t bufSize) const {
+ const char* bufptr = buf;
+ int left = static_cast<int>(bufSize);
+
+ while (left > 0) {
+ size_t bytesSent = send(sock_, bufptr, left, 0);
+ if (bytesSent == -1) {
+ if (errno != EINTR && server_->verbose())
+ myerr << ipv4() << ": send: " << std::string(strerror(errno));
+ return false;
+ }
+
+ left -= static_cast<int>(bytesSent);
+ bufptr += bytesSent;
+ }
+
+ return true;
+}
+
+bool Connection::sendResponse(Response& resp) const {
+ std::ostringstream sbuf;
+ sbuf << resp;
+
+ std::string s = sbuf.str();
+ const char* buf = s.c_str();
+ size_t bufSize = s.size();
+
+ return writeLoop(buf, bufSize);
+}
+
+std::string Connection::ipv4() const {
+ char ip[INET_ADDRSTRLEN] = {0};
+ const char* result = inet_ntop(AF_INET, (const void*)&addr_.sin_addr, ip, sizeof(ip));
+ if (result == nullptr)
+ return "?";
+
+ std::ostringstream buf;
+ buf << ip << ":" << htons(addr_.sin_port);
+ return buf.str();
+}
+
+Response Connection::processRequest(char* buf) {
+ std::stringstream sbuf;
+ int n = 0;
+ std::vector<std::string> arguments;
+ RequestType type;
+
+ Response resp;
+ resp.type = ResponseType::OK;
+
+ try {
+ char* last = nullptr;
+ const char* delim = " ";
+ for (char* token = strtok_r(buf, delim, &last);
+ token != nullptr;
+ token = strtok_r(nullptr, delim, &last)) {
+
+ char* ptr = strstr(token, "\r\n");
+ if (ptr)
+ *ptr = '\0';
+
+ if (!n++) {
+ std::string s = std::string(token);
+
+ if (s == "format")
+ type = RequestType::Format;
+
+ else if (s == "v")
+ type = RequestType::Version;
+
+ else if (s == "exec")
+ type = RequestType::Execute;
+
+ else if (s == "raw")
+ type = RequestType::Raw;
+
+ else
+ throw std::invalid_argument("invalid token: " + s);
+
+ } else if (strlen(token) > 0)
+ arguments.emplace_back(token);
+ }
+
+ switch (type) {
+ case RequestType::Version: {
+ CHECK_ARGUMENTS_LENGTH(1)
+ auto v = static_cast<unsigned>(std::stoul(arguments[0]));
+ if (v != 1)
+ throw std::invalid_argument("invalid protocol version");
+ options_.version = v;
+ break;
+ }
+
+ case RequestType::Format:
+ CHECK_ARGUMENTS_LENGTH(1)
+ options_.format = format_from_string(arguments[0]);
+ break;
+
+ case RequestType::Execute: {
+ CHECK_ARGUMENTS_MIN_LENGTH(1)
+
+ std::string& command = arguments[0];
+ auto commandArguments = std::vector<std::string>();
+
+ auto argumentsSlice = std::vector<std::string>(arguments.begin()+1, arguments.end());
+
+ p18::CommandInput input{&argumentsSlice};
+ p18::CommandType commandType = p18::validate_input(command, commandArguments, (void*)&input);
+
+ auto response = server_->executeCommand(commandType, commandArguments);
+ resp.buf << *(response->format(options_.format).get());
+
+ break;
+ }
+
+ case RequestType::Raw: {
+ throw std::runtime_error("not implemented");
+// CHECK_ARGUMENTS_LENGTH(1)
+// std::string& raw = arguments[0];
+//
+// resp.type = ResponseType::Error;
+// resp.buf << "not implemented";
+ break;
+ }
+ }
+ }
+ // we except std::invalid_argument and std::runtime_error
+ catch (std::exception& e) {
+ resp.type = ResponseType::Error;
+
+ auto err = p18::response_type::ErrorResponse(e.what());
+ resp.buf << *(err.format(options_.format));
+ }
+
+ return resp;
+}
+
+std::ostream& operator<<(std::ostream& os, Response& resp) {
+ os << (resp.type == ResponseType::OK ? "ok" : "err");
+
+ resp.buf.seekp(0, std::ios::end);
+ size_t size = resp.buf.tellp();
+ if (size) {
+ resp.buf.seekp(0);
+ os << "\r\n" << resp.buf.str();
+ }
+
+ return os << "\r\n\r\n";
+}
+
+} \ No newline at end of file
diff --git a/src/server/connection.h b/src/server/connection.h
new file mode 100644
index 0000000..eb28d51
--- /dev/null
+++ b/src/server/connection.h
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_CONNECTION_H
+#define INVERTER_TOOLS_CONNECTION_H
+
+#include <thread>
+#include <netinet/in.h>
+#include <sstream>
+
+#include "server.h"
+#include "../formatter/formatter.h"
+
+namespace server {
+
+class Server;
+struct Response;
+
+struct ConnectionOptions {
+ ConnectionOptions()
+ : version(1), format(formatter::Format::JSON)
+ {}
+
+ unsigned version;
+ formatter::Format format;
+};
+
+
+class Connection {
+private:
+ int sock_;
+ std::thread thread_;
+ struct sockaddr_in addr_;
+ Server* server_;
+ ConnectionOptions options_;
+
+public:
+ explicit Connection(int sock, struct sockaddr_in addr, Server* server);
+ ~Connection();
+ void run();
+ std::string ipv4() const;
+ bool sendResponse(Response& resp) const;
+ int readLoop(char* buf, size_t bufSize) const;
+ bool writeLoop(const char* buf, size_t bufSize) const;
+ Response processRequest(char* buf);
+};
+
+
+enum class RequestType {
+ Version,
+ Format,
+ Execute,
+ Raw,
+};
+
+
+enum class ResponseType {
+ OK,
+ Error
+};
+
+
+struct Response {
+ ResponseType type;
+ std::ostringstream buf;
+};
+std::ostream& operator<<(std::ostream& os, Response& resp);
+
+}
+
+#endif //INVERTER_TOOLS_CONNECTION_H
diff --git a/src/server/server.cc b/src/server/server.cc
new file mode 100644
index 0000000..981104d
--- /dev/null
+++ b/src/server/server.cc
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <cstring>
+#include <string>
+#include <cerrno>
+#include <algorithm>
+#include <memory>
+#include <utility>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "../voltronic/exceptions.h"
+#include "../p18/exceptions.h"
+#include "../voltronic/time.h"
+#include "../logging.h"
+//#include "hexdump/hexdump.h"
+#include "server.h"
+#include "connection.h"
+#include "signal.h"
+
+namespace server {
+
+Server::Server(std::shared_ptr<voltronic::Device> device)
+ : sock_(0)
+ , port_(0)
+ , cacheTimeout_(CACHE_TIMEOUT)
+ , verbose_(false)
+ , device_(std::move(device)) {
+ client_.setDevice(device_);
+}
+
+void Server::setVerbose(bool verbose) {
+ verbose_ = verbose;
+ device_->setVerbose(verbose);
+}
+
+void Server::setCacheTimeout(u64 timeout) {
+ cacheTimeout_ = timeout;
+}
+
+Server::~Server() {
+ if (sock_ > 0)
+ close(sock_);
+}
+
+void Server::start(std::string& host, int port) {
+ host_ = host;
+ port_ = port;
+
+ sock_ = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock_ == -1)
+ throw ServerError("failed to create socket");
+
+ struct linger sl = {0};
+ sl.l_onoff = 1;
+ sl.l_linger = 0;
+ if (setsockopt(sock_, SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)) == -1)
+ throw ServerError("setsockopt(linger): " + std::string(strerror(errno)));
+
+ int flag = 1;
+ if (setsockopt(sock_, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) == -1)
+ throw ServerError("setsockopt(reuseaddr): " + std::string(strerror(errno)));
+
+ struct sockaddr_in serv_addr = {0};
+ serv_addr.sin_family = AF_INET;
+ serv_addr.sin_addr.s_addr = inet_addr(host_.c_str());
+ serv_addr.sin_port = htons(port_);
+ memset(serv_addr.sin_zero, 0, sizeof(serv_addr.sin_zero));
+
+ if (bind(sock_, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
+ throw ServerError("bind: " + std::string(strerror(errno)));
+
+ if (listen(sock_, 50))
+ throw ServerError("start: " + std::string(strerror(errno)));
+
+ while (!shutdownCaught) {
+ struct sockaddr_in addr = {0};
+ socklen_t addr_size = sizeof(addr);
+
+ if (verbose_)
+ mylog << "waiting for client..";
+
+ int sock = accept(sock_, (struct sockaddr*)&addr, &addr_size);
+ if (sock == -1)
+ continue;
+
+ auto conn = new Connection(sock, addr, this);
+ addConnection(conn);
+ }
+}
+
+void Server::addConnection(Connection *conn) {
+ if (verbose_)
+ myerr << "adding " << conn->ipv4();
+ LockGuard lock(threads_mutex_);
+ connections_.emplace_back(conn);
+}
+
+void Server::removeConnection(Connection *conn) {
+ if (verbose_)
+ myerr << "removing " << conn->ipv4();
+ LockGuard lock(threads_mutex_);
+ connections_.erase(std::remove(connections_.begin(), connections_.end(), conn), connections_.end());
+}
+
+size_t Server::getConnectionsCount() const {
+ return connections_.size();
+}
+
+std::shared_ptr<p18::response_type::BaseResponse> Server::executeCommand(p18::CommandType commandType, std::vector<std::string>& arguments) {
+ LockGuard lock(client_mutex_);
+
+ auto it = cache_.find(commandType);
+ if (it != cache_.end()) {
+ auto cr = it->second;
+ if (voltronic::timestamp() - cr.time <= cacheTimeout_) {
+ return cr.response;
+ }
+ }
+
+ try {
+ auto response = client_.execute(commandType, arguments);
+ CachedResponse cr{voltronic::timestamp(), response};
+ cache_[commandType] = cr;
+ return response;
+ }
+ catch (voltronic::DeviceError& e) {
+ throw std::runtime_error("device error: " + std::string(e.what()));
+ }
+ catch (voltronic::TimeoutError& e) {
+ throw std::runtime_error("timeout: " + std::string(e.what()));
+ }
+ catch (voltronic::InvalidDataError& e) {
+ throw std::runtime_error("data is invalid: " + std::string(e.what()));
+ }
+ catch (p18::InvalidResponseError& e) {
+ throw std::runtime_error("response is invalid: " + std::string(e.what()));
+ }
+}
+
+
+} \ No newline at end of file
diff --git a/src/server/server.h b/src/server/server.h
new file mode 100644
index 0000000..76b2a1b
--- /dev/null
+++ b/src/server/server.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_SERVER_TCP_SERVER_H
+#define INVERTER_TOOLS_SERVER_TCP_SERVER_H
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <thread>
+#include <mutex>
+#include <csignal>
+#include <atomic>
+#include <netinet/in.h>
+
+#include "connection.h"
+#include "../numeric_types.h"
+#include "../formatter/formatter.h"
+#include "../p18/client.h"
+#include "../p18/types.h"
+#include "../voltronic/device.h"
+#include "../voltronic/time.h"
+
+namespace server {
+
+typedef std::lock_guard<std::mutex> LockGuard;
+
+class Connection;
+
+struct CachedResponse {
+ u64 time;
+ std::shared_ptr<p18::response_type::BaseResponse> response;
+};
+
+class Server {
+private:
+ int sock_;
+ std::string host_;
+ int port_;
+ bool verbose_;
+ p18::Client client_;
+ std::shared_ptr<voltronic::Device> device_;
+
+ u64 cacheTimeout_;
+ std::map<p18::CommandType, CachedResponse> cache_;
+
+ std::mutex threads_mutex_;
+ std::mutex client_mutex_;
+
+ std::vector<Connection*> connections_;
+
+public:
+ static const u64 CACHE_TIMEOUT = 1000;
+
+ volatile std::atomic<bool> sigCaught = 0;
+
+ explicit Server(std::shared_ptr<voltronic::Device> device);
+ ~Server();
+
+ void setVerbose(bool verbose);
+ void setCacheTimeout(u64 timeout);
+ void start(std::string& host, int port);
+
+ bool verbose() const { return verbose_; }
+ void addConnection(Connection* conn);
+ void removeConnection(Connection* conn);
+ size_t getConnectionsCount() const;
+
+ std::shared_ptr<p18::response_type::BaseResponse> executeCommand(p18::CommandType commandType, std::vector<std::string>& arguments);
+};
+
+
+class ServerError : public std::runtime_error {
+public:
+ using std::runtime_error::runtime_error;
+};
+
+}
+
+#endif //INVERTER_TOOLS_SERVER_TCP_SERVER_H
diff --git a/src/server/signal.cc b/src/server/signal.cc
new file mode 100644
index 0000000..ea7ae3e
--- /dev/null
+++ b/src/server/signal.cc
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "signal.h"
+
+namespace server {
+
+volatile sig_atomic_t shutdownCaught = 0;
+
+static void sighandler(int) {
+ shutdownCaught = 1;
+}
+
+void set_signal_handlers() {
+ struct sigaction sa = {0};
+ sa.sa_handler = sighandler;
+ sigaction(SIGTERM, &sa, nullptr);
+ sigaction(SIGINT, &sa, nullptr);
+}
+
+} \ No newline at end of file
diff --git a/src/server/signal.h b/src/server/signal.h
new file mode 100644
index 0000000..0a21715
--- /dev/null
+++ b/src/server/signal.h
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_SIGNAL_H
+#define INVERTER_TOOLS_SIGNAL_H
+
+#include <csignal>
+
+namespace server {
+
+extern volatile sig_atomic_t shutdownCaught;
+
+void set_signal_handlers();
+
+}
+
+#endif //INVERTER_TOOLS_SIGNAL_H
diff --git a/src/testserial.cc b/src/testserial.cc
new file mode 100644
index 0000000..9a09d02
--- /dev/null
+++ b/src/testserial.cc
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: BSD-3-Clause
+//
+// This is a test program, used to test libserialport.
+// You don't need it.
+
+#include <libserialport.h>
+#include <cstdio>
+#include <cstdlib>
+#include <hexdump.h>
+#include <iostream>
+#include <unistd.h>
+
+#define BUFSIZE 256
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+void die_on_fail(const char* s, int result) {
+ if (result != SP_OK) {
+ fprintf(stderr, "%s failed\n", s);
+ exit(1);
+ }
+}
+
+
+static const unsigned char PI[] = {
+ 0x5e, 0x50, 0x30, 0x30, 0x35, 0x50, 0x49,
+ 0x71, 0x8b, 0x0d
+};
+static const unsigned char GS[] = {
+ 0x5e, 0x50, 0x30, 0x30, 0x35, 0x47, 0x53,
+ 0x58, 0x14, 0x0d
+};
+
+int main(int argc, char** argv) {
+ struct sp_port* port;
+ struct sp_port_config *config;
+ char buf[BUFSIZE] = {0};
+
+ die_on_fail("sp_get_port_by_name", sp_get_port_by_name("/dev/ttyUSB0", &port));
+ die_on_fail("sp_open", sp_open(port, SP_MODE_READ_WRITE));
+
+ printf("configuring...\n");
+
+ die_on_fail("sp_new_config", sp_new_config(&config));
+ die_on_fail("sp_get_config", sp_get_config(port, config));
+
+ die_on_fail("sp_set_config_baudrate", sp_set_config_baudrate(config, 2400));
+ die_on_fail("sp_set_config_stopbits", sp_set_config_stopbits(config, 1));
+ die_on_fail("sp_set_config_bits", sp_set_config_bits(config, 8));
+ die_on_fail("sp_set_config_parity", sp_set_config_parity(config, SP_PARITY_NONE));
+ die_on_fail("sp_set_config_flowcontrol", sp_set_config_flowcontrol(config, SP_FLOWCONTROL_NONE));
+ die_on_fail("sp_set_config", sp_set_config(port, config));
+
+ printf("configured.\n");
+ sp_flush(port, SP_BUF_BOTH);
+
+ printf("writing %lu bytes...\n", ARRAY_SIZE(PI));
+ int written = sp_blocking_write(port, PI, ARRAY_SIZE(PI), 0);
+ printf("%d bytes written\n", written);
+
+ usleep(200000);
+
+ printf("reading...\n");
+ int read = sp_blocking_read_next(port, buf, ARRAY_SIZE(buf), 0);
+ printf("got %d bytes:\n", read);
+ std::cout << hexdump(buf, read) << std::endl;
+
+ printf("cleaning up...\n");
+
+ sp_free_config(config);
+ sp_free_port(port);
+
+ return 0;
+} \ No newline at end of file
diff --git a/src/util.cc b/src/util.cc
new file mode 100644
index 0000000..baaaef3
--- /dev/null
+++ b/src/util.cc
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <string>
+#include <algorithm>
+#include <memory>
+#include <stdexcept>
+#include <cstdlib>
+#include <cxxabi.h>
+
+#include "util.h"
+
+bool is_numeric(const std::string& s) {
+ std::string::const_iterator it = s.begin();
+ while (it != s.end() && std::isdigit(*it)) ++it;
+ return !s.empty() && it == s.end();
+}
+
+bool is_date_valid(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));
+}
+
+std::vector<std::string> split(const std::string& s, char separator) {
+ std::vector<std::string> output;
+ std::string::size_type prev_pos = 0, pos = 0;
+
+ while ((pos = s.find(separator, pos)) != std::string::npos) {
+ std::string substring(s.substr(prev_pos, pos-prev_pos));
+ output.push_back(substring);
+ prev_pos = ++pos;
+ }
+
+ output.push_back(s.substr(prev_pos, pos-prev_pos));
+
+ return output;
+}
+
+unsigned stou(const std::string& s) {
+ return static_cast<unsigned>(std::stoul(s));
+}
+
+unsigned short stouh(const std::string& s) {
+ return static_cast<unsigned short>(std::stoul(s));
+}
+
+bool string_has(std::string& s, char c) {
+ return s.find(c) != std::string::npos;
+}
+
+unsigned long hextoul(std::string& s) {
+ // strtol will store a pointer to first invalid character here
+ char* endptr = nullptr;
+
+ unsigned long n = strtol(s.c_str(), &endptr, 16);
+ if (*endptr != 0)
+ throw std::invalid_argument("input string is not a hex number");
+
+ return n;
+}
+
+// https://stackoverflow.com/questions/281818/unmangling-the-result-of-stdtype-infoname
+std::string demangle_type_name(const char* name) {
+ int status = -4; // some arbitrary value to eliminate the compiler warning
+
+ std::unique_ptr<char, void(*)(void*)> res {
+ abi::__cxa_demangle(name, nullptr, nullptr, &status),
+ std::free
+ };
+
+ return status == 0 ? res.get() : name;
+} \ No newline at end of file
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..e9c3730
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_UTIL_H
+#define INVERTER_TOOLS_UTIL_H
+
+#include <string>
+#include <vector>
+#include <algorithm>
+
+bool is_numeric(const std::string& s);
+bool is_date_valid(int y, int m, int d);
+
+template <typename T, typename P>
+long index_of(T& haystack, P& needle)
+{
+ auto _it = std::find(haystack.begin(), haystack.end(), needle);
+ if (_it == haystack.end())
+ return -1;
+ return std::distance(haystack.begin(), _it);
+}
+
+std::vector<std::string> split(const std::string& s, char separator);
+unsigned stou(const std::string& s);
+unsigned short stouh(const std::string& s);
+
+bool string_has(std::string& s, char c);
+unsigned long hextoul(std::string& s);
+
+std::string demangle_type_name(const char* name);
+
+#endif //INVERTER_TOOLS_UTIL_H
diff --git a/src/voltronic/crc.cc b/src/voltronic/crc.cc
new file mode 100644
index 0000000..485fbf5
--- /dev/null
+++ b/src/voltronic/crc.cc
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "crc.h"
+
+namespace voltronic {
+
+static const u16 table[16] = {
+ 0x0000, 0x1021, 0x2042, 0x3063,
+ 0x4084, 0x50A5, 0x60C6, 0x70E7,
+ 0x8108, 0x9129, 0xA14A, 0xB16B,
+ 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF
+};
+
+static inline bool is_reserved(u8 b) {
+ return b == 0x28 || b == 0x0D || b == 0x0A;
+}
+
+CRC crc_read(const u8* buf) {
+ CRC crc = 0;
+
+ crc |= (u16) buf[0] << 8;
+ crc |= (u16) buf[1];
+
+ return crc;
+}
+
+void crc_write(CRC crc, u8* buffer) {
+ if (buffer != nullptr) {
+ buffer[0] = (crc >> 8) & 0xFF;
+ buffer[1] = crc & 0xFF;
+ }
+}
+
+CRC crc_calculate(const u8* buf, size_t bufSize) {
+ CRC crc = 0;
+
+ if (bufSize > 0) {
+ u8 byte;
+ do {
+ byte = *buf;
+
+ crc = table[(crc >> 12) ^ (byte >> 4)] ^ (crc << 4);
+ crc = table[(crc >> 12) ^ (byte & 0x0F)] ^ (crc << 4);
+
+ buf += 1;
+ } while (--bufSize);
+
+ byte = crc;
+ if (is_reserved(byte))
+ crc += 1;
+
+ byte = crc >> 8;
+ if (is_reserved(byte))
+ crc += 1 << 8;
+ }
+
+ return crc;
+}
+
+} \ No newline at end of file
diff --git a/src/voltronic/crc.h b/src/voltronic/crc.h
new file mode 100644
index 0000000..0f34f38
--- /dev/null
+++ b/src/voltronic/crc.h
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_VOLTRONIC_CRC_H
+#define INVERTER_TOOLS_VOLTRONIC_CRC_H
+
+#include <cstdint>
+#include <cstdlib>
+#include "../numeric_types.h"
+
+namespace voltronic {
+
+typedef u16 CRC;
+
+void crc_write(CRC crc, u8* buffer);
+CRC crc_read(const u8* buf);
+CRC crc_calculate(const u8* buf, size_t bufSize);
+
+}
+
+#endif //INVERTER_TOOLS_VOLTRONIC_CRC_H
diff --git a/src/voltronic/device.cc b/src/voltronic/device.cc
new file mode 100644
index 0000000..632ed27
--- /dev/null
+++ b/src/voltronic/device.cc
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <memory>
+#include <iostream>
+#include <limits>
+#include <cstring>
+#include <sstream>
+
+#include "crc.h"
+#include "device.h"
+#include "time.h"
+#include "exceptions.h"
+#include "hexdump/hexdump.h"
+#include "../logging.h"
+
+namespace voltronic {
+
+Device::Device() :
+ flags_(FLAG_WRITE_CRC | FLAG_READ_CRC | FLAG_VERIFY_CRC),
+ timeout_(TIMEOUT) {}
+
+void Device::setFlags(int flags) {
+ flags_ = flags;
+}
+
+int Device::getFlags() const {
+ return flags_;
+}
+
+void Device::setVerbose(bool verbose) {
+ verbose_ = verbose;
+}
+
+void Device::setTimeout(u64 timeout) {
+ timeout_ = timeout;
+ timeStarted_ = timestamp();
+}
+
+u64 Device::getElapsedTime() const {
+ return timestamp() - timeStarted_;
+}
+
+u64 Device::getTimeLeft() const {
+ if (!timeout_)
+ return std::numeric_limits<uint64_t>::max();
+
+ return std::max((u64)0, timeout_ - getElapsedTime());
+}
+
+size_t Device::run(const u8* inbuf, size_t inbufSize, u8* outbuf, size_t outbufSize) {
+ send(inbuf, inbufSize);
+
+ if (!getTimeLeft())
+ throw TimeoutError("sending already took " + std::to_string(getElapsedTime()) + " ms");
+
+ return recv(outbuf, outbufSize);
+}
+
+void Device::send(const u8* buf, size_t bufSize) {
+ size_t dataLen;
+ std::shared_ptr<u8> data;
+
+ if ((flags_ & FLAG_WRITE_CRC) == FLAG_WRITE_CRC) {
+ const CRC crc = crc_calculate(buf, bufSize);
+ dataLen = bufSize + sizeof(u16) + 1;
+ data = std::unique_ptr<u8>(new u8[dataLen]);
+ crc_write(crc, &data.get()[bufSize]);
+ } else {
+ dataLen = bufSize + 1;
+ data = std::unique_ptr<u8>(new u8[dataLen]);
+ }
+
+ u8* dataPtr = data.get();
+ memcpy((void*)dataPtr, buf, bufSize);
+
+ dataPtr[dataLen - 1] = '\r';
+
+ if (verbose_) {
+ myerr << "writing " << dataLen << (dataLen > 1 ? " bytes" : " byte");
+ std::cerr << hexdump(dataPtr, dataLen);
+ }
+
+ writeLoop(dataPtr, dataLen);
+}
+
+void Device::writeLoop(const u8* data, size_t dataSize) {
+ int bytesLeft = static_cast<int>(dataSize);
+
+ while (true) {
+ size_t bytesWritten = write(data, bytesLeft);
+ if (verbose_)
+ myerr << "bytesWritten=" << bytesWritten;
+
+ bytesLeft -= static_cast<int>(bytesWritten);
+ if (bytesLeft <= 0)
+ break;
+
+ if (!getTimeLeft())
+ throw TimeoutError("data writing already took " + std::to_string(getElapsedTime()) + " ms");
+
+ data = &data[bytesWritten];
+ }
+}
+
+size_t Device::recv(u8* buf, size_t bufSize) {
+ size_t bytesRead = readLoop(buf, bufSize);
+
+ if (verbose_) {
+ myerr << "got " << bytesRead << (bytesRead > 1 ? " bytes" : " byte");
+ std::cerr << hexdump(buf, bytesRead);
+ }
+
+ bool crcNeeded = (flags_ & FLAG_READ_CRC) == FLAG_READ_CRC;
+ size_t minSize = crcNeeded ? sizeof(u16) + 1 : 1;
+
+ if (bytesRead < minSize)
+ throw InvalidDataError("response is too small");
+
+ const size_t dataSize = bytesRead - minSize;
+
+ if (crcNeeded) {
+ const CRC crcActual = crc_read(&buf[dataSize]);
+ const CRC crcExpected = crc_calculate(buf, dataSize);
+
+// buf[dataSize] = 0;
+
+ if ((flags_ & FLAG_VERIFY_CRC) == FLAG_VERIFY_CRC && crcActual == crcExpected)
+ return dataSize;
+
+ std::ostringstream error;
+ error << std::hex;
+ error << "crc is invalid: expected 0x" << crcExpected << ", got 0x" << crcActual;
+ throw InvalidDataError(error.str());
+ }
+
+// buf[dataSize] = 0;
+ return dataSize;
+}
+
+size_t Device::readLoop(u8 *buf, size_t bufSize) {
+ size_t size = 0;
+
+ while(true) {
+ size_t bytesRead = read(buf, bufSize);
+ if (verbose_)
+ myerr << "bytesRead=" << bytesRead;
+
+ while (bytesRead) {
+ bytesRead--;
+ size++;
+
+ if (*buf == '\r')
+ return size;
+
+ buf++;
+ bufSize--;
+ }
+
+ if (!getTimeLeft())
+ throw TimeoutError("data reading already took " + std::to_string(getElapsedTime()) + " ms");
+
+ if (bufSize <= 0)
+ throw std::overflow_error("input buffer is not large enough");
+ }
+}
+
+} \ No newline at end of file
diff --git a/src/voltronic/device.h b/src/voltronic/device.h
new file mode 100644
index 0000000..6584585
--- /dev/null
+++ b/src/voltronic/device.h
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_VOLTRONIC_DEVICE_H
+#define INVERTER_TOOLS_VOLTRONIC_DEVICE_H
+
+#include <string>
+#include <memory>
+#include <hidapi/hidapi.h>
+#include <libserialport.h>
+
+#include "../numeric_types.h"
+
+namespace voltronic {
+
+enum {
+ FLAG_WRITE_CRC = 1,
+ FLAG_READ_CRC = 2,
+ FLAG_VERIFY_CRC = 4,
+};
+
+
+/**
+ * Common device
+ */
+
+class Device {
+protected:
+ int flags_;
+ u64 timeout_;
+ u64 timeStarted_;
+ bool verbose_;
+
+ void send(const u8* buf, size_t bufSize);
+ size_t recv(u8* buf, size_t bufSize);
+
+ void writeLoop(const u8* data, size_t dataSize);
+ size_t readLoop(u8* buf, size_t bufSize);
+
+ u64 getElapsedTime() const;
+ u64 getTimeLeft() const;
+
+public:
+ static const u64 TIMEOUT = 1000;
+
+ Device();
+
+ virtual size_t read(u8* buf, size_t bufSize) = 0;
+ virtual size_t write(const u8* data, size_t dataSize) = 0;
+
+ void setTimeout(u64 timeout);
+ size_t run(const u8* inbuf, size_t inbufSize, u8* outbuf, size_t outbufSize);
+
+ void setFlags(int flags);
+ int getFlags() const;
+
+ void setVerbose(bool verbose);
+};
+
+
+/**
+ * USB device
+ */
+
+class USBDevice : public Device {
+private:
+ hid_device* device_;
+
+public:
+ static const u16 VENDOR_ID = 0x0665;
+ static const u16 PRODUCT_ID = 0x5161;
+ static const u16 HID_REPORT_SIZE = 8;
+ static u16 GET_HID_REPORT_SIZE(size_t size);
+
+ USBDevice(u16 vendorId, u16 productId);
+ ~USBDevice();
+
+ size_t read(u8* buf, size_t bufSize) override;
+ size_t write(const u8* data, size_t dataSize) override;
+};
+
+
+/**
+ * Serial device
+ */
+
+typedef unsigned SerialBaudRate;
+
+enum class SerialDataBits {
+ Five = 5,
+ Six = 6,
+ Seven = 7,
+ Eight = 8,
+};
+
+enum class SerialStopBits {
+ One = 1,
+ OneAndHalf = 3,
+ Two = 2
+};
+
+enum class SerialParity {
+ Invalid = SP_PARITY_INVALID,
+ None = SP_PARITY_NONE,
+ Odd = SP_PARITY_ODD,
+ Even = SP_PARITY_EVEN,
+ Mark = SP_PARITY_MARK,
+ Space = SP_PARITY_SPACE,
+};
+
+class SerialDevice : public Device {
+private:
+ struct sp_port* port_;
+ SerialBaudRate baudRate_;
+ SerialDataBits dataBits_;
+ SerialStopBits stopBits_;
+ SerialParity parity_;
+ std::string name_;
+
+ unsigned getTimeout();
+
+public:
+ static const char* DEVICE_NAME;
+ static const SerialBaudRate BAUD_RATE = 2400;
+ static const SerialDataBits DATA_BITS = SerialDataBits::Eight;
+ static const SerialStopBits STOP_BITS = SerialStopBits::One;
+ static const SerialParity PARITY = SerialParity::None;
+
+ explicit SerialDevice(std::string& name,
+ SerialBaudRate baudRate,
+ SerialDataBits dataBits,
+ SerialStopBits stopBits,
+ SerialParity parity);
+ ~SerialDevice();
+
+ [[nodiscard]] inline struct sp_port* getPort() const {
+ return port_;
+ }
+
+ size_t read(u8* buf, size_t bufSize) override;
+ size_t write(const u8* data, size_t dataSize) override;
+};
+
+class SerialPortConfiguration {
+private:
+ struct sp_port_config* config_;
+ SerialDevice& device_;
+
+public:
+ explicit SerialPortConfiguration(SerialDevice& device);
+ ~SerialPortConfiguration();
+
+ void setConfiguration(SerialBaudRate baudRate, SerialDataBits dataBits, SerialStopBits stopBits, SerialParity parity);
+};
+
+bool is_serial_baud_rate_valid(SerialBaudRate baudRate);
+bool is_serial_data_bits_valid(SerialDataBits dataBits);
+bool is_serial_stop_bits_valid(SerialStopBits stopBits);
+bool is_serial_parity_valid(SerialParity parity);
+
+
+/**
+ * Pseudo device
+ */
+
+class PseudoDevice : public Device {
+public:
+ PseudoDevice() = default;
+ ~PseudoDevice() = default;
+
+ size_t read(u8* buf, size_t bufSize) override;
+ size_t write(const u8* data, size_t dataSize) override;
+};
+
+}
+
+#endif //INVERTER_TOOLS_VOLTRONIC_DEVICE_H
diff --git a/src/voltronic/exceptions.h b/src/voltronic/exceptions.h
new file mode 100644
index 0000000..6ae9c32
--- /dev/null
+++ b/src/voltronic/exceptions.h
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_VOLTRONIC_EXCEPTIONS_H
+#define INVERTER_TOOLS_VOLTRONIC_EXCEPTIONS_H
+
+#include <stdexcept>
+
+namespace voltronic {
+
+class DeviceError : public std::runtime_error {
+public:
+ using std::runtime_error::runtime_error;
+};
+
+class TimeoutError : public std::runtime_error {
+public:
+ using std::runtime_error::runtime_error;
+};
+
+class InvalidDataError : public std::runtime_error {
+public:
+ using std::runtime_error::runtime_error;
+};
+
+}
+
+#endif //INVERTER_TOOLS_VOLTRONIC_EXCEPTIONS_H
diff --git a/src/voltronic/pseudo_device.cc b/src/voltronic/pseudo_device.cc
new file mode 100644
index 0000000..58cd95c
--- /dev/null
+++ b/src/voltronic/pseudo_device.cc
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <stdexcept>
+#include <sstream>
+#include <cstring>
+
+#include "device.h"
+#include "crc.h"
+#include "hexdump/hexdump.h"
+#include "../logging.h"
+
+namespace voltronic {
+
+// PI
+//static const char* response = "^D00518";
+
+// GS
+static const char* response = "^D1060000,000,2300,500,0115,0018,002,500,000,000,000,000,078,019,000,000,0000,0000,0000,0000,0,0,0,1,2,2,0,0";
+
+// PIRI
+//static const char* response = "^D0882300,217,2300,500,217,5000,5000,480,500,570,420,576,540,2,30,060,0,1,1,6,0,0,0,1,2,00";
+
+// DI
+//static const char* response = "^D0682300,500,0,408,540,564,460,540,060,30,0,0,1,0,0,0,1,0,0,1,1,0,1,1";
+
+// set response
+//static const char* response = "^1";
+
+// TODO: maybe move size and crc stuff to readLoop()?
+size_t PseudoDevice::read(u8* buf, size_t bufSize) {
+ size_t pseudoResponseSize = strlen(response);
+
+ size_t responseSize = pseudoResponseSize;
+ if (flags_ & FLAG_READ_CRC)
+ responseSize += 2;
+
+ if (responseSize + 1 > bufSize) {
+ std::ostringstream error;
+ error << "buffer is not large enough (" << (responseSize + 1) << " > " << bufSize << ")";
+ throw std::overflow_error(error.str());
+ }
+
+ memcpy(buf, response, responseSize);
+
+ if (flags_ & FLAG_READ_CRC) {
+ CRC crc = crc_calculate(buf, pseudoResponseSize);
+ crc_write(crc, &buf[pseudoResponseSize]);
+ }
+
+ buf[responseSize] = '\r';
+
+ return responseSize + 1;
+}
+
+size_t PseudoDevice::write(const u8* data, size_t dataSize) {
+ if (verbose_) {
+ myerr << "dataSize=" << dataSize;
+ std::cerr << hexdump((void*)data, dataSize);
+ }
+ return dataSize;
+}
+
+} \ No newline at end of file
diff --git a/src/voltronic/serial_device.cc b/src/voltronic/serial_device.cc
new file mode 100644
index 0000000..d8b7c98
--- /dev/null
+++ b/src/voltronic/serial_device.cc
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <stdexcept>
+#include <algorithm>
+#include <libserialport.h>
+
+#include "device.h"
+#include "exceptions.h"
+#include "../logging.h"
+
+namespace voltronic {
+
+const char* SerialDevice::DEVICE_NAME = "/dev/ttyUSB0";
+
+SerialDevice::SerialDevice(std::string& name,
+ SerialBaudRate baudRate,
+ SerialDataBits dataBits,
+ SerialStopBits stopBits,
+ SerialParity parity)
+ : port_(nullptr)
+ , name_(name)
+ , baudRate_(baudRate)
+ , dataBits_(dataBits)
+ , stopBits_(stopBits)
+ , parity_(parity)
+{
+ if (sp_get_port_by_name(name_.c_str(), &port_) != SP_OK)
+ throw DeviceError("failed to get port by name");
+
+ if (sp_open(port_, SP_MODE_READ_WRITE) != SP_OK)
+ throw DeviceError("failed to open device");
+
+ SerialPortConfiguration config(*this);
+ config.setConfiguration(baudRate_, dataBits_, stopBits_, parity_);
+
+ sp_flush(port_, SP_BUF_BOTH);
+}
+
+SerialDevice::~SerialDevice() {
+ if (port_ != nullptr) {
+ if (sp_close(port_) == SP_OK)
+ sp_free_port(port_);
+ }
+}
+
+unsigned int SerialDevice::getTimeout() {
+ return !timeout_
+ // to wait indefinitely if no timeout set
+ ? 0
+ // if getTimeLeft() suddently returns 0, pass 1,
+ // otherwise libserialport will treat it like 'wait indefinitely'
+ : std::max(static_cast<unsigned>(getTimeLeft()), static_cast<unsigned>(1));
+}
+
+size_t SerialDevice::read(u8* buf, size_t bufSize) {
+ if (verbose_)
+ myerr << "reading...";
+ return sp_blocking_read_next(port_, buf, bufSize, getTimeout());
+}
+
+size_t SerialDevice::write(const u8* data, size_t dataSize) {
+ return sp_blocking_write(port_, data, dataSize, getTimeout());
+}
+
+
+/**
+ * Serial port configuration
+ */
+
+SerialPortConfiguration::SerialPortConfiguration(SerialDevice& device)
+ : config_(nullptr), device_(device)
+{
+ if (sp_new_config(&config_) != SP_OK)
+ throw DeviceError("failed to allocate port configuration");
+
+ if (sp_get_config(device.getPort(), config_) != SP_OK)
+ throw DeviceError("failed to get current port configuration");
+}
+
+SerialPortConfiguration::~SerialPortConfiguration() {
+ if (config_ != nullptr)
+ sp_free_config(config_);
+}
+
+void SerialPortConfiguration::setConfiguration(SerialBaudRate baudRate,
+ SerialDataBits dataBits,
+ SerialStopBits stopBits,
+ SerialParity parity) {
+ if (sp_set_config_baudrate(config_, static_cast<int>(baudRate)) != SP_OK)
+ throw DeviceError("failed to set baud rate");
+
+ if (sp_set_config_bits(config_, static_cast<int>(dataBits)) != SP_OK)
+ throw DeviceError("failed to set data bits");
+
+ if (sp_set_config_stopbits(config_, static_cast<int>(stopBits)) != SP_OK)
+ throw DeviceError("failed to set stop bits");
+
+ if (sp_set_config_parity(config_, static_cast<enum sp_parity>(parity)) != SP_OK)
+ throw DeviceError("failed to set parity");
+
+ if (sp_set_config(device_.getPort(), config_) != SP_OK)
+ throw DeviceError("failed to set port configuration");
+}
+
+bool is_serial_baud_rate_valid(SerialBaudRate baudRate) {
+ switch (baudRate) {
+ case 110:
+ case 300:
+ case 1200:
+ case 2400:
+ case 4800:
+ case 9600:
+ case 19200:
+ case 38400:
+ case 57600:
+ case 115200:
+ return true;
+
+ default: break;
+ }
+ return false;
+}
+
+bool is_serial_data_bits_valid(SerialDataBits dataBits) {
+ switch (dataBits) {
+ case SerialDataBits::Five:
+ case SerialDataBits::Six:
+ case SerialDataBits::Seven:
+ case SerialDataBits::Eight:
+ return true;
+ default: break;
+ }
+ return false;
+}
+
+bool is_serial_stop_bits_valid(SerialStopBits stopBits) {
+ switch (stopBits) {
+ case SerialStopBits::One:
+ case SerialStopBits::OneAndHalf:
+ case SerialStopBits::Two:
+ return true;
+ default: break;
+ }
+ return false;
+}
+
+bool is_serial_parity_valid(SerialParity parity) {
+ switch (parity) {
+ case SerialParity::None:
+ case SerialParity::Odd:
+ case SerialParity::Even:
+ case SerialParity::Mark:
+ case SerialParity::Space:
+ return true;
+ default: break;
+ }
+ return false;
+}
+
+} \ No newline at end of file
diff --git a/src/voltronic/time.cc b/src/voltronic/time.cc
new file mode 100644
index 0000000..20b45cc
--- /dev/null
+++ b/src/voltronic/time.cc
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <ctime>
+#include <cstdint>
+#include <sys/time.h>
+#include "time.h"
+
+namespace voltronic {
+
+u64 timestamp() {
+ u64 ms = 0;
+
+#if defined(CLOCK_MONOTONIC)
+ static bool monotonic_clock_error = false;
+ if (!monotonic_clock_error) {
+ struct timespec ts = {0};
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
+ ms = static_cast<u64>(ts.tv_sec);
+ ms *= 1000;
+ ms += static_cast<u64>(ts.tv_nsec / 1000000);
+ return ms;
+ } else {
+ monotonic_clock_error = true;
+ }
+ }
+#endif
+
+ struct timeval tv = {0};
+ if (gettimeofday(&tv, nullptr) == 0) {
+ ms = static_cast<u64>(tv.tv_sec);
+ ms *= 1000;
+ ms += static_cast<u64>(tv.tv_usec / 1000);
+ }
+
+ return ms;
+}
+
+} \ No newline at end of file
diff --git a/src/voltronic/time.h b/src/voltronic/time.h
new file mode 100644
index 0000000..d456461
--- /dev/null
+++ b/src/voltronic/time.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#ifndef INVERTER_TOOLS_VOLTRONIC_TIME_H
+#define INVERTER_TOOLS_VOLTRONIC_TIME_H
+
+#include "../numeric_types.h"
+
+namespace voltronic {
+
+u64 timestamp();
+
+}
+
+#endif //INVERTER_TOOLS_VOLTRONIC_TIME_H
diff --git a/src/voltronic/usb_device.cc b/src/voltronic/usb_device.cc
new file mode 100644
index 0000000..ffb94d0
--- /dev/null
+++ b/src/voltronic/usb_device.cc
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include <stdexcept>
+#include <cstring>
+#include <iostream>
+
+#include "../logging.h"
+#include "device.h"
+#include "exceptions.h"
+#include "hexdump/hexdump.h"
+
+namespace voltronic {
+
+USBDevice::USBDevice(u16 vendorId, u16 productId) {
+ if (hid_init() != 0)
+ throw DeviceError("hidapi initialization failure");
+
+ device_ = hid_open(vendorId, productId, nullptr);
+ if (!device_)
+ throw DeviceError("failed to create hidapi device");
+}
+
+USBDevice::~USBDevice() {
+ if (device_)
+ hid_close(device_);
+
+ hid_exit();
+}
+
+size_t USBDevice::read(u8* buf, size_t bufSize) {
+ int timeout = !timeout_ ? -1 : static_cast<int32_t>(getTimeLeft());
+ const int bytesRead = hid_read_timeout(device_, buf, GET_HID_REPORT_SIZE(bufSize), timeout);
+ if (bytesRead == -1)
+ throw DeviceError("hidapi_read_timeout() failed");
+ return bytesRead;
+}
+
+size_t USBDevice::write(const u8* data, size_t dataSize) {
+ const size_t writeSize = GET_HID_REPORT_SIZE(dataSize);
+
+ if (verbose_) {
+ myerr << "dataSize=" << dataSize << ", writeSize=" << writeSize;
+ std::cerr << hexdump((void*)data, dataSize);
+ }
+
+ u8 writeBuffer[HID_REPORT_SIZE+1]{0};
+ memcpy(&writeBuffer[1], data, writeSize);
+
+ const int bytesWritten = hid_write(device_, writeBuffer, HID_REPORT_SIZE + 1);
+ if (bytesWritten == -1)
+ throw DeviceError("hidapi_write() failed");
+
+ return GET_HID_REPORT_SIZE(bytesWritten);
+}
+
+u16 USBDevice::GET_HID_REPORT_SIZE(size_t size) {
+ return size > HID_REPORT_SIZE ? HID_REPORT_SIZE : size;
+}
+
+} \ No newline at end of file
diff --git a/third_party/hexdump/hexdump.h b/third_party/hexdump/hexdump.h
new file mode 100644
index 0000000..671e6ca
--- /dev/null
+++ b/third_party/hexdump/hexdump.h
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2014, Zac Bergquist
+ * Copyright (c) 2021, Evgeny Zinoviev
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+ * disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef THIRD_PARTY_HEXDUMP_H_
+#define THIRD_PARTY_HEXDUMP_H_
+
+#include <cctype>
+#include <iomanip>
+#include <ostream>
+#include <ios>
+
+template <unsigned rows, bool ascii>
+class custom_hexdump {
+public:
+ custom_hexdump(void* data, unsigned length) :
+ data(static_cast<unsigned char*>(data)), length(length) { }
+
+ const unsigned char* data;
+ const unsigned length;
+};
+
+template <unsigned rows, bool ascii>
+std::ostream& operator<<(std::ostream& out, const custom_hexdump<rows, ascii>& dump)
+{
+ // save state
+ std::ios_base::fmtflags f(out.flags());
+
+ out.fill('0');
+ for (int i = 0; i < dump.length; i += rows) {
+ out << "0x" << std::setw(4) << std::hex << i << ": ";
+ for (int j = 0; j < rows; ++j) {
+ if (i + j < dump.length) {
+ out << std::hex << std::setw(2) << static_cast<int>(dump.data[i + j]) << " ";
+ } else {
+ out << " ";
+ }
+ }
+
+ out << " ";
+ if (ascii) {
+ for (int j = 0; j < rows; ++j) {
+ if (i + j < dump.length) {
+ if (std::isprint(dump.data[i + j])) {
+ out << static_cast<char>(dump.data[i + j]);
+ } else {
+ out << ".";
+ }
+ }
+ }
+ }
+ out << std::endl;
+ }
+
+ // restore state
+ out.flags(f);
+
+ return out;
+}
+
+typedef custom_hexdump<16, true> hexdump;
+
+#endif // THIRD_PARTY_HEXDUMP_H_ \ No newline at end of file