Luckfox Pico Mを外出先から使う。3,750円・64MBのLinuxボードにWireCanalで公開URLをつけるまで
秋葉原で買った3,750円のLinuxボード Luckfox Pico M(RV1103・64MB・USB-Cのみ)を、Web公開用の受信ポートを追加せずにインターネット側から使えるようにするまで。SDカード書き込み、RNDIS経由のネットワーク、WireCanal無料プランでの公開、busybox initでの常駐とブラウザからのLED操作、再起動復帰(33秒)まで実測手順で解説します。
こんにちは!
Qualitegプロダクト開発部です!
本日はLuckfox Pico Mという4000円弱の小さなLinuxボードをインターネット越しに操作できるようにしてみました。NAT越えの仕組みは無料プランのWireCanalを使いました。
先日、秋葉原の千石電商さんでLuckfox Pico M を税込3,750円で入手しました。
Raspberry Pi Picoとほぼ同じ大きさで、ピンヘッダ実装済み、CPUはRockchip RV1103(ARM Cortex-A7 1.2GHz)、メモリは64MB、起動はmicroSDカードからです。
Linuxが動くのに、この値段とこの大きさです。

これを外出先(インターネット越しに)から使えるようにしていきます。
ボードにはRJ45のコネクタ(いわゆるLAN端子)もWi-Fiもなく、あるのはUSB-Cのポートだけ。
それでもインターネット側から届く形にして、スマホから叩いてみます。
さて、できるのでしょうか?
先に結果を言うと、できました。
ボード側にWeb公開用の受信ポートを追加せず、WireCanalの無料プラン(0円)だけで、公開URLからLuckfox上のWebサーバーがそのまま見えるところまで行けました。
さらに今回は、ボードに挿したLED 1本を、公開ページのボタンでインターネット越しに点けたり消したりできるようにしました。
再起動しても、実測33秒で公開URLが自動で復活します。
ただし、Pico M自身にはWi-FiもRJ45もないため、今回はUSB接続したWindows PCをインターネットへのゲートウェイとして使います。
(今後はイーサつきLuckFoxでの実験記事も投稿予定です!)
なので、今回は、外出先からアクセスするときも、このPCは起動している必要があります。
この記事では、SDカードへのイメージ書き込みから、起動、ネットワーク、LEDの配線、WireCanalでの公開、再起動しても復活する常駐設定まで、実際にやった手順と実測値を最初から最後まで書きます。
対象はLuckfox Pico M(無印Luckfox Picoのピンヘッダ実装版)と公式のBuildrootイメージ、作業PCはWindows 11です。WireCanalは当社が開発・運営している自社サービスで、記事中の数値は2026年8月19日時点の実測値です。
Luckfox Pico Mとはどんなボードか
LuckfoxはRockchipのビジョン向けSoC(RV1103/RV1106)を載せた小型Linuxボードのシリーズで、Pico Mはその入門機です。今回の実機で確認した中身は次のとおりでした。
| 項目 | 実機で確認した値 |
|---|---|
| SoC | Rockchip RV1103(ARM Cortex-A7 1.2GHz・32bit・NEON/VFPv4)+RISC-V MCU+NPU 0.5TOPS |
| メモリ | 64MB DDR2。Linuxから見えるのは33MB(残りはNPU/ISPなどの予約) |
| OS | Buildroot(公式イメージ 250607版・Linux 5.10.160・busybox init・python3 3.11同梱) |
| ストレージ | microSD(TF)カードから起動。ルートは6GBのext4 |
| 接続 | USB-C(給電兼用・RNDIS仮想NIC+ADB)。Ethernetはパッドのみ(RJ45なし) |
| 価格 | 3,750円(税込・千石電商・2026年8月) |
Raspberry Piと違って、有線LANもWi-Fiも「そのままでは」使えません。
そこで今回は、PCとUSB-Cで繋いで仮想的なネットワークアダプタ(RNDIS)を生やして、PCとボードの間だけの小さなネットワークができるというしかけで使います。
MagJackとよばれるトランス入りのRJ45ジャック(いわゆるLAN端子)を自分で結線すればLANケーブルをつなぐことができますが少々手間がかかります。
また、そもそもRJ45ジャックつきのLuckFox製品もありますので直接LANケーブルでつなぎたい場合はそちらを使うこともできます(別記事として投稿予定です)
さて、今回はPCのインターネットをもらいにいく方法で実験しますので、外に出るにはPC側でひと工夫が要ります(手順3)。
もうひとつの特徴は、メモリの小ささです。
Linuxから見えるのは33MB。
ここにWireCanalの常駐エージェントを載せて実用になるのか、というのが今回の見どころのひとつです。
材料
| もの | 備考 |
|---|---|
| Luckfox Pico M | 千石電商で購入。ブレッドボードに挿して使いました |
| microSDカード | 数GBあれば十分。今回は手元の64GBを使用 |
| USB-Cケーブル | データ通信対応のもの(充電専用ケーブルだとRNDISが出ません) |
| LEDと抵抗(330Ω前後) | ブラウザから点灯・消灯するデモ用。ブレッドボード上で「物理ピン4 → 抵抗 → LED → 物理ピン3(GND)」とつなぎます(配線の節で説明) |
| Windows PC | Windows 11 Pro(Hyper-Vの仮想化基盤が有効な状態)。SD書き込みとインターネット共有に使います。ボードとのファイル転送にPuTTY付属のpscp.exeも使います |
| WireCanalのアカウント | 無料プラン(0円・カード登録不要)で今回の手順はすべて試せます |
WireCanalの無料プランは、canal(公開URLと転送先の組)が1本、公開アドレスはおまかせのランダムなサブドメインです。
作ったcanalはAgentを使っている限り消えず、Agentの最終接続から72時間で失効します。今回のように常駐で動かし続けるならURLは維持されます。
手順1: microSDカードにBuildrootイメージを書き込む
まず、必要なファイルとツールを集めます。入手先はすべて公式で、Luckfox Wikiの書き込み手順ページ(Flash Image)に、ツールとイメージへのリンクがまとまっています。ダウンロードするのは次の3つです。
| ダウンロードするもの | 入手のしかた |
|---|---|
| OSイメージ | 手順ページの「Official Buildroot」リンクを開くとGoogle Driveが開きます。その中の「MicroSD」フォルダに機種別のzipが5つ並んでいるので、Luckfox_Pico_MicroSD_250607.zip(無印Luckfox Pico/Pico M用・約104MB)を選びます。Plus・Mini・ProMax・WebBee用と間違えないように |
| SocToolKit | 同じ手順ページのSocToolKitの項にある「Click here to download」から(今回はv1.98)。Windows用のSDカード書き込みツールです |
| SD Card Formatter | SDアソシエーションの公式サイトから。SDカードの事前フォーマットに使います |
OSイメージのzipを展開すると、次のファイルが出てきます。
env.img idblock.img uboot.img boot.img oem.img userdata.img rootfs.img
update.img download.bin sd_update.txt tftp_update.txt最初にSD Card Formatterでカードをフォーマットします。インストールして起動すると、カードのドライブが「カードの選択」に出るので、それが本当にSDカードか(容量・ドライブレター)を確認して、クイックフォーマットのまま「フォーマット」を押します。別のUSBドライブを挿している人は、ここの選び間違いにだけ注意してください。


