どうもカッツプロダクション代表のカッツです、前回のNゲージ制御の続編です!! 今回はちょっと欲を出しすぎて撃沈しました(笑)
今回のコーディング作業はGoogle Geminiさんにお願いしましたが、他のモデルでも全然いけると思います。無料で試すなら体感的にGPTさんがおすすめかもしれません。ヴァイブコーディングのセミナーで生徒さんたちが無料枠で触ってるのを見てると、GPTが一番止まらずにコード生成できてる印象なんで。
前回はラズパイPico W2でNゲージのスライダーで加速・減速するコントローラーの構築までいったんですが、今回はその先。TOMIXの名コントローラー「DU101-CL」を模した、本物の電車みたいに徐々に加速・減速するコントローラーを作ろうとしました。
結論だとWi-Fi版は諦めましたね。でも代わりにやったことが思いのほか楽しかったので、その一部始終を書きます。
目次
目指したものはDU101-CLのマスコン挙動を再現する!
TOMIXのDU101-CLって、Nゲージ好きなら知ってる名機なんですよ※。マスターコントローラー(マスコン)式で、レバーを動かすと非常ブレーキ・減速3〜1・惰性・加速1〜3という段階があって、本物の電車みたいな操作感が味わえるやつです。
※個人的主観 DU-1だろって突っ込みは無しでお願いします、あっちPWMじゃないから常点灯ついてないしorz

単純に「電圧を上げ下げする」んじゃなくて、加速・減速に時間をかけるのがポイントなんですよね。ここを再現したかった。

まずは実機の特性を測ってみる
いきなりコードを書くんじゃなくて、まずは実機の特性を掴むところから始めました。
電圧計で各コントローラーポジションの挙動をサンプリングして、「減速1なら何秒かけてどこまで下がるか」を地道に実測しました(笑)。ここをちゃんとやらないと、なんだか上手くいかない気がして
フルノッチ(加速3)のサンプリング
ノッチ2(加速2)のサンプリング
これ、いわゆるリーバスエンジニアリングとアナライズとか呼ばれるのですね、知ることって重要なんですよね〜AIさんに良いコードをさせるためにも(これAIさんだけじゃないですが)
ヴァイブコーディングで組んだ仕様
観察した結果をもとに、仕様書を地道にカリカリAIさんに投げました。実際にAIに渡したMarkdownがこちらです。
仕様書(AIに投げたMarkdown)
# 鉄道模型Nゲージコントローラー
スマホ(webブラウザベース)で制御できるNゲージコントローラー直流DC10v~0で制御する。
## 仕様など
|項目|仕様|
|-|-|
|最大電圧|10V(12V補助電源を想定し、最大Duty比を制限)|
|ピン周り|pwm=13,GP11はAIN1,GP12はAIN2を利用|
|方向切り替え|リバース(進行方向)切り替えあり|
|HTTP通信方式|Ajax(Fetch API)式。ページ遷移なしで高速応答 **メモリを節約するため**|
|スマホとの通信方式|APモード いわゆるflashair風でpicoがホストになる|
### UIと制御について
スライダー式スッテプ属性?
(HTMLのフォームパーツで良いものがあれば採用)
**念の為 参考のコントローラー写真も添付します**
`fig2.png` ,`fig.png`
#### パーツの役目
- マスターコントローラー
- 非常ブレーキ
- **定電圧調整**で調整した電圧に即時に戻す
- 減速3
- **定電圧調整**で調整した電圧に**120秒かけて**下げる
- 減速2
- **定電圧調整**で調整した電圧に**60秒かけて**下げる
- 減速1
- **定電圧調整**で調整した電圧に**30秒かけて**下げる
- 惰性
- 電圧を一定に固定
- 加速1
- 現在の電圧から**120秒かけて**最大電圧まで上げる
- 加速2
- 現在の電圧から**60秒かけて**最大電圧まで上げる
- 加速3
- 現在の電圧から**30秒かけて**最大電圧まで上げる
- Duty比制御
- スライド調整式
- 数値が表示される(可能であれば電圧換算も)
- 周波数調整
- 0~限界まで
- スライド
- 数値が表示される
- 方向切り替え
**役目は`fig2.png`この写真が参考になりそうです**
## その他細かい指示
- **ラズパイにLEDチカで動作状況の確認ができる様にしてください**
- **wifi周りの?極力バグが少なくなる構成で**
ポイントは、参考写真(fig.png・fig2.png)も一緒に渡していることです。文字だけで「マスコン風のUI」と言っても伝わらないので、実機の写真とポジションの書き込みを添えました。これが効いたのか、UIもそれっぽく仕上がってきたんですよ。

