はじめに
本記事では、ClaudeCodeによるコード生成を試した結果を記載する。
※ClaudeCodeの説明は割愛。
目的
コード生成に定評のあるClaudeCodeを利用して、M5stackへの処理を実装する。
実装内容
今回はM5Stack・M5Tab・Unit Roller485を用いて、
M5Tab: 制御値操作用(タッチパネル)
M5Stack: モータ制御用
Unit Roller485: 制御対象(モータ)
の構成にて処理を実装する。
また、各機器間の通信については、
M5Tab ~ M5Stack間: Wi-Fi
※初めはBLEを想定していたが、M5Tab5掲載のESP32-P4はBluetooth機能を持たないため、Wi-Fiに途中で変更。
M5Stack ~ Roller485間: I2C通信
準備物
○ハードウェア系
- PC(Windows10)
- ESP32系マイコンシリーズ
→本記事ではM5Stack Gray、M5Tab5を使用。 - Unit Roller485
- USB Type-Cケーブル(PC-M5Stack・接続用及び)
○ソフトウェア系
- Arduino IDE
- Claude Code
→プランはProプランを使用している。 - (Visual Studio Code)
→VSCode向けのClaude Code拡張機能を使用する場合。
Claude Code IDEからでも可能。
手順
(1) Visual Studio Codeのダウンロード・インストール。←必要あれば
→本記事では詳細は割愛。⇒Visual Studio リンク
(2) Claude Code登録等の使用可能な環境を準備する。
→前述の通り、こちらではProプランを使用している。
(~1000行程度のスクリプト生成等への使用に推奨されているため。)
こちらの書籍を参考に準備した。
(3) 実際にClaude Codeに作成させる処理内容を記載する。
→M5TabについてはM5Stack Tab5によるRoller485の制御 内で作成した処理・UIをベースにコード生成を依頼した。M5Stackについては、ゼロベースからの生成となる。
以下に、Claude Codeとのやり取りの内容を記載する。
今回は以下の、やり取り内容の生成までClaude Codeに依頼し、少し手直ししたものを記載している。
① 初回コード生成依頼
プロンプト記載内容は以下の通りである。
M5Stack Gray(制御用)、M5Tab5(制御値操作用(タッチパネルによる))、
Roller485 Unit(制御対象)の構成でRoller485を操作する.inoファイルを作成してください。
M5Stack Gray-Roller485間はI2C通信、M5Tab5-M5Stack Gray間はBluetooth通信にてやり取りするものとします。
タッチパネルについては既存ファイルにて使用しているものをそのまま流用してください。
ClaudeCodeにより、以下の2ファイルが生成された。
| ファイル | 役割 |
|---|---|
m5tab5_bt_controller.ino |
M5Tab5側(BLEクライアント・タッチパネル操作) |
m5stackgray_roller485.ino |
M5Stack Gray側(BLEサーバー・I2C中継) |
通信構成は以下の通り。
M5Tab5 --[BLE(NimBLE)]--> M5Stack Gray --[I2C]--> Roller485
BLEのサービス・キャラクタリスティックUUIDを共通定義し、M5Tab5がコマンドバイト列をそのままWriteしてM5Stack GrayがI2C転送する設計とした。
m5tab5_bt_controller.ino の主要部(BLE送信部)
// BLE設定
#define BLE_SERVER_NAME "M5Gray_Roller"
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHAR_CMD_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
// コマンド送信 (BLE経由でM5Stack Grayへ送信)
void sendCommand(uint8_t* command, uint8_t length) {
if (bleConnected && pCmdChar != nullptr) {
pCmdChar->writeValue(command, length, false); // No-Response Write
}
}
m5stackgray_roller485.ino の主要部(BLE受信・I2C転送部)
// BLEキャラクタリスティック コールバック
class CmdCharCallbacks : public NimBLECharacteristicCallbacks {
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override {
NimBLEAttValue val = pCharacteristic->getValue();
const uint8_t* data = val.data();
size_t len = val.length();
if (len > 0) {
sendI2CCommand(data, len); // Roller485へI2C転送
}
}
};
② コンパイルエラーへの対応
生成されたコードをArduino IDEにてコンパイルしたところ、以下のエラーが発生した。
m5tab5_bt_controller.ino のエラー
fatal error: esp_bt.h: No such file or directory
m5stackgray_roller485.ino のエラー
error: 'void CmdCharCallbacks::onWrite(NimBLECharacteristic*)' marked 'override', but does not override
error: 'void ServerCallbacks::onConnect(NimBLEServer*)' marked 'override', but does not override
error: 'void ServerCallbacks::onDisconnect(NimBLEServer*)' marked 'override', but does not override
error: 'class NimBLEAdvertising' has no member named 'setScanResponse'
[原因と対応]
| 対象 | 原因 | 対応 |
|---|---|---|
| M5Tab5 | ESP32-S3はBluetooth Classicヘッダ(esp_bt.h)を持たない | 標準BLEライブラリ(BLEDevice.h)へ切り替え |
| M5Stack Gray | NimBLE-Arduino 2.x でコールバックの引数シグネチャが変更された | 2.x の新APIへ修正 |
NimBLE 2.x でのコールバック変更点は以下の通り。
// NimBLE 1.x
void onWrite(NimBLECharacteristic* pCharacteristic) override { ... }
void onConnect(NimBLEServer* pServer) override { ... }
void onDisconnect(NimBLEServer* pServer) override { ... }
// NimBLE 2.x(修正後)
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) override { ... }
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) override { ... }
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override { ... }
また setScanResponse(true) はNimBLE 2.xで廃止されたため削除した。
③ M5Tab5 クラッシュへの対応(BLE → WiFi への変更)
コンパイルエラー修正後、M5Tab5に書き込みを行ったところ起動直後にクラッシュした。シリアルモニタの出力は以下の通り。
ESP-ROM:esp32p4-eco2-20240710
...
Guru Meditation Error: Core 0 panic'ed (Illegal instruction)
PC : 0x4ffac2c0
MCAUSE : 0x38000002
[原因と対応]
シリアルモニタのROMバージョン文字列 esp32p4 により、M5Tab5が ESP32-P4 チップを搭載していることが判明した。ESP32-P4はRISC-Vアーキテクチャであり、Bluetoothを内蔵していない。そのため、BLEライブラリが存在しないハードウェアにアクセスしようとしてクラッシュしていた。
この問題を受け、M5Tab5-M5Stack Gray間の通信方式を BLE → WiFi(TCPソケット) へ変更した。
M5Tab5 --[WiFi TCP]--> M5Stack Gray --[I2C]--> Roller485
M5Stack Grayが WiFi AP として動作し、M5Tab5がSTAとして接続する構成とした。
m5tab5_bt_controller.ino の変更箇所(通信部)
// WiFi / TCP 設定
#define WIFI_SSID "M5Gray_Roller" // M5Stack Gray が作成する AP の SSID
#define WIFI_PASS "m5gray123"
#define SERVER_IP "192.168.4.1" // AP ホスト IP
#define SERVER_PORT 8080
// コマンド送信 (TCP経由)
void sendCommand(uint8_t* command, uint8_t length) {
if (tcpConnected && wifiClient.connected()) {
wifiClient.write(command, length);
}
}
m5stackgray_roller485.ino の変更箇所(WiFi AP・TCPサーバー部)
// WiFi AP 開始
WiFi.softAP(AP_SSID, AP_PASS);
// TCP サーバー 開始
tcpServer.begin();
tcpServer.setNoDelay(true);
// 受信データ処理(第1バイトでコマンド長を判別)
void processRxData() {
while (tcpClient.available() >= 1) {
uint8_t firstByte = tcpClient.peek();
uint8_t expectedLen = (firstByte == 0x40) ? 5 : 2;
if (tcpClient.available() < expectedLen) break;
tcpClient.read(rxBuf, expectedLen);
sendI2CCommand(rxBuf, expectedLen); // Roller485 へ I2C 転送
updateDisplay(true, rxBuf, expectedLen);
}
}
④ ボード設定の修正
WiFiへの変更後も同一のクラッシュダンプ(PC: 0x4ffac2c0, MCAUSE: 0x38000002)が継続した。
[原因と対応]
クラッシュダンプが変更前と完全に同一であることから、コードの内容によらず発生している問題と判断した。
原因はArduino IDEのボード選択が誤っていたことにある。ESP32-P4(RISC-V)向けにビルドすべきところを、ESP32/ESP32-S3(Xtensa)用のボードでコンパイルしていたため、命令セットの不一致によりIllegal instructionが発生していた。
以下の手順で対処した。
- ボードマネージャーURLに
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_M5Stack_index.jsonが登録されていることを確認 - ボードマネージャーにて M5Stack パッケージをバージョン 2.1.4 以降 にアップデート
-
ツール → ボードにて M5Stack-Tab5 を選択 - 再コンパイル・書き込み
上記対応により、M5Tab5が正常に起動し動作確認ができた。
⑤ UIの修正
動作確認後、以下のUI上の不具合について修正を依頼した。
[修正内容]
M5Tab5側
WiFi接続状態の切り替え時に旧テキストが残像として残る問題があった。
原因は drawWiFiStatus() 内の fillRect のクリア範囲が、テキスト描画座標(WIFI_STATUS_Y=80)より上(y=75まで)にしか届いていなかったためである。クリア範囲をテキスト座標を含む範囲に修正した。
void drawWiFiStatus(bool connected) {
// クリア範囲を WIFI_STATUS_Y を含む高さに修正
M5.Lcd.fillRect(LINE_WIDTH, LINE_WIDTH + 10,
LINE_X - LINE_WIDTH*2,
WIFI_STATUS_Y - (LINE_WIDTH + 10) + 40, // 修正
BG_COLOR);
M5.Lcd.setTextColor(connected ? TEXT_WIFI_OK : TEXT_WIFI_NG, BG_COLOR);
M5.Lcd.drawCenterString(
connected ? "WiFi: Connected " : "WiFi: Connecting...",
WIFI_STATUS_X, WIFI_STATUS_Y, &fonts::FreeMonoBold12pt7b
);
}
M5Stack Gray側
修正内容は以下の通り。
| 項目 | 修正内容 |
|---|---|
| ラベルとコンテンツの重なり | "Last Cmd:" ラベル(y=74)とコンテンツ(y=85→92)、"Motor State:" ラベル(y=124)とコンテンツ(y=135→144)の座標をそれぞれ修正 |
| TCP→WiFi 表示 | "TCP: Connected/Waiting..." を "WiFi: Connected/Connecting..." に変更 |
| 切断時表示 |
tcpClient.stop() 後に updateDisplay(false,...) を呼び出し "WiFi: Connecting..." へ切替 |
| Motor Direction 追加 | 画面右カラム(x=160〜)に "Motor Dir:" ラベルと CW/CCW 値を Motor State と並列表示。モータ状態をグローバル変数で管理し、コマンド種別に関わらず常に両方表示 |
修正後のM5Stack Gray 画面レイアウトは以下の通り。
┌────────────────────────────────┐
│ M5Gray - Roller485 │
├────────────────────────────────┤
│ WiFi: Connected │
├────────────────────────────────┤
│ Last Cmd: │
│ CMD: 40 10 27 00 00 │
├────────────────────────────────┤
│ Motor State: │ Motor Dir: │
│ ON / OFF │ CW / CCW │
│ Speed: 500 rpm │
├────────────────────────────────┤
│ AP: M5Gray_Roller 192.168.4.1 │
└────────────────────────────────┘
コード紹介
実際にClaude Codeにより生成されたソースコードを以下に示す。
// ----------------------------------------------------------------------------------------------------
// M5Stack Gray - Roller485 コントローラ (WiFi AP / TCP サーバー + I2C 送信側)
// 通信経路: M5Tab5 --[WiFi TCP]--> M5Stack Gray --[I2C]--> Roller485
// ----------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------
// ライブラリの読込
#include <M5Unified.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiServer.h>
#include <WiFiClient.h>
// ----------------------------------------------------------------------------------------------------
// I2C設定 (M5Stack Gray Port A: Grove コネクタ)
#define ROLLER_ADDRESS 0x40 // Unit-Roller485 I2Cアドレス
#define I2C_SDA 21 // M5Stack Gray Port A SDA
#define I2C_SCL 22 // M5Stack Gray Port A SCL
// ----------------------------------------------------------------------------------------------------
// WiFi AP / TCP サーバー設定 (M5Tab5 側と一致させること)
#define AP_SSID "M5Gray_Roller" // WiFi AP の SSID
#define AP_PASS "m5gray123" // WiFi AP のパスワード (8文字以上)
#define TCP_PORT 8080 // TCP サーバーのポート番号
// ----------------------------------------------------------------------------------------------------
// LCD表示設定 (M5Stack Gray: 320x240)
#define LCD_W 320
#define LCD_H 240
#define COL_RIGHT (LCD_W / 2) // 右カラム開始X座標 (Motor Dir 表示用)
// ----------------------------------------------------------------------------------------------------
// LCD表示レイアウト(Y座標)
// y= 0 - 28 : タイトル
// y= 28 : 水平区切り線
// y= 30 - 68 : WiFi接続状態
// y= 70 : 水平区切り線
// y= 72 - 88 : "Last Cmd:" ラベル
// y= 92 -118 : 最終コマンド(HEX)
// y=120 : 水平区切り線
// y=124 -140 : "Motor State:" / "Motor Dir:" ラベル (左右並列)
// y=144 -160 : Motor State値 / Motor Dir値 (左右並列)
// y=164 -180 : 速度表示 (full width)
// y=200 : 水平区切り線
// y=205 -240 : AP情報
// WiFi サーバー・クライアント
WiFiServer tcpServer(TCP_PORT);
WiFiClient tcpClient;
// 受信バッファ
uint8_t rxBuf[16];
// モータ状態(グローバル管理)
bool g_motorOn = false; // モータ ON/OFF
bool g_motorCW = true; // 回転方向 CW/CCW
int32_t g_motorSpeed = 0; // 回転速度 [rpm]
bool g_hasSpeed = false; // 速度コマンド受信済みフラグ
// ----------------------------------------------------------------------------------------------------
// Roller485 へ I2C コマンド送信
void sendI2CCommand(const uint8_t* data, size_t len) {
Wire.beginTransmission(ROLLER_ADDRESS);
for (size_t i = 0; i < len; i++) {
Wire.write(data[i]);
}
Wire.endTransmission(true);
}
// ----------------------------------------------------------------------------------------------------
// LCD 表示更新
void updateDisplay(bool clientConnected, const uint8_t* lastCmd, size_t cmdLen) {
// ── WiFi 接続状態エリア (y=30-68) ──
M5.Lcd.fillRect(0, 30, LCD_W, 38, BLACK);
M5.Lcd.setCursor(8, 36);
M5.Lcd.setTextSize(2);
if (clientConnected) {
M5.Lcd.setTextColor(GREEN);
M5.Lcd.print("WiFi: Connected ");
}
else {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("WiFi: Connecting...");
}
// ── 最終コマンド表示エリア (y=90-118) ──
if (lastCmd != nullptr && cmdLen > 0) {
M5.Lcd.fillRect(0, 90, LCD_W, 28, BLACK);
M5.Lcd.setCursor(8, 92);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.print("CMD: ");
for (size_t i = 0; i < cmdLen; i++) {
M5.Lcd.printf("%02X ", lastCmd[i]);
}
}
// ── Motor State / Motor Direction / Speed エリア (y=142-198) ──
M5.Lcd.fillRect(0, 142, LCD_W, 56, BLACK);
// Motor State (左カラム)
M5.Lcd.setCursor(8, 144);
M5.Lcd.setTextSize(2);
if (g_motorOn) {
M5.Lcd.setTextColor(GREEN);
M5.Lcd.print(" ON ");
}
else {
M5.Lcd.setTextColor(RED);
M5.Lcd.print("OFF ");
}
// Motor Direction (右カラム)
M5.Lcd.setCursor(COL_RIGHT + 8, 144);
M5.Lcd.setTextSize(2);
if (g_motorCW) {
M5.Lcd.setTextColor(GREEN);
M5.Lcd.print(" CW ");
}
else {
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("CCW ");
}
// 速度表示 (受信済みの場合のみ)
if (g_hasSpeed) {
M5.Lcd.setCursor(8, 168);
M5.Lcd.setTextColor(CYAN);
M5.Lcd.setTextSize(2);
M5.Lcd.printf("Speed: %4d rpm", g_motorSpeed);
}
}
// ----------------------------------------------------------------------------------------------------
// TCP 受信データ処理
// 第1バイトで2バイトコマンド(0x00)か5バイトコマンド(0x40)かを判別する
void processRxData() {
while (tcpClient.available() >= 1) {
uint8_t firstByte = tcpClient.peek();
uint8_t expectedLen = (firstByte == 0x40) ? 5 : 2;
if (tcpClient.available() < expectedLen) break;
tcpClient.read(rxBuf, expectedLen);
// Roller485 へ I2C 転送
sendI2CCommand(rxBuf, expectedLen);
// グローバル状態を更新
if (rxBuf[0] == 0x00) {
// Motor ON/OFF コマンド
g_motorOn = (rxBuf[1] == 0x01);
}
else if (rxBuf[0] == 0x40) {
// 速度・方向コマンド
uint32_t raw = ((uint32_t)rxBuf[4] << 24) | ((uint32_t)rxBuf[3] << 16) |
((uint32_t)rxBuf[2] << 8) | (uint32_t)rxBuf[1];
g_motorCW = (rxBuf[4] < 0x80);
g_motorSpeed = g_motorCW ? (int32_t)(raw / 100)
: (int32_t)((~raw + 1) / 100);
g_hasSpeed = true;
}
// LCD 更新
updateDisplay(true, rxBuf, expectedLen);
}
}
// ----------------------------------------------------------------------------------------------------
// メイン処理
void setup() {
// M5 初期化
M5.begin();
M5.Lcd.setRotation(1);
M5.Lcd.fillScreen(BLACK);
// I2C 初期化
Wire.begin(I2C_SDA, I2C_SCL);
// Roller485 初期化 (モータ停止)
uint8_t stop_cmd[5] = {0x40, 0x00, 0x00, 0x00, 0x00};
uint8_t off_cmd[2] = {0x00, 0x00};
sendI2CCommand(stop_cmd, 5);
delay(10);
sendI2CCommand(off_cmd, 2);
// ── LCD 初期表示 ──
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(8, 5);
M5.Lcd.print("M5Gray - Roller485");
// 水平区切り線
M5.Lcd.drawFastHLine(0, 28, LCD_W, DARKGREY);
M5.Lcd.drawFastHLine(0, 70, LCD_W, DARKGREY);
M5.Lcd.drawFastHLine(0, 120, LCD_W, DARKGREY);
M5.Lcd.drawFastHLine(0, 200, LCD_W, DARKGREY);
// 左右カラム区切り線 (Motor State / Motor Dir エリア)
M5.Lcd.drawFastVLine(COL_RIGHT, 120, 80, DARKGREY);
// "Last Cmd:" ラベル
M5.Lcd.setCursor(8, 74);
M5.Lcd.setTextColor(DARKGREY);
M5.Lcd.setTextSize(2);
M5.Lcd.print("Last Cmd:");
// "Motor State:" ラベル (左カラム)
M5.Lcd.setCursor(8, 124);
M5.Lcd.setTextColor(DARKGREY);
M5.Lcd.setTextSize(2);
M5.Lcd.print("Motor State:");
// "Motor Dir:" ラベル (右カラム)
M5.Lcd.setCursor(COL_RIGHT + 8, 124);
M5.Lcd.setTextColor(DARKGREY);
M5.Lcd.setTextSize(2);
M5.Lcd.print("Motor Dir:");
// WiFi AP 開始
WiFi.softAP(AP_SSID, AP_PASS);
IPAddress apIP = WiFi.softAPIP();
M5.Lcd.fillRect(0, 205, LCD_W, 35, BLACK);
M5.Lcd.setCursor(8, 208);
M5.Lcd.setTextSize(1);
M5.Lcd.setTextColor(DARKGREY);
M5.Lcd.printf("AP: %s IP: %s", AP_SSID, apIP.toString().c_str());
// TCP サーバー 開始
tcpServer.begin();
tcpServer.setNoDelay(true);
updateDisplay(false, nullptr, 0);
}
void loop() {
M5.update();
// 新規クライアント接続の受付 / 切断検知
if (!tcpClient || !tcpClient.connected()) {
if (tcpClient) {
tcpClient.stop();
updateDisplay(false, nullptr, 0); // WiFi: Connecting... に更新
}
WiFiClient newClient = tcpServer.accept();
if (newClient) {
tcpClient = newClient;
tcpClient.setNoDelay(true);
updateDisplay(true, nullptr, 0); // WiFi: Connected に更新
}
}
// 受信データ処理
if (tcpClient && tcpClient.connected() && tcpClient.available()) {
processRxData();
}
delay(5);
}
// ----------------------------------------------------------------------------------------------------
// M5Tab5 モータコントローラ (WiFi TCP 送信側)
// 通信経路: M5Tab5 --[WiFi TCP]--> M5Stack Gray --[I2C]--> Roller485
//
// ※ M5Tab5 は ESP32-P4 チップを搭載しており、Bluetooth が内蔵されていないため
// WiFi TCP ソケット通信を使用する。
// M5Stack Gray が WiFi AP として動作し、M5Tab5 がそれに接続する。
// ----------------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------------
// ライブラリの読込
#include <M5Unified.h>
#include <M5GFX.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include "image.h"
LGFX_Button sw_motor;
LGFX_Button sw_rotate;
LGFX_Button cm;
m5::touch_detail_t touch;
// ----------------------------------------------------------------------------------------------------
// WiFi / TCP 設定 (M5Stack Gray 側と一致させること)
#define WIFI_SSID "M5Gray_Roller" // M5Stack Gray が作成する AP の SSID
#define WIFI_PASS "m5gray123" // AP のパスワード
#define SERVER_IP "192.168.4.1" // AP ホスト IP (softAP のデフォルト)
#define SERVER_PORT 8080 // TCP ポート番号
WiFiClient wifiClient;
// 接続状態
bool tcpConnected = false;
// ----------------------------------------------------------------------------------------------------
// WiFi / TCP 接続処理
// WiFi + TCP 接続 (非ブロッキング: 1回試みて結果を返す)
bool connectServer() {
if (WiFi.status() != WL_CONNECTED) return false;
if (wifiClient.connect(SERVER_IP, SERVER_PORT)) {
tcpConnected = true;
return true;
}
tcpConnected = false;
return false;
}
// コマンド送信 (TCP経由でM5Stack Grayへ送信)
// 第1バイトで受信側がコマンド長を判別できるため、生バイト列をそのまま送信する
void sendCommand(uint8_t* command, uint8_t length) {
if (tcpConnected && wifiClient.connected()) {
wifiClient.write(command, length);
}
}
// ----------------------------------------------------------------------------------------------------
// マクロの定義
// ON/OFF
#define ON true
#define OFF false
#define CW true
#define CCW false
// 色設定(0~255)
#define BG_COLOR 0xF79E // 背景 色
#define BUTTON_COLOR 0x0000 // ボタン判定部 色
#define LINE_COLOR 0xC618 // フチ線 色
#define CIRCLE_COLOR 0xC618 // 円(装飾用) 色
#define CIRCLE_LINE_COLOR 0xb596 // 円(装飾用) フチ線 色
#define TEXT_ON_COLOR 0x2608 // 文字(ON・CW時) 色
#define TEXT_OFF_COLOR 0xC104 // 文字(OFF・CCW時) 色
#define TEXT_SPEED_COLOR 0x2112 // 文字(現在速度) 色
#define TEXT_NORMAL_COLOR 0x0000 // 文字(現在速度) 色
#define TEXT_WIFI_OK 0x07E0 // 文字(WiFi接続済) 色 ※緑
#define TEXT_WIFI_NG 0xFFE0 // 文字(WiFi未接続) 色 ※黄
// サイズ設定
#define LCD_WIDTH M5.Lcd.width() // 画面 幅
#define LCD_HEIGHT M5.Lcd.height() // 画面 高さ
#define SW_BG_WIDTH 310 // SW背景 幅
#define SW_BG_HEIGHT 110 // SW背景 高さ
#define SW_WIDTH 150 // SW 幅
#define SW_HEIGHT 100 // SW 高さ
#define SW_INTERVAL 200 // SW間距離(Y方向)
#define CM_BG_WIDTH 110 // モータCM 幅
#define CM_BG_HEIGHT 510 // モータCM 高さ
#define CM_LVR_WIDTH 100 // モータCM 操作部 幅
#define CM_LVR_HEIGHT 60 // モータCM 操作部 高さ
#define LINE_WIDTH 5 // フチ線幅
#define CIRCLE_RADIUS 10 // 円(装飾用) 半径
#define CIRCLE_INTERVAL 50 // 円(装飾用) 画面端部との間隔
#define TEXT_UPPER_CM_Y 40 // モータCM 上側配置テキスト Y方向調整用
#define TEXT_RIGHT_CM_X 10 // モータCM 右側配置テキスト X方向調整用
#define TEXT_RIGHT_CM_Y 20 // モータCM 右側配置テキスト Y方向調整用
// 座標設定(左上座標)
#define SW_MOTOR_BG_X 130 // モータON/OFF SW背景 X座標
#define SW_MOTOR_BG_Y 160 // モータON/OFF SW背景 Y座標
#define SW_MOTOR_OFF_X SW_MOTOR_BG_X + LINE_WIDTH // モータOFF時 SW X座標
#define SW_MOTOR_OFF_Y SW_MOTOR_BG_Y + LINE_WIDTH // モータOFF時 SW Y座標
#define SW_MOTOR_ON_X SW_MOTOR_BG_X + LINE_WIDTH + SW_WIDTH // モータON時 SW X座標
#define SW_MOTOR_ON_Y SW_MOTOR_BG_Y + LINE_WIDTH // モータON時 SW Y座標
#define SW_ROTATE_BG_X SW_MOTOR_BG_X // モータCW/CCW SW背景 X座標
#define SW_ROTATE_BG_Y SW_MOTOR_BG_Y + SW_HEIGHT + SW_INTERVAL + LINE_WIDTH*2 // モータCW/CCW SW背景 Y座標
#define SW_ROTATE_CCW_X SW_ROTATE_BG_X + LINE_WIDTH // モータCCW時 SW X座標
#define SW_ROTATE_CCW_Y SW_ROTATE_BG_Y + LINE_WIDTH // モータCCW時 SW Y座標
#define SW_ROTATE_CW_X SW_ROTATE_BG_X + LINE_WIDTH + SW_WIDTH // モータCW時 SW X座標
#define SW_ROTATE_CW_Y SW_ROTATE_BG_Y + LINE_WIDTH // モータCW時 SW Y座標
#define CM_BG_X 860 // モータCM 背景 X座標
#define CM_BG_Y 160 // モータCM 背景 Y座標
#define CM_LVR_X CM_BG_X + LINE_WIDTH // モータCM 操作部 X座標
#define CM_LVR_Y CM_BG_Y + CM_BG_HEIGHT - CM_LVR_HEIGHT - LINE_WIDTH // モータCM 操作部 Y座標
#define LINE_X 560 // 操作部間 仕切線 X座標
#define LINE_Y 0 // 操作部間 仕切線 Y座標
// WiFiステータス表示座標 (画面上部 左エリア中央)
#define WIFI_STATUS_X (LINE_X / 2) // WiFi状態表示 X座標(中心)
#define WIFI_STATUS_Y 80 // WiFi状態表示 Y座標
#define MIN_SPEED 100 // 最小回転速度[rpm]
#define MAX_SPEED 1500 // 最大回転速度[rpm]
// ----------------------------------------------------------------------------------------------------
// グローバル変数・定数の定義
// スイッチ状態
bool motor_sw_state = OFF;
bool rotate_sw_state = CCW;
// モータ回転速度
int32_t motor_speed = 100;
uint8_t motor_bytes[4] = {0, 0, 0, 0};
// 操作部 範囲:{左上 X座標, 左上 Y座標, 右下 X座標, 右下 Y座標}
uint16_t SW_MOTOR_AREA[4] = {SW_MOTOR_BG_X, SW_MOTOR_BG_Y, SW_MOTOR_BG_X + SW_BG_WIDTH, SW_MOTOR_BG_Y + SW_BG_HEIGHT};
uint16_t SW_ROTATE_AREA[4] = {SW_ROTATE_BG_X, SW_ROTATE_BG_Y, SW_ROTATE_BG_X + SW_BG_WIDTH, SW_ROTATE_BG_Y + SW_BG_HEIGHT};
uint16_t CM_AREA[4] = {CM_BG_X, CM_BG_Y, CM_BG_X + CM_BG_WIDTH, CM_BG_Y + CM_BG_HEIGHT};
uint16_t CM_LVR_AREA[4] = {CM_LVR_X, CM_LVR_Y, CM_LVR_X + CM_LVR_WIDTH, CM_LVR_Y + CM_LVR_HEIGHT};
// ----------------------------------------------------------------------------------------------------
// 関数の定義
// int32 --> byteに変換(Roller485-Unit用)
void cvtInt2Bytes(void) {
uint32_t remainder = motor_speed * 100; // コマンド変換用にx100
for(int i = 0; i < 4; i++) {
if (rotate_sw_state) {
motor_bytes[i] = remainder % 256; // CW
}
else {
motor_bytes[i] = ~(remainder % 256); // CCW
}
remainder /= 256;
}
}
// モータON/OFF切替
void motorSW() {
uint8_t on_command[2] = {0x00, 0x01}; // モータONコマンド
uint8_t off_command[2] = {0x00, 0x00}; // モータOFFコマンド
uint8_t length = 2; // コマンド長
if (motor_sw_state) {
// ON --> OFF
motor_sw_state = OFF;
// コマンド送信
sendCommand(off_command, length);
// 描画の更新
drawLCD(SW_MOTOR_BG_X, SW_MOTOR_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT);
drawLCD(SW_MOTOR_OFF_X, SW_MOTOR_OFF_Y, SW, SW_WIDTH, SW_HEIGHT);
}
else {
// OFF --> ON
motor_sw_state = ON;
// コマンド送信
sendCommand(on_command, length);
// 描画の更新
drawLCD(SW_MOTOR_BG_X, SW_MOTOR_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT);
drawLCD(SW_MOTOR_ON_X, SW_MOTOR_ON_Y, SW, SW_WIDTH, SW_HEIGHT);
}
}
// 回転方向切替
void rotateSW() {
if (rotate_sw_state) {
// CW --> CCW
rotate_sw_state = CCW;
// 描画の更新
drawLCD(SW_ROTATE_BG_X, SW_ROTATE_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT);
drawLCD(SW_ROTATE_CCW_X, SW_ROTATE_CCW_Y, SW, SW_WIDTH, SW_HEIGHT);
}
else {
// CCW --> CW
rotate_sw_state = CW;
// 描画の更新
drawLCD(SW_ROTATE_BG_X, SW_ROTATE_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT);
drawLCD(SW_ROTATE_CW_X, SW_ROTATE_CW_Y, SW, SW_WIDTH, SW_HEIGHT);
}
}
// 速度コマンド送信
void controlSpeed() {
uint8_t command[5] = {0x40, motor_bytes[0], motor_bytes[1], motor_bytes[2], motor_bytes[3]}; // 速度設定コマンド
uint8_t length = 5; // コマンド長
// コマンドの送信
sendCommand(command, length);
}
// レバー スライド処理
void slideLever(uint16_t pos_lvr_y) {
if (CM_LVR_Y < pos_lvr_y) { // モータCM 下端の処理
motor_speed = MIN_SPEED;
pos_lvr_y = CM_LVR_Y;
}
else if (pos_lvr_y < CM_BG_Y + LINE_WIDTH) { // モータCM 上端の処理
motor_speed = MAX_SPEED;
pos_lvr_y = CM_BG_Y + LINE_WIDTH;
}
else {
motor_speed = MAX_SPEED - (MAX_SPEED - MIN_SPEED) * (pos_lvr_y - (CM_BG_Y + LINE_WIDTH)) / (CM_BG_HEIGHT - (CM_LVR_HEIGHT + LINE_WIDTH * 2));
}
drawLCD(CM_BG_X, CM_BG_Y, CM_BG, CM_BG_WIDTH, CM_BG_HEIGHT); // コントロールメーター 背景
drawLCD(CM_LVR_X, pos_lvr_y, CM_LVR, CM_LVR_WIDTH, CM_LVR_HEIGHT); // コントロールメーター レバー
}
// 背景 描画
void drawBackground() {
// 背景
M5.Lcd.fillScreen(BG_COLOR);
// 枠線
M5.Lcd.fillRect(0, 0, LCD_WIDTH, LINE_WIDTH, LINE_COLOR); // 上
M5.Lcd.fillRect(0, 0, LINE_WIDTH, LCD_HEIGHT, LINE_COLOR); // 左
M5.Lcd.fillRect(LCD_WIDTH - LINE_WIDTH, 0, LCD_WIDTH, LCD_HEIGHT, LINE_COLOR); // 右
M5.Lcd.fillRect(0, LCD_HEIGHT - LINE_WIDTH, LCD_WIDTH, LCD_HEIGHT, LINE_COLOR); // 下
M5.Lcd.fillRect(LINE_X - LINE_WIDTH/2, LINE_Y, LINE_WIDTH, LCD_HEIGHT, LINE_COLOR); // スイッチ・メータ間
// 円 枠線
M5.Lcd.fillCircle(CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // SWエリア 左上
M5.Lcd.fillCircle(CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // SWエリア 左下
M5.Lcd.fillCircle(LINE_X - CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // SWエリア 右上
M5.Lcd.fillCircle(LINE_X - CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // SWエリア 右下
M5.Lcd.fillCircle(LINE_X + CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // CMエリア 左上
M5.Lcd.fillCircle(LINE_X + CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // CMエリア 左下
M5.Lcd.fillCircle(LCD_WIDTH - CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // CMエリア 右上
M5.Lcd.fillCircle(LCD_WIDTH - CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS + LINE_WIDTH, CIRCLE_LINE_COLOR); // CMエリア 右下
// 円
M5.Lcd.fillCircle(CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // SWエリア 左上
M5.Lcd.fillCircle(CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // SWエリア 左下
M5.Lcd.fillCircle(LINE_X - CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // SWエリア 右上
M5.Lcd.fillCircle(LINE_X - CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // SWエリア 右下
M5.Lcd.fillCircle(LINE_X + CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // CMエリア 左上
M5.Lcd.fillCircle(LINE_X + CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // CMエリア 左下
M5.Lcd.fillCircle(LCD_WIDTH - CIRCLE_INTERVAL, CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // CMエリア 右上
M5.Lcd.fillCircle(LCD_WIDTH - CIRCLE_INTERVAL, LCD_HEIGHT - CIRCLE_INTERVAL, CIRCLE_RADIUS, CIRCLE_COLOR); // CMエリア 右下
// 文字列
// 赤色文字
M5.Lcd.setTextColor(TEXT_OFF_COLOR, BG_COLOR);
M5.Lcd.drawCenterString("OFF", SW_MOTOR_BG_X, SW_MOTOR_BG_Y + SW_BG_HEIGHT + LINE_WIDTH*2, &fonts::FreeMonoBold24pt7b); // [OFF] 描画
M5.Lcd.drawCenterString("CCW", SW_ROTATE_BG_X, SW_ROTATE_BG_Y + SW_BG_HEIGHT + LINE_WIDTH*2, &fonts::FreeMonoBold24pt7b); // [CCW] 描画
// 緑色文字
M5.Lcd.setTextColor(TEXT_ON_COLOR, BG_COLOR);
M5.Lcd.drawCenterString("ON", SW_MOTOR_BG_X + SW_BG_WIDTH + LINE_WIDTH*2, SW_MOTOR_BG_Y + SW_BG_HEIGHT + LINE_WIDTH*2, &fonts::FreeMonoBold24pt7b); // [ON] 描画
M5.Lcd.drawCenterString("CW", SW_ROTATE_BG_X + SW_BG_WIDTH + LINE_WIDTH*2, SW_ROTATE_BG_Y + SW_BG_HEIGHT + LINE_WIDTH*2, &fonts::FreeMonoBold24pt7b); // [CW] 描画
// 黒色文字
M5.Lcd.setTextColor(TEXT_NORMAL_COLOR, BG_COLOR);
M5.Lcd.drawString("1500", CM_BG_X + CM_BG_WIDTH + LINE_WIDTH*2 + TEXT_RIGHT_CM_X, CM_BG_Y - TEXT_RIGHT_CM_Y, &fonts::FreeMonoBold24pt7b); // [1500] 描画
M5.Lcd.drawString("100", CM_BG_X + CM_BG_WIDTH + LINE_WIDTH*2 + TEXT_RIGHT_CM_X, CM_BG_Y - TEXT_RIGHT_CM_Y + CM_BG_HEIGHT, &fonts::FreeMonoBold24pt7b); // [100] 描画
// 青色文字
M5.Lcd.setTextColor(TEXT_SPEED_COLOR, BG_COLOR);
M5.Lcd.drawCenterString("SPEED", CM_BG_X + CM_BG_WIDTH/2, CM_BG_Y - (LINE_WIDTH + TEXT_UPPER_CM_Y*2), &fonts::FreeMonoBold24pt7b); // [SPEED] 描画
M5.Lcd.drawCenterString("0", CM_BG_X + CM_BG_WIDTH/2, CM_BG_Y - (LINE_WIDTH + TEXT_UPPER_CM_Y), &fonts::FreeMonoBold24pt7b); // [0] 描画
}
// 画像 描画処理
void drawLCD(int16_t pos_x, int16_t pos_y, const uint16_t* img, int16_t width, int16_t height) {
M5.Lcd.startWrite();
for(int16_t y = 0; y < height; y++) {
for(int16_t x = 0; x < width; x++) {
M5.Lcd.drawPixel(pos_x + x, pos_y + y, img[y * width + x]); // 各操作部 描画
}
}
M5.Lcd.endWrite();
}
// WiFi / TCP 接続ステータス 表示
void drawWiFiStatus(bool connected) {
// テキスト表示エリアをまず背景色で塗りつぶしてからテキストを描画する
// ※ WIFI_STATUS_Y(=80) を含む領域全体をクリアする
M5.Lcd.fillRect(LINE_WIDTH, LINE_WIDTH + 10,
LINE_X - LINE_WIDTH*2, WIFI_STATUS_Y - (LINE_WIDTH + 10) + 40,
BG_COLOR);
M5.Lcd.setTextColor(connected ? TEXT_WIFI_OK : TEXT_WIFI_NG, BG_COLOR);
M5.Lcd.drawCenterString(
connected ? "WiFi: Connected " : "WiFi: Connecting...",
WIFI_STATUS_X, WIFI_STATUS_Y, &fonts::FreeMonoBold12pt7b
);
}
// タッチ位置 判定
void judgePosition(uint16_t pos_x, uint16_t pos_y) {
// モータON/OFF
if(sw_motor.contains(pos_x, pos_y)) {
cvtInt2Bytes();
controlSpeed();
motorSW();
}
// モータCW/CCW
else if (sw_rotate.contains(pos_x, pos_y)) {
if (!motor_sw_state) {
rotateSW();
}
}
// モータ速度(レバー)
else if (cm.contains(pos_x, pos_y)) {
slideLever(pos_y);
cvtInt2Bytes();
controlSpeed();
}
}
// ----------------------------------------------------------------------------------------------------
// メイン処理
void setup() {
M5.begin(); // M5 初期化
M5.Lcd.setRotation(1); // LCD 画面方向の変更
drawBackground(); // 背景の描画
M5.Lcd.setCursor(0, 0); // 文字描画位置 設定
// ボタン 設定 (座標指定が中心点になることに注意)
sw_motor.initButton (&M5.Lcd, SW_MOTOR_BG_X + SW_BG_WIDTH/2, SW_MOTOR_BG_Y + SW_BG_HEIGHT/2, SW_BG_WIDTH, SW_BG_HEIGHT,
BUTTON_COLOR, BUTTON_COLOR, BUTTON_COLOR, "", 0, 0); // モータON/OFF
sw_rotate.initButton(&M5.Lcd, SW_ROTATE_BG_X + SW_BG_WIDTH/2, SW_ROTATE_BG_Y + SW_BG_HEIGHT/2, SW_BG_WIDTH, SW_BG_HEIGHT,
BUTTON_COLOR, BUTTON_COLOR, BUTTON_COLOR, "", 0, 0); // モータCW/CCW
cm.initButton (&M5.Lcd, CM_BG_X + CM_BG_WIDTH/2, CM_BG_Y + CM_BG_HEIGHT/2, CM_BG_WIDTH, CM_BG_HEIGHT,
BUTTON_COLOR, BUTTON_COLOR, BUTTON_COLOR, "", 0, 0); // モータCM
// ボタン 描画
sw_motor.drawButton(); // モータON/OFF
sw_rotate.drawButton(); // モータCW/CCW
cm.drawButton(); // コントロールメーター
// 操作部 描画
drawLCD(SW_MOTOR_BG_X, SW_MOTOR_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT); // モータON/OFF 背景
drawLCD(SW_MOTOR_OFF_X, SW_MOTOR_OFF_Y, SW, SW_WIDTH, SW_HEIGHT); // モータON/OFF スイッチ
drawLCD(SW_ROTATE_BG_X, SW_ROTATE_BG_Y, SW_BG, SW_BG_WIDTH, SW_BG_HEIGHT); // モータCW/CCW 背景
drawLCD(SW_ROTATE_CW_X, SW_ROTATE_CW_Y, SW, SW_WIDTH, SW_HEIGHT); // モータCW/CCW スイッチ
drawLCD(CM_BG_X, CM_BG_Y, CM_BG, CM_BG_WIDTH, CM_BG_HEIGHT); // コントロールメーター 背景
drawLCD(CM_LVR_X, CM_LVR_Y, CM_LVR, CM_LVR_WIDTH, CM_LVR_HEIGHT); // コントロールメーター レバー
// WiFi 接続開始 (M5Stack Gray の AP へ接続)
drawWiFiStatus(false);
WiFi.begin(WIFI_SSID, WIFI_PASS);
}
void loop() {
char speed_str[6];
// WiFi / TCP 接続管理
bool currentlyConnected = (WiFi.status() == WL_CONNECTED) && wifiClient.connected();
if (!currentlyConnected) {
if (tcpConnected) {
// 切断検知
tcpConnected = false;
wifiClient.stop();
drawWiFiStatus(false);
}
// WiFi 接続待ち → TCP 接続試行
if (WiFi.status() == WL_CONNECTED) {
connectServer(); // TCP 接続試行
if (tcpConnected) {
drawWiFiStatus(true);
// 初期コマンド: モータ停止
uint8_t stop_cmd[5] = {0x40, 0x00, 0x00, 0x00, 0x00};
uint8_t off_cmd[2] = {0x00, 0x00};
sendCommand(stop_cmd, 5);
delay(10);
sendCommand(off_cmd, 2);
}
}
delay(500); // 再接続インターバル
return;
}
// タッチ操作
M5.update(); // M5Tab5 状態の更新
touch = M5.Touch.getDetail(); // 画面タッチ位置の検出
if (touch.isPressed()) { // タッチ検出時・検出中
if (cm.contains(touch.x, touch.y)) { // モータCM操作時
slideLever(touch.y); // モータCM 操作部位置よりモータ回転速度目標値を算出
cvtInt2Bytes();
controlSpeed(); // モータ回転速度を変更
sprintf(speed_str, "%4u", motor_speed);
M5.Lcd.drawCenterString(speed_str, CM_BG_X + CM_BG_WIDTH/2, CM_BG_Y - (LINE_WIDTH + TEXT_UPPER_CM_Y), &fonts::FreeMonoBold24pt7b);
}
}
else if (touch.wasReleased()) { // タッチ解除時
judgePosition(touch.x, touch.y); // 解除位置に応じた処理を実施
}
delay(10);
}
※ image.hについては添付資料として末尾に掲載する。
実行結果
上記コードによる実行結果を以下に示す。
実施動作としては、
- M5Stackの起動(AP)
- M5Tab5の起動、自動でAPに接続
- モータON/OFF、CW/CCW、回転速度変更機能の確認
を実施する。また、その際のUI変化も併せて確認する。
実行結果(1・2)
実行結果(3)
結論
Claude Codeに適切に条件を与えながらコード生成を実施すれば、非常に高品質なコード生成を実施してくれると感じる。Proプランでは1000行程度のコード生成が推奨されているが、この程度の実装内容であれば必要十分と思われる。
また、自作スクリプトを参考にコード生成させると、自身の記載のクセに合わせて生成を実施してくれるため、その点も非常にありがたく感じる。
今回は想定外(M5Tab5にBLE機能を実装できない)もあったが、Claude Codeに状況を適宜フィードバックすることで、対応案を出し意図を組んだまま実装できたことも感触が良かった。ところどころ自力での調整も必要な場面もあったが、コーディング経験がある人が使えば非常に有用だと感じる。