次にSocToolKitです。zipを展開し、SocToolKit.exeを右クリックから「管理者として実行」で起動します(管理者で起動しないとSDカードを認識しないことがある、と公式手順にも書かれています)。最初にChip Selectionダイアログが出るので、RV1103を選んでOKを押します。
公式手順ではRV1106を選択するよう案内されていますが、今回は実機のSoCに合わせてRV1103を選択しました。この選択のまま、7つのイメージの書き込み完了(Create sdcard OK)からPico Mの実機起動まで確認しています。

「SDTool」タブに切り替えると、Usb Disk欄に挿したカード(今回は59.5G)が出ています。モードは「SD Boot」を選びます。

「Boot Files...」を押すとファイル選択ダイアログが開くので、さっき展開したフォルダから7つの.img(env.img・idblock.img・uboot.img・boot.img・oem.img・userdata.img・rootfs.img)をまとめて選びます。update.imgは含めません。読み込まれると一覧に7ファイルが並びます。

「Create SD」を押すと、書き込み先デバイスの最終確認ダイアログが出ます。デバイス名と容量をもう一度確認してYesを押すと書き込みが始まり、右側のログに各イメージの「Write disk from file (100%)...OK」が流れていきます。今回は1〜2分で「Create sdcard OK」まで到達しました。これで書き込みは完了です。

