目次

PocketTerm35

概要

WaveShareからリリースされた、Raspberry Pi4/5を組み込む形で動作させる小型PCである

実測で重量432g

uConsoleが579gなので、75%程の重量となっている

Raspberry Pi4 2GBまたは Raspberry Pi5 1GBを組み込んだモデルと、Raspberry Piを含まないベアボーン的なキットがある

ベアボーン的なキットについてもリチウムイオンバッテリーパックを含むものと含まないものとがある

リチウムイオン電池が含まれる場合、輸入の際に空路が使えないので、時間が余分にかかることは考慮すべきであろう

電池パックについて

電池パックは、3.7Vのリチウムイオン電池のセルにPH2.0ヘッダをつければ1)なんでもいいようである

但し、極性が一般的なものとは逆になっていることがこちらで触れられている

もちろん、PH2.0ヘッダなので、引っこ抜いて逆にして圧着しなおせばいいのだけれど、最初から極性を逆にしているものも売られているので、容量も含め適切なものを用意したい

写真のように、左が赤(+)で、右が黒(-)になるように配線されていれば大丈夫である

電池としては3.7Vのリチウムイオン電池であればいいが、容量的にはキットに入っているという5000mAhのものでいいだろう デバイスの種別や、SSDの有無などで変わってくるとは思うが、わたしの場合で2h程度は余裕で運用できる感じだ

電源オフ

OSをシャットダウンしても、完全に電源が切れるわけではなく、システムはスタンバイ状態になる

この状態では本体上部の電池残量のLEDは点灯しており、Raspberry Pi5 もスタンバイ状態の赤LEDが点灯している

この状態から復帰するには、Raspberry Pi5を内蔵しているモデルなら、左側のシリコンボタンを押せばいい

ただ、スタンバイ状態からだと復帰がすごい速いかというとそうでもないうえに、そこそこ電池を食うのであまりこのモードの意義は感じない

この状態から完全に電源を切るには、本体上部の電源ボタンをダブルクリックすればいい

電池残量表示のLEDが消え、Raspnerry Pi5のインジケータも消える

このデバイスが向いている人・向かない人

向いている人

Linuxのターミナルとviなどのシンプルなエディタで生活できる人、オールドPCのエミュレータで遊びたい人には向いているといえる

これは電卓といえば電卓アプリを立ち上げたりせず、bc -l と入力したり irb と入力したりする、端末に閉じて生きられる人のためのデバイスである

その場合でも、Raspberry Piを含むキット2)はメモリが少なすぎてLinuxといえどデスクトップ環境を運用するのには向いていないので、Raspberry Piは自分で適切なモデルを買って入れるべきだろう

向かない人

画面サイズ640×480しかないので、汎用的なポータブルデバイスを欲しい人には向いていない

画面サイズの制約が大きく、ウェブブラウジングやメールの読み書きも実用的でないし、VSCodeのようなエディタすら運用するのが難しい

しかもキーボードが、サイズ的な制約で仕方ないこととはいえ、きわめて、標準的ではない配列となっている

ベースはUS配列なので、普段からUS配列を使っている人であればそれでもまだなじめる可能性がなくはないが、JIS配列でないとダメな人にはかなり厳しいだろう

いくつかクセつよポイントがあるが、非常に致命的なのはシフトキーが左にしかないことだろう

右親指でシフトキーを押して、左親指で他のキーを全部カバーすることになるかと思うが、ちょっと癖が強すぎて泣けてくる

またサーバとして使用することを考えている場合も全く向いていない

サーバならば、リモートからアクセスできるようにしてあるのが普通で、ローカルにキーボードとモニターを常備しておく必要はないし、であるならば、もっと廉価なケースがあるのだ

なのでサーバにしたいなら、シンプルなケースに入れて、モニターもキーボードもなしで運用する方がいいだろう

ゲームはできなくはないが、Raspberry Pi4/5のGPUは決してパワフルなものではないので、3Dでごりごり動くようなゲームコンソールとして使いたいというのであれば、それは無理

Steam は動くのでカジュアルなゲームであれば十分に遊べるのだが

一応、Ubuntu なりの Linux ディストリビューションはおおむね ARM64だろうが x64だろうが動くは動く

物好きな向きなら Windows11さえインストールすること自体は可能だろう

OSとそれに付随するパッケージだけで暮らすのであれば問題はないが、サードパーティ製のパッケージやアプリを期待していると ARM64であることがデメリットとなりうる

たとえば、Google Chromeや Slackなどのアプリは x64版は存在しているが AMR64版は存在しない

Chromiumやブラウザ版の Slackで代替するしかない

また、Hazkey などのオープンソースのものでさえ、公式が提供しているのは x64版だけで ARM64版が欲しければ自力でビルドする以外に方法はない

少なくとも自力でこういったものをビルドできる程度の知識と経験を持っているか、それらを今から習得したいという熱意のあるユーザでないと、この端末、というか Raspberry Piを使うのには向いていない

そいうのがいやだなあ、と思うなら x64のSBCなり小型PCなりを検討するべきだ

何なら値段もうっかりするとそっちの方が安かったりもするし

設定など

組立て

組立てはそれほど難しくはない

液晶、キーボード、そしてシステム基板などはすべて組付けられた状態になっており、やることは基板と組み込む Raspberry Piとを接続し、バッテリーを接続するだけのことである

動画付きの解説があるので、その通りにやればいい

ねじは付属のドライバーで開け閉めできるし、プラグもフレキ以外は刺さる向きにしか刺せない

USB用の内部配線は解説ページとケーブル色が違う場合もあるが、刺さるように刺せばいいので色は気にしなくていい

スピーカーケーブルも刺さる向きで刺せばいい

逆に電池だけは色をしっかり確認すること

フレキは端子面(金色の側)がコネクタに対して下を向くように差し込めばいい

裏ブタは結構タイトで一番の難易度だと思う

特にPi5の場合はシリコンゴムのボタンを側面につけるのだが、これは本体に固定されるわけではなく、単にそこにはまっているだけで少し傾けただけでずれる、落ちるなので、裏ブタを取り付けるときにはボタンのある側面を下にして横向きで作業するといいだろう

このボタンは見た目にちゃんとついているようでもわずかにずれていて、Pi5側のボタンを押し込んだ状態になっていたりするので、裏ブタを閉めた後で、押し込んできちんとボタンの押下、解放ができているか確認すること

なお、写真にあるように、Pi5に純正ヒートシンクをマウントして、その上に M2 HAT+を乗せてM.2 2230のNVMe SSDを乗せた状態でもきちんとケースに収まるので、SSD運用を考えている場合はこの組み合わせなら大丈夫ということを記しておく3)

ソフトウェア設定

基本的にほとんどそのままのRaspberry Pi向けのOSが動くが、タッチパネル周りだけは DTBOを持ってきて入れたうえで、config.txtに追記する必要がある

ダウンロードリンクはPocketTerm35のWikiにある

ここで、config.txtの修正やDTBOファイルの配置について書かれているが、すべてのディストリビューションで当てはまるわけでもないようなので、適宜自分の環境に合わせて適用してほしい

以下は Ubuntu26.04LTSの場合である

config.txt は /boot の下、またはWindowsなどでTFカードをマウントして編集する場合には、TFカードのルートフォルダにある

内容はOSによって微妙に異なるが、要はpi4には waveshare-35dpi-4bを pi5には waveshare-35dpi-5bをそれぞれdtoverlayに指定してやればOKである

waveshare-35dpi-[345]b.dtbo は /boot/firmware/current/overlay の下においてやればいい

カーネルなどの更新を行った場合、新しく /boot/firmware/new/overlayが作られ、その下には waveshare-35dpi-[345]b.dtbo はないので、改めてこれらのファイルをコピーする必要がある

