見出し画像

ローラースケートロボをラジコンにする

概要

タミヤのローラースケートロボをラジコン化した過程のまとめ。

きっかけ

MFT2022 タミヤさんブースでローラースケートロボの自律走行作例を拝見してラジコン化したいぞ熱がふつふつと沸いてきたので。

必要なもの

ベースキット:タミヤ ローラースケートロボ工作セット
マイコン:M5Atom Lite(ロボット)/ M5Stack(コントローラ)
モータードライバ:ATOM Motionキット モーター/サーボドライバ
コントローラ:VKLSVAN ジョイスティック
サーボモータ:SG90(互換品)
ステアリングリンケージ:タミヤ ボールリンクマスダンパー

ハード

キット組み立て

まずはローラースケートロボを説明書通り組み立ててひとしきり遊びましょう。説明書通りに組み立てると誰でも同じものができるタミヤの工作キットは偉大。

サーボの取り付け

ローラースケートロボにはサーボを取り付ける想定の穴があいてます(MFTでタミヤさんに教えてもらった)。そんなん聞いたら欲しくなるやん。

赤丸のところがサーボの取付穴

ダッシュでキットを買いに行ってバリバリ組み立ててサーボを取り付けてみた訳ですが、一点で留めるためトルクを受けてサーボ自体が少し回転してしまう…ので、それを解消するためブラケットを作ります。

サーボモーターブラケット
サーボを複数の固定点で支えます
回転軸はキット付属ビスからロックナット締めに置換

図面引いときながらプラバン切り出しで手作りしましたが、これでサーボがグラグラすることも無くなりました。

リンクの作成

次はサーボホーンとスケートロボ本体をリンクで繋ぎたい訳ですが、なかなか都合の良いものが見当たらない。で、買ってきたのがミニ四駆パーツのボールリンクマスダンパー。ここからボールリンクだけ拝借してタイロッドを作成。ロッドエンド同士を接続する芯材はその辺に転がっていた2mmシャフトを使ってます。マスダンパー君はお子様に「いらないからいいものをあげよう」と渡しておきましょう。

ボールリンクで作ったステアリングリンク

ATOM Motionと接続

ここまで来ればハードウェアの準備はあと一歩。ロボット背面にATOM Motionユニットを両面テープでくっつけて、DCモータとサーボを接続します。これでロボット本体は完成です。

M1にDCモータ、S1とS3にサーボを接続します
背負い物がでかい

コントローラの作成

切り出したMDF板にジョイスティックとM5Stackを固定し、簡単なコントローラを作成しました。接続は下記システム構成図を参照してください。

MDFを切り出して作成したコントローラ

ソフト

システム構成

システム構成は以下の通りです。今回、M5StackとM5Atomの通信にESP-NOWを使います。ESP同士でのみ使える無線通信ですが、低遅延の通信が簡単に実現できます。

システム構成図

ロボット(受信側)

