====== PocketTerm35のキーマップを変更する ======
===== 概要 =====
[[https://www.waveshare.com/|WaveShare]]が提供する[[https://www.waveshare.com/catalogsearch/result/?q=PocketTerm35|PocketTerm35]]のキーボードコントローラはRP2040である
ファームウェアをカスタマイズすることで任意のキーマップを実装できる
そこで、サポートに、ファームウェアのソースを公開してほしいと連絡すると、[[https://circuitpython.org/|Circuit Python]]だから、code.pyを変更すればキーマップをカスタマイズできるよ、といわれた
なるほど、それならばそれでやってみよう
なお、Raspberry Pi Picoのファームウェアを変更できるだけの知識がない場合は、手を出さない方がいいと思う
また、ここに書かれていることに基づいてキーマップの変更などを試みて、PocketTerm35が回復不能なダメージを受けたとしても、当方は一切関知しない
この種の改変は、間違えば回復不能なダメージをPocketTerm35に与える可能性があることを念頭に、自己責任で行っていただきたい
===== CIRCUITPYをマウントする =====
Circuit Pythonのデバイスは、CIRCUITPYというUSBマスストレージとターミナルにつながるUSBシリアルデバイスを提供する
なので、ホスト端末からは CIRCUITPYが見えるはずだが、PocketTerm35には存在していない
サポート曰く「(RP2040の)リセットボタンを押せばみえるよ」とのことだが、リセットを押したとて出てこない
のちにわかるのだが、boot.pyのなかでCIRCUITPYを無効化しているので、基本的に出てこないのだ
以下が2026年6月到着のデバイスに入っていたデフォルトのboot.pyの内容である
import storage
sotrage.disable_usb_drive()
ではどうするのか
**いい感じにRESETボタンを連打する**
これしかないので、いい感じにRESETボタンを連打する
いいタイミングでRESETが入ると、boot.pyがキャンセルされるようで、CIRCUITPYがマウントされる
{{ :pocketterm35:code_py.png?400|}}
このとき、RP PI2がみえることがあるが、これはRP2040自体のファームウェアを変更するためのもので、このデバイスは操作しないこと
おそらくは、boot.pyではなく Circuit Python自体の起動がキャンセルされてしまうとこれが出てくるのだろうと推測しているが、確かなことはわからない
いえることはとにかくそれには触るな、である
なお、CIRCUITPYは、PocketTerm35本体からログインしていないと出現しない
リモートからsshでログインしているだけでは永遠に出てこないので、この部分だけは必ずローカルからログインした状態で操作すること
なお、このいい感じにしかCIRCUITPYが出てこないのはあまりに不便なので、この問題についても、改造の過程で解消していきたいと思う
===== 準備 =====
Circuit Pythonの改造は比較的難易度は低いが、不完全なコードをデプロイすると PocketTerm35のキーボードが一切効かないなどということになりかねないリスクがある
なので、必ず以下の準備をする
* CIRCUITPYのバックアップ
* USBキーボード
できれば以下のものもあるとよい
* PocketTerm35にsshでアクセスできる環境
* ファームウェアの回復用のuf2ファイルセット((Waveshareのサポートに頼むともらえるかもしれない。わたしはもらえた))
* 実験用のRaspbery Pi Pico (RP2040)
テスト中は頻繁にキーボードが使えなくなるのでsshでリモートからいじる方が楽である
boot.pyをいじる場合は、致命的な間違いを犯していないか確認する意味でも実験用のRaspberry Pi Picoがあると安心である
実験用のRaspberry Pi Picoがあると、キーを実装していなくても、シリアルコンソールで最低限の動作確認は行えるし、万一致命傷を与えても、Adafruitが配布している誰でも入手できる uf2ファイルで簡単に復旧できるので、Waveshareから回復用のファイルをもらえなかった場合にも安心である
実験機の上で最低限、致命的な問題が起きないことを確認してから、PocketTerm35に持ち込めば、いきなり文鎮化するような悲劇は回避できるだろう
===== キーマップの設計 =====
不便であると感じているのは以下の点
* 左シフトキーがない
* ゲームパッド風のキーがあるのにただ英字が割り当てられているだけでゲームパッドとして機能しない
* マウスがないと困る場面でマウスとして使える機能が欲しい
* CIRCUITPYの存在が不安定
の
なので、以下のようにキーマップを変更することにする
* TABキーは左シフトにして Fn+TABでTABキーとする
* 右シフトキーには右シフトを割り当てる(デフォルトでは左シフトになっている)
* Fn+上, Fn+下, Fn+左, Fn+右, Fn+L, Fn+Rはマウスカーソルの移動および左クリック、右クリックとする
* Select, Start は常時ゲームパッドの Select/Start にする
* 刻印通り Fn+SelectをPrint Screen、Fn+Start を Pauseとする
* Fn+BでカーソルキーとABXYLRのアクションキーをゲームパッドとしてふるまうようにする
* Fn+AでカーソルキーとABXYLRのアクションキーをデフォルトのカーソルキーとABXYLRの英字に戻す
* 左Fnキーを押したままRP2040をRESETすると CIRCUITPYが有効化される(デフォルトでは無効)
===== 依存するライブラリ =====
* adafruit_hid
* [[https://github.com/adafruit/Adafruit_CircuitPython_HID/blob/main/examples/hid_gamepad.py|hid_gamepad.py]]
デフォルトのファームウェアとして入っている Circuit Pythonは Adafruit hidを利用しているが、なぜかhidという名前になっている
互換性を鑑みて adafruit_hid と入れ替えたい
[[https://circuitpython.org/libraries|Circuit Python Libiraries]]からVersion 10.x用のライブラリを取得し、中にある adafruit_hid のフォルダを丸ごと CIRCUITPY/libの下にコピーする
hidは消してしまっても構わない
hid_gamepad.pyは[[https://github.com/adafruit/Adafruit_CircuitPython_HID|Adafruit CircuitPython_HID]]のexamplesの中にある
これを同じく CIRCUITPY/libにコピーしておく
===== コード =====
Circuit Pythonの制御は boot.pyと code.pyの二つで行う
boot.pyはデバイスの初期化に関する処理を、code.pyは動作中の処理を記述するファイルである
ここでは、
* CIRCUITPYを見せる・隠すの制御
* Gamepad をHIDエンドポイントとして追加
を boot.pyで、それ以外のキーの監視とキーコードの送信やインジケータ、スピーカ、LCDのバックライトなどの制御を code.py行う
boot.pyをいじるのはおっかないので、code.pyで全部やればいいだろうとは思うのだが、上の二点はどうしても boot.pyでやらないとダメなので、覚悟を決めてコードの変更を行う
code.pyの変更は比較的安全だが、boot.pyの方は中で例外などが発生して、それを適切に処理しないとファームウェアが再起不能になる危険性があるので、慎重に行う
万一、再起不能になった場合は、復旧用の uf2ファイルで修復しないと回復しない
なお、code.pyの変更は自動的に検知されてリロードされるし、シリアルコンソールで Ctrl+Dをたたくことで強制的にリロードできるが、boot.pyを変更した場合は RP2040を本体背面のリセットボタンでリセットする必要がある
==== boot.py ====
左 Fnキーが押されていたら storage.enable_usb_drive()でCIRCUITPYを有効化する処理と、GamepadをUSB HIDのエンドポイントとして追加する処理を記述する
Fnキーは、結構長めに押していないといけないが、Ubuntuや多くのディストリビューションでは、CIRCUITPYが認識された時点でデバイスの挿入音が鳴ると思うので、それを待って離してもらえればいいと思う
コメントにあるように、Gamepadのエントリはサンプルから持ってきた
ただし、ボタン数は15にしてある
サンプルは16だったが、evtestで確認したところ、16個目のボタンにコードが割り当てられていなかったので削減した
キー処理の部分は try~except((個人的にはPythonは好きではない。インデント依存なのがFORTRANみたいで嫌い。そしてこの例外処理も、except Exception as e: とかで書くのだが「except ExceptionだったらそれはException以外だろうがぁぁぁあ!」と感じるのも嫌い))で例外処理を行っている
上に書いたように万一未処理の例外が発生するとファームウェアが修復不能になるためである
なお、boot.pyの中にある print()などの出力は boot_out.txtというファイルに出力される
このファイルは、boot.pyから何も出力されなければ昔のファイルがいつまでも残っているので、boot.pyをテストする前には boot_out.txtを消してからやった方が問題の有無がわかりやすい
なお、boot.pyの中で未処理の例外が発生するなどすると、CIRCUITPYドライブ自体にアクセスできなくなるので、boot_out.txtに何が書かれたかを知る術はない
import usb_hid
import storage
import board
import digitalio
import time
# left fn key is board.GP0 x board.GP15
# key check -- if left fn key is pressed, do not disable_usb_drive()
# initialize pins
try:
storage.disable_usb_drive()
col_pin = digitalio.DigitalInOut(board.GP0)
row_pin = digitalio.DigitalInOut(board.GP15)
row_pin.direction = digitalio.Direction.INPUT
row_pin.pull = digitalio.Pull.UP
col_pin.direction = digitalio.Direction.OUTPUT
col_pin.value = False
time.sleep(0.05)
# scan left fn key
col_pin.direction = digitalio.Direction.OUTPUT
col_pin.value = True
row_pin.direction = digitalio.Direction.INPUT
row_pin.pull = digitalio.Pull.DOWN
# check if left fn is not pressed
if row_pin.value:
time.sleep(0.05)
if row_pin.value:
storage.enable_usb_drive()
except Exception as e:
print(f"An error occurred: {e}")
# This is only one example of a gamepad report descriptor,
# and may not suit your needs.
GAMEPAD_REPORT_DESCRIPTOR = bytes((
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, 0x0F, # Usage Maximum (Button 15)
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
))
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)
)
==== code.py ====
もちろん、果敢に、すべてをスクラッチから作成することも可能ではあるが、基本的にはオリジナルのcode.pyに手を入れる形で行う方がいいだろう
ここでベースにしているのは製品に同梱されていたものではなく「ファームウェア頂戴」って依頼したときに、回復用の uf2ファイルと一緒に「Fn+L/Fn+Rでマウスクリックできるようにしたサンプルもつけとく」っていって同梱してきてくれたファイルなのでもしかすると微妙に違うかもしれないが、大体同じはずである
アクションキーは row0 に大体まとまっているので、Fn+A/Fn+Bでの動作では KEY_MAP[0]をごっそり入れ替えることで実現している
標準のカスタムキー処理は押したときにしか発動しないが、キーリピートなどをキーパッドにもさせたかったので、カスタムキー処理の関数登録を(押す,離す)のタプルで登録してあった場合に、キーリピート処理ができるように拡張した
なお、code.pyの中の print()などの出力はシリアルコンソールで見ることができる
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.mouse import Mouse
from hid_gamepad import Gamepad
import board
import microcontroller
import digitalio
import time
import pwmio # PWM module
bl_pwm_value = 32767 # Middle value
ad_pwm_value = 32767 # Middle value
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)
mouse = Mouse(usb_hid.devices)
except Exception as e:
# Log the error message if initialization fails
print(f"Initialization failed: {e}")
# Reset the RP2040
microcontroller.reset()
# Initialize the GP22 pin
gp22 = digitalio.DigitalInOut(board.GP22)
gp22.direction = digitalio.Direction.OUTPUT
# Set the GP22 pin high
gp22.value = False
# Initialize the GP19 pin
gp19 = digitalio.DigitalInOut(board.GP19)
gp19.direction = digitalio.Direction.OUTPUT
# Set the GP19 pin high
gp19.value = False
gp21 = digitalio.DigitalInOut(board.GP21)
gp21.direction = digitalio.Direction.OUTPUT
# Set the GP21 pin high
gp21.value = False
# Initialize PWM output pin GP22 for the breathing light effect
# gp22_pwm = pwmio.PWMOut(board.GP22, frequency=5000, duty_cycle=0)
# Initialize BL PWM
BL_PWM_RP = pwmio.PWMOut(board.GP20, frequency=5000, duty_cycle=5000)
# Initialize AD PWM
AD_PWM_RP = pwmio.PWMOut(board.GP18, frequency=5000, duty_cycle=32700)
def toggle_gp22():
gp22.value = not gp22.value
print(f"GP22 state toggled to: {gp22.value}")
kbd.press(Keycode.CAPS_LOCK)
kbd.release(Keycode.CAPS_LOCK)
def start_breathing_light():
tmp_time = 0
stop_loop = True # Flag used to break out of the while loop
kbd = None # Make sure kbd is initialized
gp22_temp = gp22.value # Record the current Caps Lock state
for i in range(1,10+1):
gp22.value = not gp22.value
# print("GP22 TEST:",gp22.value)
time.sleep(0.3)
try:
while stop_loop:
tmp_time +=1
if tmp_time >= 255:
gp22.value = not gp22.value
# print("GP22 IS:",gp22.value)
tmp_time = 0
current_keys = set(scan_keyboard())
# Handle keys that are pressed
for key in current_keys:
if key is not None:
time.sleep(0.2)
if key is not None:
if kbd:
kbd.release_all()
gp22.value = gp22_temp
gp21.value = not gp21.value
stop_loop = False # Set the flag
break
finally:
if kbd:
kbd.release_all()
gp22.value = gp22_temp
def toggle_gp19():
gp19.value = not gp19.value
# print(f"GP19 state toggled to: {gp19.value}")
def toggle_gp21():
gp21.value = not gp21.value
# print(f"GP21 state toggled to: {gp21.value}")
start_breathing_light()
def ad_pwm_down():
global ad_pwm_value
if ad_pwm_value <= 0:
ad_pwm_value = 0
else:
ad_pwm_value -= 6553
ad_pwm_value = min(max(ad_pwm_value, 0), 65535)
# Update the duty cycle
AD_PWM_RP.duty_cycle = ad_pwm_value
print("pwm_value == ", ad_pwm_value)
def ad_pwm_up():
global ad_pwm_value
if ad_pwm_value >= 65535:
ad_pwm_value = 65535
else:
ad_pwm_value += 6553
ad_pwm_value = min(max(ad_pwm_value, 0), 65535)
# Update the duty cycle
AD_PWM_RP.duty_cycle = ad_pwm_value
print("pwm_value == ", ad_pwm_value)
# Because the backlight is controlled by an NPN transistor pull-down, a higher PWM duty cycle makes the backlight dimmer. So the pwm_up functions below actually decrease the duty cycle, and pwm_down increases it.
def bl_pwm_up():
global bl_pwm_value
if bl_pwm_value <= 0:
bl_pwm_value = 0
else:
bl_pwm_value -= 6553
bl_pwm_value = min(max(bl_pwm_value, 0), 65535)
# Update the duty cycle
BL_PWM_RP.duty_cycle = bl_pwm_value
print("pwm_value == ",bl_pwm_value)
def bl_pwm_down():
global bl_pwm_value
if bl_pwm_value >= 65535:
bl_pwm_value = 65535
else:
bl_pwm_value += 6553
bl_pwm_value = min(max(bl_pwm_value, 0), 65535)
# Update the duty cycle
BL_PWM_RP.duty_cycle = bl_pwm_value
print("pwm_value == ",bl_pwm_value)
def shift_left_bracket():
kbd.press(Keycode.SHIFT, Keycode.LEFT_BRACKET)
# kbd.release(Keycode.SHIFT)
kbd.release_all()
def shift_right_bracket():
kbd.press(Keycode.SHIFT, Keycode.RIGHT_BRACKET)
# kbd.release(Keycode.SHIFT)
kbd.release_all()
def shift_backslash():
kbd.press(Keycode.SHIFT, Keycode.BACKSLASH)
# kbd.release(Keycode.SHIFT)
kbd.release_all()
def shift_grave_accent():
kbd.press(Keycode.SHIFT, Keycode.GRAVE_ACCENT)
# kbd.release(Keycode.SHIFT)
kbd.release_all()
def lock_screen():
# Lock the screen (suspend on Linux)
kbd.press(Keycode.WINDOWS, Keycode.L)
kbd.release_all()
def scan_previous_track():
consumer_control.press(ConsumerControlCode.SCAN_PREVIOUS_TRACK)
consumer_control.release()
def play_pause():
consumer_control.press(ConsumerControlCode.PLAY_PAUSE)
consumer_control.release()
def scan_next_track():
consumer_control.press(ConsumerControlCode.SCAN_NEXT_TRACK)
consumer_control.release()
def mouse_left():
mouse.press(Mouse.LEFT_BUTTON)
time.sleep(0.02)
mouse.release(Mouse.LEFT_BUTTON)
time.sleep(0.25)
def mouse_right():
mouse.press(Mouse.RIGHT_BUTTON)
time.sleep(0.02)
mouse.release(Mouse.RIGHT_BUTTON)
time.sleep(0.25)
def mouse_press_up():
mouse.move(y=-8)
def mouse_release_up():
mouse.move(y=0)
def mouse_press_down():
mouse.move(y=8)
def mouse_release_down():
mouse.move(y=0)
def mouse_press_left():
mouse.move(x=-8)
def mouse_release_left():
mouse.move(x=0)
def mouse_press_right():
mouse.move(x=8)
def mouse_release_right():
mouse.move(x=0)
def gamepad_press_north():
# X button
gp.press_buttons(4)
def gamepad_release_north():
gp.release_buttons(4)
def gamepad_press_south():
# B button
gp.press_buttons(1)
def gamepad_release_south():
gp.release_buttons(1)
def gamepad_press_west():
# Y button
gp.press_buttons(5)
def gamepad_release_west():
gp.release_buttons(5)
def gamepad_press_east():
# A button
gp.press_buttons(2)
def gamepad_release_east():
gp.release_buttons(2)
def gamepad_press_tl():
# L button
gp.press_buttons(7)
def gamepad_release_tl():
gp.release_buttons(7)
def gamepad_press_tr():
# R button
gp.press_buttons(8)
def gamepad_release_tr():
gp.release_buttons(8)
def gamepad_btn_select():
# Select button
gp.click_buttons(11)
def gamepad_btn_start():
# Start button
gp.click_buttons(12)
def gamepad_press_up():
gp.move_joysticks(x=0, y=-127)
def gamepad_release_up():
gp.move_joysticks(x=0,y=0)
def gamepad_press_down():
gp.move_joysticks(x=0, y=127)
def gamepad_release_down():
gp.move_joysticks(x=0,y=0)
def gamepad_press_left():
# Send both Gamepad / Keyboard
gp.move_joysticks(x=-127, y=0)
def gamepad_release_left():
gp.move_joysticks(x=0,y=0)
def gamepad_press_right():
gp.move_joysticks(x=127, y=0)
def gamepad_release_right():
gp.move_joysticks(x=0,y=0)
def custom_btn_a():
KEY_MAP[0] = [Keycode.UP_ARROW, Keycode.LEFT_ARROW, Keycode.DOWN_ARROW, Keycode.RIGHT_ARROW, Keycode.L, Keycode.R, Keycode.X, Keycode.Y, Keycode.B, Keycode.A]
def custom_btn_b():
KEY_MAP[0] = [CUSTOM_KEYS["GAMEPAD_UP"], CUSTOM_KEYS["GAMEPAD_LEFT"], CUSTOM_KEYS["GAMEPAD_DOWN"], CUSTOM_KEYS["GAMEPAD_RIGHT"], CUSTOM_KEYS["GAMEPAD_TL"], CUSTOM_KEYS["GAMEPAD_TR"], CUSTOM_KEYS["GAMEPAD_NORTH"], CUSTOM_KEYS["GAMEPAD_WEST"], CUSTOM_KEYS["GAMEPAD_SOUTH"],CUSTOM_KEYS["GAMEPAD_EAST"]]
# 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,
"FN_MOUSE_LEFT" : -112,
"FN_MOUSE_RIGHT" : -113,
"FN_MOUSE_MOVE_UP" : -114,
"FN_MOUSE_MOVE_DOWN" : -115,
"FN_MOUSE_MOVE_LEFT" : -116,
"FN_MOUSE_MOVE_RIGHT" : -117,
"FN_BTN_A" : -118,
"FN_BTN_B" : -119,
"FN_BTN_X" : -120,
"FN_BTN_Y" : -121,
"GAMEPAD_NORTH" : -122,
"GAMEPAD_SOUTH" : -123,
"GAMEPAD_WEST" : -124,
"GAMEPAD_EAST" : -125,
"GAMEPAD_TL" : -126,
"GAMEPAD_TR" : -127,
"GAMEPAD_SELECT" : -128,
"GAMEPAD_START" : -129,
"GAMEPAD_UP" : -130,
"GAMEPAD_DOWN" : -131,
"GAMEPAD_LEFT" : -132,
"GAMEPAD_RIGHT" : -133
}
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["FN_MOUSE_LEFT"] : mouse_left,
CUSTOM_KEYS["FN_MOUSE_RIGHT"] : mouse_right,
CUSTOM_KEYS["FN_MOUSE_MOVE_UP"] : (mouse_press_up, mouse_release_up),
CUSTOM_KEYS["FN_MOUSE_MOVE_DOWN"] : (mouse_press_down, mouse_release_down),
CUSTOM_KEYS["FN_MOUSE_MOVE_LEFT"] : (mouse_press_left, mouse_release_left),
CUSTOM_KEYS["FN_MOUSE_MOVE_RIGHT"] : (mouse_press_right, mouse_release_right),
CUSTOM_KEYS["FN_BTN_A"] : custom_btn_a,
CUSTOM_KEYS["FN_BTN_B"] : custom_btn_b,
CUSTOM_KEYS["GAMEPAD_NORTH"] : (gamepad_press_north, gamepad_release_north),
CUSTOM_KEYS["GAMEPAD_SOUTH"] : (gamepad_press_south, gamepad_release_south),
CUSTOM_KEYS["GAMEPAD_WEST"] : (gamepad_press_west, gamepad_release_west),
CUSTOM_KEYS["GAMEPAD_EAST"] : (gamepad_press_east, gamepad_release_east),
CUSTOM_KEYS["GAMEPAD_TL"] : (gamepad_press_tl, gamepad_release_tl),
CUSTOM_KEYS["GAMEPAD_TR"] : (gamepad_press_tr, gamepad_release_tr),
CUSTOM_KEYS["GAMEPAD_SELECT"] : gamepad_btn_select,
CUSTOM_KEYS["GAMEPAD_START"] : gamepad_btn_start,
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),
}
# Define the number of rows and columns
NUM_ROWS = 7
NUM_COLS = 10
# Define the key map for the keyboard matrix
KEY_MAP = [
#col0 col1 col2 col3 col4 col5 col6 col7 col8 col9
[Keycode.UP_ARROW, Keycode.LEFT_ARROW, Keycode.DOWN_ARROW, Keycode.RIGHT_ARROW, Keycode.L, Keycode.R, Keycode.X, Keycode.Y, Keycode.B, Keycode.A], # 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_SELECT"], Keycode.SPACE, CUSTOM_KEYS["GAMEPAD_START"], Keycode.RIGHT_ALT, Keycode.WINDOWS, CUSTOM_KEYS["FN_KEY"], None] # row 6
]
# Define the FN key map for the keyboard matrix
FN_MAP = [
#col0 col1 col2 col3 col4 col5 col6 col7 col8 col9
[CUSTOM_KEYS["FN_MOUSE_MOVE_UP"],CUSTOM_KEYS["FN_MOUSE_MOVE_LEFT"], CUSTOM_KEYS["FN_MOUSE_MOVE_DOWN"], CUSTOM_KEYS["FN_MOUSE_MOVE_RIGHT"], CUSTOM_KEYS["FN_MOUSE_LEFT"], CUSTOM_KEYS["FN_MOUSE_RIGHT"], Keycode.X, Keycode.Y, CUSTOM_KEYS["FN_BTN_B"], CUSTOM_KEYS["FN_BTN_A"]], # row 0
[Keycode.F1, Keycode.F2, Keycode.F3, Keycode.F4, Keycode.F5, Keycode.F6, Keycode.F7, Keycode.F8, Keycode.F9, Keycode.F10], # row 1
[Keycode.ESCAPE, CUSTOM_KEYS["FN_MUTE"], CUSTOM_KEYS["FN_VOLUME_DOWN"], CUSTOM_KEYS["FN_VOLUME_UP"], ConsumerControlCode.SCAN_PREVIOUS_TRACK, ConsumerControlCode.PLAY_PAUSE, ConsumerControlCode.SCAN_NEXT_TRACK, CUSTOM_KEYS["FN_LOCK_SCREEN"], Keycode.F11, Keycode.F12], # row 2
[Keycode.GRAVE_ACCENT, CUSTOM_KEYS["SHIFT_GRAVE_ACCENT"], Keycode.BACKSLASH, CUSTOM_KEYS["SHIFT_BACKSLASH"], CUSTOM_KEYS["SHIFT_LEFT_BRACKET"], CUSTOM_KEYS["SHIFT_RIGHT_BRACKET"], Keycode.LEFT_BRACKET, Keycode.RIGHT_BRACKET, Keycode.L, Keycode.DELETE], # row 3
[Keycode.INSERT, Keycode.HOME, CUSTOM_KEYS["FN_BL_CONTROL_SCREEN"], Keycode.END, Keycode.PAGE_UP, Keycode.PAGE_DOWN, Keycode.SCROLL_LOCK, Keycode.FORWARD_SLASH, Keycode.ENTER, None], # row 4
[Keycode.TAB, Keycode.CAPS_LOCK, CUSTOM_KEYS["FN_BL_PWM_DOWN"], CUSTOM_KEYS["FN_BL_PWM_UP"], Keycode.SEMICOLON, Keycode.QUOTE, Keycode.COMMA, Keycode.PERIOD, Keycode.SHIFT, None], # row 5
[CUSTOM_KEYS["FN_KEY"], Keycode.CONTROL, Keycode.LEFT_ALT, Keycode.PRINT_SCREEN, Keycode.SPACE, Keycode.PAUSE, Keycode.RIGHT_ALT, Keycode.WINDOWS, CUSTOM_KEYS["FN_KEY"], None] # row 6
]
# Define the row and column pins
row_pins = [board.GP16, board.GP10, board.GP11, board.GP12, board.GP13, board.GP14, board.GP15]
col_pins = [board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5, board.GP6, board.GP7, board.GP8, board.GP9]
# Initialize row and column pins
row_gpio = [digitalio.DigitalInOut(pin) for pin in row_pins]
col_gpio = [digitalio.DigitalInOut(pin) for pin in col_pins]
# Set rows as inputs with pull-up and columns as outputs set to low
for row in row_gpio:
row.direction = digitalio.Direction.INPUT
row.pull = digitalio.Pull.UP
for col in col_gpio:
col.direction = digitalio.Direction.OUTPUT
col.value = False
# Function to scan the keyboard matrix
def scan_keyboard():
keys_pressed = []
fn_active = False
for col_index, col_pin in enumerate(col_gpio):
col_pin.direction = digitalio.Direction.OUTPUT
col_pin.value = True
for row_index, row_pin in enumerate(row_gpio):
row_pin.direction = digitalio.Direction.INPUT
row_pin.pull = digitalio.Pull.DOWN
if row_pin.value: # Key is pressed
time.sleep(0.05) # Debounce delay
if row_pin.value:
key = KEY_MAP[row_index][col_index]
if key == CUSTOM_KEYS["FN_KEY"]:
fn_active = True
keys_pressed.append((row_index, col_index)) # Record the key position
col_pin.value = False # Set the column back to low
active_map = FN_MAP if fn_active else KEY_MAP
translated_keys = []
for row_index, col_index in keys_pressed:
translated_key = active_map[row_index][col_index]
translated_keys.append(translated_key)
print("ROW", row_index, "COL", col_index)
return translated_keys
previous_keys = set()
# 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: # Filter out negative key codes
kbd.press(key)
previous_keys = current_keys
time.sleep(0.01) # Small delay to avoid too fast key presses
except Exception as e:
print(f"An error occurred: {e}")
# Perform necessary cleanup, such as releasing all keys
try:
kbd.release_all()
except Exception as cleanup_error:
print(f"Error during cleanup: {cleanup_error}")
# Reset the RP2040
microcontroller.reset()
==== デバッグ ====
シリアルコンソール(/dev/ttyACM0)を使って code.pyのデバッグを行うことができる
基本的に接続すると問題があれば大体エラーを出している
エラーメッセージが流れてしまった後でも Ctrl+Dを押せば code.pyをリロードするので、エラーがあればそこで表示される
code.pyを更新したときも、Ctrl+Dで強制的にリロードさせることができる((ほっておいても自動的に更新を検知してリロードするが))
$ cu -l /dev/ttyACM0
Connected.
Traceback (most recent call last):
File "code.py", line 414, in
KeyboardInterrupt:
Code done running.
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
Press any key to enter the REPL. Use CTRL-D to reload.
soft reboot
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Traceback (most recent call last):
File "code.py", line 414, in
KeyboardInterrupt:
Code done running.
Press any key to enter the REPL. Use CTRL-D to reload.
Adafruit CircuitPython 10.0.0-beta.0-dirty on 2025-07-23; Raspberry Pi Pico with rp2040
>>> import inspect
Traceback (most recent call last):
File "", line 1, in
ImportError: no module named 'inspect'
>>>
soft reboot
Auto-reload is on. Simply save files over USB to run them or enter REPL to disable.
code.py output:
Code stopped by auto-reload. Reloading soon.
soft reboot