リブートする前なら /boot/firmware/new/overlay の下に、リブート後なら /boot/firmware/current/overlayの下にそれぞれコピーする4)

config.txt
[all]
os_prefix=current/
 
[tryboot]
os_prefix=new/
 
[all]
arm_64bit=1
kernel=vmlinuz
cmdline=cmdline.txt
initramfs initrd.img followkernel
 
# Enable the audio output, I2C and SPI interfaces on the GPIO header. As these
# parameters related to the base device-tree they must appear *before* any
# other dtoverlay= specification
dtparam=audio=on
dtparam=i2c_arm=on
dtparam=spi=on
dtparam=watchdog=on
 
# Comment out the following line if the edges of the desktop appear outside
# the edges of your display
disable_overscan=1
 
# If you have issues with audio, you may try uncommenting the following line
# which forces the HDMI output into HDMI mode instead of DVI (which doesn''t
# support audio output)
#hdmi_drive=2
 
# Enable the KMS ("full" KMS) graphics overlay, leaving GPU memory as the
# default (the kernel is in control of graphics memory with full KMS)
dtoverlay=vc4-kms-v3d
disable_fw_kms_setup=1
 
# Autoload overlays for any recognized cameras or displays that are attached
# to the CSI/DSI ports. Please note this is for libcamera support, *not* for
# the legacy camera stack
camera_auto_detect=1
display_auto_detect=1
 
# Config settings specific to arm64
dtoverlay=dwc2
 
[pi5]
max_framebuffers=2
arm_boost=1
dtoverlay=waveshare-35dpi-5b
dtoverlay=dwc2,dr_mode=host
 
[pi4]
max_framebuffers=2
arm_boost=1
dtoverlay=wavewshare-35dpi-4b
dtoverlay=dwc2,dr_mode=host
 
[pi3+]
# Use a smaller contiguous memory area, specifically on the 3A+ to avoid an
# OOM oops on boot. The 3B+ is also affected by this section, but it shouldn''t
# cause any issues on that board
dtoverlay=vc4-kms-v3d,cma-128
 
[pi02]
# The Zero 2W is another 512MB board which is occasionally affected by the same
# OOM oops on boot.
dtoverlay=vc4-kms-v3d,cma-128
 
[cm4]
# Enable the USB2 outputs on the IO board (assuming your CM4 is plugged into
# such a board)
dtoverlay=dwc2,dr_mode=host
 
[all]

電流不足といわれたら

5V 5Aが供給できない、というような警告が出る場合がある

そんな場合は、本家のFAQにあるように、rpi-eeprom-configを使って、最大電流量に5000mAを指定してやればいい

$ sudo rpi-eeprom-config --edit

とすると、nanoが起動して、EEPROMの設定が表示される

既にPSU_MAX_CURRENTの項目があれば値を5000に、なければ末尾に

PSU_MAX_CURRENT=5000

のように、追記して保存してnanoを終了する5)

あとは再起動すれば、電流量不足の警告は出なくなるはずである

アプリなど

Ubuntu26.04LTS

OSとして Ubuntu 26.04LTSを選択した

AIなどに聞けば「その画面サイズなら LUbuntu 使えば?」なんていわれるが、今、デスクトップ環境は X11から Waylandへの移行が進んでおり、Ubuntu 26.04LTS上の GNOMEは Waylandに完全移行しており、X11ベースのLUbuntuではパッケージの一部で問題が起きるかもしれないし起きないかもしれない

そういうめんどくさい思いを抱えながら運用するくらいなら、Ubuntuでいいんじゃないか? ということで Ubuntuにした

ドック6)は「自動的に隠す」設定にしておくことで邪魔にならない

必要に応じて [CMD] キーを押せば呼び出せるので実運用上は全く問題にならない

普段は隠れてて、必要な時にだけ出てくるなら、LUbuntuのLXDEよりGNOMEのドックの方がいい

何故なら、LXDEのバーは画面に占める割合が小さい分、ボタンやメニューも小さいのだ

指タップを前提にするなら、むしろGNOMEドックの大きさがちょうどいいくらいなのだ

DevTerm を買った頃は Armbian ベースのUbuntuは設定アプリのUIが、縦480pxに全く収まっておらず、非常に不便だったのだが、純正の設定アプリのほとんどは縦が480pxでも収まるようにデザインが進化しており、Fcitx5など一部の設定以外では特に困ることはなかったことも追記しておく

GNOME Terminal

Ubuntuデスクトップを入れたら勝手に入ってくる端末ソフト

特にすごい機能もなければ、足りない機能もないというような、標準的なGNOMEの端末ソフト

しかし、これが PocketTerm35を使う上で中心となるもの

古くからのX11ユーザはテキストを選択しただけでコピーバッファに入ることを想定しているかもしれないが、Ctrl+C を押さないと入らない7)

vim

いわずと知れた UNIX系システムの鉄板スクリーンエディタ

コマンドモードとエディットモードとを行き来しながらファイルを編集する

このエディタのキモは正規表現を使った強力な検索と編集の機能や一括大量削除などの繰り返し処理にあり、新規にファイルを書き起こすよりも、既存のファイルを修正したりするのに威力を発揮する

大体どのディストリビューションでも当たり前の顔して入っているので、使い方を覚えておけば、UNIX系システムでトラブル対応する際などに力を発揮すること間違いなし

カーソルの移動をコマンドモード内で h, j, k, lで行うのに慣れるには、RogueやNetHackをプレイすればいいが、やりすぎて、u,i,n,m で斜めに移動しそうになったりすると末期的である

Microsoft Edit

マイクロソフト謹製の端末ソフト用スクリーンエディタ

Rustで書かれていて、サクサク動いて、viはちょっととつきにくいなあという人にはお勧め

GitHubにて公開されているソースを持ってきてビルドするだけ

nanoでいいんじゃないのか?

という声が聞こえてきそうではあるが、Windowsでも全く同じインターフェイスで使えるので、あっちこっちで微妙に違う、という問題に遭遇しないで済みそうという点でアドバンテージがある

$ sudo apt install -y rustup
$ rustup toolchain install stable
$ git clone https://github.com/microsoft/edit.git
$ cd edit
$ cargo build --release

Codex

今どきの端末でAIなしではいられない

とはいえ、GUIベースでは画面が小さすぎて死んでしまう

ということで、OpenAI謹製の CLI である codexを使う

多くの情報があるので、今更感もあると思うが、tmuxをインストールしておくと、tmuxのセッションを経由して、codexが直接端末を操作してくれたりするので、その便利さたるや、GUIを凌駕しているといっても過言ではない

インストールは次のコマンドで

$ curl -fSSL https://chatgpt.com/codex/install.sh | sh
$ . ~/.bashrc
$ codex

インストール後、初回起動時に、ChatGptのアカウントでのログインを求められる

ブラウザが開くので、この初回の部分だけGUIを使うがあとは基本的に端末の中に閉じる

なお、会話を続けたい場合は codex resumeで起動すれば、以前の会話のリストから選んで復帰できる

Claude Code

AnthropicのAIである

使い方の点では codexとは大きな違いはない

インストールも大体似たようなものである

$ curl -fsSL https://claude.ai/install.sh | bash
$ . ~/.bashrc
$ claude

世間に大きな衝撃を与え、米政府によって禁止されてしまったFable5こそ現状では使えないが、Opus4.8なども使えるので、codexよりパワフルかもしれない

なお、こちらのCLIは、Claude Pro以上のプランを購入しないと使えないので、無料でどうにかしたい勢は codex一択となる

Quasi88

NEC PC-8801のエミュレータ

入手はこちらから

