🐡

組込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