カテゴリー: ai-camera

  • イノシシ威嚇用 わんわん装置の制作

    MegadetectorでのAI検知と連動して働く「わんわん威嚇装置」を作ってみた。
    この装置を複数台用意し全面、背後、横などイノシシを取り囲むように時間差で
    吠えさせればかなり効果が見込める。


    必要な部品集め

    CPUは小型で周辺部品も最小で済む「秋月電子製 AE-ESP-WROOM-02-DEV」¥1,280にした。

    再生アンプは、UART経由で再生制御可能なサウンド基板をアマゾンで見つけた。

    Ahvqevn DY-20T音声再生モジュール
    12V/24V電源10W/20WアンプMicro-SD
    カードMP3ミュージックプレーヤー
    購入先 アマゾン 購入価格¥1,428

    スピーカーは防水タイプの物を探します。
    アマゾンで色々なタイプが見つかります、適当な物を用意して下さい。

    Herdio 100W HIFI 2-Way防水型4インチ スピーカー
    4Ω 100W ¥3,771 アマゾン


    事前テスト

    最初に再生基板が正常動作するか確認します。
    Windows11 PCにUSB->UART変換ケーブル経由で接続してテストします。

    DTECH USB TTL シリアル 変換 ケーブル 3.3V 1m FTDI
    チップセット 6ピン 2.54mm ピッチ メス コネクタ
    | FT232RL USB UART シリアル コンバーター ケーブル
    購入先 アマゾン 購入価格¥1,999 

    サウンド基板とシリアルコンバーターケーブルを接続します。
    Pin1(GND)->GND Pin4(TXD)->RXD Pin5(RXD)->TXD
    サウンド基板の電源端子にDC12Vを供給します。

    Jupyter Notebookをインストール
    Anaconda公式サイトから、お使いのOS(Windows / Mac)に対応したインストーラーをダウンロー
    ドします。
    ダウンロードしたファイルを実行し、画面の指示に従ってインストールを完了させます。
    インストール後、Windowsならスタートメニューから「Anaconda3」を開き、「Jupyter Notebook」をクリックして起動します。

    !pip install pyserial --break-system-packages
    
    import time
    import serial
    
    
    def calculate_checksum(command_bytes):
        """コマンドバイト配列のチェックサム(全バイトの和の下位8ビット)を計算"""
        return sum(command_bytes) & 0xFF
    
    
    def send_command(ser, command_type, data_bytes=[]):
        """DY-HV20Tにコマンドを送信する共通関数"""
        # フレーム構成: 起始値(0xAA) + コマンドタイプ + データ長 + データ + チェックサム
        frame = [0xAA, command_type, len(data_bytes)] + data_bytes
        checksum = calculate_checksum(frame)
        frame.append(checksum)
    
        # バイナリデータとして送信
        ser.write(bytes(frame))
        print(f"Sent: {[hex(b) for b in frame]}")
    
    
    def main():
        # COM3ポートを9600bpsでオープン
        PORT = "COM3"
        BAUDRATE = 9600
    
        try:
            with serial.Serial(PORT, BAUDRATE, timeout=1) as ser:
                print(f"{PORT} に接続しました。")
                time.sleep(2)  # 接続安定のためのウェイト
    
                # 1. 指定曲(先頭ファイル: 0x0001)を再生
                # コマンド番号: 0x07 (指定パス再生)
                # データ: 高位バイト(0x00), 低位バイト(0x01)
                print("先頭ファイルを再生します...")
                send_command(ser, 0x07, [0x00, 0x02])
    
                # 5秒間再生を維持
                time.sleep(5)
    
                # 2. 再生を停止
                # コマンド番号: 0x04 (停止)
                print("再生を停止します...")
                send_command(ser, 0x04)
    
        except serial.SerialException as e:
            print(f"エラー: ポートを開けませんでした。 {e}")
    
    
    if __name__ == "__main__":
        main()
    
    COM3 に接続しました。
    先頭ファイルを再生します...
    Sent: ['0xaa', '0x7', '0x2', '0x0', '0x2', '0xb5']
    再生を停止します...
    Sent: ['0xaa', '0x4', '0x0', '0xae']

    正常動作を確認しました。






    組み立て配線

    秋月電子製 AE-ESP-WROOM-02-DEV は 必要な部品は最初から配線済なのでとても便利です。

    配線が必要な部分は、GND->9,15,20番 5V電源供給->10 RXD->12 TXD->16 LED->19 のみです。

    サウンドボード側

    GND端子->2か所 12V電源端子 RXD端子 TXD端子 外部スピーカー端子 を使用します。
    基板に供給する電圧は、12Vですが、ポートの電圧は3.3Vになっています。
    電圧は35V迄OKですので24V供給するとより大きい音になるかどうかはテストしていません。
    12Vでなるべく大きい音にするには、スピーカーインピーダンスを4Ωタイプを選ぶと20Wが
    出力されると説明があります。

    犬の吠え声は、なるべく激しい吠え声が良いです。
    フリー素材のサイトなどで探して下さい。
    https://soundeffect-lab.info/

    サウンドは、mp3形式でmicroSDカードに保存して下さい。
    MQTTで play コマンドを受信した際に、ESP8266側で「1~3」のランダムな数字を生成し、DY-20Tの「指定パス再生コマンド(0x07)」を使って対象のファイルを狙って再生します。

    microSDカードはFAT32でフォーマットしてからその中に吠え声を
    00001.mp3 00002.mp3 00003.mp3という名前で保存します。

    [Micro-SDカードのルート]
    ├── 00001.mp3
    ├── 00002.mp3
    └── 00003.mp3



    回路図



    スクリプトの解説

    1. 固定IP指定: 指定された 192.168.128.100 を local_IP として設定しています。お使いのルーター環境に合わせて gateway の値を変更してください。
    2. SoftwareSerial(ポート4, 5): SoftwareSerial mySerial(4, 5) により、IO4をRX(受信)、IO5をTX(送信)として定義しています。
    3. DY-20Tコマンド仕様: DY-20Tは AA [コマンド] [データ長] [データ…] [チェックサム] というバイナリ形式で通信します。これを sendCommand 関数で動的に計算して送信しています。
    • play: 0x02(再生)
    • stop: 0x04(停止)
    • sound 0-30: 0x13(音量設定)+ 指定値
    1. 動作確認LED: Wi-Fi接続が確立するとIO16のLEDが常時点灯します。また、MQTTでメッセージを受信するたびに一瞬消灯するインジケーターの役割を持たせています。
    2. 吠え声のバリエーションを 4種類以上(例: 00004.mp3)に増やす場合(コードの random(1, 4) の数字を変更するだけで増やせます)
    3. 毎回同じ吠え声では野生動物に学習されてしまいます。
      再生するたびに完全にランダムを作ることができます。
    4. MQTT経由で「stop」->停止 「sound」->音量(0~30)を送信すれば制御出来ます。
      topic->sequ/wan payload->play stop sound 26 のように送信します。
      soundはsoundと音量数値の間にスペース1個挟みます。

    #include <ESP8266WiFi.h>
    #include <PubSubClient.h>
    #include <SoftwareSerial.h>
    
    const char* SSID = "SSID";//Set Your SSID
    const char* PWD  = "PASSWD";//Set Your PASSWD
    
    // MQTT
    const char* MQTT_SERVER = "192.168.1.200";//Set Your MQTT Server Address
    const uint16_t MQTT_PORT = 1883;
    const char* MQTT_TOPIC = "sequ/wan";
    
    // ESP-WROOM-02: GPIO4 (RX), GPIO5 (TX)
    // ESP GPI 5/TX -> DY RX    DY TX -> ESP GPI 4/RX 
    SoftwareSerial audioSerial(4, 5);
    
    const uint8_t LED_PIN = 16;
    WiFiClient wifiClient;
    PubSubClient mqttClient(wifiClient);
    
    void sendAudioCommand(uint8_t command, uint8_t dataLength, const uint8_t* data) {
      uint8_t frame[20];
      if (dataLength > sizeof(frame) - 4) return;
    
      frame[0] = 0xAA;
      frame[1] = command;
      frame[2] = dataLength;
      uint8_t checksum = frame[0] + frame[1] + frame[2];
    
      for (uint8_t i = 0; i < dataLength; i++) {
        frame[3 + i] = data[i];
        checksum += data[i];
      }
      frame[3 + dataLength] = checksum;
      audioSerial.write(frame, 4 + dataLength);
    
      Serial.print("UART sent:");
      for (uint8_t i = 0; i < 4 + dataLength; i++) {
        Serial.printf(" %02X", frame[i]);
      }
      Serial.println();
    }
    
    void playTrack(uint16_t trackNumber) {
      uint8_t data[2] = {
        (uint8_t)(trackNumber >> 8),
        (uint8_t)(trackNumber & 0xFF)
      };
      
      sendAudioCommand(0x07, 2, data);
    }
    
    void callback(char* topic, byte* payload, unsigned int length) {
      String message;
      message.reserve(length);
      for (unsigned int i = 0; i < length; i++) {
        message += (char)payload[i];
      }
      message.trim();
    
      Serial.print("MQTT topic: ");
      Serial.println(topic);
      Serial.print("MQTT message: [");
      Serial.print(message);
      Serial.println("]");
    
      digitalWrite(LED_PIN, LOW);
    
      if (String(topic) == MQTT_TOPIC && message == "play") {
        // random(min, max)
        uint16_t trackNumber = random(1, 4);
        Serial.printf("Playing random track %u\n", trackNumber);
        playTrack(trackNumber);
      } else if (String(topic) == MQTT_TOPIC && message == "stop") {
        Serial.println("Stopping playback");
        sendAudioCommand(0x04, 0, nullptr); 
      } else if (String(topic) == MQTT_TOPIC && message.startsWith("sound ")) {
        int volume = message.substring(6).toInt();
        if (volume < 0) volume = 0;
        if (volume > 30) volume = 30;
        uint8_t data[1] = { (uint8_t)volume };
        Serial.printf("Setting volume to %d\n", volume);
        sendAudioCommand(0x13, 1, data); 
      } else {
        Serial.println("Unknown command. Use play, stop, or sound 0-30.");
      }
    
      delay(30);
      digitalWrite(LED_PIN, HIGH);
    }
    
    void connectWiFi() {
      WiFi.mode(WIFI_STA);
      WiFi.config(
        IPAddress(192, 168, 1, 100),
        IPAddress(192, 168, 1, 1),
        IPAddress(255, 255, 255, 0)
      );
      WiFi.begin(SSID, PWD);
    
      Serial.print("Connecting to Wi-Fi");
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print('.');
      }
      Serial.println();
      Serial.print("Wi-Fi connected. IP: ");
      Serial.println(WiFi.localIP());
    }
    
    void reconnectMQTT() {
      while (!mqttClient.connected()) {
        Serial.print("Connecting to MQTT... ");
        if (mqttClient.connect("ESP8266_DY20T_Client")) {
          Serial.println("connected");
          if (mqttClient.subscribe(MQTT_TOPIC)) {
            Serial.print("Subscribed: ");
            Serial.println(MQTT_TOPIC);
          } else {
            Serial.println("Subscribe failed");
          }
        } else {
          Serial.print("failed, state=");
          Serial.println(mqttClient.state());
          delay(2000);
        }
      }
    }
    
    void setup() {
      pinMode(LED_PIN, OUTPUT);
      digitalWrite(LED_PIN, HIGH);
    
      Serial.begin(115200); 
      audioSerial.begin(9600);
      randomSeed(ESP.getChipId() ^ micros());
    
      Serial.println();
      Serial.println("MQTT dog sound device starting");
      connectWiFi();
      mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
      mqttClient.setCallback(callback);
    }
    
    void loop() {
      if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Wi-Fi disconnected; reconnecting");
        connectWiFi();
      }
      if (!mqttClient.connected()) reconnectMQTT();
      mqttClient.loop();
    }
    





    NODE-REDの設定方法について

    テスト用フロー (動作テストに使用するフローです)

    コマンドのテストが出来ます。

    [{"id":"918794ccc5072d70","type":"mqtt out","z":"df7fe98051533576","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"a7f27199b8eac3bd","x":630,"y":500,"wires":[]},{"id":"71b7b8b296a5805a","type":"inject","z":"df7fe98051533576","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"sequ/wan","payload":"sound 20","payloadType":"str","x":410,"y":500,"wires":[["918794ccc5072d70"]]},{"id":"4389d61c3c6072f7","type":"mqtt out","z":"df7fe98051533576","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"a7f27199b8eac3bd","x":630,"y":560,"wires":[]},{"id":"907826f01a53c1cc","type":"inject","z":"df7fe98051533576","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"sequ/wan","payload":"stop","payloadType":"str","x":430,"y":560,"wires":[["4389d61c3c6072f7"]]},{"id":"64b33d93066721f3","type":"mqtt out","z":"df7fe98051533576","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"a7f27199b8eac3bd","x":630,"y":620,"wires":[]},{"id":"319e650982f73132","type":"inject","z":"df7fe98051533576","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"sequ/wan","payload":"play","payloadType":"str","x":430,"y":620,"wires":[["64b33d93066721f3"]]},{"id":"a7f27199b8eac3bd","type":"mqtt-broker","name":"","broker":"192.168.1.200","port":1883,"clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]




    本ブログで紹介した「Megadetectorイノシシ威嚇用システム」で使用するフロー

    [{"id":"0b84606f58d67bae","type":"function","z":"df7fe98051533576","name":"animal検出ならplay","func":"// Megadetector JSONの例: { detected: true, objects: [{ class: \"person\" }, ...] }\nvar data = msg.payload;\n\n// JSONノードを通さず文字列で届いた場合にも対応\nif (typeof data === \"string\") {\n    try { data = JSON.parse(data); } catch (e) {\n        node.warn(\"payloadをJSONとして解析できません\");\n        return null;\n    }\n}\n\nif (!data || typeof data !== \"object\") return null;\nif (data.detected === false) return null;\n\nvar objects = data.objects;\nif (!Array.isArray(objects)) {\n    if (objects && typeof objects === \"object\") objects = [objects];\n    else return null;\n}\n\nfunction className(item) {\n    if (typeof item === \"string\") return item.toLowerCase();\n    if (!item || typeof item !== \"object\") return \"\";\n    var value = item.class || item.label || item.name || item.category;\n    if (value && typeof value === \"object\") {\n        value = value.name || value.label || value.class;\n    }\n    return (value == null ? \"\" : String(value)).toLowerCase();\n}\n\nvar hasPerson = objects.some(function(item) {\n    return className(item) === \"animal\";\n});\n\nif (!hasPerson) return null;\nmsg.topic = \"sequ/wan\";\nmsg.payload = \"play\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":120,"wires":[["e52415840e4d3deb"]]}]