以前の投稿( ESP32でキーボードショートカットを作ってしまおう )で、ESP32をHID(キーボード)にして、Winキーを押しながらLキーといった装飾キーとの組み合わせのキーボードショートカット入力装置を作りました。
その時には、ESP32に、物理的なボタンを用意して、そのボタン押下を契機にキーボードショートカットを送っていました。
ですが、ESP32にはBLEだけでなくWiFiもついているので、Webから受け付けようかなあと思いました。そうすると、例えば、Androidの画面から、タッチすることで、キーボードショートカットを入力するといった応用ができそうです。
こんなイメージです
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)
こんな感じでエンドポイントを定義して、
rest.function("putkey", procPutKey);
こんな感じでそのコールバック関数を実装すればよいです。
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つの指定方法を用意しました。
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で行っています。
そのための変換テーブルを作っておきました。
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ソースコード
ということで、全部繋げるとこんな感じになります。
#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デバイスとして追加します。
Bluetoothを選択します。
そうすると、指定した【デバイス名】のデバイスが見つかります。
それを選択すると、PIN入力を求められます。
そこで、指定した【BLEパスキー】を入力します。
準備完了です。
Android等で表示するWebページ
こんな感じです。
HTMLとJavascriptを示しておきます。
これらを適当なWebページに配置します。
<!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>
</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>
'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が正しいか確認しておきます。
それでは、送信タブに戻って、それぞれのボタンを押すと、デスクトップが切り替わったり、メモ帳を開いていた場合はHello Worldと入力されましたでしょうか?
それらのマクロを変更したい場合は、同じページの編集タブをクリックしてください。
新規にマクロを追加したり編集したりできます。
編集した値は、ブラウザのCookieに保存するようにしました。
※このページにアクセスできる人は、HIDで自由に入力できるので、ちゃんと使うには、ログイン認証したり、HTTPSで守るようにしてください。
以上