Based on the example USB HID keyboard https://developer.mbed.org/users/jbru/code/BLE_HID_KeyboardStreamDemo/

Dependencies:   BLE_API mbed nRF51822

Fork of HID-kb by Microbug

Revision:
0:cb1939018833
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/BLE_HID/KeyboardService.h	Mon Nov 02 18:25:58 2015 +0000
@@ -0,0 +1,367 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2015 ARM Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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 descriptor for a standard 101 keys keyboard, following the HID specification example:
+ * - 8 bytes input report (1 byte for modifiers and 6 for keys)
+ * - 1 byte output report (LEDs)
+ */
+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),
+};
+
+/// "keys pressed" report
+static uint8_t inputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+/// "keys released" report
+static const uint8_t emptyInputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
+/// LEDs report
+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
+ * @brief HID-over-Gatt keyboard service
+ *
+ * Send keyboard reports over BLE. Users should rely on the high-level functions provided by the
+ * Stream API. Because we can't send batches of HID reports, we store pending keys in a circular
+ * buffer and rely on the report ticker to spread them over time.
+ *
+ * @code
+ * BLE ble;
+ * KeyboardService kbd(ble);
+ *
+ * void once_connected_and_paired_callback(void)
+ * {
+ *     // Sequentially send keys 'Shift'+'h', 'e', 'l', 'l', 'o', '!' and <enter>
+ *     kbd.printf("Hello!\n");
+ * }
+ * @endcode
+ */
+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 void onConnection(const Gap::ConnectionCallbackParams_t *params)
+    {
+        HIDServiceBase::onConnection(params);
+
+        /* Drain buffer, in case we've been disconnected while transmitting */
+        if (!reportTickerIsActive && keyBuffer.isSomethingPending())
+            startReportTicker();
+    }
+
+    virtual void onDisconnection(const Gap::DisconnectionCallbackParams_t *params)
+    {
+        stopReportTicker();
+        HIDServiceBase::onDisconnection(params);
+    }
+
+    /**
+     * Send raw report. Should only be called by sendCallback.
+     */
+    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 transmitting 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);
+    }
+
+    /**
+     * Send a character, defined by a modifier (CTRL, SHIFT, ALT) and the key
+     *
+     * @param key Character to send (as defined in USB HID Usage Tables)
+     * @param modifier Optional modifiers (logical OR of enum MODIFIER_KEY)
+     *
+     * @returns BLE_ERROR_NONE on success, or an error code otherwise.
+     */
+    ble_error_t keyDownCode(uint8_t key, uint8_t modifier)
+    {
+        inputReportData[0] = modifier;
+        inputReportData[2] = keymap[key].usage;
+
+        return send(inputReportData);
+    }
+
+    /**
+     * Push a key on the internal FIFO
+     *
+     * @param c ASCII character to send
+     *
+     * @returns 0 on success, or ENOMEM when the FIFO is full.
+     */
+    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;
+    }
+
+    /**
+     * Pop a key from the internal FIFO, and attempt to send it over BLE
+     */
+    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();
+        }
+    }
+
+    /**
+     * Restart report ticker if it was disabled, after too many consecutive failures.
+     *
+     * This is called by the BLE stack.
+     *
+     * @param count	Number of reports (notifications) sent
+     */
+    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;
+};
+
+