summaryrefslogtreecommitdiff
path: root/src/voltronic
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2021-05-07 02:18:07 +0300
committerEvgeny Zinoviev <me@ch1p.io>2021-05-07 02:18:07 +0300
commit7e743b73433475df086fcec81be7b10c1d695a42 (patch)
tree1737c5f9bdad2a40f740e9a655e510641331b9e2 /src/voltronic
initial
Diffstat (limited to 'src/voltronic')
-rw-r--r--src/voltronic/crc.cc60
-rw-r--r--src/voltronic/crc.h20
-rw-r--r--src/voltronic/device.cc167
-rw-r--r--src/voltronic/device.h176
-rw-r--r--src/voltronic/exceptions.h27
-rw-r--r--src/voltronic/pseudo_device.cc63
-rw-r--r--src/voltronic/serial_device.cc160
-rw-r--r--src/voltronic/time.cc38
-rw-r--r--src/voltronic/time.h14
-rw-r--r--src/voltronic/usb_device.cc60
10 files changed, 785 insertions, 0 deletions
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