From 1d369ffa73ae63af821d012b1c2ab4a07a5dc9c7 Mon Sep 17 00:00:00 2001
From: Joel Spadin <joelspadin@gmail.com>
Date: Sun, 11 Oct 2020 17:03:21 -0500
Subject: [PATCH] feat: only send HID reports to one endpoint

Added some utility functions and an event for tracking the state of the
USB connection.

Updated endpoints.c to select a single endpoint to send HID reports to
based on the status of the USB and BLE connections. Partially fixes #206.

Future commits will add a user setting to control which endpoint is used if
both USB and BLE are ready.
---
 app/CMakeLists.txt                            |   1 +
 .../zmk/events/usb-conn-state-changed.h       |  20 +++
 app/include/zmk/usb.h                         |  12 +-
 app/src/endpoints.c                           | 154 ++++++++++++++----
 app/src/events/usb_conn_state_changed.c       |  10 ++
 app/src/power.c                               |   9 +-
 app/src/usb.c                                 |  29 +++-
 7 files changed, 190 insertions(+), 45 deletions(-)
 create mode 100644 app/include/zmk/events/usb-conn-state-changed.h
 create mode 100644 app/src/events/usb_conn_state_changed.c

diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 8a3971e8..bce21700 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -37,6 +37,7 @@ target_sources(app PRIVATE src/events/keycode_state_changed.c)
 target_sources(app PRIVATE src/events/modifiers_state_changed.c)
 target_sources(app PRIVATE src/events/sensor_event.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c)
+target_sources_ifdef(CONFIG_USB app PRIVATE src/events/usb_conn_state_changed.c)
 if (NOT CONFIG_ZMK_SPLIT_BLE_ROLE_PERIPHERAL)
   target_sources(app PRIVATE src/behaviors/behavior_key_press.c)
   target_sources(app PRIVATE src/behaviors/behavior_reset.c)
diff --git a/app/include/zmk/events/usb-conn-state-changed.h b/app/include/zmk/events/usb-conn-state-changed.h
new file mode 100644
index 00000000..d6cc6985
--- /dev/null
+++ b/app/include/zmk/events/usb-conn-state-changed.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2020 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+#include <zephyr.h>
+#include <usb/usb_device.h>
+
+#include <zmk/event-manager.h>
+#include <zmk/usb.h>
+
+struct usb_conn_state_changed {
+    struct zmk_event_header header;
+    enum zmk_usb_conn_state conn_state;
+};
+
+ZMK_EVENT_DECLARE(usb_conn_state_changed);
\ No newline at end of file
diff --git a/app/include/zmk/usb.h b/app/include/zmk/usb.h
index 452fd54d..30461de2 100644
--- a/app/include/zmk/usb.h
+++ b/app/include/zmk/usb.h
@@ -12,8 +12,18 @@
 #include <zmk/keys.h>
 #include <zmk/hid.h>
 
+enum zmk_usb_conn_state {
+    ZMK_USB_CONN_NONE,
+    ZMK_USB_CONN_POWERED,
+    ZMK_USB_CONN_HID,
+};
+
 enum usb_dc_status_code zmk_usb_get_status();
+enum zmk_usb_conn_state zmk_usb_get_conn_state();
+
+static inline bool zmk_usb_is_powered() { return zmk_usb_get_conn_state() != ZMK_USB_CONN_NONE; }
+static inline bool zmk_usb_is_hid_ready() { return zmk_usb_get_conn_state() == ZMK_USB_CONN_HID; }
 
 #ifdef CONFIG_ZMK_USB
-int zmk_usb_hid_send_report(u8_t *report, size_t len);
+int zmk_usb_hid_send_report(const u8_t *report, size_t len);
 #endif /* CONFIG_ZMK_USB */
\ No newline at end of file
diff --git a/app/src/endpoints.c b/app/src/endpoints.c
index 79d294ef..4f56aa5c 100644
--- a/app/src/endpoints.c
+++ b/app/src/endpoints.c
@@ -8,54 +8,138 @@
 #include <zmk/hid.h>
 #include <zmk/usb.h>
 #include <zmk/hog.h>
+#include <zmk/event-manager.h>
+#include <zmk/events/usb-conn-state-changed.h>
 
 #include <logging/log.h>
 LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 
