diff --git a/Documentation/card10-cfg.rst b/Documentation/card10-cfg.rst index c96a9a0dd865299776f9640c02d44f76884075c9..d37bedca43a49eec53be6a23e2383127a485c903 100644 --- a/Documentation/card10-cfg.rst +++ b/Documentation/card10-cfg.rst @@ -47,6 +47,8 @@ Option name Type Description ------------------ ---------- ----------- ``ble_mac`` Boolean MAC address used for BLE. Format: ``ca:4d:10:xx:xx:xx``. ------------------ ---------- ----------- +``ble_hid_enable`` Boolean Enable the Human Interface Device (HID) characteristics on BLE. +------------------ ---------- ----------- ``ble_log_enable`` Boolean Activate HCI level logging of BLE data. Creates a new btsnoop compatible log file named ``ble.log`` in the ``logs`` folder after each boot if BLE is activated. Keeps the last 10 files. ------------------ ---------- ----------- ``right_scroll`` Boolean Use both right buttons to scroll up and down. Lower left button is SELECT. diff --git a/Documentation/conf.py b/Documentation/conf.py index 10886a9fb03ead2822cafc09b08c717c2cc23b84..a09ee94ed0c473d97e1e1f216f6338df00294105 100644 --- a/Documentation/conf.py +++ b/Documentation/conf.py @@ -117,6 +117,7 @@ html_context = { autodoc_mock_imports = [ "buttons", "interrupt", + "sys_ble_hid", "sys_bme680", "sys_bhi160", "sys_display", diff --git a/Documentation/index.rst b/Documentation/index.rst index 1090ad48cb9d2fce0e4cc5ed58512cd4b133c737..2816dcd17ed6f00868c18b0b8b6ef83c21c9cf9f 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -23,6 +23,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the pycardium/overview pycardium/stdlib pycardium/bhi160 + pycardium/ble_hid pycardium/bme680 pycardium/max30001 pycardium/max86150 diff --git a/Documentation/pycardium/ble_hid.rst b/Documentation/pycardium/ble_hid.rst new file mode 100644 index 0000000000000000000000000000000000000000..98f86b975e2afc9b04adc1ba91901dc2831685f3 --- /dev/null +++ b/Documentation/pycardium/ble_hid.rst @@ -0,0 +1,88 @@ +``ble_hid`` - BLE HID +=============== +The ``ble_hid`` module provides access to the BLE Human Interface Device functionality. + +.. note:: + Make sure to enable the BLE HID functionality in ``card10.cfg`` and reboot your card10 + if you want to use BLE HID. + + Also make sure that the ``adafruit_hid`` folder from the card10 release archive is placed + on the file system of your card10. + + +.. warning:: + At least Ubuntu Linux will keep auto connecting to BLE HID devices once they are + paired to the host computer. If you want to connect your card10 to a phone again, + you might have to temporarily turn off Bluetooth on your computer. + +An example application can be found in the preload directory (named ``HID Demo``). It provides +examples how to use the card10 as keyboard, mouse or volume control. + +Please refer to the Adafruit CircuitPython HID library for examples how to use the HID service. +The card10 implements the same HID descriptors as the Adafruit CircuitPython BLE library and +should be compatible with all uses of the Adafruit CircuitPython HID library. + +**Example emulating a keyboard**: + +Adapted from https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/master/CPB_Keybutton_BLE/cpb_keybutton_ble.py + +A more complete version of this example can be found in the HID Demo app on your card10. + +.. code-block:: python + + import ble_hid + from adafruit_hid.keyboard import Keyboard + from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS + from adafruit_hid.keycode import Keycode + + k = Keyboard(ble_hid.devices) + kl = KeyboardLayoutUS(k) + + k.send(Keycode.BACKSPACE) + + # use keyboard_layout for words + kl.write("Demo with a long text to show how fast a card10 can type!") + + # add shift modifier + k.send(Keycode.SHIFT, Keycode.F19) + + +**Example emulating a mouse**: + +.. code-block:: python + + import ble_hid + import bhi160 + import buttons + from adafruit_hid.mouse import Mouse + + m = Mouse(ble_hid.devices) + + def send_report(samples): + if len(samples) > 0: + x = -int(samples[0].z) + y = -int(samples[0].y) + m.move(x, y) + + sensor = bhi160.BHI160Orientation(sample_rate=10, callback=send_report) + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + m.click(Mouse.MIDDLE_BUTTON) + elif b_new == buttons.BOTTOM_RIGHT: + m.click(Mouse.RIGHT_BUTTON) + elif b_new == buttons.BOTTOM_LEFT: + m.click(Mouse.LEFT_BUTTON) + +.. note:: + Make sure to catch ``OSError`` exceptions in real applications. The exception will be thrown if + there is connection to the host (or if it is lost) and you want to send an event. + + +.. automodule:: ble_hid + :members: diff --git a/epicardium/ble/ble_main.c b/epicardium/ble/ble_main.c index 431b4310a59cd2b811d1ec39670d8a08023423f4..2e7a5cef4472d5688ae6e7a454eae686c1daf409 100644 --- a/epicardium/ble/ble_main.c +++ b/epicardium/ble/ble_main.c @@ -34,6 +34,7 @@ #include "svc_hrs.h" #include "svc_dis.h" #include "svc_batt.h" +#include "svc_hid.h" #include "svc_rscs.h" #include "bas/bas_api.h" #include "hrps/hrps_api.h" @@ -41,11 +42,13 @@ #include "profiles/gap_api.h" #include "cccd.h" #include "ess.h" +#include "hid.h" #include "ble_api.h" #include "epicardium.h" #include "api/interrupt-sender.h" #include "modules/log.h" +#include "modules/config.h" #define SCAN_REPORTS_NUM 16 @@ -187,6 +190,29 @@ static const uint8_t bleAdvDataDisc[] = 0, /*! tx power */ }; +/*! advertising data, discoverable mode with HID service*/ +static const uint8_t bleAdvDataDiscHID[] = +{ + /*! flags */ + 2, /*! length */ + DM_ADV_TYPE_FLAGS, /*! AD type */ + DM_FLAG_LE_LIMITED_DISC | /*! flags */ + DM_FLAG_LE_BREDR_NOT_SUP, + + 3, + DM_ADV_TYPE_APPEARANCE, + UINT16_TO_BYTES(CH_APPEAR_WATCH), + + /*! service UUID list */ + 17, + DM_ADV_TYPE_128_UUID_PART, + CARD10_UUID_SUFFIX, 0x0, CARD10_UUID_PREFIX, + + 3, /*! length */ + DM_ADV_TYPE_16_UUID_PART, /*! AD type */ + UINT16_TO_BYTES(ATT_UUID_HID_SERVICE) +}; + /*! scan data, discoverable mode */ uint8_t bleScanDataDisc[] = { @@ -232,6 +258,11 @@ static const attsCccSet_t bleCccSet[BLE_NUM_CCC_IDX] = {ESS_TEMP_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* BLE_ESS_TEMP_CCC_IDX */ {ESS_HUMI_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* BLE_ESS_HUMI_CCC_IDX */ {ESS_PRES_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* BLE_ESS_PRES_CCC_IDX */ + {HID_MOUSE_BOOT_IN_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* HIDAPP_MBI_CCC_HDL */ + {HID_KEYBOARD_BOOT_IN_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* HIDAPP_KBI_CCC_HDL */ + {HID_INPUT_REPORT_1_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* HIDAPP_IN_KEYBOARD_CCC_HDL */ + {HID_INPUT_REPORT_2_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* HIDAPP_IN_MOUSE_CCC_HDL */ + {HID_INPUT_REPORT_3_CH_CCC_HDL, ATT_CLIENT_CFG_NOTIFY, DM_SEC_LEVEL_NONE}, /* HIDAPP_IN_CONSUMER_CCC_HDL */ }; /************************************************************************************************** @@ -506,7 +537,12 @@ static void bleSetup(bleMsg_t *pMsg) } /* set advertising and scan response data for discoverable mode */ - AppAdvSetData(APP_ADV_DATA_DISCOVERABLE, sizeof(bleAdvDataDisc), (uint8_t *) bleAdvDataDisc); + if(config_get_boolean_with_default("ble_hid_enable", false)) { + AppAdvSetData(APP_ADV_DATA_DISCOVERABLE, sizeof(bleAdvDataDiscHID), (uint8_t *) bleAdvDataDiscHID); + } else { + AppAdvSetData(APP_ADV_DATA_DISCOVERABLE, sizeof(bleAdvDataDisc), (uint8_t *) bleAdvDataDisc); + } + AppAdvSetData(APP_SCAN_DATA_DISCOVERABLE, sizeof(bleScanDataDisc), (uint8_t *) bleScanDataDisc); /* set advertising and scan response data for connectable mode */ @@ -737,6 +773,7 @@ static void bleProcMsg(bleMsg_t *pMsg) case ATTS_HANDLE_VALUE_CNF: BasProcMsg(&pMsg->hdr); + HidProcMsg(&pMsg->hdr); break; case ATTS_CCC_STATE_IND: @@ -771,6 +808,7 @@ static void bleProcMsg(bleMsg_t *pMsg) connOpen->peerAddr[1], connOpen->peerAddr[0]); BasProcMsg(&pMsg->hdr); bleESS_ccc_update(); + HidProcMsg(&pMsg->hdr); break; case DM_CONN_CLOSE_IND: @@ -992,6 +1030,9 @@ void BleStart(void) SvcBattCbackRegister(BasReadCback, NULL); SvcBattAddGroup(); + if(config_get_boolean_with_default("ble_hid_enable", false)) { + hid_init(); + } /* Reset the device */ DmDevReset(); } diff --git a/epicardium/ble/cccd.h b/epicardium/ble/cccd.h index 2715f6ebb6252e4995b73bb7980795b4c0354083..ea91c630617a13ad159146cf782d6860dd50a88e 100644 --- a/epicardium/ble/cccd.h +++ b/epicardium/ble/cccd.h @@ -7,6 +7,11 @@ enum BLE_ESS_TEMP_CCC_IDX, /*! Environmental sensing service, temperature characteristic */ BLE_ESS_HUMI_CCC_IDX, /*! Environmental sensing service, humidity characteristic */ BLE_ESS_PRES_CCC_IDX, /*! Environmental sensing service, pressure characteristic */ + HIDAPP_MBI_CCC_HDL, /*! HID Boot Mouse Input characteristic */ + HIDAPP_KBI_CCC_HDL, /*! HID Boot Keyboard Input characteristic */ + HIDAPP_IN_KEYBOARD_CCC_HDL, /*! HID Input Report characteristic for keyboard inputs */ + HIDAPP_IN_MOUSE_CCC_HDL, /*! HID Input Report characteristic for mouse inputs */ + HIDAPP_IN_CONSUMER_CCC_HDL, /*! HID Input Report characteristic for consumer control inputs */ BLE_NUM_CCC_IDX }; diff --git a/epicardium/ble/hid.c b/epicardium/ble/hid.c new file mode 100644 index 0000000000000000000000000000000000000000..4386f5b1dd0cbbce6d1e59b33d4fb55a14315033 --- /dev/null +++ b/epicardium/ble/hid.c @@ -0,0 +1,229 @@ +/* + * Based on ble-profiles/sources/apps/hidapp/hidapp_main.c + */ +#include "cccd.h" +#include "hid.h" + +#include "wsf_types.h" +#include "dm_api.h" +#include "att_api.h" +#include "svc_hid.h" +#include "hid/hid_api.h" + +#include "modules/log.h" + +#include <stdio.h> +#include <string.h> +#include <stdint.h> + +/*! HidApp Report Map (Descriptor) */ +/* clang-format off */ + +/* Based on https://github.com/adafruit/Adafruit_CircuitPython_BLE/blob/master/adafruit_ble/services/standard/hid.py */ +const uint8_t hidReportMap[] = +{ + 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ + 0x09, 0x06, /* Usage (Keyboard) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x85, HIDAPP_KEYBOARD_REPORT_ID, /* Report ID (1) */ + 0x05, 0x07, /* Usage Page (Kbrd/Keypad) */ + 0x19, 0xE0, /* Usage Minimum (\xE0) */ + 0x29, 0xE7, /* Usage Maximum (\xE7) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x75, 0x01, /* Report Size (1) */ + 0x95, 0x08, /* Report Count (8) */ + 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x19, 0x00, /* Usage Minimum (\x00) */ + 0x29, 0x89, /* Usage Maximum (\x89) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x89, /* Logical Maximum (137) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x06, /* Report Count (6) */ + 0x81, 0x00, /* Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x05, 0x08, /* Usage Page (LEDs) */ + 0x19, 0x01, /* Usage Minimum (Num Lock) */ + 0x29, 0x05, /* Usage Maximum (Kana) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x75, 0x01, /* Report Size (1) */ + 0x95, 0x05, /* Report Count (5) */ + 0x91, 0x02, /* Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */ + 0x95, 0x03, /* Report Count (3) */ + 0x91, 0x01, /* Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) */ + 0xC0, /* End Collection */ + 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ + 0x09, 0x02, /* Usage (Mouse) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x09, 0x01, /* Usage (Pointer) */ + 0xA1, 0x00, /* Collection (Physical) */ + 0x85, HIDAPP_MOUSE_REPORT_ID, /* Report ID (2) */ + 0x05, 0x09, /* Usage Page (Button) */ + 0x19, 0x01, /* Usage Minimum (\x01) */ + 0x29, 0x05, /* Usage Maximum (\x05) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x95, 0x05, /* Report Count (5) */ + 0x75, 0x01, /* Report Size (1) */ + 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x95, 0x01, /* Report Count (1) */ + 0x75, 0x03, /* Report Size (3) */ + 0x81, 0x01, /* Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ + 0x09, 0x30, /* Usage (X) */ + 0x09, 0x31, /* Usage (Y) */ + 0x15, 0x81, /* Logical Minimum (-127) */ + 0x25, 0x7F, /* Logical Maximum (127) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x02, /* Report Count (2) */ + 0x81, 0x06, /* Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) */ + 0x09, 0x38, /* Usage (Wheel) */ + 0x15, 0x81, /* Logical Minimum (-127) */ + 0x25, 0x7F, /* Logical Maximum (127) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x01, /* Report Count (1) */ + 0x81, 0x06, /* Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) */ + 0xC0, /* End Collection */ + 0xC0, /* End Collection */ + 0x05, 0x0C, /* Usage Page (Consumer) */ + 0x09, 0x01, /* Usage (Consumer Control) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x85, HIDAPP_CONSUMER_REPORT_ID, /* Report ID (3) */ + 0x75, 0x10, /* Report Size (16) */ + 0x95, 0x01, /* Report Count (1) */ + 0x15, 0x01, /* Logical Minimum (1) */ + 0x26, 0x8C, 0x02, /* Logical Maximum (652) */ + 0x19, 0x01, /* Usage Minimum (Consumer Control) */ + 0x2A, 0x8C, 0x02, /* Usage Maximum (AC Send) */ + 0x81, 0x00, /* Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0xC0, /* End Collection */ +#if 0 + 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ + 0x09, 0x05, /* Usage (Game Pad) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x85, 0x05, /* Report ID (5) */ + 0x05, 0x09, /* Usage Page (Button) */ + 0x19, 0x01, /* Usage Minimum (\x01) */ + 0x29, 0x10, /* Usage Maximum (\x10) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x25, 0x01, /* Logical Maximum (1) */ + 0x75, 0x01, /* Report Size (1) */ + 0x95, 0x10, /* Report Count (16) */ + 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */ + 0x15, 0x81, /* Logical Minimum (-127) */ + 0x25, 0x7F, /* Logical Maximum (127) */ + 0x09, 0x30, /* Usage (X) */ + 0x09, 0x31, /* Usage (Y) */ + 0x09, 0x32, /* Usage (Z) */ + 0x09, 0x35, /* Usage (Rz) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x04, /* Report Count (4) */ + 0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) */ + 0xC0, /* End Collection */ +#endif +}; +/* clang-format on */ + +const uint16_t hidReportMapLen = sizeof(hidReportMap); + +/*! HID Report Type/ID and attribute handle map */ +/* clang-format off */ +static const hidReportIdMap_t hidAppReportIdSet[] = +{ + /* type ID handle */ + {HID_REPORT_TYPE_INPUT, HIDAPP_KEYBOARD_REPORT_ID, HID_INPUT_REPORT_1_HDL}, /* Keyboard Input Report */ + {HID_REPORT_TYPE_OUTPUT, HIDAPP_KEYBOARD_REPORT_ID, HID_OUTPUT_REPORT_HDL}, /* Keyboard Output Report */ + {HID_REPORT_TYPE_FEATURE, HIDAPP_KEYBOARD_REPORT_ID, HID_FEATURE_REPORT_HDL}, /* Keyboard Feature Report */ + {HID_REPORT_TYPE_INPUT, HIDAPP_MOUSE_REPORT_ID, HID_INPUT_REPORT_2_HDL}, /* Mouse Input Report */ + {HID_REPORT_TYPE_INPUT, HIDAPP_CONSUMER_REPORT_ID, HID_INPUT_REPORT_3_HDL}, /* Consumer Control Input Report */ + {HID_REPORT_TYPE_INPUT, HID_KEYBOARD_BOOT_ID, HID_KEYBOARD_BOOT_IN_HDL}, /* Boot Keyboard Input Report */ + {HID_REPORT_TYPE_OUTPUT, HID_KEYBOARD_BOOT_ID, HID_KEYBOARD_BOOT_OUT_HDL}, /* Boot Keyboard Output Report */ + {HID_REPORT_TYPE_INPUT, HID_MOUSE_BOOT_ID, HID_MOUSE_BOOT_IN_HDL}, /* Boot Mouse Input Report */ +}; +/* clang-format on */ + +void hidAppOutputCback( + dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport +); +void hidAppFeatureCback( + dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport +); +void hidAppInfoCback(dmConnId_t connId, uint8_t type, uint8_t value); + +/*! HID Profile Configuration */ +/* clang-format off */ +static const hidConfig_t hidAppHidConfig = +{ + (hidReportIdMap_t*) hidAppReportIdSet, /* Report ID to Attribute Handle map */ + sizeof(hidAppReportIdSet)/sizeof(hidReportIdMap_t), /* Size of Report ID to Attribute Handle map */ + &hidAppOutputCback, /* Output Report Callback */ + &hidAppFeatureCback, /* Feature Report Callback */ + &hidAppInfoCback /* Info Callback */ +}; +/* clang-format on */ + +static void hidAppReportInit(void) +{ + uint8_t iKeyboardBuffer[HIDAPP_KEYBOARD_INPUT_REPORT_LEN]; + uint8_t iMouseBuffer[HIDAPP_MOUSE_INPUT_REPORT_LEN]; + uint8_t iConsumerBuffer[HIDAPP_CONSUMER_INPUT_REPORT_LEN]; + uint8_t oBuffer[HIDAPP_OUTPUT_REPORT_LEN]; + uint8_t fBuffer[HIDAPP_FEATURE_REPORT_LEN]; + + /* Keyboard Input report */ + memset(iKeyboardBuffer, 0, HIDAPP_KEYBOARD_INPUT_REPORT_LEN); + AttsSetAttr( + HID_INPUT_REPORT_1_HDL, + HIDAPP_KEYBOARD_INPUT_REPORT_LEN, + iKeyboardBuffer + ); + + /* Mouse Input report */ + memset(iMouseBuffer, 0, HIDAPP_MOUSE_INPUT_REPORT_LEN); + AttsSetAttr( + HID_INPUT_REPORT_2_HDL, + HIDAPP_MOUSE_INPUT_REPORT_LEN, + iMouseBuffer + ); + + /* Consumer Control Input report */ + memset(iConsumerBuffer, 0, HIDAPP_CONSUMER_INPUT_REPORT_LEN); + AttsSetAttr( + HID_INPUT_REPORT_3_HDL, + HIDAPP_CONSUMER_INPUT_REPORT_LEN, + iConsumerBuffer + ); + + /* Output report */ + memset(oBuffer, 0, HIDAPP_OUTPUT_REPORT_LEN); + AttsSetAttr(HID_OUTPUT_REPORT_HDL, HIDAPP_OUTPUT_REPORT_LEN, oBuffer); + + /* Feature report */ + memset(fBuffer, 0, HIDAPP_FEATURE_REPORT_LEN); + AttsSetAttr(HID_FEATURE_REPORT_HDL, HIDAPP_FEATURE_REPORT_LEN, fBuffer); +} + +void hid_work_init(void); +void hid_init(void) +{ +#ifdef HID_ATT_DYNAMIC + /* Initialize the dynamic service system */ + AttsDynInit(); + /* Add the HID service dynamically */ + pSHdl = SvcHidAddGroupDyn(); + AttsDynRegister(pSHdl, NULL, HidAttsWriteCback); +#else + /* Add the HID service statically */ + SvcHidAddGroup(); + SvcHidRegister(HidAttsWriteCback, NULL); +#endif /* HID_ATT_DYNAMIC */ + /* Initialize the HID profile */ + HidInit(&hidAppHidConfig); + + /* Initialize the report attributes */ + hidAppReportInit(); + + hid_work_init(); +} diff --git a/epicardium/ble/hid.h b/epicardium/ble/hid.h new file mode 100644 index 0000000000000000000000000000000000000000..82620b7333e25d5c423109227466c3b37154330d --- /dev/null +++ b/epicardium/ble/hid.h @@ -0,0 +1,20 @@ +#pragma once + +#include "wsf_types.h" +#include "wsf_os.h" + +/* The input report fits in one byte */ +#define HIDAPP_KEYBOARD_INPUT_REPORT_LEN 8 +#define HIDAPP_MOUSE_INPUT_REPORT_LEN 4 +#define HIDAPP_CONSUMER_INPUT_REPORT_LEN 2 +#define HIDAPP_OUTPUT_REPORT_LEN 1 +#define HIDAPP_FEATURE_REPORT_LEN 1 + + +/* HID Report IDs */ +#define HIDAPP_KEYBOARD_REPORT_ID 1 +#define HIDAPP_MOUSE_REPORT_ID 2 +#define HIDAPP_CONSUMER_REPORT_ID 3 + +void hid_init(void); +void HidProcMsg(wsfMsgHdr_t *pMsg); diff --git a/epicardium/ble/hid_work.c b/epicardium/ble/hid_work.c new file mode 100644 index 0000000000000000000000000000000000000000..f4eeb2e79881dfdaa86432e482c36a85beb65812 --- /dev/null +++ b/epicardium/ble/hid_work.c @@ -0,0 +1,241 @@ +#include "hid.h" +#include "cccd.h" + +#include "epicardium.h" +#include "modules/log.h" + +#include "dm_api.h" +#include "att_api.h" +#include "app_api.h" +#include "hid/hid_api.h" +#include "svc_hid.h" + +#include "FreeRTOS.h" +#include "queue.h" + +#include <stdint.h> +#include <stdio.h> +#include <string.h> + +/* HidApp TX path flags */ +#define HIDAPP_TX_FLAGS_READY 0x01 + +/*! application control block */ +/* clang-format off */ +struct +{ + uint8_t txFlags; /* transmit flags */ + uint8_t protocolMode; /* current protocol mode */ + uint8_t hostSuspended; /* TRUE if host suspended */ +} hidAppCb; +/* clang-format on */ + +struct report { + uint8_t report_id; + uint8_t data[8]; + uint8_t len; +}; + +#define QUEUE_SIZE 10 + +static QueueHandle_t queue; +static uint8_t buffer[sizeof(struct report) * QUEUE_SIZE]; +static StaticQueue_t queue_data; + +static int hid_queue_data(uint8_t report_id, uint8_t *data, uint8_t len) +{ + struct report report; + + if (report_id < 1 || report_id > 3) { + return -EINVAL; + } + + report.report_id = report_id; + + if (len > sizeof(report.data)) { + return -EINVAL; + } + + memcpy(report.data, data, len); + report.len = len; + + if (xQueueSend(queue, &report, 0) != pdTRUE) { + /* Likely full */ + return -EAGAIN; + } + + return 0; +} + +static bool hid_dequeue_data(dmConnId_t connId) +{ + uint8_t cccHandle; + struct report report; + + //lock(); + + if (!(hidAppCb.txFlags & HIDAPP_TX_FLAGS_READY)) { + //unlock(); + return false; + } + + // Loop until a CCC is enabled or the queue is empty + while (true) { + if (xQueueReceive(queue, &report, 0) != pdTRUE) { + break; + } + + if (HidGetProtocolMode() == HID_PROTOCOL_MODE_BOOT) { + if (report.report_id == HIDAPP_KEYBOARD_REPORT_ID) { + report.report_id = HID_KEYBOARD_BOOT_ID; + cccHandle = HIDAPP_KBI_CCC_HDL; + } else if (report.report_id == HIDAPP_MOUSE_REPORT_ID) { + report.report_id = HID_MOUSE_BOOT_ID; + cccHandle = HIDAPP_MBI_CCC_HDL; + } else { + break; + } + } else { + if (report.report_id == HIDAPP_KEYBOARD_REPORT_ID) { + cccHandle = HIDAPP_IN_KEYBOARD_CCC_HDL; + } else if (report.report_id == HIDAPP_MOUSE_REPORT_ID) { + cccHandle = HIDAPP_IN_MOUSE_CCC_HDL; + } else if (report.report_id == HIDAPP_CONSUMER_REPORT_ID) { + cccHandle = HIDAPP_IN_CONSUMER_CCC_HDL; + } else { + break; + }; + } + + if (AttsCccEnabled(connId, cccHandle) || 1) { + hidAppCb.txFlags &= ~(HIDAPP_TX_FLAGS_READY); + /* Send the message */ + HidSendInputReport( + connId, + report.report_id, + report.len, + report.data + ); + break; + } + } + + //unlock(); + return true; +} + +/*************************************************************************************************/ +/*! + * \brief Callback to handle an output report from the host. + * + * \param connId The connection identifier. + * \param id The ID of the report. + * \param len The length of the report data in pReport. + * \param pReport A buffer containing the report. + * + * \return None. + */ +/*************************************************************************************************/ +void hidAppOutputCback( + dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport +) { + /* TODO: process output reports */ +} + +/*************************************************************************************************/ +/*! + * \brief Callback to handle a feature report from the host. + * + * \param connId The connection identifier. + * \param id The ID of the report. + * \param len The length of the report data in pReport. + * \param pReport A buffer containing the report. + * + * \return None. + */ +/*************************************************************************************************/ +void hidAppFeatureCback( + dmConnId_t connId, uint8_t id, uint16_t len, uint8_t *pReport +) { + /* TODO: process feature reports */ +} + +/*************************************************************************************************/ +/*! + * \brief Callback to handle a change in protocol mode or control point from the host. + * + * \param connId The connection identifier. + * \param mode The type of information (HID_INFO_CONTROL_POINT or HID_INFO_PROTOCOL_MODE) + * \param value The value of the information + * + * \return None. + */ +/*************************************************************************************************/ +void hidAppInfoCback(dmConnId_t connId, uint8_t type, uint8_t value) +{ + if (type == HID_INFO_PROTOCOL_MODE) { + LOG_INFO("hid", "protocol mode: %u\n", value); + hidAppCb.protocolMode = value; + } else if (type == HID_INFO_CONTROL_POINT) { + LOG_INFO("hid", "host suspended: %u\n", value); + hidAppCb.hostSuspended = + (value == HID_CONTROL_POINT_SUSPEND) ? TRUE : FALSE; + } +} + +void HidProcMsg(wsfMsgHdr_t *pMsg) +{ + if (!queue) { + /* Not initialized (yet). */ + return; + } + if (pMsg->event == ATTS_HANDLE_VALUE_CNF) { + if (pMsg->status == ATT_SUCCESS) { + hidAppCb.txFlags |= HIDAPP_TX_FLAGS_READY; + hid_dequeue_data((dmConnId_t)pMsg->param); + } + } + if (pMsg->event == DM_CONN_OPEN_IND) { + hidAppCb.txFlags = HIDAPP_TX_FLAGS_READY; + + struct report report; + while (xQueueReceive(queue, &report, 0) == pdTRUE) + ; + + /* Todo: At this point the CCC descriptors are not set up yet + * and things which get sent until then are discarded. */ + } +} + +int epic_ble_hid_send_report(uint8_t report_id, uint8_t *data, uint8_t len) +{ + dmConnId_t connId = AppConnIsOpen(); + if (connId == DM_CONN_ID_NONE) { + return -EIO; + } + + if (!queue) { + return -EIO; + } + + int ret; + ret = hid_queue_data(report_id, data, len); + + if (ret < 0) { + return ret; + } + + if (hid_dequeue_data(connId)) { + return 0; + } else { + return 1; + } +} + +void hid_work_init(void) +{ + queue = xQueueCreateStatic( + QUEUE_SIZE, sizeof(struct report), buffer, &queue_data + ); + hidAppCb.txFlags = HIDAPP_TX_FLAGS_READY; +} diff --git a/epicardium/ble/meson.build b/epicardium/ble/meson.build index c2f10a12a8976cec3f049d9d8ca4617ef0cb70f9..341ad22571c4d5bc94c9bcf997c320682659142e 100644 --- a/epicardium/ble/meson.build +++ b/epicardium/ble/meson.build @@ -12,4 +12,6 @@ ble_sources = files( 'card10.c', 'ess.c', 'filetransfer.c', + 'hid.c', + 'hid_work.c', ) diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h index 842714028be6bdf0a9a86b31a052fbcf7fa1ec22..3a2fea1a04e31e3e5c3b5a6d84d1cbbdb7445d88 100644 --- a/epicardium/epicardium.h +++ b/epicardium/epicardium.h @@ -158,6 +158,7 @@ typedef _Bool bool; #define API_BLE_GET_LAST_PAIRING_NAME 0x145 #define API_BLE_GET_PEER_DEVICE_NAME 0x146 +#define API_BLE_HID_SEND_REPORT 0x150 /* clang-format on */ @@ -2320,5 +2321,23 @@ API(API_BLE_SET_MODE, void epic_ble_set_mode(bool bondable, bool scanner)); * */ API(API_BLE_GET_SCAN_REPORT, int epic_ble_get_scan_report(struct epic_scan_report *rpt)); + +/** + * Send an input report to the host. + * + * :param uint8_t report_id: The id of the report to use. 1: keyboard, 2: mouse, 3: consumer control + * :param uint8_t *data: Data to be reported. + * :param uint8_t len: Length in bytes of the data to be reported. Maximum length is 8 bytes. + * + * :return: `0` on success, `1` if the report is queued or a negative value if an error occured. Possible + * errors: + * + * - ``-EIO``: There is no host device connected or BLE HID is not enabled. + * - ``-EAGAIN``: There is no space in the queue available. Try again later. + * - ``-EINVAL``: Either the report_id is out of range or the data is too long. + * + */ +API(API_BLE_HID_SEND_REPORT, int epic_ble_hid_send_report(uint8_t report_id, uint8_t *data, uint8_t len)); + #endif /* _EPICARDIUM_H */ diff --git a/lib/sdk/Libraries/BTLE/meson.build b/lib/sdk/Libraries/BTLE/meson.build index 1314ad583ea2e6239b81daa630f4ed454079c7df..2bf03dbb6af2f7d34b6e6c376f9c632a28006d09 100644 --- a/lib/sdk/Libraries/BTLE/meson.build +++ b/lib/sdk/Libraries/BTLE/meson.build @@ -48,7 +48,7 @@ sources = files( 'stack/ble-profiles/sources/apps/meds/meds_htp.c', 'stack/ble-profiles/sources/apps/meds/meds_plx.c', 'stack/ble-profiles/sources/apps/meds/meds_glp.c', -'stack/ble-profiles/sources/apps/hidapp/hidapp_main.c', +#'stack/ble-profiles/sources/apps/hidapp/hidapp_main.c', 'stack/ble-profiles/sources/apps/watch/watch_main.c', 'stack/ble-profiles/sources/apps/datc/datc_main.c', 'stack/ble-profiles/sources/apps/gluc/gluc_main.c', diff --git a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h index fcd657fe391a6e9c9f610db71275a8deeae887dc..ce424a2bcf019a86a928f8ce94d51e61e624c4e0 100644 --- a/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h +++ b/lib/sdk/Libraries/BTLE/stack/ble-profiles/sources/services/svc_hid.h @@ -84,7 +84,7 @@ Macros * */ /**@{*/ -#define HID_START_HDL 0x100 /*!< \brief Start handle. */ +#define HID_START_HDL 0x80 /*!< \brief Start handle. */ #define HID_END_HDL (HID_MAX_HDL - 1) /*!< \brief End handle. */ /************************************************************************************************** diff --git a/preload/adafruit_hid/__init__.py b/preload/adafruit_hid/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5323dd3ed0396e74bdc9603f5e0d50211cc15ca6 --- /dev/null +++ b/preload/adafruit_hid/__init__.py @@ -0,0 +1,56 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`adafruit_hid` +==================================================== + +This driver simulates USB HID devices. + +* Author(s): Scott Shawcroft, Dan Halbert + +Implementation Notes +-------------------- +**Software and Dependencies:** +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +""" + +# imports + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HID.git" + + +def find_device(devices, *, usage_page, usage): + """Search through the provided list of devices to find the one with the matching usage_page and + usage.""" + if hasattr(devices, "send_report"): + devices = [devices] + for device in devices: + if ( + device.usage_page == usage_page + and device.usage == usage + and hasattr(device, "send_report") + ): + return device + raise ValueError("Could not find matching HID device.") diff --git a/preload/adafruit_hid/consumer_control.py b/preload/adafruit_hid/consumer_control.py new file mode 100644 index 0000000000000000000000000000000000000000..70176bb4204004a12fd0a8c05d5dbe71bbbefa4b --- /dev/null +++ b/preload/adafruit_hid/consumer_control.py @@ -0,0 +1,87 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.consumer_control.ConsumerControl` +==================================================== + +* Author(s): Dan Halbert +""" + +import sys + +# if sys.implementation.version[0] < 3: +# raise ImportError( +# "{0} is not supported in CircuitPython 2.x or lower".format(__name__) +# ) + +# pylint: disable=wrong-import-position +import struct +import time +from . import find_device + + +class ConsumerControl: + """Send ConsumerControl code reports, used by multimedia keyboards, remote controls, etc. + """ + + def __init__(self, devices): + """Create a ConsumerControl object that will send Consumer Control Device HID reports. + + Devices can be a list of devices that includes a Consumer Control device or a CC device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._consumer_device = find_device(devices, usage_page=0x0C, usage=0x01) + + # Reuse this bytearray to send consumer reports. + self._report = bytearray(2) + + # Do a no-op to test if HID device is ready. + # If not, wait a bit and try once more. + try: + self.send(0x0) + except OSError: + time.sleep(1) + self.send(0x0) + + def send(self, consumer_code): + """Send a report to do the specified consumer control action, + and then stop the action (so it will not repeat). + + :param consumer_code: a 16-bit consumer control code. + + Examples:: + + from adafruit_hid.consumer_control_code import ConsumerControlCode + + # Raise volume. + consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT) + + # Advance to next track (song). + consumer_control.send(ConsumerControlCode.SCAN_NEXT_TRACK) + """ + struct.pack_into("<H", self._report, 0, consumer_code) + self._consumer_device.send_report(self._report) + self._report[0] = self._report[1] = 0x0 + self._consumer_device.send_report(self._report) diff --git a/preload/adafruit_hid/consumer_control_code.py b/preload/adafruit_hid/consumer_control_code.py new file mode 100644 index 0000000000000000000000000000000000000000..735f771b3c8883a6fc57a7292c4203f091ae2e43 --- /dev/null +++ b/preload/adafruit_hid/consumer_control_code.py @@ -0,0 +1,64 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.consumer_control_code.ConsumerControlCode` +======================================================== + +* Author(s): Dan Halbert +""" + + +class ConsumerControlCode: + """USB HID Consumer Control Device constants. + + This list includes a few common consumer control codes from + https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf#page=75. + + *New in CircuitPython 3.0.* + """ + + # pylint: disable-msg=too-few-public-methods + + RECORD = 0xB2 + """Record""" + FAST_FORWARD = 0xB3 + """Fast Forward""" + REWIND = 0xB4 + """Rewind""" + SCAN_NEXT_TRACK = 0xB5 + """Skip to next track""" + SCAN_PREVIOUS_TRACK = 0xB6 + """Go back to previous track""" + STOP = 0xB7 + """Stop""" + EJECT = 0xB8 + """Eject""" + PLAY_PAUSE = 0xCD + """Play/Pause toggle""" + MUTE = 0xE2 + """Mute""" + VOLUME_DECREMENT = 0xEA + """Decrease volume""" + VOLUME_INCREMENT = 0xE9 + """Increase volume""" diff --git a/preload/adafruit_hid/gamepad.py b/preload/adafruit_hid/gamepad.py new file mode 100644 index 0000000000000000000000000000000000000000..468c0d6b04c8030bb6a13372d71ec0edb16775d0 --- /dev/null +++ b/preload/adafruit_hid/gamepad.py @@ -0,0 +1,177 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018 Dan Halbert for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.gamepad.Gamepad` +==================================================== + +* Author(s): Dan Halbert +""" + +import struct +import time + +from . import find_device + + +class Gamepad: + """Emulate a generic gamepad controller with 16 buttons, + numbered 1-16, and two joysticks, one controlling + ``x` and ``y`` values, and the other controlling ``z`` and + ``r_z`` (z rotation or ``Rz``) values. + + The joystick values could be interpreted + differently by the receiving program: those are just the names used here. + The joystick values are in the range -127 to 127. +""" + + def __init__(self, devices): + """Create a Gamepad object that will send USB gamepad HID reports. + + Devices can be a list of devices that includes a gamepad device or a gamepad device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._gamepad_device = find_device(devices, usage_page=0x1, usage=0x05) + + # Reuse this bytearray to send mouse reports. + # Typically controllers start numbering buttons at 1 rather than 0. + # report[0] buttons 1-8 (LSB is button 1) + # report[1] buttons 9-16 + # report[2] joystick 0 x: -127 to 127 + # report[3] joystick 0 y: -127 to 127 + # report[4] joystick 1 x: -127 to 127 + # report[5] joystick 1 y: -127 to 127 + self._report = bytearray(6) + + # Remember the last report as well, so we can avoid sending + # duplicate reports. + self._last_report = bytearray(6) + + # Store settings separately before putting into report. Saves code + # especially for buttons. + self._buttons_state = 0 + self._joy_x = 0 + self._joy_y = 0 + self._joy_z = 0 + self._joy_r_z = 0 + + # Send an initial report to test if HID device is ready. + # If not, wait a bit and try once more. + try: + self.reset_all() + except OSError: + time.sleep(1) + self.reset_all() + + def press_buttons(self, *buttons): + """Press and hold the given buttons. """ + for button in buttons: + self._buttons_state |= 1 << self._validate_button_number(button) - 1 + self._send() + + def release_buttons(self, *buttons): + """Release the given buttons. """ + for button in buttons: + self._buttons_state &= ~(1 << self._validate_button_number(button) - 1) + self._send() + + def release_all_buttons(self): + """Release all the buttons.""" + + self._buttons_state = 0 + self._send() + + def click_buttons(self, *buttons): + """Press and release the given buttons.""" + self.press_buttons(*buttons) + self.release_buttons(*buttons) + + def move_joysticks(self, x=None, y=None, z=None, r_z=None): + """Set and send the given joystick values. + The joysticks will remain set with the given values until changed + + One joystick provides ``x`` and ``y`` values, + and the other provides ``z`` and ``r_z`` (z rotation). + Any values left as ``None`` will not be changed. + + All values must be in the range -127 to 127 inclusive. + + Examples:: + + # Change x and y values only. + gp.move_joysticks(x=100, y=-50) + + # Reset all joystick values to center position. + gp.move_joysticks(0, 0, 0, 0) + """ + if x is not None: + self._joy_x = self._validate_joystick_value(x) + if y is not None: + self._joy_y = self._validate_joystick_value(y) + if z is not None: + self._joy_z = self._validate_joystick_value(z) + if r_z is not None: + self._joy_r_z = self._validate_joystick_value(r_z) + self._send() + + def reset_all(self): + """Release all buttons and set joysticks to zero.""" + self._buttons_state = 0 + self._joy_x = 0 + self._joy_y = 0 + self._joy_z = 0 + self._joy_r_z = 0 + self._send(always=True) + + def _send(self, always=False): + """Send a report with all the existing settings. + If ``always`` is ``False`` (the default), send only if there have been changes. + """ + struct.pack_into( + "<Hbbbb", + self._report, + 0, + self._buttons_state, + self._joy_x, + self._joy_y, + self._joy_z, + self._joy_r_z, + ) + + if always or self._last_report != self._report: + self._gamepad_device.send_report(self._report) + # Remember what we sent, without allocating new storage. + self._last_report[:] = self._report + + @staticmethod + def _validate_button_number(button): + if not 1 <= button <= 16: + raise ValueError("Button number must in range 1 to 16") + return button + + @staticmethod + def _validate_joystick_value(value): + if not -127 <= value <= 127: + raise ValueError("Joystick value must be in range -127 to 127") + return value diff --git a/preload/adafruit_hid/keyboard.py b/preload/adafruit_hid/keyboard.py new file mode 100644 index 0000000000000000000000000000000000000000..5c0e11deb73168f07e8ac5c65103bc96ea30a170 --- /dev/null +++ b/preload/adafruit_hid/keyboard.py @@ -0,0 +1,164 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Dan Halbert +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.keyboard.Keyboard` +==================================================== + +* Author(s): Scott Shawcroft, Dan Halbert +""" + +import time +from micropython import const + +from .keycode import Keycode + +from . import find_device + +_MAX_KEYPRESSES = const(6) + + +class Keyboard: + """Send HID keyboard reports.""" + + # No more than _MAX_KEYPRESSES regular keys may be pressed at once. + + def __init__(self, devices): + """Create a Keyboard object that will send keyboard HID reports. + + Devices can be a list of devices that includes a keyboard device or a keyboard device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._keyboard_device = find_device(devices, usage_page=0x1, usage=0x06) + + # Reuse this bytearray to send keyboard reports. + self.report = bytearray(8) + + # report[0] modifiers + # report[1] unused + # report[2:8] regular key presses + + # View onto byte 0 in report. + self.report_modifier = memoryview(self.report)[0:1] + + # List of regular keys currently pressed. + # View onto bytes 2-7 in report. + self.report_keys = memoryview(self.report)[2:] + + # Do a no-op to test if HID device is ready. + # If not, wait a bit and try once more. + try: + self.release_all() + except OSError: + time.sleep(1) + self.release_all() + + def press(self, *keycodes): + """Send a report indicating that the given keys have been pressed. + + :param keycodes: Press these keycodes all at once. + :raises ValueError: if more than six regular keys are pressed. + + Keycodes may be modifiers or regular keys. + No more than six regular keys may be pressed simultaneously. + + Examples:: + + from adafruit_hid.keycode import Keycode + + # Press ctrl-x. + kbd.press(Keycode.LEFT_CONTROL, Keycode.X) + + # Or, more conveniently, use the CONTROL alias for LEFT_CONTROL: + kbd.press(Keycode.CONTROL, Keycode.X) + + # Press a, b, c keys all at once. + kbd.press(Keycode.A, Keycode.B, Keycode.C) + """ + for keycode in keycodes: + self._add_keycode_to_report(keycode) + self._keyboard_device.send_report(self.report) + + def release(self, *keycodes): + """Send a USB HID report indicating that the given keys have been released. + + :param keycodes: Release these keycodes all at once. + + If a keycode to be released was not pressed, it is ignored. + + Example:: + + # release SHIFT key + kbd.release(Keycode.SHIFT) + """ + for keycode in keycodes: + self._remove_keycode_from_report(keycode) + self._keyboard_device.send_report(self.report) + + def release_all(self): + """Release all pressed keys.""" + for i in range(8): + self.report[i] = 0 + self._keyboard_device.send_report(self.report) + + def send(self, *keycodes): + """Press the given keycodes and then release all pressed keys. + + :param keycodes: keycodes to send together + """ + self.press(*keycodes) + self.release_all() + + def _add_keycode_to_report(self, keycode): + """Add a single keycode to the USB HID report.""" + modifier = Keycode.modifier_bit(keycode) + if modifier: + # Set bit for this modifier. + self.report_modifier[0] |= modifier + else: + # Don't press twice. + # (I'd like to use 'not in self.report_keys' here, but that's not implemented.) + for i in range(_MAX_KEYPRESSES): + if self.report_keys[i] == keycode: + # Already pressed. + return + # Put keycode in first empty slot. + for i in range(_MAX_KEYPRESSES): + if self.report_keys[i] == 0: + self.report_keys[i] = keycode + return + # All slots are filled. + raise ValueError("Trying to press more than six keys at once.") + + def _remove_keycode_from_report(self, keycode): + """Remove a single keycode from the report.""" + modifier = Keycode.modifier_bit(keycode) + if modifier: + # Turn off the bit for this modifier. + self.report_modifier[0] &= ~modifier + else: + # Check all the slots, just in case there's a duplicate. (There should not be.) + for i in range(_MAX_KEYPRESSES): + if self.report_keys[i] == keycode: + self.report_keys[i] = 0 diff --git a/preload/adafruit_hid/keyboard_layout_us.py b/preload/adafruit_hid/keyboard_layout_us.py new file mode 100644 index 0000000000000000000000000000000000000000..3c8137f1f67e878df01ac99bb91120d79719048b --- /dev/null +++ b/preload/adafruit_hid/keyboard_layout_us.py @@ -0,0 +1,256 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Dan Halbert +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.keyboard_layout_us.KeyboardLayoutUS` +======================================================= + +* Author(s): Dan Halbert +""" + +from .keycode import Keycode + + +class KeyboardLayoutUS: + """Map ASCII characters to appropriate keypresses on a standard US PC keyboard. + + Non-ASCII characters and most control characters will raise an exception. + """ + + # The ASCII_TO_KEYCODE bytes object is used as a table to maps ASCII 0-127 + # to the corresponding # keycode on a US 104-key keyboard. + # The user should not normally need to use this table, + # but it is not marked as private. + # + # Because the table only goes to 127, we use the top bit of each byte (ox80) to indicate + # that the shift key should be pressed. So any values 0x{8,9,a,b}* are shifted characters. + # + # The Python compiler will concatenate all these bytes literals into a single bytes object. + # Micropython/CircuitPython will store the resulting bytes constant in flash memory + # if it's in a .mpy file, so it doesn't use up valuable RAM. + # + # \x00 entries have no keyboard key and so won't be sent. + SHIFT_FLAG = 0x80 + ASCII_TO_KEYCODE = ( + b"\x00" # NUL + b"\x00" # SOH + b"\x00" # STX + b"\x00" # ETX + b"\x00" # EOT + b"\x00" # ENQ + b"\x00" # ACK + b"\x00" # BEL \a + b"\x2a" # BS BACKSPACE \b (called DELETE in the usb.org document) + b"\x2b" # TAB \t + b"\x28" # LF \n (called Return or ENTER in the usb.org document) + b"\x00" # VT \v + b"\x00" # FF \f + b"\x00" # CR \r + b"\x00" # SO + b"\x00" # SI + b"\x00" # DLE + b"\x00" # DC1 + b"\x00" # DC2 + b"\x00" # DC3 + b"\x00" # DC4 + b"\x00" # NAK + b"\x00" # SYN + b"\x00" # ETB + b"\x00" # CAN + b"\x00" # EM + b"\x00" # SUB + b"\x29" # ESC + b"\x00" # FS + b"\x00" # GS + b"\x00" # RS + b"\x00" # US + b"\x2c" # SPACE + b"\x9e" # ! x1e|SHIFT_FLAG (shift 1) + b"\xb4" # " x34|SHIFT_FLAG (shift ') + b"\xa0" # # x20|SHIFT_FLAG (shift 3) + b"\xa1" # $ x21|SHIFT_FLAG (shift 4) + b"\xa2" # % x22|SHIFT_FLAG (shift 5) + b"\xa4" # & x24|SHIFT_FLAG (shift 7) + b"\x34" # ' + b"\xa6" # ( x26|SHIFT_FLAG (shift 9) + b"\xa7" # ) x27|SHIFT_FLAG (shift 0) + b"\xa5" # * x25|SHIFT_FLAG (shift 8) + b"\xae" # + x2e|SHIFT_FLAG (shift =) + b"\x36" # , + b"\x2d" # - + b"\x37" # . + b"\x38" # / + b"\x27" # 0 + b"\x1e" # 1 + b"\x1f" # 2 + b"\x20" # 3 + b"\x21" # 4 + b"\x22" # 5 + b"\x23" # 6 + b"\x24" # 7 + b"\x25" # 8 + b"\x26" # 9 + b"\xb3" # : x33|SHIFT_FLAG (shift ;) + b"\x33" # ; + b"\xb6" # < x36|SHIFT_FLAG (shift ,) + b"\x2e" # = + b"\xb7" # > x37|SHIFT_FLAG (shift .) + b"\xb8" # ? x38|SHIFT_FLAG (shift /) + b"\x9f" # @ x1f|SHIFT_FLAG (shift 2) + b"\x84" # A x04|SHIFT_FLAG (shift a) + b"\x85" # B x05|SHIFT_FLAG (etc.) + b"\x86" # C x06|SHIFT_FLAG + b"\x87" # D x07|SHIFT_FLAG + b"\x88" # E x08|SHIFT_FLAG + b"\x89" # F x09|SHIFT_FLAG + b"\x8a" # G x0a|SHIFT_FLAG + b"\x8b" # H x0b|SHIFT_FLAG + b"\x8c" # I x0c|SHIFT_FLAG + b"\x8d" # J x0d|SHIFT_FLAG + b"\x8e" # K x0e|SHIFT_FLAG + b"\x8f" # L x0f|SHIFT_FLAG + b"\x90" # M x10|SHIFT_FLAG + b"\x91" # N x11|SHIFT_FLAG + b"\x92" # O x12|SHIFT_FLAG + b"\x93" # P x13|SHIFT_FLAG + b"\x94" # Q x14|SHIFT_FLAG + b"\x95" # R x15|SHIFT_FLAG + b"\x96" # S x16|SHIFT_FLAG + b"\x97" # T x17|SHIFT_FLAG + b"\x98" # U x18|SHIFT_FLAG + b"\x99" # V x19|SHIFT_FLAG + b"\x9a" # W x1a|SHIFT_FLAG + b"\x9b" # X x1b|SHIFT_FLAG + b"\x9c" # Y x1c|SHIFT_FLAG + b"\x9d" # Z x1d|SHIFT_FLAG + b"\x2f" # [ + b"\x31" # \ backslash + b"\x30" # ] + b"\xa3" # ^ x23|SHIFT_FLAG (shift 6) + b"\xad" # _ x2d|SHIFT_FLAG (shift -) + b"\x35" # ` + b"\x04" # a + b"\x05" # b + b"\x06" # c + b"\x07" # d + b"\x08" # e + b"\x09" # f + b"\x0a" # g + b"\x0b" # h + b"\x0c" # i + b"\x0d" # j + b"\x0e" # k + b"\x0f" # l + b"\x10" # m + b"\x11" # n + b"\x12" # o + b"\x13" # p + b"\x14" # q + b"\x15" # r + b"\x16" # s + b"\x17" # t + b"\x18" # u + b"\x19" # v + b"\x1a" # w + b"\x1b" # x + b"\x1c" # y + b"\x1d" # z + b"\xaf" # { x2f|SHIFT_FLAG (shift [) + b"\xb1" # | x31|SHIFT_FLAG (shift \) + b"\xb0" # } x30|SHIFT_FLAG (shift ]) + b"\xb5" # ~ x35|SHIFT_FLAG (shift `) + b"\x4c" # DEL DELETE (called Forward Delete in usb.org document) + ) + + def __init__(self, keyboard): + """Specify the layout for the given keyboard. + + :param keyboard: a Keyboard object. Write characters to this keyboard when requested. + + Example:: + + kbd = Keyboard(usb_hid.devices) + layout = KeyboardLayoutUS(kbd) + """ + + self.keyboard = keyboard + + def write(self, string): + """Type the string by pressing and releasing keys on my keyboard. + + :param string: A string of ASCII characters. + :raises ValueError: if any of the characters are not ASCII or have no keycode + (such as some control characters). + + Example:: + + # Write abc followed by Enter to the keyboard + layout.write('abc\\n') + """ + for char in string: + keycode = self._char_to_keycode(char) + # If this is a shifted char, clear the SHIFT flag and press the SHIFT key. + if keycode & self.SHIFT_FLAG: + keycode &= ~self.SHIFT_FLAG + self.keyboard.press(Keycode.SHIFT) + self.keyboard.press(keycode) + self.keyboard.release_all() + + def keycodes(self, char): + """Return a tuple of keycodes needed to type the given character. + + :param char: A single ASCII character in a string. + :type char: str of length one. + :returns: tuple of Keycode keycodes. + :raises ValueError: if ``char`` is not ASCII or there is no keycode for it. + + Examples:: + + # Returns (Keycode.TAB,) + keycodes('\t') + # Returns (Keycode.A,) + keycode('a') + # Returns (Keycode.SHIFT, Keycode.A) + keycode('A') + # Raises ValueError because it's a accented e and is not ASCII + keycode('é') + """ + keycode = self._char_to_keycode(char) + if keycode & self.SHIFT_FLAG: + return (Keycode.SHIFT, keycode & ~self.SHIFT_FLAG) + + return (keycode,) + + def _char_to_keycode(self, char): + """Return the HID keycode for the given ASCII character, with the SHIFT_FLAG possibly set. + + If the character requires pressing the Shift key, the SHIFT_FLAG bit is set. + You must clear this bit before passing the keycode in a USB report. + """ + char_val = ord(char) + if char_val > 128: + raise ValueError("Not an ASCII character.") + keycode = self.ASCII_TO_KEYCODE[char_val] + if keycode == 0: + raise ValueError("No keycode available for character.") + return keycode diff --git a/preload/adafruit_hid/keycode.py b/preload/adafruit_hid/keycode.py new file mode 100644 index 0000000000000000000000000000000000000000..b6fd7ad3a4fbee42a082870b9c66a14e38510158 --- /dev/null +++ b/preload/adafruit_hid/keycode.py @@ -0,0 +1,315 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.keycode.Keycode` +==================================================== + +* Author(s): Scott Shawcroft, Dan Halbert +""" + + +class Keycode: + """USB HID Keycode constants. + + This list is modeled after the names for USB keycodes defined in + https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf#page=53. + This list does not include every single code, but does include all the keys on + a regular PC or Mac keyboard. + + Remember that keycodes are the names for key *positions* on a US keyboard, and may + not correspond to the character that you mean to send if you want to emulate non-US keyboard. + For instance, on a French keyboard (AZERTY instead of QWERTY), + the keycode for 'q' is used to indicate an 'a'. Likewise, 'y' represents 'z' on + a German keyboard. This is historical: the idea was that the keycaps could be changed + without changing the keycodes sent, so that different firmware was not needed for + different variations of a keyboard. + """ + + # pylint: disable-msg=invalid-name + A = 0x04 + """``a`` and ``A``""" + B = 0x05 + """``b`` and ``B``""" + C = 0x06 + """``c`` and ``C``""" + D = 0x07 + """``d`` and ``D``""" + E = 0x08 + """``e`` and ``E``""" + F = 0x09 + """``f`` and ``F``""" + G = 0x0A + """``g`` and ``G``""" + H = 0x0B + """``h`` and ``H``""" + I = 0x0C + """``i`` and ``I``""" + J = 0x0D + """``j`` and ``J``""" + K = 0x0E + """``k`` and ``K``""" + L = 0x0F + """``l`` and ``L``""" + M = 0x10 + """``m`` and ``M``""" + N = 0x11 + """``n`` and ``N``""" + O = 0x12 + """``o`` and ``O``""" + P = 0x13 + """``p`` and ``P``""" + Q = 0x14 + """``q`` and ``Q``""" + R = 0x15 + """``r`` and ``R``""" + S = 0x16 + """``s`` and ``S``""" + T = 0x17 + """``t`` and ``T``""" + U = 0x18 + """``u`` and ``U``""" + V = 0x19 + """``v`` and ``V``""" + W = 0x1A + """``w`` and ``W``""" + X = 0x1B + """``x`` and ``X``""" + Y = 0x1C + """``y`` and ``Y``""" + Z = 0x1D + """``z`` and ``Z``""" + + ONE = 0x1E + """``1`` and ``!``""" + TWO = 0x1F + """``2`` and ``@``""" + THREE = 0x20 + """``3`` and ``#``""" + FOUR = 0x21 + """``4`` and ``$``""" + FIVE = 0x22 + """``5`` and ``%``""" + SIX = 0x23 + """``6`` and ``^``""" + SEVEN = 0x24 + """``7`` and ``&``""" + EIGHT = 0x25 + """``8`` and ``*``""" + NINE = 0x26 + """``9`` and ``(``""" + ZERO = 0x27 + """``0`` and ``)``""" + ENTER = 0x28 + """Enter (Return)""" + RETURN = ENTER + """Alias for ``ENTER``""" + ESCAPE = 0x29 + """Escape""" + BACKSPACE = 0x2A + """Delete backward (Backspace)""" + TAB = 0x2B + """Tab and Backtab""" + SPACEBAR = 0x2C + """Spacebar""" + SPACE = SPACEBAR + """Alias for SPACEBAR""" + MINUS = 0x2D + """``-` and ``_``""" + EQUALS = 0x2E + """``=` and ``+``""" + LEFT_BRACKET = 0x2F + """``[`` and ``{``""" + RIGHT_BRACKET = 0x30 + """``]`` and ``}``""" + BACKSLASH = 0x31 + r"""``\`` and ``|``""" + POUND = 0x32 + """``#`` and ``~`` (Non-US keyboard)""" + SEMICOLON = 0x33 + """``;`` and ``:``""" + QUOTE = 0x34 + """``'`` and ``"``""" + GRAVE_ACCENT = 0x35 + r""":literal:`\`` and ``~``""" + COMMA = 0x36 + """``,`` and ``<``""" + PERIOD = 0x37 + """``.`` and ``>``""" + FORWARD_SLASH = 0x38 + """``/`` and ``?``""" + + CAPS_LOCK = 0x39 + """Caps Lock""" + + F1 = 0x3A + """Function key F1""" + F2 = 0x3B + """Function key F2""" + F3 = 0x3C + """Function key F3""" + F4 = 0x3D + """Function key F4""" + F5 = 0x3E + """Function key F5""" + F6 = 0x3F + """Function key F6""" + F7 = 0x40 + """Function key F7""" + F8 = 0x41 + """Function key F8""" + F9 = 0x42 + """Function key F9""" + F10 = 0x43 + """Function key F10""" + F11 = 0x44 + """Function key F11""" + F12 = 0x45 + """Function key F12""" + + PRINT_SCREEN = 0x46 + """Print Screen (SysRq)""" + SCROLL_LOCK = 0x47 + """Scroll Lock""" + PAUSE = 0x48 + """Pause (Break)""" + + INSERT = 0x49 + """Insert""" + HOME = 0x4A + """Home (often moves to beginning of line)""" + PAGE_UP = 0x4B + """Go back one page""" + DELETE = 0x4C + """Delete forward""" + END = 0x4D + """End (often moves to end of line)""" + PAGE_DOWN = 0x4E + """Go forward one page""" + + RIGHT_ARROW = 0x4F + """Move the cursor right""" + LEFT_ARROW = 0x50 + """Move the cursor left""" + DOWN_ARROW = 0x51 + """Move the cursor down""" + UP_ARROW = 0x52 + """Move the cursor up""" + + KEYPAD_NUMLOCK = 0x53 + """Num Lock (Clear on Mac)""" + KEYPAD_FORWARD_SLASH = 0x54 + """Keypad ``/``""" + KEYPAD_ASTERISK = 0x55 + """Keypad ``*``""" + KEYPAD_MINUS = 0x56 + """Keyapd ``-``""" + KEYPAD_PLUS = 0x57 + """Keypad ``+``""" + KEYPAD_ENTER = 0x58 + """Keypad Enter""" + KEYPAD_ONE = 0x59 + """Keypad ``1`` and End""" + KEYPAD_TWO = 0x5A + """Keypad ``2`` and Down Arrow""" + KEYPAD_THREE = 0x5B + """Keypad ``3`` and PgDn""" + KEYPAD_FOUR = 0x5C + """Keypad ``4`` and Left Arrow""" + KEYPAD_FIVE = 0x5D + """Keypad ``5``""" + KEYPAD_SIX = 0x5E + """Keypad ``6`` and Right Arrow""" + KEYPAD_SEVEN = 0x5F + """Keypad ``7`` and Home""" + KEYPAD_EIGHT = 0x60 + """Keypad ``8`` and Up Arrow""" + KEYPAD_NINE = 0x61 + """Keypad ``9`` and PgUp""" + KEYPAD_ZERO = 0x62 + """Keypad ``0`` and Ins""" + KEYPAD_PERIOD = 0x63 + """Keypad ``.`` and Del""" + KEYPAD_BACKSLASH = 0x64 + """Keypad ``\\`` and ``|`` (Non-US)""" + + APPLICATION = 0x65 + """Application: also known as the Menu key (Windows)""" + POWER = 0x66 + """Power (Mac)""" + KEYPAD_EQUALS = 0x67 + """Keypad ``=`` (Mac)""" + F13 = 0x68 + """Function key F13 (Mac)""" + F14 = 0x69 + """Function key F14 (Mac)""" + F15 = 0x6A + """Function key F15 (Mac)""" + F16 = 0x6B + """Function key F16 (Mac)""" + F17 = 0x6C + """Function key F17 (Mac)""" + F18 = 0x6D + """Function key F18 (Mac)""" + F19 = 0x6E + """Function key F19 (Mac)""" + + LEFT_CONTROL = 0xE0 + """Control modifier left of the spacebar""" + CONTROL = LEFT_CONTROL + """Alias for LEFT_CONTROL""" + LEFT_SHIFT = 0xE1 + """Shift modifier left of the spacebar""" + SHIFT = LEFT_SHIFT + """Alias for LEFT_SHIFT""" + LEFT_ALT = 0xE2 + """Alt modifier left of the spacebar""" + ALT = LEFT_ALT + """Alias for LEFT_ALT; Alt is also known as Option (Mac)""" + OPTION = ALT + """Labeled as Option on some Mac keyboards""" + LEFT_GUI = 0xE3 + """GUI modifier left of the spacebar""" + GUI = LEFT_GUI + """Alias for LEFT_GUI; GUI is also known as the Windows key, Command (Mac), or Meta""" + WINDOWS = GUI + """Labeled with a Windows logo on Windows keyboards""" + COMMAND = GUI + """Labeled as Command on Mac keyboards, with a clover glyph""" + RIGHT_CONTROL = 0xE4 + """Control modifier right of the spacebar""" + RIGHT_SHIFT = 0xE5 + """Shift modifier right of the spacebar""" + RIGHT_ALT = 0xE6 + """Alt modifier right of the spacebar""" + RIGHT_GUI = 0xE7 + """GUI modifier right of the spacebar""" + + # pylint: enable-msg=invalid-name + @classmethod + def modifier_bit(cls, keycode): + """Return the modifer bit to be set in an HID keycode report if this is a + modifier key; otherwise return 0.""" + return ( + 1 << (keycode - 0xE0) if cls.LEFT_CONTROL <= keycode <= cls.RIGHT_GUI else 0 + ) diff --git a/preload/adafruit_hid/mouse.py b/preload/adafruit_hid/mouse.py new file mode 100644 index 0000000000000000000000000000000000000000..404d7e19c729cc463febc72396002f4dfda2d190 --- /dev/null +++ b/preload/adafruit_hid/mouse.py @@ -0,0 +1,165 @@ +# The MIT License (MIT) +# +# Copyright (c) 2017 Dan Halbert +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +""" +`adafruit_hid.mouse.Mouse` +==================================================== + +* Author(s): Dan Halbert +""" +import time + +from . import find_device + + +class Mouse: + """Send USB HID mouse reports.""" + + LEFT_BUTTON = 1 + """Left mouse button.""" + RIGHT_BUTTON = 2 + """Right mouse button.""" + MIDDLE_BUTTON = 4 + """Middle mouse button.""" + + def __init__(self, devices): + """Create a Mouse object that will send USB mouse HID reports. + + Devices can be a list of devices that includes a keyboard device or a keyboard device + itself. A device is any object that implements ``send_report()``, ``usage_page`` and + ``usage``. + """ + self._mouse_device = find_device(devices, usage_page=0x1, usage=0x02) + + # Reuse this bytearray to send mouse reports. + # report[0] buttons pressed (LEFT, MIDDLE, RIGHT) + # report[1] x movement + # report[2] y movement + # report[3] wheel movement + self.report = bytearray(4) + + # Do a no-op to test if HID device is ready. + # If not, wait a bit and try once more. + try: + self._send_no_move() + except OSError: + time.sleep(1) + self._send_no_move() + + def press(self, buttons): + """Press the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + + Examples:: + + # Press the left button. + m.press(Mouse.LEFT_BUTTON) + + # Press the left and right buttons simultaneously. + m.press(Mouse.LEFT_BUTTON | Mouse.RIGHT_BUTTON) + """ + self.report[0] |= buttons + self._send_no_move() + + def release(self, buttons): + """Release the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + """ + self.report[0] &= ~buttons + self._send_no_move() + + def release_all(self): + """Release all the mouse buttons.""" + self.report[0] = 0 + self._send_no_move() + + def click(self, buttons): + """Press and release the given mouse buttons. + + :param buttons: a bitwise-or'd combination of ``LEFT_BUTTON``, + ``MIDDLE_BUTTON``, and ``RIGHT_BUTTON``. + + Examples:: + + # Click the left button. + m.click(Mouse.LEFT_BUTTON) + + # Double-click the left button. + m.click(Mouse.LEFT_BUTTON) + m.click(Mouse.LEFT_BUTTON) + """ + self.press(buttons) + self.release(buttons) + + def move(self, x=0, y=0, wheel=0): + """Move the mouse and turn the wheel as directed. + + :param x: Move the mouse along the x axis. Negative is to the left, positive + is to the right. + :param y: Move the mouse along the y axis. Negative is upwards on the display, + positive is downwards. + :param wheel: Rotate the wheel this amount. Negative is toward the user, positive + is away from the user. The scrolling effect depends on the host. + + Examples:: + + # Move 100 to the left. Do not move up and down. Do not roll the scroll wheel. + m.move(-100, 0, 0) + # Same, with keyword arguments. + m.move(x=-100) + + # Move diagonally to the upper right. + m.move(50, 20) + # Same. + m.move(x=50, y=-20) + + # Roll the mouse wheel away from the user. + m.move(wheel=1) + """ + # Send multiple reports if necessary to move or scroll requested amounts. + while x != 0 or y != 0 or wheel != 0: + partial_x = self._limit(x) + partial_y = self._limit(y) + partial_wheel = self._limit(wheel) + self.report[1] = partial_x & 0xFF + self.report[2] = partial_y & 0xFF + self.report[3] = partial_wheel & 0xFF + self._mouse_device.send_report(self.report) + x -= partial_x + y -= partial_y + wheel -= partial_wheel + + def _send_no_move(self): + """Send a button-only report.""" + self.report[1] = 0 + self.report[2] = 0 + self.report[3] = 0 + self._mouse_device.send_report(self.report) + + @staticmethod + def _limit(dist): + return min(127, max(-127, dist)) diff --git a/preload/apps/hid/__init__.py b/preload/apps/hid/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9aaa8c76a0626aebe5aaec3a1ffa846b46af61a1 --- /dev/null +++ b/preload/apps/hid/__init__.py @@ -0,0 +1,191 @@ +import buttons +import color +import display +import ble_hid +import bhi160 +import config + +from adafruit_hid.keyboard import Keyboard +from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS +from adafruit_hid.keycode import Keycode + +from adafruit_hid.mouse import Mouse + +from adafruit_hid.consumer_control_code import ConsumerControlCode +from adafruit_hid.consumer_control import ConsumerControl + + +import time +import os + + +disp = display.open() + + +def keyboard_demo(): + disp.clear() + disp.print(" card10", posy=0) + disp.print(" Keyboard", posy=20) + disp.print("F19", posy=60, fg=color.BLUE) + disp.print("Backspace", posy=40, posx=20, fg=color.RED) + disp.print("Type", posy=60, posx=100, fg=color.GREEN) + disp.update() + + k = Keyboard(ble_hid.devices) + kl = KeyboardLayoutUS(k) + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + # print("back") # for debug in REPL + k.send(Keycode.BACKSPACE) + + elif b_new == buttons.BOTTOM_RIGHT: + # use keyboard_layout for words + kl.write("Demo with a long text to show how fast a card10 can type!") + + elif b_new == buttons.BOTTOM_LEFT: + # add shift modifier + k.send(Keycode.SHIFT, Keycode.F19) + + +def mouse_demo(): + disp.clear() + disp.print(" card10", posy=0) + disp.print(" Mouse", posy=20) + disp.print("Left", posy=60, fg=color.BLUE) + disp.print("Midd", posy=40, posx=100, fg=color.RED) + disp.print("Right", posy=60, posx=80, fg=color.GREEN) + disp.update() + + m = Mouse(ble_hid.devices) + + def send_report(samples): + if len(samples) > 0: + x = -int(samples[0].z) + y = -int(samples[0].y) + print("Reporting", x, y) + + m.move(x, y) + + sensor = bhi160.BHI160Orientation(sample_rate=10, callback=send_report) + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + m.click(Mouse.MIDDLE_BUTTON) + elif b_new == buttons.BOTTOM_RIGHT: + m.click(Mouse.RIGHT_BUTTON) + elif b_new == buttons.BOTTOM_LEFT: + m.click(Mouse.LEFT_BUTTON) + + +def control_demo(): + disp.clear() + disp.print(" card10", posy=0) + disp.print(" Control", posy=20) + disp.print("Play", posy=60, fg=color.BLUE) + disp.print("Vol+", posy=40, posx=100, fg=color.RED) + disp.print("Vol-", posy=60, posx=100, fg=color.GREEN) + disp.update() + + cc = ConsumerControl(ble_hid.devices) + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + cc.send(ConsumerControlCode.VOLUME_INCREMENT) + elif b_new == buttons.BOTTOM_RIGHT: + cc.send(ConsumerControlCode.VOLUME_DECREMENT) + elif b_new == buttons.BOTTOM_LEFT: + cc.send(ConsumerControlCode.PLAY_PAUSE) + + +def selection_screen(): + disp.clear() + disp.print("card10 HID", posy=0) + disp.print(" Demo", posy=20) + disp.print("KBD", posy=60, fg=color.BLUE) + disp.print("Mouse", posy=40, posx=80, fg=color.RED) + disp.print("Control", posy=60, posx=60, fg=color.GREEN) + disp.update() + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + mouse_demo() + elif b_new == buttons.BOTTOM_RIGHT: + control_demo() + elif b_new == buttons.BOTTOM_LEFT: + keyboard_demo() + + +def set_config(enable): + if enable: + config.set_string("ble_hid_enable", "true") + else: + config.set_string("ble_hid_enable", "false") + + disp.clear() + disp.print("resetting", posy=0, fg=[0, 255, 255]) + disp.print("to toggle", posy=20, fg=[0, 255, 255]) + disp.print("HID state", posy=40, fg=[0, 255, 255]) + disp.update() + os.reset() + + +def welcome_screen(is_enabled): + disp.clear() + disp.print("card10 HID", posy=0) + disp.print(" Demo", posy=20) + + if is_enabled: + disp.print("Start ->", posy=40, posx=40, fg=color.GREEN) + disp.print("<- Disable", posy=60, posx=0, fg=color.RED) + else: + disp.print("Enable ->", posy=40, posx=30, fg=color.GREEN) + + disp.update() + + b_old = buttons.read() + while True: + b_new = buttons.read() + if not b_old == b_new: + print(b_new) + b_old = b_new + if b_new == buttons.TOP_RIGHT: + if is_enabled: + # while buttons.read(): pass + selection_screen() + else: + set_config(True) + elif b_new == buttons.BOTTOM_LEFT: + if is_enabled: + set_config(False) + + +is_enabled = False +try: + enabled = config.get_string("ble_hid_enable") + if enabled.lower() == "true" or enabled == "1": + is_enabled = True +except OSError: + pass + +welcome_screen(is_enabled) diff --git a/preload/apps/hid/metadata.json b/preload/apps/hid/metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..a4d27e2c24bfe066d8357d26e8fe5f190862c516 --- /dev/null +++ b/preload/apps/hid/metadata.json @@ -0,0 +1 @@ +{"author": "card10 contributors", "name": "HID Demo", "description": "HID Keyboard/Mouse/Control", "category": "Hardware", "revision": -1, "source":"preload"} diff --git a/pycardium/meson.build b/pycardium/meson.build index 2bfb09d31b1c3578ed0da629cf3f47c1131bee35..8621e66128ca22cecdd3d53b021f5ee03d1fab1d 100644 --- a/pycardium/meson.build +++ b/pycardium/meson.build @@ -17,6 +17,7 @@ modsrc = files( 'modules/power.c', 'modules/spo2_algo.c', 'modules/sys_ble.c', + 'modules/sys_ble_hid.c', 'modules/sys_bme680.c', 'modules/sys_display.c', 'modules/sys_leds.c', diff --git a/pycardium/modules/py/ble_hid.py b/pycardium/modules/py/ble_hid.py new file mode 100644 index 0000000000000000000000000000000000000000..9a0d9715b2396224815aa45f9fdd18349c057a42 --- /dev/null +++ b/pycardium/modules/py/ble_hid.py @@ -0,0 +1,85 @@ +import sys_ble_hid +import time + + +class Report: + """ + The Report class provides an interface to the Adafruit CircuitPython HID library + (https://github.com/adafruit/Adafruit_CircuitPython_HID/). + + ``ble_hid.devices`` exposes a list of reports for use with the CircuitPython HID + classes. You usually do not have to interact with a report yourself but you can make + use of its ``send_report`` method to send raw HID reports to the host. + + **Example using Adafruit CircuitPython HID library**: + + .. code-block:: python + + import ble_hid + from adafruit_hid.mouse import Mouse + + m = Mouse(ble_hid.devices) + m.click(Mouse.MIDDLE_BUTTON) + + **Example using raw non blocking access**: + .. code-block:: python + + import ble_hid + report = ble_hid.Report(report_id=3, blocking=False) # Consumer Control + report.send_report(b'\\xe9\\x00') # 0x00E9: Volume Increase + report.send_report(b'\\x00\\x00') # 0x0000: Release button + + + .. versionadded:: 1.17 + """ + + def __init__(self, report_id, blocking=False, usage_page=None, usage=None): + """ + Initializes a report. + + All parameters are available as properties of the resulting object and can be modified + during runtime. + + :param report_id: The id of the report. Currently supported: 1: Keyboard, 2: Mouse, 3: Consumer control + :param blocking: If True :py:func`send_report()` will try sending the report until + there is space in the queue (unless the host is not connected). + :param usage_page: Used by Adafruit CircuitPython HID library to identify report + :param usage: Used by Adafruit CircuitPython HID library to identify report + """ + + self.report_id = report_id + self.usage_page = usage_page + self.usage = usage + self.blocking = True + + def send_report(self, data): + """ + Tries to send a report to the host. + + :param data: The data to be sent. Must not exceed the configured length of the report. + :rtype: bool + :returns: `True` if the report was sent, `False` if the report was queued for sending. + :raises OSError: if there is no connection to a host or BLE HID is not enabled. + :raises MemoryError: if there was no space in the queue (only raised if ``blocking`` was set to `False`). + """ + + if self.blocking: + # Loop until we are able to place the report in the queue + # Forward all other exceptions to the caller + while True: + try: + return sys_ble_hid.send_report(self.report_id, data) + break + except MemoryError: + time.sleep(0.1) + else: + # Forward all exceptions to the caller + return sys_ble_hid.send_report(self.report_id, data) + + +# Reports as defined in the HID report map in epicardium/ble/hid.c +devices = [ + Report(report_id=1, blocking=True, usage_page=0x01, usage=0x06), + Report(report_id=2, blocking=True, usage_page=0x01, usage=0x02), + Report(report_id=3, blocking=True, usage_page=0x0C, usage=0x01), +] diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build index b8d3310f053c8a46febe1935b15686cbcff86c6a..0130c75b8407fb77535b33bff087ab6160c3b5ff 100644 --- a/pycardium/modules/py/meson.build +++ b/pycardium/modules/py/meson.build @@ -1,6 +1,7 @@ python_modules = files( 'config.py', 'bhi160.py', + 'ble_hid.py', 'bme680.py', 'color.py', 'display.py', diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h index f842f5344616ab139c988be27c73f7f893e88310..f804089f3c66b8abf03e408b0b7deeedaf5228e0 100644 --- a/pycardium/modules/qstrdefs.h +++ b/pycardium/modules/qstrdefs.h @@ -211,6 +211,8 @@ Q(EVENT_HANDLE_NUMERIC_COMPARISON) Q(EVENT_PAIRING_COMPLETE) Q(EVENT_PAIRING_FAILED) Q(EVENT_SCAN_REPORT) +Q(sys_ble_hid) +Q(send_report) /* SpO2 */ Q(spo2_algo) diff --git a/pycardium/modules/sys_ble_hid.c b/pycardium/modules/sys_ble_hid.c new file mode 100644 index 0000000000000000000000000000000000000000..7bc725babaf50cf47a53337fe6ca4e25b6521dea --- /dev/null +++ b/pycardium/modules/sys_ble_hid.c @@ -0,0 +1,45 @@ +#include "epicardium.h" + +#include "py/builtin.h" +#include "py/obj.h" +#include "py/runtime.h" + +static mp_obj_t mp_sys_ble_hid_send_report(mp_obj_t report_id, mp_obj_t data) +{ + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(data, &bufinfo, MP_BUFFER_READ); + + int ret = epic_ble_hid_send_report( + mp_obj_get_int(report_id), bufinfo.buf, bufinfo.len + ); + + if (ret == -EAGAIN) { + mp_raise_msg(&mp_type_MemoryError, NULL); + } + + if (ret < 0) { + mp_raise_OSError(-ret); + } + + return ret == 0 ? mp_const_true : mp_const_false; +} +static MP_DEFINE_CONST_FUN_OBJ_2( + sys_ble_hid_send_report_obj, mp_sys_ble_hid_send_report +); + +static const mp_rom_map_elem_t sys_ble_hid_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_ble_hid) }, + { MP_ROM_QSTR(MP_QSTR_send_report), + MP_ROM_PTR(&sys_ble_hid_send_report_obj) }, +}; +static MP_DEFINE_CONST_DICT( + sys_ble_hid_module_globals, sys_ble_hid_module_globals_table +); + +const mp_obj_module_t sys_ble_hid_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&sys_ble_hid_module_globals, +}; + +/* clang-format off */ +MP_REGISTER_MODULE(MP_QSTR_sys_ble_hid, sys_ble_hid_module, MODULE_HID_ENABLED); diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h index 647b159f2886d1497f31858430c99d53e8a1c751..8749da69403d4eeadd7e799f612c94662bc1d5fd 100644 --- a/pycardium/mpconfigport.h +++ b/pycardium/mpconfigport.h @@ -53,6 +53,7 @@ int mp_hal_csprng_read_int(void); #define MICROPY_PY_UERRNO (1) #define MICROPY_PY_FRAMEBUF (1) #define MICROPY_PY_BLUETOOTH (1) +#define MICROPY_PY_BUILTINS_MEMORYVIEW (1) /* Modules */ #define MODULE_BHI160_ENABLED (1) @@ -61,6 +62,7 @@ int mp_hal_csprng_read_int(void); #define MODULE_BUTTONS_ENABLED (1) #define MODULE_DISPLAY_ENABLED (1) #define MODULE_GPIO_ENABLED (1) +#define MODULE_HID_ENABLED (1) #define MODULE_INTERRUPT_ENABLED (1) #define MODULE_LEDS_ENABLED (1) #define MODULE_LIGHT_SENSOR_ENABLED (1)