ArduinoでIoTにチャレンジ<その4>日付時刻を表示する
時刻はいろいろ場面で必要となる基本データです。Wi-Fiルータは定期的にNTPサーバにアクセスし、修正して正確な時刻を保っています。またWiFiNINAライブラリには、WiFi.getTime()と呼ばれるWi-Fiルータから1970年1月1日の午前0時からの秒数で示す現在の時刻を取り出す関数が用意されています。基準日からの経過秒数なので、利用時にはわかりやすい表示に変更します。
●RTCzeroライブラリ
Arduino MRK WiFi 1010に搭載されているマイクロコントローラSAM21には、RTC(リアルタイム・カウンタ)が搭載されています。このRTCを利用して、リアルタイム・クロック実現するライブラリRTCZeroも用意されています。
これらを利用して、前回作成したWebブラウザに表示される画面に、日時の表示を追加します。
●日時の表示のための手順
具体的に日時を読める形で表示する手順を考えます。前提として、WiFiNINAライブラリは利用できる状態になっているものとします。
① RTCZeroライブラリを利用するためにプログラムの最初に次の記述をする
#include <RTCZero.h>
② RTCZeroのインスタンスを作成する。作成されたオブジェクトのメソッドで必要な処理を行う。次の命令でリアルタイム・クロックの処理のためのオブジェクトrtcを作る
RTCZero rtc;
③ リアルタイムを次の命令で起動する
rtc.begin();
④ リアルタイム・クロックの時刻合わせのため、WiFiNINAライブラリのWiFi.getTime()メソッドで現在の時刻をunsigned long epoch;で定義されたepochにセットし、次のrtcのメソッドでリアルタイム・クロックの時刻合わせを行う
rtc.setEpoch(epoch);
⑤ 今までの処理で、リアルタイム・クロックから正確な日時を得ることができます。得られるデータはバイナリの整数データです。
rtc.getYear() 年が得られる
rtc.getMonth() 月が得られる(整数)
rtc.getDay() 日が得られる
rtc.getHours() + 9 時が得られる時差の補正が必要
rtc.getMinutes() 分が得られる
rtc.getSeconds()); 秒が得られる
日時のバイナリ・データを読める形の文字列する必要があります。一つの例として、
yy/mm/dd/ HH:MM:SS
を表示するため、sprintf()関数を使用します。
●sprintf()関数
sprintf関数の書式は次のようになります。
int sprintf(char文字列、書式、可変数の引数)
文字列:書式に従い、すべての引数を文字列に変換し、この文字列に格納する。格納する文字列は変換された文字列を格納するに十分な大きさとする。
書式 :可変数の引数に対応する変換指定子と見出しや区分けの文字列で構成された書式
可変数の引数 : 文字列に組み込む数値、文字などの引数、変換指定子と引数は対応し同じ数
【例】上記の日時の表示を行う
char tary[40];
sprintf( tary,
"%2d/%2d/%2d %2d:%2d:%2d",
rtc.getYear(),rtc.getManth(),rtc.getDay(),
rtc.getHours() + 9, rtc.getMinutes(), rtc.getSeconds());
taryは、変換された文字列を格納する文字列(文字の配列)、%2d の%は変換指定子であることを示しています。2は表示桁数を示します。dは変換文字で符号付き10進数の形式で表示します。
%から変換文字までが変換指定子です。それ以外は空白も含めて文字列として認識され、文字列(tray)に格納されます。/、日付と時刻の間の空白、:それぞれ区切りとして文字列に変換された結果と共に格納されます。
●LEDの表示色を変える
LEDの点灯のON/OFFに応じて、LEDの文字を赤または青に変更する機能を追加します。点灯するhereの文字をクリックするとLEDが点灯し、併せて両方のLEDの文字が赤くなるようにします。OFFのhereをクリックするとLEDが消灯し、両方のLEDの文字が青くなります。
次に示すのが、点灯しているときの状態です。LEDの点滅の状態が画面で確認できるようになります。
LEDが消灯しているときは、次の画面になります。
●LEDの表示を変える方法
「Click here to turn the LED on pin 9 on」 でhereの文字をクリックしたときにLEDをONになります。表示はこのようですが、実際にWebブラウザに渡されるこの部分は、LEDの文字が赤で表示されているときは次の記述になります。
<p>Click <a href="/H">here</a> to turn<span style="color:red">the LED</span>on pin 9 on.</p>
<p> から</p>は文のかたまりを示す段落で、p要素と呼びます。Clickは表示される文字です。
次の<a here=“/H”>here</a>は、表示されているhereの文字をクリックしたときのリンク先を示すa要素です。この場合は、同じページの/Hを呼び出します。”/H“はリンク先の指示となります。このリンク先に基づく処理も、このプログラムの中で行っています。次のto turn はそのまま表示される文字です。
次の<span style="color:red">the LED</span>は、the LEDの発色を変えるためのものです。<span> </span>タグは、汎用的な範囲を示すspan要素でCSSのスタイルなどを指定するときなどに多く利用します。
styleは、HTML文書の中にCSSのスタイルシートの情報を組み込むための要素です。colorはテキストの色を指定します。ここではredと指定されていますが、マイコンから書き込むときに文字列変数の値が書き込まれます。変数はその時に応じてredまたはblueが指定されています。
●a要素
hereをクリックするとLEDを点灯するためにはa要素を用い<a href="/H">here</a>を記述し、消灯するときは<a href="/L">here</a>と記述します。
<aの後のhref="/H"で同じフォルダの/Hのページにリンクされます。IPアドレスに/Hが付加されたメッセージが送られてきたときにはLEDをONにするようにポート9にHIGHを出力します。消灯のときはクライアントからIPアドレスに/Lが付加されたメッセージが送られ、ポートにLOWを出力します。
●/H、/Lのメッセージの確認方法
クライアントから送られたLEDの点灯の/H、消灯の/Lの確認はクライアントからのデータを受信して蓄えている文字列オブジェクトの最後の文字列が“GET /H”か”GET /L”かをチェックします。次に示すようにif文でチェックし一致したら該当する処理を追加します。
次の例は、LEDをONにして、表示色をCOLOR1に格納されている赤redに設定しています。
if (currentLine.endsWith("GET /H")) {
digitalWrite(9, HIGH); // GET /H turns the LED on
cr1=COLOR1;
cr2=COLOR1;
}
/MH、/MLなどと追加して、対応する処理を容易に増やすことができます。
●作成したプログラム
全体のプログラム以下に示します。
最初の行は/*から*/で囲まれた範囲のコメントです。プログラム名が記入されています。
次の#include文で、今回利用する事前に組み込まれていないライブラリを読み込むためのヘッダ・ファイルを指定しています。SPI.hとWiFiNINA.hはWi-Fi処理のため、RTCZero.hは時刻表示のため、arduino_secrets.hはWi-Fiに接続するためにSSID、キーワードを読み込むためです。
/* MKRWiFi010021.ins */
#include <SPI.h>
#include <WiFiNINA.h>
#include <RTCZero.h>
#include "arduino_secrets.h"
次は、プログラムの中で全体で利用する変数、定数の定義とオブジェクトの作成を行っています。
クライアントのPCとの間で、WEbブラウザを用いて通信を行うサーバのWiFiServerのインスタンスとしてserver(80)、時刻表示処理のためのrtcのインスタンスの作成を行っています。
char ssid[] = SECRET_SSID; // your network SSID
char pass[] = SECRET_PASS; // your network password
int status = WL_IDLE_STATUS;
WiFiServer server(80);
RTCZero rtc
char tary[40];
String C
OLOR1 = "red";
String COLOR2 = "blue";
String cr1 = COLOR1;
String cr2 = COLOR2;
;
初期化処理を行うsetup()関数の中では、
①シリアル通信の初期化
②WiFiモジュールの有無のチェック
③WiFiモジュールのバージョン・チェック
④WiFiへの接続
⑤LED制御のポートの出力設定、サーバの起動
⑥時刻合わせ
を行っています。
void setup() {
最初にシリアル通信の初期化を行い、必要な情報をPCでモニタできるようにします。このシリアル通信は、PC側で受信しなくてもプログラムの進行に影響を与えません。
Serial.begin(9600);
while (!Serial) {
; // シリアル通信ができるようになるのを待つ
}
Wi-Fi通信モジュールがボードにあるかチェックしています。なければプログラムがここで止まります。止まっていることは、シリアルモニタでCommunication with WiFi module failedの表示を確認するとわかります。
if (WiFi.status() == WL_NO_MODULE) {
Serial.println("Communication with WiFi module failed!");
while (true); // don't continue
}
WiFiのファームウェアのバージョン・チェックを行います。より新しいバージョンがある場合は、アップデートを促す警告が表示されますが、プログラムは進行します。
String fv = WiFi.firmwareVersion();
if (fv < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println("Please upgrade the firmware");
}
ここで、Wi-Fiへの接続処理を行います。
while (status != WL_CONNECTED) {
Serial.print("Attempting to connect to SSID: ");
Serial.println(ssid);
// Connect to WPA/WPA2 network. Change this line if using open or WEP network:
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
LED表示のためディジタル・ポート9を出力に指定し、サーバを起動します。Wi-Fiの接続状態をシリアルモニタに表示しています。リアルタイム・クロックにrtcを起動し、Wi-Fiのルータから現在の時刻を読み取り、リアルタイム・クロックの時刻の補正を行って初期化処理を終えます。
pinMode(9, OUTPUT);
server.begin();
// you're connected now, so print out the status:
printWifiStatus();
rtc.begin(); //+
unsigned long epoch;
int numberOfTries = 0, maxTries = 6;
do {
epoch = WiFi.getTime();
numberOfTries++;
}
while ((epoch == 0) && (numberOfTries < maxTries));
Serial.println(epoch);
Serial.println(numberOfTries);
if (numberOfTries == maxTries) {
Serial.print("NTP unreachable!!");
while (1);
}
else {
Serial.print("Epoch received: ");
rtc.setEpoch(epoch);
Serial.println();
}
}
loop関数の先頭でクライアントからサーバに接続され、受信データのあるクライアントを取得し作成します。新しいクライアントがある場合、シリアルモニタにその旨送信し、受信データのバッファcurrentLineをクリアします。その後そのクライアントが接続している間 以下の処理を続けます。
読み取る文字がある場合、クライアントから1文字読み取って変数cにセットします。読み取った文字をSerial.write(c);でシリアルモニタに送信します。シリアルモニタではWebブラウザからの応答を確認できます。
WiFiClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
Serial.println("new client"); // print a message out the serial port
String currentLine = ""; // make a String to hold incoming data from the client
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write(c); // print it out the serial monitor
クライアントとサーバが接続され、読み取り可能なデータがあるクライアントを取得します。
受信文字が\nの場合は、currentLineに格納されている文字数をチェックします。0なら新しい行ということで、クライアントに応答文を送信します。
if (c == '\n') { // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
送信する前に、現在の時刻を読み取り送信できるように文字列に変換します。
文字列の変換は次のsprintf()関数で、時刻を取り出し見出しを付け、書式を整え文字列taryにセットします。この文字列は、後ほどclient.println(tary)でWebブラウザに書き出します。
sprintf(tary, "%2d/%2d/%2d %2d:%2d:%2d", rtc.getYear(), rtc.getMonth(), rtc.getDay(), \
rtc.getHours() + 9, rtc.getMinutes(), rtc.getSeconds());
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println();
// the content of the HTTP response follows the header:
client.print("<!DOCTYPE html> \n<html lang=\"ja\">");
次に、<head> </head>の指定を行いhead部を追加します。head部では文字セットをutf-8として漢字も利用できるようにします。
<meta http-equiv='refresh' content='30'/>の記述で30秒ごとに画面の更新が行われるようになります。30を任意の値に変更すると設定された値の秒数の間隔で更新が行われます。<title> ~</title>で設定されるタイトルは、表示はされませんが、ページのタイトルとして検索の参照対象となります。
client.print("<head>");
client.print("<metacharset=\"utf-8\"/>");
client.print("<meta name=\"viewport\"content=\"width=device-width,initial-scale=1\">");
client.print("<meta http-equiv='refresh' content='30'/>");
client.print("<title>ArduinoMKRWiFi1010 021</title>");
client.println(“</head>”);
時刻の文字を左寄せにするためにstyle="text-align:left"を<div>の中に追加します。これらのメッセージは、client.print()関数に“ ”で囲った文字列として定義します。“ ”のなかの“は¥”と記述します。
client.print("<div style=\"text-align:left\">")
client.println(tary);
client.print("</div>");
見出しおよびそれ以後の表示は、中央ぞろえになるようにstyle="text-align:center"を指定しています。
client.print("<body style=\"text-align:center\">");
client.print("<h1>Arduino MKR WiFi 1010 </h1>");
client.print("<p>Click <a href=\"/H\">here</a> to turn ");
client.print("<span style=\"color:");
client.print(cr1 + "\">the LED </span>on pin 9 on.</p>");
client.print("<p>Click <a href=\"/L\">here</a> to turn <span style=\"color:");
client.print(cr2 + "\">the LED </span> on pin 9 off.</p>");
client.print("</body>");
break;
} else { // if you got a newline, then clear currentLine:
currentLine = "";
}
} else if (c != '\r') { // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
クライアントからのメッセージは、ここで1文字ずつcurrentLineに追加されます。
ここからは、クライアントからのメッセージに対応する処理です。
// Check to see if the client request was "GET /H" or "GET /L":
if (currentLine.endsWith("GET /H")) {
digitalWrite(9, HIGH); // GET /H turns the LED on
cr1=COLOR1;
cr2=COLOR1;
}
if (currentLine.endsWith("GET /L")) {
digitalWrite(9, LOW); // GET /L turns the LED off
cr2=COLOR2;
cr1=COLOR2;
}
}
}
// close the connection:
client.stop();
Serial.println("client disconnected");
}
}
void printWifiStatus() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print your board's IP address:
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.print(rssi);
Serial.println(" dBm");
}
●クライアントからのデータ
クライアントからのメッセージをシリアルモニタで確認できます。setup()関数でのWi-Fiへの接続の様子、ここでWi-Fiに接続できIPアドレスが192.168.185と確認できます。
<シリアルモニタの表示例>
Attempting to connect to SSID: Buffalo-G-2C6E
SSID: Buffalo-G-2C6E
IP Address: 192.168.1.85
signal strength (RSSI):-48 dBm
1635028078
1
Epoch received:
現在時刻が読み取られ、時刻合わせが終わりsetup()関数の処理が終わっています。
ここからloop()関数からの出力となります。ONのhereをクリックしたのでGET /Hとなっています。
new client
GET /H HTTP/1.1
Host: 192.168.1.85
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.1.85/H
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
client disconnected
new client
GET /favicon.ico HTTP/1.1
Host: 192.168.1.85
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Mobile Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.1.85/H
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
client disconnected
OFFのhereをクリックすると、次に示すようにGET /Lが送信されてきています。
new client
GET /L HTTP/1.1
Host: 192.168.1.85
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Mobile Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.1.85/H
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
client disconnected
new client
GET /favicon.ico HTTP/1.1
Host: 192.168.1.85
Connection: keep-alive
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Mobile Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.1.85/L
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
2番目に送られてくる GET /favicon.ico HTTP/1.1のメッセージは、Webブラウザがアイコンのデータをサーバに探しに来ているもののようです。詳しくはまだ把握していません。
何とかブラウザに時刻やメッセージを表示できました。今後、いろいろ試してみます。
(2021/10/29)
<神崎康宏>