From a7da7d653cef5a2592c951abf20d785337f76d5f 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 | 102 +++++++++++ Config.cmake.in | 5 + LICENSE | 24 +++ README.md | 147 ++++++++++++++++ RELEASES.md | 11 ++ cmake_uninstall.cmake.in | 21 +++ inc/osp3.h | 213 +++++++++++++++++++++++ pkgconfig.in | 13 ++ src/osp3.c | 364 +++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 5 + test/test_osp3_unit.c | 95 ++++++++++ utils/CMakeLists.txt | 12 ++ utils/osp3-dump.c | 130 ++++++++++++++ utils/osp3-poll.c | 175 +++++++++++++++++++ 17 files changed, 1382 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 CMakeLists.txt create mode 100644 Config.cmake.in 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..196b744 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,102 @@ +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 + EXPORT OSP3Targets + 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) + + +# CMake package helper + +include(CMakePackageConfigHelpers) +set(OSP3_CMAKE_CONFIG_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/OSP3) +set(CONFIG_TARGETS_FILE OSP3Targets.cmake) +set(CONFIG_REQUIRED_COMPONENTS osp3) +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/OSP3Config.cmake + INSTALL_DESTINATION ${OSP3_CMAKE_CONFIG_INSTALL_DIR} +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/OSP3ConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMinorVersion +) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OSP3Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/OSP3ConfigVersion.cmake + DESTINATION ${OSP3_CMAKE_CONFIG_INSTALL_DIR} + COMPONENT OSP3_Development) +install(EXPORT OSP3Targets + DESTINATION ${OSP3_CMAKE_CONFIG_INSTALL_DIR} + NAMESPACE OSP3:: + COMPONENT OSP3_Development) + + +# 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) + + +# Subdirectories + +add_subdirectory(test) +add_subdirectory(utils) diff --git a/Config.cmake.in b/Config.cmake.in new file mode 100644 index 0000000..55c7e6b --- /dev/null +++ b/Config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/@CONFIG_TARGETS_FILE@") + +check_required_components(@CONFIG_REQUIRED_COMPONENTS@) 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..9741f87 --- /dev/null +++ b/inc/osp3.h @@ -0,0 +1,213 @@ +/** + * 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); + +/** + * Flush unread data from the receive buffer (e.g., to drop old log entries). + * + * @param dev An open device + * @return 0 on success, -1 on error + */ +int osp3_flush(osp3_device* dev); + +/** + * Read from an OSP3. + * + * Any buffered data will be read first. + * On error, it's possible that some bytes may have been read, but may or may not be reported in `transferred`. + * + * @param dev An open device + * @param buf The destination buffer + * @param len The destination buffer size (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* buf, size_t len, size_t* transferred, unsigned int timeout_ms); + +/** + * Read a complete line from an OSP3. + * + * Multiple reads are often necessary to read a complete line, and the same timeout is used for each read operation. + * Therefore, this function could theoretically block for up to `len * timeout_ms` milliseconds. + * However, in practice, OSP3 devices stream entire lines to the serial port, not single bytes in isolation. + * The total number of reads is thus more likely to be `ceil(line_length / OSP3_W_MAX_PACKET_SIZE)`. + * After waiting (up to `timeout_ms`) for data to become available for the first read, each additional read likely only + * blocks long enough for the next packet to be received, repeated until a newline character is found. + * + * If the final read captures data after a newline character, it is buffered separately. + * Any following read (including from `osp3_read`) will first get data from this buffer before reading from the OSP3. + * + * @param dev An open device + * @param buf The destination buffer + * @param len The destination buffer size + * @param line_writte The number of bytes actually read + * @param timeout_ms A timeout in milliseconds + * @return 0 on success, -1 on error + */ +int osp3_read_line(osp3_device* dev, unsigned char* buf, size_t len, size_t* transferred, unsigned int timeout_ms); + +/** + * Perform a checksum on a log entry. + * + * @param log The log entry buffer + * @param log_sz Must be `>= OSP3_LOG_PROTOCOL_SIZE` + * @param cs8_2s The resulting 2s complement checksum + * @param cs8_xor The resulting XOR checksum + * @return 0 on checksum match, 1 on checksum mismatch, -1 on error + */ +int osp3_log_checksum(const char* log, size_t log_sz, uint8_t* cs8_2s, uint8_t* cs8_xor); + +/** + * Test the given checksum values against 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_log_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_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..2424cbc --- /dev/null +++ b/src/osp3.c @@ -0,0 +1,364 @@ +/** + * 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 +#include + +#ifndef OSP3_DEBUG + #define OSP3_DEBUG 0 +#endif + +typedef struct osp3_rw_buffer { + unsigned char buf[OSP3_W_MAX_PACKET_SIZE]; + size_t idx; + size_t rem; +} osp3_rw_buffer; + +struct osp3_device { + osp3_rw_buffer rbuf; + 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, TCIFLUSH); +} + +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; +} + +int osp3_flush(osp3_device* dev) { + dev->rbuf.idx = 0; + dev->rbuf.rem = 0; + return tcflush(dev->fd, TCIFLUSH); +} + +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; +} + +static size_t sz_min(size_t a, size_t b) { + return a <= b ? a : b; +} + +int osp3_read(osp3_device* dev, unsigned char* buf, size_t len, size_t* transferred, unsigned int timeout_ms) { + *transferred = sz_min(dev->rbuf.rem, len); + memcpy(buf, &dev->rbuf.buf[dev->rbuf.idx], *transferred); + assert(dev->rbuf.rem >= *transferred); + dev->rbuf.rem -= *transferred; + dev->rbuf.idx = dev->rbuf.rem > 0 ? dev->rbuf.idx + *transferred : 0; + if (*transferred < len) { + ssize_t bytes_read = osp3_read_buf(dev->fd, &buf[*transferred], len - *transferred, timeout_ms); + if (bytes_read < 0) { + return -1; + } + *transferred += (size_t) bytes_read; + } + return 0; +} + +static int lineccpy(void* restrict dst, size_t dst_sz, size_t* written, const void* restrict src, size_t src_sz) { + const size_t sz = sz_min(dst_sz, src_sz); + const void* ret = memccpy(dst, src, '\n', sz); + *written = ret == NULL ? sz : (size_t) ((const char*) ret - (const char*) dst); + return ret == NULL ? 0 : 1; +} + +int osp3_read_line(osp3_device* dev, unsigned char* buf, size_t len, size_t* transferred, unsigned int timeout_ms) { + size_t line_seg_written = 0; + int complete = lineccpy(buf, len, &line_seg_written, &dev->rbuf.buf[dev->rbuf.idx], dev->rbuf.rem); + *transferred = line_seg_written; + assert(dev->rbuf.rem >= line_seg_written); + dev->rbuf.rem -= line_seg_written; + dev->rbuf.idx = dev->rbuf.rem > 0 ? dev->rbuf.idx + line_seg_written : 0; + while (!complete) { + unsigned char packet[OSP3_W_MAX_PACKET_SIZE]; + assert(len >= *transferred); + size_t packet_sz = sz_min(sizeof(packet), len - *transferred); + if (packet_sz == 0) { + errno = ENOBUFS; + return -1; + } + ssize_t bytes_read = osp3_read_buf(dev->fd, packet, packet_sz, timeout_ms); + if (bytes_read < 0) { + return -1; + } + size_t packet_written = (size_t) bytes_read; + line_seg_written = 0; + complete = lineccpy(&buf[*transferred], len - *transferred, &line_seg_written, packet, packet_written); + *transferred += line_seg_written; + assert(dev->rbuf.idx == 0); + assert(packet_written >= line_seg_written); + dev->rbuf.rem = packet_written - line_seg_written; + assert(dev->rbuf.rem > 0 ? complete : 1); // if we actually populate the buffer, we must be finished + memcpy(dev->rbuf.buf, &packet[line_seg_written], dev->rbuf.rem); + } + 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) + +static void osp3_log_checksum_compute(const char* log, uint8_t* cs8_2s, uint8_t* cs8_xor) { + *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; +} + +int osp3_log_checksum(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; + } + osp3_log_checksum_compute(log, cs8_2s, cs8_xor); + return osp3_log_checksum_test(log, log_sz, *cs8_2s, *cs8_xor); +} + +int osp3_log_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_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,%"SCNx8",%"SCNx8"\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..cad9af9 --- /dev/null +++ b/test/test_osp3_unit.c @@ -0,0 +1,95 @@ +#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_checksum(void) { + uint8_t cs8_2s = 0; + uint8_t cs8_xor = 0; + // Bad size. + assert(osp3_log_checksum(test_log1, sizeof(test_log1) - 1, &cs8_2s, &cs8_xor) == -1); + // Good size. + assert(osp3_log_checksum(test_log1, sizeof(test_log1), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x14); + assert(cs8_xor == 0x12); + assert(osp3_log_checksum(test_log2, sizeof(test_log2), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x1c); + assert(cs8_xor == 0x12); + assert(osp3_log_checksum(test_log3, sizeof(test_log3), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x09); + assert(cs8_xor == 0x17); + assert(osp3_log_checksum(test_log4, sizeof(test_log4), &cs8_2s, &cs8_xor) == 0); + assert(cs8_2s == 0x11); + assert(cs8_xor == 0x19); + // Bad checksums (modified test_log1). + static const char test_log1_bad_2s[OSP3_LOG_PROTOCOL_SIZE] = \ + "0000815169,15296,0036,00550,0,00000,0000,00000,0,00,00000,0000,00000,0,00,15,12\r\n"; + static const char test_log1_bad_xor[OSP3_LOG_PROTOCOL_SIZE] = \ + "0000815169,15296,0036,00550,0,00000,0000,00000,0,00,00000,0000,00000,0,00,14,13\r\n"; + assert(osp3_log_checksum(test_log1_bad_2s, sizeof(test_log1_bad_2s), &cs8_2s, &cs8_xor) == 1); + assert(osp3_log_checksum(test_log1_bad_xor, sizeof(test_log1_bad_xor), &cs8_2s, &cs8_xor) == 1); +} + +static void test_osp3_log_checksum_test(void) { + // Bad size. + assert(osp3_log_checksum_test(test_log1, sizeof(test_log1) - 1, 0x14, 0x12) == -1); + // Good size. + assert(osp3_log_checksum_test(test_log1, sizeof(test_log1), 0x14, 0x12) == 0); + assert(osp3_log_checksum_test(test_log2, sizeof(test_log2), 0x1c, 0x12) == 0); + assert(osp3_log_checksum_test(test_log3, sizeof(test_log3), 0x09, 0x17) == 0); + assert(osp3_log_checksum_test(test_log4, sizeof(test_log4), 0x11, 0x19) == 0); + // Bad checksums. + assert(osp3_log_checksum_test(test_log1, sizeof(test_log1), 0x15, 0x12) == 1); + assert(osp3_log_checksum_test(test_log1, sizeof(test_log1), 0x14, 0x13) == 1); +} + +static void test_osp3_log_parse(void) { + osp3_log_entry log_entry; + // Something that's not 0. + memset(&log_entry, 0xFF, sizeof(log_entry)); + // Bad size. + assert(osp3_log_parse(test_log1, sizeof(test_log1) - 1, &log_entry) == -1); + // Good size. + assert(osp3_log_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 == 0x14); + assert(log_entry.checksum8_xor == 0x12); + // test_log2 checksum values include letters (hexadecimal). + assert(osp3_log_parse(test_log2, sizeof(test_log2), &log_entry) == 0); + assert(log_entry.checksum8_2s_compl == 0x1c); + assert(log_entry.checksum8_xor == 0x12); +} + +int main(void) { + test_osp3_log_checksum(); + test_osp3_log_checksum_test(); + test_osp3_log_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..289800b --- /dev/null +++ b/utils/osp3-dump.c @@ -0,0 +1,130 @@ +/** + * 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 (!running) { + return 0; + } + if (errno == ETIME) { + fprintf(stderr, "Read timeout expired\n"); + } else { + perror("osp3_read"); + } + 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..8ea86d0 --- /dev/null +++ b/utils/osp3-poll.c @@ -0,0 +1,175 @@ +/** + * Poll log entries from an ODROID Smart Power 3. + * + * @author Connor Imes + * @date 2024-03-19 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#define PATH_DEFAULT "/dev/ttyUSB0" + +// Conservative, but effective. +#define TIMEOUT_MS_DEFAULT (OSP3_INTERVAL_MS_MAX * 2) + +// Much bigger than anything an OSP3 should produce. +#define OSP3_LINE_LEN_MAX 1024 + +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 int count = 0; +static int parse = 1; +static int checksum = 1; + +static const char short_options[] = "hp:b:t:n:"; +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'}, + {"num", required_argument, NULL, 'n'}, + // Long-only options. + {"no-parse", no_argument, &parse, 0}, + {"no-checksum", no_argument, &checksum, 0}, + {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" + " -n, --num=N Stop after N log entries\n" + " --no-parse Disable log entry parsing verification\n" + " --no-checksum Disable log entry checksum verification\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 'n': + count = 1; + running = atoi(optarg); + break; + case 0: + // Long-only option. + 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_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_entry log_entry; + uint8_t cs8_2s; + uint8_t cs8_xor; + while (running) { + char line[OSP3_LINE_LEN_MAX + 1] = { 0 }; + size_t line_written = 0; + if (osp3_read_line(dev, (unsigned char*) line, sizeof(line) - 1, &line_written, timeout_ms) < 0) { + if (!running) { + return 0; + } + if (errno == ETIME) { + fprintf(stderr, "Read timeout expired\n"); + } else { + perror("osp3_read_line"); + } + return 1; + } + assert(line_written > 0); + assert(line[line_written - 1] == '\n'); + if (line_written < OSP3_LOG_PROTOCOL_SIZE) { + fprintf(stderr, "Dropping shorter line than expected: %s", line); + } else if (line_written > OSP3_LOG_PROTOCOL_SIZE) { + fprintf(stderr, "Dropping longer line than expected: %s", line); + } else if (parse && osp3_log_parse(line, OSP3_LOG_PROTOCOL_SIZE, &log_entry)) { + fprintf(stderr, "Log entry parsing failed: %s", line); + } else if (checksum && osp3_log_checksum(line, OSP3_LOG_PROTOCOL_SIZE, &cs8_2s, &cs8_xor)) { + fprintf(stderr, "Log entry checksum failed (cs8_2s=%02x, cs8_xor=%02x): %s", cs8_2s, cs8_xor, line); + } else { + printf("%s", line); + if (count) { + running--; + } + } + } + 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; +}