ESP32でキーボーショートカットを作ってしまおう：改良版

以前の投稿( [ESP32でキーボードショートカットを作ってしまおう](https://qiita.com/poruruba/items/eff3fedb1d4a63cbe08d) )で、ESP32をHID(キーボード)にして、Winキーを押しながらLキーといった装飾キーとの組み合わせのキーボードショートカット入力装置を作りました。
その時には、ESP32に、物理的なボタンを用意して、そのボタン押下を契機にキーボードショートカットを送っていました。
ですが、ESP32にはBLEだけでなくWiFiもついているので、Webから受け付けようかなあと思いました。そうすると、例えば、Androidの画面から、タッチすることで、キーボードショートカットを入力するといった応用ができそうです。

こんなイメージです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a9c1ff85-e1ee-db43-4fcd-33bc07b50983.png)

ESP32として、M5StickCを使っています。
まず事前にM5StickCをHIDとして、PCに認識させておきます。M5StickCには、HTTP Getでリクエストを受け付けるRestfulサーバを立ち上げておきます。
AndroidのブラウザでWebページを開き、ボタン押下をトリガに、M5StickCにHTTP Getでリクエストをし、M5StickCではそれを契機に、HIDのキー入力イベントをPCに伝えます。

もろもろのソースコードを以下に上げておきます。

poruruba/RemoteHid
　https://github.com/poruruba/RemoteHid


# M5StickCをPCにHIDとして認識させる

PCには、BLEで接続されるHIDとして認識させます。
そこらへんは、以前の記事 [ESP32でキーボードショートカットを作ってしまおう](https://qiita.com/poruruba/items/eff3fedb1d4a63cbe08d) を参照してください。

# M5StickCをRestfulサーバとして立ち上げる

Restfulサーバとし立ち上げるために、ArduinoライブラリのaREST を使いました。

marcoshwarts/aREST
　https://github.com/marcoschwartz/aREST

※aRESTをちょっと改造しています。
https://qiita.com/poruruba/items/34220a4dd6aaf48392aa#m5stick-c%E3%81%AE%E6%A7%8B%E6%88%90
改造しない場合は、以下をintで返すように変更してください。
```String procPutKey(String command)``` → ```int procPutKey(String command)```

こんな感じでエンドポイントを定義して、

```c:RemoteHid.ino
  rest.function("putkey", procPutKey);
```

こんな感じでそのコールバック関数を実装すればよいです。

```c:RemoteHid.ino
String procPutKey(String command) {
  Serial.println("procPutKey called");
  Serial.println(command);
・・・
```

これで、

<pre>
http://[M5StickCのIPアドレス]:[ポート番号]/putkey?param=[渡したい文字列]
</pre>

という具合にHTTP GETで呼び出せば、[渡したい文字列]の部分が、String commandとして受け取ることができます。

# HIDのキー入力イベントを発生させる

HTTP GET呼び出しを契機にHIDのキー入力イベントを発生するように実装します。
2つの指定方法を用意しました。


```c:RemoteHid.ino
void sendKeys(uint8_t mod, const uint8_t *keys, uint8_t num_key = 1){
  BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
  if( !desc->getNotifications() )
    return;

  uint8_t msg[] = {mod, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  for( int i = 0 ; i < num_key && i < 6 ; i++ ){
    msg[2 + i] = keys[i];
  }

  input->setValue(msg, sizeof(msg));
  debug_dump(msg, sizeof(msg));
  input->notify();

  uint8_t msg1[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  input->setValue(msg1, sizeof(msg1));
  debug_dump(msg1, sizeof(msg1));
  input->notify();
    
  delay(20);
}

void sendKeyString(const char* ptr){
  BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
  if( !desc->getNotifications() )
    return;
      
  // 1文字ずつHID(BLE)で送信
  while(*ptr){
    if( *ptr >= ASCIIMAP_SIZE_JP ){
      ptr++;
      continue;
    }
    KEYMAP map = asciimap_jp[(uint8_t)*ptr];
    uint8_t msg[] = {map.modifier, 0x00, map.usage, 0x00, 0x00, 0x00, 0x00, 0x00};
    input->setValue(msg, sizeof(msg));
    debug_dump(msg, sizeof(msg));
    input->notify();
    ptr++;
    
    uint8_t msg1[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    input->setValue(msg1, sizeof(msg1));
    debug_dump(msg1, sizeof(msg1));
    input->notify();
    
    delay(20);
  }
}
```

* void sendKeys(uint8_t mod, const uint8_t *keys, uint8_t num_key = 1)

modが装飾キーで、keysが入力するキーIDです。
装飾キーには、Ctrlキー、Shiftキー、Altキー、Winキーがあり、ORで指定します。
keysはASCIIコードではないので気を付けてください。キーIDは、USB HID Usageで定義されています。以下を参考にさせていただきました。

Japanese Keyboard (layout and scancode)
　http://hp.vector.co.jp/authors/VA003720/lpproj/others/kbdjpn.htm

キーIDは最大6個を指定できます。
例えばこれで、Ctrl＋Winキーを押しながら右矢印を押す、といった指定ができます。
キーIDの値への変換は、ブラウザ上のJavascriptで変換するようにしています。

* void sendKeyString(const char* ptr)

ASCIIの文字列を指定して対応するキーを文字列の長さ分連続して押下するようにふるまいます。
ASCII文字コードから、USB HID UsageのキーIDへの変換は、M5StickCで行っています。
そのための変換テーブルを作っておきました。

```c:asciimap.c
const KEYMAP asciimap_jp[ASCIIMAP_SIZE_JP] = {
・・・
    {0x04, KEY_SHIFT},      /* A */
    {0x05, KEY_SHIFT},      /* B */
    {0x06, KEY_SHIFT},      /* C */
    {0x07, KEY_SHIFT},      /* D */
    {0x08, KEY_SHIFT},      /* E */
・・・
}
```

の部分です。
とはいいつつも、以下を参照させていただきました。

T-vK/ESP32-BLE-Keyboard
　https://github.com/T-vK/ESP32-BLE-Keyboard/blob/master/BleKeyboard.cpp

#Arduinoソースコード

ということで、全部繋げるとこんな感じになります。

```c:RemoteHid.ino
#include <Arduino.h>

#define M5STICKC

#ifdef M5STICKC
#include <M5StickC.h>
#endif
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include <BLEHIDDevice.h>
#include <HIDTypes.h>
#include <WiFi.h>
#include <WiFiServer.h>
#include <ArduinoJson.h>
#include <aREST.h>
#include <base64.hpp>
#include "asciimap.h"

// BLEのパスキー
#define BLE_PASSKEY 【BLEパスキー】

const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

const char* deviceName = "【デバイス名】";
const char* manufacturerName = "RemoteHid";


// GET接続を待ち受けるポート番号
#define REST_PORT     80

#define MAX_MACROS 4

const int message_capacity = JSON_OBJECT_SIZE(1) + JSON_ARRAY_SIZE(MAX_MACROS) + MAX_MACROS * JSON_OBJECT_SIZE(3);
StaticJsonDocument<message_capacity> json_message;
char message_buffer[1024];
char base64_buffer[1024];

WiFiServer server(REST_PORT);
aREST rest = aREST();

void sendKeys(uint8_t mod, const uint8_t *keys, uint8_t num_key);
void sendKeyString(const char* ptr);

void debug_dump(const uint8_t *p_bin, uint16_t len){
  for( uint16_t i = 0 ; i < len ; i++ ){
    Serial.print((p_bin[i] >> 4) & 0x0f, HEX);
    Serial.print(p_bin[i] & 0x0f, HEX);
  }
  Serial.println("");
}

unsigned char tohex(char c){
  if( c >= '0' && c <= '9')
    return c - '0';
  if( c >= 'a' && c <= 'f' )
    return c - 'a' + 10;
  if( c >= 'F' && c <= 'F' )
    return c - 'A' + 10;

  return 0;
}

long parse_hex(const char* p_hex, unsigned char *p_bin){
  int index = 0;
  while( p_hex[index * 2] != '\0'){
    p_bin[index] = tohex(p_hex[index * 2]) << 4;
    p_bin[index] |= tohex(p_hex[index * 2 + 1]);
    index++;
  }

  return index;
}

String procPutKey(String command) {
  Serial.println("procPutKey called");
  Serial.println(command);
  
  decode_base64((unsigned char*)command.c_str(), (unsigned char*)base64_buffer);
  DeserializationError err = deserializeJson(json_message, base64_buffer);
  if( err ){
    Serial.print("Deserialize error: ");
    Serial.println(err.c_str());
    return "Deserialize Error";
  }

  JsonArray arry = json_message["keys"];
  for( int i = 0 ; i < arry.size(); i++ ){
    const char* type = arry[i]["type"];
    if( strcmp(type, "code") == 0 ){
      const char* code = arry[i]["code"];
      int mod = arry[i]["mod"];
      Serial.print("code=");
      Serial.println(code);
      Serial.print("mod=");
      Serial.println(mod);

      unsigned char code_bin[6]; 
      if( strlen(code) > sizeof(code_bin) * 2 ){
        Serial.println("Code size Error");
        return "Code size Error";
      }

      int code_len = parse_hex(code, code_bin);
      sendKeys(mod, code_bin, code_len);
    }else if( strcmp(type, "text") == 0 ){
      const char* text = arry[i]["text"];
      Serial.print("text=");
      Serial.println(text);

      sendKeyString(text);
    }
  }

  return "OK";
}

/*
 * BLEデバイス処理
 */
BLEHIDDevice* hid;
BLECharacteristic* input;
BLECharacteristic* output;

bool connected = false;

class MyCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer){
    connected = true;
    BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
    desc->setNotifications(true);
  }

  void onDisconnect(BLEServer* pServer){
    connected = false;
    BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
    desc->setNotifications(false);
  }
};

// ペアリング処理用
class MySecurity : public BLESecurityCallbacks {
  bool onConfirmPIN(uint32_t pin){
    return false;
  }

  uint32_t onPassKeyRequest(){
    Serial.println("ONPassKeyRequest");
    return BLE_PASSKEY;
  }

  void onPassKeyNotify(uint32_t pass_key){
    // ペアリング時のPINの表示
    Serial.println("onPassKeyNotify number");
    Serial.println(pass_key);
  }

  bool onSecurityRequest(){
    Serial.println("onSecurityRequest");
    return true;
  }

  void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl){
    Serial.println("onAuthenticationComplete");
    if(cmpl.success){
      // ペアリング完了
      Serial.println("auth success");
    }else{
      // ペアリング失敗
      Serial.println("auth failed");
    }
  }
};

// BLEデバイスの起動
void taskServer(void*){
  BLEDevice::init(deviceName);

  BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_MITM);
  BLEDevice::setSecurityCallbacks(new MySecurity());  

  BLEServer *pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyCallbacks());

  hid = new BLEHIDDevice(pServer);
  input = hid->inputReport(1); // <-- input REPORTID from report map
  output = hid->outputReport(1); // <-- output REPORTID from report map

  hid->manufacturer()->setValue(manufacturerName);

  hid->pnp(0x02, 0xe502, 0xa111, 0x0210);
  hid->hidInfo(0x00,0x02);

  BLESecurity *pSecurity = new BLESecurity();
  pSecurity->setKeySize(16);

  pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
  pSecurity->setCapability(ESP_IO_CAP_OUT);
  pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);

  uint32_t passkey = BLE_PASSKEY;
  esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t));

    const uint8_t report[] = {
      USAGE_PAGE(1),      0x01,       // Generic Desktop Ctrls
      USAGE(1),           0x06,       // Keyboard
      COLLECTION(1),      0x01,       // Application
      REPORT_ID(1),       0x01,        //   Report ID (1)
      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,
      HIDINPUT(1),           0x02,       //   Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position
      REPORT_COUNT(1),    0x01,       //   1 byte (Reserved)
      REPORT_SIZE(1),     0x08,
      HIDINPUT(1),           0x01,       //   Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
      REPORT_COUNT(1),    0x06,       //   6 bytes (Keys)
      REPORT_SIZE(1),     0x08,
      LOGICAL_MINIMUM(1), 0x00,
      LOGICAL_MAXIMUM(1), 0x65,       //   101 keys
      USAGE_MINIMUM(1),   0x00,
      USAGE_MAXIMUM(1),   0x65,
      HIDINPUT(1),           0x00,       //   Data,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
      HIDOUTPUT(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,
      HIDOUTPUT(1),          0x01,       //   Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
      END_COLLECTION(0)
    };

  hid->reportMap((uint8_t*)report, sizeof(report));
  hid->startServices();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->setAppearance(HID_KEYBOARD);
  pAdvertising->addServiceUUID(hid->hidService()->getUUID());
  pAdvertising->start();
  hid->setBatteryLevel(7);

//  Serial.println("Advertising started!");
  delay(portMAX_DELAY);
};

void sendKeys(uint8_t mod, const uint8_t *keys, uint8_t num_key = 1){
  BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
  if( !desc->getNotifications() )
    return;

  uint8_t msg[] = {mod, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  for( int i = 0 ; i < num_key && i < 6 ; i++ ){
    msg[2 + i] = keys[i];
  }

  input->setValue(msg, sizeof(msg));
  debug_dump(msg, sizeof(msg));
  input->notify();

  uint8_t msg1[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
  input->setValue(msg1, sizeof(msg1));
  debug_dump(msg1, sizeof(msg1));
  input->notify();
    
  delay(20);
}

void sendKeyString(const char* ptr){
  BLE2902* desc = (BLE2902*)input->getDescriptorByUUID(BLEUUID((uint16_t)0x2902));
  if( !desc->getNotifications() )
    return;
      
  // 1文字ずつHID(BLE)で送信
  while(*ptr){
    if( *ptr >= ASCIIMAP_SIZE_JP ){
      ptr++;
      continue;
    }
    KEYMAP map = asciimap_jp[(uint8_t)*ptr];
    uint8_t msg[] = {map.modifier, 0x00, map.usage, 0x00, 0x00, 0x00, 0x00, 0x00};
    input->setValue(msg, sizeof(msg));
    debug_dump(msg, sizeof(msg));
    input->notify();
    ptr++;
    
    uint8_t msg1[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
    input->setValue(msg1, sizeof(msg1));
    debug_dump(msg1, sizeof(msg1));
    input->notify();
    
    delay(20);
  }
}

void wifi_connect(void){
  Serial.print("WiFi Connenting");
  WiFi.begin(wifi_ssid, wifi_password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected : ");
  Serial.println(WiFi.localIP());
#ifdef M5STICKC
  M5.Lcd.println(WiFi.localIP());
#endif
}

void setup() {
#ifdef M5STICKC
  M5.begin();
  M5.IMU.Init();

  M5.Axp.ScreenBreath(9);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextSize(1);

  M5.Lcd.printf("[%s]\n", deviceName);
  M5.Lcd.println(" A:Switch Desktop");
  M5.Lcd.println(" B:Save Desktop");
  delay(500);
#endif
    
  Serial.begin(9600);
  Serial.printf("Starting %s!\n", deviceName);

  // BLEデバイスの起動処理の開始
  xTaskCreate(taskServer, "server", 20000, NULL, 5, NULL);

  // GETエンドポイントの定義
  rest.function("putkey", procPutKey);
  
  // Give name & ID to the device (ID should be 6 characters long)
  rest.set_id("0001");
  rest.set_name("esp32");
  
  // Webサーバ起動
  wifi_connect();
  server.begin();
  Serial.println("Server started");
}

void loop() {
  WiFiClient client = server.available();
  if (client) {
    // GET呼び出しを検知
    for( int i = 0 ; i < 10000; i += 10 ){
      if(client.available()){
        // GET呼び出しのコールバック呼び出し
        rest.handle(client);
        return;
      }
      delay(10);
    }
    // まれにGET呼び出し受付に失敗するようです。
    Serial.println("timeout");
  }

#ifdef M5STICKC
  M5.update();

  if( M5.BtnA.wasReleased() ){
    if(connected){
        Serial.println("BtnA released");
        // Switch Desktop
        uint8_t keys[] = { 0x07 /* D */};
        sendKeys(KEY_MASK_WIN, keys, 1);

        delay(10);
    }
  }
  if( M5.BtnB.wasReleased() ){
    if(connected){
        Serial.println("BtnB released");
        // Save Desktop
        uint8_t keys[] = { 0x46 /* PrintScreen */};
        sendKeys(KEY_MASK_WIN, keys, 1);

        delay(10);
    }
  }
#endif
}
```

環境に合わせて変更が必要です。

【BLEパスキー】：M5StickCをHIDとしてペアリングするときに入力する数字です。他人に使われないよう秘匿の値にしてください。
【WiFiアクセスポイントのSSID】：M5StickCが接続するWiFiアクセスポイントのSSIDです。
【WiFiアクセスポイントのパスワード】：M5StickCが接続するWiFiアクセスポイントのパスワードです。
【デバイス名】：好きな名前を指定してください。

M5StickC等のESP32に書き込む際には、WiFiとBLE両方使うのでプログラムサイズが大きくあふれてしまうため、Partition SchemaとしてNo OTAを選択してください。

# PCにHIDとして登録してみる

M5StickCに書き込んで起動させます。
その後、設定から、Bluetoothデバイスとして追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/189ce8e0-929b-9dca-dfcb-f4944fa90ad1.png)
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/deec1086-8221-f16a-2fc4-54ccaf4ea5c1.png)

