M5Stackで日本語入力を作る
M5Stack。かわいいですよね。私はM5Paperが好きですが、M5Stack Basicも一個持っています。
M5Paperはフォントファイルさえ用意すれば日本語を表示できますが、M5Stackでも簡単に日本語を表示する方法があります。LovyanGFXを使う!
M5Stackの小さい画面に日本語を表示させるだけでもかなりかわいいですが、こう…その場で好きな日本語を表示させたくなりました。WebやWiFiやシリアルポートを弄ることなく日本語を入力するには…?
そう、日本語入力プログラムを作ればいいですね。作った。単純なのを…
キーボード
M5Stackに繋げられるかわいいキーボード、CardKBというのがあります。
これをM5Stackに繋げるとI2Cでキーを受信できます。こんな感じよ
// read keyboard
Wire.requestFrom(CARDKB_ADDR, 1);
while (Wire.available())
{
char c = Wire.read(); // receive a byte as characterif
ASCIIコードでキーを受信できるので、次はこれをローマ字→かな変換しましょう。
ローマ字→かな変換
これもまあなんとかなりますよね。a→あ、ka→か のようなマッピングを作っておいて、片っ端から当てはめていけばいいだけです。std::mapを使いました。
std::map<String, String> kanaFromRomanMap;
String kanaFromRoman(String roman)
{
if (kanaFromRomanMap.count(roman) > 0)
{
Serial.println(roman);
return kanaFromRomanMap.at(roman);
}
return "";
}
マッピングファイルは自分で作りました。抜けは多大にありそうですがまあいいや。なお手を抜いたので小さい「っ」は常にttとかそういうのがあります。
かな→漢字変換
かな漢字変換を自分で作るにはいくつかの難点があります。
形態素解析
現代のスマホやPCの日本語入力はローマ字にしろかなにしろバーッと打って行ってから変換キーを押すと、自動で文節や単語を区切って漢字に変換してくれます。でも自分でこの文節や単語区切りを判別しようと思ったら相当に大変。MeCabというオープンソースもありますがM5Stackに移植も大変そう。
そこで今回は形態素解析をせず、単漢字や単語ごとに変換キーを押していってちまちま変換する古代のワープロみたいな方式を使います。これならローマ字→かな変換のようにマッピングを用意すればいいだけですね。だけかあ…
単語辞書
じゃあローマ字→かなみたいな辞書を自分で作ればいいかっていうと、やっぱりこれもあまりに大変ですよね。なんとかフリーだったりオープンソースだったりの単語辞書はないかと探していて、SKKというのを見つけました。
SKKはそれ自体かな漢字変換プログラムです。ちょっと独特な操作方法なんですが、今回この操作方法はスルーして辞書だけ使いました。
SKK-JISYO.SやSKK-JISYO.Mがサイズが小さくてM5Stack向きです。ただし、明確なライセンス規定が無いようなのでプロジェクトに含むことはしません。あと文字エンコードがEUC-JPなのでUTF-8に変換して使いました。
SKK辞書特有のやつ
SKK辞書は品詞の情報が無いので、活用のある単語は送り仮名を含まないように作られているようです。つまり、「動く」みたいな動詞は「うごk→動k」のようになっていて、「動か」「動き」「動け」「動こ」みたいな活用に対応してるんですね。
これが今回の超シンプル漢字変換には都合がよく、ローマ字→かな変換をしているときに「うごく」だけでなく「うごk+u」のように最後のローマ字情報も保存しておいて漢字変換の時こちらでも検索をかけます。
ヒープメモリ
さて、これでかな→漢字のマッピングを読み込めばいいんですが、ローマ字→かなのようにstd::mapを使ったら一瞬でヒープメモリを使いつくしてクラッシュしました。M5Stack Basicは520KBのメモリを積んでいるのですが、富豪プログラミングには足りないようです…
じゃあどうしようかと考えて、辞書ファイル全部を読み込むのではなく、変換候補の最初の1文字をキーにして辞書ファイルのpositionを覚えておき、実際に変換するときにそこまでseekしてから該当行を探すことにしました。やってみるとこれで十分速度が出ます。
UTF-8
簡単に「変換候補の最初の1文字」とか言いましたけど、ArduinoのString型では日本語ってUTF-8で格納されてますよね。UTF-8の「1文字」は可変長なわけですよ。どうしようと思って…WikipediaのUTF-8の項目を読んで…判別コードを書きました。
std::vector<String> multiByteCharVectorFromString(String multiByteString)
{
int length = multiByteString.length();
const byte *cstr = (const byte *)(multiByteString.c_str());
std::vector<String> result;
String currentString = "";
String oneByteString = " ";
for (size_t i = 0; i < length; i++)
{
byte aByte = cstr[i];
byte mark = aByte & B11000000;
String oneByteString = " ";
if (mark <= B01000000)
{
// single byte
if (currentString.length() > 0)
{
result.push_back(currentString);
currentString = "";
}
oneByteString.setCharAt(0, aByte);
result.push_back(oneByteString);
}
else if (mark == B10000000)
{
// 2nd or later byte
oneByteString.setCharAt(0, aByte);
currentString += oneByteString;
}
else
{
// 1st byte
if (currentString.length() > 0)
{
result.push_back(currentString);
}
oneByteString.setCharAt(0, aByte);
currentString = oneByteString;
}
}
if (currentString.length() > 0)
{
result.push_back(currentString);
}
return result;
}
こういうコードでUTF-8のStringから、「1文字」ずつ切り出すことができるはずです。実際にはひらがなの範囲だけキーにできればいいので、UTF-8でひらがなを表す3バイトから後半2バイト分のコードを取ってきてuint16_tとかにしています。ひらがなの範囲ならcharでいいと思うんですけど。
ひらがな→カタカナ変換
変換キー押して候補を選んでるときに漢字だけじゃなくカタカナも出したいですよね。カタカナに変換するキーとかあってもいい。じゃあひらがな→カタカナ変換はどうすればいいんでしょうか。
これは単純にUnicodeではひらがな→カタカナのコード番号の差が一定なので、コード番号を得てから加算してUTF-8に直せばいいわけですね。ただUTF-8の1バイト中には直接コード番号を表しているわけではない上位ビットとかがあるので色々ビットシフトとかしてコード番号を算出してなんとかします。
どんな感じに動くか
画面の下の方を変換に使って、Enterキーを押すと上の文字列に追加するようにしました。まずローマ字で入力します
ひらがなで単語を入力してSpaceキーを押すと候補を順番に表示します。Enterキーを押すと上の文字列に追加されます
旧世紀のワープロにすら劣る機能ですが、かわいいですね。あとでSDカードに保存する機能とかをつけよう