PC-8801のエミュレータは吐いて捨てるほど存在しているのだが、Quasi88が PocketTerm35 用としては一歩抜き出ている部分がある

それは 0.7以降で追加されたツールバーの存在である

タッチ操作を前提として考えたときに、このツールバーは非常に強力なのである

フルスクリーンモードで 640×480の画面にぴったり収まるサイズも素晴らしい

PC6001VX

NEC PC-6001シリーズのエミュレータ

入手はこちらから

ビルドにはQt6 が必要であるが、Ubuntu 26.04LTSには Qt6.10.2のパッケージがあるので特に問題はないと思われる

このエミュレータの秀逸なところは、キーマップが、キーボードに従うということである

何をいっているのかというと、多くの日本製PCのエミュレータはJISキーボードを前提としていて、キー配列がUS配列と合致しないのだ

具体的には Shift + 2を押すと '“'が入力されるというような話

まあ、単にずれているだけなら大きな問題ではないが、JIS配列の方がキーが多い関係で、マッピングが存在しない文字がエミュレータによって発生することがあるのだが、PC6001VXは刻印通りのキーが渡されるため、入力できない文字のようなものがない

PocketTerm35は変態キー配列だがそのベースは当たり前のように英字配列だし、いくつかのキーはFnキーとのコンビネーションとなるため、エミュレータ側の対応いかんでは何が入力されるのか分かったものではないのだ

なので、この実装は非常にありがたいのである

$ git clone https://github.com/eighttails/pc6001vx
$ cd pc6001vx
$ qmake6 PC6001VX.pro
$ make -j4

OpenMSX

MSXエミュレータ

入手はこちらから

ビルドはできるし動作もするが、このエミュレータで遊ぶために必須となるのが「マウス」である

洗練されたメニューバーは細く、指でタップしようとしてもうまくタップできないのだ

また、メニューを非活性化しないとMSX側を操作できないが、この操作はマウスの右クリックに割り当てられているので、指だけでやるのはなかなか難しい

マウスでクリックすることで問題なく使えるので、マウスを接続する前提でならPocketTerm35でも遊ぶことができる

逆にマウスがないと画面を眺めるだけに近い状態になってしまう

NP2改

PC-9801/9821エミュレータ

入手はGitHubから

BIOSやフォントは ~/.config/wxnp21kai の下に bios.rom, bios9821.rom, font.bmp のような名前で置く

すべて小文字で置くこと

bios9821.romは PC-9821から持ってくる場合のみ

font.bmpはなければ font.tmpというファイルが生成されてそれを使うのでなければないでなんとかなる

なんならBASICを使わないなら、NECチェックを外せるなら、bios.romも多分いらない

最新版はWX Widgetsを使用しているがα版の位置づけのようである

とりあえず git で持ってきたらビルドする

$ git clone https://github.com/AZO234/NP2kai
$ cd NP2kai
$ cmake -S . -B build_wx -DBUILD_WX=ON -DUSE_NETWORK=ON -G Ninja
$ cmake --build build_wx -j4

ただし、このままビルドすると多分普通の人には使い物にならないものが出来上がる

キーマップが正しくないのだ

最初US配列との食い合わせが悪いのかと思ったが、さにあらず wx/kbtrans.cpp の中にあるASCII文字列のテーブルが全くどのキーボードともあってないのだ

なので、とりあえず、それっぽく動くようにマッピングを修正した

パッチを下に置くので、適宜パッチをあてるなどして使ってもらえればいいと思う

wxkbtrans.diff
--- wx/kbtrans.cpp.orig 2026-06-12 12:02:45.278737976 +0900
+++ wx/kbtrans.cpp      2026-06-12 21:45:49.464169487 +0900
@@ -33,14 +33,14 @@
        { WXK_F8,         0x69, 0x69 },
        { WXK_F9,         0x6a, 0x6a },
        { WXK_F10,        0x6b, 0x6b },
-       { WXK_F11,        0x73, 0x73 }, /* VF1 */
-       { WXK_F12,        0x74, 0x74 }, /* VF2 */
+       { WXK_F11,        0x52, 0x52 }, /* VF1 */
+       { WXK_F12,        0x53, 0x53 }, /* VF2 */
        /* Special keys */
        { WXK_ESCAPE,     0x00, 0x00 },
        { WXK_RETURN,     0x1c, 0x1c },
        { WXK_BACK,       0x0e, 0x0e },
        { WXK_TAB,        0x0f, 0x0f },
-       { WXK_SPACE,      0x35, 0x35 },
+       { WXK_SPACE,      0x34, 0x34 },
        /* Cursor keys */
        { WXK_UP,         0x3a, 0x3a },
        { WXK_DOWN,       0x3d, 0x3d },
@@ -64,10 +64,10 @@
        { WXK_NUMPAD_DIVIDE,   0x3e, 0x3e },
        { WXK_NUMPAD_DECIMAL,  0x3a, 0x3a },
        /* Insert/Delete/Home/End/PageUp/PageDown */
-       { WXK_INSERT,     0x3e, 0x3e },
-       { WXK_DELETE,     0x46, 0x46 },
-       { WXK_HOME,       0x3f, 0x3f },
-       { WXK_END,        0x3d, 0x3d },
+       { WXK_INSERT,     0x38, 0x38 },
+       { WXK_DELETE,     0x39, 0x39 },
+       { WXK_HOME,       0x3e, 0x3e },
+       { WXK_END,        0x3f, 0x3f },
        { WXK_PAGEUP,     0x36, 0x36 },
        { WXK_PAGEDOWN,   0x37, 0x37 },
        /* Modifiers */
@@ -80,18 +80,18 @@
 /* ASCII key mappings (0x20-0x7e) for 106-key layout */
 static const UINT8 ascii_to_pc98_106[0x60] = {
 /*       0     1     2     3     4     5     6     7     8     9     A     B     C     D     E     F */
-/* 20 */ 0x35, NC,   NC,   NC,   NC,   NC,   NC,   0x27, /* ' ( ) ... */
-/* 28 */ NC,   NC,   NC,   NC,   NC,   0x0b, NC,   0x0c, /* - . / */
+/* 20 */ 0x34, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, /* ' ( ) ... */
+/* 28 */ 0x08, 0x09, 0x0a, 0x0b, 0x30, 0x0b, 0x31, 0x32, /* - . / */
 /* 30 */ 0x0a, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
-/* 38 */ 0x08, 0x09, 0x26, NC,   NC,   NC,   NC,   NC,
-/* 40 */ 0x1a, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,
-/* 48 */ 0x24, 0x17, 0x25, 0x24, 0x25, 0x23, 0x22, 0x18,
-/* 50 */ 0x19, 0x10, 0x13, 0x1e, 0x14, 0x16, 0x2a, 0x11,
-/* 58 */ 0x2b, 0x15, 0x29, 0x1b, 0x0d, 0x28, 0x0c, NC,
-/* 60 */ NC,   0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,
-/* 68 */ 0x24, 0x17, 0x25, 0x24, 0x25, 0x23, 0x22, 0x18,
-/* 70 */ 0x19, 0x10, 0x13, 0x1e, 0x14, 0x16, 0x2a, 0x11,
-/* 78 */ 0x2b, 0x15, 0x29, NC,   NC,   NC,   NC,   NC,
+/* 38 */ 0x08, 0x09, 0x27, 0x26, 0x30, 0x0b, 0x31, 0x32,
+/* 40 */ 0x1a, 0x1d, 0x2d, 0x2b, 0x1f, 0x12, 0x20, 0x21,
+/* 48 */ 0x22, 0x17, 0x23, 0x24, 0x25, 0x2f, 0x2e, 0x18,
+/* 50 */ 0x19, 0x10, 0x13, 0x1e, 0x14, 0x16, 0x2c, 0x11,
+/* 58 */ 0x2a, 0x15, 0x29, 0x1b, 0x0d, 0x28, 0x0c, 0x33,
+/* 60 */ 0x1a, 0x1d, 0x2d, 0x2b, 0x1f, 0x12, 0x20, 0x21,
+/* 68 */ 0x22, 0x17, 0x23, 0x24, 0x25, 0x2f, 0x2e, 0x18,
+/* 70 */ 0x19, 0x10, 0x13, 0x1e, 0x14, 0x16, 0x2c, 0x11,
+/* 78 */ 0x2a, 0x15, 0x29, 0x1b, 0x0d, 0x28, 0x0c, NC,
 };
 
 static bool key_states[0x100];

