2026-04-02
PicoRuby で MQTT クライアントを作った
Raspberry Pi Pico W 上で動く PicoRuby 向けに、MQTT クライアントライブラリを作りました。
以前、X上で作ったよっていう報告をしたのですが、 わざわざ記事にしたのは、Claude君に僕のブログのデザインを改修してもらい、いい感じになったブログサイトをみんなに見てもらいたかったからです。
背景
PicoRubyでスマートロックを作りたいと思ったからです。 その際に色々な壁に遭遇したのですが、とても話が逸れてしまうので、詳細は2025年の東京Ruby会議12やPicoRuby Overflowをご覧ください。
東京Ruby会議12
PicoRuby Overflow
さらに、RubyKaigi 2026に登壇が決まったので、資料作りのためのアウトプットとしてここに残します。

補足:
- まだ時間に余裕があるから、機能をアップデートするかも。
- あとYuuu(https://x.com/Y_uuu)さんがpicoruby-socket上のノンブロッキング対応をしてくれています。対応後に取り込み & 記事の更新します。
アーキテクチャ
ライブラリは 4 層構成になっています。
mrblib/mqtt.rb ← Ruby API(ユーザーが触る層)
src/mrubyc/mqtt.c ← mrubyc バインディング(Ruby ↔ C)
ports/rp2040/mqtt.c ← lwIP native MQTT の実装
src/mqtt.c ← VM 切り替えレイヤー
Ruby から呼び出すと、mrubyc のバインディングを通じて lwIP の MQTT API に到達する流れです。
使い方
require "net/mqtt"
client = Net::MQTT::Client.connect("192.168.1.10", 1883)
# メッセージを publish
client.publish("sensor/temp", "23.5")
# トピックを subscribe して受信
client.subscribe("command/#")
client.get do |topic, payload|
puts "#{topic}: #{payload}"
end
client.disconnect
ブロック付きの connect も使えます。
Net::MQTT::Client.connect("192.168.1.10") do |c|
c.publish("hello", "world")
end
実装の詳細
Ruby 層
mrblib/mqtt.rb が公開 API です。クライアント ID はデフォルトで picoruby-{タイムスタンプ} を使います。
def initialize(host, port = 1883, **options)
@host = host
@port = port
@client_id = options[:client_id] || "picoruby-#{Time.now.to_i}"
@keep_alive = options[:keep_alive] || 60
@connected = false
end
内部では lwIP のコールバックベース API を使って非同期に接続処理を開始しますが、公開 API としての connect は同期的です。Ruby 側では最大 3 秒間ポーリングしながら接続完了を待つ実装になっています。なお、keep_alive は Ruby 側では保持していますが、現時点では C 層で固定値 60 秒を使っています。
ステートマシン
lwIP のコールバックベース API に対応するため、C 層で FSM を実装しています。
typedef enum {
MQTT_STATE_IDLE,
MQTT_STATE_CONNECTING,
MQTT_STATE_CONNACK_WAIT,
MQTT_STATE_ACTIVE,
MQTT_STATE_SUBSCRIBING,
MQTT_STATE_PUBLISHING,
MQTT_STATE_DISCONNECTING,
MQTT_STATE_ERROR,
MQTT_STATE_TIMEOUT
} mqtt_fsm_state_t;
publish や subscribe も同様で、内部実装は lwIP のコールバックで状態を進める非同期型ですが、公開 API としてはポーリングしながら処理を進める同期呼び出しになっています。CONNACK_WAIT や TIMEOUT は enum にはありますが、現時点の実装ではまだ使っていません。
mrubyc バインディング
src/mrubyc/mqtt.c では各 Ruby メソッドを C 関数にマッピングしています。
mrbc_define_method(0, mrbc_class_object, "_connect_impl", c_mqtt_connect);
mrbc_define_method(0, mrbc_class_object, "_publish_impl", c_mqtt_publish);
mrbc_define_method(0, mrbc_class_object, "_subscribe_impl", c_mqtt_subscribe);
mrbc_define_method(0, mrbc_class_object, "_get_message_impl", c_mqtt_get_message);
mrbc_define_method(0, mrbc_class_object, "_is_connected_impl", c_mqtt_is_connected);
mrbc_define_method(0, mrbc_class_object, "_poll_impl", c_mqtt_poll);
mrbc_define_method(0, mrbc_class_object, "_disconnect_impl", c_mqtt_disconnect);
_ プレフィックスのメソッドは公開 API ではなく、Ruby 層の公開メソッドから内部的に呼び出すためのものです。RBS では private 扱いにしています。
現時点の制限
- QoS 0 のみ対応(1, 2 は未実装)
- TLS/SSL 非対応
- ユーザー名・パスワード認証非対応
- 同時サブスクライブは 1 トピックのみ
keep_aliveとclean_sessionは現時点では未反映unsubscribeとpingは未対応
マイコン上で動かすことを優先したため、必要最小限の機能に絞っています。
依存ライブラリ
mrbgem として以下に依存しています。
spec.add_dependency 'picoruby-socket'
spec.add_dependency 'picoruby-pack'
spec.add_dependency 'picoruby-time'
picoruby-socket はネットワーク基盤や補助関数を提供しており、ビルド時には lwIP のヘッダや include path もそこから利用しています。一方、MQTT の実装自体は lwip/apps/mqtt.h の native API を直接呼び出して行なっています。つまり、picoruby-socket の基盤の上で、この gem が lwIP の MQTT API を直接扱っている、という関係です。
おわりに
PicoRuby のエコシステムはまだ小さく、コントリビュートチャンスは無限大です。 この gem は mruby/c を使う RP2040 系ボード向けの実装です。mruby ベースの PicoRuby には picoruby-net-mqtt があり、RP2350 や ESP32 ボードをお持ちの方はそちらをご利用ください。