自作キーボードができるまで

f:id:kumatoki:20170925120214j:plain 他の写真

ババーン

これ、私が作りました。すごいでしょ。 batmanっぽいのでbatkeysと名付けました。

デザイン的にはkeyboardioとnisseを模範している。 それらをさらにコンパクトかつ高さを低く抑えた形となった。

使い勝手としては、タイピング速度測定サイトで記録を更新することができたのでいい方と言ってもいいだろう。気になると思うのは親指キー周り。率直なところ親指に役割を持たせすぎると辛い。自分では3キーくらいがベストだと思う。がキーを少なくしすぎたのでどうしても親指をフル活用せざる追えない。まぁそのうち慣れてくるでしょう。

回顧録

さて自作キーボードという世界がある。世の中に普及しているキーボードは人間工学的に好ましくないものが多い。そこで流行ったのがErgodox、最近ではlet's split。しかしこれでも満足しない人たちがいる。そういう人たちが足を踏み入れるのが自作キーボードという世界。

私もその一人だ。と言いたいがそうでもなく、ただ作って見たかったというだけでそもそもプログラマですらなければデスクワーカーでもない。本当に趣味で、好奇心で、作りたかっただけなのでした。

始まりは、twitterでキーボードを作り始めている方を見かけたから。その方はないんさんと言う方でキーボードのモックアップを最初に作られていた。そのキーボードは当時自分が使っていたergodoxと比べてとても小さく、さらに左右分離式で、親指をうまく活用できるようキーを配置してあった。それはとてもかっこよくてSNS引きこもりな自分がついコメントしてしまうほど魅力的だった。しかしその方は、さらに基盤やケース、ファームウェアも自分で作られていて、とても自分が真似できるようなことでは無いとも思った。

とは思いつつも電子工作に子供の頃から漠然とした興味があったので、少しずつ初めてみることにした。 まずはArduino unoを買って簡単なプログラムを動かした。highとlow、vccやgnd、抵抗やLEDやタクトスイッチ。とても簡単なところから一つずつ試していってゆっくり体で覚えて言った。正直なところ、それがなぜそうなるのか、もっと根本的な原理はなんなのか、までは知らない。ただそうすればそう動くと覚えていった。

少しずつ慣れていき、次第にキーボードに焦点を当てていった。キーボードの根本はキーマトリックスというものとそれをPC側へキー入力として伝えるマイコン。この二つを覚えればキーボードの仕組みはほぼ覚えたようなものだった。

あとは具体的にどのように実用的な形にしていくか。ここが自分にとっては難しいところだった。 デザインはどうすればいいのか。デザインをどう形にすればいいのか。イマイチ踏み出せないでいた。

そんなこんなしてる間に、他の方のキーボードが出てき始めていた。 ゆかりさんhrhgさん言った方々のとてもかっこ良いものがさらに自分を刺激した。 このお二人に共通しているのは3DPrinterを使って媒体を作られていること。 これで簡単にケースが作れると知って、fusion360という3DCadソフトを覚えることにした。 このソフトとても高機能ではあるがそのぶん難しい。しかし自分のデザインが形となっている様はとても楽しく、それに身を任せてすぐに使い方を覚えることができた。

デザインをする上で参考としていたのがesrilleのnisse。それを作られている方のブログ。 Shiki's Weblog この方のブログを読み、左右分離型を前提としていた考えを変え、一体型にすることにした。 まあ、技術的にも一体型の方が楽だというのもある。

dmm.makeで3DPrintしてもらうことができたのでそこに注文した。値段は4k~5k。まあ、40キーしかないこのサイズだからこの値段で済んでるとも言える。あとはスイッチやキーキャプ、マイコンなどを用意しケースの出来上がりを待つだけ。届いたらあとは根気よく半田付け。これで出来上がり。 今回配線に使ったポリウレタン銅線は少し半田付けにコツがいるようで、最初は苦労したがtwitterでゆかりさんにコツを教えていただいたら、すぐにできるようになったのでありがたい。