Bluetoothを選択します。
そうすると、指定した【デバイス名】のデバイスが見つかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5260046b-31cf-dd50-8454-e8ecc4c6622f.png)

それを選択すると、PIN入力を求められます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f5b9713f-17b6-d601-1deb-e2cea96d4173.png)
 
そこで、指定した【BLEパスキー】を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/98bc575c-d6e1-c66a-b87c-0b8b7133241b.png)

準備完了です。


# Android等で表示するWebページ

こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7b49ecff-b987-150a-d186-d0eee3bb5408.png)

HTMLとJavascriptを示しておきます。
これらを適当なWebページに配置します。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <title>RemoteHid</title>

  <link rel="stylesheet" href="css/start.css">
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="dist/js/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>RemoteHid</h1>

        <ul class="nav nav-tabs">
          <li role="presentation" class="active"><a href="#sending" data-toggle="tab">送信</a></li>
          <li role="presentation"><a href="#editing" data-toggle="tab">編集</a></li>
          <li role="presentation"><a href="#destination" data-toggle="tab">接続先</a></li>
        </ul>
        <br>

        <div class="tab-content">

          <div id="destination" class="tab-pane fade in">
            <label>remote_url</label>
            <input type="text" class="form-control" v-model="remote_url">
          </div>

          <div id="sending" class="tab-pane fade in active">
            <span v-for="(macro, index) in macro_list">
              <button class="btn btn-primary btn-lg" v-on:click="do_send(index)">{{macro.title}}</button>&nbsp;
            </span>
          </div>

          <div id="editing" class="tab-pane fade in">
            <div class="form-inline">
              macro:
              <select class="form-control" v-model.number="editing_macro_index" v-on:change="select_macro">
                <option disabled value="-2">選択してください</option>
                <option value="-1">新規作成</option>
                <option v-for="(macro, index) in macro_list" v-bind:value="index">{{macro.title}}</option>
              </select>
            </div>
            <br>

            <div v-if="editing_macro">
              <span class="form-inline">
                title:<input type="text" class="form-control" v-model="editing_macro.title">
              </span>
              <ol>
                <li v-for="(value, index) in editing_macro.keys">
                  <label>{{value.type}}</label>
                  <span class="form-inline" v-if="value.type=='code'">
                    mod:
                    <select class="form-control" v-model.number="value.mod" multiple>
                      <option v-for="(value1, index1) in mod_list" v-bind:value="index1">{{value1}}</option>
                    </select>
                    code:
                    <select v-for="(value1, index1) in value.code" class="form-control" v-model="value.code[index1]">
                      <option v-for="(value2, index2) in keyid_list" v-bind:value="value2.keyid">{{value2.disp}}</option>
                    </select>
                  </span>
                  <span class="form-inline" v-else>
                    <input type="text" class="form-control" v-model="value.text">                    
                  </span>
                  <button class="btn btn-default btn-sm" v-on:click="delete_key(index)">削除</button>
                </li>
                <li><button class="btn btn-default btn-sm" v-on:click="append_key">追加</button></li>
              </ol>
              <div v-if="editing_macro_index>=0">
                <button class="btn btn-primary" v-on:click="update_key">Update</button>
                <button class="btn btn-primary" v-on:click="delete_macro(editing_macro_index)">Delete</button>
              </div>
              <div v-else>
                <button class="btn btn-primary" v-on:click="update_key">Create</button>
              </div>
            </div>

          </div>
        </div>


        <modal-dialog id="append_select_dialog">
          <template v-slot:content="props">
            <div class="modal-body">
              <span class="form-inline">
                <label>type</label>
                <select class="form-control" v-model="dialog_content.type">
                  <option value="code">code</option>
                  <option value="text">text</option>
                </select>
              </span>
            </div>
            <div class="modal-footer">
              <button class="btn btn-default" v-on:click="dialog_custom_close(0)">追加</button>
              <button class="btn btn-default" v-on:click="dialog_custom_close(-1)">キャンセル</button>
            </div>
          </template>
        </modal-dialog>       


        <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/transform.js"></script>

    <script src="js/start.js"></script>
