これまでのあらすじ:
2016年3月、フェルト生地を手で裁断している際にレーザーカッターがあれば複雑なカットが容易にできるなあと思って、安価になってきたレーザーカッターを購入しようと思ったのがきっかけ。調べていくうちに、合板も切れたほうがいいと思うようになって、CNCルーター(CNCミリング)についても考えるようになった。
Arduinoは以前から使っており、CNCシールドがあると気付いて自作も可能と思うようになった。当初はShapeOkoやX-CARVEを参考にMakerSlide、OpenRail、V-Wheel、2GTタイミングベルトなどで5万円くらいで自作しようと思っていた。AliExpressでも部品が安く買えることが分かって、しばらくは部品探し。探せば探すほど安くて本格的な部品も見つかってくるので、そんなにケチらなくてもいいのではないかと徐々にスペックアップ。最終的には剛性や精度のことも考えてボールスクリューやリニアスライドを使うことになり、予想以上に重厚な3軸CNCマシンをつくることに(約7万円)。
構想から約5週間(制作約3週間)でルーターとレーザーともに使えるようになり、現在はgrbl1.1+Arduino CNCシールドV3.5+bCNCを使用中(Macで)。余っていたBluetoothモジュールをつけてワイヤレス化。bCNCのPendant機能でスマホやタブレット上のブラウザからもワイヤレス操作可能。


CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:

*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


2017年3月24日金曜日

IoTその4:再度ESP8266+Blynk

もともとはCNCマシンのワイヤレス化(bCNCのPendant機能Bluetooth装備など)から始まったのですが、bCNCのカメラ機能をIPカメラでも撮影可能にしようとしたところ、
Pythonの再学習(bCNC自体がPythonで書かれているため)
・Wifiデバイス(ESP8266ESP32)の利用
ということになり、今まで使っていたArduino IDEだけではなく、
ESP-IDF
PlatformIOAtom Editor
もやってみようかと。
要はパソコンやスマホのブラウザからもいろんなものを操作可能にしたいということなので、
・HTML
・Javascript
も同時に学習していかなければいけない。やらなければいけないことが一気に増えてしまったという感じです。

その結果、ある程度はESP8266を使って、
・エアコンのオンオフ(赤外線信号)
・温度と湿度の読み込み
・照明器具のオンオフ(あるいは、CNCマシンのメイン電源のオンオフ)
などをスマホのChrome上からも操作できるようになりました。

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>

#define RelayPin 5
#define IRLedPin 15
#define DHTPin   2
#define LedPin   4

#define duty_high 8
#define duty_low 16
#define DHTTYPE DHT11

DHT_Unified dht(DHTPin, DHTTYPE);

uint32_t delayMS;
String temp;
String humid;

const char* ssid = "*****";
const char* password = "*****";

MDNSResponder mdns;
ESP8266WebServer server(5900);

String webPage = "";

unsigned long airOn[243] = {
 3400, 1400, 600, 250, 550, 250, 550, 1050, 550, 250, 550, 1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550, 
 1050, 550, 250, 550, 300, 500, 250, 550, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 300,
 500, 300, 500, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350,
 450, 1150, 450, 1150, 450, 1150, 450, 1150, 450, 1150, 450, 1200, 450, 350, 450, 350, 450, 350, 450, 1150,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 1200, 450, 350,
 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 400,
 400, 1200, 400, 400, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 400,
 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400,
 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 1150, 450, 1150, 450, 350, 450, 1200, 400, 400, 400,
 400, 400, 1200, 400, 400, 400};
unsigned long airOff[99] = {
 3400, 1450, 550, 250, 550, 250, 500, 1100, 550, 250, 550, 1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550,
 1050, 550, 300, 500, 250, 550, 300, 500, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 350,
 450, 300, 500, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450};