+enum endpoint {
+    ENDPOINT_USB,
+    ENDPOINT_BLE,
+};
+
+static enum endpoint current_endpoint =
+    COND_CODE_1(IS_ENABLED(CONFIG_ZMK_BLE), (ENDPOINT_BLE), (ENDPOINT_USB));
+
+static int send_keypad_report() {
+    struct zmk_hid_keypad_report *keypad_report = zmk_hid_get_keypad_report();
+
+    switch (current_endpoint) {
+#if IS_ENABLED(CONFIG_ZMK_USB)
+    case ENDPOINT_USB: {
+        int err = zmk_usb_hid_send_report((u8_t *)keypad_report, sizeof(*keypad_report));
+        if (err) {
+            LOG_ERR("FAILED TO SEND OVER USB: %d", err);
+        }
+        return err;
+    }
+#endif /* IS_ENABLED(CONFIG_ZMK_USB) */
+
+#if IS_ENABLED(CONFIG_ZMK_BLE)
+    case ENDPOINT_BLE: {
+        int err = zmk_hog_send_keypad_report(&keypad_report->body);
+        if (err) {
+            LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
+        }
+        return err;
+    }
+#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */
+
+    default:
+        LOG_ERR("Unsupported endpoint %d", current_endpoint);
+        return -ENOTSUP;
+    }
+}
+
+static int send_consumer_report() {
+    struct zmk_hid_consumer_report *consumer_report = zmk_hid_get_consumer_report();
+
+    switch (current_endpoint) {
+#if IS_ENABLED(CONFIG_ZMK_USB)
+    case ENDPOINT_USB: {
+        int err = zmk_usb_hid_send_report((u8_t *)consumer_report, sizeof(*consumer_report));
+        if (err) {
+            LOG_ERR("FAILED TO SEND OVER USB: %d", err);
+        }
+        return err;
+    }
+#endif /* IS_ENABLED(CONFIG_ZMK_USB) */
+
+#if IS_ENABLED(CONFIG_ZMK_BLE)
+    case ENDPOINT_BLE: {
+        int err = zmk_hog_send_consumer_report(&consumer_report->body);
+        if (err) {
+            LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
+        }
+        return err;
+    }
+#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */
+
+    default:
+        LOG_ERR("Unsupported endpoint %d", current_endpoint);
+        return -ENOTSUP;
+    }
+}
+
 int zmk_endpoints_send_report(u8_t usage_page) {
-    int err;
-    struct zmk_hid_keypad_report *keypad_report;
-    struct zmk_hid_consumer_report *consumer_report;
+
     LOG_DBG("usage page 0x%02X", usage_page);
     switch (usage_page) {
     case USAGE_KEYPAD:
-        keypad_report = zmk_hid_get_keypad_report();
-#ifdef CONFIG_ZMK_USB
-        if (zmk_usb_hid_send_report((u8_t *)keypad_report, sizeof(struct zmk_hid_keypad_report)) !=
-            0) {
-            LOG_DBG("USB Send Failed");
-        }
-#endif /* CONFIG_ZMK_USB */
-
-#ifdef CONFIG_ZMK_BLE
-        err = zmk_hog_send_keypad_report(&keypad_report->body);
-        if (err) {
-            LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
-        }
-#endif /* CONFIG_ZMK_BLE */
-
-        break;
+        return send_keypad_report();
     case USAGE_CONSUMER:
-        consumer_report = zmk_hid_get_consumer_report();
-#ifdef CONFIG_ZMK_USB
-        if (zmk_usb_hid_send_report((u8_t *)consumer_report,
-                                    sizeof(struct zmk_hid_consumer_report)) != 0) {
-            LOG_DBG("USB Send Failed");
-        }
-#endif /* CONFIG_ZMK_USB */
-
-#ifdef CONFIG_ZMK_BLE
-        err = zmk_hog_send_consumer_report(&consumer_report->body);
-        if (err) {
-            LOG_ERR("FAILED TO SEND OVER HOG: %d", err);
-        }
-#endif /* CONFIG_ZMK_BLE */
-
-        break;
+        return send_consumer_report();
     default:
         LOG_ERR("Unsupported usage page %d", usage_page);
         return -ENOTSUP;
     }
+}
+
+static bool is_usb_ready() {
+#if IS_ENABLED(CONFIG_ZMK_USB)
+    return zmk_usb_is_hid_ready();
+#else
+    return false;
+#endif
+}
+
+static bool is_ble_ready() {
+#if IS_ENABLED(CONFIG_ZMK_BLE)
+    return zmk_ble_active_profile_is_connected();
+#else
+    return false;
+#endif
+}
+
+static enum endpoint get_selected_endpoint() {
+    if (is_ble_ready()) {
+        if (is_usb_ready()) {
+            LOG_DBG("Both endpoints ready. Selecting USB");
+            // TODO: add user setting to control this
+            return ENDPOINT_USB;
+        }
+
+        return ENDPOINT_BLE;
+    }
+
+    return ENDPOINT_USB;
+}
+
+static int endpoint_listener(const struct zmk_event_header *eh) {
+    enum endpoint new_endpoint = get_selected_endpoint();
+
+    if (new_endpoint != current_endpoint) {
+        // TODO: send null report on previous endpoint
+        current_endpoint = new_endpoint;
+        LOG_INF("Endpoint changed: %d", current_endpoint);
+    }
 
     return 0;
 }