プログラミングにはM5Flow(https://flow.m5stack.com/)を使います。色々試すときはブロックプログラミングツールが本当にお手軽で便利だと思います。
if文だらけでやたら長いですが、基本的にやってることはESP-NOWで受信したメッセージ毎に、
・モータの回転数を変更する
・LEDの色を変更する

これだけです。
下記はブロックプログラムをPythonに変更したもので、これをM5Flowで読み込んでmacアドレスを自分のものに書き換えれば動くはず。です。

from m5stack import *
from m5ui import *
from uiflow import *
from libs.m5_espnow import M5ESPNOW
from base.Motion import Motion

addr = None
data = None
speed = None

now = M5ESPNOW()

def recv_cb(dummy):
  global addr,data,speed
  addr, data = now.espnow_recv_str()
  if data == 'ACC3':
    motion.set_motor_speed(1, speed)
  if data == 'ACC2':
    motion.set_motor_speed(1, int((speed * 0.67)))
  if data == 'ACC1':
    motion.set_motor_speed(1, int((speed * 0.33)))
  if data == 'ACC0':
    motion.set_motor_speed(1, 0)
  if data == 'LEF3':
    motion.set_servo_angle(1, 110)
    motion.set_servo_angle(3, 110)
  if data == 'LEF2':
    motion.set_servo_angle(1, 103)
    motion.set_servo_angle(3, 103)
  if data == 'LEF1':
    motion.set_servo_angle(1, 97)
    motion.set_servo_angle(3, 97)
  if data == 'RIG3':
    motion.set_servo_angle(1, 70)
    motion.set_servo_angle(3, 70)
  if data == 'RIG2':
    motion.set_servo_angle(1, 77)
    motion.set_servo_angle(3, 77)
  if data == 'RIG1':
    motion.set_servo_angle(1, 83)
    motion.set_servo_angle(3, 83)
  if data == 'NUT0':
    motion.set_servo_angle(1, 90)
    motion.set_servo_angle(3, 90)
  if data == 'KAM1':
    speed = 63
    rgb.setColorAll(0x00ff00)
  if data == 'NAM1':
    speed = 80
    rgb.setColorAll(0x0000ff)
  if data == 'ONI1':
    speed = 127
    rgb.setColorAll(0xff0000)
  if data == 'NUL0':
    speed = 0
    rgb.setColorAll(0x000000)

  pass
now.espnow_recv_cb(recv_cb)

motion = Motion()
speed = 0
rgb.setColorAll(0x000000)
now.espnow_add_peer('ffffffffffff', 1, 1, False) #ffffffffffffは送信側のmacアドレス

コントローラ(送信側)

同様にコントローラ側もM5Flowでプログラムします。
こちらのプログラムもやってることは単純で
・ジョイスティックの傾きに応じて特定のメッセージを送信する
・ボタン押下で特定のメッセージを送信する

ただそれだけです。
ボタン押下でモード(速度域)が変化して、それを示すアイコンがM5Stackに表示されます。モードの詳細は後述。

from m5stack import *
from m5ui import *
from uiflow import *
from libs.m5_espnow import M5ESPNOW
from easyIO import *

setScreenColor(0x222222)

now = M5ESPNOW()

acc = M5TextBox(41, 56, "ACCEL", lcd.FONT_Default, 0xFFFFFF, rotate=0)
steer = M5TextBox(237, 56, "STEER", lcd.FONT_Default, 0xFFFFFF, rotate=0)
batt = M5TextBox(255, 0, "batt:000", lcd.FONT_Default, 0xFFFFFF, rotate=0)
accstat = M5TextBox(41, 89, "OFF", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
strstat = M5TextBox(237, 89, "N0", lcd.FONT_DejaVu24, 0xFFFFFF, rotate=0)
image_kame = M5Img(26, 150, "res/kame.png", True)
image_nami = M5Img(120, 150, "res/nami.png", True)
image_oni = M5Img(214, 150, "res/oni.png", True)

def buttonA_wasPressed():
  # global params
  image_nami.hide()
  image_oni.hide()
  image_kame.show()
  now.espnow_send_str(1, 'KAM1')
  pass
btnA.wasPressed(buttonA_wasPressed)

def buttonB_wasPressed():
  # global params
  image_kame.hide()
  image_oni.hide()
  image_nami.show()
  now.espnow_send_str(1, 'NAM1')
  pass
btnB.wasPressed(buttonB_wasPressed)

def buttonC_wasPressed():
  # global params
  image_kame.hide()
  image_nami.hide()
  image_oni.show()
  now.espnow_send_str(1, 'ONI1')
  pass
btnC.wasPressed(buttonC_wasPressed)

def buttonA_pressFor():
  # global params
  image_kame.hide()
  image_nami.hide()
  image_oni.hide()
  now.espnow_send_str(1, 'NUL0')
  pass
btnA.pressFor(0.8, buttonA_pressFor)

def buttonB_pressFor():
  # global params
  image_kame.hide()
  image_nami.hide()
  image_oni.hide()
  now.espnow_send_str(1, 'NUL0')
  pass
btnB.pressFor(0.8, buttonB_pressFor)

def buttonC_pressFor():
  # global params
  image_kame.hide()
  image_nami.hide()
  image_oni.hide()
  now.espnow_send_str(1, 'NUL0')
  pass
btnC.pressFor(0.8, buttonC_pressFor)

image_kame.hide()
image_nami.hide()
image_oni.hide()
batt.setText(str((str('batt:') + str((power.getBatteryLevel())))))
now.espnow_add_peer('ffffffffffff', 1, 0, False) #ffffffffffffは受信側のmacアドレス
while True:
  if (analogRead(35)) >= 1270:
    accstat.setText('HI')
    now.espnow_send_str(1, 'ACC3')
  elif (analogRead(35)) > 950:
    accstat.setText('MID')
    now.espnow_send_str(1, 'ACC2')
  elif (analogRead(35)) > 630:
    accstat.setText('LO')
    now.espnow_send_str(1, 'ACC1')
  else:
    accstat.setText('OFF')
    now.espnow_send_str(1, 'ACC0')
  if (analogRead(36)) >= 1270:
    strstat.setText('R3')
    now.espnow_send_str(1, 'RIG3')
  elif (analogRead(36)) > 950:
    strstat.setText('R2')
    now.espnow_send_str(1, 'RIG2')
  elif (analogRead(36)) > 630:
    strstat.setText('R1')
    now.espnow_send_str(1, 'RIG1')
  elif (analogRead(36)) <= 0:
    strstat.setText('L3')
    now.espnow_send_str(1, 'LEF3')
  elif (analogRead(36)) < 285:
    strstat.setText('L2')
    now.espnow_send_str(1, 'LEF2')
  elif (analogRead(36)) < 570:
    strstat.setText('L1')
    now.espnow_send_str(1, 'LEF1')
  else:
    strstat.setText('N0')
    now.espnow_send_str(1, 'NUT0')
  wait_ms(2)

できた

ここに記載している以外にも色々試してて一回で完成にたどり着いたわけじゃないですが、何とか完成しました。以下、モードの解説と走行動画です。

モード解説

M5Stack側のボタンを押すと、速度域が異なる3つのモードが選べる仕様にしました。
・亀モード(MAX電圧の50%。マジで亀。)
・並モード(MAX電圧の63%。気持ちよく走らせるのにはこれが限界。)
・鬼モード(MAX電圧。鬼むず。)

亀モード。めっっっちゃ遅い。
並モード。これが走らせてて一番楽しい。
鬼モード。すぐこける。

走らせてみよう

こけたら自分で起き上がれないのが玉にキズ。でもかわいい。楽しい。


この記事が気に入ったらサポートをしてみませんか?