void setup() {
  pinMode(IRLedPin, OUTPUT);
  pinMode(LedPin, OUTPUT);
  pinMode(RelayPin, OUTPUT);
  digitalWrite(LedPin, HIGH);
  digitalWrite(RelayPin, LOW);
  dht.begin();
  sensor_t sensor;
  dht.temperature().getSensor(&sensor);
  dht.humidity().getSensor(&sensor);
  temp_humid();
  webContents();
  
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  delay(1000);
  WiFi.begin(ssid, password);
  Serial.println("");
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
  WiFi.config(IPAddress(192,168,3,10),IPAddress(192,168,3,1),IPAddress(255,255,255,0));
  
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
  if (mdns.begin("mirror", WiFi.localIP())) {
    Serial.println("MDNS responder started");
  }

  server.on("/", []() {
    temp_humid();
    webContents();
    server.send(200, "text/html", webPage);
  });
  server.on("/index.html", []() {
    temp_humid();
    webContents();
    server.send(200, "text/html", webPage);
  });
  server.on("/on", []() {
    server.send(200, "text/html", webPage);
    digitalWrite(LedPin, LOW);
    digitalWrite(RelayPin, HIGH);
    delay(1000);
  });
  server.on("/off", []() {
    server.send(200, "text/html", webPage);
    digitalWrite(LedPin, HIGH);
    digitalWrite(RelayPin, LOW);
    delay(1000);
  });
  server.on("/airon", []() {
    temp_humid();
    webContents();
    server.send(200, "text/html", webPage);
    air_on();
    delay(1000);
  });

  server.on("/airoff", []() {
    temp_humid();
    webContents();
    server.send(200, "text/html", webPage);
    air_off();
    delay(1000);
  });

  server.on("/temphumid", []() {
    webContents();
    server.send(200, "text/html", webPage);
    delay(1000);
  });

    ArduinoOTA.onStart([]() {
    Serial.println("Start");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();  
  server.begin();
  Serial.println("HTTP server started");
  delayMS = sensor.min_delay / 1000;
}

void loop(void) {
  ArduinoOTA.handle();
  server.handleClient();
  yield();
}

void air_off() {
  int dataSize = sizeof(airOff) / sizeof(airOff[0]);
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = airOff[cnt];
    unsigned long us = micros();
    while (us + len > micros()) {
      digitalWrite(IRLedPin, 1 - (cnt & 1));
      delayMicroseconds(duty_high);
      digitalWrite(IRLedPin, 0);
      delayMicroseconds(duty_low);
    }
  }
}

void air_on() {
  int dataSize = sizeof(airOn) / sizeof(airOn[0]);
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = airOn[cnt];
    unsigned long us = micros();
    while (us + len > micros()) {
      digitalWrite(IRLedPin, 1 - (cnt & 1));
      delayMicroseconds(duty_high);
      digitalWrite(IRLedPin, 0);
      delayMicroseconds(duty_low);
    }
  }
}

void temp_humid() {
  delay(delayMS);
  sensors_event_t event;
  dht.temperature().getEvent(&event);
  if (isnan(event.temperature)) {
    temp = "Error reading!";
  } else {
    temp = String(event.temperature);
  }
  dht.humidity().getEvent(&event);
  if (isnan(event.relative_humidity)) {
    humid = "Error reading!";
  } else {
    humid = String(event.relative_humidity);
  }
}

void webContents() {
  webPage = "";
  webPage += "<html><header><title>WIFI SWITCH</title></header>";
  webPage += "<body style=\"text-align:center;font-size:48px\"><div>ESP8266 WEB SERVER</div><br/>";
  webPage += "<div><div>AC100V SWITCH:</div>";
  webPage += "<div><a href=\"on\"><button style=\"width:80%;font-size:60px\">AC100V:  ON</button></a></div><br/>";
  webPage += "<div><a href=\"off\"><button style=\"width:80%;font-size:60px\">AC100V: OFF</button></a></div><br/>";
  webPage += "<div>AIR-CON SWITCH:</div>";
  webPage += "<div><a href=\"airon\"><button style=\"width:80%;font-size:60px\">AIR-CON: ON</button></a></div><br/>";
  webPage += "<div><a href=\"airoff\"><button style=\"width:80%;font-size:60px\">AIR-CON: OFF</button></a></div>";
  webPage += "<div>Temperature: ";
  webPage += temp;
  webPage += " *C</div>";
  webPage += "<div>Humidity: ";
  webPage += humid;
  webPage += " %</div>";
  webPage += "</div></body></html>";
}


これは前回からの改良で、
・ESP8266
・赤外線LED(エアコン用)
・温度湿度センサー
・リレー(照明器具AC100V用)
を使っています。OTAのコードも含めたので、ワイヤレスでコードの書き換えが可能です。そのぶんコードも長く、メモリ消費量も多いプログラムとなりました。
基本的にはローカルネットワーク内でしか使えないのですが、
Wifi.config()を使えば固定IPアドレスにできるので、ルーターをポートフォワーディングしてみると、外部からもつながりました(スマホ4G通信で確認)。以下はブラウザ上の操作画面。


LANだけでなくWANからも操作できてよかったのですが、以前みつけたスマホアプリのBlynkでやってみたらどうなるかも試してみました。このアプリ見つけたのはいいのですが、しばらく使っていませんでした。どのくらい便利なのか?


以前、ざっと説明を見た限りでは、アプリ上でボタンやスイッチ部品を配置して、余計なコードは書かずにプログラムをアップロード(ワイヤレスでスマホから?)という感じでしたが、
実際手順に沿ってやってみると:
・スマホアプリをインストール+メールアドレスを登録
・New Project作成(使うハードウェアを指定:ESP8266など)
・セキュリティコードがメールへ届く
・Arduino IDEにBlynkライブラリをアップロードする
・Arduino IDEでセキュリティコードとWifiのIPアドレス+パスワードを専用プログラムに追記する
・専用プログラムをパソコンからUSB経由でESP8266へアップロード
・再度スマホアプリに戻り、好きなピンを割り当てながらボタンやスイッチを配置
・LANだけでなくWANからも操作可能
という感じです。


