From 78d393bdc9221ca55f544bbc3914bf07474dab73 Mon Sep 17 00:00:00 2001 From: Connor Imes Date: Fri, 22 Mar 2024 16:33:05 -0400 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 26 ++++ .gitignore | 37 +++++ AUTHORS | 2 + CMakeLists.txt | 74 ++++++++++ LICENSE | 24 +++ README.md | 147 +++++++++++++++++++ RELEASES.md | 11 ++ cmake_uninstall.cmake.in | 21 +++ inc/osp3.h | 195 +++++++++++++++++++++++++ pkgconfig.in | 13 ++ src/osp3.c | 308 +++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 5 + test/test_osp3_unit.c | 103 +++++++++++++ utils/CMakeLists.txt | 12 ++ utils/osp3-dump.c | 125 ++++++++++++++++ utils/osp3-poll.c | 235 +++++++++++++++++++++++++++++ 16 files changed, 1338 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RELEASES.md create mode 100644 cmake_uninstall.cmake.in create mode 100644 inc/osp3.h create mode 100644 pkgconfig.in create mode 100644 src/osp3.c create mode 100644 test/CMakeLists.txt create mode 100644 test/test_osp3_unit.c create mode 100644 utils/CMakeLists.txt create mode 100644 utils/osp3-dump.c create mode 100644 utils/osp3-poll.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1aa72ad --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Continuous Integration +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + name: ${{ matrix.os }} Test + steps: + - uses: actions/checkout@v3 + - name: Build + run: | + export CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector -pedantic -Wall -Wextra -Wbad-function-cast -Wcast-align \ + -Wcast-qual -Wdisabled-optimization -Wendif-labels -Wfloat-conversion -Wfloat-equal -Wformat=2 -Wformat-nonliteral \ + -Winline -Wmissing-declarations -Wmissing-noreturn -Wmissing-prototypes -Wnested-externs -Wpointer-arith -Wshadow \ + -Wsign-conversion -Wstrict-prototypes -Wstack-protector -Wundef -Wwrite-strings -Werror" + cmake -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_BUILD_TYPE=Release -S . -B _build + cmake --build _build/ -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da15f90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ + +# Other +.DS_Store +*~ +build/ diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..48aa236 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Connor Imes + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f077fd5 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.12...3.28) + +project(osp3 VERSION 0.0.1 + LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) +add_compile_options(-Wall -Wextra -Wpedantic) + +include(GNUInstallDirs) +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake + @ONLY +) +add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) + +enable_testing() + + +# Libraries + +add_library(osp3 src/osp3.c) +target_include_directories(osp3 PRIVATE ${PROJECT_SOURCE_DIR}/inc + PUBLIC $ + $) +set_target_properties(osp3 PROPERTIES PUBLIC_HEADER "${PROJECT_SOURCE_DIR}/inc/osp3.h" + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) +install(TARGETS osp3 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT OSP3_Runtime + NAMELINK_COMPONENT OSP3_Development + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT OSP3_Development + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/osp3 + COMPONENT OSP3_Development) + + +# Subdirectories + +add_subdirectory(test) +add_subdirectory(utils) + + +# pkg-config + +set(PKG_CONFIG_PREFIX "${CMAKE_INSTALL_PREFIX}") +set(PKG_CONFIG_EXEC_PREFIX "\${prefix}") +if(IS_ABSOLUTE "${CMAKE_INSTALL_INCLUDEDIR}") + set(PKG_CONFIG_INCLUDEDIR "${CMAKE_INSTALL_INCLUDEDIR}/osp3") +else() + set(PKG_CONFIG_INCLUDEDIR "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}/osp3") +endif() +if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PKG_CONFIG_LIBDIR "${CMAKE_INSTALL_LIBDIR}") +else() + set(PKG_CONFIG_LIBDIR "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}") +endif() +set(PKG_CONFIG_NAME "osp3") +set(PKG_CONFIG_DESCRIPTION "Library for managing an ODROID Smart Power 3") +set(PKG_CONFIG_REQUIRES "") +set(PKG_CONFIG_REQUIRES_PRIVATE "") +set(PKG_CONFIG_CFLAGS "-I\${includedir}") +set(PKG_CONFIG_LIBS "-L\${libdir} -losp3") +set(PKG_CONFIG_LIBS_PRIVATE "") +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/pkgconfig.in + ${CMAKE_CURRENT_BINARY_DIR}/osp3.pc + @ONLY +) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/osp3.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + COMPONENT OSP3_Development) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f33230 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2024, Connor Imes +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the University of Chicago nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF CHICAGO BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6ec075 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# ODROID Smart Power 3 Library and Utilities + +A library and tools for an [ODROID Smart Power 3](https://wiki.odroid.com/accessory/power_supply_battery/smartpower3) device with a USB connection. + +> NOTE: If you're using a first generation [ODROID Smart Power](https://wiki.odroid.com/old_product/accessory/odroidsmartpower) device, see the [hosp](https://github.com/energymon/hosp) project instead. + +This project is tested using a device running SmartPower3 firmware v2.2 (20230518), though may work with devices running v1.7 (20211214) or newer. +Older firmware versions use a different logging protocol. + + +## Building + +### Prerequisites + +This project uses [CMake](https://cmake.org/). + +On Debian-based Linux systems (including Ubuntu): + +```sh +sudo apt install cmake +``` + +On macOS, using [Homebrew](https://brew.sh/): + +```sh +brew install cmake +``` + +### Compiling + +To build, run: + +```sh +cmake -S . -B build/ +cmake --build build/ +``` + +To build a shared object library (instead of a static library), add `-DBUILD_SHARED_LIBS=On` to the first cmake command. +Add `-DCMAKE_BUILD_TYPE=Release` for an optimized build. +Refer to CMake documentation for more a complete description of build options. + +To install, run with proper privileges: + +```sh +cmake --build . --target install +``` + +On Linux, installation typically places libraries in `/usr/local/lib` and header files in `/usr/local/include/osp3`. + +Install must be run before uninstalling in order to have a manifest. +To uninstall, run with proper privileges (install must have been run first to create a manifest): + +```sh +cmake --build . --target uninstall +``` + +### Linking + +To link against `osp3`, use `pkg-config` to get compiler and linker flags. +E.g., in a Makefile: + +```sh +CFLAGS+=$(shell pkg-config --cflags osp3) +LDFLAGS+=$(shell pkg-config --libs --static osp3) +``` + +The `--static` flag is unnecessary if you built/installed a shared object library. + + +## Linux Privileges + +To use an ODROID Smart Power 3 without needing sudo/root at runtime, set appropriate [udev](https://en.wikipedia.org/wiki/Udev) privileges. + +You can give access to a specific group, e.g. `plugdev`, by creating/modifying a `udev` config file like `/etc/udev/rules.d/10-local.rules`. +For example, add the following rules: + +``` +# OROID Smart Power 3 +SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", GROUP="plugdev" +``` + +For the new permissions to take effect, the device must be remounted by the kernel - either disconnect and reconnect the device or reboot the system. + + +## Utilities + +The following command-line utilities are included. +See their help output for usage. + +* `osp3-dump` - dump the device's serial output. +* `osp3-poll` - poll the device's serial output for complete log entries. + + +## C API + +The following is a simple example that reads data from an ODROID Smart Power 3 device. +A real application will likely do more complex things with the data (like parse it). + +```C +#include +#include +#include + +int main(void) { + int ret = 0; + + // Open the device. + const char* path = "/dev/ttyUSB0"; + unsigned int baud = OSP3_BAUD_DEFAULT; + osp3_device* dev; + if ((dev = osp3_open_device(path, baud)) == NULL) { + perror("Failed to open ODROID Smart Power 3 connection"); + return 1; + } + + // Work with the device. + unsigned char packet[OSP3_W_MAX_PACKET_SIZE] = { 0 }; + size_t transferred = 0; + unsigned int timeout_ms = 0; + while (1) { + if (osp3_read(dev, packet, sizeof(packet), &transferred, timeout_ms) < 0) { + perror("Failed to read from ODROID Smart Power 3"); + ret = 1; + break; + } + for (size_t i = 0; i < transferred; i++) { + putchar((const char) packet[i]); + } + } + + // Close the device. + if (osp3_close(dev)) { + perror("Failed to close ODROID Smart Power 3 connection"); + ret = 1; + } + + return ret; +} +``` + + +## Project Source + +Find this and related project sources at the [energymon organization on GitHub](https://github.com/energymon). +This project originates at: https://github.com/energymon/osp3 + +Bug reports and pull requests for bug fixes and enhancements are welcome. diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..1934482 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,11 @@ +# Release Notes + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## Unreleased + +- Initial public release. diff --git a/cmake_uninstall.cmake.in b/cmake_uninstall.cmake.in new file mode 100644 index 0000000..2c34c81 --- /dev/null +++ b/cmake_uninstall.cmake.in @@ -0,0 +1,21 @@ +if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + message(FATAL_ERROR "Cannot find install manifest: @CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") +endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + +file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +string(REGEX REPLACE "\n" ";" files "${files}") +foreach(file ${files}) + message(STATUS "Uninstalling $ENV{DESTDIR}${file}") + if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") + exec_program( + "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" + OUTPUT_VARIABLE rm_out + RETURN_VALUE rm_retval + ) + if(NOT "${rm_retval}" STREQUAL 0) + message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") + endif(NOT "${rm_retval}" STREQUAL 0) + else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") + message(STATUS "File $ENV{DESTDIR}${file} does not exist.") + endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") +endforeach(file) \ No newline at end of file diff --git a/inc/osp3.h b/inc/osp3.h new file mode 100644 index 0000000..13416ab --- /dev/null +++ b/inc/osp3.h @@ -0,0 +1,195 @@ +/** + * A library for managing an ODROID Smart Power 3 (OSP3) device. + * + * Supported baud rates: 9600, 19200, 38400, 57600, 115200 (default), 230400, 460800, 500000, 576000, and 921600. + * However, not all platforms support configuring all baud rates, e.g., macOS is limited. + * + * The OSP3 device must first be configured to perform serial logging. + * Supported logging intervals (some constrained by the baud rate): 5, 10 (default), 50, 100, 500, and 1000 ms. + * + * Log entries may be read from the device at the logging interval configured on the device. + * The max serial packet size is less than that of a log entry, so multiple reads are necessary to capture an entry. + * The user is responsible for identifying and handling log entry slices. + * + * @author Connor Imes + * @date 2024-03-19 + */ +#ifndef _OSP3_H_ +#define _OSP3_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * Minimum supported serial baud rate. + */ +#define OSP3_BAUD_MIN 9600 + +/** + * Maximum supported serial baud rate. + */ +#define OSP3_BAUD_MAX 921600 + +/* + * Default serial baud rate used on the device UI. + */ +#define OSP3_BAUD_DEFAULT 115200 + +/** + * Minimum serial logging interval. + */ +#define OSP3_INTERVAL_MS_MIN 5 + +/** + * Maximum serial logging interval. + */ +#define OSP3_INTERVAL_MS_MAX 1000 + +/** + * Default serial logging interval used on the device UI. + */ +#define OSP3_INTERVAL_MS_DEFAULT 10 + +/** + * Maximum serial packet size. + */ +#define OSP3_W_MAX_PACKET_SIZE 64 + +/** + * Device log entry line length, including trailing escape characters. + */ +#define OSP3_LOG_PROTOCOL_SIZE 81 + +/* + * Interrupt Bits. + * + * Bit | Function | Notes + * ----------------------------------------------------------------------------- + * 0 | Overvoltage protection + * 1 | Constant current function + * 2 | Short-circuit protection + * 3 | Power-on + * 4 | Watchdog + * 5 | Overtemperature protection | Junction temperatrue 165 Celsius + * 6 | Overtemperature warning | Junction temperature 145 Celsius + * 7 | Inductor peak current protection + */ +#define OSP3_INTR_OVERVOLTAGE_PROT (1u) +#define OSP3_INTR_CONSTANT_CURRENT_FUNC (1u << 1) +#define OSP3_INTR_SHORT_CIRCUIT_PROT (1u << 2) +#define OSP3_INTR_POWER_ON (1u << 3) +#define OSP3_INTR_WATCHDOG (1u << 4) +#define OSP3_INTR_OVERTEMPERATURE_PROT (1u << 5) +#define OSP3_INTR_OVERTEMPERATURE_WARN (1u << 6) +#define OSP3_INTR_INDUCTOR_PEAK_CURRENT_PROT (1u << 7) + +/** + * Opaque OSP3 handle. + */ +typedef struct osp3_device osp3_device; + +typedef struct osp3_log_entry { + unsigned long ms; + unsigned int mV_in; + unsigned int mA_in; + unsigned int mW_in; + unsigned int onoff_in; + unsigned int mV_0; + unsigned int mA_0; + unsigned int mW_0; + unsigned int onoff_0; + unsigned int intr_0; + unsigned int mV_1; + unsigned int mA_1; + unsigned int mW_1; + unsigned int onoff_1; + unsigned int intr_1; + uint8_t checksum8_2s_compl; + uint8_t checksum8_xor; +} osp3_log_entry; + +/** + * Open an OSP3 device. + * + * @param path The device path + * @param baud The baud rate (or 0 for default) + * @return A osp3_device handle, or NULL on failure + */ +osp3_device* osp3_open_device(const char* path, unsigned int baud); + +/** + * Close an OSP3 device handle. + * + * @param dev An open device + * @return 0 in success, -1 on error + */ +int osp3_close(osp3_device* dev); + +/** + * Read from an OSP3. + * + * @param dev An open device + * @param packet The destination buffer + * @param len Should probably be `>= OSP3_W_MAX_PACKET_SIZE` + * @param transferred The number of bytes actually read + * @param timeout_ms A timeout in milliseconds + * @return 0 on success, -1 on error + */ +int osp3_read(osp3_device* dev, unsigned char* packet, size_t len, size_t* transferred, unsigned int timeout_ms); + +/** + * Helper function to write some or all of a packet to a log entry buffer. + * Since packets aren't always aligned with log lines, this usually needs to be called multiple times per log entry. + * The function returns early when a log entry is complete, so there may still be remaining bytes in `packet`. + * Remaining bytes should be written to a new log entry, otherwise the beginning of that next log entry will be lost. + * + * @param log The log entry buffer + * @param log_sz Must be `>= OSP3_LOG_PROTOCOL_SIZE` + * @param written The number of bytes actually written + * @param packet The packet read from the device + * @param packet_sz The packet length (transferred from the device) - will be `<= OSP3_W_MAX_PACKET_SIZE` + * @return 1 if log entry is complete, 0 otherwise + */ +int osp3_log_write(char* log, size_t log_sz, size_t* written, const unsigned char* packet, size_t packet_sz); + +/** + * Compute the checksums for a log entry. + * + * @param log The log entry buffer + * @param log_sz Must be `>= OSP3_LOG_PROTOCOL_SIZE` + * @param cs8_2s The 2s complement checksum + * @param cs8_xor The XOR checksum + * @return 0 on success, -1 on failure + */ +int osp3_checksum_compute(const char* log, size_t log_sz, uint8_t* cs8_2s, uint8_t* cs8_xor); + +/** + * Test the checksums for a log entry. + * + * @param log The log entry buffer + * @param log_sz Must be `>= OSP3_LOG_PROTOCOL_SIZE` + * @param cs8_2s The 2s complement checksum + * @param cs8_xor The XOR checksum + * @return 0 on checksum match, 1 on checksum mismatch, -1 on error + */ +int osp3_checksum_test(const char* log, size_t log_sz, uint8_t cs8_2s, uint8_t cs8_xor); + +/** + * Parse a log entry. + * + * @param log The log entry buffer + * @param log_sz Must be `>= OSP3_LOG_PROTOCOL_SIZE` + * @param log_entry The struct to be populated + * @return 0 on success, 1 if not all fields were parsed, -1 on error + */ +int osp3_log_entry_parse(const char* log, size_t log_sz, osp3_log_entry* log_entry); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/pkgconfig.in b/pkgconfig.in new file mode 100644 index 0000000..0d4632f --- /dev/null +++ b/pkgconfig.in @@ -0,0 +1,13 @@ +prefix=@PKG_CONFIG_PREFIX@ +exec_prefix=@PKG_CONFIG_EXEC_PREFIX@ +includedir=@PKG_CONFIG_INCLUDEDIR@ +libdir=@PKG_CONFIG_LIBDIR@ + +Name: @PKG_CONFIG_NAME@ +Description: @PKG_CONFIG_DESCRIPTION@ +Version: @PROJECT_VERSION@ +Requires: @PKG_CONFIG_REQUIRES@ +Requires.private: @PKG_CONFIG_REQUIRES_PRIVATE@ +Cflags: @PKG_CONFIG_CFLAGS@ +Libs: @PKG_CONFIG_LIBS@ +Libs.private: @PKG_CONFIG_LIBS_PRIVATE@ diff --git a/src/osp3.c b/src/osp3.c new file mode 100644 index 0000000..561ed5a --- /dev/null +++ b/src/osp3.c @@ -0,0 +1,308 @@ +/** + * A library for managing an ODROID Smart Power 3 device over USB. + * + * @author Connor Imes + * @date 2024-03-19 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef OSP3_DEBUG + #define OSP3_DEBUG 0 +#endif + +struct osp3_device { + int fd; +}; + +static speed_t baud_to_speed(unsigned int baud) { + switch (baud) { + case 9600: + return B9600; + case 19200: + return B19200; + case 38400: + return B38400; + case 57600: + return B57600; + case 115200: + return B115200; + case 230400: + return B230400; +#ifdef B460800 + case 460800: + return B460800; +#endif +#ifdef B500000 + case 500000: + return B500000; +#endif +#ifdef B576000 + case 576000: + return B576000; +#endif +#ifdef B921600 + case 921600: + return B921600; +#endif + // Higher baud rates not supported by device. + default: + break; + } + errno = EINVAL; + return B0; +} + +static int osp3_set_serial_attributes(int fd, unsigned int baud) { + struct termios t; + speed_t speed; + if (tcgetattr(fd, &t) < 0) { + return -1; + } + if ((speed = baud_to_speed(baud)) == B0) { +#if OSP3_DEBUG + fprintf(stderr, "Unsupported baud: %u\n", baud); +#endif + return -1; + } + if (cfsetspeed(&t, speed) < 0) { + return -1; + } + if (tcsetattr(fd, TCSANOW, &t) < 0) { + return -1; + } + return tcflush(fd, TCIOFLUSH); +} + +static int osp3_open(const char* filename, int* fd) { + struct stat s; + char buf[32]; + const char* shortname; + + // Check if device node exists and is writable + if (stat(filename, &s) < 0) { +#if OSP3_DEBUG + perror(filename); +#endif + return -1; + } + if (!S_ISCHR(s.st_mode)) { + errno = ENOTTY; +#if OSP3_DEBUG + perror("osp3_open: Not a TTY character device"); +#endif + return -1; + } + if (access(filename, R_OK | W_OK)) { + perror(filename); + return -1; + } + + // Get shortname by dropping leading "/dev/" + if (!(shortname = strrchr(filename, '/'))) { + // shouldn't happen since we've already checked filename + errno = EINVAL; +#if OSP3_DEBUG + perror(filename); +#endif + return -1; + } + shortname++; + + // Check if "/sys/class/tty/" exists and is correct type + snprintf(buf, sizeof(buf), "/sys/class/tty/%s", shortname); + if (stat(buf, &s) < 0) { +#if OSP3_DEBUG + perror(buf); +#endif + return -1; + } + if (!S_ISDIR(s.st_mode)) { + errno = ENODEV; +#if OSP3_DEBUG + perror("osp3_open: Not a TTY device"); +#endif + return -1; + } + + // Open the device file + *fd = open(filename, O_RDWR | O_NONBLOCK); + if (*fd < 0) { +#if OSP3_DEBUG + perror(filename); +#endif + return -1; + } + return 0; +} + +osp3_device* osp3_open_device(const char* path, unsigned int baud) { + osp3_device* dev; + if (path == NULL) { + errno = EINVAL; + return NULL; + } + if ((dev = calloc(1, sizeof(osp3_device))) == NULL) { + return NULL; + } + if (osp3_open(path, &dev->fd) < 0) { + free(dev); + return NULL; + } + if (osp3_set_serial_attributes(dev->fd, baud > 0 ? baud : OSP3_BAUD_DEFAULT) < 0) { + close(dev->fd); + free(dev); + return NULL; + } + return dev; +} + +int osp3_close(osp3_device* dev) { + int ret = close(dev->fd); + free(dev); + return ret; +} + +static ssize_t osp3_read_buf(int fd, unsigned char* buf, size_t buflen, unsigned int timeout_ms) { + ssize_t ret = -1; + fd_set set; + FD_ZERO(&set); + FD_SET(fd, &set); + struct timespec ts_timeout = { + .tv_sec = timeout_ms / 1000, + .tv_nsec = (timeout_ms % 1000) * 1000 * 1000, + }; + switch (pselect(fd + 1, &set, NULL, NULL, timeout_ms > 0 ? &ts_timeout : NULL, NULL)) { + case -1: + // failed + break; + case 0: + // timed out + errno = ETIME; + break; + default: + ret = read(fd, buf, buflen); + break; + } + return ret; +} + +int osp3_read(osp3_device* dev, unsigned char* packet, size_t len, size_t* transferred, unsigned int timeout_ms) { + ssize_t ret = osp3_read_buf(dev->fd, packet, len, timeout_ms); + if (ret >= 0) { + *transferred = (size_t) ret; + return 0; + } + return -1; +} + +int osp3_log_write(char* log, size_t log_sz, size_t* written, const unsigned char* packet, size_t packet_sz) { + *written = 0; + for (size_t i = 0; i < packet_sz && i < log_sz; i++) { + log[(*written)++] = (char) packet[i]; + if (packet[i] == '\n') { + // Final byte of a log entry. + return 1; + } + } + return 0; +} + +// TOTAL: 81 (79 printable characters + 2 escape characters) +// Time| INPUT POWER | CHANNEL 0 | CHANNEL 1 | CHECKSUM | LF +// (ms), volt(mV), ampere(mA), watt(mW), on/off, volt(mV), ampere(mA), watt(mW), on/off, interrupts, volt(mV), ampere(mA), watt(mW), on/off, interrupts, CheckSum8 2s Complement, CheckSum8 Xor '\r\n' +// 0000815169,15296,0036,00550,0,00000,0000,00000,0,00,00000,0000,00000,0,00,14,12\r\n +#define MS_SZ 10 +#define MV_SZ 5 +#define MA_SZ 4 +#define MW_SZ 5 +#define ONOFF_SZ 1 +#define INTR_SZ 2 +#define CS_2COMPL_SZ 2 +#define CS_XOR_SZ 2 + +#define MS_OFF 0 + +#define MV_IN_OFF (MS_OFF + MS_SZ + 1) +#define MA_IN_OFF (MV_IN_OFF + MV_SZ + 1) +#define MW_IN_OFF (MA_IN_OFF + MA_SZ + 1) +#define ONOFF_IN_OFF (MW_IN_OFF + MW_SZ + 1) + +#define MV_0_OFF (ONOFF_IN_OFF + ONOFF_SZ + 1) +#define MA_0_OFF (MV_0_OFF + MV_SZ + 1) +#define MW_0_OFF (MA_0_OFF + MA_SZ + 1) +#define ONOFF_0_OFF (MW_0_OFF + MW_SZ + 1) +#define INTR_0_OFF (ONOFF_0_OFF + ONOFF_SZ + 1) + +#define MV_1_OFF (INTR_0_OFF + INTR_SZ + 1) +#define MA_1_OFF (MV_1_OFF + MV_SZ + 1) +#define MW_1_OFF (MA_1_OFF + MA_SZ + 1) +#define ONOFF_1_OFF (MW_1_OFF + MW_SZ + 1) +#define INTR_1_OFF (ONOFF_1_OFF + ONOFF_SZ + 1) + +#define CS_2COMPL_OFF (INTR_1_OFF + INTR_SZ + 1) +#define CS_XOR_OFF (CS_2COMPL_OFF + CS_2COMPL_SZ + 1) + +int osp3_checksum_compute(const char* log, size_t log_sz, uint8_t* cs8_2s, uint8_t* cs8_xor) { + if (log_sz < OSP3_LOG_PROTOCOL_SIZE) { + errno = EINVAL; + return -1; + } + *cs8_2s = 0; + *cs8_xor = 0; + for (size_t i = 0; i < CS_2COMPL_OFF; i++) { + *cs8_2s += (unsigned char) log[i]; + *cs8_xor ^= (unsigned char) log[i]; + } + *cs8_2s = (~(*cs8_2s)) + 1; + return 0; +} + +int osp3_checksum_test(const char* log, size_t log_sz, uint8_t cs8_2s, uint8_t cs8_xor) { + if (log_sz < OSP3_LOG_PROTOCOL_SIZE) { + errno = EINVAL; + return -1; + } + // Log values are in hexadecimal. + // Arrays are at least 8 bytes long to avoid `stack-protector` warnings. + char cs8_2s_log_bytes[8] = { log[CS_2COMPL_OFF], log[CS_2COMPL_OFF + 1], '\0' }; + char cs8_xor_log_bytes[8] = { log[CS_XOR_OFF], log[CS_XOR_OFF + 1], '\0' }; + uint8_t cs8_2s_log = (uint8_t) strtoul(cs8_2s_log_bytes, NULL, 16); + uint8_t cs8_xor_log = (uint8_t) strtoul(cs8_xor_log_bytes, NULL, 16); + return !(cs8_2s == cs8_2s_log && cs8_xor == cs8_xor_log); +} + +int osp3_log_entry_parse(const char* log, size_t log_sz, osp3_log_entry* log_entry) { + if (log_sz < OSP3_LOG_PROTOCOL_SIZE) { + errno = EINVAL; + return -1; + } + int matched = sscanf(log, + "%010lu,%05u,%04u,%05u,%1u,%05u,%04u,%05u,%1u,%02x,%05u,%04u,%05u,%1u,%02x,%02"SCNu8",%02"SCNu8"\r\n", + // Time + &log_entry->ms, + // Input Power + &log_entry->mV_in, &log_entry->mA_in, &log_entry->mW_in, &log_entry->onoff_in, + // Channel 0 Output + &log_entry->mV_0, &log_entry->mA_0, &log_entry->mW_0, &log_entry->onoff_0, &log_entry->intr_0, + // Channel 1 Output + &log_entry->mV_1, &log_entry->mA_1, &log_entry->mW_1, &log_entry->onoff_1, &log_entry->intr_1, + // Checksum + &log_entry->checksum8_2s_compl, &log_entry->checksum8_xor); + if (matched == EOF) { + if (!errno) { + errno = EILSEQ; + } + return -1; + } + return !(matched == 17); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..080843c --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,5 @@ +# Tests + +add_executable(test_osp3_unit test_osp3_unit.c) +target_link_libraries(test_osp3_unit PRIVATE osp3) +add_test(test_osp3_unit test_osp3_unit) diff --git a/test/test_osp3_unit.c b/test/test_osp3_unit.c new file mode 100644 index 0000000..216c96c --- /dev/null +++ b/test/test_osp3_unit.c @@ -0,0 +1,103 @@ +#undef NDEBUG +#include +#include +#include + +// From the wiki. +static const char test_log1[OSP3_LOG_PROTOCOL_SIZE] = \ + "0000815169,15296,0036,00550,0,00000,0000,00000,0,00,00000,0000,00000,0,00,14,12\r\n"; + +// From the device. +static const char test_log2[OSP3_LOG_PROTOCOL_SIZE] = \ + "0343732187,15321,0072,01103,0,00000,0000,00000,0,00,00000,0000,00000,0,00,1c,12\r\n"; +static const char test_log3[OSP3_LOG_PROTOCOL_SIZE] = \ + "0343732197,15332,0084,01287,0,00000,0000,00000,0,00,00000,0000,00000,0,00,09,17\r\n"; +static const char test_log4[OSP3_LOG_PROTOCOL_SIZE] = \ + "0343732207,15328,0055,00843,0,00000,0000,00000,0,00,00000,0000,00000,0,00,11,19\r\n"; + + +static void test_osp3_log_write(void) { + char log[OSP3_LOG_PROTOCOL_SIZE] = { 0 }; + size_t written = 0; + // Full buffer. + assert(osp3_log_write(log, sizeof(log), &written, (const unsigned char*) test_log1, OSP3_W_MAX_PACKET_SIZE) == 0); + assert(written == OSP3_W_MAX_PACKET_SIZE); + assert(osp3_log_write(&log[written], sizeof(log) - written, &written, (const unsigned char*) &test_log1[written], + OSP3_LOG_PROTOCOL_SIZE - written) == 1); + assert(written == OSP3_LOG_PROTOCOL_SIZE - OSP3_W_MAX_PACKET_SIZE); + assert(strncmp(log, test_log1, sizeof(test_log1)) == 0); + // Partial log entry. + assert(osp3_log_write(log, sizeof(log), &written, (const unsigned char*) &test_log1[sizeof(test_log1) - 1], 1) == 1); + assert(written == 1); + // Overlapping log entries. + const unsigned char* test_log_overlap = (const unsigned char*) "foo\r\nbar"; + assert(osp3_log_write(log, sizeof(log), &written, test_log_overlap, sizeof(test_log_overlap)) == 1); + assert(written == 5); + assert(osp3_log_write(log, sizeof(log), &written, &test_log_overlap[written], sizeof(test_log_overlap) - 5) == 0); + assert(written == 3); +} + +static void test_osp3_checksum_compute(void) { + uint8_t cs8_2s = 0; + uint8_t cs8_xor = 0; + // Bad size. + assert(osp3_checksum_compute(test_log1, sizeof(test_log1) - 1, &cs8_2s, &cs8_xor) == -1); + // Good size. + assert(osp3_checksum_compute(test_log1, sizeof(test_log1), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x14); + assert(cs8_xor == 0x12); + assert(osp3_checksum_compute(test_log2, sizeof(test_log2), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x1c); + assert(cs8_xor == 0x12); + assert(osp3_checksum_compute(test_log3, sizeof(test_log3), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x09); + assert(cs8_xor == 0x17); + assert(osp3_checksum_compute(test_log4, sizeof(test_log4), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x11); + assert(cs8_xor == 0x19); +} + +static void test_osp3_checksum_test(void) { + // Bad size. + assert(osp3_checksum_test(test_log1, sizeof(test_log1) - 1, 0x14, 0x12) == -1); + // Good size. + assert(osp3_checksum_test(test_log1, sizeof(test_log1), 0x14, 0x12) == 0); + assert(osp3_checksum_test(test_log2, sizeof(test_log2), 0x1c, 0x12) == 0); + assert(osp3_checksum_test(test_log3, sizeof(test_log3), 0x09, 0x17) == 0); + assert(osp3_checksum_test(test_log4, sizeof(test_log4), 0x11, 0x19) == 0); +} + +static void test_osp3_log_entry_parse(void) { + osp3_log_entry log_entry; + // Something that's not 0. + memset(&log_entry, 0xFF, sizeof(log_entry)); + // Bad size. + assert(osp3_log_entry_parse(test_log1, sizeof(test_log1) - 1, &log_entry) == -1); + // Good size. + assert(osp3_log_entry_parse(test_log1, sizeof(test_log1), &log_entry) == 0); + assert(log_entry.ms == 815169); + assert(log_entry.mV_in == 15296); + assert(log_entry.mA_in == 36); + assert(log_entry.mW_in == 550); + assert(log_entry.onoff_in == 0); + assert(log_entry.mV_0 == 0); + assert(log_entry.mA_0 == 0); + assert(log_entry.mW_0 == 0); + assert(log_entry.onoff_0 == 0); + assert(log_entry.intr_0 == 0); + assert(log_entry.mV_1 == 0); + assert(log_entry.mA_1 == 0); + assert(log_entry.mW_1 == 0); + assert(log_entry.onoff_1 == 0); + assert(log_entry.intr_0 == 0); + assert(log_entry.checksum8_2s_compl == 14); + assert(log_entry.checksum8_xor == 12); +} + +int main(void) { + test_osp3_log_write(); + test_osp3_checksum_compute(); + test_osp3_checksum_test(); + test_osp3_log_entry_parse(); + return 0; +} diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..ed80b37 --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,12 @@ +# Utilities + +add_executable(osp3-dump osp3-dump.c) +target_link_libraries(osp3-dump PRIVATE osp3) + +add_executable(osp3-poll osp3-poll.c) +target_link_libraries(osp3-poll PRIVATE osp3) + +install(TARGETS osp3-dump + osp3-poll + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT OSP3_Utils_Runtime) diff --git a/utils/osp3-dump.c b/utils/osp3-dump.c new file mode 100644 index 0000000..7244dd0 --- /dev/null +++ b/utils/osp3-dump.c @@ -0,0 +1,125 @@ +/** + * Dump serial output from an ODROID Smart Power 3. + * + * @author Connor Imes + * @date 2024-03-27 + */ +#include +#include +#include +#include +#include +#include + +#define PATH_DEFAULT "/dev/ttyUSB0" + +#define TIMEOUT_MS_DEFAULT 0 + +static const char* path = PATH_DEFAULT; +static unsigned int baud = OSP3_BAUD_DEFAULT; +static unsigned int timeout_ms = TIMEOUT_MS_DEFAULT; +static volatile int running = 1; + +static const char short_options[] = "hp:b:t:"; +static const struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"path", required_argument, NULL, 'p'}, + {"baud", required_argument, NULL, 'b'}, + {"timeout", required_argument, NULL, 't'}, + {0, 0, 0, 0} +}; + +__attribute__ ((noreturn)) +static void print_usage(int exit_code) { + fprintf(exit_code ? stderr : stdout, + "Dump serial output from an ODROID Smart Power 3.\n\n" + "Usage: osp3-dump [OPTION]...\n" + "Options:\n" + " -h, --help Print this message and exit\n" + " -p, --path=FILE Device path (default: %s)\n" + " -b, --baud=RATE Device baud rate (default: %u)\n" + " -t, --timeout=MS Read timeout in milliseconds (default: %u)\n", + PATH_DEFAULT, OSP3_BAUD_DEFAULT, TIMEOUT_MS_DEFAULT); + exit(exit_code); +} + +static void parse_args(int argc, char** argv) { + int c; + while ((c = getopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (c) { + case 'h': + print_usage(0); + break; + case 'p': + path = optarg; + break; + case 'b': + baud = (unsigned int) atoi(optarg); + break; + case 't': + timeout_ms = (unsigned int) atoi(optarg); + break; + case '?': + default: + print_usage(1); + break; + } + } +} + +static void shandle(int sig) { + switch (sig) { + case SIGTERM: + case SIGINT: +#ifdef SIGQUIT + case SIGQUIT: +#endif +#ifdef SIGKILL + case SIGKILL: +#endif +#ifdef SIGHUP + case SIGHUP: +#endif + running = 0; + default: + break; + } +} + +static int osp3_dump(osp3_device* dev) { + unsigned char packet[OSP3_W_MAX_PACKET_SIZE] = { 0 }; + while (running) { + size_t transferred = 0; + if (osp3_read(dev, packet, sizeof(packet), &transferred, timeout_ms) < 0) { + if (errno == ETIME) { + fprintf(stderr, "Read timeout expired\n"); + } + return 1; + } + for (size_t i = 0; i < transferred; i++) { + putchar((const char) packet[i]); + } + } + return 0; +} + +int main(int argc, char** argv) { + osp3_device* dev; + int ret; + + signal(SIGINT, shandle); + parse_args(argc, argv); + + if ((dev = osp3_open_device(path, baud)) == NULL) { + perror("Failed to open ODROID Smart Power 3 connection"); + return 1; + } + + ret = osp3_dump(dev); + + if (osp3_close(dev)) { + perror("Failed to close ODROID Smart Power 3 connection"); + } + + return ret; +} diff --git a/utils/osp3-poll.c b/utils/osp3-poll.c new file mode 100644 index 0000000..3357042 --- /dev/null +++ b/utils/osp3-poll.c @@ -0,0 +1,235 @@ +/** + * Poll log entries from an ODROID Smart Power 3. + * + * @author Connor Imes + * @date 2024-03-19 + */ +#include +#include +#include +#include +#include +#include +#include + +#define PATH_DEFAULT "/dev/ttyUSB0" + +// Conservative, but effective. +#define TIMEOUT_MS_DEFAULT (OSP3_INTERVAL_MS_MAX * 2) + +static const char* path = PATH_DEFAULT; +static unsigned int baud = OSP3_BAUD_DEFAULT; +static unsigned int timeout_ms = TIMEOUT_MS_DEFAULT; +static int checksum = 0; +static volatile int running = 1; +static int count = 0; + +static const char short_options[] = "hp:b:t:cn:"; +static const struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"path", required_argument, NULL, 'p'}, + {"baud", required_argument, NULL, 'b'}, + {"timeout", required_argument, NULL, 't'}, + {"checksum", no_argument, NULL, 'c'}, + {"num", required_argument, NULL, 'n'}, + {0, 0, 0, 0} +}; + +__attribute__ ((noreturn)) +static void print_usage(int exit_code) { + fprintf(exit_code ? stderr : stdout, + "Poll log entries from an ODROID Smart Power 3.\n\n" + "Usage: osp3-poll [OPTION]...\n" + "Options:\n" + " -h, --help Print this message and exit\n" + " -p, --path=FILE Device path (default: %s)\n" + " -b, --baud=RATE Device baud rate (default: %u)\n" + " -t, --timeout=MS Read timeout in milliseconds (default: %u)\n" + " -c, --checksum Verify checksum on log entries\n" + " -n, --num=N Stop after N log entries\n", + PATH_DEFAULT, OSP3_BAUD_DEFAULT, TIMEOUT_MS_DEFAULT); + exit(exit_code); +} + +static void parse_args(int argc, char** argv) { + int c; + while ((c = getopt_long(argc, argv, short_options, long_options, NULL)) != -1) { + switch (c) { + case 'h': + print_usage(0); + break; + case 'p': + path = optarg; + break; + case 'b': + baud = (unsigned int) atoi(optarg); + break; + case 't': + timeout_ms = (unsigned int) atoi(optarg); + break; + case 'c': + checksum = 1; + break; + case 'n': + count = 1; + running = atoi(optarg); + break; + case '?': + default: + print_usage(1); + break; + } + } +} + +static void shandle(int sig) { + switch (sig) { + case SIGTERM: + case SIGINT: +#ifdef SIGQUIT + case SIGQUIT: +#endif +#ifdef SIGKILL + case SIGKILL: +#endif +#ifdef SIGHUP + case SIGHUP: +#endif + running = 0; + default: + break; + } +} + +typedef struct osp3_log_reader { + unsigned char packet[OSP3_W_MAX_PACKET_SIZE]; + char log[OSP3_LOG_PROTOCOL_SIZE + 1]; // +1 for null terminator + size_t packet_idx; + size_t packet_rem; + size_t log_idx; +} osp3_log_reader; + +static int osp3_log_read(osp3_device* dev, osp3_log_reader* rdr, unsigned int timeout) { + // Try to drain the prior packet. + size_t written = 0; + assert(rdr->packet_idx + rdr->packet_rem <= sizeof(rdr->packet)); + int complete = osp3_log_write(&rdr->log[rdr->log_idx], sizeof(rdr->log) - rdr->log_idx - 1, &written, + &rdr->packet[rdr->packet_idx], rdr->packet_rem); + + // Configure packet. + assert(written <= rdr->packet_rem); + rdr->packet_rem -= written; + if (rdr->packet_rem == 0) { + // Successfully drained the prior packet. + rdr->packet_idx = 0; + } else { + // Didn't drain the prior entire packet. + assert(written > 0); // otherwise we'd be stuck looping forever + rdr->packet_idx += written; + } + + // Configure log and test the log message status. + // In any case, the next log entry may already be started in the read buffer, so don't clear it. + rdr->log_idx += written; + assert(rdr->log_idx < sizeof(rdr->log)); + if (rdr->log_idx == OSP3_LOG_PROTOCOL_SIZE) { + rdr->log_idx = 0; + // If not complete, the device is producing a longer line than expected and more errors will likely follow... + return complete ? 0 : -2; + } + if (complete) { + // The device produced a shorter line than expected, hopefully just the tail of a log entry on the first read... + assert(rdr->log_idx < OSP3_LOG_PROTOCOL_SIZE); + rdr->log_idx = 0; + return -3; + } + + // Read the next packet if the prior packet was drained. + if (rdr->packet_idx == 0) { + assert(rdr->packet_rem == 0); + if (osp3_read(dev, rdr->packet, sizeof(rdr->packet), &rdr->packet_rem, timeout) < 0) { + return -1; + } + } + return 1; +} + +static int maybe_checksum(const osp3_log_reader* rdr) { + int fail = 0; + if (checksum) { + uint8_t cs8_2s = 0; + uint8_t cs8_xor = 0; + fail = osp3_checksum_compute(rdr->log, sizeof(rdr->log), &cs8_2s, &cs8_xor); + assert(fail == 0); + fail = osp3_checksum_test(rdr->log, sizeof(rdr->log), cs8_2s, cs8_xor); + assert(fail >= 0); + if (fail) { + fprintf(stderr, "Checksum failed (cs8_2s=%02x, cs8_xor=%02x): %s\n", cs8_2s, cs8_xor, rdr->log); + } + } + return fail; +} + +static int osp3_poll(osp3_device* dev) { + // Print header. + printf("ms,"); + printf("mV_in,mA_in,mW_in,onoff_in,"); + printf("mV_0,mA_0,mW_0,onoff_0,interrupts_0,"); + printf("mV_1,mA_1,mW_1,onoff_1,interrupts_1,"); + printf("CheckSum8_2s_Complement,CheckSum8_Xor\n"); + osp3_log_reader rdr = { 0 }; + while (running) { + int ret = osp3_log_read(dev, &rdr, timeout_ms); + switch (ret) { + case 0: + if (!maybe_checksum(&rdr)) { + printf("%s", rdr.log); + } + if (count) { + running--; + } + break; + case -1: + if (errno == ETIME) { + fprintf(stderr, "Read timeout expired\n"); + } else { + perror("osp3_read"); + } + return 1; + case -2: + fprintf(stderr, "Dropping longer line than expected: %s\n", rdr.log); + break; + case -3: + fprintf(stderr, "Dropping shorter line than expected: %s\n", rdr.log); + break; + case 1: + // continue reading + break; + default: + assert(0); + break; + } + } + return 0; +} + +int main(int argc, char** argv) { + osp3_device* dev; + int ret; + + signal(SIGINT, shandle); + parse_args(argc, argv); + + if ((dev = osp3_open_device(path, baud)) == NULL) { + perror("Failed to open ODROID Smart Power 3 connection"); + return 1; + } + + ret = osp3_poll(dev); + + if (osp3_close(dev)) { + perror("Failed to close ODROID Smart Power 3 connection"); + } + + return ret; +}