~/ryosk7
cd ~/

2026-04-02

PicoRuby で MQTT クライアントを作った


Raspberry Pi Pico W 上で動く PicoRuby 向けに、MQTT クライアントライブラリを作りました。

GitHub - ryosk7/picoruby-net-mqtt-femtoContribute to ryosk7/picoruby-net-mqtt-femto development by creating an account on GitHub.
favicon of https://github.com/ryosk7/picoruby-net-mqtt-femtogithub.com
ogp of https://opengraph.githubassets.com/cb1b1c25bb69b115804358b63970e35227528ff14b2cc6e6e78f46f5a2047514/ryosk7/picoruby-net-mqtt-femto

以前、X上で作ったよっていう報告をしたのですが、 わざわざ記事にしたのは、Claude君に僕のブログのデザインを改修してもらい、いい感じになったブログサイトをみんなに見てもらいたかったからです。

背景

PicoRubyでスマートロックを作りたいと思ったからです。 その際に色々な壁に遭遇したのですが、とても話が逸れてしまうので、詳細は2025年の東京Ruby会議12やPicoRuby Overflowをご覧ください。

東京Ruby会議12

PicoRuby Overflow

さらに、RubyKaigi 2026に登壇が決まったので、資料作りのためのアウトプットとしてここに残します。

PicoRuby with Practical MQTTRubyKaigi 2026, #rubykaigi
favicon of https://rubykaigi.org/2026rubykaigi.org
ogp of https://rubykaigi.org/2026/images/ogp-dd7976fa.png

補足:

  • まだ時間に余裕があるから、機能をアップデートするかも。
  • あと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_WAITTIMEOUT は 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_aliveclean_session は現時点では未反映
  • unsubscribeping は未対応

マイコン上で動かすことを優先したため、必要最小限の機能に絞っています。

依存ライブラリ

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 ボードをお持ちの方はそちらをご利用ください。