FirmataのようなBlynkファームウェア:
いちいちArduino IDEでプログラムを書かなくても済むというのが予想外に便利でした。最初は、スマホでボタンやスイッチへピンを割り当てて、それにあわせたプログラムをArduino IDEのほうでも書かなければいけないのかと思っていましたが、そうではなく、Blynkの専用プログラムがFirmataのようなファームウェアとして機能しているため、後からでも自由自在にBlynkアプリでESP8266のピンの機能割り当てができるという感じです。デジタル入力にするかデジタル出力にするか、それともPWM出力にするかなど、いちいちESP8266にプログラムをアップロードしなくても、その場で自由に変えられるということです。
ESP8266の場合は、
スケッチ例>Blynk>Boards_WiFi>ESP8266_Standalone
を選択し、
このプログラム内の、
YourAuthToken:メールで送られてきたセキュリティコード
YourNetworkName:自宅Wifiネットワーク名
YourPassword:自宅Wifiのパスワード
を書き込むだけでOKです。すべてのピンへの機能割り当てはBlynkのほうでしてくれるので、他のプログラムを書き込む必要はありません。
最初に送られてくるセキュリティコードはブロジェクトごとに使えるので、ボタンの配置や機能の割り当てを変えてもずっと使えます。

ということで、先ほどのプログラムをBlynkに適用させてみようかと。
しかし、赤外線信号の部分はBlynkではできなさそうなので、Blynkの応用的な機能であるVirtual Pinというものを使ってみました。このVirtual Pinは、実際のESP8266上のピンとは無関係で、Blynk上で使える変数やトリガーのようなものです。今回の赤外線信号の場合であれば、ボタンがオンで1、オフで0という値を割り当てておいて、Virtual Pinが1ならエアコンをオンにする赤外線信号を発する、0ならオフの信号を発するという感じにしておきます。そんな感じで、一度Virtual Pinを介して動作するプログラムにしてみました。

#define BLYNK_PRINT Serial
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>

#define IRPin  4
#define DHTPin  2
#define duty_high 8
#define duty_low 16
#define DHTTYPE DHT11
DHT_Unified dht(DHTPin, DHTTYPE);
int temp;
int humid;

char auth[] = "*****";
char ssid[] = "*****";
char pass[] = "*****";

unsigned long data_on[243] = {
 3400, 1400, 600, 250, 550, 250, 550, 1050, 550, 250, 550, 1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550,
 1050, 550, 250, 550, 300, 500, 250, 550, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 300,
 500, 300, 500, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350,
 450, 1150, 450, 1150, 450, 1150, 450, 1150, 450, 1150, 450, 1200, 450, 350, 450, 350, 450, 350, 450, 1150,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 1200, 450, 350,
 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 400,
 400, 1200, 400, 400, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 400,
 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 400,
 400, 400, 400, 400, 400, 400, 400, 400, 450, 350, 450, 1150, 450, 1150, 450, 350, 450, 1200, 400, 400, 400,
 400, 400, 1200, 400, 400, 400};
unsigned long data_off[99] = {
 3400, 1450, 550, 250, 550, 250, 500, 1100, 550, 250, 550, 1050, 550, 250, 550, 250, 550, 250, 550, 1050, 550,
 1050, 550, 300, 500, 250, 550, 300, 500, 1100, 500, 1100, 500, 300, 500, 300, 500, 300, 500, 300, 500, 350,
 450, 300, 500, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150, 450, 350, 450, 350,
 450, 350, 450, 350, 450, 350, 450, 400, 450, 350, 450, 1150, 450, 350, 450, 350, 450, 350, 450, 350, 450, 1150,
 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450, 350, 450};

void setup(){
  Serial.begin(9600);
  Blynk.begin(auth, ssid, pass);
  pinMode(IRPin, OUTPUT);
  dht.begin();
  sensor_t sensor;
  dht.temperature().getSensor(&sensor);
  dht.humidity().getSensor(&sensor);
  dht.humidity().getSensor(&sensor);
}

BLYNK_WRITE(V0){
  int value = param.asInt();
  if (value) {
    air_on();
  } else {
    air_off();
  }
}

BLYNK_READ(V1){
  temp_humid();
  Blynk.virtualWrite(V1, String(temp)+" °C");
  Blynk.virtualWrite(V2, String(humid)+" %");
  Blynk.virtualWrite(V3, temp);
}

