pockestrap

Programmer's memo

Raspberry Pi Pico 単体で動作するRubyのREPL

こんにちは。 id:Pocke です。最近はずっとPicoRubyを触っています。

PicoRubyを使ってRaspberry Pi Pico単体で動作するREPLであるSpartanREPLを作ったので、今回はそれの紹介をします。

github.com

PicoRubyとは

picoruby.org

PicoRubyはとても小さいRuby実装で、Raspberry Pi Picoのようなマイコン上で動かすことをメインターゲットとしています。

PicoRubyは小さい実装ながら、REPLに不可欠なevalを提供しています。実際にR2P2はirbを提供しています。

Raspberry Pi Picoとは

Raspberry Pi Picoは、ラズベリーパイ財団が開発したマイコン開発基板です。 普通のRaspberry Piとは異なり、組み込み向けに特化しています。限定された性能ですが、安価に手に入りサイズも小さいのが特徴です。

SpartanREPLのRaspberry Pi Pico の使い方

SpartanREPLで重要となるのは、Raspberry Pi Picoはそれ単体で入出力のための機構を提供していることです。

まず、Raspberry Pi Pico は入力としてBOOTSELボタンという小さなボタンを持ちます。 通常このボタンはPicoにプログラムを書き込む際に使用します。このボタンを押しながらPCにPicoを接続することで書き込みモードとなるためです。 そしてBOOTSELボタンの状態はPico上で動作するプログラムから読み取ることもできます。つまり、キーボードやPC、あるいは何かしら追加のスイッチなどを用意しなくとも、ボタンのオン/オフという形でRaspberry Pi Pico単体でユーザーからの入力を得ることができます。

次に出力ですが、Raspberry Pi Picoには小さなLEDがオンボードで実装されています。これも当然プログラムから光らせることが可能です。

つまり、BOOTSELボタンのオン/オフパターンを文字に対応させ、実行した結果の文字列をLEDの明滅パターンに対応させれば、Raspberry Pi Pico単体で(すなわちRaspberry Pi Picoに電源以外のなんの部品も装置もつなげずに)REPLが実装できそうです。

SpartanREPL

ということでSpartanREPLというRaspberry Pi Picoで実現できる最小のREPLを作りました。

BOOTSELボタンを押している時間の長さを信号として受け取り、それをLEDが光る時間の長さとして出力します。

時間の長短の符号化には、欧文モールス符号を扱います。

  • BOOTSELボタンが押された時間を計測し、200msをしきい値としてそれより短ければ短点、長ければ長点とする。
  • 一定時間(750ms)入力がなければ、文字の区切りとする。
    • モールス符号はある文字を示す符号語が別の文字を示す符号語のprefixになっている場合があります。例えばA(・―)はR(・―・)のprefixです。
    • 単純に頭からパターンを読むだけでは正しく文字に変換ができないため、入力のない時間を区切りとして扱います。
  • 2000ms以上BOOTSELボタンが押された場合、"\n"の入力として扱う。
  • "\n"が入力されたらそれまで入力された文字を確定し、evalする。
  • evalした値を#inspectした文字列を出力とする。
  • 出力をモールス符号で符号化し、短点を250ms, 長点を750msの点灯時間としてLEDを明滅させる。

では実際にSpartanREPLの挙動を動画で見てみましょう。以下の動画では、pをモールス符号で入力し、実行結果であるnilに相当する文字列をモールス符号で符号化したパターンでLEDが明滅しています。1

www.dropbox.com

p (・ ― ― ・)の入力後に、nil(― ・ / ・ ・ / ・ ― ・ ・)が出力されていることがわかると思います。そして、Picoには電源となるモバイルバッテリー以外なにも接続されていません!

SpartanREPLの実装

ではSpartanREPLの実装を見てみましょう。 SpartanREPLはいくつかのコンポーネントで構成されています。

コンポーネントとデータの流れ

Encoding

ここで最も重要な役割を持っているのがSpartanREPL::Encodingクラスです。このクラスは各文字がどのように符号化されるのかを定義します。モールス符号の定義の一部を見てみましょう。

module SpartanRepl
  class Encoding
    MORSE = new do |e|
      # アルファベットを定義
      s = :short
      l = :long
      e.alphabet = [s, l]

      # 各文字を定義
      e.define "a", [s, l]
      e.define "b", [l, s, s, s]
      e.define "c", [l, s, l, s]
      e.define "d", [l, s, s]
      e.define "e", [s]
      # ...