</body>
```

```js:start.js
'use strict';

//var vConsole = new VConsole();

const remote_url = '【M5StickCのURL】';
const COOKIE_EXPIRE = 365;

var macro_list = [
    {
        title: "デスクトップ切替(←)",
        keys : [
            {
                "type": "code",
                "code": [0x50, 0x00, 0x00, 0x00, 0x00, 0x00],
                "mod": [0, 3]
            },
        ]
    },
    {
        title: "デスクトップ切替(→)",
        keys : [
            {
                "type": "code",
                "code": [0x4f, 0x00, 0x00, 0x00, 0x00, 0x00],
                "mod": [0, 3]
            },
        ]
    },
    {
        title: "Hello World",
        keys : [
            {
                "type": "text",
                "text": "Hello World"
            }
        ]
    }
];

var vue_options = {
    el: "#top",
    data: {
        progress_title: '', // for progress-dialog

        remote_url: remote_url,
        macro_list: macro_list,
        editing_macro_index: -2,
        editing_macro: null,
        newing_macro: null,
        dialog_content: {},
        mod_list: mod_list,
        keyid_list: keyid_list,
    },
    computed: {
    },
    methods: {
        select_macro: function(){
            if( this.editing_macro_index >= 0 ){
                this.editing_macro = JSON.parse(JSON.stringify(this.macro_list[this.editing_macro_index]));
            }else if( this.editing_macro_index == -1 ){
                this.editing_macro = {
                    title: "No title",
                    keys: []
                };
            }else{
                this.editing_macro = null;
            }
        },
        append_key: function(){
            this.dialog_open("#append_select_dialog");
        },
        dialog_custom_close: function(result){
            this.dialog_close("#append_select_dialog");
            if( result != 0 )
                return;
            if( this.dialog_content.type == "text")
                this.editing_macro.keys.push({ type: "text", text: "" });
            else if( this.dialog_content.type == "code")
                this.editing_macro.keys.push({ type: "code", code: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00], mod: [] });
            this.save_cookie();
        },
        delete_key: function(index){
            if( !confirm("本当に削除しますか？") )
                return;

            Vue.delete(this.editing_macro.keys, index);
            this.save_cookie();
        },
        update_key: function(){
            if( this.editing_macro_index >= 0 )
                this.macro_list[this.editing_macro_index] = this.editing_macro;
            else
                this.macro_list.push(this.editing_macro);
            this.editing_macro = null;
            this.editing_macro_index = -2;
            this.save_cookie();
            alert("追加/更新しました。");
        },
        delete_macro: function(index){
            if( !confirm("本当に削除しますか？") )
                return;

            this.editing_macro = null;
            this.editing_macro_index = -2;
            Vue.delete(this.macro_list, index);
            this.save_cookie();
        },
        do_send: function(index){
            var params = {
                keys: []
            };
            var macro = this.macro_list[index];
            for( var i = 0 ; i < macro.keys.length ; i++ ){
                if( macro.keys[i].type == 'code'){
                    var key_code = macro.keys[i];
                    var key = {
                        type: "code",
                        mod: 0,
                    };
                    for( var j = 0 ; j < key_code.mod.length ; j++ )
                        key.mod |= (0x01 << key_code.mod[j]);
                    var code_list = [];
                    for( var j = 0 ; j < key_code.code.length ; j++ ){
                        if( key_code.code[j] != 0x00 )
                            code_list.push(key_code.code[j])
                    }
                    key.code = bufferToHex(code_list);
                    params.keys.push(key);
                }else if( macro.keys[i].type == 'text'){
                    params.keys.push(macro.keys[i]);
                }
            }
            console.log(params);
            do_get(this.remote_url + '/putkey', { params: btoa(JSON.stringify(params)) } );
        },
        save_cookie: function(){
            Cookies.set('macros', JSON.stringify(this.macro_list), { expires: COOKIE_EXPIRE });
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        var macros = Cookies.get('macros');
        if( macros !== undefined )
            this.macro_list = JSON.parse(macros);
    }
};
vue_add_methods(vue_options, methods_bootstrap);
vue_add_components(vue_options, components_bootstrap);
var vue = new Vue( vue_options );