なお、このキーマップだと PocketTerm35のキーボードでは':'が入力できない

0x20 の行を以下のようにすればUS配列で一般的に”;“の右隣にある ”'“で”:“が入力できるようになる

/* 20 */ 0x34, 0x01, 0x27, 0x03, 0x04, 0x05, 0x06, 0x27, /* ' ( ) ... */

MZ800 Emulator

Linuxでも動くMZ800のエミュレータで、2.0.xよりMZ-1500/MZ-700のエミュレータもビルド可能となった

旧バージョンのようにMZ-700のROMにパッチあてしたものを与えなくても使用可能である

ただし、2.0.4-previewの時点では、ビルドはできてもクラッシュする

codexに調査させてパッチをあてたものならば動いた

ソースはGitHubにて公開されている

パッチをあててビルドすると、mz800emu, mz700emu-ntsc, mz700emu-pal, mz1500emuの四種類ができる

mz700emu-ntsc だけ試してみてあるが、ほかも動くのではないかと思う

なお、ROMは日本語版にしないとキーボードもフォントもおかしな状態になるので注意

$ git clone https://github.com/michalhucik/mz800emu.git
$ cd mz800emu
$ patch -p1 <mz800emu.diff
$ cmake -B build -G Ninja
$ cmake --build build -j4
mz800emu.diff
diff --git a/src/iface-video/sdlapp/sdlapp_imgui_video.cpp b/src/iface-video/sdlapp/sdlapp_imgui_video.cpp
index 64f2e4e..5508c36 100644
--- a/src/iface-video/sdlapp/sdlapp_imgui_video.cpp
+++ b/src/iface-video/sdlapp/sdlapp_imgui_video.cpp
@@ -41,7 +41,7 @@
 typedef struct SdlappMyImGuiVideo_t
 {
     SDL_Surface *surface;
-    SDL_TimerID update_title_timer;
+    guint update_title_timer;
     Uint64 lastPictureTime;
     int TARGET_FPS;
     int FRAME_TIME;
@@ -153,14 +153,18 @@ static void sdlapp_myimgui_video_set_colormap(uint32_t *colormap)
     video_sdl3_set_surface_colormap(colormap, g_sdlapp_myimgui_video->surface);
 }
 
-static Uint32 sdlapp_mygui_video_update_status_line(void *userdata, SDL_TimerID timerID, Uint32 interval)
+static gboolean sdlapp_mygui_video_update_status_line(guint timer_id, gpointer user_data)
 {
-    (void)timerID;
-    SDL_Window *window = (SDL_Window *)userdata;
+    (void)timer_id;
+    SDL_Window *window = (SDL_Window *)user_data;
+    if (!window)
+    {
+        return FALSE;
+    }
     char *title_text = iface_video_create_window_title_text();
     SDL_SetWindowTitle(window, title_text);
     g_free(title_text);
-    return interval;
+    return TRUE;
 }
 
 static gboolean sdlapp_myimgui_video_init(void)
@@ -308,7 +312,7 @@ static gboolean sdlapp_myimgui_video_init(void)
     // protože SetForegroundWindow selže když foreground drží konzole)
 
     // Nastavení titulku okna
-    g_sdlapp_myimgui_video->update_title_timer = SDL_AddTimer(IFACE_VIDEO_UPDATE_WINDOW_TITLE_INTERVAL_MS, sdlapp_mygui_video_update_status_line, win->sdl_window);
+    g_sdlapp_myimgui_video->update_title_timer = sdlapp_add_timer(g_sdlapp, IFACE_VIDEO_UPDATE_WINDOW_TITLE_INTERVAL_MS, sdlapp_mygui_video_update_status_line, win->sdl_window);
 
     sdlapp_myimgui_video_reset_fps_timer();
 
@@ -326,7 +330,8 @@ static void sdlapp_myimgui_video_exit(void)
     {
         if (g_sdlapp_myimgui_video->update_title_timer)
         {
-            SDL_RemoveTimer(g_sdlapp_myimgui_video->update_title_timer);
+            sdlapp_remove_timer(g_sdlapp, g_sdlapp_myimgui_video->update_title_timer);
+            g_sdlapp_myimgui_video->update_title_timer = 0;
         };
 
         if (g_sdlapp_myimgui_video->surface)
@@ -337,6 +342,14 @@ static void sdlapp_myimgui_video_exit(void)
         g_free(g_sdlapp_myimgui_video);
         g_sdlapp_myimgui_video = NULL;
     };
