複数 Mac への接続方法を変更したので更新しました。
- 切り替えにはBluetooth のアドレスを変更するようにしました
- Cmd-1, 2, 3, 4 で4台まで接続を切り替えれるようにしました
- だいぶ安定してるような気がします
最近在宅で仕事をすることが多いので、入力環境の改善を図ろうとキーボードをいろいろ検討していました。
結局 Realforce TKL SA for Mac に落ち着いたのですが、このキーボードは USB接続のため複数の Mac で簡単に切り替えて使えません。
USB2BT っていうのが売ってるのですが、結構なお値段がするので、自分で作ってみることにしました。
準備するもの
ハードウェア
リンク先はスイッチサイエンスです。
ソフトウェア
- Arduino 1.8.15
- esp32 1.0.4
- M5Stack Library 0.3.1
- USB Host Shield Library 2.0 1.4.0
- NimBLE-Arduino 1.2.0
使っている環境
- MacBook Pro (13-inch, Mid 2012) macOS Catalina Version 10.15.7
- MacBook Air (13-inch, Early 2015) macOS Big Sur Version 11.4
- Realforce TKL SA for Mac R2TLSA-JP3M-WH
使い方
- Advertising 1-4 の表示がでている時に Mac から接続します。
- Setting の Keyboard から接続できない時は Bluetooth の方から Connect するとうまくいくようです。なぜかはよくわかってないです。
- F14とF15で1〜2台目の Mac に切り替えます。
- Cmd-1〜4 で1〜4台目の Mac に切り替えます。
- Logicool の Flow に擬似的に対応していて、Flow を Ctrl を押した場合のみ移動する設定にしておけば、Option + Ctrl を押しながら移動させるとキーボードも切り替えます。Option + Ctrl を500ms以上長押ししていると1台目と2台目の Mac を切り替えるだけですが、結構使えます。
- 起動直後や Sleep からの復帰後につながらない時は M5Stack の電源を入れ直せば繋がるようです。
- 現在の私の使用方法では4台の切り替えで十分なのですが、必要であれば5台以上でも大丈夫だと思います。
- Realforce じゃなくても使えると思いますが、Media Key あたりは少し書き換えないといけないと思います。
- Windows にも接続できますが、キーボードの種類に応じていろいろ変えないといけないかもしれません。
USB Host Shield 2.0 の変更
最新ではどうかわかりませんが、少し変更してます。前の記事に変更点を書いています。
ソースコード
Arduinoのソースコードです。
M5Stack_Realforce_BLE2.ino
/*
MIT License
Copyright (c) 2021 Satoru Sato
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.
*/
# include <M5Stack.h>
# include <hiduniversal.h>
# include <NimBLEDevice.h>
# include "HIDKeyboardTypes.h"
# include "HIDTypes.h"
# include "Free_Fonts.h"
# include <EEPROM.h>
# define REALFORCE_VID 0x0853
# define REALFORCE_PID 0x0105
# define HID_KEYBOARD 0x03C1
// Report ID
# define KEYBOARD_ID 0x01
# define MEDIA_KEYS_ID 0x03
static const uint8_t reportMap[] = {
USAGE_PAGE(1), 0x01, // USAGE_PAGE (Generic Desktop)
USAGE(1), 0x06, // USAGE (Keyboard)
COLLECTION(1), 0x01, // COLLECTION (Application) Start Keyboard Collection
// ------------------------------------------------- Keyboard
USAGE_PAGE(1), 0x07, // USAGE_PAGE (Kbrd/Keypad)
REPORT_ID(1), KEYBOARD_ID, // REPORT_ID (1) Report ID = 1 (Keyboard)
USAGE_MINIMUM(1), 0xE0, // USAGE_MINIMUM (0xE0)
USAGE_MAXIMUM(1), 0xE7, // USAGE_MAXIMUM (0xE7)
LOGICAL_MINIMUM(1), 0x00, // LOGICAL_MINIMUM (0)
LOGICAL_MAXIMUM(1), 0x01, // LOGICAL_MAXIMUM (1)
// -------------------------------------------------
REPORT_SIZE(1), 0x01, // REPORT_SIZE (1) ; 1 byte (Modifier?)
REPORT_COUNT(1), 0x08, // REPORT_COUNT (8) ; 8 bits
HIDINPUT(1), 0x02, // INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// -------------------------------------------------
REPORT_COUNT(1), 0x01, // REPORT_COUNT (1) ; 1 byte (Reserved)
REPORT_SIZE(1), 0x08, // REPORT_SIZE (8) ; 8 bits
HIDINPUT(1), 0x01, // INPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
// -------------------------------------------------
REPORT_COUNT(1), 0x05, // REPORT_COUNT (5) ; 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
REPORT_SIZE(1), 0x01, // REPORT_SIZE (1)
USAGE_PAGE(1), 0x08, // USAGE_PAGE (LEDs)
REPORT_ID(1), KEYBOARD_ID, // REPORT_ID (1) Report ID = 1 (Keyboard)
USAGE_MINIMUM(1), 0x01, // USAGE_MINIMUM (0x01) ; Num Lock
USAGE_MAXIMUM(1), 0x05, // USAGE_MAXIMUM (0x05) ; Kana
HIDOUTPUT(1), 0x02, // OUTPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
// -------------------------------------------------
REPORT_COUNT(1), 0x01, // REPORT_COUNT (1) ; 3 bits (Padding)
REPORT_SIZE(1), 0x03, // REPORT_SIZE (3)
HIDOUTPUT(1), 0x03, // OUTPUT (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
// -------------------------------------------------
REPORT_COUNT(1), 0x06, // REPORT_COUNT (6) ; 6 bytes (Keys)
REPORT_SIZE(1), 0x08, // REPORT_SIZE(8)
LOGICAL_MINIMUM(1), 0x00, // LOGICAL_MINIMUM(0)
LOGICAL_MAXIMUM(1), 0x65, // LOGICAL_MAXIMUM(0x65) ; 101 keys
USAGE_PAGE(1), 0x07, // USAGE_PAGE (Kbrd/Keypad)
USAGE_MINIMUM(1), 0x00, // USAGE_MINIMUM (0)
USAGE_MAXIMUM(1), 0x65, // USAGE_MAXIMUM (0x65)
HIDINPUT(1), 0x00, // INPUT (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
// -------------------------------------------------
END_COLLECTION(0), // END_COLLECTION
// ------------------------------------------------- Media Keys
USAGE_PAGE(1), 0x0C, // USAGE_PAGE (Consumer)
USAGE(1), 0x01, // USAGE (Consumer Control)
COLLECTION(1), 0x01, // COLLECTION (Application)
REPORT_ID(1), MEDIA_KEYS_ID, // REPORT_ID (3)
USAGE_PAGE(1), 0x0C, // USAGE_PAGE (Consumer)
LOGICAL_MINIMUM(1), 0x00, // LOGICAL_MINIMUM (0)
LOGICAL_MAXIMUM(1), 0x01, // LOGICAL_MAXIMUM (1)
REPORT_SIZE(1), 0x01, // REPORT_SIZE (1) ; 2 bytes
REPORT_COUNT(1), 0x10, // REPORT_COUNT (16)
USAGE(1), 0xB5, // USAGE (Scan Next Track) ; bit 0: 1
USAGE(1), 0xB6, // USAGE (Scan Previous Track) ; bit 1: 2
USAGE(1), 0xB7, // USAGE (Stop) ; bit 2: 4
USAGE(1), 0xCD, // USAGE (Play/Pause) ; bit 3: 8
USAGE(1), 0xE2, // USAGE (Mute) ; bit 4: 16
USAGE(1), 0xE9, // USAGE (Volume Increment) ; bit 5: 32
USAGE(1), 0xEA, // USAGE (Volume Decrement) ; bit 6: 64
USAGE(1), 0xB8, // Usage (Eject) ; bit 7: 128
USAGE(1), 0x70, // Usage (Brightness Down) ; bit 0: 1
USAGE(1), 0x6F, // Usage (Brightness Up) ; bit 1: 2
HIDINPUT(1), 0x02, // INPUT (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// -------------------------------------------------
END_COLLECTION(0) // END_COLLECTION
};
static int target_connection = 0;
static uint16_t target_handle;
static boolean connection_established = false;
static char bluetooth_name[4][11] = {
"USBKBD2BT1",
"USBKBD2BT2",
"USBKBD2BT3",
"USBKBD2BT4"
};
static char message[4][13] = {
"Connection 1",
"Connection 2",
"Connection 3",
"Connection 4"
};
static char message2[4][14] = {
"Advertising 1",
"Advertising 2",
"Advertising 3",
"Advertising 4"
};
static uint8_t org_mac[6] = {0};
static NimBLEServer* pServer;
static NimBLECharacteristic* pReport1;
static NimBLECharacteristic* pReport2;
static NimBLECharacteristic* pReport3;
void disp(void) {
static int prev_connection_state = -1;
static int prev_target_connection = -1;
if (prev_connection_state != connection_established ||
prev_target_connection != target_connection) {
M5.Lcd.setTextColor(LIGHTGREY);
M5.Lcd.setTextDatum(CC_DATUM);
M5.Lcd.setFreeFont(FF30);
M5.Lcd.setRotation(3);
M5.Lcd.fillRect(0, 90, 320, 60, BLACK);
prev_connection_state = connection_established;
prev_target_connection = target_connection;
if (connection_established) {
M5.Lcd.drawString(message[target_connection], 160, 120, GFXFF); // for rotation 1 and 3
}
else {
M5.Lcd.drawString(message2[target_connection], 160, 120, GFXFF); // for rotation 1 and 3
}
}
}
class ServerCallbacks: public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) {
NimBLEDevice::stopAdvertising();
};
void onConnect(NimBLEServer* pServer, ble_gap_conn_desc* desc) {
target_handle = desc->conn_handle;
connection_established = true;
pServer->updateConnParams(desc->conn_handle, 0x10, 0x20, 0, 600);
};
void onDisconnect(NimBLEServer* pServer) {
if (!connection_established) {
NimBLEDevice::startAdvertising();
}
connection_established = false;
};
void onAuthenticationComplete(ble_gap_conn_desc* desc) {
};
};
class CharacteristicCallbacks: public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pCharacteristic) {
};
};
static CharacteristicCallbacks chrCallbacks;
void bluetooth_deinit(void) {
NimBLEDevice::deinit(true);
}
void bluetooth_init(int connection) {
EEPROM.put(0, connection);
EEPROM.commit();
uint8_t new_mac[6];
for (int i = 0; i < 6; i++) {
new_mac[i] = org_mac[i];
}
new_mac[5] += (4 * target_connection);
esp_base_mac_addr_set(new_mac);
NimBLEDevice::init(bluetooth_name[target_connection]);
NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND);
pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
NimBLEService* pDeviceInfoService = pServer->createService("180A");
// DeviceInfo Service - pnp
NimBLECharacteristic* pPnpCharacteristic = pDeviceInfoService->createCharacteristic("2A50",
NIMBLE_PROPERTY::READ);
uint8_t sig = 0x02;
uint16_t vid = 0x5308; // 東プレ
uint16_t pid = 0x5101; // RealForce for Mac
uint16_t version = 0x0001;
uint8_t pnp[] = {sig, (uint8_t) (vid >> 8), (uint8_t) vid, (uint8_t) (pid >> 8), (uint8_t) pid, (uint8_t) (version >> 8), (uint8_t) version };
pPnpCharacteristic->setValue(pnp, sizeof(pnp));
// DeviceInfo Service - Manufacturer
NimBLECharacteristic* pManufacturerCharacteristic = pDeviceInfoService->createCharacteristic("2A29",
NIMBLE_PROPERTY::READ);
pManufacturerCharacteristic->setValue("M5Stack");
// HID Service
NimBLEService* pHidService = pServer->createService(NimBLEUUID("1812"), 40);
// HID Service - HID Information
NimBLECharacteristic* pHidInfoCharacteristic = pHidService->createCharacteristic("2A4A",
NIMBLE_PROPERTY::READ);
uint8_t country = 0x00;
uint8_t flags = 0x01;
uint8_t info[] = {0x11, 0x1, country, flags};
pHidInfoCharacteristic->setValue(info, sizeof(info));
// HID Service - Report Map
NimBLECharacteristic* pReportMapCharacteristic = pHidService->createCharacteristic("2A4B",
NIMBLE_PROPERTY::READ);
pReportMapCharacteristic->setValue((uint8_t *)reportMap, sizeof(reportMap));
// HID Service - HID Control Point
pHidService->createCharacteristic("2A4C", NIMBLE_PROPERTY::WRITE_NR);
// HID Service - Protocol Mode
NimBLECharacteristic* pProtocolModeCharacteristic = pHidService->createCharacteristic("2A4E",
NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ);
const uint8_t pMode[] = {0x01}; // 0: Boot Protocol 1: Rport Protocol
pProtocolModeCharacteristic->setValue((uint8_t *)pMode, 1);
// HID Service - Report 1
NimBLECharacteristic* pInputCharacteristic1 = pHidService->createCharacteristic("2A4D",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);
pReport1 = pInputCharacteristic1;
// Report Descriptor 1
NimBLEDescriptor* pDesc1 = pInputCharacteristic1->createDescriptor("2908",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
uint8_t desc1_val[] = {1, 0x01}; // Report ID 1 を Input に設定
pDesc1->setValue((uint8_t*)desc1_val, 2);
// HID Service - Report 2
NimBLECharacteristic* pInputCharacteristic2 = pHidService->createCharacteristic("2A4D",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);
pReport2 = pInputCharacteristic2;
// Report Descriptor 2
NimBLEDescriptor* pDesc2 = pInputCharacteristic2->createDescriptor("2908",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
uint8_t desc2_val[] = {3, 0x01}; // Report ID 3 を Input に設定
pDesc2->setValue((uint8_t*) desc2_val, 2);
// HID Service - Report 3
NimBLECharacteristic* pOutputCharacteristic = pHidService->createCharacteristic("2A4D",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC);
pOutputCharacteristic->setCallbacks(&chrCallbacks);
pReport3 = pOutputCharacteristic;
// Report Descriptor 3
NimBLEDescriptor* pDesc3 = pOutputCharacteristic->createDescriptor("2908",
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::WRITE_ENC, 20);
uint8_t desc3_val[] = {1, 0x02}; // Report ID 1 を Output に設定
pDesc3->setValue((uint8_t*) desc3_val, 2);
pDeviceInfoService->start();
pHidService->start();
pServer->advertiseOnDisconnect(false);
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->setAppearance(HID_KEYBOARD);
pAdvertising->addServiceUUID(pHidService->getUUID());
pAdvertising->setScanResponse(false);
pAdvertising->start();
}
void change_bluetooth_connection(int connection) {
target_connection = connection;
if (connection_established) {
connection_established = false;
pServer->disconnect(target_handle);
}
bluetooth_deinit();
bluetooth_init(connection);
}
class REALFORCE : public HIDUniversal {
public:
REALFORCE(USB *p) : HIDUniversal(p) {};
bool connected() {
return HIDUniversal::isReady() &&
HIDUniversal::VID == REALFORCE_VID && HIDUniversal::PID == REALFORCE_PID;
};
private:
void ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
uint8_t OnInitSuccessful() { // Called by the HIDUniversal library on success
if (HIDUniversal::VID != REALFORCE_VID || HIDUniversal::PID != REALFORCE_PID) {
return 1;
}
return 0;
};
};
USB Usb;
REALFORCE realforce(&Usb);
void sendKey(uint8_t *key) {
if (pServer->getConnectedCount() && connection_established) {
pReport1->setValue(key, 8);
pReport1->notify();
}
}
void sendMediaKey(uint8_t *mediakey) {
if (pServer->getConnectedCount() && connection_established) {
pReport2->setValue(mediakey, 2);
pReport2->notify();
}
}
// USB
void REALFORCE::ParseHIDData(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
uint8_t mediakey[2];
static boolean modifyflag = false;
static boolean flowflag = false; // Logicool Flow Support
static unsigned long flowstart = 0;
if (len == 8) {
boolean releaseallflag = true;
for (int i = 1; i < 8; i++) {
if (buf[i] != 0) {
releaseallflag = false;
}
}
// Logicool Flow suport
if (buf[0] == 0x05 && buf[2] == 0x00 && buf[3] == 0x00) { // 左 Option + 左 Ctrl
flowflag = true;
flowstart = millis();
}
else if (flowflag) {
if (buf[0] == 0x05 && (buf[2] != 0x00 || buf[3] != 0x00)) { // 違うキーが押されたのでクリア
flowflag = false;
}
else if (buf[0] == 0x00 && buf[2] == 0x00 && buf[3] == 0x00) { // リリース
flowflag = false;
if (millis() - flowstart > 500) {
sendKey(buf);
// 切り替え
if (target_connection == 0) {
target_connection = 1;
}
else {
target_connection = 0;
}
change_bluetooth_connection(target_connection);
return;
}
}
else {
sendKey(buf);
}
}
// 接続先切り替えキー
if (buf[0] == 0x08 && (buf[2] == 0x1e || buf[3] == 0x1e)) {
// cmd-1
if (target_connection != 0) {
change_bluetooth_connection(0);
}
}
else if (buf[0] == 0x08 && (buf[2] == 0x1f || buf[3] == 0x1f)) {
// cmd-2
if (target_connection != 1) {
change_bluetooth_connection(1);
}
}
else if (buf[0] == 0x08 && (buf[2] == 0x20 || buf[3] == 0x20)) {
// cmd-3
if (target_connection != 2) {
change_bluetooth_connection(2);
}
}
else if (buf[0] == 0x08 && (buf[2] == 0x21 || buf[3] == 0x21)) {
// cmd-4
if (target_connection != 3) {
change_bluetooth_connection(3);
}
}
else if (buf[0] == 0 && (buf[2] == 0x69 || buf[3] == 0x69)) {
// F14 単独
if (target_connection != 0) {
change_bluetooth_connection(0);
}
}
else if (buf[0] == 0 && (buf[2] == 0x6a || buf[3] == 0x6a)) {
// F15 単独
if (target_connection != 1) {
change_bluetooth_connection(1);
}
}
else {
if (releaseallflag && modifyflag) {
mediakey[0] = 0;
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = false;
}
sendKey(buf);
}
}
else if (len == 2) {
if (buf[1] == 2) {
// Brightness Down
mediakey[0] = 0;
mediakey[1] = 1;
sendMediaKey(mediakey);
modifyflag = true;
}
if (buf[1] == 1) {
// Brightness Up
mediakey[0] = 0;
mediakey[1] = 2;
sendMediaKey(mediakey);
modifyflag = true;
}
if (buf[1] == 4) {
// Eject
mediakey[0] = 128;
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0) {
// release
mediakey[0] = 0;
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = false;
}
}
else if (len == 3) {
if (buf[1] == 0x04) {
mediakey[0] = 2; // Previous Track
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0x01) {
mediakey[0] = 8; // Play / Pause
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0x08) {
mediakey[0] = 1; // Next Track
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0x10) {
mediakey[0] = 16; // Mute
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0x20) {
mediakey[0] = 64; // Volume Down
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0x40) {
mediakey[0] = 32; // Volume Up
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = true;
}
else if (buf[1] == 0) {
// release
mediakey[0] = 0;
mediakey[1] = 0;
sendMediaKey(mediakey);
modifyflag = false;
}
}
}
void setup() {
// EEPROM support
EEPROM.begin(4); // 4 byte EEPROM data
EEPROM.get(0, target_connection);
if (target_connection < 0 || 3 < target_connection) {
target_connection = 0;
}
M5.begin();
esp_efuse_mac_get_default(org_mac);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setBrightness(20);
Usb.Init();
bluetooth_init(target_connection);
}
void loop() {
Usb.Task();
disp();
}