スポンサーリンク

ESP32とダイソーのLED蛍光灯を使ってAlexa対応調光機能付きシーリングライトを作る-プログラム編

スポンサーリンク

前回は、Alexa対応調光機能付きシーリングライトの回路を作りました。

今回は、ESP32のプログラムを作っていきたいと思います。

スポンサーリンク

必要な機能

Alexa対応調光機能付きシーリングライトのプログラムに必要な機能は以下のとおりです。

  • Alexa対応する
  • スマホから無線LANのアクセスポイントを設定する
  • Alexaの指示を明るさの制御信号に変換する
  • 無線LANの接続が切れたら再接続する
  • 電源ON時は100%で点灯し、マイコンの再起動時には過去の明るさで点灯する

Alexa対応する

ESP32でAlexaに対応するには以前行った実験で使った、とっても便利なEspalexaというライブラリを使います。

Alexaの部分のプログラムはこのようになります。

//ネットワーク関係
#include <WiFi.h>
#include <Preferences.h>

//Alexa
#include <Espalexa.h>
Espalexa espalexa;

// 明るさ情報を確保
#include <EEPROM.h>
int dim;  //0から100の値

void setup() {
  EEPROM.begin(4);

  //Alexa設定
  espalexa.addDevice("Denki", lightChanged, dim*2.55);
  espalexa.begin();
}

void loop() {
  espalexa.loop();
  delay(1);
}

//Alexa callback functions
void lightChanged(uint8_t brightness) {
  dim = brightness / 2.55; //alexaの値(最大255)を%(最大100)に変換
  EEPROM.write(0, dim); //EEPROMに値を保存
  EEPROM.commit();
  Serial.printf("Alexa:%d->dim:%d[%%]\n", brightness, dim );
}

17行目:Denkiというデバイス名でAlexaから制御できるようにします

18行目:ESPAlexaを起動

あとはループの中でESPalexaを常に実行し続けます。

Alexaが明るさの変更を行うと、27行目のcall Back関数が実行されます。この中で設定さえた明るさをdim変数に保存します。Alexaから与えられる値の範囲は消灯の0から最大の明るさの255までとなっています。プログラム上0〜100%で扱いたいので、2.55で割ってdimの変数に代入しています。

また、29行目で、EEPROMに現在の明るさを保存しています。この理由は後述します。

スマホから無線LANのアクセスポイントを設定する

以前「目覚まし時計風」IN-14ニキシー管置き時計を作った時に使った、WiFiManagerを使います。

このライブラリは、起動時にアクセスポイントに接続できなかったら、ESP32自身がアクセスポイントになります。スマホからこのアクセスポイントに接続すると、無線LANのSSIDとパスワードを設定することができます。

プログラムはとてもシンプルで、以下のようになります。

//WifiManager関係
#include <DNSServer.h>
#include <WebServer.h>
#include "WiFiManager.h"
#define DeviceName "AlexaLight" //アクセスポイント名
#define WMPassword "password"   //パスワード
WiFiManager wm;

void setup() {
  //WiFi設定ポータル起動 WiFiに接続できなかったら10分間だけポータルを起動
  wm.setDebugOutput(false);
  wm.setConfigPortalTimeout(10 * 60);
  bool ret = wm.autoConnect(DeviceName, WMPassword);
}

7行目でクラスを生成

12行目で起動時に10分間だけWiFiManagerを有効に設定し

13行目でポータルの起動と自動接続を行います。スマホから接続するSSID名は5行目で定義した”AlexaLight”、パスワードは6行目で定義した”password”となっています。これらは自由に変更可能です。

Alexaの指示を明るさの制御信号に変換する

Alexaから指定された明るさは、グローバル変数dimに0〜100の値で保存されています。

dimが0の場合は照明をOFFするためにソリッドステートリレー(S0)をOFFし、0以外ではONします。dimが1〜255の場合は3つのフォトモスリレー(S1,S2,S3)を適切に設定して8段回の明るさに調光します。

今回、dimの値が変更されたら、すぐにその明るさになるのではなく、指定した明るさまでゆっくりと明るさが変化するようにしたいと思います。