ハードが出来上がればあとはソフト。キーマトリックスを読み、それを元にキーコードを発行していく。 コンパクトなキーボードに欠かせないのがレイヤー。これもすんなり実装できた。もう一つ必要な機能があってそれはqmk firmwareではmod tapと呼ばれる機能で一つのキーに修飾キーと普通の文字キーの二つを役割を与える機能。これの実装に手間取った。というかすぐに諦めた。そこで大人しくqmk firmwareに対応させることにした。これも前述のないんさんが記事にされているのでそれにしたがって進めて言った。途中わからないところがあったがすぐにないんさんにフォローしていただいた。おかげでqmk対応もすんなりできたのでキーボードとして満足に機能させることができた。

@matsPodさんにRedditにポストしていただいので海外の方からの反応もいただくことができた。英語わかんないので雰囲気だけだけどね。 reddit

先人たちのアドバイスのおかげで自分もキーボードというものを作り上げることができた。 感謝したい。

elmでライフゲーム

elm

リアクティブプログラミングを軸にした関数型言語であり、Javascriptへ変換されるいわゆるAltJS。 破壊的な操作も関数的に扱える仕組みはモナドではない新しいタイプの操作体系で、Elm Architectureと呼ばれる。

そんなelmに入門。個人的に新しい言語を覚えるときはHello worldの代わりにLifegameを書いているので、例によってelmでもライフゲームから始める。

ライフゲーム

お堅い呼び名はセルオートマトン。実態は単細胞シミュレーション。 あるセルの状態とその周囲の状態からルールを元に次の状態を決定していく。 そのルールは、

  • あるセルの状態1であり、周囲に2つの状態1があれば、次のセルの状態は1。
  • あるセルの状態1であり、周囲に3つの状態1があれば、次のセルの状態は1。
  • あるセルの状態0であり、周囲に3つの状態1があれば、次のセルの状態は1。
  • それ以外は、セルの状態は0となる。

コード

https://github.com/kmtoki/lifegame-elm

レコードと呼ばれる、JSのオブジェクトのようなものでまとめて状態管理。 セルの実態はArray (Array Bool)と2次元配列。

module Lifegame exposing (..)

import Array exposing (Array)


type alias Lifegame =
    { size : { y : Int, x : Int }
    , count : Int
    , isContiune : Bool
    , cells : Array (Array Bool)
    }

init : Int -> Int -> Lifegame
init y x =
    { size = { y = y, x = x }
    , count = 0
    , isContiune = False
    , cells = Array.repeat y <| Array.repeat x False
    }

上記のルールを関数化したものがこれ。

rule : Bool -> Int -> Bool
rule b n =
    case ( b, n ) of
        ( True, 2 ) ->
            True

        ( True, 3 ) ->
            True

        ( False, 3 ) ->
            True

        _ ->
            False

周囲の状態を調べるのがenv。 ルールと周囲の状態を元にセルを更新するのがnext。

env : Int -> Int -> Lifegame -> Int
env y x lg =
    let
        arounds =
            [ ( y - 1, x - 1 )
            , ( y - 1, x )
            , ( y - 1, x + 1 )
            , ( y, x - 1 )
            , ( y, x + 1 )
            , ( y + 1, x - 1 )
            , ( y + 1, x )
            , ( y + 1, x + 1 )
            ]

        f ( y, x ) =
            case Array.get y lg.cells of
                Nothing ->
                    0

                Just xs ->
                    case Array.get x xs of
                        Nothing ->
                            0

                        Just b ->
                            if b then
                                1
                            else
                                0
    in
        List.sum <| List.map f arounds


next : Lifegame -> Lifegame
next lg =
    let
        cells =
            Array.indexedMap
                (\y xs ->
                    Array.indexedMap
                        (\x b -> rule b <| env y x lg)
                        xs
                )
                lg.cells
    in
        { lg
            | count = lg.count + 1
            , cells = cells
        }

ライフゲームの実態は以上のコードで済む。 そのライフゲームをブラウザ上でアニメーションすることがelmでは簡単にできる。 Model Update Viewを基本とし、Subscriptionsでタイマーなどの副作用のある操作などをすることができる。