まずEncoding#alphabet=メソッドで、この符号で使われる符号アルファベットを定義します。2 モールス符号では:short:longという2つのシンボルをアルファベットとして扱っています。モールス符号のアルファベットは2つのシンボルのみを持ちますが、3つ以上のシンボルを定義することも可能です。

そしてそのシンボルを使って、それぞれの文字がどのように符号化されるかを定義します。 例えばこの例だと、"a"という文字は:short, :longと続く場合にマッチします。

なお、Encodingが持つのは文字と符号語の対応のみで、ボタンの押下時間やLEDの点灯時間は定義しません。これらはそれぞれDecoder, Encoderが定義をします。

Decoder

DecoderはEncodingに従ってボタンのon/offイベントを文字に変換します。先ほど触れたように、ボタンの押下時間とシンボルの対応はこのクラスが保持します。 Decoderを使う例は以下のようになります。

include SpartanRepl

# Decoderの生成
dec = Decoder.new(
  SpartanRepl::Encoding::MORSE,
  durations_ms: { short: 0..200, long: 201..99999 },
  separator_wait_ms: 750,
)

# マッチすれば文字を返す
dec.decode(events)

Encodingと各シンボルに対応するボタンの押下時間の範囲を渡してDecoderを作ります。 そしてdecodeメソッドにはボタンのオンオフのイベントを配列で渡し、イベントがEncoding中のパターンにマッチすればマッチした結果の文字を返します。

Encoder

EncoderはEncodingに従って文字列をLEDの明滅に変換します。こちらもDecoderと同じく、LEDの点灯時間とシンボルの関係を保持します。 Encoderを使う例は以下のようになります。

include SpartanRepl

# Encoderの生成
enc = Encoder.new(
  SpartanRepl::Encoding::MORSE,
  durations_ms: { short: 250, long: 750 },
  led: LED.new,
)

# 文字列に対応したパターンでLEDを明滅させる
enc.encode(str)

Decoderと似た初期化ですが、こちらはLEDを点灯させる時間を範囲ではなく数値で指定します。 そしてencodeメソッドは文字列を受け取り、Encodingに応じてLEDを明滅させます。

コマンド用Encoding

符号化の項目で「2000ms以上BOOTSELボタンが押された場合、"\n"の入力として扱う。」という説明をしましたが、実はこれもEncodingで実現されています。 2000ms以上のボタン押下を"\n"とするEncoding, Decoderを定義し、それをモールス符号のデコードよりも前にデコードすることでこれを実現しています。

また、同様の仕組みを使ってEncodingの切り替え機能も実装しています。50ms以下のごく短い押下(ボタンを爪ではじくような入力に相当します)をコマンド用Encodingに符号切り替え命令として定義しています。 そして符号切り替え命令が発生したら、モールス符号を別の符号に切り替えるようにしています。

現在はモールス符号のほかにASCII 3 コードによる入出力も実装されています。

Encodingを定義すれば異なる符号も簡単に実装できるため、今後追加してみても面白いかもしれません。例えば数値の入力を簡単にするものなどを考えています。

最後に

PicoRubyを使って、Raspberry Pi Pico単体で動くREPLを作った話でした。このプログラムはRaspberry Pi Picoさえあれば誰でも試せるので、ぜひGitHubのリリースページからuf2ファイルをダウンロードして試してみてください。

https://github.com/pocke/spartan-repl/releases/latest

SpartanREPL自体は一種のジョークアプリではありますが、BOOTSELボタンの活用は応用の効くテクニックです。LEDの点滅につなげて何らかの状態を表示させてもよいですし、別のアプリではBOOTSELボタンの押下で無限ループを終了するような活用もしています。 Picoだけで完結する便利なボタンとして活用できるでしょう。

明日から始まるRubyKaigi 2026では、SpartanREPLのデモをします! Day 1(22日)のLunch BreakにSmartHR様のRuby showcase郭でお待ちしています。12:45ごろから立っている予定です。とても地味なデモになると思いますが、ぜひ足を運んでいただければと思います。 また小さいデバイスなので会期中はこれを常に持ち歩こうと思っています。任意のタイミングで声をかけていただければと思います。


  1. 無引数のKernel.#pメソッドはnilを返します。これはコードゴルフでよく使われるテクニックの1つです。
  2. abc...ではなく、その符号を構成するシンボルの一覧。
  3. 正確にはASCIIの先頭1bitを落としたもの。