diff options
author | Evgeny Zinoviev <me@ch1p.io> | 2021-05-07 02:18:07 +0300 |
---|---|---|
committer | Evgeny Zinoviev <me@ch1p.io> | 2021-05-07 02:18:07 +0300 |
commit | 7e743b73433475df086fcec81be7b10c1d695a42 (patch) | |
tree | 1737c5f9bdad2a40f740e9a655e510641331b9e2 /src |
initial
Diffstat (limited to 'src')
39 files changed, 5225 insertions, 0 deletions
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 |