ここで面白いのは、同梱の sd_update.txt にSDカード上の配置がセクタ単位で書いてあることです。抜粋するとこうなっています。
#env.img 0x0:0x0 0x40:0x8000 0x40:0x8000
#idblock.img 0x40:0x8000 0x400:0x80000 0x170:0x2E000
#uboot.img 0x440:0x88000 0x400:0x80000 0x200:0x40000
#boot.img 0x840:0x108000 0x10000:0x2000000 0x1899:0x313200
#oem.img 0x10840:0x2108000 0x100000:0x20000000 0x17C78:0x2F8F000
#userdata.img 0x110840:0x22108000 0x80000:0x10000000 0x4C4A:0x989400
#rootfs.img 0x190840:0x32108000 0xC00000:0x180000000 0x4CD80:0x99B0000各行は「名前 開始セクタ:開始バイト 領域サイズ 実体サイズ」です。つまりSocToolKitがやっているのは、Rockchip/Luckfox側で定められたこのオフセットへ各イメージをそのまま置くことです。
MBR/GPTのパーティションテーブルを自分で作る必要はありません。RockchipのブートROMはSDカードのセクタ64(0x8000バイト目)にある idblock.img を読みに来て起動が始まります(ブート時のパーティション構成はU-Boot環境側にも保持されています)。
Linux機で書きたい人は、この表のとおりに各イメージを所定オフセットへ直接書けば同じものが作れます。
ですので、実際にわたしもPythonつかって自分で各イメージを書き込んでみたところ、うまく起動しました。
ただ、その場合、書き込み先のディスクを間違えると別のドライブを消すので、そこだけは慎重にしましょう。
手順2: 起動して、SSHで入る
書き込んだmicroSDをPico Mに挿し、USB-CでPCに繋ぎます。これが給電になり、そのまま起動します。10秒ほどで、Windows側にネットワークアダプタ「Remote NDIS based Internet Sharing Device」が現れました。追加ドライバは不要でした(Windows 11標準のRNDISドライバで認識)。
ボード側のIPアドレスは 172.32.0.93 に固定されています。ちなみにLuckfox既定のこの 172.32.0.0/16 は、RFC 1918のプライベートアドレス範囲(172.16.0.0〜172.31.255.255)の外なので、同じ範囲への経路がある環境では競合に注意してください。
PC側のRNDISアダプタに、同じネットワークのIPアドレスを手で付けます。管理者権限のPowerShellで、アダプタのifIndexを確認してから設定します。
# RNDIS アダプタの ifIndex を確認(InterfaceDescription が Remote NDIS ... の行)
Get-NetAdapter | Where-Object InterfaceDescription -match 'NDIS'
# その ifIndex に固定 IP を付ける(例: ifIndex 41)。自動で付いた 169.254.x.x は先に外す
Remove-NetIPAddress -InterfaceIndex 41 -AddressFamily IPv4 -Confirm:$false -ErrorAction SilentlyContinue
New-NetIPAddress -InterfaceIndex 41 -IPAddress 172.32.0.100 -PrefixLength 16これで ping 172.32.0.93 が返り、SSHで入れます。ユーザーは root、初期パスワードは luckfox です。
$ ssh root@172.32.0.93
# uname -a
Linux luckfox 5.10.160 #22 Wed Jun 11 18:43:57 CST 2025 armv7l GNU/Linux
# free -m
total used free shared buff/cache available
Mem: 33 10 4 0 19 20
# df -h | head -2
Filesystem Size Used Available Use% Mounted on
/dev/root 5.8G 79.1M 5.6G 1% /メモリの合計が33MB。ここに公開の仕組みを載せていきます。
手順3: ボードをインターネットに出す(PC経由)
ここが、Raspberry Piにはなかった一手間です。(しかもあまり本質的ではない・・・)
Pico MはPCとしか繋がっていないので、PCをルーター役にしてボードのパケットを外へ出します。Windowsには WinNAT という仕組みがあり、管理者権限のPowerShellで作れます。まず既存のNATがないか確認してください。
# 既存の NAT を確認(何か表示されたら注意。下の注記を参照)
Get-NetNatWinNATの利用は、Microsoftの公式手順ではHyper-Vが有効になっていることが前提です(本記事のPCはWindows 11 Proで、WSL 2でも使われるHyper-Vの仮想化基盤が有効な状態で実測しています)。
また、WinNATは1台のPCにつき内部NATのサブネットプレフィックスを1つしかサポートしません。DockerやHyper-Vの仮想ネットワークなどが既にNATを作っているPCでは、次のコマンドが通らないことがあります。その場合、既存のNATは他の仕組みが使っているので削除せず、構成への影響を確認してから進めてください。
確認できたら、NATを作ってフォワーディングを有効にします。
New-NetNat -Name LuckfoxNAT -InternalIPInterfaceAddressPrefix 172.32.0.0/16
Set-NetIPInterface -InterfaceIndex 41 -Forwarding Enabledボード側には、デフォルトゲートウェイ(PCのアドレス)とDNSを教えます。
ip route add default via 172.32.0.100 dev usb0
printf 'nameserver 1.1.1.1\nnameserver 8.8.8.8\n' > /etc/resolv.confこれで ping 1.1.1.1 が通り、名前も引けます。ただ、HTTPSはまだ通りません。ここで踏んだのが次の2つでした。
ひとつめは、証明書のルート集(CAバンドル)がイメージに入っていないこと。TLSの相手を検証できないので、Pythonでもエージェントでも certificate verify failed になります。Git for Windowsに同梱されているMozillaのバンドルをそのまま置きました。
# 実機側: 置き場所を作る
mkdir -p /etc/ssl/certs
# Windows 側(PowerShell): PC から実機へコピー(PuTTY 付属の pscp)
pscp -pw luckfox "C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt" root@172.32.0.93:/etc/ssl/certs/ca-certificates.crt
# 実機側: 権限と、python が既定で見に行くパスへのリンク
chmod 644 /etc/ssl/certs/ca-certificates.crt
ln -sf /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pemふたつめは、時計です。起動直後の実機は1970年1月1日から始まっていて、この状態では証明書の有効期間の外なので、これもTLSが失敗します。ntpdは入っているのですが、経路ができる前に起動しているので、いったん手で合わせました。
# 現在の日時(UTC)に置き換えて実行してください
date -s '2026-08-18 08:45:51'この2つを入れたところで、実機からHTTPSが通るようになりました。
# python3 -c 'import urllib.request;print(urllib.request.urlopen("https://download.wirecanal.com/latest/VERSION").read())'
b'0.18.3\n'経路・DNS・時刻は再起動で消えるので、手順6の起動スクリプトにまとめて入れ直します。
配線: LEDを1本つなぐ
公開の前に、今回のデモの主役であるLEDをつないでおきます。使うのは物理ピン4(GPIO1_C7)と、その隣の物理ピン3(GND)です。

