G◯ogleの二番煎じとか言わないの
この記事は CAMPHOR- Advent Calendar 2022 17日目の記事です
経緯
- @artic_kuee , 自作キーボード沼に片足を突っ込む
- キーボードを一つも組み立てていないのにスイッチを蒐め始める
- スイッチの押し心地は気になる
というわけで、キーチェッカーを自作する、という目標でやってきました
ただ光るだけじゃつまらないから、どうせならモールスでもやるか、と。そういうわけです。
なお本人はモールスはからきしの模様
諸々の事情があってハード編はしばらくおあずけになるんですが、、
まあ、そこはまたのお楽しみに
使用ツール
ハード
開発はM5atom-lite, 本番ではM5stampを使用(小型化のため)
積んでるチップは両者同じなので、設定を特にいじる必要はそんなになかった(ハズ)
ソフト
- arduinojson
- ESP32 BLE Keyboard library
- FastLED
- esp32のタイマー割り込み
- esp32のdeep sleep
モールス信号を処理する方式に関していくつか迷いましたが、適当にググったところモールスのjsonが見つかったので拝借することに
トンツーから文字を検索したいので、適当にググって見つけた方法を使ってColabで適当に変換したものをそのまま文字列としてソースコードに貼りました。
arduinojson
arduinojsonの使い方はだいたいここを参照
// String json = "調達したjsonの文字列をペースト"
StaticJsonDocument<1536> doc;
DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}
//参照のしかた
const char* read = doc["---"]; //"o"
簡単に叩けてありがたいですね
ちなみにjsonの部分は抜いています。
著作権云々が怖かったのと、むやみに行が長くなって見づらくなるのでね。
なお、docの定義にある謎の数字1536はここに読み込みたいjsonを食わせると得られます。バッファのサイズですね。
このページにおける設定は順に ESP32, Deserialize, Stringとしました。
Tips
- 文字列としてソースコードに組み込むので、クオーテーションマークのエスケープ(\")をお忘れなく
- スコープを抜けると参照できなくなる(うまいやり方は知らん)ので、お気をつけて
私がやった方法としては以下がある
- loop関数で毎回”読み込みが必要か”を確かめて必要なら読み込む
- setup関数内に無限ループを打ち込む(最後のソースコードはこの方法)
ESP32 BLE Keyboard library
こちらも非常に便利。
まあ適当に調べたら日本語でも資料があるし、example読めば大体わかります。
bleKeyboard.print() で文章は打ち込めるし、BSやEnter,ShiftはbleKeyboard.write(KEY_***)という形式で送れます。
タイマー割り込み
長押しの判定のためですね。
最初はM5atom.hを使っていました。
ボタンに対してisPressed,isReleasedのほかにpressedFor,releasedForみたいな便利な関数が多かったんですね
ただ、外部のボタンに対して基本的に適応できるものじゃないんですよ。
適当にマクロ宣言したらいけるかな、とライブラリのソースとか見たんですが、だめでした。
LavyanGFXとかと依存関係があるみたいで、追うのは面倒困難でした。
らびやん大先生強過ぎません?
というわけで、タイマーを叩くことにしました。
Tickerとか、頼れるライブラリはいろいろあるんでしょうが、せっかく低レイヤーの民を僭称しているのでね
ここなんかを参照しました。
ここも非常にいいですね
//For timer interruption
volatile int PressStatus;
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// detecting dash
void IRAM_ATTR onTimer(){
portENTER_CRITICAL_ISR(&timerMux);
PressStatus++;
portEXIT_CRITICAL_ISR(&timerMux);
}
//中略
int setup(){
//中略
//タイマーの登録など
timer = timerBegin(0,80,true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000, true);
timerAlarmEnable(timer);
//タイマーのリセット
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer, 0);
//変数の読み込み
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
Serial.println(status);
//使用例
if(status > sec_turnoff) {
変数名はすごく適当です すまん
さて、割り込みハンドラを登録して、メインタスクと共有する変数のためにミューテックスを使っています。
1msごとにPressStatusがインクリメントされるだけです。
この程度の内容ならタイマーの発火時間を長くするだけで実装できるでしょう。
やりたかっただけです。ええ。
ちなみに、トンツーの時間を調整したい人はTやtdashなどをいじってください。
なお、エンター、スペース、BSは流石に機能としてほしいのでオレオレ実装してます。
deep sleep
ケースを作るときに、電源スイッチを設けるのを忘れていたので
というかスイッチテスターなのに別途スイッチが要るってどうなのよ?
ってわけでやってみました。とても簡単にできて良かった。
ここなんかを見ました。
ここも大変良いですね
//復帰条件の設定
esp_sleep_enable_ext0_wakeup(GPIO_NUM_39,0);
//中略
//deep sleep
leds[0] = CRGB(0,0,0);
FastLED.show();
esp_deep_sleep_start();
復帰条件はタイマーとかGPIOとかいろいろ設定できるみたいですね。便利
注意点としては、LEDとかは勝手に消えません。手動で消しましょう。
ちなみに、今回のソースコードでは10秒で寝る設定になっているので、適宜sec_turnoffをいじってください
成果
実際に動いているのはこんな感じ
https://twitter.com/artic_kuee/status/1603813522734952448?s=20&t=eRNxpdJHO-51uOzpGtIFWQ
いいですね(小並感)
なお、キーテスタとしてのハードは結構頑張って設計していて
3Dプリンタの印刷までこぎつけたのですが…
いざ組み上げる段になって致命的なミスが発覚!
LEDの窓の位置が合わないというね…
さらに @artic_kuee 君がCAMPHOR-Makeの3Dプリンタを詰まらせたり…
と、いうわけで、FreeCADで治安の悪いモデルを作った話や3Dプリンタを使ってみた所感など諸々をまとめて、次回ハード編をお送りしたいと思います!
乞うご期待っ!
ソースコード
大変汚くて恐縮です。質問歓迎。
ただ、qiitaはあまり巡回しないのでtwitterで聞いた方がはやい
(学生の皆さんは、ぜひCAMPHOR-HOUSEにも足を運んでくださいね!)
先述の通りjsonの部分は抜いています。 使いたい人は適宜埋めてね
#include "Arduino.h"
#include <ArduinoJson.h>
#include <BleKeyboard.h>
#include <FastLED.h>
#include "esp_system.h"
BleKeyboard bleKeyboard;
//For timer interruption
volatile int PressStatus;
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// detecting dash
void IRAM_ATTR onTimer(){
portENTER_CRITICAL_ISR(&timerMux);
PressStatus++;
portEXIT_CRITICAL_ISR(&timerMux);
}
bool sign;
// time unit(ms)
int T = 200;
int tdash = 2 * T;
int tfunc = 4 * T;
int tblank = 4 * T;
int sec_turnoff = 10 * 1000;
CRGB dispColor(uint8_t r, uint8_t g, uint8_t b) {
return (CRGB)((b << 16) | (r << 8) | g);
}
#define DATA_PIN 27
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
void setup() {
bleKeyboard.begin();
FastLED.addLeds<SK6812, DATA_PIN, RGB>(leds, NUM_LEDS);
leds[0] = CRGB(128,128,128);
FastLED.show();
pinMode(GPIO_NUM_39, INPUT);
esp_sleep_enable_ext0_wakeup(GPIO_NUM_39,0);
Serial.begin(115200);
delay(500);
Serial.print("Pico Start\n");
timer = timerBegin(0,80,true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 1000, true);
timerAlarmEnable(timer);
int count = 0;
char m[7];
bool fin = false;
bool func = false;
bool shift = false;
int status;
// String json = "調達したjsonの文字列をペースト"
StaticJsonDocument<1536> doc;
DeserializationError error = deserializeJson(doc, json);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}
//setupのスコープを抜けるとjsonの中身が読めなくなるためsetup関数内でループ
while(1){
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
//接続していない場合の挙動
while (!bleKeyboard.isConnected()) {
Serial.println("not connected");
delay(100);
if(!digitalRead(GPIO_NUM_39)){
leds[0] = CRGB(128,128,128);
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer,0);
} else {
leds[0] = CRGB(128,0,0);
}
FastLED.show();
//sec_sleep秒後にdeep sleep
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
Serial.println(status);
if(status > sec_turnoff) {
Serial.println("deep sleep");
leds[0] = CRGB(0,0,0);
FastLED.show();
esp_deep_sleep_start();
}
}
count = 0;
fin = false;
func = false;
shift = false;
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer,0);
leds[0] = CRGB(0,128,128);
FastLED.show();
//ボタン押下待ち
while (digitalRead(GPIO_NUM_39)){
delay(10);
//sec_sleep秒後にdeep sleep
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if(status > sec_turnoff){
Serial.println("deep sleep");
leds[0] = CRGB(0,0,0);
FastLED.show();
esp_deep_sleep_start();
}
};
//ボタンが押された状態からスタート
while (!fin) {
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer, 0); //ここでタイマーをリセット
sign = false;
leds[0] = CRGB(0,255,0);
FastLED.show();
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
//T以上経過すれば長音扱い
if (status > T) {
sign = true;
leds[0] = CRGB(0,0,255);
FastLED.show();
}
//2*T以上経過すれば特殊キーモードへ
if (status > tfunc) {
func = true;
leds[0] = CRGB(128,0,128);
FastLED.show();
}
} while (!digitalRead(GPIO_NUM_39));
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer, 0); //ここでタイマーをリセット
leds[0] = CRGB(128,128,128);
FastLED.show();
if (func) break;
if (sign) {
m[count] = '-';
} else {
m[count] = '.';
}
count++;
//押されない状態で3*T以上経過すれば文字の切れ目
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > tblank || count >= 6) {
fin = true;
break;
}
} while (digitalRead(GPIO_NUM_39));
}
//特殊キー用のモード
if (func) {
func = false;
Serial.println("func mode");
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > tblank) {
leds[0] = CRGB(128,128,128);
FastLED.show();
fin = true;
break;
};
} while (digitalRead(GPIO_NUM_39));
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer,0);
if (fin) {
fin = false;
} else {
leds[0] = CRGB(0,255,0);
FastLED.show();
sign = false;
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > T) {
sign = true;
leds[0] = CRGB(0,0,255);
FastLED.show();
}
} while (!digitalRead(GPIO_NUM_39));
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer,0);
//スペースキー
if (sign) {
sign = false;
bleKeyboard.print(" ");
Serial.println("space");
while (true) {
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > 2*T) {
fin = true;
}
} while (digitalRead(GPIO_NUM_39));
if (fin) {
break;
}
while (!digitalRead(GPIO_NUM_39)){};
bleKeyboard.print(" ");
}
leds[0] = CRGB(128,128,128);
FastLED.show();
fin = false;
} else {
//短音1回はエンター
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > tblank) {
bleKeyboard.write(KEY_RETURN);
Serial.println("enter");
delay(50);
bleKeyboard.releaseAll();
shift = false;
leds[0] = CRGB(128,128,128);
FastLED.show();
fin = true;
break;
};
} while (digitalRead(GPIO_NUM_39));
if (fin) {
fin = false;
} else {
sign = false;
leds[0] = CRGB(0,255,0);
FastLED.show();
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > T) {
sign = true;
leds[0] = CRGB(0,0,255);
FastLED.show();
}
} while (!digitalRead(GPIO_NUM_39));
portENTER_CRITICAL_ISR(&timerMux);
PressStatus = 0;
portEXIT_CRITICAL_ISR(&timerMux);
timerWrite(timer,0);
if (sign) {
shift = !shift;
if (shift) {
bleKeyboard.press(KEY_LEFT_SHIFT);
} else {
bleKeyboard.releaseAll();
}
Serial.println("shift");
} else {
bleKeyboard.press(KEY_BACKSPACE);
Serial.println("back space");
delay(50);
bleKeyboard.releaseAll();
shift = false;
while (true) {
do {
delay(10);
portENTER_CRITICAL_ISR(&timerMux);
status = PressStatus;
portEXIT_CRITICAL_ISR(&timerMux);
if (status > tblank) {
fin = true;
break;
}
} while (digitalRead(GPIO_NUM_39));
if (fin) {
break;
}while (!digitalRead(GPIO_NUM_39)){};
bleKeyboard.press(KEY_BACKSPACE);
delay(50);
bleKeyboard.releaseAll();
shift = false;
}
fin = false;
}
leds[0] = CRGB(128,128,128);
FastLED.show();
}
}
}
} else {
m[count] = '\0';
Serial.println(m);
const char* send = doc[m];
if (send) {
Serial.println(send);
bleKeyboard.print(send);
} else Serial.println("invalid morse code");
}
}
}
void loop() {}