編集履歴一覧に戻る
karakirimuのアイコン画像

karakirimu が 2021年02月14日23時05分45秒 に編集

目次削除

記事種類の変更

+

製作品

本文の変更

-

# 目次 [1. はじめに](#1はじめに) [2. システムの概要](#2システムの概要) [3. 回路](#3回路) [4. プログラム](#4プログラム) [5. 実験](#5実験) [6. おわりに](#6おわりに)

# はじめに ESP32を利用して赤外線送信をローカルのWebサーバーからできるようにしてみました。元々Raspberry Pi 3bにて個人的に製作し、1年ほど運用しておりましたが、3bでこの機能を継続することは役不足と感じ、別のマイコンを利用しての再構築を考えていました。また、冬になり暖房で肌が乾燥してきたため、追加で部屋の湿度を知りたいと思っていました。そこで、温湿度計(DHT11)を追加してみました。 ![完成品](https://camo.elchika.com/c02b9c1e035c52b10d774d72eebb7afa9404f7e7/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f35663737626663362d363639642d343031652d383164612d313739653832653335653536/) ![3Dデータ](https://camo.elchika.com/a1010630ae69856c1f25c731e923b82ad414ca92/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f38396135383731372d346235332d346532622d383336322d306533306637613466343830/) # 概要 大まかな構成はこんな感じなります。ユーザーはブラウザからIRコードの送信や温湿度計の情報の受信を行います。ESP32 DevKit CはWiFiへ接続してサーバーIPの取得、SDカードからの各種情報の読み取り、温湿度計の情報の取得、ブラウザのボタンに対応する赤外線コードの取得、送信を行います。IRコードの追加、削除やSSIDの変更など、変更頻度の高いデータをすべてSDカードに集約しています。 :::plantuml:構成 @startuml title このデバイスと外部との関係 rectangle PCやスマートフォンのブラウザ as cl rectangle ルーター as ro rectangle このデバイス { rectangle ESP32_DevKitC as d rectangle SDカード as sd rectangle 温湿度計 as temp rectangle 赤外線LED as ir sd -up->d: ページ、赤外線コード、\nSSIDとパスワードを取得 d.>ir: 赤外線コード送信 temp->d: 温度・湿度取得 } cl -left->d: 赤外線選択データ送信\n温湿度計更新要求 cl<..d: 要求への応答 d -up.>ro: IPアドレス取得 @enduml ::: # 回路 以下の材料を使用しました。 - ESP32 DevKit C x1 - DHT11 x1 - ユニバーサル基板 x1 - LED取付基板 x1 - 1K抵抗 x1 - 10K抵抗 x1 - 33K抵抗 x4 - 33Ω抵抗 x7 - 940nm赤外線LED(OSI5LA5A33A-B) x7 - microSDモジュール x1 - 2SD2012 (トランジスタ) x1 図のように結線します。センサとSDは3.3V、LEDは5Vへ接続します。 ![回路図](https://camo.elchika.com/336d406f0a4885513118aa843f4e1686bc547d41/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f65656466363133612d613665362d343864622d396236642d346136656561623738363639/) ![完成した基板(表)](https://camo.elchika.com/4c08e48e79e95b515d14f59afdc08268ed6b0098/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f62653434393430332d623939362d346662322d613261612d656163356535343663353463/) ![完成した基板(裏)](https://camo.elchika.com/5322b1d45d70ff14790d9017fa75c82c1d9b712f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f62313938323832622d613966642d343732312d613033642d343037373735333930373832/) ## 温湿度計 回路図左上のDHT11です。IOポートは電源電圧と同じ3.3Vでプルアップします。 ## microSDモジュール SPIで接続します。信号線(CS,CLK,MOSI,MISO)をすべて3.3Vでプルアップします。 ## 赤外線LED 赤外線LEDの駆動回路です。赤外線LEDは940nmのものを使い、33Ωの抵抗を並列に配線しています。 # プログラム プログラムはArduino IDEで書き込むものと、SDカードに配置するhtml,js,cssの2つあります。ESP32とクライアント(ブラウザ)との主な送受信処理はこんな感じになります。 :::plantuml:温湿度計 @startuml title 送受信処理の流れ クライアント -> ESP32: ブラウザを開く ESP32 --> クライアント: ESP32: ブラウザの要求データをロード クライアント -> ESP32: 10秒ごとにESP32にデータ送信要求 クライアント <-- ESP32: クライアントの要求に応じてセンサーデータを返却 クライアント -> クライアント: 温度表示UIを更新 @enduml ::: :::plantuml:赤外線送信 @startuml title 送受信処理の流れ クライアント -> ESP32: ブラウザ上の送信ボタンを押す ESP32 --> クライアント: コードを受信 ESP32 -> 赤外線LED: デバイス名と同名のjsonにキーが存在する場合、送信 @enduml ::: ## Arduino IDEのコード ### 使用するライブラリ プログラムは以下のライブラリを使っています。最初にインストールが必要です。 - Arduino Json 6.17.2 - DHT sensor library for ESPx 1.17.0 - SDFat 2.0.4 - IRremoteESP8266 2.7.14 ### コード Arduino のコードは少し長めなので、説明はコード内に記述しました。 ```arduino:ESP32に書き込むコード #include <WiFi.h> #include <ArduinoJson.h> #include <SdFat.h> #include <IRsend.h> #include "DHTesp.h" // 4KBキャッシュ const uint16_t SD_LOAD_BUFFERSIZE = 4096; // 赤外線LEDの出力ピン const uint8_t GPIO_IR_OUT_PIN = 4; // 温湿度計の入力ピン const uint8_t GPIO_DHT11_PIN = 22; // SDのSPI選択ピン const uint8_t CHIP_SELECT_SD = 5; // 赤外線データフォルダ const char* IR_JSON_DIR = "data/"; // サーバーの応答タイムアウト時間(ms) const long timeoutTime = 5000; // SDFatライブラリ SdFat SD; // IRremote (送信) IRsend irsend(GPIO_IR_OUT_PIN); // DHTXXの温湿度計センサライブラリ DHTesp dht; // ウェブサーバーをポート80に設定 WiFiServer server(80); // 現在の起動経過時間 static uint32_t currentTime = millis(); // 1つ前の起動経過時間 static uint32_t previousTime = 0; /** * ブラウザへの応答タイムアウト判定 */ bool timeout(){ previousTime = currentTime; return (timeoutTime < (currentTime - previousTime)); } /** * 温湿度計センサの値を取得 */ String getSensorValue(){ TempAndHumidity values = dht.getTempAndHumidity(); // 失敗した場合、空の文字列を返却 if (dht.getStatus() != 0) { Serial.println("DHT11 error status: " + String(dht.getStatusString())); return ""; } Serial.println(" T:" + String(values.temperature) + " H:" + String(values.humidity)); // {"temperature":数値,"humidity":数値} を返す return ("{\"temperature\":" + String(values.temperature) + ", \"humidity\":" + String(values.humidity) + "}"); } /** * 取得したURLに応じたファイルを読み込み、ブラウザへ * 応答を返す。 */ void loadUrl(String url, WiFiClient *client){ // SDからの読み込みバッファを確保 char readBuffer[SD_LOAD_BUFFERSIZE] = {0}; unsigned short counter = 0; File file = SD.open(url, FILE_READ); if (!file){ // 失敗時は下の文字列がブラウザに表示される。 client->println("<p>File could not load<p>"); return; } // ファイル読み込み while (file.available()) { readBuffer[counter] = file.read(); counter++; if(counter == SD_LOAD_BUFFERSIZE){ // バッファが最大に達した場合、書き込み (応答を返す) client->write(readBuffer, SD_LOAD_BUFFERSIZE); counter = 0; } } // 最後に読み込んだバッファを書き込み (応答を返す) if(counter > 0) client->write(readBuffer, counter); file.close(); // 送信は最後に空行が必要 client->println(); } /** * ヘッダー内からURLを抽出 */ String parseUrl(String *header){ unsigned int first = header->indexOf("GET "); unsigned int last = header->indexOf(" HTTP/1.1"); if (first >= 0 && last >= 0 && last > first) { return header->substring(first + 4, last); } return ""; } /** * ブラウザのリクエストが赤外線コードの送信要求かどうか判定 * フォーマット: ?sig=<デバイス名>&<機能> */ bool parseIrFunc(String url, String *device, String *func){ unsigned int first = url.indexOf("?sig="); unsigned int second = url.indexOf("&"); if (first >= 0 && second >= 0 && second > first) { *device = url.substring(first + 5, second); *func = url.substring(second + 1); return true; } return false; } /** * ブラウザのリクエストがセンサの更新要求かどうか判定 * フォーマット: ?req=<センサ名>&<時刻> */ bool parseSensorUpdate(String url, String *device){ unsigned int first = url.indexOf("?req="); unsigned int second = url.indexOf("&"); if (first >= 0 && second >= 0 && second > first) { *device = url.substring(first + 5, second); return true; } return false; } /** * ブラウザからの要求に対して応答を返す。(レスポンスヘッダー) */ void sendContentType(String type, WiFiClient *client){ client->println("HTTP/1.1 200 OK"); client->println("Content-type:" + type); client->println("Connection: keep-alive"); client->println(); // 最後に空行が必要 } /** * ヘッダーの1行分のバッファを取得 */ String readLine(WiFiClient *client){ String cline = ""; char ch; while (client->connected()) { ch = client->read(); if (ch == '\n') { break; } else if (ch != '\r') { cline += ch; } } Serial.println("Header : " + cline); return cline; } /** * データの種類を取得 */ String parseDataType(String path) { String dataType = "text/plain"; if (path.endsWith("/")) { path += "index.html"; } if (path.endsWith(".htm") || path.endsWith(".html")) { dataType = "text/html"; } else if (path.endsWith(".css")) { dataType = "text/css"; } else if (path.endsWith(".js")) { dataType = "application/javascript"; } else if (path.endsWith(".ico")) { dataType = "image/x-icon"; } return dataType; } /** * 文字列を16進数に変換 */ uint32_t stringToHex(String num){ String candidate = num; candidate.replace("0x",""); candidate.toUpperCase(); unsigned int len = candidate.length(); unsigned int array_max = len - 1; uint32_t result = 0; char c; for(int i = 0; i < len; i++){ c = candidate.charAt(i); if (c >= '0' && c <= '9') { result |= ((c - '0') << (4 * (array_max - i))); } else if (c >= 'A' && c <= 'F') { result |= (((c - 'A') + 10) << (4 * (array_max - i))); } } Serial.print("[stringToHex] result : 0x"); Serial.println(result, HEX); return result; } /** * WiFiのSSIDとパスワードを取得 */ void getWifiKeys(String &ssid, String &password){ // readline buffer String readBuffer; char ch; // config.jsonを開く File file = SD.open("/config.json", FILE_READ); if (!file){ Serial.println("[Config] file could not load."); return; } while (file.available()) { ch = file.read(); if (ch == '\n') { DynamicJsonDocument doc(readBuffer.length() + 4011); DeserializationError error = deserializeJson(doc, readBuffer); // Test if parsing succeeds. if (error) { Serial.print("size: "); Serial.println(readBuffer.length(), DEC); Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); return; } const char* key = doc["ssid"]; const char *type = doc["password"]; ssid = key; password = type; readBuffer = ""; } else if (ch != '\r') { readBuffer += ch; } } } /** * 赤外線コードを送信 */ void sendIr(String &device, String &func){ // readline buffer String readBuffer; char ch; // 送信周波数を38kHzに設定 const uint8_t IR_SEND_FREQ = 38; // フォルダ名 + デバイス名.jsonを開く File file = SD.open(IR_JSON_DIR + device + ".json", FILE_READ); if (!file){ Serial.println("[SendIr] file could not load."); String out = "file : " + *IR_JSON_DIR + device + ".json"; Serial.println(out); return; } while (file.available()) { ch = file.read(); if (ch == '\n') { // JSONフォーマットのファイルを取得 DynamicJsonDocument doc(readBuffer.length() + 4011); DeserializationError error = deserializeJson(doc, readBuffer); if (error) { Serial.print("size: "); Serial.println(readBuffer.length(), DEC); Serial.print("deserializeJson() failed: "); Serial.println(error.c_str()); return; } // 送信コードのキーを取得 const char* key = doc["key"]; // ブラウザから送信されたキーと同じかどうか判定 if(func.equals(key)){ // キーのタイプ(NEC,生コード)を取得 const char *type = doc["type"]; Serial.print("type : "); Serial.println(type); if(strcmp(type, "raw") == 0){ JsonArray codejson = doc["code"].as<JsonArray>(); std::vector<uint16_t> code; for(JsonVariant v : codejson) { code.emplace_back(v.as<uint16_t>()); Serial.print(v.as<uint16_t>(), DEC); Serial.print(","); } Serial.println(""); // IR送信を実行 irsend.sendRaw(&code[0], codejson.size(), IR_SEND_FREQ); Serial.println("[sendIr] Send raw code"); }else if(strcmp(type, "nec") == 0){ // IR送信を実行 irsend.sendNEC(stringToHex(doc["code"].as<String>())); Serial.println("[sendIr] Send nec code"); } break; } readBuffer = ""; } else if (ch != '\r') { readBuffer += ch; } } } void setup() { Serial.begin(115200); // LEDの初期化 pinMode(GPIO_IR_OUT_PIN, OUTPUT); digitalWrite(GPIO_IR_OUT_PIN, LOW); // 温湿度計の初期化 dht.setup(GPIO_DHT11_PIN, DHTesp::DHT11); // SDカードの初期化 if (!SD.begin(CHIP_SELECT_SD, SD_SCK_MHZ(25))) { Serial.println("SD card initialize failed."); return; } // SDからWiFiのSSIDとパスワードを取得 String ssid, password; getWifiKeys(ssid, password); Serial.println("SSID : " + ssid); // Serial.println("Password : " + password); // WiFi接続を確立 WiFi.begin(ssid.c_str(), password.c_str()); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); // ローカルのIPアドレスの表示 Serial.print("WiFi connected. IP: "); Serial.println(WiFi.localIP()); // サーバー開始 server.begin(); } void loop(){ // クライアントの取得 (取得失敗対策のため、5回リトライ) WiFiClient client; int i = 0; for(; i < 5; i++){ client = server.available(); if(client) break; delay(50); } if(i == 5) return; Serial.println("Connection started."); // レスポンスヘッダーの読み取りに必要なバッファ String header = ""; String currentLine = ""; // 接続中 && タイムアウト時間を超えていない currentTime = millis(); while (client.connected() && timeout() == false) { currentTime = millis(); if (!client.available()) continue; currentLine = readLine(&client); header += (currentLine + '\n'); if (currentLine.length() == 0) { // ヘッダーを最後まで読み込んだ場合 String url = parseUrl(&header); sendContentType(parseDataType(url), &client); // センサ更新要求への応答 String sensor; if(parseSensorUpdate(url, &sensor)){ String response = getSensorValue(); client.println(response.c_str()); Serial.println("Response: " + response); break; } // 赤外線コードの送信 (応答は何でもいい) String device, func; if(parseIrFunc(url, &device, &func)){ Serial.println("Remote Device : " + device); Serial.println("Function : " + func); sendIr(device, func); client.println("OK"); break; } // ページ読み込みへの応答 if (url.endsWith("/")) { url += "index.html"; } loadUrl(url, &client); break; } } // 接続を終了する (ブラウザへの応答完了) client.stop(); Serial.println("Connection closed.\n"); } ``` ## SDカード内に配置するコード SDカードには、UIとなる部分とESP32にデータを送信する部分を作成します。見た目はこんな感じになります。 ![ホーム画面](https://camo.elchika.com/1ce03f5be7c33d2a28b3c5cfe509ca8b9318d884/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f31663933353033332d396430632d343036322d613465642d626338333133363639613332/) ![リモコン操作UI](https://camo.elchika.com/8168364f8139d3b8eea8b1a75a72512eb2efffad/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f36353566666566642d333334332d343166352d626135382d393832356461633035376533/) ### 使用するライブラリ SDの読み込み速度やSPIFFSなどを利用することも想定し、軽量で簡単に十分なUIを作れるPure.cssを採用しました。 ### フォルダ構成 SDカードにはファイルを下のように配置しました。appフォルダがリモコン操作する機器を入れたものになります。cssフォルダはUIの補助、jsフォルダにはリモコン操作の実行処理やメニューバーの実装が含まれています。dataフォルダは赤外線送信のコードが含まれています。今回は、ダミーのコードを作成し、送受信のテストをしてみます。 ルートフォルダ ├ index.html (ホーム画面) ├ config.json (WiFi設定) ├:file_folder:app │ └ :file_folder:dummy │ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;└ index.html (赤外線送信選択UI) ├:file_folder:css │ ├ layouts.css (赤外線送信UIの調整) │ ├ side-menu.css (サイドメニュー) │ └ pure-min.css (Pure.css本体) ├:file_folder:js │ ├ ui.js (サイドメニュー) │ └ signal.js (赤外線送信命令の送信) └:file_folder:data &nbsp;&nbsp;&nbsp;&nbsp;└ DUMMY.json (赤外線送信命令に紐づけられたデータ値リスト) ### ホーム画面 (ルートフォルダのindex.html) script部分のupdateSensorで温湿度計の更新を行っています。具体的には、10秒ごとにこの関数が定期的に実行され、結果を得るとhtml上のtemp,temp_text,humid,humid_textに該当するidの要素が更新されます。 ==updateSensor内で利用されているXMLHTTPRequestは、ESP32のサーバー上では動作します。PC上にあるファイルのhtmlを開いて実行した場合は、CORSというエラーが発生します。== ```html:index.html <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="IR send server for ESP32"> <title>Remote Panel</title> <link rel="stylesheet" href="./css/pure-min.css"> <link rel="stylesheet" href="./css/side-menu.css"> <link rel="stylesheet" href="./css/layouts.css"> <script> window.onload = function (){ updateSensor('dht11'); } function updateSensor(device) { var xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.responseType = 'text'; xhr.onload = function (e) { if (xhr.readyState === 4) { if (xhr.status === 200) { console.log(xhr.responseText); var tempprogress = document.getElementById('temp'); var temp = document.getElementById('temp_text'); var humidprogress = document.getElementById('humid'); var humid = document.getElementById('humid_text'); // 結果を読み取り const parsed = JSON.parse(xhr.responseText); tempprogress.value = Number(parsed.temperature); temp.innerHTML = Number(parsed.temperature).toFixed(1) + ' ℃'; humidprogress.value = Number(parsed.humidity); humid.innerHTML = Number(parsed.humidity).toFixed(1) + ' %'; } else { console.error(xhr.statusText); } } }; xhr.ontimeout = function(e) { xhr.abort() ; } xhr.open('GET', '?req=' + device + '&' + new Date().getTime(), true); xhr.setRequestHeader('Content-type', 'application/json'); xhr.setRequestHeader('Cache-Control', 'no-cache'); xhr.setRequestHeader('X-Content-Type-Options','nosniff'); xhr.send(); } window.addEventListener('load', function () { // 10秒間隔で更新を設定 setInterval(function () { updateSensor('dht11'); }, 10000); }); </script> </head> <body> <div id="layout"> <!-- Menu toggle --> <a href="#menu" id="menuLink" class="menu-link"> <!-- Hamburger icon --> <span></span> </a> <div id="menu"> <div class="pure-menu"> <a class="pure-menu-heading" href="#">HOME</a> <ul class="pure-menu-list"> <!-- ボタン定義 --> <li class="pure-menu-item"> <a href="/app/dummy/" class="pure-menu-link">テスト</a> </li> </ul> </div> </div> <div id="main"> <div class="content"> <h2 class="content-subhead">部屋</h2> <div class="pure-g room"> <!-- disp 1 --> <div class="pure-u-1-2"> <label for="temp">温度</label> <meter style="width: 10vw; max-width: 100px;" id="temp" min="-20" max="80" low="10" high="30" optimum="25" value="20"> </meter> </div> <div class="pure-u-1-2"> <div id="temp_text"> - ℃</div> </div> </div> <div class="pure-g room"> <!-- disp 2 --> <div class="pure-u-1-2"> <label for="humid">湿度</label> <meter style="width: 10vw; max-width: 100px;" id="humid" min="0" max="100" low="33" high="66" optimum="80" value="90.8"> </meter> </div> <div class="pure-u-1-2"> <div id="humid_text"> - %</div> </div> </div> <h2 class="content-subhead">デバイス</h2> <div class="pure-g"> <div class="pure-u-1-2"> <button class="index-button pure-button" onclick="location.href='/app/dummy/';">テスト<br>(Dummy)</button> </div> </div> </div> </div> </div> <script src="js/ui.js"></script> </body> </html> ``` ### Wifi設定 (ルートフォルダのconfig.json) ””の中を置き換えて使用します。 ```json:config.json {"ssid":"SSID","password":"パスワード"} ``` ==jsonを読み込むためには、行の最後に改行が必要です。== ### 赤外線送信選択UI (app/dummy/index.html) ボタンでsendSignalを呼び出すようにして、後述のsignal.jsに定義されたsendSignalBaseを通してESP32にリクエストを送信します。 ```html:app/dummy/index.html <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="IR send server for ESP32"> <title>Remote Panel</title> <link rel="stylesheet" href="../../css/pure-min.css"> <link rel="stylesheet" href="../../css/side-menu.css"> <link rel="stylesheet" href="../../css/layouts.css"> <script src="../../js/signal.js"></script> <script>function sendSignal(key){ sendSignalBase('DUMMY', key); }</script> </head> <body> <div id="layout"> <!-- Menu toggle --> <a href="#menu" id="menuLink" class="menu-link"> <!-- Hamburger icon --> <span></span> </a> <div id="menu"> <div class="pure-menu"> <a class="pure-menu-heading" href="../../index.html">HOME</a> <ul class="pure-menu-list"> <!-- ボタン定義 --> <li class="menu-item-divided pure-menu-item pure-menu-selected"> <a href="#" class="pure-menu-link">テスト</a> </li> </ul> </div> </div> <div id="main"> <div class="content"> <div class="pure-g"> <!-- row 1 --> <div class="pure-u-1-3"> <button class="button-green pure-button" onclick="sendSignal('KEY_1 ')">テスト1</button> </div> <div class="pure-u-1-3"> <button class="button-lightgray pure-button" onclick="sendSignal('KEY_2')">テスト2</button> </div> <div class="pure-u-1-3"> <button class="button-lightgray pure-button" onclick="sendSignal('KEY_3')">テスト3</button> </div> <!-- row 2 --> <div class="pure-u-1-1"> <p style="text-align: right"><br>Dummy</p> </div> </div> </div> </div> </div> <script src="../../js/ui.js"></script> </body> </html> ``` ### UI調整ファイル (css/layouts.css) センサ部分の表示サイズの設定と、ボタンの色設定です。 ```css:layouts.css .room { width: 100%; height: 100%; margin: 0.5em; color: white; font-size: 1rem; border-radius: 4px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); text-align: center; } .index-button { width: 98%; height: 90%; padding-bottom: 0.3em; margin: 1em; border-radius: 4px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); background: rgb(200, 200, 200); /* this is a gray */ } .button-green, .button-lightgray { width: 98%; height: 90%; padding-bottom: 0.3em; margin: 0.5em; color: white; font-size: 0.6rem; border-radius: 4px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); } .button-green { background: rgb(28, 184, 65); /* this is a green */ } .button-lightgray { background: rgb(200, 200, 200); /* this is a light gray */ color: black; } ``` ### サイドメニュー (css/side-menu.css) [https://purecss.io/layouts/side-menu/](https://purecss.io/layouts/side-menu/)の説明にこのファイルへのリンクがあります。このコードをコピーし、以下の部分を改変します。 - bodyのかっこ内にbackground-color: #2c2c2c;を追加 - #menuかっこ内に-webkit-overflow-scrolling: touch;の追加 - #menu .pure-menu-selected, #menu .pure-menu-heading{}の部分を以下に変更 ```css #menu .pure-menu-selected { background: #1f8dd6; } #menu .pure-menu-heading { background: transparent; } ``` - .menu-link span,.menu-link span:before,.menu-link span:after {}部分のpointer-events: none;の削除 ### Pure.css本体 (css/pure-min.css) 公式サイトのリンクを辿って中身をそのままコピーしてきます。 https://purecss.io/ ### サイドバー選択処理 (js/ui.js) https://purecss.io/layouts/side-menu/ の説明にリンクがあるので、本体をコピーしてきます。 ### リモコン送信処理 (js/signal.js) sendSignalBaseを呼び出すと、ESP32側に現在のパス+?sig=デバイス名&送信リモコンキー名が送信されます。 ```javascript:signal.js function sendSignalBase(device, key){ var xhr = new XMLHttpRequest(); xhr.open('GET', '?sig=' + device +'&' + key ); xhr.timeout = 1000 ; xhr.setRequestHeader('Cache-Control', 'no-cache'); xhr.responseType = 'document' ; xhr.ontimeout = function(e) { xhr.abort() ; } xhr.send(); } ``` ### リモコン送信コード (data/DUMMY.json) 送信コードです。今回はNECと生コードで実験します。コードの説明を次に示します。 | キー名 | 値の説明 | | ------ | ------------------------ | | type | 値の種類 (rawかnec) | | key | ブラウザから送られるキー | | code | 赤外線コードの値 | ```json:DUMMY.json {"type":"nec","key":"KEY_1","code":"0x12345678"} {"type":"nec","key":"KEY_2","code":"0x56781234"} {"type":"raw","key":"KEY_3","code":[9000,4500,560,350,560,560,560,560,560,560]} ``` ==jsonを読み込むためには、行の最後に改行が必要です。== # 実験 ArduinoのIRremoteのサンプル、IRrecvDumpをそのまま利用して、Arduino Leonardoで今回作成したデバイスの送信コードを受信してみます。実験結果は下の通りで、NECのコードは一致し、Rawのコードはタイミングが少々違いますが、赤外線リモコンとしては許容範囲であると思います。(受信ICは秋月のSPS-442-1です。) ``` Ready to receive IR signals at pin 11 12345678 Decoded NEC: 12345678 (32 bits) Raw (68): 9050 -4400 650 -500 650 -450 600 -550 650 -1600 650 -500 600 -500 650 -1600 650 -500 550 -550 650 -500 600 -1700 550 -1600 650 -500 600 -1650 600 -500 650 -500 600 -550 600 -1600 600 -550 600 -1650 600 -500 650 -1650 600 -1600 600 -500 650 -500 600 -1650 600 -1700 550 -1650 600 -1650 550 -550 650 -500 600 -500 600 56781234 Decoded NEC: 56781234 (32 bits) Raw (68): 9050 -4400 650 -500 650 -1550 700 -450 650 -1600 650 -450 650 -1600 650 -1600 650 -500 650 -450 650 -1600 650 -1600 650 -1600 650 -1600 600 -500 650 -500 600 -500 650 -500 600 -500 650 -500 600 -1650 600 -500 650 -500 600 -1650 600 -500 650 -500 600 -500 650 -1600 650 -1600 600 -550 600 -1650 600 -500 650 -500 600 9D334F57 Unknown encoding: 9D334F57 (32 bits) Raw (10): 9100 -4400 600 -300 700 -450 650 -450 700 ``` ![実験の様子](https://camo.elchika.com/4eac21056b9295fe60632079f42fbf2b6960ed8e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f35306135653266332d343138362d343364302d393661392d6333643265323438646131392f35346636393865632d356634372d346235652d613163362d653533343032313838653438/) # おわりに - SPIFFSでアップロードする話が少し出てきたと思いますが、開発中だったコードをアップロードして試すと結構速度が遅かったので、SDを採用することになりました。 - ブレッドボードで試すと接触のせいか、時々SDが読めなかったりします。 - たまにレイアウトが崩れます。ブラウザ更新すれば直るのですが、原因はまだわかりません。