ピン配置は公式のGPIOドキュメントに図があります。
USB-Cコネクタを上にしてボードを見て、左の列の上から3番目がGND、4番目がGPIO1_C7です。

ブレッドボード上で
ピン4 → 抵抗(1KΩ)→ LEDアノード(長い足)>LEDカソード(短い足)→ ピン3(GND)
とつないであります。
出力とGNDが隣同士のピンなので配線は最短で済みます。
なお、物理ピン1・2はデバッグ用のシリアルコンソール(UART2)に割り当てられているので、LED用途には使わないでおきます。
つないだら、SSHから点くかどうかを先に確かめておきます。LinuxのGPIOはsysfsからそのまま操作できます。
echo 55 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio55/direction
echo 1 > /sys/class/gpio/gpio55/value # 点灯
echo 0 > /sys/class/gpio/gpio55/value # 消灯55という番号は「GPIO番号 = bank×32 +(groupをA=0〜D=3として)group×8 + 番号」の式から来ています。
GPIO1_C7なら 1×32+2×8+7 = 55 です。echo 1 でブレッドボードのLEDが点けば、配線は合っています。
手順4: WireCanalでcanalを作る
ここからはRaspberry Pi 5の記事と同じ流れです。ダッシュボードの「新しい canal を作成」から、4ステップのウィザードで作ります。今回はアカウントを無料プランのままにして、そのままの条件で進めます。


転送先には localhost:18080 を入れます。Luckfoxの中でこれから立てるWebサーバーのポートです。


作成すると、canal詳細画面に接続ファイル(wirecanal.json)とセットアップ手順が出ます。Linuxタブの手順は「ワンライナーでAgentを入れる → wirecanal.jsonを置く → 起動する」の3つです。