この部分のプログラムは以下のとおりです。

//GPIO
#define LED1_S0 26
#define LED1_S1 25
#define LED1_S2 33
#define LED1_S3 32
#define LED2_S0 13
#define LED2_S1 12
#define LED2_S2 14
#define LED2_S3 27

//タスクハンドル
TaskHandle_t taskHandle[10];
enum task { dimControl };

void setup() {
  //IOピンの初期化
  pinMode(LED1_S0, OUTPUT);
  pinMode(LED1_S1, OUTPUT);
  pinMode(LED1_S2, OUTPUT);
  pinMode(LED1_S3, OUTPUT);
  pinMode(LED2_S0, OUTPUT);
  pinMode(LED2_S1, OUTPUT);
  pinMode(LED2_S2, OUTPUT);
  pinMode(LED2_S3, OUTPUT);

  digitalWrite(LED1_S0, LOW);
  digitalWrite(LED1_S1, LOW);
  digitalWrite(LED1_S2, LOW);
  digitalWrite(LED1_S3, LOW);
  digitalWrite(LED2_S0, LOW);
  digitalWrite(LED2_S1, LOW);
  digitalWrite(LED2_S2, LOW);
  digitalWrite(LED2_S3, LOW);

  //明るさ制御タスク
  xTaskCreatePinnedToCore(dimControlTask, "dimControl", 4096, NULL, 0, &taskHandle[dimControl], 0);
} 

//LEDを0〜8の明るさにする
void LEDDimm( int value )
{
  int s0 = LOW , s1 = LOW , s2 = LOW , s3 = LOW;
  if ( value < 0 )
    value = 0;
  if ( value > 8 )
    value = 8;

  switch ( value ) {
    case 0:
      s0 = LOW; s1 = LOW; s2 = LOW; s3 = LOW;
      break;
    case 1:
      s0 = HIGH;  s1 = LOW; s2 = LOW; s3 = LOW;
      break;
    case 2:
      s0 = HIGH;  s1 = HIGH;  s2 = LOW; s3 = LOW;
      break;
    case 3:
      s0 = HIGH;  s1 = LOW; s2 = HIGH;  s3 = LOW;
      break;
    case 4:
      s0 = HIGH;  s1 = HIGH;  s2 = HIGH;  s3 = LOW;
      break;
    case 5:
      s0 = HIGH;  s1 = LOW; s2 = LOW; s3 = HIGH;
      break;
    case 6:
      s0 = HIGH;  s1 = HIGH;  s2 = LOW; s3 = HIGH;
      break;
    case 7:
      s0 = HIGH;  s1 = LOW; s2 = HIGH;  s3 = HIGH;
      break;
    case 8:
      s0 = HIGH;  s1 = HIGH;  s2 = HIGH;  s3 = HIGH;
      break;
    default:
      s0 = LOW; s1 = LOW; s2 = LOW; s3 = LOW;
  }

  if ( s0 == LOW )
  {
    digitalWrite(S0, s0);
    digitalWrite(LED1_S0, s0);
    digitalWrite(LED2_S0, s0);
  }

  //いったん消す
  digitalWrite(LED1_S3, LOW);
  digitalWrite(LED1_S2, LOW);
  digitalWrite(LED1_S1, LOW);
  digitalWrite(LED2_S3, LOW);
  digitalWrite(LED2_S2, LOW);
  digitalWrite(LED2_S1, LOW);

  //明るさを設定
  digitalWrite(LED1_S1, s1);
  digitalWrite(LED1_S2, s2);
  digitalWrite(LED1_S3, s3);
  digitalWrite(LED2_S1, s1);
  digitalWrite(LED2_S2, s2);
  digitalWrite(LED2_S3, s3);
  digitalWrite(S1, s1);
  digitalWrite(S2, s2);
  digitalWrite(S3, s3);

  if ( s0 == HIGH )
  {
    digitalWrite(S0, s0);
    digitalWrite(LED1_S0, s0);
    digitalWrite(LED2_S0, s0);
  }
}

