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.

KeyboardService.h

Committer:
Jean-Philippe Brucker
Date:
2015-09-15
Revision:
0:cfd70fa91663
Child:
1:7a6c2e2c9371

File content as of revision 0:cfd70fa91663:

#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;
};