手順5: AgentをLuckfoxに入れて、公開する
WireCanalのAgentは、CPUの種類ごとに1個の静的バイナリです。Luckfox Pico Mは32bit ARM(uname -m で armv7l)なので、wirecanal-linux-arm を使います。ワンライナーの install.sh は uname -m で自動判定して同じファイルを取りに行くのですが、今回のBuildrootイメージには curl が入っていません。なのでPC側でダウンロードして、実機へコピーしました。
# Windows 側(PowerShell)
curl.exe -fsSL https://download.wirecanal.com/latest/wirecanal-linux-arm -o wirecanal
pscp -pw luckfox wirecanal root@172.32.0.93:/root/wirecanal_demo/wirecanal
pscp -pw luckfox wirecanal.json root@172.32.0.93:/root/wirecanal_demo/wirecanal.json # canal 詳細からダウンロードした接続ファイル実機側で実行権限を付けて、バージョンを確認します。
# cd /root/wirecanal_demo && chmod +x wirecanal && chmod 600 wirecanal.json
# ./wirecanal --version
wirecanal v.0.18.3 powered by Qualiteg Inc. https://qualiteg.com公開する中身は、ブラウザからLEDを点け消しできる小さなWebサーバーにしました。ONボタンを押すと、さっき配線したLEDが実際に光り、OFFで消えます。押されている側のボタンに色が付くので、いまどちらの状態かも画面で分かります。イメージにpython3が入っているので、標準ライブラリだけで書いています。127.0.0.1の18080番で待ち、GET /led で現在の状態を返し、POST /led/on・/led/off で切り替えます。LEDの操作は、配線の節で試したのと同じ /sys/class/gpio への書き込みです。
demo_server.py(実機に置いたもの・全文)
# LED control page for the Luckfox Pico M
# Served on 127.0.0.1:18080 and published to the internet through a WireCanal tunnel.
# The ON/OFF buttons drive GPIO1_C7 (sysfs number 55, physical pin 4) where an LED is wired.
import http.server, subprocess, socket, html, os
GPIO = "55"
GPIO_DIR = "/sys/class/gpio/gpio" + GPIO
def gpio_init():
if not os.path.isdir(GPIO_DIR):
with open("/sys/class/gpio/export", "w") as f:
f.write(GPIO)
with open(GPIO_DIR + "/direction", "w") as f:
f.write("out")
def gpio_set(v):
with open(GPIO_DIR + "/value", "w") as f:
f.write("1" if v else "0")
def gpio_get():
with open(GPIO_DIR + "/value") as f:
return f.read().strip() == "1"
def sh(c):
try: return subprocess.run(c, shell=True, capture_output=True, text=True, timeout=5).stdout.strip()
except Exception as e: return str(e)
PAGE = """<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Luckfox Pico M LED control</title>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#f7fafd;color:#15222b}
.wrap{max-width:720px;margin:40px auto;padding:0 20px}.card{background:#fff;border:1px solid #dde5ee;border-radius:16px;padding:28px 32px;box-shadow:0 2px 12px rgba(0,0,0,.05)}
h1{font-size:1.5rem;margin:0 0 6px}.sub{color:#5f6b78;margin:0 0 20px}table{border-collapse:collapse;width:100%%}td{padding:8px 6px;border-bottom:1px solid #eef2f6;vertical-align:top}
td:first-child{color:#5f6b78;width:150px}code{background:#f1f5f9;padding:2px 6px;border-radius:6px}
.led{display:flex;align-items:center;gap:16px;margin:18px 0 22px}
.dot{width:26px;height:26px;border-radius:50%%;background:#cfd8e3;transition:all .15s}
.dot.on{background:#e5484d;box-shadow:0 0 14px 3px rgba(229,72,77,.55)}
button{font-size:1rem;font-weight:600;border-radius:10px;border:1px solid #cdd7e1;background:#fff;color:#15222b;padding:10px 26px;cursor:pointer;transition:all .15s}
button.active{background:#1f7ae0;border-color:#1f7ae0;color:#fff}
#btnOn.active{background:#e5484d;border-color:#e5484d}
.state{font-weight:700;min-width:44px}
.foot{color:#8b96a4;font-size:.85rem;margin-top:16px}</style></head><body><div class="wrap"><div class="card">
<h1>Luckfox Pico M — LED control</h1>
<p class="sub">The buttons below drive a real LED wired to GPIO1_C7 (physical pin 4) on the board, through a WireCanal tunnel. No inbound port is exposed directly to the internet.</p>
<div class="led"><div id="dot" class="dot"></div><span id="state" class="state">...</span>
<button id="btnOn" onclick="setLed(1)">ON</button><button id="btnOff" onclick="setLed(0)">OFF</button></div>
<table>
<tr><td>Board</td><td>Luckfox Pico M (Rockchip RV1103, ARM Cortex-A7, 64 MB DDR2)</td></tr>
<tr><td>Hostname</td><td><code>%(host)s</code></td></tr>
<tr><td>Kernel</td><td><code>%(uname)s</code></td></tr>
<tr><td>Uptime</td><td>%(uptime)s</td></tr>
<tr><td>LED pin</td><td><code>GPIO1_C7</code> = sysfs gpio%(gpio)s, physical pin 4 (GND on pin 3)</td></tr>
</table>
<p class="foot">Agent: WireCanal (linux/arm) · Powered by Qualiteg</p>
</div></div>
<script>
async function refresh(){const r=await fetch('/led');const j=await r.json();
document.getElementById('dot').className='dot'+(j.on?' on':'');
document.getElementById('state').textContent=j.on?'ON':'OFF';
document.getElementById('btnOn').className=j.on?'active':'';
document.getElementById('btnOff').className=j.on?'':'active';}
async function setLed(v){await fetch('/led/'+(v?'on':'off'),{method:'POST'});refresh();}
refresh();setInterval(refresh,3000);
</script></body></html>"""
class H(http.server.BaseHTTPRequestHandler):
def _send(self, code, body, ctype="text/html; charset=utf-8"):
data = body.encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if self.path == "/led":
return self._send(200, '{"on": %s}' % ("true" if gpio_get() else "false"), "application/json")
body = PAGE % {
"host": html.escape(socket.gethostname()),
"uname": html.escape(sh("uname -srm")),
"uptime": html.escape(sh("uptime")),
"gpio": GPIO,
}
self._send(200, body)
def do_POST(self):
if self.path == "/led/on":
gpio_set(True)
elif self.path == "/led/off":
gpio_set(False)
else:
return self._send(404, '{"error":"not found"}', "application/json")
self._send(200, '{"on": %s}' % ("true" if gpio_get() else "false"), "application/json")
def log_message(self, *a): pass
gpio_init()
http.server.HTTPServer(("127.0.0.1", 18080), H).serve_forever()ひとつだけ注意点があります。今回のBuildroot環境では python3 -m http.server をそのまま起動するとIPv6側のソケットを開こうとして Address family not supported で落ちました(このイメージのカーネルにはIPv6がありません)。上のコードのように 127.0.0.1 を明示して待ち受ければ問題ありません。
Webサーバーを裏で起動し、続けてAgentを起動します。
cd /root/wirecanal_demo
(setsid nohup python3 demo_server.py >/dev/null 2>&1 &)
./wirecanal -config wirecanal.jsonwirecanal v.0.18.3 powered by Qualiteg Inc. https://qualiteg.com
wirecanal: warning: mode=http かつ http ルール未設定のため、全パスを転送先へ通します。特定のパス・メソッドに絞る場合は http.default: deny + http.rules を設定してください
wirecanal: 自動更新が有効です(無効化は wirecanal.json に "auto_update": false)
wirecanal: canal urym: https://fxsu8b0c.ja000.wirecanal.com へのアクセスをローカルの転送先 localhost:18080 に届けます
wirecanal: 設定は https://app.wirecanal.com/canals/urym をご確認ください
wirecanal 0.18.3: connecting tenant=urym mode=http forward_target=localhost:18080この時点で、ダッシュボードの接続状態が「● 接続中」に変わります。