void air_on() {
  int dataSize = sizeof(data_on) / sizeof(data_on[0]);
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = data_on[cnt];
    unsigned long us = micros();
    while (us + len > micros()) {
      digitalWrite(IRPin, 1 - (cnt & 1));
      delayMicroseconds(duty_high);
      digitalWrite(IRPin, 0);
      delayMicroseconds(duty_low);
    }
  }
}

void air_off() {
  int dataSize = sizeof(data_off) / sizeof(data_off[0]);
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = data_off[cnt];
    unsigned long us = micros();
    while (us + len > micros()){
      digitalWrite(IRPin, 1 - (cnt & 1));
      delayMicroseconds(duty_high);
      digitalWrite(IRPin, 0);
      delayMicroseconds(duty_low);
    }
  }
}

void temp_humid(){
  sensors_event_t event;  
  dht.temperature().getEvent(&event);
  if (isnan(event.temperature)) {
    temp=0;
  }else {
    temp=int(event.temperature);
  }
  dht.humidity().getEvent(&event);
  if (isnan(event.relative_humidity)) {
    humid=0;
  }else {
    humid=int(event.relative_humidity);
  }
}

void loop(){
  Blynk.run();
}
こんな感じで、BLYNK_WRITE(V0)内に条件分岐をつくって赤外線信号のオンオフ操作をしています。BLYNK_READ(V1)でも、温度センサから読み取った値をBlynkアプリへ渡すようにしています。

スマホアプリのほうでは、こんな感じのボタン配置にしました。
一番上の黄色いスイッチが照明用、その下の白いスイッチがエアコン用、その下に温度と湿度表示、さらに温度のグラフもあります。一応4G通信で外部からアクセスも可能でした。
しかし、温度グラフに関しては、一旦このアプリを消してしまうとそれまでの記録も消えてしまってダメでした。もしかしたら配列を使って過去の記録を覚えさせていけないのかもしれません。このグラフ機能以外にもヒストリーグラフというウィジットがあって、それならいいのかもしれませんが、実はこのアプリ、各ウィジットにはポイント(コスト)があり、例えばボタン一個で200エナジー、グラフが400エナジーであり、合計の上限が決まっています。

ヒストリーグラフは何と900エナジーもします(赤い数値)。この段階で、ボタン2個なので400エナジー、温度と湿度数値表示で400エナジー、そしてグラフで400エナジー、このプロジェクトだけで合計1200エナジー消費しています。もう一つ別のプロジェクトもあって、そっちで500エナジー消費しているので、すべてのプロジェクトを合わせて1700エナジー使っていることになります。残り200エナジーしかありません(画面上部)。
そして、このエナジー量を増やすには課金が必要ということでした。すっかりオープンソースで無料なのかと思っていたら、なるほど、よくある一部有料というアプリでした。

ということで、温度グラフのかわりに消費エナジー500のビデオストリーミングも試してみました。
以前使ったIP WebCamというアプリでタブレットのカメラから配信させてみました。
LAN通信ですが、ちゃんと映ります。これはビデオなのでESP8266とは無関係。このビデオストリーミングの場合、カメラのIPアドレスの欄にhttp://192.168.3.2:8080/videoを入れると大丈夫でした。温度グラフよりも、こっちのほうがよさげ。
ただし、Blynkのサイトにも書いてありますが、Blynkサーバはストリーミングサーバを提供していないためWANからは見れないということです。その場合は他のサービスを使えと書いてあります。

とはいったものの、Blynkで当初予定していた内容はほぼできてしまいました。しかも設定なども簡単です。自前でHTMLやJavascriptまで書いてESP8266をサーバにしてもいいのですが、たしかに面倒。当然自分の思い描いているような内容にすることはできますが、Blynkだとあっというまにできてしまったので、これはこれでいいのかもしれません。この手のサービスはWANからもアクセス可能という部分が便利かもしれません。自宅のルーターをポートフォワーディングさせれば済むことですが、セキュリティなどのことも考えると面倒なので、外部サーバーを利用できるのであれば、それに越したことはありません。

次のステップとしては、ESP8266かESP32にカメラモジュールを接続して小型のIPカメラをつくろうと思っていますが、なかなか思うように進みません。その小型IPカメラをCNCマシンに搭載し、bCNCのカメラ機能をIP化するところまで行くというのがとりあえずの目標です。
ただ、そのついでにその他の照明や電源などもIP化しようとしているので、いろいろやることが増えているという現状です。あわよくば、スマホ上のボタン操作ではなく、音声入力で照明をオンにできないかとも考えていて、Python、HTML5、Javascript、場合によってはPHPなども同時進行という感じです。さらには、それらの開発環境となるAtom EditorやPlatformIOの使い方も学習中です。

0 件のコメント:

コメントを投稿