+
+ZMK_LISTENER(endpoint_listener, endpoint_listener);
+#if IS_ENABLED(CONFIG_USB)
+ZMK_SUBSCRIPTION(endpoint_listener, usb_conn_state_changed);
+#endif
+// TODO: add BLE state subscription
\ No newline at end of file
diff --git a/app/src/events/usb_conn_state_changed.c b/app/src/events/usb_conn_state_changed.c
new file mode 100644
index 00000000..d845f6d7
--- /dev/null
+++ b/app/src/events/usb_conn_state_changed.c
@@ -0,0 +1,10 @@
+/*
+ * Copyright (c) 2020 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <kernel.h>
+#include <zmk/events/usb-conn-state-changed.h>
+
+ZMK_EVENT_IMPL(usb_conn_state_changed);
\ No newline at end of file
diff --git a/app/src/power.c b/app/src/power.c
index 73b3f123..bad54d28 100644
--- a/app/src/power.c
+++ b/app/src/power.c
@@ -23,14 +23,7 @@ static u32_t power_last_uptime;
 
 bool is_usb_power_present() {
 #ifdef CONFIG_USB
-    enum usb_dc_status_code usb_status = zmk_usb_get_status();
-    switch (usb_status) {
-    case USB_DC_DISCONNECTED:
-    case USB_DC_UNKNOWN:
-        return false;
-    default:
-        return true;
-    }
+    return zmk_usb_is_powered();
 #else
     return false;
 #endif /* CONFIG_USB */
diff --git a/app/src/usb.c b/app/src/usb.c
index 434b3d4d..d4bc2e41 100644
--- a/app/src/usb.c
+++ b/app/src/usb.c
@@ -13,6 +13,8 @@
 
 #include <zmk/hid.h>
 #include <zmk/keymap.h>
+#include <zmk/event-manager.h>
+#include <zmk/events/usb-conn-state-changed.h>
 
 LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 
@@ -53,9 +55,34 @@ int zmk_usb_hid_send_report(const u8_t *report, size_t len) {
 
 #endif /* CONFIG_ZMK_USB */
 
+static void raise_usb_status_changed_event() {
+    struct usb_conn_state_changed *ev = new_usb_conn_state_changed();
+    ev->conn_state = zmk_usb_get_conn_state();
+
+    ZMK_EVENT_RAISE(ev);
+}
+
 enum usb_dc_status_code zmk_usb_get_status() { return usb_status; }
 
-void usb_status_cb(enum usb_dc_status_code status, const u8_t *params) { usb_status = status; };
+enum zmk_usb_conn_state zmk_usb_get_conn_state() {
+    switch (usb_status) {
+    case USB_DC_DISCONNECTED:
+    case USB_DC_UNKNOWN:
+        return ZMK_USB_CONN_NONE;
+
+    case USB_DC_ERROR:
+    case USB_DC_RESET:
+        return ZMK_USB_CONN_POWERED;
+
+    default:
+        return ZMK_USB_CONN_HID;
+    }
+}
+
+void usb_status_cb(enum usb_dc_status_code status, const u8_t *params) {
+    usb_status = status;
+    raise_usb_status_changed_event();
+};
 
 static int zmk_usb_init(struct device *_arg) {
     int usb_enable_ret;