インターネット側から見てみる
公開URLをブラウザで開きます。
公開URLを開いてONを押したところ。この瞬間、手元のブレッドボードでLEDが実際に光っている

「Linux 5.10.160 armv7l」というカーネルも稼働時間も、さっきSSHで見た実機の生きた値がそのままインターネット越しに出ています。そしてONボタンを押すと、手元のブレッドボードの上でLEDが実際に光ります。ブラウザは公開URLを叩いているだけで、その先で実物が動く。スマホでも同じURLをそのまま開けます。

curlでの実測はこうでした。ページを経由せず、APIを直接叩いてLEDを操作することもできます。
$ curl -s -o /dev/null -w "HTTP %{http_code} / %{time_total}s" https://fxsu8b0c.ja000.wirecanal.com/
HTTP 200 / 0.232s
$ curl -s -X POST https://fxsu8b0c.ja000.wirecanal.com/led/on
{"on": true}
$ curl -s -X POST https://fxsu8b0c.ja000.wirecanal.com/led/off
{"on": false}このときインターネット側からPico Mへ直接到達できる受信ポートは、ひとつもありません。18080番は 127.0.0.1 だけで待っていて、PCのNATにも受信(ポートフォワード)の設定は何も入れていません。公開は、Agentの外向き接続だけで成立しています。SSH(22番)はRNDIS側で待受していますが、これはPCとボードの間だけの閉じたネットワークで、インターネット側からは届きません。
なお、今回は認証を付けていないため、URLを知っている人はページの閲覧もLEDのON/OFF操作もできます。今回は抵抗を介したLED 1本だけのデモなのでそのまま公開していますが、リレー・モーター・鍵・電源制御など実物に影響するGPIOを公開する場合は、canalのアクセス保護(IP制限・認証)やHTTPルールで、操作できる相手・パス・メソッドを必ず絞ってください。
手順6: 常駐させて、電源を抜いても戻ってくるようにする
手順5の起動はターミナルを閉じると止まりますし、経路・DNS・時刻は再起動で消えます。Raspberry Pi OSならsystemdの雛形を置くだけでしたが、Buildrootのinitはbusyboxの /etc/init.d/S??name 方式なので、起動スクリプトを1本書きます。
その前に、スワップを用意しておきます。Linuxから見えるメモリが33MBで、Agentは自動更新のための常駐ウォッチドッグ(wirecanal-safeguard)と組で動くので、そのぶんの余裕を持たせるためです。SDカード上に128MBのファイルを切って有効にしました。
dd if=/dev/zero of=/swapfile bs=1M count=128
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile起動スクリプトは次のとおりです。やっていることは、スワップを有効化し、USB側のネットワーク(usb0)にアドレスが付くのを待ち、経路とDNSを入れ、ntpを起動し直して時計が合うのを待ってから、Webサーバーとエージェントを起動する、という順番です。エージェントは終了したら5秒後に立ち上げ直すループにしています。systemdの Restart=always の代わりです。監視ループのPIDをファイルに控えて、二重起動の防止と、stop で監視ループごと全部止まるようにしてあります(監視ループを残したままエージェントだけkillすると、5秒後に勝手に復活してしまいます)。
/etc/init.d/S99wirecanal(実機に置いたもの・全文)
#!/bin/sh
# WireCanal Agent (Luckfox Pico M / Buildroot busybox init)
WD=/root/wirecanal_demo
SUPERVISOR_PID=/var/run/wirecanal-supervisor.pid
case "$1" in
start)
# 二重起動防止(前回の監視ループが生きていれば何もしない)
if [ -f "$SUPERVISOR_PID" ] && kill -0 "$(cat $SUPERVISOR_PID)" 2>/dev/null; then
echo "wirecanal supervisor already running (pid $(cat $SUPERVISOR_PID))"
exit 0
fi
(
[ -f /swapfile ] && swapon /swapfile 2>/dev/null
i=0; while ! ip -4 addr show usb0 2>/dev/null | grep -q inet && [ $i -lt 60 ]; do sleep 2; i=$((i+1)); done
ip route | grep -q '^default' || ip route add default via 172.32.0.100 dev usb0
grep -q nameserver /etc/resolv.conf 2>/dev/null || printf 'nameserver 1.1.1.1\nnameserver 8.8.8.8\n' > /etc/resolv.conf
/etc/init.d/S49ntp restart >/dev/null 2>&1
i=0; while [ "$(date +%Y)" -lt 2025 ] && [ $i -lt 20 ]; do sleep 3; i=$((i+1)); done
[ "$(date +%Y)" -lt 2025 ] && python3 $WD/settime.py >/dev/null 2>&1
cd $WD && (setsid nohup python3 demo_server.py >/dev/null 2>&1 &)
# Agent が終了したら 5 秒後に立ち上げ直す(systemd の Restart=always の代わり)
cd $WD && while true; do ./wirecanal -config wirecanal.json >> agent.log 2>&1; sleep 5; done
) >/dev/null 2>&1 &
echo $! > $SUPERVISOR_PID
;;
stop)
# 監視ループ → Agent/ウォッチドッグ → Web サーバーの順にすべて止める
if [ -f "$SUPERVISOR_PID" ]; then
kill "$(cat $SUPERVISOR_PID)" 2>/dev/null
rm -f $SUPERVISOR_PID
fi
for p in $(ps | grep -E '[w]irecanal -config|[w]irecanal-safeguard|[d]emo_server' | awk '{print $1}'); do
kill "$p" 2>/dev/null
done
;;
restart) "$0" stop; sleep 1; "$0" start ;;
esac
exit 0途中で呼んでいる settime.py は、ntpが間に合わなかったときの保険で、HTTPの応答ヘッダにある日付で時計を合わせるだけの数行のスクリプトです。これはTLSを使える時刻まで戻すためのデモ用フォールバックで、通常の時刻同期はNTPに任せます。
settime.py(全文)
import urllib.request, urllib.error, email.utils, subprocess
try:
r = urllib.request.urlopen('http://download.wirecanal.com/', timeout=10)
except urllib.error.HTTPError as e:
r = e
t = email.utils.parsedate_to_datetime(r.headers.get('Date'))
subprocess.run(['date', '-u', '-s', t.strftime('%Y-%m-%d %H:%M:%S')])ここで一度ハマった話を書いておきます。busyboxの ip route show default は「default」の指定を無視して全経路を返します。最初はこれで「経路がある」と誤判定して、再起動後に外へ出られませんでした。上のスクリプトのように ip route | grep '^default' で見るのが確実です。もうひとつはPIDの控え方で、サブシェルの中で $$ を書くと、BusyBoxのshでは親のPIDが記録されてしまい、二重起動ガードが効きません。上のスクリプトのように、サブシェルをバックグラウンドにした直後に親側で $! を書き出すのが確実です(これも実測で気づきました)。
実行権限を付けて sync したら、いよいよ再起動です。
chmod +x /etc/init.d/S99wirecanal && sync && reboot再起動しても、公開は自動で復活するか
再起動をかけた瞬間から時間を測り、公開URLが200を返すまでを実測しました。
| 検収項目 | 実測結果 |
|---|---|
| 公開URL | 再起動から33秒でHTTP 200に復活 |
| スワップ | /swapfile 128MB が自動で有効化(復帰直後の使用は9MB) |
| Webサーバー(demo_server.py) | 自動起動。GPIO(gpio55)のエクスポートも自動でやり直し |
| Agent(wirecanal)+ウォッチドッグ | 自動起動・自動接続 |
| LED操作 | 復帰後に公開URLへ POST /led/on → 点灯を実測 |
| メモリ(復帰直後) | used 17MB。Agent本体のRSSは4.4MB、ウォッチドッグは1.1MB |
# uptime
10:31:08 up 0 min, load average: 5.00, 1.25, 0.41
# free -m
total used free shared buff/cache available
Mem: 33 17 1 0 14 13
Swap: 128 9 119
# ps | grep -E 'wirecanal|demo_server'
793 root python3 demo_server.py
794 root ./wirecanal -config wirecanal.json
808 root /root/wirecanal_demo/wirecanal-safeguard -config wirecanal.json -exec /root/wirecanal_demo/wirecanal -state-dir .wirecanal-state復帰直後に公開URLを開くと、ページの稼働時間が「up 0 min」でした。再起動したばかりの実物が、そのままインターネット側から見えている証拠です。USB-Cを物理的に抜き差ししたときも同じで、挿し直すだけで何もせずに公開URLが復活しました。
3,750円のボードでも、外から届くサーバーになった
Luckfox Pico Mは、Linuxから見えるメモリが33MB、有線もWi-Fiもない、USB-Cが1本だけのボードです。それでも、SDカードへの書き込みからインターネット側で公開URLが返るまで、必要だったのは、この記事に書いた手順だけでした。Web公開のための受信ポートは最後まで追加していませんし、WireCanalは無料プランのままです。
Raspberry Pi 5のときと違ったのは、ネットワークをPC経由にする一手間と、Buildrootならではの下ごしらえ(CAバンドル・時刻・busybox init)です。逆に言うと、そこさえ越えれば、Agentのやること自体は同じでした。wirecanal-linux-arm のバイナリを置いて、wirecanal.jsonを隣に置いて、起動する。それだけです。
今回はLED 1本でしたが、この小さなボードには、これからセンサーやカメラを載せて、外出先から見られるものを増やしていきたいとおもいます!
それでは、また次回お会いしましょう!
出典・参考
- Luckfox Wiki(Luckfox Pico RV1103 シリーズの公式ドキュメント・イメージ配布・SDカード書き込み手順)
- WireCanal IoTデバイス対応ページ(対応チップ+Linuxの組み合わせと、対応チップを搭載したデバイスの例)
- WireCanal Linuxセットアップガイド(Agentの配布ファイル名と常駐手順)
- Luckfox Wiki: Flash Image(SDカード書き込みの公式手順。SocToolKitとOSイメージの配布リンクもこのページ)
- Luckfox Wiki: GPIO(ピン配置図と /sys/class/gpio でのGPIO操作)
- SD Card Formatter(SDアソシエーション公式のフォーマットツール)
- New-NetNat(Microsoft Learn・WinNAT のコマンドリファレンス)