ESP32でfreeRTOS まとめ Interface誌でfreeRTOSが特集されたので、、、 [ESP32]
ESP32でfreeRTOS まとめ Interface誌でfreeRTOSが特集されたので、、、一覧にしてみた。
Interfaceのページ
https://interface.cqpub.co.jp/magazine/202104/
過去に書いた「ESP32はRTOSができるらしい、、、」で始まるESP32でfreeRTOSを使う、いや使ってみた内容のまとめと言うか、リンク集。
ただし2018年に作成した記事なので、freeRTOSのバージョンが上がっている事から必ずしも現状と一致していないところが有るはず。なので、以下のリンクのドキュメントを参照する必要性はあると思う。
https://www.freertos.org/Documentation/RTOS_book.html
ESP32で使用しているfreeRTOSのバージョンも現在はver.10.2.0となっているようだ。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/freertos-smp.html
1.はじめに
ESP32はDual Core(SMP)なので、そこにちょっと触れて、タスクの生成手順を解説。
https://hamayan.blog.ss-blog.jp/2018-02-16
2.1の続き
1では1つのタスクの生成を行ったが、今回は複数のタスクの生成を行っている。
https://hamayan.blog.ss-blog.jp/2018-02-17
3.それぞれのタスクが動いているうえで、しかし一つのCore上で動く事のできるタスクは1つしか無い。なのでそこで動くタスクが現在どの様な状態にあるのか?の解説。
RTOSを理解するうえでとても重要な概念となる。
https://hamayan.blog.ss-blog.jp/2018-02-18
4.タスクの生成、停止、終了、削除、優先度と言ったタスク管理機能の解説。
https://hamayan.blog.ss-blog.jp/2018-02-18-1
5.タイムティック、つまり処理の時間単位と、それを使った遅延処理、周期処理について解説。
https://hamayan.blog.ss-blog.jp/2018-02-18-2
6.実行すべきユーザータスクが無い時、何が行われるのか?IDLE状態について解説。
WDTリセットとか省電力のカギかも!
https://hamayan.blog.ss-blog.jp/2018-02-19
7.タスク間通信のデータキューの解説。
タスク間で通信する手順が提供されているのです。この通信方法は複数ありまして、今回はデータキューです。このデータキューを利用すると、タスクの実行制御もできます。
https://hamayan.blog.ss-blog.jp/2018-02-19-1
8.タスク優先度に絡むうっかり話。
メカニズムが判れば面白いと思う!
https://hamayan.blog.ss-blog.jp/2018-02-20
9.WDTリセットの実験。
モヤモヤ、、、
https://hamayan.blog.ss-blog.jp/2018-02-21
10.7のデータキューの続き。
https://hamayan.blog.ss-blog.jp/2018-02-21-1
11.資源に対する排他制御、バイナリーセマフォについて解説。
マルチタスク、いやマルチタスクに限らず割込みも含めて、一つの資源に複数のコンテキストからのアクセスに対する制御を行う。
https://hamayan.blog.ss-blog.jp/2018-02-22
12.11の続き。
13.freeRTOSの多重割込みについて解説。
https://hamayan.blog.ss-blog.jp/2018-02-26
14.11とも関係するが、資源の危険な扱い(クリティカルセクション)に関する解説。
https://hamayan.blog.ss-blog.jp/2018-02-26-1
15.ESP32のdigitalWriteについての実験。
https://hamayan.blog.ss-blog.jp/2018-02-26-2
16.12の続き。カウンティングセマフォについて解説。
https://hamayan.blog.ss-blog.jp/2018-02-26-3
17.16の続き。
https://hamayan.blog.ss-blog.jp/2018-02-28
18.17の続き。
https://hamayan.blog.ss-blog.jp/2018-03-01
19.実は14の続き。
https://hamayan.blog.ss-blog.jp/2018-03-02
20.freeRTOSのクリティカルセクションの扱いについて解説。
割込み禁止、割込み許可。
https://hamayan.blog.ss-blog.jp/2018-03-06
21.20の続き。
ディスパッチ禁止、ディスパッチ許可。
https://hamayan.blog.ss-blog.jp/2018-03-07
22.21の続き。
ミューテックス。
https://hamayan.blog.ss-blog.jp/2018-03-07-1
23.22の続き。
https://hamayan.blog.ss-blog.jp/2018-03-08
24.23の続き。
まだまだ続くよミューテックス。
https://hamayan.blog.ss-blog.jp/2018-03-08-1
25.24の続き。
デッドロック。
https://hamayan.blog.ss-blog.jp/2018-03-08-2
26.Tick割込みからのコールバック。
https://hamayan.blog.ss-blog.jp/2018-03-08-3
27.25の続き。
ゲートキーパータスク!結局解決策はそれかよ、、、良いと思う。
https://hamayan.blog.ss-blog.jp/2018-03-08-4
28.データタイプと命名規則。
freeRTOSのAPIがちょっと読み易くなるかもしれない。
RTOSのAPI名等にやたら長い名前はどうかと思うけれど、今時の開発環境なら問題無いのでしょう。
https://hamayan.blog.ss-blog.jp/2018-03-09
29.28の続き。
https://hamayan.blog.ss-blog.jp/2018-03-09-1
30.タスクの状態参照とスタックサイズ。
https://hamayan.blog.ss-blog.jp/2018-03-09-2
31.メモリ管理。
https://hamayan.blog.ss-blog.jp/2018-03-12
33.ドキュメントの訳と解説作業に疲れたので、AquesTalk pico for ESP32の評価版をマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-26
34.ドキュメントの訳と解説作業に疲れたので、Blynkをマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-27
35.ドキュメントの訳と解説作業に疲れたので、BlynkとAquesTalkをマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-28
36.35の続き。
https://hamayan.blog.ss-blog.jp/2018-03-29
37.36の続き。
https://hamayan.blog.ss-blog.jp/2018-03-31
ESP32のシステムタイマーの値を取得する。
https://hamayan.blog.ss-blog.jp/2019-02-11
おまけ。
ESP32 ArduinoでI2Cをマルチタスクで動かす by freeRTOS
https://hamayan.blog.ss-blog.jp/2018-07-11
上の続き。
https://hamayan.blog.ss-blog.jp/2018-07-12
ESP32のGPIOの割り付けについて調べてみた。
https://hamayan.blog.ss-blog.jp/2018-08-03
Arduino AVRでマルチタスクしてみる not freeRTOS !
https://hamayan.blog.ss-blog.jp/2018-10-20
上の続き。
https://hamayan.blog.ss-blog.jp/2018-10-25
上の続き。
https://hamayan.blog.ss-blog.jp/2018-11-02
ESP32のLEDCについて調べてみた。
https://hamayan.blog.ss-blog.jp/2018-11-16
上の続き。
https://hamayan.blog.ss-blog.jp/2018-11-26
M5Stackとフォント
https://hamayan.blog.ss-blog.jp/2019-03-08
M5Stackとフォント ランレングスへの対応
https://hamayan.blog.ss-blog.jp/2019-03-12
M5Stackとフォント GFXFFへの対応
https://hamayan.blog.ss-blog.jp/2019-03-12-1
M5Stackとフォント 漢字フォントの対応
https://hamayan.blog.ss-blog.jp/2019-03-16
M5Stackとフォント 漢字フォントの表示 UTF8への対応
https://hamayan.blog.ss-blog.jp/2019-03-23
モチベーションが尽きたので、一旦無期限サスペンドタスク。
Interfaceのページ
https://interface.cqpub.co.jp/magazine/202104/
過去に書いた「ESP32はRTOSができるらしい、、、」で始まるESP32でfreeRTOSを使う、いや使ってみた内容のまとめと言うか、リンク集。
ただし2018年に作成した記事なので、freeRTOSのバージョンが上がっている事から必ずしも現状と一致していないところが有るはず。なので、以下のリンクのドキュメントを参照する必要性はあると思う。
https://www.freertos.org/Documentation/RTOS_book.html
ESP32で使用しているfreeRTOSのバージョンも現在はver.10.2.0となっているようだ。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/freertos-smp.html
1.はじめに
ESP32はDual Core(SMP)なので、そこにちょっと触れて、タスクの生成手順を解説。
https://hamayan.blog.ss-blog.jp/2018-02-16
2.1の続き
1では1つのタスクの生成を行ったが、今回は複数のタスクの生成を行っている。
https://hamayan.blog.ss-blog.jp/2018-02-17
3.それぞれのタスクが動いているうえで、しかし一つのCore上で動く事のできるタスクは1つしか無い。なのでそこで動くタスクが現在どの様な状態にあるのか?の解説。
RTOSを理解するうえでとても重要な概念となる。
https://hamayan.blog.ss-blog.jp/2018-02-18
4.タスクの生成、停止、終了、削除、優先度と言ったタスク管理機能の解説。
https://hamayan.blog.ss-blog.jp/2018-02-18-1
5.タイムティック、つまり処理の時間単位と、それを使った遅延処理、周期処理について解説。
https://hamayan.blog.ss-blog.jp/2018-02-18-2
6.実行すべきユーザータスクが無い時、何が行われるのか?IDLE状態について解説。
WDTリセットとか省電力のカギかも!
https://hamayan.blog.ss-blog.jp/2018-02-19
7.タスク間通信のデータキューの解説。
タスク間で通信する手順が提供されているのです。この通信方法は複数ありまして、今回はデータキューです。このデータキューを利用すると、タスクの実行制御もできます。
https://hamayan.blog.ss-blog.jp/2018-02-19-1
8.タスク優先度に絡むうっかり話。
メカニズムが判れば面白いと思う!
https://hamayan.blog.ss-blog.jp/2018-02-20
9.WDTリセットの実験。
モヤモヤ、、、
https://hamayan.blog.ss-blog.jp/2018-02-21
10.7のデータキューの続き。
https://hamayan.blog.ss-blog.jp/2018-02-21-1
11.資源に対する排他制御、バイナリーセマフォについて解説。
マルチタスク、いやマルチタスクに限らず割込みも含めて、一つの資源に複数のコンテキストからのアクセスに対する制御を行う。
https://hamayan.blog.ss-blog.jp/2018-02-22
12.11の続き。
13.freeRTOSの多重割込みについて解説。
https://hamayan.blog.ss-blog.jp/2018-02-26
14.11とも関係するが、資源の危険な扱い(クリティカルセクション)に関する解説。
https://hamayan.blog.ss-blog.jp/2018-02-26-1
15.ESP32のdigitalWriteについての実験。
https://hamayan.blog.ss-blog.jp/2018-02-26-2
16.12の続き。カウンティングセマフォについて解説。
https://hamayan.blog.ss-blog.jp/2018-02-26-3
17.16の続き。
https://hamayan.blog.ss-blog.jp/2018-02-28
18.17の続き。
https://hamayan.blog.ss-blog.jp/2018-03-01
19.実は14の続き。
https://hamayan.blog.ss-blog.jp/2018-03-02
20.freeRTOSのクリティカルセクションの扱いについて解説。
割込み禁止、割込み許可。
https://hamayan.blog.ss-blog.jp/2018-03-06
21.20の続き。
ディスパッチ禁止、ディスパッチ許可。
https://hamayan.blog.ss-blog.jp/2018-03-07
22.21の続き。
ミューテックス。
https://hamayan.blog.ss-blog.jp/2018-03-07-1
23.22の続き。
https://hamayan.blog.ss-blog.jp/2018-03-08
24.23の続き。
まだまだ続くよミューテックス。
https://hamayan.blog.ss-blog.jp/2018-03-08-1
25.24の続き。
デッドロック。
https://hamayan.blog.ss-blog.jp/2018-03-08-2
26.Tick割込みからのコールバック。
https://hamayan.blog.ss-blog.jp/2018-03-08-3
27.25の続き。
ゲートキーパータスク!結局解決策はそれかよ、、、良いと思う。
https://hamayan.blog.ss-blog.jp/2018-03-08-4
28.データタイプと命名規則。
freeRTOSのAPIがちょっと読み易くなるかもしれない。
RTOSのAPI名等にやたら長い名前はどうかと思うけれど、今時の開発環境なら問題無いのでしょう。
https://hamayan.blog.ss-blog.jp/2018-03-09
29.28の続き。
https://hamayan.blog.ss-blog.jp/2018-03-09-1
30.タスクの状態参照とスタックサイズ。
https://hamayan.blog.ss-blog.jp/2018-03-09-2
31.メモリ管理。
https://hamayan.blog.ss-blog.jp/2018-03-12
33.ドキュメントの訳と解説作業に疲れたので、AquesTalk pico for ESP32の評価版をマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-26
34.ドキュメントの訳と解説作業に疲れたので、Blynkをマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-27
35.ドキュメントの訳と解説作業に疲れたので、BlynkとAquesTalkをマルチタスクで動かす。
https://hamayan.blog.ss-blog.jp/2018-03-28
36.35の続き。
https://hamayan.blog.ss-blog.jp/2018-03-29
37.36の続き。
https://hamayan.blog.ss-blog.jp/2018-03-31
ESP32のシステムタイマーの値を取得する。
https://hamayan.blog.ss-blog.jp/2019-02-11
おまけ。
ESP32 ArduinoでI2Cをマルチタスクで動かす by freeRTOS
https://hamayan.blog.ss-blog.jp/2018-07-11
上の続き。
https://hamayan.blog.ss-blog.jp/2018-07-12
ESP32のGPIOの割り付けについて調べてみた。
https://hamayan.blog.ss-blog.jp/2018-08-03
Arduino AVRでマルチタスクしてみる not freeRTOS !
https://hamayan.blog.ss-blog.jp/2018-10-20
上の続き。
https://hamayan.blog.ss-blog.jp/2018-10-25
上の続き。
https://hamayan.blog.ss-blog.jp/2018-11-02
ESP32のLEDCについて調べてみた。
https://hamayan.blog.ss-blog.jp/2018-11-16
上の続き。
https://hamayan.blog.ss-blog.jp/2018-11-26
M5Stackとフォント
https://hamayan.blog.ss-blog.jp/2019-03-08
M5Stackとフォント ランレングスへの対応
https://hamayan.blog.ss-blog.jp/2019-03-12
M5Stackとフォント GFXFFへの対応
https://hamayan.blog.ss-blog.jp/2019-03-12-1
M5Stackとフォント 漢字フォントの対応
https://hamayan.blog.ss-blog.jp/2019-03-16
M5Stackとフォント 漢字フォントの表示 UTF8への対応
https://hamayan.blog.ss-blog.jp/2019-03-23
モチベーションが尽きたので、一旦無期限サスペンドタスク。
ITRONプログラミング入門 H8マイコンとHOSで始める組み込み開発
- 出版社/メーカー: オーム社
- 発売日: 2005/04/23
- メディア: Kindle版
リアルタイムOSと組み込み技術の基礎―実践μITRONプログラミング (TECHI (Vol.17))
- 作者: 高田 広章
- 出版社/メーカー: CQ出版
- 発売日: 2004/02
- メディア: 単行本
ESP8266への気付き [ESP32]
ESP8266のIO0って、出力設定しないと暴れているよね!なんで???
Deep Sleep!
ESP.deepSleep( 100 * 1000 * 1000UL , WAKE_RF_DEFAULT );
で省電力に入った場合、指定した時間の間だけ省電力で、その後タイムアウトすると8mAくらい消費している感じ。
Light Sleep!
light sleepモードは1mA程度に抑えられるのは助かる。んが、結構手続きが面倒かも。あと起床する手段が必要だが外部割込みが使える。以下のサイトがよく判る。
http://okiraku-camera.tokyo/blog/?p=4996
以下のリンク先のPDFのサンプルをそのまま使っても、ただしくlight sleep modeに入らないのがムカつくよね。タイムアウトで抜ける処理をやりたいのに、、、
https://www.espressif.com/sites/default/files/documentation/2c-esp8266_non_os_sdk_api_reference_en.pdf
http://okiraku-camera.tokyo/blog/?p=4996
上記リンクのサンプルを若干アレンジして試してみたのだが、かなりの頻度でlight sleepから抜けてしまう現象が発生していた。またコールバックルーチンがいきなり呼ばれてしまう、、、
割込み入力端子はプルアップ抵抗とスイッチのみ接続し、GNDに落とすトリガーを入れている。
なぜいきなり省電力から抜けるのか結局判らない。ただし対策としてコールバックルーチンの中で端子状態を確認し、GNDレベルでなければ再度light sleepに入る様に対策して、確実に省電力状態に入る様にした。
以下、サンプルコード
light sleep時のタイマー
うすうすはそうじゃないか?とは思っていたが、しかしwifi_fpm_do_sleepの引数はμs単位の時間を指定できる事もあり、何度かタイムアウトで省電力から抜けないか?と試したが、少なくともESP-WROOM-02ではできなかった。
light sleep時はタイマー自体が止まってしまうようで、他のマイコンでよくある低消費電力モードに入ってもRCオシレータは動いている!という事は無いのかもしれない。
この為、ESP8266単体で省電力に入っていた時間を知る事はできないのかもしれない。復帰した時にNTPなどに接続すればイイのだが、、、イチイチそんな目的の為にNTPに接続するのもねぇ!
※micros()で省電力に入る直前、直後の時間を出力しても、省電力の時間分進んでいる事は無かった。
3.3Vレギュレータ!
ESP8266とは関係無い?が、3.3Vレギュレータはこれを使った。
無負荷時電流が小さいのが助かる。
http://akizukidenshi.com/catalog/g/gI-11299/
TOUT!
TOUTを使って電圧測定をしてみた。その前に、TOUTのADCは10bitのADCであり、入力範囲は0V~1Vであるらしい。
プロットさせてみたのが左のグラフ。簡易的にデータを取った為かどうか判らないが、直線ではない。でも小数点以下1桁くらいの精度の電圧測定なら十分な気もする。これって、内部で補正されているんだっけ?
※Vinは分圧してからTOUTに接続している。
ソフトウエアシリアルのバッファサイズを変更する!
なんであんなケチケチバッファを割り当てているねん?
SoftwareSerial gpsSerial( GPS_TXD, GPS_RXD ); // RX, TX
gpsSerial.begin( 9600, SWSERIAL_8N1, GPS_TXD, GPS_RXD, false , 4096, 4096 );
// 最後から二番目はバッファサイズ、最後は割込み時のバッファサイズ。大きなデータを受信する時は大きめに!
Deep Sleep!
ESP.deepSleep( 100 * 1000 * 1000UL , WAKE_RF_DEFAULT );
で省電力に入った場合、指定した時間の間だけ省電力で、その後タイムアウトすると8mAくらい消費している感じ。
Light Sleep!
light sleepモードは1mA程度に抑えられるのは助かる。んが、結構手続きが面倒かも。あと起床する手段が必要だが外部割込みが使える。以下のサイトがよく判る。
http://okiraku-camera.tokyo/blog/?p=4996
以下のリンク先のPDFのサンプルをそのまま使っても、ただしくlight sleep modeに入らないのがムカつくよね。タイムアウトで抜ける処理をやりたいのに、、、
https://www.espressif.com/sites/default/files/documentation/2c-esp8266_non_os_sdk_api_reference_en.pdf
http://okiraku-camera.tokyo/blog/?p=4996
上記リンクのサンプルを若干アレンジして試してみたのだが、かなりの頻度でlight sleepから抜けてしまう現象が発生していた。またコールバックルーチンがいきなり呼ばれてしまう、、、
割込み入力端子はプルアップ抵抗とスイッチのみ接続し、GNDに落とすトリガーを入れている。
なぜいきなり省電力から抜けるのか結局判らない。ただし対策としてコールバックルーチンの中で端子状態を確認し、GNDレベルでなければ再度light sleepに入る様に対策して、確実に省電力状態に入る様にした。
以下、サンプルコード
/* ESP8266 light sleep test. 気づき 1.ESP8266をlight sleepに入れると、タイマー系は全て停止してしまうので、 sleepに入っていた時間を知る事ができない。 2.wifi再接続時は13秒とか掛かる事が多い。 3.なぜか一発でlight sleepに入らない事も多い。 4.上記は、light sleepに入った途端にコールバックルーチンが呼ばれてしまう様で、ポートで端子状態を確認が必要。 5.コールバックルーチンの中でdelayとかyieldとか呼ぶと、コアを吐いて死んでしまう。 参考 http://okiraku-camera.tokyo/blog/?p=4996 */ #include <ESP8266WiFi.h> extern "C" { #include "user_interface.h" #include "gpio.h" } /**********************************************************/ /* defines */ /**********************************************************/ #define ASSOC 0 #define INT_X 2 #define GPS_POWER_PIN 15 #define FPM_SLEEP_MAX_TIME 0xFFFFFFF /**********************************************************/ /* global variables */ /**********************************************************/ bool lightSleepLoop; /**********************************************************/ /* setup. */ /**********************************************************/ void setup() { Serial.begin( 115200 ); delay( 20UL ); Serial.println(); Serial.println( "ESP8266 light sleep test." ); /* GPIOの初期化。消費電力を抑える目的でもある */ pinMode( ASSOC, OUTPUT ); digitalWrite( ASSOC, LOW ); /* active high */ pinMode( INT_X, INPUT_PULLUP ); /* active low input */ pinMode( GPS_POWER_PIN, OUTPUT ); digitalWrite( GPS_POWER_PIN, LOW ); /* active high */ /* enter light sleep mode. */ enterLightSleep( FPM_SLEEP_MAX_TIME ); } /**********************************************************/ /* loop. */ /**********************************************************/ void loop() { delay( 500 ); digitalWrite( ASSOC, (digitalRead( ASSOC ) == LOW) ? HIGH : LOW ); } /**********************************************************/ /* call back routine from wake up. */ /**********************************************************/ void fpm_wakup_cb( void ) { if( digitalRead( INT_X ) == LOW && digitalRead( INT_X ) == LOW && digitalRead( INT_X ) == LOW ) lightSleepLoop = false; Serial.write( 'c' ); } /**********************************************************/ /* enter the light sleep mode. */ /**********************************************************/ void enterLightSleep( uint32_t period ) { WiFi.mode( WIFI_OFF ); wifi_set_opmode_current( NULL_MODE ); wifi_fpm_set_sleep_type( LIGHT_SLEEP_T ); wifi_fpm_open(); gpio_pin_wakeup_enable( INT_X, GPIO_PIN_INTR_LOLEVEL ); /* or GPIO_PIN_INTR_HILEVEL */ wifi_fpm_set_wakeup_cb( fpm_wakup_cb ); // Set wakeup callback lightSleepLoop = true; do { wifi_fpm_do_sleep( period ); // sleep until gpio activity. delay( 50UL ); Serial.write( '*' ); } while( lightSleepLoop ); gpio_pin_wakeup_disable(); wifi_fpm_close(); // disable force sleep function }
light sleep時のタイマー
うすうすはそうじゃないか?とは思っていたが、しかしwifi_fpm_do_sleepの引数はμs単位の時間を指定できる事もあり、何度かタイムアウトで省電力から抜けないか?と試したが、少なくともESP-WROOM-02ではできなかった。
light sleep時はタイマー自体が止まってしまうようで、他のマイコンでよくある低消費電力モードに入ってもRCオシレータは動いている!という事は無いのかもしれない。
この為、ESP8266単体で省電力に入っていた時間を知る事はできないのかもしれない。復帰した時にNTPなどに接続すればイイのだが、、、イチイチそんな目的の為にNTPに接続するのもねぇ!
※micros()で省電力に入る直前、直後の時間を出力しても、省電力の時間分進んでいる事は無かった。
3.3Vレギュレータ!
ESP8266とは関係無い?が、3.3Vレギュレータはこれを使った。
無負荷時電流が小さいのが助かる。
http://akizukidenshi.com/catalog/g/gI-11299/
TOUT!
TOUTを使って電圧測定をしてみた。その前に、TOUTのADCは10bitのADCであり、入力範囲は0V~1Vであるらしい。
プロットさせてみたのが左のグラフ。簡易的にデータを取った為かどうか判らないが、直線ではない。でも小数点以下1桁くらいの精度の電圧測定なら十分な気もする。これって、内部で補正されているんだっけ?
※Vinは分圧してからTOUTに接続している。
ソフトウエアシリアルのバッファサイズを変更する!
なんであんなケチケチバッファを割り当てているねん?
SoftwareSerial gpsSerial( GPS_TXD, GPS_RXD ); // RX, TX
gpsSerial.begin( 9600, SWSERIAL_8N1, GPS_TXD, GPS_RXD, false , 4096, 4096 );
// 最後から二番目はバッファサイズ、最後は割込み時のバッファサイズ。大きなデータを受信する時は大きめに!
Arduinoライブラリでテレビジョンを制御 家庭でできるIoT! 5パルス目 [ESP32]
前回( https://hamayan.blog.ss-blog.jp/2019-12-09-1 )作成したリモコンのコードを受信するプログラムを使って、テレビジョン(AQUOS)の幾つかのリモコンコードをサンプリングしました。その内容を使ってBlynkでリモコンを実現します。
もうお判りでしょうが、AQUOSライブラリを作成するにあたってHeatpumpIR Classを使って赤外線ドライバー部分はお手軽に実現しています。
テレビはNHKとテレ東くらいしか観ないので、Blynkのパネルの一番下に一発選局ボタンを付けておきました(笑)。
もうちょい機能を追加すれば、テレビのリモコン無くしても安心ですね!
aquos.h
aquos.cpp
スケッチの追加部分のみ
もうお判りでしょうが、AQUOSライブラリを作成するにあたってHeatpumpIR Classを使って赤外線ドライバー部分はお手軽に実現しています。
テレビはNHKとテレ東くらいしか観ないので、Blynkのパネルの一番下に一発選局ボタンを付けておきました(笑)。
もうちょい機能を追加すれば、テレビのリモコン無くしても安心ですね!
aquos.h
/****************************************************************************/ /* お家のAQUOSをなんとかするヘッダー */ /* Copyright (C) 2014 hamayan All Rights Reserved. */ /****************************************************************************/ #ifndef aquos_h #define aquos_h #include <Arduino.h> #include <HeatpumpIR.h> extern "C" { // #include <mul_tsk.h> } /****************************************************************************/ /* なにかの定義 */ /****************************************************************************/ #define AEHA_T_PERIOD 425UL // #define AEHA_REPEAT_PERIOD 100UL // typedef struct /*AQUOSのフォーマットはAEHAタイプ*/ { uint8_t customerCode[2]; uint8_t data[4]; } AQUOS_CODE; class aquos : public HeatpumpIR { private: int antenaSelect; int channelSelect; uint32_t startTim; void send( IRSender& IR, const uint8_t *code, size_t size ); public: aquos(); void onOff( IRSender& IR ); void ch( IRSender& IR, int value ); void chUpDown( IRSender& IR, int value ); void volume( IRSender& IR, int value ); void antena( IRSender& IR ); void decide( IRSender& IR ); void back( IRSender& IR ); void program( IRSender& IR ); void recorderList( IRSender& IR ); void selectorSwitch( IRSender& IR, int upDown, int rightLeft ); void favorite( IRSender& IR, int sel ); char type( void ); }; #endif /*aquos_h*/ /****************************************************************************/ /* Copyright (C) 2014 hamayan All Rights Reserved. */ /****************************************************************************/
aquos.cpp
/*********************************************************************************/ /* お家のAQUOSをなんとかするソース */ /* designed by hamayan since 2015/05/28 */ /*********************************************************************************/ #include "aquos.h" #if !defined( _MULTITASK_H_ ) #define dly_tsk(tim) delay(tim) #define rot_rdq() #define loc_cpu() interrupts() #define unl_cpu() noInterrupts() #endif /*_MULTITASK_H_ */ /*************************************************************************/ /* 大域変数宣言 */ /*************************************************************************/ /*************************************************************************/ /* プロトタイプ宣言 */ /*************************************************************************/ /*************************************************************************/ /* インスタンス */ /*************************************************************************/ aquos::aquos() { antenaSelect = 0; channelSelect = 1; } /*************************************************************************/ /* send */ /*************************************************************************/ void aquos::send( IRSender& IR, const uint8_t *code, size_t size ) { uint8_t tempUC[ size ]; memcpy( tempUC, code, size ); // 40 kHz PWM frequency IR.setFrequency( 38 ); //リピートでメソッドが呼ばれた時、強制的に間隔を開ける while( (millis() - startTim) < AEHA_REPEAT_PERIOD ) delay( 10 ); // start time startTim = millis(); // Header IR.mark( 8 * AEHA_T_PERIOD ); //frame ppm on IR.space( 4 * AEHA_T_PERIOD ); //frame ppm off // Data for( unsigned int i = 0; i < size; i++ ) { IR.sendIRbyte( tempUC[i], AEHA_T_PERIOD, AEHA_T_PERIOD, 3 * AEHA_T_PERIOD ); // data,426us,425us,1275us // pulse on period, pulse off period at ZERO, pulse off period at 1 } // End mark IR.mark( AEHA_T_PERIOD ); IR.space( 0 ); } /*************************************************************************/ /* 番組表 */ /*************************************************************************/ static const uint8_t TVPROGRAMLIST[] = {0xaa,0x5a,0x8f,0x12,0x60,0xf2}; void aquos::program( IRSender& IR ) { send( IR, TVPROGRAMLIST, sizeof( TVPROGRAMLIST ) ); } /*************************************************************************/ /* 電源の入り切。だが実際にはテレビがそれに追従してはいない。 */ /*************************************************************************/ static const uint8_t TVOnOff[] = {0xaa,0x5a,0x8f,0x12,0x16,0xd1}; void aquos::onOff( IRSender& IR ) { send( IR, TVOnOff, sizeof( TVOnOff ) ); } /*************************************************************************/ /* 決定ボタン */ /*************************************************************************/ static const uint8_t DECIDE[] = {0xaa,0x5a,0x8f,0x12,0x52,0xd1}; void aquos::decide( IRSender& IR ) { send( IR, DECIDE, sizeof( DECIDE ) ); } /*************************************************************************/ /* 戻るボタン */ /*************************************************************************/ static const uint8_t BACK[] = {0xaa,0x5a,0x8f,0x12,0xe4,0x01}; void aquos::back( IRSender& IR ) { send( IR, BACK, sizeof( BACK ) ); } /*************************************************************************/ /* 録画リスト */ /*************************************************************************/ static const uint8_t RECORDERLIST[] = {0xaa,0x5a,0x8f,0x12,0xad,0x3f}; void aquos::recorderList( IRSender& IR ) { send( IR, RECORDERLIST, sizeof( RECORDERLIST ) ); } /*************************************************************************/ /* 選択用スイッチ */ /*************************************************************************/ static const uint8_t SELECTORSWITCH[][6] = { {0xaa,0x5a,0x8f,0x12,0x57,0x81}, /*up*/ {0xaa,0x5a,0x8f,0x12,0x20,0x81}, /*down*/ {0xaa,0x5a,0x8f,0x12,0xd8,0xf1}, /*right*/ {0xaa,0x5a,0x8f,0x12,0xd7,0x01}, /*left*/ }; void aquos::selectorSwitch( IRSender& IR, int upDown, int rightLeft ) { if( upDown >= 1 ) send( IR, SELECTORSWITCH[0], sizeof( SELECTORSWITCH[0] ) ); else if( upDown <= -1 ) send( IR, SELECTORSWITCH[1], sizeof( SELECTORSWITCH[1] ) ); dly_tsk( 100UL ); if( rightLeft >= 1 ) send( IR, SELECTORSWITCH[2], sizeof( SELECTORSWITCH[2] ) ); else if( rightLeft <= -1 ) send( IR, SELECTORSWITCH[3], sizeof( SELECTORSWITCH[3] ) ); } /*************************************************************************/ /* チャネルの切り替え */ /*************************************************************************/ static const uint8_t TVCH[][6] = { {0xaa,0x5a,0x8f,0x12,0x4e,0x32}, /*ch1*/ {0xaa,0x5a,0x8f,0x12,0x4f,0x22}, /*ch2*/ {0xaa,0x5a,0x8f,0x12,0x50,0xc2}, /*ch3*/ {0xaa,0x5a,0x8f,0x12,0x51,0xd2}, /*ch4*/ {0xaa,0x5a,0x8f,0x12,0x52,0xe2}, /*ch5*/ {0xaa,0x5a,0x8f,0x12,0x53,0xf2}, /*ch6*/ {0xaa,0x5a,0x8f,0x12,0x54,0x82}, /*ch7*/ {0xaa,0x5a,0x8f,0x12,0x55,0x92}, /*ch8*/ {0xaa,0x5a,0x8f,0x12,0x56,0xa2}, /*ch9*/ {0xaa,0x5a,0x8f,0x12,0x57,0xb2}, /*ch10*/ {0xaa,0x5a,0x8f,0x12,0x58,0x42}, /*ch11*/ {0xaa,0x5a,0x8f,0x12,0x59,0x52}, /*ch12*/ }; void aquos::ch( IRSender& IR, int value ) { if( value >= 1 && value <= 12 ) { send( IR, TVCH[value - 1], sizeof( TVCH[0] ) ); channelSelect = value; } } /*************************************************************************/ /* チャネルの上下 */ /*************************************************************************/ static const uint8_t TVSEL[][6] = { {0xaa,0x5a,0x8f,0x12,0x11,0xa1}, /*decide up*/ {0xaa,0x5a,0x8f,0x12,0x12,0x91}, /*decide down*/ }; void aquos::chUpDown( IRSender& IR, int value ) { if( value > 0 ) //up { send( IR, TVSEL[0], sizeof( TVSEL[0] ) ); channelSelect++; } else if( value < 0 ) //down { send( IR, TVSEL[1], sizeof( TVSEL[1] ) ); channelSelect--; } } /*************************************************************************/ /* チューナー(アンテナ)の切り替え */ /*************************************************************************/ static const uint8_t TVANTENA[][6] = { {0xaa,0x5a,0x8f,0x12,0x89,0x82}, /*地デジ*/ {0xaa,0x5a,0x8f,0x12,0x8a,0xb2}, /*BS*/ {0xaa,0x5a,0x8f,0x12,0x8b,0xa2}, /*CS*/ }; void aquos::antena( IRSender& IR ) { if( ++antenaSelect > 1 ) antenaSelect = 0; send( IR, TVANTENA[antenaSelect], sizeof( TVANTENA[0] ) ); } /*************************************************************************/ /* ボリュームの上下 */ /*************************************************************************/ static const uint8_t TVVOLUME[][6] = { {0xaa,0x5a,0x8f,0x12,0x14,0xf1}, /*volume up*/ {0xaa,0x5a,0x8f,0x12,0x15,0xe1}, /*volume down*/ }; void aquos::volume( IRSender& IR, int value ) { if( value < 0 ) { send( IR, TVVOLUME[1], sizeof( TVVOLUME[1] ) ); } else if( value > 0 ) { send( IR, TVVOLUME[0], sizeof( TVVOLUME[0] ) ); } } /*************************************************************************/ /* お気に入り番組登録 */ /*************************************************************************/ void aquos::favorite( IRSender& IR, int value ) { if( value == 0 ) /* nhk sougou. */ { antenaSelect = 0; channelSelect = 1; } else if( value == 1 ) /* nhk bs premium. */ { antenaSelect = 1; channelSelect = 3; } else if( value == 2 ) /* tokyo televison. */ { antenaSelect = 0; channelSelect = 7; } else if( value == 3 ) /* bs tokyo televison. */ { antenaSelect = 1; channelSelect = 7; } else return; send( IR, TVANTENA[antenaSelect], sizeof( TVANTENA[0] ) ); send( IR, TVCH[channelSelect - 1], sizeof( TVCH[0] ) ); } /*************************************************************************/ /* リモコンフォーマットを答える */ /*************************************************************************/ char aquos::type( void ) { return 'A'; /*AEHAのA!*/ } /*********************************************************************************/ /* end of file */ /* designed by hamayan since 2015/05/28 */ /*********************************************************************************/
スケッチの追加部分のみ
aquos *tv = new aquos(); /*************************************************************************/ /* blynk virtual pin 6 function as TV power on or off. */ /*************************************************************************/ BLYNK_WRITE( V6 ) { tv->onOff( irSender ); } /*************************************************************************/ /* blynk virtual pin 7 function as TV program list on or off. */ /*************************************************************************/ BLYNK_WRITE( V7 ) { tv->program( irSender ); } /*************************************************************************/ /* blynk virtual pin 8 function as TV record list on or off. */ /*************************************************************************/ BLYNK_WRITE( V8 ) { tv->recorderList( irSender ); } /*************************************************************************/ /* blynk virtual pin 9 function as TV antena(tuner) change. */ /*************************************************************************/ BLYNK_WRITE( V9 ) { tv->antena( irSender ); } /*************************************************************************/ /* blynk virtual pin 10 function as channel NHK SOUGOU. */ /*************************************************************************/ BLYNK_WRITE( V10 ) { tv->favorite( irSender, 0 ); } /*************************************************************************/ /* blynk virtual pin 11 function as channel NHK BS PREMIUM. */ /*************************************************************************/ BLYNK_WRITE( V11 ) { tv->favorite( irSender, 1 ); } /*************************************************************************/ /* blynk virtual pin 12 function as channel TOKYO TELEVISION. */ /*************************************************************************/ BLYNK_WRITE( V12 ) { tv->favorite( irSender, 2 ); } /*************************************************************************/ /* blynk virtual pin 13 function as channel BS TOKYO TELEVISION. */ /*************************************************************************/ BLYNK_WRITE( V13 ) { tv->favorite( irSender, 3 ); } /*************************************************************************/ /* blynk virtual pin 14 function as cursor move. */ /*************************************************************************/ BLYNK_WRITE( V14 ) { int rightLeft = param[0].asInt(); int upDown = param[1].asInt(); tv->selectorSwitch( irSender, upDown, rightLeft ); } /*************************************************************************/ /* blynk virtual pin 15 function as channel up. */ /*************************************************************************/ BLYNK_WRITE( V15 ) { int channel = param.asInt(); tv->chUpDown( irSender, channel ); } /*************************************************************************/ /* blynk virtual pin 16 function as channel down. */ /*************************************************************************/ BLYNK_WRITE( V16 ) { int channel = param.asInt(); tv->chUpDown( irSender, channel ); } /*************************************************************************/ /* blynk virtual pin 17 function as volume up. */ /*************************************************************************/ BLYNK_WRITE( V17 ) { int volume = param.asInt(); tv->volume( irSender, volume ); } /*************************************************************************/ /* blynk virtual pin 18 function as volume down. */ /*************************************************************************/ BLYNK_WRITE( V18 ) { int volume = param.asInt(); tv->volume( irSender, volume ); } /*************************************************************************/ /* blynk virtual pin 19 function as decide button. */ /*************************************************************************/ BLYNK_WRITE( V19 ) { tv->decide( irSender ); } /*************************************************************************/ /* blynk virtual pin 20 function as back button. */ /*************************************************************************/ BLYNK_WRITE( V20 ) { tv->back( irSender ); }
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~12畳 SH8237LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~14畳 SH8234LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
Arduinoで赤外線リモコンコードを読み出す 家庭でできるIoT! 4パルス目 [ESP32]
前回ちらっと書いたリモコンコードの受信です。
秋月電子で売られている赤外線受光素子( http://akizukidenshi.com/catalog/g/gI-04659/ )をデータシート通りに抵抗、コンデンサ等を接続し、OUT端子をESP8266に接続します。
この受光素子はサブキャリアを検出するとLowを出力、サブキャリアが無ければHigh(HI-Z)です。
ESP8266側では端子割込みを使ってLowやHighの時間を測定し、時間をビットに換算します。
比較的簡単な処理ですが、悩むのがフレームの終了検出で、NECフォーマットであればビット数は固定長ですが、AEHAフォーマットの場合はビット数は可変長となるので、どこまで取り込んで良いのか判りません。
ですがフレーム間の間隔は必ず空く仕様なので、時間でフレームの終端を強制的に決めてしまう事としました。
とは言え三菱のエアコンのリモコンコードはデータ部が18byte有り、もし全ビットが1であればフレーム長は250msを超えてしますので、いかがなものか?と思わないでもないですが、、、
まぁリモコンコードを見るだけの処理なので、細かい事は気にしません。
実際にスケッチを組んで読み出したものです。オーデリックのリモコンとアクオスのリモコンを受信しています。
オーデリックは前出の様にNECフォーマットです。アクオスはAEHAフォーマットでした。このリモコンコードはメーカーが仕様で示したものではなく、あくまでも自分で解析したコードである事に十分注意して下さい。仮に他のプログラムでこのコードを送信しても上手くは行かないと思います。学習リモコンの様にする場合は、受信したコードを送信可能なプログラムが別途必要です。なお、前回のプログラムでは使えます。
秋月電子で売られている赤外線受光素子( http://akizukidenshi.com/catalog/g/gI-04659/ )をデータシート通りに抵抗、コンデンサ等を接続し、OUT端子をESP8266に接続します。
この受光素子はサブキャリアを検出するとLowを出力、サブキャリアが無ければHigh(HI-Z)です。
ESP8266側では端子割込みを使ってLowやHighの時間を測定し、時間をビットに換算します。
比較的簡単な処理ですが、悩むのがフレームの終了検出で、NECフォーマットであればビット数は固定長ですが、AEHAフォーマットの場合はビット数は可変長となるので、どこまで取り込んで良いのか判りません。
ですがフレーム間の間隔は必ず空く仕様なので、時間でフレームの終端を強制的に決めてしまう事としました。
とは言え三菱のエアコンのリモコンコードはデータ部が18byte有り、もし全ビットが1であればフレーム長は250msを超えてしますので、いかがなものか?と思わないでもないですが、、、
まぁリモコンコードを見るだけの処理なので、細かい事は気にしません。
実際にスケッチを組んで読み出したものです。オーデリックのリモコンとアクオスのリモコンを受信しています。
オーデリックは前出の様にNECフォーマットです。アクオスはAEHAフォーマットでした。このリモコンコードはメーカーが仕様で示したものではなく、あくまでも自分で解析したコードである事に十分注意して下さい。仮に他のプログラムでこのコードを送信しても上手くは行かないと思います。学習リモコンの様にする場合は、受信したコードを送信可能なプログラムが別途必要です。なお、前回のプログラムでは使えます。
#include <ESP8266WiFi.h> void ICACHE_RAM_ATTR irRemocon_interrupt( void ); #define IR_CONT 12 /* Ir LED ON/OFF */ #define IR_RECV 13 /* Ir receive */ #define Max_IR_Bits_NEC 32 #define Max_IR_Bits_AEHA 256 #define NEC_HEADER_LOW_MIN_PERIOD 462UL #define NEC_HEADER_LOW_MAX_PERIOD 662UL #define AEHA_HEADER_LOW_MIN_PERIOD 350UL #define AEHA_HEADER_LOW_MAX_PERIOD 500UL unsigned long IR_Low_Time; // holds IR bursting time unsigned long IR_High_Time; // holds IR high idling time unsigned long Latest_Time_ms; unsigned short IR_Data[Max_IR_Bits_AEHA] = {0,}; // holds the bit length of each data bit by micro-sec. int IR_State; int IR_Bit_Count; bool IR_Active = false; // when true, the capturing IR data stream is valid char IR_formatType; void setup() { Serial.begin( 115200UL ); Serial.println( "\r\n\r\nIR recieve code print out." ); pinMode( IR_RECV, INPUT_PULLUP ); attachInterrupt( IR_RECV, irRemocon_interrupt, CHANGE ); } void loop() { if( IR_Active ) { if( IR_formatType == 'N' && (millis() - Latest_Time_ms) >= 18UL ) { if( irRemocon_available() >= 4 ) IR_nec(); irRemocon_init(); } else if( IR_formatType == 'A' && (millis() - Latest_Time_ms) >= 10UL ) { if( irRemocon_available() >= 6 ) IR_aeha(); irRemocon_init(); } } } /*************************************************************************/ /* int to hexadecimal */ /*************************************************************************/ char *itoh( char *dst, unsigned int value ) { sprintf( dst, "%02x", value ); return dst; } /*************************************************************************/ /* IR_REMOCON初期化 */ /*************************************************************************/ void irRemocon_init( void ) { IR_Active = false; // when 1, the capturing IR data stream is valid IR_formatType = 0; IR_State = 0; } /*************************************************************************/ /* データ数確認 */ /*************************************************************************/ int irRemocon_available( void ) { if( IR_Active ) return IR_State / 8; return 0; } /*************************************************************************/ /* NECタイプのデータ受信 */ /*************************************************************************/ int irRemocon_rxNEC( byte dst[], int size ) { int index = 0; for( int i = 0; i < Max_IR_Bits_NEC; i++ ) { dst[index] >>= 1; if ( IR_Data[i] > ((NEC_HEADER_LOW_MIN_PERIOD + NEC_HEADER_LOW_MAX_PERIOD) / 2) * 3 ) dst[index] |= 0x80; if( (i % 8) == 7 ) index++; } return index; } /*************************************************************************/ /* AEHAタイプのデータ受信 */ /*************************************************************************/ int irRemocon_rxAEHA( byte dst[], int size ) { volatile int limit = IR_State; int index = 0; for( int i = 0; i < limit; i++ ) { dst[index] >>= 1; if ( IR_Data[i] > ((AEHA_HEADER_LOW_MIN_PERIOD + AEHA_HEADER_LOW_MAX_PERIOD) / 2) * 3 ) dst[index] |= 0x80; if( (i % 8) == 7 ) index++; } return index; } /****************************************************************************/ /* IR RECEIVE for NEC format */ /****************************************************************************/ void IR_nec( void ) { int sz = irRemocon_available(); byte *iRData = new byte[sz]; sz = irRemocon_rxNEC( iRData, sz ); String str = ""; char asc[8]; for(int i = 0; i < sz; i++) { str += itoh(asc,(unsigned int)iRData[i]); str += ","; } delete[] iRData; if( str.length() > 0 ) { Serial.print( "type NEC:sz = " ); Serial.print( sz, DEC ); Serial.print( " code:" ); Serial.println( str ); } } /****************************************************************************/ /* IR RECEIVE for AEHA format */ /****************************************************************************/ void IR_aeha( void ) { int sz = irRemocon_available(); byte *iRData = new byte[sz]; sz = irRemocon_rxAEHA( iRData, sz ); char buf[8]; String str = ""; for(int i = 0; i < sz; i++) { str += itoh(buf,(unsigned int)iRData[i]); str += ","; } delete[] iRData; if( str.length() > 0 ) { Serial.print( "type AEHA: sz=" ); Serial.print( sz, DEC ); Serial.print( " code:" ); Serial.println( str ); } } /*************************************************************************/ /* IR_REMOCON割り込み */ /*************************************************************************/ void ICACHE_RAM_ATTR irRemocon_interrupt( void ) { if( digitalRead( IR_RECV ) == LOW ) // edge is low. { if( IR_Active ) //LOWからLOWまでの時間を計測して配列に保存する { unsigned long period = micros() - IR_Low_Time; if( IR_formatType == 'N' && period >= NEC_HEADER_LOW_MIN_PERIOD * 2 && period <= NEC_HEADER_LOW_MAX_PERIOD * 4 ) { if( IR_State < Max_IR_Bits_NEC ) { IR_Data[IR_State++] = (unsigned short)period; IR_Bit_Count = IR_State; Latest_Time_ms = millis(); } else irRemocon_init(); } else if( IR_formatType == 'A' && period >= AEHA_HEADER_LOW_MIN_PERIOD * 2 && period <= AEHA_HEADER_LOW_MAX_PERIOD * 4 ) { if( IR_State < Max_IR_Bits_AEHA ) { IR_Data[IR_State++] = (unsigned short)period; IR_Bit_Count = IR_State; Latest_Time_ms = millis(); } else irRemocon_init(); } } IR_Low_Time = micros(); } else // edge is high. { unsigned long period = micros() - IR_Low_Time; if( period >= NEC_HEADER_LOW_MIN_PERIOD * 8 ) // NECフォーマット { IR_Active = true; IR_State = 0; IR_High_Time = millis(); //先頭フレームの後半の時間となる IR_formatType = 'N'; Latest_Time_ms = millis(); } else if( period >= AEHA_HEADER_LOW_MIN_PERIOD * 8 ) // 家製協(AEHA)フォーマット { IR_Active = true; IR_State = 0; IR_High_Time = millis(); //先頭フレームの後半の時間となる IR_formatType = 'A'; Latest_Time_ms = millis(); } else {} } }
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~12畳 SH8237LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~14畳 SH8234LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
Arduinoライブラリで天井照明を制御 家庭でできるIoT! 3パルス目 [ESP32]
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~12畳 SH8237LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
誰も期待していないでしょうけれど、Blynkで動かす。
今回のネタは天井照明です。家にはLEDタイプのオーデリックの天井照明を全部の部屋に入れました。
アマゾンリンクの前の型ですが、多分リモコン制御に関しては変わらないと思います。
この天井照明は結構コストパフォーマンスに優れていると思います。12畳でリモコン付き、調光付きで実売が1万円切っていますから。家に入れたのは2015年3月ですので、4年以上安定稼働しています。またわざわざIoTにしなくても付属のリモコンに時計が付いていますので、指定時間に照明を消す/点けるができます。
さて、この天井照明をリモコンで制御するためにはリモコンコードを知らなくてはできませんが(※相変わらずリモコンコードの仕様は公開されていない)、、、とは言え秋月電子とかで売られている赤外線受光素子を用意すれば( http://akizukidenshi.com/catalog/g/gI-04659/ )、Arduinoを使って比較的容易にコードを読み取る事ができます。※受光についてはその内やります。
とりあえずこの天井照明はNECフォーマットとなっています。
一般的に家電に使われているリモコンフォーマットは、NECフォーマット、AEHAフォーマット、SONYフォーマットが有ります。これらのフォーマットの詳細については以下のページを読んで下さい。
ChaNさんのリモコンコードの解説ページ
http://elm-chan.org/docs/ir_format.html
ちょっと補足
キャリア:キャリアは使用する赤外線LEDで決まってしまいます。秋月で買える赤外線LED( http://akizukidenshi.com/catalog/g/gI-12612/ )は大概波長940μmです。要らないリモコンを分解して取り出しても構いません。半減角:15°(狭角)~60°(広角)や、LEDに流す電流で使い易さが変わってくるでしょう。
サブキャリア周波数:キャリアをパルス状にして出力する事で、ちまたに溢れている赤外線と、リモコンの赤外線を区別していますが、そのパルスの周波数です。一般的に38kHz前後です。
つまりリモコンコードのビット毎の0または1に対応するパターンに従って赤外線LEDをON/OFFさせれば良いことが判ります。この時ArduinoであればサブキャリアはanalogWrite、パルスの時間はdelayMicrosecondsを使えば実現できます。
/*************************************************************************/ /* NECタイプのデータ送信 */ /*************************************************************************/ void iRRemocon::txNEC(const unsigned char data[], unsigned int sz) { #define T_VALUE 555UL // 562UL #define PPM_ON 128 //170 #define PPM_OFF 255 analogWriteRange( 255 ); analogWriteFreq( 38 * 1000UL ); analogWrite(_IR_CONT,0); //frame ppm on analogWrite(_IR_CONT,PPM_ON); delayMicroseconds(T_VALUE * 16); //16T //frame ppm off analogWrite(_IR_CONT,PPM_OFF); delayMicroseconds(T_VALUE * 8); //8T for(int i = 0; i < sz; i++) { byte temp = data[i]; for(int j = 0; j < 8; j++) { if(temp & 0x01) { //frame ppm on analogWrite(_IR_CONT,PPM_ON); delayMicroseconds(T_VALUE * 1); //1T //frame ppm off analogWrite(_IR_CONT,PPM_OFF); delayMicroseconds(T_VALUE * 3); //3T } else { //frame ppm on analogWrite(_IR_CONT,PPM_ON); delayMicroseconds(T_VALUE * 1); //1T //frame ppm off analogWrite(_IR_CONT,PPM_OFF); delayMicroseconds(T_VALUE * 1); //1T } temp >>= 1; } } //stop bit //frame ppm on analogWrite(_IR_CONT,PPM_ON); delayMicroseconds(T_VALUE * 1); //1T //frame ppm off analogWrite(IR_CONT,0); }
ですがまぁ、折角エアコン制御を実現しているので、そこで使われている赤外線出力のClassを利用する事とします。
Classの使い方を知るために、三菱のエアコンのソースコードを見てみます。三菱のエアコンはAEHAフォーマットです。
MitsubishiHeatpumpIR.cpp
void MitsubishiHeatpumpIR::sendMitsubishi(IRSender& IR, uint8_t powerMode, uint8_t operatingMode, uint8_t fanSpeed, uint8_t temperature, uint8_t swingV, uint8_t swingH) { 長いので抜粋 // 40 kHz PWM frequency IR.setFrequency(38); // The Mitsubishi data is repeated twice for (int j=0; j<2; j++) { // Header IR.mark(MITSUBISHI_AIRCON1_HDR_MARK); IR.space(MITSUBISHI_AIRCON1_HDR_SPACE); // Data for (unsigned int i=0; i<sizeof(MitsubishiTemplate); i++) { IR.sendIRbyte(MitsubishiTemplate[i], MITSUBISHI_AIRCON1_BIT_MARK, MITSUBISHI_AIRCON1_ZERO_SPACE, MITSUBISHI_AIRCON1_ONE_SPACE); } // Pause between the first and the second data burst // Also modify one byte for the second burst on MSY model. This does not affect the checksum of the second burst if (j == 0) { IR.mark(MITSUBISHI_AIRCON1_BIT_MARK); IR.space(MITSUBISHI_AIRCON1_MSG_SPACE); if (_mitsubishiModel == MITSUBISHI_MSY) { MitsubishiTemplate[14] = 0x24; } } } // End mark IR.mark(MITSUBISHI_AIRCON1_BIT_MARK); IR.space(0); }
MitsubishiHeatpumpIR.h
/* Mitsubishi MSZ FD-25 heatpump control (remote control P/N KM09D 0052376) */ #ifndef MitsubishiHeatpumpIR_h #define MitsubishiHeatpumpIR_h #include "HeatpumpIR.h" 長いので抜粋 // Mitsubishi MSZ FD-25 timing constants (remote control P/N KM09D 0052376) #define MITSUBISHI_AIRCON1_HDR_MARK 3500 #define MITSUBISHI_AIRCON1_HDR_SPACE 1700 #define MITSUBISHI_AIRCON1_BIT_MARK 430 #define MITSUBISHI_AIRCON1_ONE_SPACE 1250 #define MITSUBISHI_AIRCON1_ZERO_SPACE 390 #define MITSUBISHI_AIRCON1_MSG_SPACE 17500 class MitsubishiHeatpumpIR : public HeatpumpIR { protected: // Cannot create generic Mitsubishi heatpump instances MitsubishiHeatpumpIR(); uint8_t _mitsubishiModel; // Tells whether this is FD or EF (or other supported model...) public: void send(IRSender& IR, uint8_t powerModeCmd, uint8_t operatingModeCmd, uint8_t fanSpeedCmd, uint8_t temperatureCmd, uint8_t swingVCmd, uint8_t swingHCmd); private: void sendMitsubishi(IRSender& IR, uint8_t powerMode, uint8_t operatingMode, uint8_t fanSpeed, uint8_t temperature, uint8_t swingVCmd, uint8_t swingHCmd); }; #endif
定義のMITSUBISHI_AIRCON1_HDR_MARKは赤外線フレームのLeader部の8Tに、MITSUBISHI_AIRCON1_HDR_SPACEはLeader部の4Tに相当します。
MITSUBISHI_AIRCON1_BIT_MARKはパルスを出している時間、MITSUBISHI_AIRCON1_ONE_SPACEはビット1のパルスの無い時間、MITSUBISHI_AIRCON1_ZERO_SPACEはビット0のパルスの無い時間に相当します。
ChaNさんの解説と微妙に時間が合わないのは、調整が入っているのかもしれません。
上記を参考にオーデリックのライブラリを作成してみました。現時点ではZIPインストールをせず、プロジェクトディレクトリに保存しているので、includeパスに注意が必要です。
odelic.h
/****************************************************************************/ /* お家のオーデリック製LED照明をなんとかするヘッダー */ /* Copyright (C) 2014 hamayan All Rights Reserved. */ /****************************************************************************/ #ifndef odelic_h #define odelic_h #include <Arduino.h> #include <HeatpumpIR.h> extern "C" { // #include <mul_tsk.h> } /****************************************************************************/ /* なにかの定義 */ /****************************************************************************/ #define NEC_T_PERIOD 562UL // 562us #define NEC_REPEAT_PERIOD 120UL // 108ms #define ODELIC_CHANNEL_1 0 #define ODELIC_CHANNEL_2 1 #define ODELIC_CHANNEL_3 2 typedef struct /*オーデリックのフォーマットはNECタイプ*/ { unsigned char customerCode[2]; unsigned char data[2]; } ODELIC_CEILING_LIGHT_CODE; class odelic : public HeatpumpIR { private: uint8_t channel; uint32_t startTim; void send( IRSender& IR, uint8_t *code ); public: odelic( uint8_t ch = 0 ); void onOff( IRSender& IR, bool onoff ); void coldLight( IRSender& IR, int value ); void warmLight( IRSender& IR, int value ); char type( void ); }; #endif /*odelic_h*/ /****************************************************************************/ /* Copyright (C) 2014 hamayan All Rights Reserved. */ /****************************************************************************/
odelic.cpp
/*************************************************************************/ /* お家のオーデリック製LED照明をなんとかするソース */ /* designed by hamayan since 2015/05/28 */ /*************************************************************************/ #include "odelic.h" /*************************************************************************/ /* 大域変数宣言 */ /*************************************************************************/ /*調光を行う時は連続して2つのコードを送信する。つまり寒色と暖色の両方のLEDを制御している*/ /*また、輝度毎にコードが異なる。それ以外に3byte目と4byte目は反転の関係となる*/ static const unsigned char on[] = { 0x84, 0x51, 0x9a ,0x65 }; /*ch2: 0x84, 0x51, 0x9b,0x64*/ static const unsigned char off[] = { 0x84, 0x51, 0x08 ,0xf7 }; /*ch2: 0x84, 0x51, 0x09,0xf6*/ static const unsigned char cold[][4] = { { 0x84, 0x51, 0x6c ,0x93 }, /*10% ch2: 0x84, 0x51, 0x6d,0x92*/ { 0x84, 0x51, 0x6e ,0x91 }, /*20% ch2: 0x84, 0x51, 0x6f,0x90*/ { 0x84, 0x51, 0x70 ,0x8f }, /*30% ch2: 0x84, 0x51, 0x71,0x8e*/ { 0x84, 0x51, 0x72 ,0x8d }, /*40% ch2: 0x84, 0x51, 0x73,0x8c*/ { 0x84, 0x51, 0x74 ,0x8b }, /*50% ch2: 0x84, 0x51, 0x75,0x8a*/ { 0x84, 0x51, 0x76 ,0x89 }, /*60% ch2: 0x84, 0x51, 0x77,0x88*/ { 0x84, 0x51, 0x78 ,0x87 }, /*70% ch2: 0x84, 0x51, 0x79,0x86*/ { 0x84, 0x51, 0x7a ,0x85 }, /*80% ch2: 0x84, 0x51, 0x7b,0x84*/ { 0x84, 0x51, 0x7c ,0x83 }, /*90% ch2: 0x84, 0x51, 0x7d,0x82*/ { 0x84, 0x51, 0x7e ,0x81 }, /*100% ch2: 0x84, 0x51, 0x7f,0x80*/ }; static const unsigned char warm[][4] = { { 0x84, 0x51, 0x58 ,0xa7 }, /*10% ch2: 0x84, 0x51, 0x59,0xa6*/ { 0x84, 0x51, 0x5a ,0xa5 }, /*20% ch2: 0x84, 0x51, 0x5b,0xa4*/ { 0x84, 0x51, 0x5c ,0xa3 }, /*30% ch2: 0x84, 0x51, 0x5d,0xa2*/ { 0x84, 0x51, 0x5e ,0xa1 }, /*40% ch2: 0x84, 0x51, 0x5f,0xa0*/ { 0x84, 0x51, 0x60 ,0x9f }, /*50% ch2: 0x84, 0x51, 0x61,0x9e*/ { 0x84, 0x51, 0x62 ,0x9d }, /*60% ch2: 0x84, 0x51, 0x63,0x9c*/ { 0x84, 0x51, 0x64 ,0x9b }, /*70% ch2: 0x84, 0x51, 0x65,0x9a*/ { 0x84, 0x51, 0x66 ,0x99 }, /*80% ch2: 0x84, 0x51, 0x67,0x98*/ { 0x84, 0x51, 0x68 ,0x97 }, /*90% ch2: 0x84, 0x51, 0x69,0x96*/ { 0x84, 0x51, 0x6a ,0x95 }, /*100% ch2: 0x84, 0x51, 0x6b,0x94*/ }; /*************************************************************************/ /* プロトタイプ宣言 */ /*************************************************************************/ /*************************************************************************/ /* インスタンス */ /*************************************************************************/ odelic::odelic( uint8_t ch ) { channel = ch; startTim = millis(); } /*************************************************************************/ /* send */ /*************************************************************************/ void odelic::send( IRSender& IR, uint8_t *code ) { uint8_t tempUC[4]; memcpy( tempUC, code, sizeof(tempUC) ); tempUC[2] = tempUC[2] + channel; tempUC[3] = tempUC[3] - channel; // 40 kHz PWM frequency IR.setFrequency( 38 ); //リピートでメソッドが呼ばれた時、強制的に間隔を開ける while( (millis() - startTim) < NEC_REPEAT_PERIOD ) delay( 10 ); // start time startTim = millis(); // Header IR.mark( 16 * NEC_T_PERIOD ); //frame ppm on IR.space( 8 * NEC_T_PERIOD ); //frame ppm off // Data for( unsigned int i = 0; i < sizeof( tempUC ); i++ ) { IR.sendIRbyte( tempUC[i], NEC_T_PERIOD, NEC_T_PERIOD, 3 * NEC_T_PERIOD ); // data,562us,562us,1686us // pulse on period, pulse off period at ZERO, pulse off period at 1 } // End mark IR.mark( NEC_T_PERIOD ); IR.space( 0 ); } /*************************************************************************/ /* 引数valueは2以上で100以下の数字 */ /*************************************************************************/ void odelic::coldLight( IRSender& IR, int value ) { if( value >= 2 && value <= 100 ) { value--; value /= 10; send( IR, (uint8_t *)cold[ value ] ); } } /*************************************************************************/ /* 引数valueは2以上で100以下の数字 */ /*************************************************************************/ void odelic::warmLight( IRSender& IR, int value ) { if( value >= 2 && value <= 100 ) { value--; value /= 10; send( IR, (uint8_t *)warm[ value ] ); } } /*************************************************************************/ /* 引数valueは0または1またはそれ以上で100以下の数字 */ /*************************************************************************/ void odelic::onOff( IRSender& IR, bool onoff ) { if( onoff ) send( IR, (uint8_t *)on ); else send( IR, (uint8_t *)off ); } /*************************************************************************/ /* リモコンフォーマットを答える */ /*************************************************************************/ char odelic::type( void ) { return 'N'; /*NECのN!*/ } /*************************************************************************/ /* end of file */ /* designed by hamayan since 2015/05/28 */ /*************************************************************************/
スケッチ側はこんなんなります。
#include "odelic.h" #define IR_CONT 12 // Ir control IRSenderBitBang irSender( IR_CONT ); odelic *room1 = new odelic( ODELIC_CHANNEL_1 ); /*************************************************************************/ /* blynk virtual pin 2 function. */ /*************************************************************************/ BLYNK_WRITE( V2 ) { int value = param.asInt(); if( value ) { room1->onOff( irSender, true ); Blynk.virtualWrite( V4, 100 ); Blynk.virtualWrite( V5, 100 ); } else { room1->onOff( irSender, false ); Blynk.virtualWrite( V4, 0 ); Blynk.virtualWrite( V5, 0 ); } } /*************************************************************************/ /* blynk virtual pin 3 function. */ /*************************************************************************/ BLYNK_WRITE( V3 ) { room1->coldLight( irSender, 70 ); room1->warmLight( irSender, 70 ); Blynk.virtualWrite( V4, 70 ); Blynk.virtualWrite( V5, 70 ); } /*************************************************************************/ /* blynk virtual pin 4 function. */ /*************************************************************************/ BLYNK_WRITE( V4 ) { int value = param.asInt(); room1->warmLight( irSender, value ); } /*************************************************************************/ /* blynk virtual pin 5 function. */ /*************************************************************************/ BLYNK_WRITE( V5 ) { int value = param.asInt(); room1->coldLight( irSender, value ); }
オーデリックの天井照明は、隣り合う部屋でリモコンを使った時に混信しないように、3チャネルまで設定できます。
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~14畳 SH8234LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
Arduinoライブラリでエアコン(HEAT PUMP)を制御 家庭でできるIoT! 2パルス目 [ESP32]
誰も期待していないでしょうけれど、Blynkで動かす。
いろいろな都合でエアコンは三菱に変更している
ChaNさんのリモコンコードの解説ページ、、、httpsではない!
http://elm-chan.org/docs/ir_format.html
いろいろな都合でエアコンは三菱に変更している
/* ダイキン,,,もとい三菱のエアコンの制御サンプル 以下のライブラリを利用する。 Arduinoのライブラリマネージャーからもインストールできる。 https://github.com/ToniA/arduino-heatpumpir */ //#include <DaikinHeatpumpIR.h> #include <MitsubishiHeatpumpIR.h> #include <ESP8266WiFi.h> #include <BlynkSimpleEsp8266.h> /* Blynk library */ #include "authentication.h" /* 認証用ファイル */ /*************************************************************************/ /* defines */ /*************************************************************************/ #define IR_CONT 12 // Ir control /*************************************************************************/ /* instances */ /*************************************************************************/ IRSenderBitBang irSender( IR_CONT ); /* 赤外線送信用インスタンスの生成。引数は赤外線LEDが接続されているピン番号 */ //HeatpumpIR *airCon = new DaikinHeatpumpIR(); /* ダイキンのエアコンのインスタンスの生成 */ HeatpumpIR *airCon = new MitsubishiFDHeatpumpIR(); /* 三菱のエアコンのインスタンス */ IPAddress myIP; // self IP address // Attach virtual serial terminal to Virtual Pin V1 WidgetTerminal terminal(V1); /*************************************************************************/ /* global variables */ /*************************************************************************/ /*wifi AP informations*/ const struct ACCESS_POINT_LIST { const char *ssid; const char *pass; } ap[] = { { SSID_STATION_HOME, PASSWORD_STATION_HOME }, { SSID_STATION, PASSWORD_STATION }, }; char blynkAuth[] = BLYNK_AUTH; /***************************************************************************/ /* associate to wifi AP */ /***************************************************************************/ int associateToAP( const char *id, const char *ps ) { WiFi.begin( id, ps ); int i = 0; while( WiFi.status() != WL_CONNECTED ) /*だいたい8秒から10秒くらい接続に掛かっている。*/ { delay( 100 ); Serial.print( "." ); if(++i >= (15000 / 100)) /*15秒で諦める*/ { return (-1); //end of routine ESP.reset(); //ESP.deepSleep(1 * 1000 * 1000UL, WAKE_RF_DEFAULT ); //old code was WAKE_RF_DISABLED delay( 100 ); } } return i; } /*************************************************************************/ /* setup */ /*************************************************************************/ void setup() { Serial.begin( 115200 ); Serial.println(); pinMode( IR_CONT, OUTPUT ); digitalWrite( IR_CONT, LOW ); /*アクセスポイントへ接続*/ Serial.println( "connect to AP." ); if( associateToAP( ap[ 0 ].ssid, ap[ 0 ].pass ) < 0 ) { /*接続に失敗した時、リストから検索して接続してみる*/ int i; for( i = 0; i < sizeof(ap) / sizeof(ap[0]); i++ ) { int ret = associateToAP( ap[i].ssid, ap[i].pass ); if( ret >= 0 ) { Serial.print( "\r\n AP connect time = " ); Serial.print( ret * 100, DEC ); Serial.println( "ms" ); break; } } if( i == sizeof(ap) / sizeof(ap[0]) ) /*接続に失敗した場合*/ { ESP.reset(); //ESP.deepSleep( 1 * 1000 * 1000UL, WAKE_RF_DEFAULT ); //old code was WAKE_RF_DISABLED delay(100); } } myIP = WiFi.localIP(); Serial.print( "\r\nIP number assigned by DHCP is " ); Serial.println( myIP ); /* initialize blynk. */ // Blynk.begin( blynkAuth, ssid, pass ); Blynk.config( blynkAuth ); } /*************************************************************************/ /* loop */ /*************************************************************************/ void loop() { Blynk.run(); /* executing blynk. */ } /*************************************************************************/ /* blynk virtual pin 0 function. */ /*************************************************************************/ BLYNK_WRITE( V0 ) { int value = param.asInt(); if( value ) { /* ir instance,power,mode,fan,temperature,swing vertical,swing horizontal */ airCon->send( irSender, POWER_ON, MODE_AUTO, FAN_AUTO, 24, VDIR_DOWN, HDIR_AUTO ); /* power on */ terminal.println( "power on." ); } else { /* ir instance,power,mode,fan,temperature,swing vertical,swing horizontal */ airCon->send( irSender, POWER_OFF, MODE_AUTO, FAN_AUTO, 24, VDIR_DOWN, HDIR_AUTO ); /* power off */ terminal.println( "power off." ); } terminal.flush(); }
ChaNさんのリモコンコードの解説ページ、、、httpsではない!
http://elm-chan.org/docs/ir_format.html
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~12畳 SH8237LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
オーデリック LEDシーリングライト リモコン付き LED一体型 電球色~昼光色 調光・調色タイプ ~14畳 SH8234LDR
- 出版社/メーカー: オーデリック
- メディア: ホーム&キッチン
Arduinoライブラリでエアコン(HEAT PUMP)を制御 家庭でできるIoT! 1パルス目 [ESP32]
寒くなってきたので、エアコンを制御してみましょう!と言う話です。
カテゴリはESP32ですが、使っているのはESP8266です。あまり変わりませんが!
さて、とっくに寒くなってるじゃんね~!って感じですが、昔からやっているネタの再発掘。
エアコン、業務用とかではなければ赤外線リモコンが付属していると思います。
このエアコンの赤外線制御は結構面倒くさい感じでして、
1.メーカーがリモコンコードの仕様を出さない
2.コード長が100bitを超える
など自分の家のエアコンのコードを解析するのも一苦労です。
んが、世の中には地道にコード解析、ライブライを提供してくれる人もおるんですね!
ここで公開されているライブラリを利用しています。
https://github.com/ToniA/arduino-heatpumpir
HEATPUMPで検索すれば、Arduino IDEからライブラリ管理でインストールできます。
また、ライブラリ管理からIRremoteESP8266もインストールしておきます。多分、、、
ソースコードが有るので、Arduino以外の環境への移植も可能な気がします。
ダイキンのエアコンの電源入り切りサンプルです。
ESP8266の12ピンに赤外線LEDを接続しました。12ピン側がアノード、カソード側は100Ωの抵抗を通してGNDに接続しています。
赤外線が出ているかどうかは見れば、、、いえ目で見ても判らないので、スマフォのカメラを通してみればなんとなく判りますね。
/* ダイキンのエアコンの制御サンプル 以下のライブラリを利用する。 Arduinoのライブラリマネージャーからもインストールできる。 https://github.com/ToniA/arduino-heatpumpir */ #include <DaikinHeatpumpIR.h> /*************************************************************************/ /* defines */ /*************************************************************************/ #define IR_CONT 12 // Ir control /*************************************************************************/ /* instances */ /*************************************************************************/ IRSenderBitBang irSender( IR_CONT ); /* 赤外線送信用インスタンスの生成。引数は赤外線LEDが接続されているピン番号 */ HeatpumpIR *airCon = new DaikinHeatpumpIR(); /* ダイキンのエアコンのインスタンスの生成 */ /*************************************************************************/ /* setup */ /*************************************************************************/ void setup() { Serial.begin( 115200 ); pinMode( IR_CONT, OUTPUT ); digitalWrite( IR_CONT, LOW ); while ( 1 ) { /* ir instance,power,mode,fan,temperature,swing vertical,swing horizontal */ airCon->send( irSender, POWER_ON, MODE_HEAT, FAN_3, 24, VDIR_DOWN, HDIR_AUTO ); /* power on */ delay( 30 * 1000UL ); airCon->send( irSender, POWER_OFF, MODE_HEAT, FAN_3, 24, VDIR_DOWN, HDIR_AUTO ); /* power off */ break; delay( 10 * 1000UL ); } } /*************************************************************************/ /* loop */ /*************************************************************************/ void loop() { }
ライブラリを組み込んでみればエアコンの制御は非常に簡単だと思います。つくづくライブラリ提供者に感謝です。
自分の家のエアコンにどのリモコンコードを適用すればイイのか?は、ライブラリの作例にsimpleが有り、これを実行すれば判ります。
ライブラリの使い方が判ってしまえば、これをBlynkで制御!とか余裕ですね!
M5Stackとフォント 漢字フォントの表示 UTF8への対応 #m5stack [ESP32]
おおむね成功ですっ!ちぃ、、ですっ!
さて前回は16×16ドットのフォントでJIS第2水準程度の日本語を表示する。ただし表示できるのはShift-JISコード!と言う内容でしたが、今回はUTF8まで対応してみます。
UTF8でJIS第2水準程度をサポートしていれば、日本語の表示ではそれほど苦労しないで済みそうですね。
前回書いた様にここに有るのはShift-JISに対応した漢字のフォントデータ。しかし対応しなければならないのはUTFのコード。
ならばUTFのコードをShift-JISに変換するしかない!
と言う訳で、ここではその手順を書いてみます。ちなみにこれがベストの方法だとは思いませんが、このブログは基本的に備忘録なので。
1.FONTX形式のファイルのShift-JISコードだけの(つまりここではビットマップデータは必要無い)リストを作る。※PCで作業
2.上記リストのコードを16進表記した値(文字列)と、そのコードをバイナリー化した値をレコードとしたファイルを生成する。
以下の様なファイルですね。これはShift-JISのファイルとなります。このファイルをエディタで開くとバイナリー化した値は文字として読めます。※PCで作業
3.このファイルをエディタの機能を使ってUTF8に変換する。以下の様になりますね。見た目はまったく変わりませんが(笑)、漢字はUTF8のコードに直っています。つまり1行毎に16進表記のShift-JISのコードとUTF8のコードの対応が取れました。※PCで作業
4.上記ファイルのUTF8のバイナリーデータを16進表記に変換して、構造体の配列としてCソースファイルを生成します。この時検索性を良くするためにUTF8のバイナリーデータを昇順で配列の並べ替えを行って置きます。
以下の様なファイルを作ります。この場合UTF8のコードは4byte長としています。UTF8のコードは最長で6byteとなるようですが、今回変換した漢字コードは最大でも3byteしか使っていませんでしたので、4byte長で充分だと思います。目的は手持ちのShift-JISコードのフォントデータを利用するだけですから。※PCで作業
5.上記変換データを収めたCソースファイルをプロジェクトに取り込む。
6.文字列を先頭から1byte単位で読み出し、最上位bitが立っていれば漢字、立っていなければASCII文字として処理します。※マイコンで作業
7.漢字であれば上位bitのパターンから後ろに何byte続くか判断し、それらを32bit長の変数に代入し、その値をキーに変換テーブルから該当するShift-JISコードを取得します。※マイコンで作業
8.後は前回の漢字表示処理を行うだけです。※マイコンで作業
プロジェクト一式
※今回の漢字をマイコンで表示させる手順としては、別にM5stackに限らず様々なマイコンで同じ方法が使えると思います。
※しかしフォントデータと変換データを合わせて、かなりの量のROM容量が必要ですね!
参考
ウィキペディア https://ja.wikipedia.org/wiki/UTF-8
今日もスミマセン。
http://d.hatena.ne.jp/snaka72/20100710/SUMMARY_ABOUT_JAPANESE_CHARACTER_CODE
さて前回は16×16ドットのフォントでJIS第2水準程度の日本語を表示する。ただし表示できるのはShift-JISコード!と言う内容でしたが、今回はUTF8まで対応してみます。
UTF8でJIS第2水準程度をサポートしていれば、日本語の表示ではそれほど苦労しないで済みそうですね。
前回書いた様にここに有るのはShift-JISに対応した漢字のフォントデータ。しかし対応しなければならないのはUTFのコード。
ならばUTFのコードをShift-JISに変換するしかない!
と言う訳で、ここではその手順を書いてみます。ちなみにこれがベストの方法だとは思いませんが、このブログは基本的に備忘録なので。
1.FONTX形式のファイルのShift-JISコードだけの(つまりここではビットマップデータは必要無い)リストを作る。※PCで作業
2.上記リストのコードを16進表記した値(文字列)と、そのコードをバイナリー化した値をレコードとしたファイルを生成する。
以下の様なファイルですね。これはShift-JISのファイルとなります。このファイルをエディタで開くとバイナリー化した値は文字として読めます。※PCで作業
0x889F,亜; // 亜 0x88A0,唖; // 唖 0x88A1,娃; // 娃 0x88A2,阿; // 阿 0x88A3,哀; // 哀
3.このファイルをエディタの機能を使ってUTF8に変換する。以下の様になりますね。見た目はまったく変わりませんが(笑)、漢字はUTF8のコードに直っています。つまり1行毎に16進表記のShift-JISのコードとUTF8のコードの対応が取れました。※PCで作業
0x889F,亜; // 亜 0x88A0,唖; // 唖 0x88A1,娃; // 娃 0x88A2,阿; // 阿 0x88A3,哀; // 哀
4.上記ファイルのUTF8のバイナリーデータを16進表記に変換して、構造体の配列としてCソースファイルを生成します。この時検索性を良くするためにUTF8のバイナリーデータを昇順で配列の並べ替えを行って置きます。
以下の様なファイルを作ります。この場合UTF8のコードは4byte長としています。UTF8のコードは最長で6byteとなるようですが、今回変換した漢字コードは最大でも3byteしか使っていませんでしたので、4byte長で充分だと思います。目的は手持ちのShift-JISコードのフォントデータを利用するだけですから。※PCで作業
const struct SJIS_UTF8_TABLE { unsigned short sjis; unsigned long utf8; } sjis_utf8_table[] = { {0x8198,0x0000C2A7}, {0x814E,0x0000C2A8}, ・ ・ ・ {0xFA55,0x00EFBFA4}, {0x818F,0x00EFBFA5}, }; const int sjis_utf8_table_number = 8127;
5.上記変換データを収めたCソースファイルをプロジェクトに取り込む。
6.文字列を先頭から1byte単位で読み出し、最上位bitが立っていれば漢字、立っていなければASCII文字として処理します。※マイコンで作業
7.漢字であれば上位bitのパターンから後ろに何byte続くか判断し、それらを32bit長の変数に代入し、その値をキーに変換テーブルから該当するShift-JISコードを取得します。※マイコンで作業
8.後は前回の漢字表示処理を行うだけです。※マイコンで作業
プロジェクト一式
※今回の漢字をマイコンで表示させる手順としては、別にM5stackに限らず様々なマイコンで同じ方法が使えると思います。
※しかしフォントデータと変換データを合わせて、かなりの量のROM容量が必要ですね!
参考
ウィキペディア https://ja.wikipedia.org/wiki/UTF-8
今日もスミマセン。
http://d.hatena.ne.jp/snaka72/20100710/SUMMARY_ABOUT_JAPANESE_CHARACTER_CODE
M5Stackとフォント 漢字フォントの対応 [ESP32]
FONTX形式の漢字フォントの表示。但しShift-JIS配列で漢字フォントデータが構築されているので、UTFでは表示できない!
FONTX形式の解説はCHAN氏がされているので、そこを参考にしてね。
http://elm-chan.org/docs/dosv/fontx.html
このブログでも過去記事か有ります。
https://hamayan.blog.so-net.ne.jp/2009-09-19-1
※本当はもっと詳しく書いた記事が有ったのですが、失われた、、、
今回使用しているFONTX形式のフォントデータは力武健次氏が公開されている物を利用させていただきました。もうだいぶ昔の事なので改めて氏のリンク先を探したら、以下のページが有りました。
http://www.k2r.org/gijyutsushi
なぜFONTX形式?
実際のところM5Stackが流行るずいぶん前から組込みで液晶に日本語を表示する事は広く行われてきました。その液晶表示する時に比較的扱い易いフォントデータ形式としてFONTX形式がよく使われています。他にもBDF形式とかですね。
なので、「FONTX形式」で検索してもらえれば、沢山の利用例が見つかります。
なぜShift-JIS?
上記の日本語表示の流行の頃は日本語のコード体系ではShift-JISが一般的だったのです。今でもShift-JIS配列のFree fontを多く見つけられるでしょう。
例えば16×16ドットのフォントで第2水準までサポートした場合のROM容量は250kbyteくらいです。昔のマイコンならいざ知らず、ESP32とかなら組み込んでも余裕ですね!
但し!
現在主流のコードはUTFです。Arduino IDEで日本語を書いた時、そのコードはUTFになっています。UTFのコードをそのままShift-JISのコードに変換できず、これが結構面倒です。組み込みには厳しい状況です。
同じ日本語( 覇漏倭亞琉弩、夜露死苦 )をShift-JISとUTF-8で書いた物をバイナリーエディターで開いた物が以下です。
上がShift-JIS、下がUTF-8N
Shift-JISの文字コードは16bit固定長ですが、UTFでは拡張性?からか8bit~の可変長です。なのでこの場合はデータ量が増えていますね。
さて、UTFのコードからShift-JISのフォントデータにアクセスする方法、どうしましょう?
FONTX形式の解説はCHAN氏がされているので、そこを参考にしてね。
http://elm-chan.org/docs/dosv/fontx.html
このブログでも過去記事か有ります。
https://hamayan.blog.so-net.ne.jp/2009-09-19-1
※本当はもっと詳しく書いた記事が有ったのですが、失われた、、、
今回使用しているFONTX形式のフォントデータは力武健次氏が公開されている物を利用させていただきました。もうだいぶ昔の事なので改めて氏のリンク先を探したら、以下のページが有りました。
http://www.k2r.org/gijyutsushi
なぜFONTX形式?
実際のところM5Stackが流行るずいぶん前から組込みで液晶に日本語を表示する事は広く行われてきました。その液晶表示する時に比較的扱い易いフォントデータ形式としてFONTX形式がよく使われています。他にもBDF形式とかですね。
なので、「FONTX形式」で検索してもらえれば、沢山の利用例が見つかります。
なぜShift-JIS?
上記の日本語表示の流行の頃は日本語のコード体系ではShift-JISが一般的だったのです。今でもShift-JIS配列のFree fontを多く見つけられるでしょう。
例えば16×16ドットのフォントで第2水準までサポートした場合のROM容量は250kbyteくらいです。昔のマイコンならいざ知らず、ESP32とかなら組み込んでも余裕ですね!
但し!
現在主流のコードはUTFです。Arduino IDEで日本語を書いた時、そのコードはUTFになっています。UTFのコードをそのままShift-JISのコードに変換できず、これが結構面倒です。組み込みには厳しい状況です。
同じ日本語( 覇漏倭亞琉弩、夜露死苦 )をShift-JISとUTF-8で書いた物をバイナリーエディターで開いた物が以下です。
上がShift-JIS、下がUTF-8N
Shift-JISの文字コードは16bit固定長ですが、UTFでは拡張性?からか8bit~の可変長です。なのでこの場合はデータ量が増えていますね。
さて、UTFのコードからShift-JISのフォントデータにアクセスする方法、どうしましょう?
M5Stackとフォント GFXFFへの対応 [ESP32]
前回ではランレングスに対応しました。今回はGFX Free Fontと言うフォントの対応です。
GFX Free Fontもプロポーショナルフォントですが、特に筆記体を表示するのに向いている様な気がします。
マイコンにとって最も処理の負荷が小さそうなのは等幅フォントですが、等幅フォントでは例えば小文字の'i'が連続した時に、'i'と'i'の間が大きく開いて体裁が良くありません。
プロポーショナルフォントなら文字毎に適切な文字間隔を指定できるので、とても体裁が良くなります。しかし制御が増えるのでその点は良し悪しです。
ランレングスはSPIとは相性が良さそうな気がしますね。
GFXフォントの使い方の解説(本家?)はここから。
https://learn.adafruit.com/adafruit-gfx-graphics-library/using-fonts
Customフォルダーの中のフォントは以下のサイトで生成された物?
http://oleddisplay.squix.ch/#/home
GFX Free Font(GFXFF)を使う為のもっとも簡単な方法は、すでにM5ライブラリに用意されているメソッドを使う事です。
例えば
setFreeFont( GFXFFにアクセスする為の構造体のポインタ );でこれから使用するフォントを選択。
drawString( テキスト, 横位置, 縦位置, フォント番号 );で指定された位置に先に指定されたフォントでテキストを描画します。まあつまりこんな感じで、
これらメソッドはM5Stack.hには記載されていませんが、M5Display.hの中でTFT_eSPIクラスが継承されているのでIn_eSPI.hを参考にすると良いでしょう。
しかし相変わらずこのメソッドでは画面の右端に行った時に折り返しをしてくれません、、、よね?
折り返しとか改行とかを行いたいなら、自分で描画メソッドを作るしかない?
以下ではその解説を!
1.GFXフォーマットのフォントデータファイルは3つの要素で構成されている。
1) 文字をビットマップデータ化したデータ領域
Yelllowtail_32.hであれば
const uint8_t Yellowtail_32Bitmaps[] PROGMEM
配列名は任意でこのファイルの中でしか利用されないが、static宣言はされていない。
2) 該当文字の属性データ。例えば該当文字のデータが始まる位置を、上記ビットマップデータ
の先頭からのオフセットとか、文字の横サイズ、文字の縦サイズ、次の文字までのピッチ、
描画原点からの横と縦のオフセット等が配列化されている。
3) ビットマップデータ、上記属性データ、行間のピッチをまとめた物。
文字を表示する場合はこの変数をアクセスする事となる。
2.GFXフォーマット
gfxfont.hの中で2つの構造体が定義されている。
1) GFXglyph
Yelllowtail_32.hの中の文字'A'のGFXglyphレコード
{ 1141, 24, 24, 20, 1, -23 }, // 'A'
bitmapOffset : ビットマップデータ領域の先頭からのオフセット。この場合は1141。
width, height : 文字の横幅 = 24ピクセル。高さ = 24ピクセル。
xAdvance : 次の文字までの横方向のピッチ = 20
xOffset, yOffset : 文字の描画開始位置からの横方向のオフセット = 1。
文字の描画開始位置からの縦方向のオフセット = -23。
2) GFXfont
Yelllowtail_32.hの中のGFXfontレコード
const GFXfont Yellowtail_32 PROGMEM = {
(uint8_t *)Yellowtail_32Bitmaps,(GFXglyph *)Yellowtail_32Glyphs,0x20, 0x7D, 45};
bitmap : ビットマップデータ全体へのポインタ
glyph : GFXglyph構造体の配列のポインタ
first, last : 最初の文字、最後の文字。ASCIIコードからfirstを引いた値を
GFXglyph構造体配列のインデックスとする。
yAdvance : 次の行までの高さ。
3.GFXフォーマットの文字データの展開
該当文字のGFXglyph構造体レコードを取得すると、該当文字データの開始アドレスを
取得できる。
このデータはランレングス(RUN LENGTH)の様な圧縮は行われておらず、ビット列を
ビットマップとして画面上に展開する。しかしFont16.cの様な無圧縮のビットマップ
データをそのままメモリ領域に展開してしまうと文字サイズが大きい時は必要なメモリ
領域も大きくなってしまうので文字の描画開始位置を指定する事でメモリサイズの
増大を防いでいる。
例えば'A'であれば文字の上の辺りから描画開始しなければならないが、'.'であれば
随分下の位置からの描画開始となる。
以下はYelllowtail_32.hの文字'A'と文字'.'のデータである。
文字'A'のビットマップデータ
0x00,0x00,0x1E,0x00, 0x00,0x3F,0x00,0x00,
0x7E,0x00,0x01,0xEE, 0x00,0x03,0xDC,0x00,
0x07,0x9C,0x00,0x0F, 0x38,0x00,0x1E,0x38,
0x00,0x1C,0x78,0x00, 0x38,0x70,0x00,0x70,
0x70,0x00,0xE0,0xE0, 0x01,0xE0,0xE0,0x03,
0xC1,0xC0,0x7F,0xFF, 0xC0,0x7F,0xFF,0x80,
0x0E,0x03,0x80,0x1C, 0x07,0x00,0x38,0x07,
0x00,0x78,0x0F,0x00, 0x70,0x0E,0x00,0xE0,
0x1E,0x00,0xE0,0x1C, 0x00,0xC0,0x38,0x00, // 'A'
でかい!
文字'.'のビットマップデータ
0x7F,0xE0, // '.'
文字によって横幅、高さ、描画開始位置等の属性が異なり、文字を描画する為には
その情報にアクセスしなければならない。
文字'A'の属性データ
{ 1141, 24, 24, 20, 1, -23 }, // 'A'
文字'.'の属性データ
{ 431, 4, 3, 9, 2, -2 }, // '.'
文字'A'のxOffsetは1、yOffsetは-23。
文字'.'のxOffsetは2、yOffsetは-2。
xOffsetは文字開始位置からの相対位置、yOffsetはyAdvanceからの相対位置となる。
基本的にM5stackの描画は左上から右下方向に開始され、Xが大きくなれば右に、
Yが大きくなれば下に進む。
それぞれの文字の描画開始位置は横方向ならxOffsetから、
縦方向なら yAdvance + yOffset(注1)から開始する。Yelllowtail_32.hのyAdvanceは45である。
お試しプログラムは以下のリンク
https://1drv.ms/u/s!AgxfaDqma1yrhk6AY-NUsBh7pyXb
(注1) とか言いつつソースには高さ方向で調整が入っているじゃあないかい!
※TFT_eSPIクラスにはreadPixelメソッドが存在するので、ピクセルの読み出し可能か?と期待しましたが、見事にFFFFが返ってくるorz
GFX Free Fontもプロポーショナルフォントですが、特に筆記体を表示するのに向いている様な気がします。
マイコンにとって最も処理の負荷が小さそうなのは等幅フォントですが、等幅フォントでは例えば小文字の'i'が連続した時に、'i'と'i'の間が大きく開いて体裁が良くありません。
プロポーショナルフォントなら文字毎に適切な文字間隔を指定できるので、とても体裁が良くなります。しかし制御が増えるのでその点は良し悪しです。
ランレングスはSPIとは相性が良さそうな気がしますね。
GFXフォントの使い方の解説(本家?)はここから。
https://learn.adafruit.com/adafruit-gfx-graphics-library/using-fonts
Customフォルダーの中のフォントは以下のサイトで生成された物?
http://oleddisplay.squix.ch/#/home
GFX Free Font(GFXFF)を使う為のもっとも簡単な方法は、すでにM5ライブラリに用意されているメソッドを使う事です。
例えば
setFreeFont( GFXFFにアクセスする為の構造体のポインタ );でこれから使用するフォントを選択。
drawString( テキスト, 横位置, 縦位置, フォント番号 );で指定された位置に先に指定されたフォントでテキストを描画します。まあつまりこんな感じで、
M5.Lcd.setFreeFont( &Yellowtail_32 ); // Select the font M5.Lcd.drawString( "hello world.", 0, 0, 1 );
これらメソッドはM5Stack.hには記載されていませんが、M5Display.hの中でTFT_eSPIクラスが継承されているのでIn_eSPI.hを参考にすると良いでしょう。
しかし相変わらずこのメソッドでは画面の右端に行った時に折り返しをしてくれません、、、よね?
折り返しとか改行とかを行いたいなら、自分で描画メソッドを作るしかない?
以下ではその解説を!
1.GFXフォーマットのフォントデータファイルは3つの要素で構成されている。
1) 文字をビットマップデータ化したデータ領域
Yelllowtail_32.hであれば
const uint8_t Yellowtail_32Bitmaps[] PROGMEM
配列名は任意でこのファイルの中でしか利用されないが、static宣言はされていない。
2) 該当文字の属性データ。例えば該当文字のデータが始まる位置を、上記ビットマップデータ
の先頭からのオフセットとか、文字の横サイズ、文字の縦サイズ、次の文字までのピッチ、
描画原点からの横と縦のオフセット等が配列化されている。
3) ビットマップデータ、上記属性データ、行間のピッチをまとめた物。
文字を表示する場合はこの変数をアクセスする事となる。
2.GFXフォーマット
gfxfont.hの中で2つの構造体が定義されている。
1) GFXglyph
typedef struct { // Data stored PER GLYPH uint16_t bitmapOffset; // Pointer into GFXfont->bitmap uint8_t width, height; // Bitmap dimensions in pixels uint8_t xAdvance; // Distance to advance cursor (x axis) int8_t xOffset, yOffset; // Dist from cursor pos to UL corner } GFXglyph;
Yelllowtail_32.hの中の文字'A'のGFXglyphレコード
{ 1141, 24, 24, 20, 1, -23 }, // 'A'
bitmapOffset : ビットマップデータ領域の先頭からのオフセット。この場合は1141。
width, height : 文字の横幅 = 24ピクセル。高さ = 24ピクセル。
xAdvance : 次の文字までの横方向のピッチ = 20
xOffset, yOffset : 文字の描画開始位置からの横方向のオフセット = 1。
文字の描画開始位置からの縦方向のオフセット = -23。
2) GFXfont
typedef struct { // Data stored for FONT AS A WHOLE: uint8_t *bitmap; // Glyph bitmaps, concatenated GFXglyph *glyph; // Glyph array uint8_t first, last; // ASCII extents uint8_t yAdvance; // Newline distance (y axis) } GFXfont;
Yelllowtail_32.hの中のGFXfontレコード
const GFXfont Yellowtail_32 PROGMEM = {
(uint8_t *)Yellowtail_32Bitmaps,(GFXglyph *)Yellowtail_32Glyphs,0x20, 0x7D, 45};
bitmap : ビットマップデータ全体へのポインタ
glyph : GFXglyph構造体の配列のポインタ
first, last : 最初の文字、最後の文字。ASCIIコードからfirstを引いた値を
GFXglyph構造体配列のインデックスとする。
yAdvance : 次の行までの高さ。
3.GFXフォーマットの文字データの展開
該当文字のGFXglyph構造体レコードを取得すると、該当文字データの開始アドレスを
取得できる。
このデータはランレングス(RUN LENGTH)の様な圧縮は行われておらず、ビット列を
ビットマップとして画面上に展開する。しかしFont16.cの様な無圧縮のビットマップ
データをそのままメモリ領域に展開してしまうと文字サイズが大きい時は必要なメモリ
領域も大きくなってしまうので文字の描画開始位置を指定する事でメモリサイズの
増大を防いでいる。
例えば'A'であれば文字の上の辺りから描画開始しなければならないが、'.'であれば
随分下の位置からの描画開始となる。
以下はYelllowtail_32.hの文字'A'と文字'.'のデータである。
文字'A'のビットマップデータ
0x00,0x00,0x1E,0x00, 0x00,0x3F,0x00,0x00,
0x7E,0x00,0x01,0xEE, 0x00,0x03,0xDC,0x00,
0x07,0x9C,0x00,0x0F, 0x38,0x00,0x1E,0x38,
0x00,0x1C,0x78,0x00, 0x38,0x70,0x00,0x70,
0x70,0x00,0xE0,0xE0, 0x01,0xE0,0xE0,0x03,
0xC1,0xC0,0x7F,0xFF, 0xC0,0x7F,0xFF,0x80,
0x0E,0x03,0x80,0x1C, 0x07,0x00,0x38,0x07,
0x00,0x78,0x0F,0x00, 0x70,0x0E,0x00,0xE0,
0x1E,0x00,0xE0,0x1C, 0x00,0xC0,0x38,0x00, // 'A'
でかい!
文字'.'のビットマップデータ
0x7F,0xE0, // '.'
文字によって横幅、高さ、描画開始位置等の属性が異なり、文字を描画する為には
その情報にアクセスしなければならない。
文字'A'の属性データ
{ 1141, 24, 24, 20, 1, -23 }, // 'A'
文字'.'の属性データ
{ 431, 4, 3, 9, 2, -2 }, // '.'
文字'A'のxOffsetは1、yOffsetは-23。
文字'.'のxOffsetは2、yOffsetは-2。
xOffsetは文字開始位置からの相対位置、yOffsetはyAdvanceからの相対位置となる。
基本的にM5stackの描画は左上から右下方向に開始され、Xが大きくなれば右に、
Yが大きくなれば下に進む。
それぞれの文字の描画開始位置は横方向ならxOffsetから、
縦方向なら yAdvance + yOffset(注1)から開始する。Yelllowtail_32.hのyAdvanceは45である。
お試しプログラムは以下のリンク
https://1drv.ms/u/s!AgxfaDqma1yrhk6AY-NUsBh7pyXb
(注1) とか言いつつソースには高さ方向で調整が入っているじゃあないかい!
※TFT_eSPIクラスにはreadPixelメソッドが存在するので、ピクセルの読み出し可能か?と期待しましたが、見事にFFFFが返ってくるorz