出てきたコードとUI、正直かなりいい感じだったんですよ。UIもそれっぽく仕上がってるし、コードのロジックも問題なさそうに見えました。が、しかし、、、
ラズパイPico W2の限界にぶつかった
そして実機にてすとと、、、ここで壁にぶつかりました。
とにかくWi-FiもWebサーバーも重いんですよ。症状を順に書くとこんな感じです。
- まずアクセスポイントがなかなか出てこない、、、
- やっと出てきて接続してサーバーのURLを叩いても、今度はUIが開かない
- ラズパイ本体を触るとやたら熱々、、、ですよね〜
最終的にスマホに表示されるところまでは行ったんですが、本体はほかほかで動作も怪しい。この状態だとデバッグもまともにできないので、Wi-Fi版は諦めました。
以前作ったシンプルなコントローラーも実は結構怪しかったんですよ。なかなか開かなかったんですが、あれはボタンを押せばすぐ動くという単純な構造だったのでなんとかなってたんですよね。
今回は加減速のタイマー処理が常時走ってる上に、スライダーの値も頻繁にやり取りする構造なので、前回より格段に複雑。Pico W2の処理能力とメモリでは荷が重かった??、ということだと思います。
しかし、TOMIXのエンジニアさん達ってすごいって実感しましたTT
代わりにシェルコマンド式にリプレイスしたら楽しかった
で、諦めて終わり、、、じゃ面白くないので方針転換しました
Webじゃなくてシェルからコマンドで操作する形に作り替えました。ここもヴァイブコーディングで、AIに「コマンドライン形式にリプレイスして」と投げただけです。
PROMPT
コマンドライン形式にリプレイス
一応ヴァイブコーディングで出力されたコード
一応ですが
from machine import Pin, PWM
import sys
import select
import time
import re
# ==========================================
# 1. 各種設定
# ==========================================
MAX_DUTY = 54612 # 12V入力時に最大10Vに抑える制限値 (65535 * 10/12)
# ==========================================
# 2. ハードウェア初期設定
# ==========================================
motor_in1 = Pin(11, Pin.OUT) # AIN1
motor_in2 = Pin(12, Pin.OUT) # AIN2
motor_pwm = PWM(Pin(13)) # PWMA
motor_in1.value(0)
motor_in2.value(0)
motor_pwm.freq(1000) # 初期周波数 1kHz
motor_pwm.duty_u16(0)
led = Pin("LED", Pin.OUT)
led.value(1) # 稼働中サインとしてLED常時点灯
# ==========================================
# 3. 物理演算・状態管理変数
# ==========================================
current_speed = 0.0 # 現在のスピード (0.0 〜 100.0 %)
target_speed = 0.0 # 目標スピード (0.0 〜 100.0 %)
base_speed = 0.0 # 定電圧調整の値 (0.0 〜 100.0 %)
current_dir = "stop" # "fwd", "rev", "stop"
accel_rate = 0.0 # 1秒あたりの速度変化量 (%/sec)
last_update_time = time.ticks_ms()
last_disp_time = time.ticks_ms()
# ==========================================
# 4. モーター物理制御ロジック
# ==========================================
def apply_motor_hardware():
"""実際のピンにPWMと方向を出力(定電圧の下限チェック付き)"""
global current_speed, base_speed, current_dir
# 【定電圧機能の補正】
# 走行中(fwd/rev)であれば、現在の速度が定電圧(base_speed)より下回らないように調整します
effective_speed = current_speed
if current_dir in ["fwd", "rev"]:
effective_speed = max(current_speed, base_speed)
else:
effective_speed = 0.0 # 完全停止(stop)時は安全のため0Vにします
# 0〜100% の速度割合を PWMの値 (0〜54612) に変換
duty = int((effective_speed / 100.0) * MAX_DUTY)
duty = max(0, min(MAX_DUTY, duty))
if current_dir == "fwd":
motor_in1.value(1)
motor_in2.value(0)
motor_pwm.duty_u16(duty)
elif current_dir == "rev":
motor_in1.value(0)
motor_in2.value(1)
motor_pwm.duty_u16(duty)
else:
motor_in1.value(1)
motor_in2.value(1)
motor_pwm.duty_u16(0)
def update_physics():
"""マスコンのタイマー加減速処理"""
global current_speed, target_speed, accel_rate, last_update_time
now = time.ticks_ms()
dt = time.ticks_diff(now, last_update_time) / 1000.0
last_update_time = now
if accel_rate != 0 and current_speed != target_speed:
if current_speed < target_speed:
current_speed += accel_rate * dt
if current_speed >= target_speed:
current_speed = target_speed
accel_rate = 0
elif current_speed > target_speed:
current_speed -= accel_rate * dt
if current_speed <= target_speed:
current_speed = target_speed
accel_rate = 0
apply_motor_hardware()
def process_command(cmd_str):
"""入力された文字コマンドの解析・処理"""
global target_speed, current_speed, accel_rate, base_speed, current_dir
cmd_str = cmd_str.strip().lower()
if not cmd_str:
return
print(f"\n[受信成功]: '{cmd_str}'")
# 1. 進行方向の制御
if cmd_str == "fwd":
current_dir = "fwd"
apply_motor_hardware()
print(">> [方向] 前進 (FORWARD)")
elif cmd_str == "rev":
current_dir = "rev"
apply_motor_hardware()
print(">> [方向] 後進 (REVERSE)")
elif cmd_str == "stop":
current_dir = "stop"
current_speed = 0.0
target_speed = 0.0
accel_rate = 0.0
apply_motor_hardware()
print(">> [方向] 停止 (STOP)")
# 2. 定電圧調整 (例: base 2.0 -> 2.0Vに設定)
elif cmd_str.startswith("base"):
try:
val = float(cmd_str.split()[1])
# 12Vスケールでの割合を計算 (2.0V -> 約16.7%)
base_speed = (val / 12.0) * 100.0
apply_motor_hardware()
print(f">> [定電圧設定] {val:.1f}V (Duty換算: {base_speed:.1f}%)")
except:
print("エラー: 'base 2.0' の形式で数字を入力してください。")
# 3. 加減速ノッチの制御
elif cmd_str == "acc1":
target_speed = 100.0
accel_rate = (100.0 - current_speed) / 120.0 if current_speed < 100.0 else 0
print(">> [加速1] 120秒かけて100%へ加速開始")
elif cmd_str == "acc2":
target_speed = 100.0
accel_rate = (100.0 - current_speed) / 60.0 if current_speed < 100.0 else 0
print(">> [加速2] 60秒かけて100%へ加速開始")
elif cmd_str == "acc3":
target_speed = 100.0
accel_rate = (100.0 - current_speed) / 30.0 if current_speed < 100.0 else 0
print(">> [加速3] 30秒かけて100%へ加速開始")
elif cmd_str == "coast":
target_speed = current_speed
accel_rate = 0
print(f">> [惰性] 現在の速度 ({current_speed:.1f}%) を保持")
elif cmd_str == "dec1":
target_speed = base_speed
accel_rate = (current_speed - base_speed) / 30.0 if current_speed > base_speed else 0
print(">> [減速1] 30秒かけて定電圧へ減速開始")
elif cmd_str == "dec2":
target_speed = base_speed
accel_rate = (current_speed - base_speed) / 60.0 if current_speed > base_speed else 0
print(">> [減速2] 60秒かけて定電圧へ減速開始")
elif cmd_str == "dec3":
target_speed = base_speed
accel_rate = (current_speed - base_speed) / 120.0 if current_speed > base_speed else 0
print(">> [減速3] 120秒かけて定電圧へ減速開始")
elif cmd_str in ["ebrake", "eb"]:
target_speed = base_speed
current_speed = base_speed
accel_rate = 0
apply_motor_hardware()
print(f">> [非常ブレーキ] 即時に定電圧 ({base_speed * 0.12:.1f}V) へ減速")
# 4. 周波数調整 (例: freq 1000)
elif cmd_str.startswith("freq"):
try:
freq_val = int(cmd_str.split()[1])
motor_pwm.freq(freq_val)
print(f">> [周波数] {freq_val}Hz に変更")
except:
print("エラー: 'freq 1000' の形式で入力してください。")
else:
print(f"不明なコマンド: '{cmd_str}'")
# ==========================================
# 5. メインループ
# ==========================================
print("\n==========================================")
print(" Nゲージ シェルコントローラー 起動完了")
print("==========================================")
try:
while True:
# 物理計算を常に更新
update_physics()
# 加減速中の進捗状況を表示
now = time.ticks_ms()
if time.ticks_diff(now, last_disp_time) >= 1000:
last_disp_time = now
if accel_rate != 0:
print(f"[走行中] 速度: {current_speed:5.1f}% | 方向: {current_dir}")
# シリアル入力チェック(1行読み込み)
if select.select([sys.stdin], [], [], 0.001)[0]:
line = sys.stdin.readline()
process_command(line)
time.sleep(0.01)
finally:
# シャットダウン処理
current_dir = "stop"
current_speed = 0.0
apply_motor_hardware()
led.value(0)
print("\n[安全停止] 通電を遮断しました。")
ちなみに非常ブレーキのログ表示、電圧の数値がちょっとズレてます(笑)動作には影響ないです
あっという間にリプレイスされました。UI周りの実装がごっそり消えて、シリアル経由でコマンドを叩く形に。当然ですがWi-Fiの負荷がゼロになるので、動作は劇的に安定しました。
そしてこれ、楽しかったんですよ!! PCからコマンドを打ち込んで、「加速2」「惰性」「減速1」と指示を送ると、目の前のNゲージがその通りに動く。マスコンのレバーを操作する感覚とはまた違う、運転指令みたいな面白さがあります。
実際のコマンドラインでの走行動画
次にやりたいことはMCPで音声操作!
コマンドを打ちながら思いついたんですが、これMCPで音声操作できるんじゃないですかね?!
コマンドライン形式になってるということは、AIエージェントから叩ける構造になってるということです。ということは「加速して」「そろそろ止めて」と話しかけるだけでNゲージが動く、みたいなことができるはず。
今度やってみます!! 完全にロマンの領域ですが(笑) こういうのが一番楽しいんですよねー
体感的にはわかってたんですが、ハードの制約は正直だなと改めて痛感しましたorz
AIがどれだけいいコードを書いても、載せる先の性能が足りなければ動かない。ここはAIには解決できない領域ですね。
ではでは今日はここまで!AIを活用した〇〇・ヴァイブコーディングに興味がある方気軽にご連絡くださいなんちゃって!