Modelを各関数で引き回し、それを元にViewがHtmlを生成し、ブラウザ上のイベントをMsgという形でupdateへ送信し、各操作へ振り分けていく。

面白いのが副作用の扱い方。副作用をSub/Cmdで包み関数の返り値として返すとElm Architectureが裏側でよしなにしてくれる。OCamlHaskellとも違うながらも純粋関数的な程を保っている。

今回のコードではランダムとタイマーしか使っていないが、この仕組みでWebsocketなども扱える豊かな表現力ある。

module Main exposing (..)

import Html as H
import Html.Attributes as HA
import Html.Events as HE
import Svg as S
import Svg.Attributes as SA
import Array exposing (Array)
import Time
import List
import String
import Random
import Lifegame


main =
    H.program
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


type alias Model =
    Lifegame.Lifegame

type Msg
    = New
    | SetY String
    | SetX String
    | Start
    | Next
    | Stop
    | Random
    | RandomSet (Array (Array Bool))


init : ( Model, Cmd Msg )
init =
    ( Lifegame.init 0 0, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions model =
    if model.isContiune then
        Time.every (Time.millisecond * 100) <| always Next
    else
        Sub.map (always Stop) Sub.none


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        New ->
            ( Lifegame.init model.size.y model.size.x, Cmd.none )

        SetY s ->
            case String.toInt s of
                Ok n ->
                    ( { model | size = { x = model.size.x, y = n } }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )

        SetX s ->
            case String.toInt s of
                Ok n ->
                    ( { model | size = { y = model.size.y, x = n } }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )

        Start ->
            ( { model | isContiune = True }, Cmd.none )

        Next ->
            ( Lifegame.next model, Cmd.none )

        Stop ->
            ( { model | isContiune = False }, Cmd.none )

        RandomSet cells ->
            ( { model | cells = cells }, Cmd.none )

        Random ->
            let
                gen =
                    Random.map Array.fromList <|
                        Random.list model.size.y <|
                            Random.map Array.fromList <|
                                Random.list model.size.x Random.bool
            in
                ( model, Random.generate RandomSet gen )


view : Model -> H.Html Msg
view model =
    H.div []
        [ H.div []
            [ H.input [ HA.type_ "number", HA.placeholder "y", HE.onInput SetY ] []
            , H.input [ HA.type_ "number", HA.placeholder "x", HE.onInput SetX ] []
            , H.button [ HE.onClick New ] [ H.text "New" ]
            , H.button [ HE.onClick Start ] [ H.text "Start" ]
            , H.button [ HE.onClick Stop ] [ H.text "Stop" ]
            , H.button [ HE.onClick Random ] [ H.text "Random" ]
            ]
        , H.div [] [ H.text (toString model.count) ]
        , H.div [] [ viewLifegame model ]
        ]


viewLifegame : Model -> H.Html Msg
viewLifegame model =
    S.svg
        [ SA.width (toString <| model.size.x * 50)
        , SA.height (toString <| model.size.y * 50)
        ]
        (List.concat <|
            Array.toList <|
                Array.indexedMap
                    (\y xs ->
                        Array.toList <|
                            Array.indexedMap
                                (\x b ->
                                    S.rect
                                        [ SA.x (toString <| x * 5)
                                        , SA.y (toString <| y * 5)
                                        , SA.width "5"
                                        , SA.height "5"
                                        , SA.fill
                                            (if b then
                                                "green"
                                             else
                                                "black"
                                            )
                                        , SA.stroke "black"
                                        ]
                                        []
                                )
                                xs
                    )
                    model.cells
        )

出来上がり

f:id:kumatoki:20170731172704g:plain

所感

元々はFRPベースだったらしく、小難しい概念で副作用を取り扱っていようだがElm Architectureとやらのおかげでずいぶん簡単に入門できた。パターンマッチばかりで関数合成が少なくなってしまったが語彙の問題なので覚えればすぐ簡潔にかけるようになるだろう。elm-formatにはちょっと趣味と合わない部分があるが、まあ人様に見やすく編集しやすいコード書式という意味でelm-formatで統一的に合わせていくのは良いと思う。
HTMLイベントの発火の分、Msgを追加していかないといけないのは面倒なのでどうにかまとめたいが方法がまだよくわからない。あとTaskという概念もあるらしく、非同期通信などを使う場合は必要になってくるらしい。Haskellで関数型の思考には慣れているので書き味は程よく気持ちのいいものであったが、Haskellと比べ、型安全なライブラリ設計なので冗長的な部分も多々ある。型クラスの導入も議論されているらしいがHaskellの標準ライブラリの型クラス化などを見ている限り、なかなか型クラスも面倒なところがありそうなので、無いなら無いで良いのかもしれない。データ操作が明示的なのも、書くときは面倒だが、読むときに楽できるので良い部分もある。
フロントエンドの知識はまるでないので、これを機会に少し覚えていけたらと思うだけで特に勉強しない結末。

let's split キーマップ

  /* Qwerty
   * ,-------------------------------------------, ,------------------------------------------,
   * | Tab   |   Q  |   W  |   E  |   R   |   T  | |   Y   |   U  |   I  |   O  |   P  |  BS  |
   * |-------+------+------+------+-------+------| |-------+------+------+------+------+------|
   * |Esc/Ctl| A/Md |   S  |   D  |   F   |   G  | |   H   |   J  |   K  |   L  |;:/Md |'"/Ctl|
   * |-------+------+------+------+-------+------| |-------+------+------+------+------+------|
   * |  =+   |   Z  |   X  |   C  |   V   |   B  | |   N   |   M  |  ,<  |  .>  |  /?  |  -_  |
   * |-------+------+------+------+-------+------| |-------+------+------+------+------+------|
   * |   `~  | Raise| Alt  | GUI  | Lower |Sp/Sft| |Ent/Sft| Lower| GUI  | ALT  | Raise|  \|  |
   * `-------------------------------------------' `------------------------------------------'
   */

  /* Lower 
   * ,-----------------------------------------, ,-----------------------------------------,
   * |      |   1  |   2  |   3  |   4  |   5  | |   6  |   7  |   8  |   9  |   0  |      |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * | PREV |   !  |   @  |   #  |   $  |   %  | |   ^  |   &  |   *  |   (  |   )  | NEXT |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |   ~  |   `  |   |  |   \  |      | |      |   [  |   ]  |   {  |   }  |      |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      | Eisu |      | |      | Kana |      |      |      |      |
   * `-----------------------------------------' `-----------------------------------------'
   */

  /* Raise 
   * ,-----------------------------------------, ,-----------------------------------------.
   * |  F1  |  F2  |  F3  |  F4  |  F5  |  F6  | |  F7  |  F8  |  F9  |  F10 |  F11 |  F12 |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      |      |      | |      |      |      |      |      |      |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      |      |      | |      |      |      |      |      |      |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      |      |      | |      |      |      |      |      |      |
   * `-----------------------------------------' `-----------------------------------------'
   */

  /* Media 
   * ,-----------------------------------------, ,-----------------------------------------.
   * |      |      | WhUp | MsUp | WhDn |      | |  End | PgDn |  Up  | PgUp | Home |      |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |Reset |      | MsLf | MsDn | MsRg |      | |      | Left | Down | Right|      |Reset |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      |      |      | | Prev |StpPly| Next |Mute  |VolDn |VolUp |
   * |------+------+------+------+------+------| |------+------+------+------+------+------|
   * |      |      |      |      |Click2|Click1| |      |      |      |      |      |      |
   * `-----------------------------------------' `-----------------------------------------'
   */

keymap.c

最初に

qmk_firmwareとは多様なキーボードに対応するための汎用ファームウェアであり、自分でキーマップを変えることができる。さらに普通のキーボードにはない機能があり、レイヤー、タップ/ホールド、タップダンス、マクロなどがある。

私のレツプリキーマップ

レツプリことlet’s split、48のキーをどう効率よく使えるか、は悩ましく楽しい問題。 48キーで十分と言えるのは一重に「レイヤー」と呼ばれる機能のおかげ。このレイヤーをうまく使い各々のキーマップを作り上げていける。 私の場合、4つのレイヤーを作っている。アルファベットやスペース、エンターなどの基本キーを配置したトップレイヤー、記号を散りばめたLowerレイヤー、ほぼ使われないFnキーのRaiseレイヤー、マウスカーソルやクリック、アローキーや音楽再生と音量調整ができるMediaレイヤー。

Qwerty

トップレイヤーはコード上ではQwertyレイヤーと読んでいる。今時わざわざQwerty配列なんぞ使わんでもよかろうと言う気もしなくはないが、新しい配列を覚えるコストは非常に高く、さらに既存のQwerty配列を前提に設計されたソフトウェアを多用する身としてはなかなか抜け出せない。

Qwertyレイヤーの肝は親指にある。親指スペースは当たり前だが、さらにエンターを持ってきた。さらにこの2キーはスペースとエンターとして機能すると同時にシフトとしても機能する。 どう言うことかと言う これはキーをタップした場合とホールドした場合で挙動を分けられる機能。 これによって一つのキーに二つの役割を与えることができる。よって親指シフトを実現することができている。

英数とかなの位置はなかなか難しく、当初はQwertyレイヤーに置いていたが誤入力した場合に非常に煩わしいのでLowerレイヤーに置くことにした。しかし多用するキーを入力するのに2つのキーを押さなければならないと言うのは少し納得が行かない面もある。

WindowsでのIME切り替えはAlt + `で全角/半角ができる

Lower

次にLowerレイヤーについて。プログラミングするので記号は全て多用する。その中でもさらによく使う記号を良い位置に置いておきたいがある程度既存の配列と互換性を持たせて置くと、配列を覚えやすいのでそれを優先した結果が上記の配列になっている。defaultの配列ではLowerとRaiseに記号を分けているので少々覚えるのにコストがかかるのではないかと思い、Lowerのみに記号を詰め込んだ。

NEXTTABとPREVTABとは、文字どうりタブ移動を一つのキーにまとめたもの。 MacOSでしか使えないショートカットだと思うのでWindowsではCtrl+Tab / Ctrl+Tab+Shiftで同様のことができる(多分。

// MacOS only
#define NEXTTAB ACTION_MODS_KEY(MOD_LGUI, KC_RCBR)
#define PREVTAB ACTION_MODS_KEY(MOD_LGUI, KC_LCBR)

Raise

Raiseレイヤーはファンクションキーを割り当てている。 そもそもレイヤー切り替えは脳に負担がかかるものだと思うので、少ないほど良いと思う。

Media

Aか;をホールドすると使える、 Mediaレイヤーはなかなか便利なもので、ホームポジションから手を動かさずマウス操作ができるのでかなり便利。キーボードからマウス操作もできるのもqmk_firmwareの機能。ただ矢印上と右か左を同時押しででカーソルがロックと言うか動かなくなるのでどうにかならないかな。

let's split

f:id:kumatoki:20170715123637j:plain

ミニマリズムエルゴノミクスキーボード、let’s split

Twitter上の日本のキーボードファンの間でlet’s splitというキーボードをみんなで作ろう的な企画が@matsPodさん主催で開催されていたので参加して作ってみることにした。

もとよりストレートネックと肩こりに悩まされていたのでエルゴノミクスキーボードへの興味は強くあった。そのうちにErgodoxを買い、キーマップを自分流に染めていくうちにErgodoxの無駄なキーの多さが気になっていたのでlet’s splitのコンセプトにはとても共感した。

組み立て、失敗続きの一週間

今回のために半田ごてを買うような電子工作初心者だったので組み立てには手間取った。 正確には組み立て自体はすんなりいった。多分一番乗りで完成!くらいの勢いで。 しかし分離している左右のキーボード同士の通信と左側の一部キーが機能しなかった。

Twitterで親切な自作ガチ勢の皆様(敬称)にアドバイスをいただきながら、あれこれ試してみたがうまくいかず、結局一度解体することに。 この解体作業中にも問題があったのだろう。 それはPCBのランド剥がれ。 pro microを取り外した時にランドが剥がれてしまった。 それもピンポイントでシリアル通信に使うピンのところが。 それを知識不足から確認せず、なんども分解と組み立てを繰り返していたので無駄な労力が発生した。

結局、基盤2つ、pro micro4つ犠牲に。 新しい基盤とpro microを追加で注文して組み立ててみるとあら不思議、完全に機能するlet’s splitの姿が。

分解に問題があるとはわかったが、何故最初はダメだったのかイマイチわからずじまい。多分半田付け漏れか、半田が足りてなかったのだろう。

躓きやすい所

公式ガイドや日本語圏のブログなどでも十分書かれているがいくつか注意点があるとすれば、

  • pro micro取り付け前にその裏にくるスイッチを取り付けて置かないと後からつけられない。
  • ヘッダーピンと基盤またはヘッダーピンとpro microは半田吸い取りしても取れない。無理に取ろうとするとランドも一緒に剥がれてしまう。私はこれで失敗した。諦めて、新しい基盤とpro microを用意した方が早いかもしれない。
  • ダイオードの向きをなんども確認した上で次の工程に進む。
  • pro micro初回書き込みはリセットボタンの押し方やタイミングなどでOS側に認識されるかどうか変わってくるので、諦めず試行錯誤。ls /dev/tty.usb*などで認識されているかどうかを確認できる。連打が良いという記載もあったが、OS側に認識されたことを確認したら連打をやめること。でないと書き込み中にリセットされてしまうので注意を。
  • 左右をつなぐケーブルは4極であるかどうか。3極でも少しハックするば動くらしいが4極が無難。
  • 最新版(2017-7-15)ではrev2fliphalfは必要なくmake rev2-KEYMAP-avrdudeを両方のpro microに書き込む。初回を終えたらキーマップを書き換えるたびにわざわざ両方のpro microに書き込む必要はない(確認不十分)

できたよ!

歓喜の瞬間)

さて、そんな苦労を重ねて作り上げた通称レツプリ、どれほどのものなのか。

キーマップ

キーマップについてはまだまだ試行錯誤中。 特に英数、かなについては今は小指の斜め下の位置に設定しているが打ち間違いが多い。 LT(LOWER, KC_LANG1/2)で親指位置に持っていけるかと思ったが、どうもLTのレイヤー切り替えが遅いので使い物にならない。MO(layer)なら問題ないのだけども。 他にもbackspaceとpの打ち間違い。 あとはcの打ちにくさ。cについてはレツプリは完全な格子型配列なので最適な位置よりも少し位置が下にあるように思える。これは自分が小さい頃左中指を怪我して指の形が少し変形していることに由来する。

キーマップについては随時変更していくのでレポジトリを参照。 keymap/kmtoki

マウス

qmk firmwareにはキーボードからマウスカーソルを操作する機能があり、それを多用している。 デフォルトではカーソルの動きがカクカクだったので、config.hに以下を追加したら解決した。

#undef MOUSEKEY_INTERVAL
#define MOUSEKEY_INTERVAL 0

#undef MOUSEKEY_TIME_TO_MAX
#define MOUSEKEY_TIME_TO_MAX 150

#undef MOUSEKEY_MAX_SPEED
#define MOUSEKEY_MAX_SPEED 3

#undef MOUSEKEY_MOVE_DELTA
#define MOUSEKEY_MOVE_DELTA 5

#undef MOUSEKEY_DELAY
#define MOUSEKEY_DELAY 0

リストレスト

あとはリストレストもどうするか。 現状ではErgodox ez純正の物を使っているが、レツプリで十分ergodoxを置き換えられているので、ezは売りに出したい。 その時リストレストもセットである必要があると思うので代価のものが必要になる。 幾つ候補があるので試してみたい。

まとめ

実用性としては結局のところ、なれ。 タイピングは身体で覚えるものであると思うので、練習が必要なものであるのは間違いなく、それを使いこなすには労力が必要な物。 それでもなお、これを使いこなしたいと思える魅力溢れた美しいキーボードであり、それを作り上げていく過程はとても良い体験だった。お世話になった皆様へ改めて感謝したい。