Roller485 Liteを回してみた

概要

統合型ブラシレス DC モーターモーションコントロールキット(Roller485 Lite)というモーターなのにディスプレイが付いている。
興味をひかれたので使ってみる。
コントローラにはATOM Matrixを使う。

環境

  • モーター Roller485 Lite
  • コントローラ ATOM Matrix
  • MQTTブローカー
  • Arduino IDE バージョン: 2.3.4

構成図

MQTTブローカー PCの起動でサービスも起動する。

使い方

Pythonからコマンドを送る
MQTT_Matrix_pub_commans_NEW.pyをIDLEで開き、実行する。
MQTTを使ってモーターに送るコマンド(変数commans )を調整する。

commans = (("SPIN", 100, 5), ("POS", 30, 5), ("SPIN", 0, 5))
"SPIN": モーターの回転指示
100:    モーターの回転数(RTM)
5:      次のコマンドを送るまでの時間(sec)。通常はこの時間指定した回転数を維持する
"POS"   現在の回転位置をレスポンスで取得する

コマンドプロンプトからコマンドを送る

(1) PC(GMK)にリモートログイン
(2) cd C:\Program Files\mosquitto
トピックをパブリッシュする。
(3) mosquitto_pub -h localhost -t motor/command -m {\"seq\":1,\"cmd\":\"SPIN\",\"rpm\":100,\"sec\":5}
                                                   100RPMでモーターを回す。コマンドではsecに意味はない。
(4) mosquitto_pub -h localhost -t motor/command -m {\"seq\":1,\"cmd\":\"POS\",\"rpm\":0,\"sec\":5}
                                                   モーターのエンコーダー値をレスポンスで取得する。
トピックをサブスクライブする。
(5) mosquitto_sub -h localhost -t motor/response   
MQTT_MX_OTA_SUB_commands_NEW

// プログラム名: MQTT_MX_OTA_SUB_commands_NEW:2025-09-17

// 機能概略:

// – MQTT通信を利用して、Atom Matrixに接続されたモーターを遠隔制御します。

// – 受信トピック: “motor/command”

// – cmdが”SPIN”の場合:

//   – “rpm”と”sec”の値に応じてモーターを回転させ、指定された時間、速度を維持します。

//   – 速度は滑らかに目標値に遷移します。

//   – “sec”経過後、受信メッセージを”motor/response”にエコーバックします。

//   – “sec”経過後もモーターは指定された速度で回転し続けます。

// – cmdが”POS”の場合:

//   – モーターの動きは変えず、現在の位置情報を取得します。

//   – {“seq”: 受信値, “POSITION”: 現在位置}というメッセージを直ちに”motor/response”に送信します。

//   – 送信後もモーターの状態は維持され、次のコマンドを待ちます。

// – cmdがない、または”SPIN”, “POS”以外の値の場合:

//   – “motor/response”に{“command”: “cmd ERROR”}を送信します。

//   – モーターの動きは現在の状態を維持し、次のコマンドを待ちます。

// – Wi-Fi、固定IP、OTAアップデートに対応しています。

#include <unit_rolleri2c.hpp> // M5Stack Unit RollerモジュールをI2C経由で制御するためのライブラリ

#include <WiFi.h>              // Wi-Fi接続機能を提供

#include <ArduinoOTA.h>        // OTA(Over-the-Air)ファームウェアアップデート用ライブラリ

#include “M5Atom.h”            // M5Atom Matrixを制御するためのライブラリ

#include <PubSubClient.h>      // MQTTクライアント機能を提供

#include <ArduinoJson.h>       // JSONデータの解析用ライブラリ

// ====== ユーザー設定変数 ======

// #includeの直後に目立つように記述する

// MQTTブローカー設定

const char* mqtt_broker = “192.168.0.13”;

const int mqtt_port = 1883;

// MQTTトピック設定

const char* mqtt_topic_sub = “motor/command”;

const char* mqtt_topic_pub = “motor/response”;

// Wi-Fiネットワークの認証情報を設定

const char *ssid = “aterm-2b4139-a”;

const char *password = “3e00cfa4ba409”;

// デバイスに割り当てる固定IPアドレス

const IPAddress ip(192, 168, 0, 125);

const IPAddress gateway(192, 168, 0, 1);

const IPAddress subnet(255, 255, 255, 0);

const IPAddress dns1(192, 168, 0, 1);

// OTAアップデート時のネットワーク上のデバイス名

const char* host_name = “M5Stack-AtomMatrix”;

// メインループの遅延時間 (ms)。この値を調整して、モーターの回転の滑らかさを変更します。

int delay_ms = 8;

// ====== オブジェクトインスタンスの作成 ======

UnitRollerI2C roller;

WiFiClient wifiClient;

PubSubClient mqttClient(wifiClient);

StaticJsonDocument<256> doc;

// ====== モーター制御と状態管理 ======

int32_t currentTargetSpeed = 0; // 現在の設定速度 (RPM)

int32_t received_rpm = 0; // 受信したrpmの値

int received_sec = 0;      // 受信したsecの値

bool isCommandReceived = false; // 新しいコマンドが受信されたかどうかのフラグ

unsigned long commandReceivedTime = 0; // コマンドを受信した時刻

// **追加: プログラム起動時のエンコーダ初期位置**

int32_t initialPosition = 0;

// プログラムの状態を管理するenum

enum ProgramState {

    STATE_WAITING_FOR_COMMAND,      // コマンド待機状態

    STATE_TRANSITIONING_TO_NEXT_SPEED,  // 次の速度へ滑らかに移行中

    STATE_RUNNING_AT_TARGET_SPEED,      // 目標速度で回転中

};

ProgramState currentState = STATE_WAITING_FOR_COMMAND; // 初期状態はコマンド待機

// ====== LED設定 ======

// LEDマトリックスの色を定義

#define LED_COLOR_WIFI_CONNECTING 0x0000FF // 青色 (Wi-Fi接続中)

#define LED_COLOR_WIFI_CONNECTED 0x00FF00   // 緑色 (Wi-Fi接続成功)

#define LED_COLOR_MQTT_CONNECTING 0xFFFF00 // 黄色 (MQTT接続中)

#define LED_COLOR_ERROR 0xFF0000           // 赤色 (エラー)

#define LED_COLOR_IDLE 0x000000            // 消灯 (待機状態)

// rpm値とsec値に基づいたLED表示を更新する関数

void updateLeds(int rpm, int sec) {

    M5.dis.fillpix(LED_COLOR_IDLE); // 全LEDを一旦消灯

    // RPM値に応じたLED表示

    int rpm_leds_to_light = constrain(abs(rpm) / 10, 0, 15);

    uint32_t rpm_color = (rpm >= 0) ? 0x00FF00 : 0x0000FF; // 正なら緑、負なら青

    for (int i = 0; i < rpm_leds_to_light; i++) {

        M5.dis.drawpix(i, rpm_color); // 1行目左から3行目右へ順に点灯

    }

    // SEC値に応じたLED表示

    int sec_leds_to_light = constrain(sec, 0, 10);

    uint32_t sec_color = 0xFFFF00; // 黄色

    for (int i = 0; i < sec_leds_to_light; i++) {

        M5.dis.drawpix(15 + i, sec_color); // 4行目左から5行目右へ順に点灯

    }

}

// I2Cバス上でRollerモジュールを見つけて初期化する関数

void initRoller() {

    // Atom MatrixのI2Cピンは固定 (SDA:26, SCL:32)

    const auto sda = 26;

    const auto scl = 32;

    for (auto addr = 1; addr < 127; ++addr) {

        if (roller.begin(&Wire, addr, sda, scl)) {

            // モジュールが見つかったら初期設定を実行

            roller.setRGBMode(roller_rgb_t::ROLLER_RGB_MODE_USER_DEFINED);

            roller.setRGB(0);

            roller.resetStalledProtect();

            roller.setOutput(0);

            roller.setMode(ROLLER_MODE_SPEED);

            return;

        }

    }

    for (int i = 0; i < 25; i++) {

        M5.dis.drawpix(i, LED_COLOR_ERROR);

    }

    while(true) { delay(1000); }

}

// Wi-Fi接続中のピクセル点滅アニメーション

void connectAnimation() {

    int count = 0;

    while (WiFi.waitForConnectResult() != WL_CONNECTED) {

        M5.dis.drawpix(count % 25, LED_COLOR_WIFI_CONNECTING);

        delay(500);

        M5.dis.drawpix(count % 25, LED_COLOR_IDLE);

        count++;

    }

}

// 指定された目標速度へ滑らかに変化させる関数

// @param targetSpeed: 目標速度 (RPM)

// @return:

//   true: 遷移が完了した

//   false: 遷移が継続中

bool updateSpeedTransition(int32_t targetSpeed) {

    const int step_size = 2; // 加速・減速の変化量

    if (currentTargetSpeed < targetSpeed) {

        currentTargetSpeed += step_size;

        if (currentTargetSpeed > targetSpeed) {

            currentTargetSpeed = targetSpeed;

        }

    } else if (currentTargetSpeed > targetSpeed) {

        currentTargetSpeed -= step_size;

        if (currentTargetSpeed < targetSpeed) {

            currentTargetSpeed = targetSpeed;

        }

    } else {

        return true; // 遷移完了

    }

    roller.setSpeed(currentTargetSpeed * 100);

    return false; // 遷移継続中

}

// MQTTメッセージを受信したときに呼び出される関数

void onMqttMessage(char* topic, byte* payload, unsigned int length) {

    String receivedPayload = “”;

    for (unsigned int i = 0; i < length; i++) {

        receivedPayload += (char)payload[i];

    }

    // JSONペイロードの解析

    DeserializationError error = deserializeJson(doc, receivedPayload);

    // JSON解析が成功し、かつ”cmd”キーが存在する場合

    if (!error && doc.containsKey(“cmd”)) {

        String cmd = doc[“cmd”].as<String>();

        // — cmdが”SPIN”の場合 —

        if (cmd == “SPIN”) {

            // 受信ペイロードからrpmとsecを取得

            // rpmとsecが欠けていても0として処理を続行

            received_rpm = doc.containsKey(“rpm”) ? doc[“rpm”].as<int32_t>() : 0;

            received_sec = doc.containsKey(“sec”) ? doc[“sec”].as<int>() : 0;

            // モーターを有効化し、状態を速度遷移に設定

            roller.setOutput(1);

            isCommandReceived = true;

            commandReceivedTime = millis();

            currentState = STATE_TRANSITIONING_TO_NEXT_SPEED;

            // LED表示を更新

            updateLeds(received_rpm, received_sec);

        }

        // — cmdが”POS”の場合 —

        else if (cmd == “POS”) {

            // **修正: 初期位置からの差分を計算して、相対的な位置を取得**

            int32_t current_position = roller.getPosReadback() – initialPosition;

            // 応答用のJSONドキュメントを作成

            StaticJsonDocument<64> posResponseDoc;

            posResponseDoc[“seq”] = doc.containsKey(“seq”) ? doc[“seq”].as<int>() : 0;

            posResponseDoc[“POSITION”] = current_position;

            String posResponse;

            serializeJson(posResponseDoc, posResponse);

            // “motor/response”に即座に送信

            mqttClient.publish(mqtt_topic_pub, posResponse.c_str());

            // **モーターの状態やプログラムの状態は変更しない**

            // **既存の動作を維持し、次の受信を待つ**

        }

        // — それ以外のcmdの場合 —

        else {

            // 無効なコマンドのためエラー応答を返す

            StaticJsonDocument<64> errorDoc;

            errorDoc[“command”] = “cmd ERROR”;

            String errorResponse;

            serializeJson(errorDoc, errorResponse);

            mqttClient.publish(mqtt_topic_pub, errorResponse.c_str());

            // **モーターの状態やプログラムの状態は変更しない**

        }

    } else {

        // JSON解析失敗時、または”cmd”キーがない場合

        // エラー応答を返す

        StaticJsonDocument<64> errorDoc;

        errorDoc[“command”] = “cmd ERROR”;

        String errorResponse;

        serializeJson(errorDoc, errorResponse);

        mqttClient.publish(mqtt_topic_pub, errorResponse.c_str());

        // **モーターの状態やプログラムの状態は変更しない**

    }

}

// MQTTブローカーに接続する関数

void connectToMqtt() {

    M5.dis.fillpix(LED_COLOR_MQTT_CONNECTING);

    while (!mqttClient.connected()) {

        String clientId = “M5AtomMatrix-“;

        clientId += String(random(0xffff), HEX);

        if (mqttClient.connect(clientId.c_str())) {

            mqttClient.subscribe(mqtt_topic_sub);

            M5.dis.fillpix(LED_COLOR_IDLE);

        } else {

            M5.dis.fillpix(LED_COLOR_ERROR);

            delay(5000);

            M5.dis.fillpix(LED_COLOR_MQTT_CONNECTING);

        }

    }

}

void setup() {

    // M5Atomの初期化

    M5.begin(true, false, true);

    initRoller(); // Unit Rollerモジュールを初期化

    // **修正: プログラム起動時にエンコーダの初期値を記録**

    initialPosition = roller.getPosReadback();

    // Wi-FiとOTAを初期化

    if (!WiFi.config(ip, gateway, subnet, dns1)) {

    }

    WiFi.mode(WIFI_STA);

    WiFi.begin(ssid, password);

    connectAnimation();

    ArduinoOTA.setHostname(host_name);

    // OTAイベントハンドラの設定

    ArduinoOTA.onStart([]() {

        M5.dis.fillpix(0xFFFFFF);

    });

    ArduinoOTA.onEnd([]() {

        M5.dis.fillpix(0x00FF00);

        delay(3000);

        ESP.restart();

    });

    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {

        int leds_to_light = (progress / (total / 25));

        for (int i = 0; i < 25; i++) {

            M5.dis.drawpix(i, (i < leds_to_light) ? 0x0000FF : 0x000000);

        }

    });

    ArduinoOTA.onError([](ota_error_t error) {

        M5.dis.fillpix(0xFF0000);

        delay(100);

        M5.dis.fillpix(0x000000);

        delay(100);

        M5.dis.fillpix(0xFF0000);

    });

    ArduinoOTA.begin();

    // MQTTクライアントの設定

    mqttClient.setServer(mqtt_broker, mqtt_port);

    mqttClient.setCallback(onMqttMessage);

    // MQTT接続の開始

    connectToMqtt();

    M5.dis.fillpix(LED_COLOR_IDLE);

}

void loop() {

    M5.update();

    ArduinoOTA.handle();

    if (!mqttClient.connected()) {

        connectToMqtt();

    }

    mqttClient.loop();

    switch (currentState) {

        case STATE_WAITING_FOR_COMMAND:

            // モーターの状態は変更しない

            break;

        case STATE_TRANSITIONING_TO_NEXT_SPEED:

            // 目標速度への遷移が完了したら状態を更新

            if (updateSpeedTransition(received_rpm)) {

                // 遷移が完了したら、STATE_RUNNING_AT_TARGET_SPEEDへ移行

                currentState = STATE_RUNNING_AT_TARGET_SPEED;

            }

            break;

        case STATE_RUNNING_AT_TARGET_SPEED:

            // 受信したsec秒が経過したら

            if (millis() – commandReceivedTime >= (unsigned long)received_sec * 1000) {

                if (isCommandReceived) {

                    // JSONドキュメントをシリアライズしてペイロードを作成

                    String jsonString;

                    serializeJson(doc, jsonString);

                    // publish

                    mqttClient.publish(mqtt_topic_pub, jsonString.c_str());

                    isCommandReceived = false;

                    // **変更: 状態を STATE_WAITING_FOR_COMMAND に戻さない**

                }

            } else {

              // 残り時間を計算し、LEDの点灯数を更新

              unsigned long elapsedTime = millis() – commandReceivedTime;

              unsigned long total_duration_ms = (unsigned long)received_sec * 1000;

              int leds_to_light = (int)((total_duration_ms – elapsedTime) / 1000);

              updateLeds(received_rpm, leds_to_light);

            }

            break;

    }

    delay(delay_ms);

}

MQTT_Matrix_pub_commans_NEW.py
# プログラム名: MQTT_Matrix_pub_commans_NEW:2025-09-17
# 機能概略:
#   定義されたモーターコマンドのシーケンスを順番にMQTTで送信します。
#   各コマンドは、"cmd"(コマンド名)、"rpm"(回転数)、"sec"(秒数)の3つの要素から構成されます。
#   Atom Matrixからの応答を受信するたびに次のコマンドを送信し、すべてのコマンドを送信したら終了します。
#   送信メッセージはシーケンス番号(seq)を含み、ターミナルに送受信メッセージを表示します。

import paho.mqtt.client as mqtt
import json
import time

# 送信するコマンドのリスト (cmd, rpm, sec)
# コマンドはタプル形式で定義され、最初の要素がコマンド文字列、2番目がRPM、3番目が秒数です。
commans = (("SPIN", 60, 5), ("SPIN", 30, 5), ("POS", 0, 5), ("SPIN", -30, 5), ("SPIN", 0, 2))
# commans = (("SPIN", 60, 5), ("SPIN", -60, 5), ("SPIN", 60, 5), ("SPIN", 30, 5), ("SPIN", -30, 5), ("SPIN", 0, 1))
# commans = (("SPIN", 150, 10), ("SPIN", -150, 10), ("SPIN", 150, 10), ("SPIN", -150, 10), ("SPIN", 150, 10), ("SPIN", -150, 10), ("SPIN", 0, 1))
commans = (("SPIN", 100, 5), ("POS", 30, 5), ("SPIN", 0, 5))
commans = (("SPIN", 100, 5), ("SPIN", -150, 5), ("SPIN", 100, 5), ("SPIN", 0, 5), ("SPIN", 100, 5), ("SPIN", -150, 5), ("SPIN", 100, 5), ("SPIN", 0, 5))


# 現在のコマンドインデックス
command_index = 0


# ====== 設定変数 ======
# MQTTブローカーのホストとポート
BROKER_HOST = "192.168.0.13"
BROKER_PORT = 1883

# 送信トピックと受信トピック
TOPIC_COMMAND = "motor/command"
TOPIC_RESPONSE = "motor/response"



# 通信の制御フラグ
# False: 次のメッセージは送信しない
# True: 次のメッセージを送信可能
next_message_ready = True
# メッセージのシーケンス番号
message_sequence = 0

# ====== コールバック関数 ======

def on_connect(client, userdata, flags, rc, properties):
    """ブローカーに接続したときのコールバック関数"""
    print(f"Connected to MQTT Broker with result code {rc}")
    # 応答を受信するためにトピックを購読する
    client.subscribe(TOPIC_RESPONSE)
    print(f"Subscribed to topic: {TOPIC_RESPONSE}")

def on_message(client, userdata, msg):
    """メッセージを受信したときのコールバック関数"""
    global next_message_ready
    print("\n--- Received message ---")
    print(f"Topic: {msg.topic}")
    
    try:
        # ペイロードを文字列にデコードし、JSONとして解析する
        payload_str = msg.payload.decode("utf-8")
        payload_json = json.loads(payload_str)
        print(f"Payload: {payload_json}")
    except json.JSONDecodeError:
        print(f"Payload (non-JSON): {payload_str}")
        
    print("----------------------")
    
    # 次のメッセージ送信を許可する
    next_message_ready = True

def send_message(client):
    """
    メッセージを送信する関数。
    commansリストから現在のcmd, rpm, secの値を取得し、MQTTメッセージとして送信します。
    """
    global message_sequence
    global next_message_ready
    global command_index

    # 現在のコマンドを取得
    cmd_value = commans[command_index][0]
    rpm_value = commans[command_index][1]
    sec_value = commans[command_index][2]
    
    # シーケンス番号をインクリメント
    message_sequence += 1
    
    # 送信するペイロードをJSON形式で作成
    payload = {
        "seq": message_sequence,
        "cmd": cmd_value,
        "rpm": rpm_value,
        "sec": sec_value
    }
    
    # ペイロードをJSON文字列に変換
    payload_str = json.dumps(payload)
    
    # MQTTメッセージを送信
    client.publish(TOPIC_COMMAND, payload_str)
    
    print("--- Sent message ---")
    print(f"Topic: {TOPIC_COMMAND}")
    print(f"Payload: {payload_str}")
    print("--------------------")
    
    # 次の応答を待つため、送信を一時的に停止
    next_message_ready = False
    
    # 次回送信のためにインデックスを更新
    command_index += 1

# ====== メイン処理 ======
if __name__ == "__main__":
    # MQTTクライアントのインスタンスを作成
    # 最新のAPIバージョンを指定して警告を解消
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)

    # コールバック関数を登録
    client.on_connect = on_connect
    client.on_message = on_message

    # ブローカーに接続
    try:
        client.connect(BROKER_HOST, BROKER_PORT, 60)
        print("Connecting to broker...")
    except Exception as e:
        print(f"Connection failed: {e}")
        exit()

    # バックグラウンドでネットワーク通信を開始
    client.loop_start()

    # 起動後3秒待ってから最初のメッセージを送信
    print("Waiting 3 seconds before first publish...")
    time.sleep(1)

    try:
        # メッセージの送受信ループ
        # すべてのコマンドを送信するまでループを続ける
        while command_index < len(commans):
            # 応答が来るまで待機
            while not next_message_ready:
                time.sleep(0.1)
            
            # 次のメッセージを送信
            send_message(client)
            
        # すべてのコマンドを送信した後、最後の応答を待つ
        while not next_message_ready:
            time.sleep(0.1)

        print("\nAll commands sent. Exiting script.")
        
    except KeyboardInterrupt:
        print("\nExiting script.")
        
    finally:
        # クライアントを停止
        client.loop_stop()
        client.disconnect()