function do_get(url, qs) {
    var params = new URLSearchParams(qs);
    var url2 = new URL(url);
    url2.search = params;
  
    return fetch(url2.toString(), {
        method: 'GET',
      })
      .then((response) => {
        if (!response.ok)
          throw 'status is not 200';
        return response.json();
      });
}

function bufferToHex(buffer) {
    return Array
        .from (new Uint8Array (buffer))
        .map (b => b.toString (16).padStart (2, "0"))
        .join ("");
}
```

いくつかユーティリティを作って使っていますが、詳細はGitHubを参照してください。

環境に合わせて以下の修正が必要です。
　【M5StickCのURL】：M5StickCのURL

たとえば、M5StickCのIPアドレスが192.168.0.1の場合は、 http://192.168.0.1 とします。

本Webページは、以下のURLからもアクセスできるようにしておきました。
　https://poruruba.github.io/RemoteHid/www/public

それでは、HID登録したPCとは別のPCたとえばAndroidからWebページを開きます。

ボタンが3つありますが、あらかじめ、3つのサンプルマクロを用意しておきました。
Ctrl＋Win＋→ とCtrl＋Win＋← によるデスクトップ切り替えと、「Hello World」と文字列入力するマクロです。

先に、接続先タブから、M5StickCのURLが正しいか確認しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/68377b4b-b528-6c6f-fa92-0aa9bedbcca4.png)

それでは、送信タブに戻って、それぞれのボタンを押すと、デスクトップが切り替わったり、メモ帳を開いていた場合はHello Worldと入力されましたでしょうか？

それらのマクロを変更したい場合は、同じページの編集タブをクリックしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/44372c42-49b0-5846-baba-5ecec1f40140.png)

新規にマクロを追加したり編集したりできます。
編集した値は、ブラウザのCookieに保存するようにしました。

※このページにアクセスできる人は、HIDで自由に入力できるので、ちゃんと使うには、ログイン認証したり、HTTPSで守るようにしてください。

以上