/********* タスク *********/
//明るさを滑らかに変更していくタスク
void dimControlTask(void* arg)
{
  int nowDim = 0;
  double now_dim = 0;
  int gosa_dim = 0;
  int out = 0;
  int led_out_old = -1;
  double setDim = 0;
  double targetDim = 0;

  while (1)
  {
    //0〜100%を誤差拡散で8段階に分類
    now_dim += round(setDim) - gosa_dim;
    out = round( now_dim / 12.5 );
    gosa_dim = round(out * 12.5);

    targetDim = ceil( dim / 12.5 ) * 12.5;
    if ( setDim < targetDim )
      setDim += 0.25;
    if ( setDim > targetDim )
      setDim -= 0.25;
    if ( setDim < 0 )
      setDim = 0;
    if ( setDim > 100 )
      setDim = 100;

    //過去の値と違っていれば明るさを変更する
    if( led_out_old != out )
    {
      LEDDimm( out );
      Serial.printf("setDim=%3.2f out=%d targetDim=%3.2f\n", setDim, out, targetDim);
    }
    led_out_old = out;

    delay(1);
  }
}

LEDDimm関数は、0から8の値からS0,S1,S2,S3の制御信号に変換しています。S1からS3は同時にON,OFFが変化するのではなく、プログラムで記述した順番にIOが変化します。そのため変化中は過去の状態と更新後の状態が混ざった状態となり、意図しない明るさになってしまいます。そのためいったん全て消灯し、S1,S2,S3の変更が終わってからS0をONさせています。

目的の明るさまでゆっくりと変化することを実現するために、明るさの変更はタスクを利用しています。36行目で、明るさを変更するタスクdimControlTaskを起動しています。

dimControlTaskは、dimで指定された値に向けて、現在の値に少しずつ加算、減算していきます。今回の調光回路は明るさの段階が8段階しかないため、シグマデルタの誤差拡散を利用して、擬似的に細かい階調を表現し滑らかに明るさが変化するようにしてみました。

無線LANの接続が切れたら再接続する

常に起動していると、いつの間にか無線LANが切れることもあると思うので、接続されているか常時監視し、切れていたら再接続を行うようにします。

//タスクハンドル
TaskHandle_t taskHandle[10];
enum task { dimControl, wifiConnect };

void setup() {
  //無線LANが切れたら再接続するタスク
  xTaskCreatePinnedToCore(wifiConnectTask, "wifiConnect", 4096, NULL, 1, &taskHandle[wifiConnect], 1);
}

//無線LANが切れたら再接続するタスク
void wifiConnectTask(void* arg)
{
  int wiFiStatus = WL_DISCONNECTED;
  int connecting = 0;

  while (1)
  {
    //無線LAN未接続
    if ( WiFi.status() != WL_CONNECTED && connecting == 0)
    {
      digitalWrite(WiFiEnable, LOW);

      //無線LANに接続する
      WiFi.mode(WIFI_STA);
      WiFi.begin();

      //記録されているSSIDを表示する
      char wifi_ssid[100] = {};
      Preferences preferences;
      preferences.begin("nvs.net80211", true);
      preferences.getBytes("sta.ssid", wifi_ssid, sizeof(wifi_ssid));
      Serial.printf("Connecting to %s.\n", &wifi_ssid[4]);

      connecting = 1;
    }

    //接続待ち
    if ( connecting > 0 )
    {
      connecting++;
      Serial.printf("*");

      //WiFiLED点滅
      if ( digitalRead(WiFiEnable) == LOW )
        digitalWrite(WiFiEnable, HIGH );
      else
        digitalWrite(WiFiEnable, LOW );

      //30秒待っても接続できなかったら再接続
      if ( connecting > 2 * 30 )
      {
        WiFi.disconnect();
        connecting = 0;
        Serial.printf("\n");
        delay(5);
      }
    }

    //無線LAN接続済み
    if ( WiFi.status() == WL_CONNECTED )
    {
      //WiFi接続できた
      if ( wiFiStatus != WL_CONNECTED )
      {
        Serial.print("\nWiFi connected\r\nIP address: ");
        Serial.println(WiFi.localIP());
        digitalWrite(WiFiEnable, HIGH); //WiFiLED点灯
        wiFiStatus = WL_CONNECTED;
      }
      connecting = 0;
    }
    delay(500);
  }
}

