HID-over-GATT implementation with the BLE API. This library allows to create devices such as mouse, keyboard or joystick, over Bluetooth Low Energy.

Dependents:   BLENano_HID BLE_HID_MouseScrollDemo BLE_HID_KeyboardStreamDemo Shervs_TestKeyboard_TinyBLE ... more

The development repository is currently hosted on github. It contains examples and documentation. This is a snapshot of the library. The documentation can be read on github, or on docs.mbed.com.

Revision:
0:cfd70fa91663
Child:
1:7a6c2e2c9371
diff -r 000000000000 -r cfd70fa91663 KeyboardService.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/KeyboardService.h	Tue Sep 15 20:16:58 2015 +0100
@@ -0,0 +1,278 @@
+#include <errno.h>
+#include "mbed.h"
+#include "CircularBuffer.h"
+
+#include "HIDServiceBase.h"
+#include "Keyboard_types.h"
+
+/* TODO: make this easier to configure by application (e.g. as a template parameter for
+ * KeyboardService) */
+#ifndef KEYBUFFER_SIZE
+#define KEYBUFFER_SIZE 512
+#endif
+
+report_map_t KEYBOARD_REPORT_MAP = {
+    USAGE_PAGE(1),      0x01,       // Generic Desktop Ctrls
+    USAGE(1),           0x06,       // Keyboard
+    COLLECTION(1),      0x01,       // Application
+    USAGE_PAGE(1),      0x07,       //   Kbrd/Keypad
+    USAGE_MINIMUM(1),   0xE0,
+    USAGE_MAXIMUM(1),   0xE7,
+    LOGICAL_MINIMUM(1), 0x00,
+    LOGICAL_MAXIMUM(1), 0x01,
+    REPORT_SIZE(1),     0x01,       //   1 byte (Modifier)
+    REPORT_COUNT(1),    0x08,
+    INPUT(1),           0x02,       //   Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position
+    REPORT_COUNT(1),    0x01,       //   1 byte (Reserved)
+    REPORT_SIZE(1),     0x08,
+    INPUT(1),           0x01,       //   Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
+    REPORT_COUNT(1),    0x05,       //   5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
+    REPORT_SIZE(1),     0x01,
+    USAGE_PAGE(1),      0x08,       //   LEDs
+    USAGE_MINIMUM(1),   0x01,       //   Num Lock
+    USAGE_MAXIMUM(1),   0x05,       //   Kana
+    OUTPUT(1),          0x02,       //   Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
+    REPORT_COUNT(1),    0x01,       //   3 bits (Padding)
+    REPORT_SIZE(1),     0x03,
+    OUTPUT(1),          0x01,       //   Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
+    REPORT_COUNT(1),    0x06,       //   6 bytes (Keys)
+    REPORT_SIZE(1),     0x08,
+    LOGICAL_MINIMUM(1), 0x00,
+    LOGICAL_MAXIMUM(1), 0x65,       //   101 keys
+    USAGE_PAGE(1),      0x07,       //   Kbrd/Keypad
+    USAGE_MINIMUM(1),   0x00,
+    USAGE_MAXIMUM(1),   0x65,
+    INPUT(1),           0x00,       //   Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
+    END_COLLECTION(0),
+};
+
+static uint8_t inputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+static const uint8_t emptyInputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+static uint8_t outputReportData[] = { 0 };
+
+/**
+ * @class KeyBuffer
+ *
+ * Buffer used to store keys to send.
+ * Internally, it is a CircularBuffer, with the added capability of putting the last char back in,
+ * when we're unable to send it (ie. when BLE stack is busy)
+ */
+class KeyBuffer: public CircularBuffer<uint8_t, KEYBUFFER_SIZE>
+{
+public:
+    KeyBuffer() :
+        CircularBuffer(),
+        dataIsPending (false),
+        keyUpIsPending (false)
+    {
+    }
+
+    /**
+     * Mark a character as pending. When a freshly popped character cannot be sent, because the
+     * underlying stack is busy, we set it as pending, and it will get popped in priority by @ref
+     * getPending once reports can be sent again.
+     *
+     * @param data  The character to send in priority. The second keyUp report is implied.
+     */
+    void setPending(uint8_t data)
+    {
+        MBED_ASSERT(dataIsPending == false);
+
+        dataIsPending = true;
+        pendingData = data;
+        keyUpIsPending = true;
+    }
+
+    /**
+     * Get pending char. Either from the high priority buffer (set with setPending), or from the
+     * circular buffer.
+     *
+     * @param   data Filled with the pending data, when present
+     * @return  true if data was filled
+     */
+    bool getPending(uint8_t &data)
+    {
+        if (dataIsPending) {
+            dataIsPending = false;
+            data = pendingData;
+            return true;
+        }
+
+        return pop(data);
+    }
+
+    bool isSomethingPending(void)
+    {
+        return dataIsPending || keyUpIsPending || !empty();
+    }
+
+    /**
+     * Signal that a keyUp report is pending. This means that a character has successfully been
+     * sent, but the subsequent keyUp report failed. This report is of highest priority than the
+     * next character.
+     */
+    void setKeyUpPending(void)
+    {
+        keyUpIsPending = true;
+    }
+
+    /**
+     * Signal that no high-priority report is pending anymore, we can go back to the normal queue.
+     */
+    void clearKeyUpPending(void)
+    {
+        keyUpIsPending = false;
+    }
+
+    bool isKeyUpPending(void)
+    {
+        return keyUpIsPending;
+    }
+
+protected:
+    bool dataIsPending;
+    uint8_t pendingData;
+    bool keyUpIsPending;
+};
+
+class KeyboardService : public HIDServiceBase, public Stream
+{
+public:
+    KeyboardService(BLE &_ble) :
+        HIDServiceBase(_ble,
+                KEYBOARD_REPORT_MAP, sizeof(KEYBOARD_REPORT_MAP),
+                inputReport         = emptyInputReportData,
+                outputReport        = outputReportData,
+                featureReport       = NULL,
+                inputReportLength   = sizeof(inputReportData),
+                outputReportLength  = sizeof(outputReportData),
+                featureReportLength = 0,
+                reportTickerDelay   = 24),
+        failedReports(0)
+    {
+    }
+
+    virtual ble_error_t send(const report_t report)
+    {
+        static unsigned int consecutiveFailures = 0;
+        ble_error_t ret = HIDServiceBase::send(report);
+
+        /*
+         * Wait until a buffer is available (onDataSent)
+         * TODO. This won't work, because BUSY error is not only returned when we're short of
+         * notification buffers, but in other cases as well (e.g. when disconnected). We need to
+         * find a reliable way of knowing when we actually need to wait for onDataSent to be called.
+        if (ret == BLE_STACK_BUSY)
+            stopReportTicker();
+         */
+        if (ret == BLE_STACK_BUSY)
+            consecutiveFailures++;
+        else
+            consecutiveFailures = 0;
+
+        if (consecutiveFailures > 20) {
+            /*
+             * We're not tranmitting anything anymore. Might as well avoid overloading the
+             * system in case it can magically fix itself. Ticker will start again on next _putc
+             * call. It could also be started on next connection, but we can't register a callback
+             * for that, currently.
+             */
+            stopReportTicker();
+            consecutiveFailures = 0;
+        }
+
+        return ret;
+    }
+
+    /**
+     * Send an empty report, representing keyUp event
+     */
+    ble_error_t keyUpCode(void)
+    {
+        return send(emptyInputReportData);
+    }
+
+    ble_error_t keyDownCode(uint8_t key, uint8_t modifier)
+    {
+        inputReportData[0] = modifier;
+        inputReportData[2] = keymap[key].usage;
+
+        return send(inputReportData);
+    }
+
+    virtual int _putc(int c) {
+        if (keyBuffer.full()) {
+            return ENOMEM;
+        }
+
+        keyBuffer.push((unsigned char)c);
+
+        if (!reportTickerIsActive)
+            startReportTicker();
+
+        return 0;
+    }
+
+    uint8_t lockStatus() {
+        // TODO: implement numlock/capslock/scrolllock
+        return 0;
+    }
+
+    virtual void sendCallback(void) {
+        ble_error_t ret;
+        uint8_t c;
+
+        if (!keyBuffer.isSomethingPending()) {
+            /* Stop until the next call to putc */
+            stopReportTicker();
+            return;
+        }
+
+        if (!keyBuffer.isKeyUpPending()) {
+            bool hasData = keyBuffer.getPending(c);
+
+            /*
+             * If something is pending and is not a keyUp, getPending *must* return something. The
+             * following is only a sanity check.
+             */
+            MBED_ASSERT(hasData);
+
+            if (hasData) {
+                ret = keyDownCode(c, keymap[c].modifier);
+                if (ret) {
+                    keyBuffer.setPending(c);
+                    failedReports++;
+                    return;
+                }
+            }
+        }
+
+        ret = keyUpCode();
+        if (ret) {
+            keyBuffer.setKeyUpPending();
+            failedReports++;
+        } else {
+            keyBuffer.clearKeyUpPending();
+        }
+    }
+
+    virtual void onDataSent(unsigned count)
+    {
+        if (!reportTickerIsActive && keyBuffer.isSomethingPending())
+            startReportTicker();
+    }
+
+    unsigned long failedReports;
+
+protected:
+    virtual int _getc() {
+        return 0;
+    }
+
+protected:
+    KeyBuffer keyBuffer;
+
+    //GattCharacteristic boot_keyboard_input_report;
+    //GattCharacteristic boot_keyboard_output_report;
+};
+