Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPIOD Support through alternative implementation of the HWIF interface #525

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build+test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: install deps
run: sudo apt-get update && sudo apt-get install libjson-c-dev libcurl4-openssl-dev libmicrohttpd-dev libgcrypt20-dev libsasl2-dev libunistring-dev libmosquitto-dev libgmock-dev libgtest-dev
run: sudo apt-get update && sudo apt-get install libjson-c-dev libcurl4-openssl-dev libmicrohttpd-dev libgcrypt20-dev libsasl2-dev libunistring-dev libmosquitto-dev libgpiod-dev libgmock-dev libgtest-dev
- name: build libsml
run: git clone https://github.com/volkszaehler/libsml.git ../libsml && cd ../libsml && make
- uses: actions/checkout@v2
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,13 @@ endif(WIN32)

find_library(LIBUUID uuid)
find_library(LIBGCRYPT gcrypt)
find_library(GPIOD_LIBRARY NAMES gpiod)

if(NOT GPIOD_LIBRARY)
message(FATAL_ERROR "gpiod library not found")
else()
message(STATUS "gpiod library found: ${GPIOD_LIBRARY}")
endif()

include_directories(${CMAKE_BINARY_DIR})
include_directories(${CMAKE_SOURCE_DIR}/include)
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ RUN apk add --no-cache \
libunistring-dev \
automake \
autoconf \
gtest-dev
gtest-dev \
libgpiod-dev

WORKDIR /vzlogger

Expand Down Expand Up @@ -66,7 +67,8 @@ RUN apk add --no-cache \
mosquitto-libs \
libunistring \
libstdc++ \
libgcc
libgcc \
libgpiod

# libsml is linked statically => no need to copy
COPY --from=builder /usr/local/bin/vzlogger /usr/local/bin/vzlogger
Expand Down
33 changes: 33 additions & 0 deletions include/protocols/MeterS0.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#define _S0_H_

#include <atomic>
#include <gpiod.h>
#include <termios.h>
#include <thread>

Expand Down Expand Up @@ -85,6 +86,38 @@ class MeterS0 : public vz::protocol::Protocol {
std::string _device;
};

class HWIF_GPIOD : public HWIF {
public:
HWIF_GPIOD(int gpiopin, const std::list<Option> &options);
virtual ~HWIF_GPIOD();

virtual bool _open();
virtual bool _close();
virtual bool waitForImpulse(bool &timeout);
virtual int status() { return -1; }; // not supported always return error
virtual bool is_blocking() const { return true; }

protected:
int _gpiopin;
bool _configureGPIO;
int _debounce_delay_ms;
int _high_count;
int _high_wait_ms;
struct gpiod_chip *_chip;
struct gpiod_line *_line;
struct timespec _ts_next_state_transition;
int _gpio_line_status;

enum States {
NO_TRANSITION = -1,
STATE_LOW = 0,
STATE_HIGH = 1,
STATE_DEBOUNCE = 2,
STATE_HIGH_WAIT = 3
};
States _state;
};