常時監視するため、タスクを利用しています。

7行目でタスクの起動を行っています。wifiConnectTaskタスクは、無線LANが切れていたら接続するだけのタスクです。

電源ON時は100%で点灯し、マイコンの再起動時には過去の明るさで点灯する

壁のスイッチをONした時には、照明が必ずONして欲しいものです。

また、マイコンは何かのトラブルでウォッチドッグタイマーが動作するなどして勝手に再起動することがあります。夜寝ている時に再起動して勝手に明かりがついてしまったら、びっくりしてしまいます。逆に、照明がついている時に再起動し勝手に消えてしまうのも、困り物です。

そこで、現在の明るさをEEPROM記憶しておいて、再起動の場合には過去の明るさで点灯するようにします。

ここで必要なのが、ブートした時の原因を知ることです。パワーオンによるブートなのか、ウォッチドッグタイマーやブラウンアウトなどによるリセットのブートなのか、ブートの原因を知る機能が必要です。実際、リセット要因を知る方法があるのですが、試したところパワーオン時にもウォッチドックタイマーによるリセットが発生してしまっていて、パワーオンリセットと、本当のウォッチドッグによるリセットの区別ができませんできた。

そこで、以下のような回路を追加しました。

電源がONするとコンデンサにゆっくりと電気が溜まっていきます。約5秒間くらいは、GPIO36ピンをリードするとLOWと認識されます。それ以降はコンデンサに電気が溜まりHIGHとなります。マイコンが途中でリセットがかかった場合には、コンデンサはすでに満タンになっているため、GPIOピンはHIGHの状態です。

電源が切れると、ダイオードを経由してコンデンサに溜まった電気が放電されます。

このため、プログラムのsetup中でGPIOのピンの状態を確認することで、LOWであればパワーオンしたことがわかり、HIGHであれば再起動したことがわかります。

//GPIO
#define POWERON 36

void setup() {
  //IOピンの初期化
  pinMode(POWERON, INPUT);
  
  //電源ONだったら100%、ソフトリセットだったら過去の明るさにする
  EEPROM.begin(4);
  if( digitalRead(POWERON) == LOW )
  {
    Serial.printf("Power ON");
    dim = 100;  //PowerOnResetなら最大の明るさ
    EEPROM.write(0, dim); //EEPROMに値を保存
    EEPROM.commit();
  }
  else
  {
    Serial.printf("RESET");
    dim = EEPROM.read(0); //EEPROMに保存してある明るさにする
  }
  Serial.printf(" dim:%d\n", dim);
}

起動時に、POWERONピンをチェックして、LOWであれば100%の明るさにし、HIGHであればEEPROMの値を読みこんでdim変数にセットしています。

スポンサーリンク

プログラムが完成しました

以上のプログラムを合体させて、Alexa対応調光機能付きシーリングライトのプログラムが完成しました。

全体のプログラムはこちらに置いておきます

セットアップ

ESP32にプログラムを書き込むと、まずはWiFiManagerが起動します。スマホからのWiFiの設定方法はこちらをご覧ください。

ご自宅の無線LANに接続されると、Alexaアプリからデバイスの追加を行います。設定の仕方はこちらをご覧ください。

今回のプログラムでは「Denki」というデバイス名になっています。セットアップ後、「電気」や「ライト」など言いやすい名前に変更することができます。

動作確認

さて、ここまで設定が終わったらAlexaで照明をON、OFFしてみましょう。

Amazon Echoに向かって「アレクサ、電気をつけて」「アレクサ、電気を消して」と声をかけます。

パッと光るのではなく、フワッと光ったり消えたりしますね。動作完璧です。

「アレクサ、電気を暗くして」「アレクサ、電気を明るくして」「アレクサ、電気を50%にして」も反応してくれます。

さて、ソフトウェアも完成しました。次回は、蛍光灯式の古いシーリングライトに組み込んで、Alexa対応調光機能付きのシーリングライトに改造したいと思います。

2020.7.23 追加 つづきはこちら

追加終わり

コメント