USB上下スクロールボタンをつくる

 なんか右手首が腱鞘炎気味で少し痛い。打鍵よりも、Xを読むためのタップスクロールが原因じゃないかと思ったりしている。

 というわけで、手持ちの部材でスクロールキーを作ろう。

 ATOM S3 LITEとDual Button Unitを出してくる。

 前にもArduinoベースで作ったことがあるのだけど、普通のキーにすると、長押しでスクロールを連続すると、キーリピートが早すぎるので、ヌルヌルくらいの速度で画面が読めるようにスクロールしたい。

 今回は、HIDはマウスにして、ホイールイベントを使うのがいいだろうか。すると、ESP-IDF + TinyUSBになるかな。

 ATOMS3 LITEは、USBポートは1つしかないので、とりあえず、TinyUSBに割り当てることにして、FWの書き込みはボタンでGPIO0をLowに落として書き込むことにする。というわけで、シリアルコンソールまわりは無効にしておく。

 % idf.py create-project smoothwheel

 % cd smoothwheel

 % idf.py set-target esp32s3

 % idf.py menuconfig

 Component config --->

 ESP System Settings --->

 Channel for console output (None) --->

 Channel for console secondary output (No secondary console) --->

 あれ? TinyUSBの設定がないな... デフォルトでは含まれていないのかな?

 別コンポーネントなのかな... あったあった。

 espressif/esp_tinyusb • v2.2.0 • ESP Component Registry

 https://components.espressif.com/components/espressif/esp_tinyusb/versions/2.2.0/readme

 コンポーネントを追加する。

 % idf.py add-dependency "espressif/esp_tinyusb^2.2.0"

 % cat main/idf_component.yml

 ## IDF Component Manager Manifest File

 dependencies:

 ## Required IDF version

 idf:

 version: '>=4.1.0'

 # # Put list of dependencies here

 # # For components maintained by Espressif:

 # component: "~1.0.0"

 # # For 3rd party components:

 # username/component: ">=1.0.0,<2.0.0"

 # username2/component2:

 # version: "~1.0.0"

 # # For transient dependencies public flag can be set.

 # # public flag doesn't have an effect dependencies of the main component.

 # # All dependencies of main are public by default.

 # public: true

 espressif/esp_tinyusb: ^2.2.0

 もっかいmenuconfigする。esp_tinyusbが起動ログに認識されている。

 NOTICE: Processing 3 dependencies:

 NOTICE: [1/3] espressif/esp_tinyusb (2.2.0)

 NOTICE: [2/3] espressif/tinyusb (0.19.0~3)

 NOTICE: [3/3] idf (5.5.2)

 TinyUSB Stackが表示されている。

 TinyUSB Stack --->

 HIDを一個使うので、1にする。

 Human Interface Device Class (HID) --->

 TinyUSB HID interfaces count = 1

 デバイス情報はこんな感じになる。HIDなので、あんまり気にしない。

 保村して終了する。

 コードはChatGPTに書いてもらう。こんな感じ。しかし、案の定一回では動かないw 修正してもらった。

 % vi main/smoothwheel.c

 #include <stdint.h>

 #include <stdbool.h>

 #include "freertos/FreeRTOS.h"

 #include "freertos/task.h"

 #include "esp_err.h"

 #include "esp_log.h"

 #include "esp_timer.h"

 #include "driver/gpio.h"

 #include "tinyusb.h"

 #include "tinyusb_default_config.h"

 #include "class/hid/hid_device.h"

 // ATOMS3 Lite Grove想定

 #define BTN_UP_GPIO GPIO_NUM_1

 #define BTN_DOWN_GPIO GPIO_NUM_2

 #define BUTTON_ACTIVE_LEVEL 0

 #define DEBOUNCE_MS 25

 #define LONG_PRESS_MS 350

 #define REPEAT_INTERVAL_MS 120

 #define WHEEL_STEP 1

 static const char *TAG = "smoothwheel";

 enum {

 REPORT_ID_MOUSE = 1,

 };

 #define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_HID * TUD_HID_DESC_LEN)

 static bool suspended = false;

 static bool wakeup_host = false;

 /*

 HID report descriptor

 MouseだけのHIDとして定義します。

 */

 const uint8_t hid_report_descriptor[] = {

 TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(REPORT_ID_MOUSE))

 };

 /*

 String descriptor

 */

 const char *hid_string_descriptor[5] = {

 (char[]){0x09, 0x04}, // 0: English

 "M5Stack", // 1: Manufacturer

 "smoothwheel", // 2: Product

 "0001", // 3: Serial

 "Mouse Wheel HID", // 4: HID interface

 };

 /*

 Configuration descriptor

 HID interface 1個だけ。

 */

 static const uint8_t hid_configuration_descriptor[] = {

 TUD_CONFIG_DESCRIPTOR(

 1, // configuration number

 1, // interface count

 0, // string index

 TUSB_DESC_TOTAL_LEN, // total length

 TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP,

 100 // power mA

 ),

 TUD_HID_DESCRIPTOR(

 0, // interface number

 4, // string index

 false, // boot protocol

 sizeof(hid_report_descriptor),

 0x81, // EP IN address

 16, // EP size

 10 // polling interval ms

 ),

 };

 typedef enum {

 SCROLL_UP = 0,

 SCROLL_DOWN,

 } scroll_dir_t;

 typedef struct {

 gpio_num_t gpio;

 scroll_dir_t dir;

 bool stable_pressed;

 bool last_raw_pressed;

 int64_t last_change_ms;

 bool long_mode;

 int64_t press_start_ms;

 int64_t last_repeat_ms;

 } button_state_t;

 static int64_t now_ms(void)

 {

 return esp_timer_get_time() / 1000;

 }

 static bool button_raw_pressed(gpio_num_t gpio)

 {

 return gpio_get_level(gpio) == BUTTON_ACTIVE_LEVEL;

 }

 static void send_mouse_wheel(scroll_dir_t dir)

 {

 if (!tud_mounted()) {

 return;

 }

 if (!tud_hid_ready()) {

 return;

 }

 if (suspended) {

 if (wakeup_host) {

 tud_remote_wakeup();

 wakeup_host = false;

 }

 return;

 }

 int8_t wheel = (dir == SCROLL_UP) ? WHEEL_STEP : -WHEEL_STEP;

 tud_hid_mouse_report(

 REPORT_ID_MOUSE,

 0, // buttons

 0, // x

 0, // y

 wheel, // vertical wheel

 0 // horizontal wheel

 );

 vTaskDelay(pdMS_TO_TICKS(5));

 if (tud_mounted() && tud_hid_ready()) {

 tud_hid_mouse_report(

 REPORT_ID_MOUSE,

 0,

 0,

 0,

 0,

 0

 );

 }

 }

 static void update_button(button_state_t *b)

 {

 const int64_t t = now_ms();

 bool raw_pressed = button_raw_pressed(b->gpio);

 if (raw_pressed != b->last_raw_pressed) {

 b->last_raw_pressed = raw_pressed;

 b->last_change_ms = t;

 return;

 }

 if ((t - b->last_change_ms) < DEBOUNCE_MS) {

 return;

 }

 if (raw_pressed != b->stable_pressed) {

 b->stable_pressed = raw_pressed;

 if (b->stable_pressed) {

 b->press_start_ms = t;

 b->last_repeat_ms = t;

 b->long_mode = false;

 send_mouse_wheel(b->dir);

 } else {

 b->long_mode = false;

 }

 }

 if (!b->stable_pressed) {

 return;

 }

 if (!b->long_mode) {

 if ((t - b->press_start_ms) >= LONG_PRESS_MS) {

 b->long_mode = true;

 b->last_repeat_ms = t;

 send_mouse_wheel(b->dir);

 }

 return;

 }

 if ((t - b->last_repeat_ms) >= REPEAT_INTERVAL_MS) {

 b->last_repeat_ms = t;

 send_mouse_wheel(b->dir);

 }

 }

 static esp_err_t button_gpio_init(void)

 {

 gpio_config_t io_conf = {

 .pin_bit_mask =

 (1ULL << BTN_UP_GPIO) |

 (1ULL << BTN_DOWN_GPIO),

 .mode = GPIO_MODE_INPUT,

 .pull_up_en = GPIO_PULLUP_ENABLE,

 .pull_down_en = GPIO_PULLDOWN_DISABLE,

 .intr_type = GPIO_INTR_DISABLE,

 };

 return gpio_config(&io_conf);

 }

 uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)

 {

 (void) instance;

 return hid_report_descriptor;

 }

 uint16_t tud_hid_get_report_cb(

 uint8_t instance,

 uint8_t report_id,

 hid_report_type_t report_type,

 uint8_t *buffer,

 uint16_t reqlen

 ) {

 (void) instance;

 (void) report_id;

 (void) report_type;

 (void) buffer;

 (void) reqlen;

 return 0;

 }

 void tud_hid_set_report_cb(

 uint8_t instance,

 uint8_t report_id,

 hid_report_type_t report_type,

 uint8_t const *buffer,

 uint16_t bufsize

 ) {

 (void) instance;

 (void) report_id;

 (void) report_type;

 (void) buffer;

 (void) bufsize;

 }

 void tud_suspend_cb(bool remote_wakeup_en)

 {

 suspended = true;

 wakeup_host = remote_wakeup_en;

 ESP_LOGI(TAG, "USB suspended, remote_wakeup=%d", remote_wakeup_en);

 }

 void tud_resume_cb(void)

 {

 suspended = false;

 wakeup_host = false;

 ESP_LOGI(TAG, "USB resumed");

 }

 static esp_err_t tinyusb_hid_init(void)

 {

 tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();

 tusb_cfg.descriptor.device = NULL;

 tusb_cfg.descriptor.full_speed_config = hid_configuration_descriptor;

 tusb_cfg.descriptor.string = hid_string_descriptor;

 tusb_cfg.descriptor.string_count =

 sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]);

 #if (TUD_OPT_HIGH_SPEED)

 tusb_cfg.descriptor.high_speed_config = hid_configuration_descriptor;

 #endif

 return tinyusb_driver_install(&tusb_cfg);

 }

 void app_main(void)

 {

 ESP_LOGI(TAG, "smoothwheel start");

 ESP_ERROR_CHECK(button_gpio_init());

 button_state_t btn_up = {

 .gpio = BTN_UP_GPIO,

 .dir = SCROLL_UP,

 .stable_pressed = false,

 .last_raw_pressed = false,

 .last_change_ms = now_ms(),

 .long_mode = false,

 .press_start_ms = 0,

 .last_repeat_ms = 0,

 };

 button_state_t btn_down = {

 .gpio = BTN_DOWN_GPIO,

 .dir = SCROLL_DOWN,

 .stable_pressed = false,

 .last_raw_pressed = false,

 .last_change_ms = now_ms(),

 .long_mode = false,

 .press_start_ms = 0,

 .last_repeat_ms = 0,

 };

 ESP_ERROR_CHECK(tinyusb_hid_init());

 ESP_LOGI(TAG, "USB HID mouse wheel ready");

 ESP_LOGI(TAG, "UP gpio=%d, DOWN gpio=%d", BTN_UP_GPIO, BTN_DOWN_GPIO);

 while (1) {

 update_button(&btn_up);

 update_button(&btn_down);

 vTaskDelay(pdMS_TO_TICKS(2));

 }

 }

 修正したコードをビルドしてみる。

 % idf.py build

 こんどはエラーは出ないね。

 書き込む。リセットボタンを長押しして書き込みモードに入れる。うまくいったかな。

 % idf.py flash monitor

 USBデバイスとして認識していないな...

 コード側を修正。HID用のconfiguration descriptorをtusb_cfg.descriptor.full_speed_configに渡していなかった模様。

 ビルドして書き込む。

 認識した。

 赤が上、青が下。ちょっとヌルヌルとはいえ遅いかな。速度調整しているのはここ。

 #define REPEAT_INTERVAL_MS 120

 1回あたりのスクロール量はここ。

 #define WHEEL_STEP 1

 長押し開始までの待ち時間はここ。

 #define LONG_PRESS_MS 350

 REPEAT_INTERVAL_MSを少し小さくしてみるか。

 80にすると微妙に早い感じ。100あたりがいいかな。

 読みながらスクロールできる速度になった。

 しばらく使ってみるかな。

 使っていると、100だと微妙に遅い。90とか95かな。90だと微妙に読み流しできないな。


オリジナル投稿:
USB上下スクロールボタンをつくる|kinneko|pixivFANBOX
https://kinneko.fanbox.cc/posts/11886264