🐡
組込SWのAI駆動開発
概要
目的
AI駆動開発の記事は数多いが、組込SWを対象としたものは少ない。
組込SWに適したAI駆動開発を考える。
以下の検証を実施。
- 設計図にmermaid記法を活用
- 状態遷移をsimulink stateflowのコード変換likeに生成する指令方法
- センサ実装をサンプルコード提供でコード生成
- LPFを正しく実装するための指令方法
開発
距離センサを用いて、冷蔵庫のドアの開けっ放しを検出したい。
冷蔵庫のドアの開けっ放しを1分以上検出したら、ブザーを鳴らす。
対象
パナソニックの冷凍庫には開けっぱなしでブザーが鳴る機能がなく、開けっぱなしにしてしまい、悲惨なことになりかけたため。
下図のような形で取り付けた。
ツール
- Editor:VS code
- 拡張機能:Roo Code, PlatformIO
- LLM:gemini-2.5-pro(途中からo4-mini)copilotのpreview上限に達したため
- m5stickC Plus
- M5StickC ToF Hat
設計書
入出力
指令
以下のSW仕様を"data_flow.md"に記述する。
mermaid記法のflowchart LRを作成してください。
入力:距離センサ
出力:ブザー
距離センサー>メイン->ブザー
生成されたmermaid記法の図
flowchart LR
A[距離センサ] --> B(メイン)
B --> C[ブザー]
状態遷移
指令
以下のSW仕様を"state_flow.md"に記述する。
mermaid記法のState diagramを作成してください。
距離センサを用いて、ドアの開けっ放しを検出したい。ドアの開けっ放しを1分以上検出したら、ブザーを鳴らす。
### 状態定義
親状態1:監視中
親状態2:アラート
親状態1-子状態1-1:タイマー停止
親状態1-子状態1-2:タイマーカウント
### 遷移条件
親状態1->親状態2:開放時間1分以上
子状態1-1->子状態1-2:ドア開放検出
子状態1-2->子状態1-1:ドア密封検出
親状態2->親状態1:ドア密封検出
生成されたmermaid記法の図
stateDiagram-v2
[*] --> 監視中
state 監視中 {
[*] --> タイマー停止
タイマーカウント --> タイマー停止 : ドア密封検出
タイマー停止 --> タイマーカウント : ドア開放検出
}
state アラート
監視中 --> アラート : 開放時間1分以上
アラート --> 監視中 : ドア密封検出
platformio
センサに必要なライブラリを予めインポートしておく。
[env:m5stick-c]
platform = espressif32
board = m5stick-c
framework = arduino
lib_deps =
pololu/VL53L0X@^1.3.1
m5stack/M5Unified@^0.2.7
実装
距離センサークラス実装
指令
ローパスフィルタを実装するためのヒントを入れた指令を入力。
src/peripheral配下に新規ファイルを作成し、距離センサークラスを作成してください。
## クラス仕様
距離センサーの出力を取得し、ローパスフィルタを通し、その結果を出力する。
### 定義
- 実行周期ts:10ms
- フィルタ時定数tau:200ms
- LPF係数アルファalpha:alpha=ts/(tau+ts)
### メンバ変数
- センサ出力
- センサフィルタ値
- センサフィルタ前回値
### メンバ関数
#### private
- ローパスフィルタ
#### public
- フィルタ値を渡す
センサの使い方は"src/sample/TOF.ino"を参照してください。
生成コード
DistanceSensor.cpp
#include "DistanceSensor.h"
#include <Wire.h>
#include <M5Unified.h> // Include for M5.Lcd and Serial if needed
DistanceSensor::DistanceSensor(float ts, float tau)
: execution_cycle_ts(ts),
filter_time_constant_tau(tau),
sensor_output(0),
sensor_filtered_value(0.0f),
sensor_filtered_value_prev(0.0f) {
lpf_alpha = execution_cycle_ts / (filter_time_constant_tau + execution_cycle_ts);
}
void DistanceSensor::init() {
Wire.begin(0, 26, 100000UL); // Refer to TOF.ino for HAT connection settings
sensor.setTimeout(500);
if (!sensor.init()) {
// Error handling: Display on M5StickC LCD, etc.
// Example of serial output and LCD display
Serial.println("Failed to detect and initialize VL53L0X sensor!");
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(RED);
M5.Lcd.print("Sensor Fail");
// Consider stopping processing with an infinite loop, etc.
while(1);
}
sensor.startContinuous();
}
void DistanceSensor::update() {
sensor_output = sensor.readRangeContinuousMillimeters();
if (sensor.timeoutOccurred()) {
Serial.println("VL53L0X TIMEOUT");
// Consider how to handle the value in case of timeout (e.g., maintain previous value, set as error value, etc.)
// Here, the filter calculation is not performed, and the previous value is maintained.
return;
}
lowPassFilter();
}
void DistanceSensor::lowPassFilter() {
sensor_filtered_value = lpf_alpha * sensor_output + (1.0f - lpf_alpha) * sensor_filtered_value_prev;
sensor_filtered_value_prev = sensor_filtered_value;
}
float DistanceSensor::getFilteredValue() const {
return sensor_filtered_value;
}
uint16_t DistanceSensor::getRawValue() const {
return sensor_output;
}
DistanceSensor.h
#ifndef DISTANCE_SENSOR_H
#define DISTANCE_SENSOR_H
#include <VL53L0X.h>
class DistanceSensor {
public:
DistanceSensor(float ts = 0.01, float tau = 0.2); // ts: 10ms, tau: 200ms
void init();
void update();
float getFilteredValue() const;
uint16_t getRawValue() const;
private:
void lowPassFilter();
VL53L0X sensor; // Type: VL53L0X (TOF sensor library class)
float execution_cycle_ts; // Type: float (Execution cycle)
float filter_time_constant_tau; // Type: float (Filter time constant)
float lpf_alpha; // Type: float (Low-pass filter coefficient)
uint16_t sensor_output; // Type: uint16_t (Unsigned 16-bit integer, raw sensor value)
float sensor_filtered_value; // Type: float (Filtered sensor value)
float sensor_filtered_value_prev; // Type: float (Previous filtered sensor value)
}; // Missing closing brace for class
#endif // DISTANCE_SENSOR_H
コンパイル
予めplatformioをインストールしておきます。コンパイルエラーが発生した時に、コンパイル↔︎修正を繰り返してくれます。
pip install platformio
"pio run"コマンドでコンパイルエラーが出ないか確認してください。
エラーが生じた場合は修正してください。
コンパイル成功
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [= ] 7.6% (used 24948 bytes from 327680 bytes)
Flash: [=== ] 33.0% (used 432957 bytes from 1310720 bytes)
Building .pio/build/m5stick-c/firmware.bin
esptool.py v4.5.1
Creating esp32 image...
Merged 2 ELF sections
Successfully created esp32 image.
===== [SUCCESS] Took 12.60 seconds
指令
ドアが開いているかを出力する関数がなかったので追加。
DistanceSensorクラスにsensor_filtered_valueがある定数以上だったらtrue、未満だったらfalseを返すpublic関数を追加してください。定数の初期値は5cmとしてください。
生成コード
bool DistanceSensor::isAboveThreshold() const {
return sensor_filtered_value >= FILTERED_VALUE_THRESHOLD_MM;
}
状態遷移クラス実装
simulinkのstateflowをCコード生成した時のコードに近づけるように指令した。
指令
状態遷移クラスを実装する。設計書を作成したので、それを参照させる。
新規にドア開放判定クラスを作成してください。
距離センサを用いて、ドアの開けっ放しを検出する。
ドアの開けっ放しを1分以上検出したら、ブザーを鳴らす。
ブザーは"M5.Speaker.tone(4000, 1000)"で鳴る。
ドアが開いているかどうかは"DistanceSensor::isAboveThreshold"を使用する。
Switch文で状態遷移を行い、case文が親状態:システム状態(監視中orアラート)->子状態:タイマー状態(停止orカウント)になるように実装してください。
entry関数には監視中状態の入場動作:タイマーリセット、アラート状態の入場動作:ブザー
状態遷移は以下を参照すること。
```mermaid
stateDiagram-v2
[*] --> 監視中
state 監視中 {
[*] --> タイマー停止
タイマーカウント --> タイマー停止 : ドア密封検出
タイマー停止 --> タイマーカウント : ドア開放検出
}
state アラート
監視中 --> アラート : 開放時間1分以上
アラート --> 監視中 : ドア密封検出
生成コード
DoorOpenDetector.cpp
#include "DoorOpenDetector.h"
#include <M5Unified.h>
DoorOpenDetector::DoorOpenDetector(DistanceSensor* sensor)
: sensor_(sensor),
systemState_(SystemState::Monitoring),
timerState_(TimerState::Stopped),
openStartMillis_(0) {}
void DoorOpenDetector::update() {
switch (systemState_) {
case SystemState::Monitoring:
handleMonitoring();
break;
case SystemState::Alert:
handleAlert();
break;
}
}
void DoorOpenDetector::handleMonitoring() {
switch (timerState_) {
case TimerState::Stopped:
if (sensor_->isAboveThreshold()) {
transitionTo(SystemState::Monitoring, TimerState::Counting);
} else {
doAction(systemState_, timerState_);
}
break;
case TimerState::Counting:
if (!sensor_->isAboveThreshold()) {
transitionTo(SystemState::Monitoring, TimerState::Stopped);
} else {
if (millis() - openStartMillis_ >= ALERT_THRESHOLD_MS) {
transitionTo(SystemState::Alert, TimerState::Stopped);
} else {
doAction(systemState_, timerState_);
}
}
break;
}
}
void DoorOpenDetector::handleAlert() {
if (!sensor_->isAboveThreshold()) {
transitionTo(SystemState::Monitoring, TimerState::Stopped);
} else {
doAction(systemState_, timerState_);
}
}
void DoorOpenDetector::transitionTo(SystemState newSystem, TimerState newTimer) {
exit(systemState_, timerState_);
systemState_ = newSystem;
timerState_ = newTimer;
entry(systemState_, timerState_);
}
void DoorOpenDetector::reset() {
systemState_ = SystemState::Monitoring;
timerState_ = TimerState::Stopped;
openStartMillis_ = 0;
}
void DoorOpenDetector::entry(SystemState system, TimerState timer) {
// Reset timer on monitoring entry
if (system == SystemState::Monitoring && timer == TimerState::Counting) {
openStartMillis_ = millis();
}
// Beep on alert entry
if (system == SystemState::Alert) {
M5.Speaker.tone(4000, 1000);
}
}
void DoorOpenDetector::exit(SystemState system, TimerState timer) {
}
void DoorOpenDetector::doAction(SystemState system, TimerState timer) {
}
DoorOpenDetector.h
#pragma once
#include "../peripheral/DistanceSensor.h"
class DoorOpenDetector {
public:
enum class SystemState {
Monitoring,
Alert
};
enum class TimerState {
Stopped,
Counting
};
DoorOpenDetector(DistanceSensor* sensor);
void update();
void reset();
void entry(SystemState system, TimerState timer);
void exit(SystemState system, TimerState timer);
void doAction(SystemState system, TimerState timer);
private:
DistanceSensor* sensor_;
SystemState systemState_;
TimerState timerState_;
unsigned long openStartMillis_;
static constexpr unsigned long ALERT_THRESHOLD_MS = 60000; // 1min
void transitionTo(SystemState newSystem, TimerState newTimer);
void handleMonitoring();
void handleAlert();
};
mainのloopでセンサー取得を10ms、状態遷移を1000msごとに呼び出し、実装完了です。
実際に動作させた所、ブザーは入場条件で一度だけ鳴らすより、状態変化しないままの場合に鳴らし続けたほうが良かったので、実装を更新しています。
Discussion