class HWIF_MMAP : public HWIF {
public:
HWIF_MMAP(int gpiopin, const std::string &hw);
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ if( TARGET )
target_link_libraries(vzlogger dl)
endif( ${TARGET} STREQUAL "ar71xx")
endif( TARGET )
target_link_libraries(vzlogger ${CURL_STATIC_LIBRARIES} ${CURL_LIBRARIES} unistring ${GNUTLS_LIBRARIES} ${OPENSSL_LIBRARIES} )
target_link_libraries(vzlogger ${CURL_STATIC_LIBRARIES} ${CURL_LIBRARIES} unistring ${GNUTLS_LIBRARIES} ${OPENSSL_LIBRARIES} ${GPIOD_LIBRARY})

# add programs to the install target
INSTALL(PROGRAMS
Expand Down
262 changes: 262 additions & 0 deletions src/protocols/MeterS0.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include <errno.h>
#include <fcntl.h>
#include <gpiod.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -75,6 +76,10 @@ MeterS0::MeterS0(std::list<Option> options, HWIF *hwif, HWIF *hwif_dir)
}
if (use_mmap) {
_hwif = new HWIF_MMAP(gpiopin, mmap);
} else if (gpiopin > 1000) {
print(log_info, "GPIO Pin %d greater than 1000. Using GPIOD interface for gpio.",
name().c_str(), gpiopin);
_hwif = new HWIF_GPIOD(gpiopin - 1000, options);
} else
_hwif = new HWIF_GPIO(gpiopin, options);
} else {
Expand All @@ -95,6 +100,11 @@ MeterS0::MeterS0(std::list<Option> options, HWIF *hwif, HWIF *hwif_dir)
}
if (use_mmap) {
_hwif_dir = new HWIF_MMAP(gpiodirpin, mmap);
} else if (gpiopin > 1000) {
print(log_info,
"GPIO Pin %d greater than 1000. Using GPIOD interface for gpio_dir.",
name().c_str(), gpiopin);
_hwif_dir = new HWIF_GPIOD(gpiodirpin - 1000, options);
} else
_hwif_dir = new HWIF_GPIO(gpiodirpin, options);
}
Expand Down Expand Up @@ -752,3 +762,255 @@ bool MeterS0::HWIF_GPIO::waitForImpulse(bool &timeout) {
timeout = false;
return false;
}

/* GPIOD based interface, takes three additional config parameters:
*
* configureGPIO - whether or not to configure the GPIO pin active low (default: true)
* debounce_delay (milliseconds) - it implements its own debounce logic where after an
* initial low->high or high->low transition, all additional
* transitions are ignored for a period (default: 30)
* high_wait (milliseconds) - the minimum time the signal needs to stay high (after debounce)
* before an impulse is recognized (default: -1 / disabled)
*/

MeterS0::HWIF_GPIOD::HWIF_GPIOD(int gpiopin, const std::list<Option> &options)
: _gpiopin(gpiopin), _configureGPIO(true), _debounce_delay_ms(30), _high_count(0),
_high_wait_ms(-1), _chip(NULL), _line(NULL),
_ts_next_state_transition({.tv_sec = 0L, .tv_nsec = 0L}), _gpio_line_status(-1),
_state(STATE_LOW) {
OptionList optlist;

if (_gpiopin < 0)
throw vz::VZException("invalid (<0) gpio(pin) set");

try {
_configureGPIO = optlist.lookup_bool(options, "configureGPIO");
} catch (vz::VZException &e) {
print(log_info, "Missing bool configureGPIO using default true", "S0");
_configureGPIO = true;
}

try {
_debounce_delay_ms = optlist.lookup_int(options, "debounce_delay");
} catch (vz::OptionNotFoundException &e) {
_debounce_delay_ms = 30;
} catch (vz::VZException &e) {
print(log_alert, "Failed to parse debounce_delay", "");
throw;
}

try {
_high_wait_ms = optlist.lookup_int(options, "high_wait");
} catch (vz::OptionNotFoundException &e) {
_high_wait_ms = -1;
} catch (vz::VZException &e) {
print(log_alert, "Failed to parse high_wait", "");
throw;
}

print(log_info,
"Initialized with config gpiopin %d, configureGPIO %s, debounce_delay %d, high_wait %d",
"S0", _gpiopin, _configureGPIO ? "true" : "false", _debounce_delay_ms, _high_wait_ms);
}

MeterS0::HWIF_GPIOD::~HWIF_GPIOD() { _close(); }