+    if (!headless)
+    {
+        if (g_gui && g_gui->win && g_gui->win->sdl_window && g_gui->win->gl_context)
+        {
+            SDL_GL_MakeCurrent(g_gui->win->sdl_window, g_gui->win->gl_context);
+        }
+        imgui_images_destroy_all();
+    };
     if (headless && g_gui)
     {
         /* Stub g_gui byl alokovaný v init headless větvi - free, ne destroy
@@ -344,11 +357,6 @@ static void sdlapp_myimgui_video_exit(void)
         g_free(g_gui);
     };
     g_gui = NULL;
-    sdl3_backend_video_quit();
-    if (!headless)
-    {
-        imgui_images_destroy_all();
-    };
 }
 
 static SDL_Window *getSDL_Window_by_name(const char *name)
diff --git a/src/iface/iface_video.c b/src/iface/iface_video.c
index 278f66f..a040743 100644
--- a/src/iface/iface_video.c
+++ b/src/iface/iface_video.c
@@ -107,6 +107,15 @@ void iface_video_exit(void)
     if (!g_iface_video)
         return;
 
+    if (g_iface_video_callbacks->exit)
+    {
+        g_iface_video_callbacks->exit();
+    }
+    else
+    {
+        WARN("Exit callback is not set!\n");
+    };
+
     if (g_iface_video->redraw_full_screen_request_rwlock)
     {
         APP_RWLOCK_DESTROY(g_iface_video->redraw_full_screen_request_rwlock);
@@ -129,15 +138,6 @@ void iface_video_exit(void)
 
     g_free(g_iface_video);
     g_iface_video = NULL;
-
-    if (g_iface_video_callbacks->exit)
-    {
-        g_iface_video_callbacks->exit();
-    }
-    else
-    {
-        WARN("Exit callback is not set!\n");
-    };
 }
 
 char *iface_video_create_window_title_text(void)
diff --git a/src/ui-imgui/bootstrap/myimgui.cpp b/src/ui-imgui/bootstrap/myimgui.cpp
index f1cbd30..9ae5b44 100644
--- a/src/ui-imgui/bootstrap/myimgui.cpp
+++ b/src/ui-imgui/bootstrap/myimgui.cpp
@@ -260,6 +260,11 @@ static gboolean myimgui_init_for_sdl3_opengl(SdlAppWindow *win, gpointer user_da
         SDLAPP_ERROR("Failed to get GL context");
         return false;
     };
+    if (!SDL_GL_MakeCurrent(sdl_window, gl_context))
+    {
+        SDLAPP_ERROR("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
+        return false;
+    };
 
     // Setup Dear ImGui context
     IMGUI_CHECKVERSION();
@@ -597,6 +602,11 @@ void myimgui_render_cb(SdlAppWindow *win, gpointer user_data)
     }
     else if (win->renderer_type == SDLAPP_RENDERER_OPENGL)
     {
+        if (!SDL_GL_MakeCurrent(win->sdl_window, win->gl_context))
+        {
+            SDLAPP_ERROR("SDL_GL_MakeCurrent failed: %s", SDL_GetError());
+            return;
+        };
         ImGui_ImplOpenGL3_NewFrame();
     }
     else
@@ -665,6 +675,10 @@ void myimgui_destroy_cb(SdlAppWindow *win, gpointer user_data)
     }
     else if (win->renderer_type == SDLAPP_RENDERER_OPENGL)
     {
+        if (win->sdl_window && win->gl_context)
+        {
+            SDL_GL_MakeCurrent(win->sdl_window, win->gl_context);
+        }
         ImGui_ImplOpenGL3_Shutdown();
     }
     else

Steam

ARM64 UbuntuでSteamとか、って思っているかもしれないが、ゲームコンソールっぽいボタンもついているのだから、試してみる

もちろん、っぽいだけで、現状、このボタンはただのカーソルキーと英字キーだ

なので、すべてのゲームが快適に遊べるわけではない

それでも、それなりのキーアサインのゲームなら問題なく遊べる

インストールは snapから

$ sudo snap install steam

このスクリーンショットは日本語になっているが、設定をただ「日本語」にしてもフォントがないのか、□が表示されていた

原因をまじめに突き止めてもよかったのだが、codexを起動して「steamの設定を日本語にしたが、フォントがないのか□が表示されてしまう」とプロンプトしたらあっという間に設定をなおしてくれたので、どうやったのかはあまりちゃんと把握していないが、とにかく日本語が表示されるようになった

なので、同じトラブルに見舞われた人がいたら、codex8)に相談することをお勧めする

なお、テストプレイのために XeGrader plusを導入して遊んでみた

カーソルキーとスペースキーで操作できるので、この端末で遊ぶのにはもってこいだ

文字も十分可読サイズで、プレイするのには全く問題なかった

ただし、アクションゲームは(も)うまくはないので、ガチプレイに耐えられるかどうかは保証できない

起動時に、x64をARM64に変換するからなのか、起動時にはちょっと時間がかかるし、ウィンドウが消えている時間がそこそこ長いので本当に動いているか不安になるかもしれないが、大体動いているので、気長に待って吉である

Wireguard(VPN)

UbuntuならばWireguard VPNで自宅内へもアクセスしたい

TailscaleというフリーのVPNもあるので、自宅内にサーバおくとかめんどくさいな、という向きはそっちでもいいと思う

ただ、わたしは、多少管理の手間があってもWireguardの方がよかったと感じている

自宅内でホスティングしているwireguardの方がスループットが出るし、接続も安定している

Tailscaleだと、なんかしょっちゅうセッションが切れている印象

なので、設定してあるとめちゃ便利

設定については WireGuard を参照のこと

あると便利な周辺機器

マウス

基本的には大半のアプリをフルスクリーンで運用するので、あまり細かい作業はないため、大体のことは指タップで済む

が、一部のアプリ9)ではUIに細かいウィジェットが使われている関係で、指タップではうまく操作できなかったりする

そんなときはやはりマウスがあると便利である

携行することを考えれば、携行性が高いものがよいだろう

写真は、サンワサプライのペンマウスで、Bluetoothまたは 2.4GHzの無線でUSBドングル経由で接続できる

サイズ的にコンパクトで、PocketTerm35にちょうどいいと思ったので選んでみた

OpenMSXみたいなマウスがないと困るやつを操作するときにはもちろんだが、意外といいなと思ったのが、Terminal の上で文字列を選択してコピペする操作

X11などでおなじみの、左クリックしてマウスをぐりぐりっとドラッグして文字列を選択し、それを中ボタンでペーストするアレである

GNOME Terminalの場合は上でも書いたが、「コピー(Ctrl+C)」「ペースト(Ctrl+V)」になるが、選択は左クリックしながらのドラッグになる

この操作が、このペンマウスの場合は、カーソルを文字列の開始点に合わせたら、ペン先を押し込んで、そのままラインマーカで線を引くように動かすことになる

この動作がことのほか良い感じでちょっと感心した

選択したら右クリックして出てきたメニューから「コピー(Ctrl+C)」を選べばいい

ただ、逆に、1ワードだけ選択するときに使うダブルクリックはこのペンマウスだとちょっとやりにくいということも併せて記しておく10)

どちらを多用するのかによっては使い勝手の評価が分かれるのではないかと思う

キーボード

キー配列が変態的なため本格的にタイプしたければ外付けのキーボードは選択肢としてあると思うが、実用性としては疑問である 理由は単純で、画面が640×480なうえ小さいため、外付けのキーボードを置いた状態で画面を見えるように置くのが難しいという物理的制約があるからである

そんな使い方するくらいなら、sshdを動かしておいて、別の画面の大きなちゃんとしたキーボードのついた端末からログインして作業する方がいいだろう

改善されるといい点

画面サイズ

そもそものこのデバイスのサイズ(幅)がRaspberry Piのそれで決まっているように見えるが、横幅を拡大し、より大型のディスプレイを搭載すればより実用性の高いデバイスになると思う

具体的には uConsoleと同程度の5inch 1280×720 が載っていれば、より汎用的に利用できたのにと思う

アクションキー

uConsoleや DevTermでもおなじみゲーム用と思しきキーが PocketTerm35にもあるが、これ、左側はただのカーソルキーだし、右側の A/B/X/Y、さらにはいかにもマウスの左右クリックを想起させるような L/Rボタンに至ってはそれぞれ、'A','B','X','Y','L','R' が割り当てられたただの英字キーである

ジョイパッドとして認識されているわけでもないため、ゲームやPCエミュレータからジョイパッドとしても使えなければ、ゲームで一般的に使われる英字キーでもないので、まったく利用価値がない

これはジョイパッドとして認識されるようにするか、最低でも、単純な英字キーとは別のものに割り当てるべきだろう これらの制御は RP2040で行われているようなので、ファームウェアを書き換えることで改善できると思われるので、今後の研究対象のひとつだと思っている

キーコードの割り当ては、RP2040側にインストールされている CircuitPythonにて行われている

背面のRP2040リセットのボタンをいい感じに連打していると、CIRCUITPYというUSBメモリデバイスが現れることがある11)ので、それが出てきたら、中の boot.pyや code.pyを変更したり、追加のライブラリを libフォルダーに入れたりしてカスタムすることができる

電池容量のOS側からの検出

電池容量や充電状態は画面上に搭載された四つの赤色LEDで表示されている

だが、OS側からアクセスできるようになっておらず、結果OS側では電池残量に応じた制御を行うことはできない

I2Cでもいいので、これが読み出せるようにしてあればいろいろできただろうにと、非常に残念

死にポートの存在

Raspberry Pi5はHDMIポートを二つ持っているが、一つを本体のモニタにつないで、もう一つは何もつながない状態でケースに封印されてしまっている

外付けのモニターをつなげられるようにすれば、移動中は極小デバイスとして、移動先で外部モニターやキーボードをつないでまあまあな性能のPCとして運用できればより利用価値が高まるのに残念である

ポーチがでかすぎる

ポーチ……でかすぎませんか?

このポーチで持ち運ぶ気にはちょっとなれない

諸々のこと

配送オプション

わたしは待ちきれない人なので、DHLの配送オプションにお金を支払った

6/8発送になって、当初6/11配送と出ていたが、6/9には国内に届き通関まですんで、結局は6/10に届いた

通常配送に比べて一週間程度早いようなので、この一週間に金を支払う価値をみいだせるなら悪いオプションではない

なお、DHLは配送日時の変更をおこなえるが、国内に届くタイミングまで待った方がいい

わたしの場合、最初変更が可能になった時点では6/11が選べる最速だったが、トラッキングで6/9に国内に入ったのを確認したところで改めて配送日時の変更をおこなったら、当初なかった6/10がオプションに現れた

なので、国内に届いたタイミング次第で前倒しできる可能性があるので、慌てて、変更可能になった時点で指定しない方がいいかもしれない

SSD

SSDに関してはWaveshareではPCIe TO M.2 Boardを推奨してるが、写真のように、純正ヒートシンク + M.2 Hat+ でもケースに収まる

ただし、純正ヒートシンクをマウントしたうえで、スペーサーをつけて M.2 HAT+を乗せる形になるので、GPIOピンへのアクセスはかなり悪くなる

また2230しか乗せられないので、SSDの選択肢も少なくなる

PCIe TO M.2 Boardは、ヒートシンク部分の高さが当然低く、SSDの真下にあたる部分の冷却12)が、純正ヒートシンクと比べてどのくらい良いのかで、SSDの温度に影響しそうな気もする

ただ、ケースとSSDとの間のスペースが大きいので、SSDの方に剣山状のヒートシンクを乗せればいいようにも思う

純正の場合は、写真のように、金属のプレート状のものを乗せるくらいしかできないのでどっちがSSD運用上有利なのかはわからない

キーマップ変更

ファームウェアのソースを公開して、って Waveshareに頼んだら、ファームウェアいじらないでもキーマップなら変えられるよ? ……と、いわれた

そう、PocketTerm35のファームウェアは Adafruit CircuitPythonで構成されているようだ

つまりは、できること・できないことは Adafruit CircuitPython次第ということだ

なお、この部分は大変に長くなるのでPocketTerm35のキーマップを変更するにまとめる

まず、作業を始める前に、スクリーンロックをOFFにするかUSBキーボードを用意する スクリーンロックがかかると、CIRCUITPYドライブはアンマウントされてしまうので、万一不具合がある状態だとキー入力ができず、スクリーンロックを解除できず、結果CIRCUITPYを修正できなくなってしまう なので、スクリーンロックをオフにするか、万一に備えてUSBの外付けキーボードを用意しておくことを推奨する やりたいことによって、code.pyを修正すれば済むのか、あるいはboot.pyを修正したり、ほかのライブラリをたしたりしないといけないのかが変わってくる === CIRCUITPYをマウントする === 実はこの操作は運任せである まあ、気分はわかるのだが、Waveshareは、boot.pyの中で、disable_usb_drive()しているので、リセットを押してboot.pyが走る前にいい感じのところで再度リセットがかかれば、CIRCUITPYが現れる この状態を逃さずに、バックアップを作成し作業用のコピーを作成する なお、CIRCUITPYが出たりでなかったりするのは不便なので、ある程度の作業が終わるまでは、boot.pyから``storage.disable_usb_drive()``の行をコメントアウトしてしまうといいかもしれない === キーボードが返すことができるキーコードの範囲で我慢する === 例えば、左シフトであるべき一にTABキーがあって間違って押しちゃうし、大体、右の方のキーとSHIFTを同時押しするのもめんどくさくてやだなあと思っていたとする このTABを普通におしたらシフト、Fnと押したらTABになるようにしたら便利そうだと思ったら、KEY_MAPという配列を書き換えればいい 具体的には、KEY_MAPの定義の row 5を下のように書き換えればいい Keycode.TABをKeycode.SHIFTに、Keycode.SHIFTを Keycode.RIGHT_SHIFTにそれぞれ置き換えただけである 右シフトをRIGHT_SHIFTにする必要があるかどうかはどうでもいいかもしれないが、これで動作がそのようになる 改造も小さくて、typoでもしない限りは、問題は起きないだろう <code python> [Keycode.SHIFT, Keycode.CAPS_LOCK, Keycode.MINUS, Keycode.EQUALS, Keycode.SEMICOLON, Keycode.QUOTE, Keycode.COMMA, Keycode.PERIOD, Keycode.RIGHT_SHIFT, None], # row 5 </code> === アクションキーはゲームパッドであるべきだ === ゲームパッドをHIDデバイスとして追加するのはそれほど難しくもないが、めんどくさくはある ゲームパッドは非常に多くの種類が存在してるのだが、PocketTerm35 にあるのは、四方向のデジタル方向キーと、A,B,X,Y,L,R, とSelect, Startの12ボタンになる 方向キーをD Hatとして実装するのか、アナログスティック13)として実装するのかは難しいところだが、手元にあったSNES風のパッドがアナログスティック実装していたのでそれに倣うことにする == ライブラリの準備 == hid_gamepad.pyをCIRCUITPYのlibフォルダーに放り込む このとき、15行目あたりにある adafruit_hidを hidに変更する <code python> from hid import find_device </code> 本来、CircuitPythonのhidのバンドルライブラリは adafruit_hidなのだが、PocketTerm35のものは hid になっているので、どちらかを変更しないといけないのだが、hidを adafruit_hidにすると code.pyの方に波及するので、ここでは追加ライブラリの hid_gamepad.pyの方を合わせてしまう == Gamepadデバイスの追加 == デバイスを追加する作業はboot.py に手を入れる必要がある ほとんど呪文みたいなものなので、このままコピペすればいいと思う <file python boot.py> import usb_hid # This is only one example of a gamepad report descriptor, # and may not suit your needs. GAMEPAD_REPORT_DESCRIPTOR = bytes14) gamepad = usb_hid.Device( report_descriptor=GAMEPAD_REPORT_DESCRIPTOR, usage_page=0x01, # Generic Desktop Control usage=0x05, # Gamepad report_ids=(4,), # Descriptor uses report ID 4. in_report_lengths=(6,), # This gamepad sends 6 bytes in its report. out_report_lengths=(0,), # It does not receive any reports. ) usb_hid.enable( (usb_hid.Device.KEYBOARD, usb_hid.Device.MOUSE, usb_hid.Device.CONSUMER_CONTROL, gamepad) ) </file> == コードを変更する == PocketTerm35のキーマップは、標準とFnキー押下時の二種類があり、キーマトリックスの配列の形で定義されている 単純にキーボードのキーコードが返ればいいものであれば、単純にキーコードを書いておけばいいが、何か特別な処理をさせたい場合には、CUSTOM_KEYSに負数のコードを定義し、CUSTOM_KEYS_FUNCTION でマッピングされた関数がキーが押されたときに呼ばれるというような動きになっている もともとのマッピングではこの動作でも問題はなかったが、ゲームパッド動作を割り当てようと思うと、押しっぱなし(オートリピート)がきかなくなってしまったため、CUSTOM_KEYS_FUNCTIONに関数ではなく、makeとbreakの二つの手続きのペアのタプルを登録できるようにし、キーの処理部でも、タプルが登録されていたら、離す処理を呼び出すように変更した ここまでの準備をして、code.pyを書き換えた まず、ゲームパッドを code.pyで扱えるように宣言する from hid_gamepad import Gamepad と gp = Gamepad(usb_hid.devices) を追加するのがこの部分で必要なことである <code python> import usb_hid from hid.keyboard import Keyboard from hid.keycode import Keycode from hid.consumer_control import ConsumerControl from hid.consumer_control_code import ConsumerControlCode from hid.keyboard_layout_us import KeyboardLayoutUS from hid_gamepad import Gamepad import board import microcontroller import digitalio import time import pwmio #PWM模块 bl_pwm_value = 32767 #中间值 ad_pwm_value = 32767 #中间值 try: # Set up a keyboard device. kbd = Keyboard(usb_hid.devices) layout = KeyboardLayoutUS(kbd) consumer_control = ConsumerControl(usb_hid.devices) gp = Gamepad(usb_hid.devices) </code> そして、カスタムのキーコード(負数)を追加する -112以降の “GAMEPAD_xxx”が追加されたシンボルである <code python> # Define custom function keys using a dictionary CUSTOM_KEYS = { # Add more custom keys as needed “FN_KEY” : -100, “FN_MUTE” : -101, “FN_VOLUME_DOWN” : -102, “FN_VOLUME_UP” : -103, “FN_LOCK_SCREEN” : -104, “FN_BL_CONTROL_SCREEN” : -105, “FN_BL_PWM_DOWN” : -106, “FN_BL_PWM_UP” : -107, “SHIFT_GRAVE_ACCENT” : -108, “SHIFT_BACKSLASH” : -109, “SHIFT_LEFT_BRACKET” : -110, “SHIFT_RIGHT_BRACKET” : -111, “GAMEPAD_THUMB” : -112, “GAMEPAD_THUMB2” : -113, “GAMEPAD_TRIGGER” : -114, “GAMEPAD_TOP” : -115, “GAMEPAD_BASE3” : -116, “GAMEPAD_BASE4” : -117, “GAMEPAD_TOP2” : -118, “GAMEPAD_PINKIE” : -119, “GAMEPAD_UP” : -120, “GAMEPAD_DOWN” : -121, “GAMEPAD_LEFT” : -122, “GAMEPAD_RIGHT” : -123 } </code> そして、カスタムキーが操作されたときに呼び出される処理のマッピングが以下である マッピングが関数のタプルになっているものは、押されたとき、離されたときの処理で、単純に関数だけが書かれているものは押されたときだけ呼び出される <code pyhotn> SPECIAL_KEY_FUNCTIONS = { CUSTOM_KEYS[“FN_MUTE”] : toggle_gp19, CUSTOM_KEYS[“FN_VOLUME_DOWN”] : ad_pwm_down, CUSTOM_KEYS[“FN_VOLUME_UP”] : ad_pwm_up, CUSTOM_KEYS[“FN_LOCK_SCREEN”] : lock_screen, CUSTOM_KEYS[“FN_BL_CONTROL_SCREEN”] : toggle_gp21, CUSTOM_KEYS[“FN_BL_PWM_DOWN”] : bl_pwm_down, CUSTOM_KEYS[“FN_BL_PWM_UP”] : bl_pwm_up, CUSTOM_KEYS[“SHIFT_GRAVE_ACCENT”] : shift_grave_accent, CUSTOM_KEYS[“SHIFT_BACKSLASH”] : shift_backslash, CUSTOM_KEYS[“SHIFT_LEFT_BRACKET”] : shift_left_bracket, CUSTOM_KEYS[“SHIFT_RIGHT_BRACKET”] : shift_right_bracket, Keycode.CAPS_LOCK : toggle_gp22, ConsumerControlCode.SCAN_PREVIOUS_TRACK : scan_previous_track, ConsumerControlCode.PLAY_PAUSE : play_pause, ConsumerControlCode.SCAN_NEXT_TRACK : scan_next_track, CUSTOM_KEYS[“GAMEPAD_THUMB”] : (gamepad_press_thumb, gamepad_release_thumb), CUSTOM_KEYS[“GAMEPAD_THUMB2”] : (gamepad_press_thumb2, gamepad_release_thumb2), CUSTOM_KEYS[“GAMEPAD_TRIGGER”] : (gamepad_press_trigger, gamepad_release_trigger), CUSTOM_KEYS[“GAMEPAD_TOP”] : (gamepad_press_top, gamepad_release_top), CUSTOM_KEYS[“GAMEPAD_BASE3”] : gamepad_btn_base3, CUSTOM_KEYS[“GAMEPAD_BASE4”] : gamepad_btn_base4, CUSTOM_KEYS[“GAMEPAD_TOP2”] : (gamepad_press_top2, gamepad_release_top2), CUSTOM_KEYS[“GAMEPAD_PINKIE”] : (gamepad_press_pinkie, gamepad_release_pinkie), CUSTOM_KEYS[“GAMEPAD_UP”] : (gamepad_press_up, gamepad_release_up), CUSTOM_KEYS[“GAMEPAD_DOWN”] : (gamepad_press_down, gamepad_release_down), CUSTOM_KEYS[“GAMEPAD_LEFT”] : (gamepad_press_left, gamepad_release_left), CUSTOM_KEYS[“GAMEPAD_RIGHT”] : (gamepad_press_right, gamepad_release_right), } </code> この変更を行ったために、メインループにも手を入れた <code python> # Main loop try: while True: current_keys = set(scan_keyboard()) # Handle keys that were released for key in previous_keys - current_keys: if key is not None and key >= 0: kbd.release(key) elif key is not None and key in SPECIAL_KEY_FUNCTIONS: if isinstance(SPECIAL_KEY_FUNCTIONS[key], tuple): (fun_make, fun_break) = SPECIAL_KEY_FUNCTIONS[key] fun_break() # Handle keys that are pressed for key in current_keys - previous_keys: if key is not None: if key in SPECIAL_KEY_FUNCTIONS: if isinstance(SPECIAL_KEY_FUNCTIONS[key], tuple): (fun_make, fun_break) = SPECIAL_KEY_FUNCTIONS[key] fun_make() else: SPECIAL_KEY_FUNCTIONS[key]() elif key >= 0: # 过滤掉负值键码 kbd.press(key) </code> キーマップは以下のようになる <code python> # Define the key map for the keyboard matrix KEY_MAP = [ #col0 col1 col2 col3 col4 col5 col6 col7 col8 col9 [CUSTOM_KEYS[“GAMEPAD_UP”], CUSTOM_KEYS[“GAMEPAD_LEFT”], CUSTOM_KEYS[“GAMEPAD_DOWN”], CUSTOM_KEYS[“GAMEPAD_RIGHT”], CUSTOM_KEYS[“GAMEPAD_TOP2”], CUSTOM_KEYS[“GAMEPAD_PINKIE”], CUSTOM_KEYS[“GAMEPAD_TRIGGER”], CUSTOM_KEYS[“GAMEPAD_TOP”], CUSTOM_KEYS[“GAMEPAD_THUMB2”],CUSTOM_KEYS[“GAMEPAD_THUMB”]], # row 0 [Keycode.ONE, Keycode.TWO, Keycode.THREE, Keycode.FOUR, Keycode.FIVE, Keycode.SIX, Keycode.SEVEN, Keycode.EIGHT, Keycode.NINE, Keycode.ZERO], # row 1 [Keycode.Q, Keycode.W, Keycode.E, Keycode.R, Keycode.T, Keycode.Y, Keycode.U, Keycode.I, Keycode.O, Keycode.P], # row 2 [Keycode.A, Keycode.S, Keycode.D, Keycode.F, Keycode.G, Keycode.H, Keycode.J, Keycode.K, Keycode.L, Keycode.BACKSPACE], # row 3 [Keycode.Z, Keycode.X, Keycode.C, Keycode.V, Keycode.B, Keycode.N, Keycode.M, Keycode.FORWARD_SLASH, Keycode.ENTER, None], # row 4 [Keycode.SHIFT, Keycode.CAPS_LOCK, Keycode.MINUS, Keycode.EQUALS, Keycode.SEMICOLON, Keycode.QUOTE, Keycode.COMMA, Keycode.PERIOD, Keycode.RIGHT_SHIFT, None], # row 5 [CUSTOM_KEYS[“FN_KEY”], Keycode.CONTROL, Keycode.LEFT_ALT, CUSTOM_KEYS[“GAMEPAD_BASE3”], Keycode.SPACE, CUSTOM_KEYS[“GAMEPAD_BASE4”], Keycode.RIGHT_ALT, Keycode.WINDOWS, CUSTOM_KEYS[“FN_KEY”], None] # row 6 ] </code> あとは、キーを押したり離したりの処理を書けば完成である カーソルキーはゲームパッドでもあり、キーボード上のカーソルキーでもあるので、押すと両方に対してコードを送信している もしこれで何か問題が起きるようであれば、キーマップの方を Keycode.UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROWに戻してしまえばとりあえず不具合は出ないと思われる <code python> def gamepad_press_thumb(): # A button gp.press_buttons(2) def gamepad_release_thumb(): gp.release_buttons(2) def gamepad_press_thumb2(): # B button gp.press_buttons(3) def gamepad_release_thumb2(): gp.release_buttons(3) def gamepad_press_trigger(): # X button gp.press_buttons(1) def gamepad_release_trigger(): gp.release_buttons(1) def gamepad_press_top(): # Y button gp.press_buttons(4) def gamepad_release_top(): gp.release_buttons(4) def gamepad_press_top2(): # L button gp.press_buttons(5) def gamepad_release_top2(): gp.release_buttons(5) def gamepad_press_pinkie(): # R button gp.press_buttons(6) def gamepad_release_pinkie(): gp.release_buttons(6) def gamepad_btn_base3(): # Select button gp.click_buttons(9) def gamepad_btn_base4(): # Start button gp.click_buttons(10) def gamepad_press_up(): # Send both Gamepad / Keyboard gp.move_joysticks(x=0, y=-127) kbd.press(Keycode.UP_ARROW) def gamepad_release_up(): gp.move_joysticks(x=0,y=0) kbd.release(Keycode.UP_ARROW) def gamepad_press_down(): # Send both Gamepad / Keyboard gp.move_joysticks(x=0, y=127) kbd.press(Keycode.DOWN_ARROW) def gamepad_release_down(): gp.move_joysticks(x=0,y=0) kbd.release(Keycode.DOWN_ARROW) def gamepad_press_left(): # Send both Gamepad / Keyboard gp.move_joysticks(x=-127, y=0) kbd.press(Keycode.LEFT_ARROW) def gamepad_release_left(): gp.move_joysticks(x=0,y=0) kbd.release(Keycode.LEFT_ARROW) def gamepad_press_right(): # Send both Gamepad / Keyboard gp.move_joysticks(x=127, y=0) kbd.press(Keycode.RIGHT_ARROW) def gamepad_release_right(): gp.move_joysticks(x=0,y=0) kbd.release(Keycode.RIGHT_ARROW) </code> boot.pyをいじれば、ゲームパッドなどもついかできそうだが、boot.pyを壊してしまったら CIRCUITPYドライブは現れない つまり結局ファームウェアを復元できる状態でないととてもじゃないが怖くていじれないというのが正解だ だから、code.pyの範囲でできることしかできない 背面のRESETボタンを押すと、CIRCUITPYというドライブが PocketTerm35上に出てくるので、その中にある code.pyを編集して、もう一回背面のリセットボタンを押すと、更新した code.pyを読み込んでキーボードが起動するので、新しいキーマップで使えるようになるよ とのこと が、この「RESETすればCIRCUITPYが見える」というのが曲者だった PiOSじゃなくてUbuntu26.04LTSだから悪いのか、そもそもそういうものなのか、RESETしたくらいでは何も出てこない ダブルクリックすると CIRCUITPYかRP2040のファームウェア更新用ドライブのどっちかが見えるので、CIRCUITPYが現れるまで繰り返すしかない そして、もう一つ問題が このCIRCUITPYがマウントされた状態では、内蔵キーボードが使えないのだ 外付けキーボード、またはマウスで操作しなければならない なお、必ず、code.pyのオリジナルを保存しておくこと 万一やらかしたときに回復できなくなってしまうので、絶対だ さて、code.pyだが、おおむねキーマトリックスがわかるようにキー定義が置かれている 但し、キーコードは adafruit hidでカバーされているキーコードしか返せない なるほど、だから L/R/A/B/X/Yに英字キーが割り当てられているわけだ adafruit hidで定義されているキーコードに、トリガーキーみたいなものはなく、また、ジョイパッドはカバーされていないので、これ以外にやりようがなかったのだろうと推測される せいぜい、F13-F18のファンクションキーでも割り当てて、本体側のソフトで対応するしかなさそうだ TABキーを左シフトにして、TABキーはなくして、CTRL+Iで代替するというのもありかもしれない L/Rにマウスの左クリック、右クリックを割り当てようと思ったが、それをやったらキー入力を受け付けなくなった 受け付けなくなったからか、CIRCUITPYが読み取り専用でマウントされるようになってしまった 強制的に RWモードでマウントしなおして保存しておいたオリジナルのcode.pyを書き戻した <code bash> $ sudo mount -o remount,rw /dev/sda1 $ cp -p code.py.orig /run/media/araki/CIRCUITPY </code> 今のところ、自前のcode.pyはうまく動かない デバッグなどできればいいのだが、Pythonはインデントなどに強く依存する変態言語なので、見た目大丈夫そうでも実は動かないとかあるので、本当に勘弁してほしい

1)
ケースに収まるサイズに限るが
2)
Paspberry Pi4 2GBまたはRaspberry Pi5 1GB
3)
もちろんほかの組み合わせでもできるものもあるだろうが、試してないので何とも言えない
4)
いずれも Ubuntu26.04LTSの場合
5)
Ctrl+X で変更内容を保存するか聞かれるので Y と答えて、ファイル名等はそのままENTERを押せば保存終了する
6)
画面左端のタスクバー的なもの
7)
入っているときもあるのでこの辺の挙動がちょっと謎だが
8)
claude(Opus4.8)は解決できなかったのでcodex一択である
9)
OpenMSXなど
10)
単に私が下手なだけで、練習したらできるようになりそうな気もする
11)
標準のboot.pyによって、CIRCUITPYが出ないようにされているので、いい感じでRESETを連打することで、boot.pyの実行がキャンセルされることがあるので、そのタイミングでCIRCUITPYが現れる。本当にいい感じで連打してとしかいいようがない
12)
エアフロー
13)
だが0か全開かだけ
14)
  0x05, 0x01,  # Usage Page (Generic Desktop Ctrls)
  0x09, 0x05,  # Usage (Game Pad)
  0xA1, 0x01,  # Collection (Application)
  0x85, 0x04,  #   Report ID (4)
  0x05, 0x09,  #   Usage Page (Button)
  0x19, 0x01,  #   Usage Minimum (Button 1)
  0x29, 0x0A,  #   Usage Maximum (Button 10) ... サンプルは16ボタンだが参考にしたSNES風ゲームパッドが10ボタンだったので
  0x15, 0x00,  #   Logical Minimum (0)
  0x25, 0x01,  #   Logical Maximum (1)
  0x75, 0x01,  #   Report Size (1)
  0x95, 0x10,  #   Report Count (16)
  0x81, 0x02,  #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0x05, 0x01,  #   Usage Page (Generic Desktop Ctrls)
  0x15, 0x81,  #   Logical Minimum (-127)
  0x25, 0x7F,  #   Logical Maximum (127)
  0x09, 0x30,  #   Usage (X)
  0x09, 0x31,  #   Usage (Y)
  0x09, 0x32,  #   Usage (Z)
  0x09, 0x35,  #   Usage (Rz)
  0x75, 0x08,  #   Report Size (8)
  0x95, 0x04,  #   Report Count (4)
  0x81, 0x02,  #   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
  0xC0,        # End Collection