6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-02

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

こんなイメージです

image.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でキーボードショートカットを作ってしまおう を参照してください。

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)

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

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

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

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

これで、


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

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

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

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

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で行っています。
そのための変換テーブルを作っておきました。

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ソースコード

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

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

image.png

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

image.png

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

image.png

そこで、指定した【BLEパスキー】を入力します。

image.png

準備完了です。

Android等で表示するWebページ

こんな感じです。

image.png

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

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>
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

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

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

image.png

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

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

以上

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?