bool MeterS0::HWIF_GPIOD::_open() {
const char *consumername = "vzlogger-s0";
const char *chipname = "gpiochip0"; // this is currently hardcoded since the interesting GPIO
// lines are on chip 0 of a raspberry pi

_chip = gpiod_chip_open_by_name(chipname);
if (!_chip) {
throw vz::VZException("open chip failed, errno " + std::to_string(errno));
}

print(log_debug, "get GPIO line %d", "S0", _gpiopin);
_line = gpiod_chip_get_line(_chip, _gpiopin);
if (!_line) {
_close();
throw vz::VZException("get line " + std::to_string(_gpiopin) + " failed, errno " +
std::to_string(errno));
}

int flags = 0;
if (_configureGPIO) {
print(log_info, "configuring GPIO via GPIOD (active low)", "S0");
flags = GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
}

// initialize status based on line status
if (gpiod_line_request_input_flags(_line, consumername, flags)) {
_close();
throw vz::VZException("line request input failed, errno " + std::to_string(errno));
}
_gpio_line_status = gpiod_line_get_value(_line);
if (_gpio_line_status == -1) {
_close();
throw vz::VZException("Read line status failed, errno " + std::to_string(errno));
}
_state = (_gpio_line_status) ? STATE_HIGH : STATE_LOW;
gpiod_line_release(_line);

// subscribe to GPIO events
if (gpiod_line_request_both_edges_events_flags(_line, consumername, flags)) {
_close();
throw vz::VZException("line request both edge events failed, errno " +
std::to_string(errno));
}

return true;
}

bool MeterS0::HWIF_GPIOD::_close() {
if (_line) {
gpiod_line_release(_line);
_line = NULL;
}

if (_chip) {
gpiod_chip_close(_chip);
_chip = NULL;
}

_gpio_line_status = -1;
_state = NO_TRANSITION;

return true;
}

