PWM制御をマスターしよう!Node-REDでサーボモータを遠隔制御する方法&初ハンズオンレポート
暑さ本番、いよいよ大好きな生ビールの季節がやってまいりました!
みなさまいかがお過ごしでしょうか?どうも、宇宙エンジニアのたくろーどんです。毎日、暑いですね。
さて先日、初めてハンズオンイベントを企画・実施してみました!題して、「【IoT】PWM制御をマスター!Nefry BT+Node-REDでサーボモータを遠隔制御」!
「モノづくりをしたい!」「こんなものをつくりたい!」と思っても「こういう技術がつかえる」ということを知らないと、なかなかモノづくりが進みません。
なので、動くものを作りたいときに便利なサーボモータとその制御方法であるPWM制御をマスターし、PCからサーボモータを遠隔制御をしようというのがハンズオンの目論見です。
では、ちらっとハンズオンの内容を振り返ってみたいと思います。
PWM制御を用いたプログラムの書き方
PWM制御について
PWM制御の大まかな説明は以前、ドキュメントに書きました。ようするに、PWM制御は「パルスのオン・オフ繰り返し切り替えることで出力される電力を制御」するという意味です。
でも結局のところ、「PWM制御ってどんなイメージやねん!」と多くの方が思うでしょう。
なので、これがわかりやすいのではないかという説明を思いつきました。それが、下記の動画です。
例えば、自宅から最寄りの駅まで走らなければならない状況を考えます。まるで、小学校の算数にでてくるたかしくんの問題を彷彿させますね。知らんけど。
- 100%の力で走れば5分で駅につく ≒ 100%出力されるので、5V出力される
- 全力で走って途中で歩くと9分で駅に着く(全体的に80%の力で走る) ≒ 80%出力されるので、4V出力される
- 全力で走って途中で歩くと10分で駅に着く(全体的に50%の力で走る) ≒ 50%出力されるので、2.5V出力される
- 全力で走って途中で歩くと15分で駅に着く(全体的に10%の力で走る) ≒ 10%出力されるので、0.5V出力される
というようなイメージです。自宅から駅まで走る場合は走る速さの具合によって、到着時間が伸びます。しかし、電圧の場合は実行電圧(実際に出力される電圧)は下がっていきます。
ESP32開発ボードでのプログラムについて
Nefry BTのようなESP32を搭載しているような開発ボードでPWM制御する場合は、ledc関数を使います。ESP32ではArduinoでつかうようなanalogWrite関数が実装されていないとのことなので注意しましょう(ただし、今後変更される可能性があります)。
例えば、下記のようなプログラムになります。
//基本Iotは2.4GHz帯を使うこと
#include<Nefry.h>
#include"esp32-hal-ledc.h"
//PWM信号の周波数をPWMをつくっているクロック周波数で割ったもの
//ledcSetupでPWMの範囲を8bitに設定したとき、0~255 10bitのときは0~1023となる、2進数の話
#define PWM_BITWIDTH 16
//わかりやすいように角度に変換する関数/////////////////////
int deg2pw(int deg, int bit){
double ms = ((double) deg - 90.0) * 0.95 / 90.0 + 1.45;
return (int) (ms / 20.0 * pow(2, bit));
}
//////////////////////////////////////////////////////////
void setup() {
Serial.begin(115200);
//ledcSetup(チャンネル数(0~),周波数(たとえばPWMサイクル20mHzなら50Hzになるという意味(SG90の場合))、分解能は任意(ただし限度はある))
ledcSetup(0,50,PWM_BITWIDTH);
ledcAttachPin(ピン番号,0);//left_motor
Nefry.enableSW();
}
void loop(){
if((Nefry.readSW())){
ledcWrite(0,deg2pw(90, PWM_BITWIDTH));
delay(800);
ledcWrite(0,deg2pw(0, PWM_BITWIDTH));
}
}
プログラムの書き方
プログラムを詳しく見てましょう。
ledc関数を使うため、ライブラリ「esp32-hal-ledc.h」を最初にインクルードします。
#include"esp32-hal-ledc.h"
ライブラリ「esp32-hal-ledc.h」を使うときの主な関数は以下に示すものです。
- ledcSetup(チャンネル,周波数,分解能)
- ledcAttachPin(ピン番号,チャンネル):チャンネルはピン番号の識別番号です
- ledcWrite(チャンネル,パルス幅):設定した角度になるように与えるべきパルス幅を算出する関数
実際の記述は以下のようになります。
void setup() {
Serial.begin(115200);
ledcSetup(0,50,PWM_BITWIDTH);
ledcAttachPin(A2,0);//left_motor
Nefry.enableSW();
}
分解能はどれだけ細かく制御するのかを表しているイメージです。これはビット数で表します。
例えば、分解能を8ビットとするれば0〜255バイト(2の8乗)、10ビットなら0〜1023バイト(2の10乗)となります。
これを、どのようにPWM制御に使えるのか説明します。PWM制御とは一定電圧の入力からパルス列のオンとオフの一定周期を作り、オンの時間幅を変化させる電力制御方式のことです。つまり、実効電圧を変化させることができます。
なので、例えば、最大5[V]の出力を考えたとき、
- 分解能8ビットだと0→0[V]、127→2.5[V]、255→5[V]
- 分解能10ビットだと0→0[V]、511→2.5[V]、1023→5[V]
と表せます。分解能が大きい方が、細かい制御ができることがわかるかと思います。
Node-RED×MQTTでサーボモータを遠隔制御
Node-REDとは
Node-REDはハードウェアデバイス/APIおよびオンラインサービスを接続するためのツールです。ウェブ上で、フローチャートのように直感的にプログラムをつくることができます。そして、最終的に自分が欲しい仕組みを作り上げるというものです。
Node-REDを使うには大まかに2パターンあります。
- 自分のPC上でローカルで動かす(今回はこちらで進めていきます)
- 外部のサービスを利用する(IBMクラウドやenebularなど)
導入方法は、こちらで説明していますので参考にしてください。
また、後ほどNode-REDにフローのコードを貼り付ける必要があるので簡単に貼り付け方法を説明します。
まずフローのコードをコピーし、Node-REDを立ち上げたら右上のメニュー(3本線)をクリックします。
すると上記のような画面があらわれます。「読み込み」をクリックして、あらわれた画面にコピーしたコードを貼り付ければ完了です。
MQTTとは
MQTTとは、多数のデバイスの間で短いメッセージを頻繁に送受信することを想定した通信プロトコルです。つまりインフラのようなもの、もしくはメッセージを送受信するので土管のようなものをイメージすると良いかと思います。
MQTTは先ほど説明したような仕組みの名前です。なので、実際にメッセージを送受信するためにはMQTTブローカーが必要です。イメージは下記の画像のようになります。
MQTTブローカーを用意するには、いくつか方法があります(こちらのサイトにまとめられています)。今回、ハンズオンでつかったの以下の2パターンです。
- ローカルホストでMQTTブローカーをたてる
- Moscaを利用する -> MQTT brokerのためのライブラリ(IBMクラウドのような外部サービスでNode-REDを使う場合、うまくいきませんでした)
- mosquittoのテストサーバーを利用する
- これから記載するプログラムはこちらを使っています。
Node-REDのフローのコード
下記のNode-REDのフローのコードを、Node-REDにコピーして使いましょう。
[
{
"id": "7878093f.d68778",
"type": "debug",
"z": "782bda22.769d84",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"x": 522.0130081176758,
"y": 161.99999809265137,
"wires": []
},
{
"id": "22ca4d97.8e3382",
"type": "inject",
"z": "782bda22.769d84",
"name": "",
"topic": "",
"payload": "{\"motor\":1}",
"payloadType": "json",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 220.01303100585938,
"y": 244.00000190734863,
"wires": [
[
"6b18ace.785b854"
]
]
},
{
"id": "6b18ace.785b854",
"type": "mqtt out",
"z": "782bda22.769d84",
"name": "/sub/NefryBT/SAMPLE",
"topic": "/sub/NefryBT/SAMPLE",
"qos": "",
"retain": "",
"broker": "3890dddf.26f532",
"x": 508.0130386352539,
"y": 293.9999957084656,
"wires": []
},
{
"id": "e0581ff4.bce8a",
"type": "inject",
"z": "782bda22.769d84",
"name": "",
"topic": "",
"payload": "{\"motor\":0}",
"payloadType": "json",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 220.01303100585938,
"y": 344.00000190734863,
"wires": [
[
"6b18ace.785b854"
]
]
},
{
"id": "3dccf617.5970da",
"type": "mqtt in",
"z": "782bda22.769d84",
"name": "",
"topic": "/sub/NefryBT/SAMPLE",
"qos": "2",
"broker": "cd2e6f9.ab8069",
"x": 235.09634399414062,
"y": 159.3697967529297,
"wires": [
[
"7878093f.d68778"
]
]
},
{
"id": "f5c8b0fb.93d8",
"type": "comment",
"z": "782bda22.769d84",
"name": "Node-RED上に文字列表示",
"info": "",
"x": 237.09635162353516,
"y": 114.08594131469727,
"wires": []
},
{
"id": "47872155.bfb16",
"type": "comment",
"z": "782bda22.769d84",
"name": "MQTTブローカー側に文字列を送信",
"info": "",
"x": 265.0130310058594,
"y": 206.0104217529297,
"wires": []
},
{
"id": "d5a5e79d.ec27a8",
"type": "comment",
"z": "782bda22.769d84",
"name": "MQTTブローカー側に文字列を送信するためのフロー",
"info": "",
"x": 609.0130004882812,
"y": 257.0104160308838,
"wires": []
},
{
"id": "3700aeb9.5a7ec2",
"type": "comment",
"z": "782bda22.769d84",
"name": "mosquittoのテストサーバーを利用",
"info": "",
"x": 265.0130310058594,
"y": 52.010416984558105,
"wires": []
},
{
"id": "3890dddf.26f532",
"type": "mqtt-broker",
"z": "",
"name": "",
"broker": "http://test.mosquitto.org/",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": true,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
},
{
"id": "cd2e6f9.ab8069",
"type": "mqtt-broker",
"z": "",
"name": "",
"broker": "http://test.mosquitto.org/",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": true,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closeQos": "0",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
}
]
Nefry BTのプログラム
下記がNefry BT側のプログラムになります。
//Nefryがwifiにつながっているか確認、2.4Ghzにつなごう
//できたtest.mosquitto.orgで使える!
#include <Nefry.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include"esp32-hal-ledc.h"
#define URL "mosquitto.org"
#define PWM_BITWIDTH 16
int deg2pw(int deg, int bit){
double ms = ((double) deg - 90.0) * 0.95 / 90.0 + 1.45;
return (int) (ms / 20.0 * pow(2, bit));
}
IPAddress endpoint;
const int port = 1883;
const char *pubTopic;
const char *subTopic;
const char *deviceName;
const char *mqtt_server = "test.mosquitto.org";
WiFiClient httpsClient;
PubSubClient mqttClient(httpsClient);
void setup() {
ledcSetup(0,50,PWM_BITWIDTH);
ledcAttachPin(A2,0);
//// NefryBT設定画面まわり ////////////////////////////////////////
// NefryBT
pubTopic = "/pub/NefryBT/SAMPLE";
subTopic = "/sub/NefryBT/SAMPLE";
deviceName = "NefryBT_SAMPLE"; // 複数台で利用する場合は必ずかぶらないように変更する
// ローカルホストでMQTTブローカーを立てている場合は、パソコンのIPAddress型に収納。配列っぽく入れる。ここはその都度確認すること
//endpoint指定部
endpoint[0] = 192;
endpoint[1] = 168;
endpoint[2] = 43;
endpoint[3] = 105;
//// 以下通常処理 ////////////////////////////////////////
Serial.begin(115200);
//mqttClient.setServer()関数でMQTTブローカーを指定する
mqttClient.setServer(mqtt_server, port);
mqttClient.setCallback(mqttCallback);
connectMQTT();
}
//MQTTがちゃんと動いているか、つながっているかを判断
void connectMQTT() {
Serial.println("connectMQTT");
Serial.println(deviceName);
while (!mqttClient.connected()) {
Serial.print(".");
if (mqttClient.connect(deviceName)) {
Serial.println("Connected.");
int qos = 0;
mqttClient.subscribe(subTopic, qos);
Serial.println("Subscribed.");
} else {
Serial.print("Failed. Error state=");
Serial.print(mqttClient.state());
// Wait 5 seconds before retrying
delay(5000);
}
}
}
///////////////////////////////////////////////////////
char pubMessage[128];
void mqttCallback (char* topic, byte* payload, unsigned int length) {
String str = "";
Serial.print("Received. topic=");
Serial.println(topic);
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
str += (char)payload[i];
}
Serial.print("\n");
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(str);
// 読み取った文字列をパース
if (!root.success()) {
Serial.println("parseObject() failed");
return;
}
//Node-REDから文字列を読み取ってくる部分//////
const char* message = root["message"];
int motor = root["motor"];
////////////////////////////////////////////
Serial.print("motor = ");
Serial.println(motor);
///サーボモータを動かす処理//////////////////
if( motor == 1 ){
for(int i=90; i<=115; i++){
ledcWrite(0,deg2pw(i, PWM_BITWIDTH));
}
delay(800);
ledcWrite(0,deg2pw(90, PWM_BITWIDTH));
} else {
for(int i=90; i>=65; --i){//0~180°の位置で考える、90度が基準点としてそこからどう動くかを考える
ledcWrite(0,deg2pw(i, PWM_BITWIDTH));
}
delay(800);
ledcWrite(0,deg2pw(90, PWM_BITWIDTH));
}
Nefry.ndelay(1000);
}
//////////////////////////////////////////////
//mqttを動かしている部分///////////////////////
void mqttLoop() {
if (!mqttClient.connected()) {
connectMQTT();
}
mqttClient.loop();
}
void loop() {
mqttLoop();
}
//////////////////////////////////////////////
動作させてみると……
このように、PC上から遠隔でサーボモータを制御できるようになります。
Node-RED×MQTTでサーボモータをUI(ブラウザ)から制御
先ほどのプログラムはNode-RED上のボタンを押すとある角度回転するというものでした。次はそれを発展させて、ブラウザから操作して角度を遠隔で制御してみます。
ハンズオンでは、製作途中にタイムアップしてしまいました。少し難易度高めです。
Node-REDのフローのコード
下記のNode-REDのフローのコードをNode-REDにコピーして使いましょう。
[
{
"id": "f2dc1b0b.520f98",
"type": "mqtt out",
"z": "fab71764.66ef78",
"name": "",
"topic": "servo/pan",
"qos": "1",
"retain": "false",
"broker": "954b988c.ec1e08",
"x": 520,
"y": 400,
"wires": []
},
{
"id": "954b988c.ec1e08",
"type": "mqtt-broker",
"z": "",
"name": "",
"broker": "https://test.mosquitto.org/",
"port": "1883",
"clientid": "",
"usetls": false,
"compatmode": true,
"keepalive": "60",
"cleansession": true,
"birthTopic": "",
"birthQos": "0",
"birthPayload": "",
"closeTopic": "",
"closePayload": "",
"willTopic": "",
"willQos": "0",
"willPayload": ""
}
]
Nefry BTのプログラム
#include <Nefry.h>
#include <WiFiClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
//esp32でPWMを使えるようにするためのライブラリー//
#include"esp32-hal-ledc.h"
//////////////////////////////////////////////
//分解能//////////////////////////////////////
#define PWM_BITWIDTH 16
//////////////////////////////////////////////
//PWM制御:周波数->角度に変換(プログラムするときにわかりやすい)//
int deg2pw(int deg, int bit){
double ms = ((double) deg - 90.0) * 0.95 / 90.0 + 1.45;
return (int) (ms / 20.0 * pow(2, bit));
}
////////////////////////////////////////////////////////////
IPAddress endpoint;
const int port = 1883;
const char *pubTopic;
const char *subTopic;
const char *deviceName;
const char *mqtt_server = "test.mosquitto.org";
WiFiClient httpsClient;
PubSubClient mqttClient(httpsClient);
void setup() {
ledcSetup(0,50,PWM_BITWIDTH);
ledcAttachPin(A1,0);
//// NefryBT設定 ////////////////////////////////////////
// NefryBT
pubTopic = "/pub/NefryBT/SAMPLE";
subTopic = "/sub/NefryBT/SAMPLE";
deviceName = "NefryBT_SAMPLE"; // 複数台で利用する場合は必ずかぶらないように変更する
// ローカルホストならパソコンのIPAddress型に収納。配列っぽく入れる。ここはその都度確認すること
//mosquittoのテストサーバーを使うときは下記のednpointは気にしなくて大丈夫です
endpoint[0] = 192;
endpoint[1] = 168;
endpoint[2] = 1;
endpoint[3] = 1;
////////////////////////////////////////////
Serial.begin(115200);
mqttClient.setServer(mqtt_server, port);
mqttClient.setCallback(mqttCallback);
connectMQTT();
}
void connectMQTT() {
Serial.println("connectMQTT");
Serial.println(deviceName);
while (!mqttClient.connected()) {
Serial.print(".");
if (mqttClient.connect(deviceName)) {
Serial.println("Connected.");
int qos = 0;
mqttClient.subscribe(subTopic, qos);
Serial.println("Subscribed.");
} else {
Serial.print("Failed. Error state=");
Serial.print(mqttClient.state());
// Wait 5 seconds before retrying
delay(5000);
}
}
}
char pubMessage[128];
void mqttCallback (char* topic, byte* payload, unsigned int length) {
String str = "";
Serial.print("Received. topic=");
Serial.println(topic);
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
str += (char)payload[i];
}
Serial.print("\n");
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(str);
// パースが成功かどうか判断
if (!root.success()) {
Serial.println("parseObject() failed");
return;
}
const char* message = root["message"];
int takudooon = root["takudooon"];
Serial.print("takudooon = ");
Serial.println(takudooon);
if( takudooon == 1 ){
for(int i=90; i<=115; i++){
ledcWrite(0,deg2pw(i, PWM_BITWIDTH));
}
delay(800);
ledcWrite(0,deg2pw(90, PWM_BITWIDTH));
} else {
for(int i=90; i>=65; --i){//0~180°の位置で考える、90度が基準点としてそこからどう動くかを考える
ledcWrite(0,deg2pw(i, PWM_BITWIDTH));
}
delay(800);
ledcWrite(0,deg2pw(90, PWM_BITWIDTH));
}
Nefry.ndelay(1000);
}
void mqttLoop() {
if (!mqttClient.connected()) {
connectMQTT();
}
mqttClient.loop();
}
void loop() {
mqttLoop();
}
操作画面を用意
それぞれのプログラムが準備できたら、Node-REDの画面から、
URLの「http://localhost:1880
」を「http://localhost:1880/ui
」と記述すると以下のような操作画面があらわれます。ここから、直感的にサーボモータの角度を遠隔制御できるようになります。
実際に動かすと……
このようになります。
おわりに
いろいろな技術を知ることでモノづくりの幅が増え、作ってみたいものを自由に作れるようになってもらえればな、と思います。
では!