Sfoglia il codice sorgente

Merge branch 'kbf-14' of kraxor/kbf into develop

Bence Balint 3 anni fa
parent
commit
7d5b38c405

+ 1 - 0
CMakeLists.txt

@@ -4,6 +4,7 @@ idf_component_register(
         SRCS
         "src/kbf.cpp"
         "src/adc.cpp"
+        "src/driver/button.cpp"
         "src/driver/lcd.cpp"
         "src/driver/ultrasonic.cpp"
         "src/gpio.cpp"

+ 84 - 0
include/kbf/driver/button.h

@@ -0,0 +1,84 @@
+#ifndef KBF_BUTTON_H
+#define KBF_BUTTON_H
+
+#include "kbf/gpio.h"
+#include "kbf/rtos.h"
+
+namespace kbf::driver {
+    /**
+     * @brief Button driver.
+     */
+    class Button {
+    public:
+        /** @brief Tag used for logging. */
+        static const constexpr char *const TAG = "kbf::driver::Button";
+
+        /**
+         * @brief Default constructor.
+         *
+         * @param pin GPIO pin the button is connected to
+         * @param longPressMs long press delay in milliseconds
+         */
+        explicit Button(int pin, int longPressMs = 1000);
+
+        /** @brief GPIO pin in use. */
+        const int pin;
+
+        /** @brief Long press delay in milliseconds. */
+        const int longPressMs;
+
+        /**
+         * @brief Called when button is pressed.
+         *
+         * @param button instance
+         */
+        void (*onPress)(Button &button) = nullptr;
+
+        /**
+         * @brief Called when button is released.
+         *
+         * @warning Will be called in parallel to onLongRelease() if it's defined after long button presses.
+         *
+         * @param button instance
+         */
+        void (*onRelease)(Button &button) = nullptr;
+
+        /**
+         * @brief Called when button is pressed and held for longPressDelayMs milliseconds.
+         *
+         * @param button instance
+         */
+        void (*onLongPress)(Button &button) = nullptr;
+
+        /**
+         * @brief Called when button is released after at least longPressDelayMs milliseconds.
+         *
+         * @warning Will be called in parallel to onRelease() if it's defined.
+         *
+         * @param button instance
+         */
+        void (*onLongRelease)(Button &button) = nullptr;
+
+    private:
+        kbf::gpio::Input input;
+
+        static void handleHigh(kbf::gpio::Input &);
+
+        static void handleLow(kbf::gpio::Input &);
+
+        [[noreturn]] static void eventTaskHandler(void *);
+
+        static const int      eventHigh = 0;
+        static const int      eventLow  = 1;
+        kbf::rtos::EventGroup eventGroup;  // TODO implement queue
+        const kbf::rtos::Task *eventTask;
+
+        static void longPressTaskHandler(void *);
+
+        static const int      eventLongRelease = 0;
+        kbf::rtos::EventGroup longPressEventGroup;
+        kbf::rtos::Task       *longPressTask   = nullptr;
+    };
+}
+
+#endif //KBF_BUTTON_H

+ 74 - 15
include/kbf/gpio.h

@@ -11,43 +11,39 @@
 namespace kbf::gpio {
     static constexpr const char *TAG = "kbf::gpio";
 
+    /**
+     * @brief GPIO / output
+     */
     class Output {
     public:
         /**
-         * Initialises GPIO pin by resetting it and setting it's direction to GPIO_MODE_OUTPUT.
+         * @brief Initialises GPIO pin by resetting it and setting it's direction to GPIO_MODE_OUTPUT.
          *
          * @param pin GPIO pin number
          */
         explicit Output(int pin);
 
         /**
-         * GPIO pin number
+         * @brief GPIO pin number.
          */
         const gpio_num_t pin;
 
         /**
-         * Sets pin output to HIGH.
+         * @brief Sets pin output to HIGH.
          */
         void high() const;
 
         /**
-         * Sets pin output to LOW.
+         * @brief Sets pin output to LOW.
          */
         void low() const;
 
         /**
-         * Checks the current state of the pin.
-         *
-         * @return true if pin is HIGH; false if pin is LOW
-         */
-        [[nodiscard]] bool isHigh() const;
-
-        /**
-         * Alternates between HIGH and LOW states, sleeping delay_ms milliseconds in between. Starts with HIGH.
+         * @brief Alternates between HIGH and LOW states, sleeping delay_ms milliseconds in between. Starts with HIGH.
          *
          * @note If the limit is negative (default), it will continue indefinitely.
          *
-         * @note This method uses kbf::sleep() which in turn uses vTaskDelay() from RTOS which is fine for
+         * @warning This method uses kbf::sleep() which in turn uses vTaskDelay() from RTOS which is fine for
          * blinking an LED but is probably not good enough for high precision stuff.
          *
          * @param delay_ms delay in milliseconds
@@ -56,7 +52,7 @@ namespace kbf::gpio {
         void startPulse(int delay_ms, int limit = -1);
 
         /**
-         * Stop pulse loop.
+         * @brief Stop pulse loop.
          *
          * @note Will ABORT if pulse loop not running
          */
@@ -68,12 +64,75 @@ namespace kbf::gpio {
         int pulseLimit = 0;
 
         /**
-         * Callback for pulse task.
+         * @brief Callback for pulse task.
          *
          * @param arg Output instance
          */
         static void pulseCallback(void *arg);
     };
+
+    /**
+     * @brief GPIO / input
+     */
+    class Input {
+    public:
+        /**
+         * @brief Initialises GPIO pin by resetting it and setting it's direction to GPIO_MODE_OUTPUT.
+         *
+         * @param pin GPIO pin number
+         * @param userData custom data; useful in callbacks
+         */
+        explicit Input(int pin, void *userData = nullptr);
+
+        /**
+         * @brief Removes interrupt handler.
+         */
+        ~Input();
+
+        /**
+         * @brief GPIO pin number
+         */
+        const gpio_num_t pin;
+
+        /**
+         * @brief Custom data; useful in callbacks
+         */
+         void *const userData;
+
+        /**
+         * @brief Returns the current state of the pin.
+         *
+         * @return true if pin is HIGH; false if LOW
+         */
+        [[nodiscard]] bool isHigh() const;
+
+        /**
+         * @brief Returns the current state of the pin.
+         *
+         * @return true if pin is LOW; false if HIGH
+         */
+        [[nodiscard]] bool isLow() const;
+
+        /**
+         * @brief Method to be called when the pin is pulled high.
+         *
+         * @param input gpio::Input instance
+         */
+        void (*onHigh)(Input &input) = nullptr;
+
+        /**
+         * @brief Method to be called when the pin is pulled low.
+         *
+         * @param input gpio::Output instance
+         */
+        void (*onLow)(Input &input) = nullptr;
+
+    private:
+        /**
+         * @brief ISR handler. Calls onHigh() or onLow() if set.
+         */
+        static void interruptHandler(void *);
+    };
 }
 
 #endif //KBF_GPIO_H

+ 65 - 0
src/driver/button.cpp

@@ -0,0 +1,65 @@
+#include <iostream>
+
+#include <esp_log.h>
+
+#include "kbf/driver/button.h"
+
+using namespace std;
+using namespace kbf;
+
+driver::Button::Button(int pin, int longPressMs) :
+        pin(pin),
+        longPressMs(longPressMs),
+        input(gpio::Input(pin, this)),
+        eventTask(new rtos::Task("kbf_button_task", eventTaskHandler, this)
+        ) {
+
+    ESP_LOGI(TAG, "Button(%d, %d)", pin, longPressMs);
+
+    input.onHigh = handleHigh;
+    input.onLow  = handleLow;
+}
+
+void driver::Button::handleHigh(gpio::Input &input) {
+    auto button = static_cast<driver::Button *>(input.userData);
+
+    button->eventGroup.setBit(eventHigh);
+
+    if (button->longPressTask) delete button->longPressTask; // NOLINT(readability-delete-null-pointer)
+    button->longPressTask = new rtos::Task("kbf_button_lp" + to_string(button->pin), longPressTaskHandler, button);
+}
+
+void driver::Button::handleLow(gpio::Input &input) {
+    auto button = static_cast<driver::Button *>(input.userData);
+
+    button->eventGroup.setBit(eventLow);
+    button->longPressEventGroup.setBit(eventLongRelease);
+}
+
+[[noreturn]] void driver::Button::eventTaskHandler(void *arg) {
+    auto button = static_cast<driver::Button *>(arg);
+
+    while (true) {
+        button->eventGroup.waitForAny({eventLow, eventHigh});
+        if (button->eventGroup.getBit(eventHigh)) {
+            button->eventGroup.clearBit(eventHigh);
+            if (button->onPress) button->onPress(*button);
+        }
+        if (button->eventGroup.getBit(eventLow)) {
+            button->eventGroup.clearBit(eventLow);
+            if (button->onRelease) button->onRelease(*button);
+        }
+    }
+}
+
+void driver::Button::longPressTaskHandler(void *arg) {
+    auto button = static_cast<driver::Button *>(arg);
+
+    if (button->longPressEventGroup.waitForBit(eventLongRelease, button->longPressMs)) {
+        button->longPressEventGroup.clear();
+        return;
+    }
+    if (button->onLongPress) button->onLongPress(*button);
+    button->longPressEventGroup.waitForBit(eventLongRelease);
+    if (button->onLongRelease) button->onLongRelease(*button);
+}

+ 37 - 4
src/gpio.cpp

@@ -24,10 +24,6 @@ void gpio::Output::low() const {
     gpio_set_level(pin, 0);
 }
 
-bool gpio::Output::isHigh() const {
-    return gpio_get_level(pin);
-}
-
 void gpio::Output::startPulse(int delay_ms, int limit) {
     ESP_LOGI(TAG, "starting pulse on GPIO %d; delay = %d, limit = %d", pin, delay_ms, limit);
     string taskName = "kbf_gpio_p" + std::to_string(pin);
@@ -61,3 +57,40 @@ void gpio::Output::pulseCallback(void *arg_ptr) {
         kbf::sleep(output->pulseDelay);
     }
 }
+
+gpio::Input::Input(int pin, void *userData) : pin(static_cast<gpio_num_t>(pin)), userData(userData) {
+    ESP_LOGI(TAG, "setting GPIO %d to INPUT mode", pin);
+
+    static bool isrInstalled = false;
+    if (!isrInstalled) {
+        ESP_LOGI(TAG, "installing ISR service");
+        CHECK(gpio_install_isr_service(0));
+        isrInstalled = true;
+    }
+
+    CHECK(gpio_reset_pin(this->pin));
+    CHECK(gpio_set_direction(this->pin, GPIO_MODE_INPUT));
+    CHECK(gpio_set_intr_type(this->pin, GPIO_INTR_ANYEDGE));
+    CHECK(gpio_isr_handler_add(this->pin, interruptHandler, this));
+}
+
+void gpio::Input::interruptHandler(void *data) {
+    auto input = static_cast<Input *>(data);
+    if (input->isHigh() && input->onHigh) {
+        input->onHigh(*input);
+    } else if (input->isLow() && input->onLow) {
+        input->onLow(*input);
+    }
+}
+
+bool gpio::Input::isHigh() const {
+    return gpio_get_level(pin) == 1;
+}
+
+bool gpio::Input::isLow() const {
+    return gpio_get_level(pin) == 0;
+}
+
+gpio::Input::~Input() {
+    CHECK(gpio_isr_handler_remove(pin));
+}

+ 40 - 0
test/driver/test_button.cpp

@@ -0,0 +1,40 @@
+#include "esp_log.h"
+
+#include "kbf/driver/button.h"
+#include "kbf/rtos.h"
+
+#include <unity.h>
+
+#define KBF_TEST_BUTTON_PIN 19  // use Kconfig
+
+using namespace kbf;
+
+const char *const TAG = "test_kbf::driver::Button";
+static auto eventGroup = rtos::EventGroup();
+
+TEST_CASE("Button driver", "[kbf_driver_button]") {
+    auto button = driver::Button(KBF_TEST_BUTTON_PIN, 2000);
+
+    button.onPress = {[](driver::Button &button) {
+        ESP_LOGI(TAG, "onPress");
+        TEST_ASSERT_EQUAL(KBF_TEST_BUTTON_PIN, button.pin);
+    }};
+    button.onRelease = {[](driver::Button &button) {
+        ESP_LOGI(TAG, "onRelease");
+        TEST_ASSERT_EQUAL(KBF_TEST_BUTTON_PIN, button.pin);
+    }};
+    button.onLongPress = {[](driver::Button &button) {
+        ESP_LOGI(TAG, "onLongPress");
+        TEST_ASSERT_EQUAL(KBF_TEST_BUTTON_PIN, button.pin);
+    }};
+    button.onLongRelease = {[](driver::Button &button) {
+        ESP_LOGI(TAG, "onLongRelease");
+        TEST_ASSERT_EQUAL(KBF_TEST_BUTTON_PIN, button.pin);
+        eventGroup.setBit(0);
+    }};
+
+    ESP_LOGI(TAG, "waiting for onLongRelease...");
+    eventGroup.waitForBit(0);
+    ESP_LOGI(TAG, "success");
+    eventGroup.clear();
+}

+ 66 - 0
test/test_gpio.cpp

@@ -0,0 +1,66 @@
+#include <atomic>
+
+#include "kbf.h"
+#include "kbf/gpio.h"
+
+#include <unity.h>
+
+#define INPUT_PIN   27
+#define INPUT_PIN_2 17
+#define OUTPUT_PIN  13
+
+using namespace std;
+using namespace kbf;
+
+static atomic<int> counter = {0};
+
+TEST_CASE("GPIO", "[kbf_gpio]") {
+    auto input   = new gpio::Input(INPUT_PIN);  // TODO use Kconfig
+    auto notUsed = new gpio::Input(INPUT_PIN_2);
+    TEST_ASSERT_TRUE(notUsed->isLow())
+
+    auto output = gpio::Output(OUTPUT_PIN);  // TODO use Kconfig
+    TEST_ASSERT_EQUAL(OUTPUT_PIN, output.pin);
+
+    kbf::sleep(100);
+    TEST_ASSERT_EQUAL(0, counter);
+
+    output.high();
+    kbf::sleep(100);
+    TEST_ASSERT_EQUAL(0, counter);
+
+    output.low();
+    kbf::sleep(100);
+    TEST_ASSERT_EQUAL(0, counter);
+
+    input->onLow = {[](gpio::Input &input) {
+        TEST_ASSERT_EQUAL(INPUT_PIN, input.pin);
+        counter += 1;
+    }};
+    input->onHigh = {[](gpio::Input &input) {
+        TEST_ASSERT_EQUAL(INPUT_PIN, input.pin);
+        counter += 100;
+    }};
+
+    output.high();
+    kbf::sleep(20);
+    TEST_ASSERT_EQUAL(100, counter);
+
+    kbf::sleep(300);
+    TEST_ASSERT_EQUAL(100, counter);
+
+    output.low();
+    kbf::sleep(20);
+    TEST_ASSERT_EQUAL(101, counter);
+
+    kbf::sleep(300);
+    TEST_ASSERT_EQUAL(101, counter);
+
+    delete input;
+    output.high();
+    kbf::sleep(20);
+    TEST_ASSERT_EQUAL(101, counter);
+    output.low();
+    kbf::sleep(20);
+    TEST_ASSERT_EQUAL(101, counter);
+}

+ 5 - 0
test_app/.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>