/*
the GPIOD interface provides a subscription mechanism to events and guarantees that no events are
lost This means even if counter_thread sleeps, rising & falling edge events occurring will be
reported once waitForImpulse is called again. Therefore we implement our own debouncing mechanism in
a state machine.
*/
bool MeterS0::HWIF_GPIOD::waitForImpulse(bool &timeout) {
struct gpiod_line_event event = {{0L, 0L}, -1};

// STATE_HIGH and STATE_LOW: wait 1 sec max before returning and giving the counter thread the
// option to exit
struct timespec ts_max_wait_for_events = {1L, 0L};
if (_state == STATE_DEBOUNCE || _state == STATE_HIGH_WAIT) {
// STATE_DEBOUNCE AND STATE_HIGH_WAIT: wait max until next state transition is scheduled
struct timespec ts_now = {0L, 0L};
clock_gettime(CLOCK_REALTIME, &ts_now);
timespec_sub(_ts_next_state_transition, ts_now, ts_max_wait_for_events);
}

// wait for GPIO events unless we're already at the next state transition
int event_wait_result = 0;
if (ts_max_wait_for_events.tv_sec >= 0 && ts_max_wait_for_events.tv_nsec >= 0) {
event_wait_result = gpiod_line_event_wait(_line, &ts_max_wait_for_events);
print(log_debug, "MeterS0:HWIF_GPIOD: wait for gpio line event returned %d", "S0",
event_wait_result);
} else {
print(log_debug, "MeterS0:HWIF_GPIOD: no wait - scheduled state transition due", "S0");
}

// read & handle event if one was detected: ensure that _gpio_line_status reflects the physical
// world
if (event_wait_result < 0) { // error
throw vz::VZException("error waiting for event, errno " + std::to_string(errno));
} else if (event_wait_result > 0) { // event received
if (gpiod_line_event_read(_line, &event)) {
throw vz::VZException("error reading event, errno " + std::to_string(errno));
}
print(log_info,
"[%ld.%ld] GPIO event %d (1=rising edge, 2=falling edge) in current state %d", "S0",
event.ts.tv_sec, event.ts.tv_nsec, event.event_type, _state);
if (event.event_type == GPIOD_LINE_EVENT_RISING_EDGE) {
_gpio_line_status = 1;
} else {
_gpio_line_status = 0;
}
}

// transition state machine depending on events, timeouts, gpio line state & configuration
States newstate = NO_TRANSITION;
switch (_state) {
case STATE_LOW:
if (event_wait_result > 0 && event.event_type == GPIOD_LINE_EVENT_RISING_EDGE) {
// if rising edge detected, transition to STATE_DEBOUNCE (if configured),
// STATE_HIGH_WAIT (if configured) or directly to STATE_HIGH
if (_debounce_delay_ms > 0) {
newstate = STATE_DEBOUNCE;
} else if (_high_wait_ms > 0) {
newstate = STATE_HIGH_WAIT;
} else {
newstate = STATE_HIGH;
}
}
break; // no state change on timeout or falling edge event

case STATE_DEBOUNCE:
if (event_wait_result == 0) {
// if wait time exceeded, transition to STATE_LOW if GPIO line is low
// and STATE_HIGH_WAIT (if configured) or STATE_HIGH if GPIO line is high
if (_gpio_line_status == 0) {
newstate = STATE_LOW;
} else if (_high_wait_ms > 0) {
newstate = STATE_HIGH_WAIT;
} else {
newstate = STATE_HIGH;
}
}
break; // no state change on either rising or falling edge event

case STATE_HIGH_WAIT:
if (event_wait_result == 0) {
// if wait time exceeded, transition to STATE_HIGH
newstate = STATE_HIGH;
}
[[fallthrough]]; // STATE_HIGH_WAIT reacts on events the same way as STATE_HIGH
case STATE_HIGH:
if (event_wait_result > 0 && event.event_type == GPIOD_LINE_EVENT_FALLING_EDGE) {
// if falling edge detected, transition to STATE_DEBOUNCE (if configured) or directly to
// STATE_LOW
newstate = (_debounce_delay_ms > 0) ? STATE_DEBOUNCE : STATE_LOW;
}
break; // no state change on rising edge event or (in case of STATE_HIGH) timeout

default:
throw vz::VZException("Illegal state in state machine");
}

// set timestamps for automated state transitions for STATE_DEBOUNCE and STATE_HIGH_WAIT
if (newstate == STATE_DEBOUNCE || newstate == STATE_HIGH_WAIT) {
clock_gettime(CLOCK_REALTIME, &_ts_next_state_transition);
timespec_add_ms(_ts_next_state_transition,
(newstate == STATE_DEBOUNCE) ? _debounce_delay_ms : _high_wait_ms);
} else {
_ts_next_state_transition = {-1L, -1L};
}

if (newstate != NO_TRANSITION) {
print(
log_info,
"MeterS0:HWIF_GPIOD: state machine transition from %d to %d with a timeout at %ld.%ld",
"S0", _state, newstate, _ts_next_state_transition.tv_sec,
_ts_next_state_transition.tv_nsec);
if (newstate == STATE_HIGH) {
_high_count++;
} else if (newstate == STATE_LOW) {
_high_count = 0;
}
_state = newstate;
}

// A pulse is detected at the first transition to high after a low (i.e. once _high_count is
// exactly 1). A high->debounce->high will therefore not be counted as pulse. According to
// https://github.com/volkszaehler/vzlogger/commit/790438d69453cc3dfd3cbcdc92fe5a9d18963a9e in
// case of detected pulse, it should return true and set timeout to false to avoid additional
// debouncing in the calling code. In all other cases, it should return false and set timeout to
// true to indicate that no error has happened.

int pulse_detected = (newstate == STATE_HIGH) && (_high_count == 1);
if (newstate == STATE_HIGH && !pulse_detected) {
print(log_info,
"MeterS0:HWIF_GPIOD: ignoring transition to HIGH state due to high state count %d",
"S0", _high_count);
}
timeout = !pulse_detected;
return (pulse_detected);
}
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ set(test_libraries
${GNUTLS_LIBRARIES}
${OCR_LIBRARIES}
${OPENSSL_LIBRARIES}
${GPIOD_LIBRARY}
)

if(SML_FOUND AND ENABLE_SML)
Expand Down
Loading