ESP32電子工作ファンのブログ

ESP32を使った電子工作の話題。その他備忘録など。

Yahoo!気象情報表示電光掲示板用にMDF材で筐体を作ってみる。

最近検討していること。それは、最近作成したYahoo!気象情報表示掲示板をリビングに飾るためにMDF材を使って筐体を作ってみたいということです。
どうしますかね。
とりあえず、ノートにラフスケッチを書いてみました。
f:id:riraosan:20190707231906j:plain

まず、LEDマトリクスを囲って見栄えを良くしようかなと目論んでいます。

いつ出来るかはよくわかりません。毎週土日に作業を行うだけですから、気楽に作っていこうと思っています。
今考えている仕様は以下の通り。
・横面は正方形
・縦面は長方形
・LEDマトリクスの全面には灰色の半透明のアクリル板をつけてLEDの輝度を若干下げてあげる。
・基板はMDF材にマウントする。

これぐらいですかね。
小さいブレッドボードを箱の裏側に配置することで、とりあえず、見栄えを良くしてブレッドボードは背面に隠すという構成にしたいと思っています。
ゆくゆくは専用の基板を設計して発注してよりスマートに設置したいなぁとも思っています。どうなることやら。
コツコツやっていきましょうか。

ESP32とLEDマトリックスを使ってYahoo!気象情報APIから降雨情報を取得して表示してみた

f:id:riraosan:20190622153705j:plain

何を作ってみた?

ESP32マイコンを使ってLEDマトリクスを制御してみました。
とりあえず、文字が表示出来たのでESP32の得意としているWifi経由でインターネットにつなげる機能を使ってなにか表示出来ることはないか考えてみました。
思いついたのが、Yahoo!気象情報APIを使った降雨予報の表示でした。
30分前に雨が降りそうとわかったら、洗濯物を取り込むことが出来ますよねー。
まず、作ってみて役に立つのか立たないのか見分けてみましょうか。(^^

仕様を最初から決めていたわけではないのですが、以下のようにしました。
・降雨予報を表示していない時は時刻を表示しておく。
・10分毎に降雨予報をLED電光掲示板に表示する
・取得した時刻時点の予報を表示する。
・降雨を予報する地点は郵便番号で設定する。
 →Yahoo!APIで郵便番号から緯度経度情報を取得できるので、その情報をYahoo!気象情報APIに食わせてやる。

動作の様子

youtu.be

開発環境

私のESP32マイコン開発環境をご紹介します。

MacBook Pro

若いときからWindowsに親しんできましたが、Macのフォントの美しさに惹かれて最近はMac使いです。
Mac上でマイコンの開発環境構築に苦労したことはありません。現在はMac用の各種開発環境は整っています。
Unix系OSに近いということでコマンドラインでGitを使って重宝してます。
Windows使いの人はもうMacに乗り換えても大丈夫ですよ。

VSCode

code.visualstudio.com

このエディタはインテリセンス機能が秀逸ですね。Gitとも連携していてコードの差分を表示してくれます。手放せませんね。

PlatformIO

platformio.org

純正のArduinoIDEを使うとインテリセンスが使えませんので、VSCodeのPlatformIOプラグインで環境を構築しました。
普通にArduinoのライブラリが使えるので開発環境として問題ありません。
有料になりますが、Unitテストフレームワークを使うことができます。30日トライアルした感じではかなり便利でした。
なるべくお金をかけたくないので、30日後はトライアル終了で、地道にデバッグしています。

ESPr Developer 32

ESPr Developer 32

ESPr Developer 32

色々調べた結果スイッチサイエンスさんのESPr Developer 32が安定?しているので採用しました。
DevKit-Cだとダウンロード後のリセットがかからない問題が報告されていました。こちらのボードではダウンロード後に自動的に再起動しない問題は一切発生していません。

ドットマトリクスLEDパネル

大阪日本橋の電子パーツ屋さんデジットで買った、赤/橙/緑 32×16ドット ドットマトリクスLEDパネルを使用しました。
eleshop.jp

資料はこちら
http://www.kyohritsu.jp/eclib/DIGIT/JNK/32x16dot0158.pdf

mgo-tecさんの東雲フォント取得ライブラリ。

こちらのmgo-tec記事に書かれている東雲フォント取得ライブラリを使わさせていただきました。ありがとうございました。
これらの記事の要約としては、東雲フォントをESP32のSPIFFS領域にコピーしてmgo-tecさん作成の東雲フォントロードライブラリを使ってOLEDにフォントを表示するというものです。
私は東雲フォントをLEDマトリクスに表示させた、ということになります。
www.mgo-tec.com
www.mgo-tec.com
www.mgo-tec.com

東雲フォントについて
www.mgo-tec.com

プログラムの大まかな構成

  • LEDマトリクス表示処理
  • 時計処理(時計タスク 優先度1←CPU1で動作)
  • Yahoo!天気情報取得処理(天気情報取得タスク 優先度2←CPU0で動作)

に大体分かれてます。LEDマトリクス表示処理は事実上のLEDマトリクスドライバなので、必要でしたら使ってみてください。
Arduinoのライブラリにしたいですね。まったく需要がないと思いますが(笑)
時計タスクと天気情報取得タスクは排他制御をしています。
常に時計タスクが動いていて、10分経過したかをチェックしています。10分経過したら天気情報取得タスクを起動させます。

Yahoo!APIを使って郵便番号からその地点の降雨予報を取得する方法

郵便番号検索APIと気象情報APIを使いました。

YOLP(地図):郵便番号検索API - Yahoo!デベロッパーネットワーク
YOLP(地図):気象情報API - Yahoo!デベロッパーネットワーク

  • サーバー"map.yahooapis.jp"に以下のリクエストを送ります。
GET /search/zip/V1/zipCodeSearch?query=xxx-xxxx&output=json HTTP/1.1
Host: map.yahooapis.jp
User-Agent: Yahoo AppID: <アプリケーションID>
Connection: close

xxx-xxxxは郵便番号です。output=jsonとしてレスポンスをJSON形式で取得します。

  • レスポンスをJSON形式で取得します。

以下のようなレスポンスがJSON形式で取得できます。これをArduinoJsonでパースして緯度経度情報を取得します。
"Coordinates":"135.44933744,34.53605758" の箇所を抽出します。

{"ResultInfo":{"Count":1,"Total":1,"Start":1,"Status":200,"Description":"","Copyright":"","Latency":0.009},"Feature":
[{"Id":"3a520ae595e3393bb6ccf8f1384a68ed","Gid":"","Name":"\u3012592-8344","Geometry":
{"Type":"point","Coordinates":"135.44933744,34.53605758"},"Category":
["\u90f5\u4fbf\u756a\u53f7","\u753a\u57df\u90f5\u4fbf\u756a\u53f7"],"Description":"Yahoo!\u90f5\u4fbf\u756a\u53f7\u691c\u7d22","Style":[],"Property":
{"Uid":"7e6a84bdaded39e942992aca4be9239797b82aa5","CassetteId":"3ee7f7f5fe1ef2267e319b15168e37d3","Country":
{"Code":"JP","Name":"\u65e5\u672c"},"Address":"\u5927\u962a\u5e9c\u583a\u5e02\u897f\u533a\u6d5c\u5bfa\u5357\u753a","GovernmentCode":"27144","AddressMatchingLevel":"6","PostalName":"\u5927\u962a\u5e9c\u583a\u5e02\u897f\u533a\u6d5c\u5bfa\u5357\u753a","Station":
[{"Id":"26132","SubId":"2613201","Name":"\u6d5c\u5bfa\u516c\u5712","Railway":"\u5357\u6d77\u96fb\u6c17\u9244\u9053","Exit":"\u6771\u51fa\u53e3","ExitId":"11600","Distance":"964","Time":"12","Geometry":{"Type":"point","Coordinates":"135.444616,34.540964"}},
{"Id":"26145","SubId":"2614501","Name":"\u6771\u7fbd\u8863","Railway":"JR\u5728\u6765\u7dda","Exit":"\u51fa\u53e3","ExitId":"11836","Distance":"1005","Time":"12","Geometry":{"Type":"point","Coordinates":"135.442421,34.535326"}},
{"Id":"26125","SubId":"2612501","Name":"\u7fbd\u8863","Railway":"\u5357\u6d77\u96fb\u6c17\u9244\u9053","Exit":"\u6771\u51fa\u53e3","ExitId":"11585","Distance":"1043","Time":"13","Geometry":{"Type":"point","Coordinates":"135.441921,34.534382"}}]}}]}
  • JSONデータから経度緯度文字列を取得します。
135.44933744,34.53605758

この経度、緯度はサンプルです。

  • 取得した経度緯度文字列を追加してYahoo!天気情報APIにリクエストを送信します。
GET /weather/V1/place?coordinates=135.44933744,34.53605758&output=json HTTP/1.1
Host: map.yahooapis.jp
User-Agent: Yahoo AppID: <アプリケーションID>
Connection: close

”135.44933744,34.53605758”はサンプルです。経度、緯度の順番に並んでいます。

以下の様なJSON形式のデータがレスポンスで返ってきます。
これをまたArduinoJsonでパースして、降雨情報を取得して何分後に雨が降るなどと判断します。
その判断によりLEDマトリクスにメッセージを表示させます。

{"ResultInfo":{"Count":1,"Total":1,"Start":1,"Status":200,"Latency":0.004173,"Description":"","Copyright":"(C) Yahoo Japan 
Corporation."},"Feature":[{"Id":"201906291810_135.44934_34.536058","Name":"地点(135.44934,34.536058)の2019年06月29日 18時10分から60分間の天気情報","Geometry":{"Type":"point","Coordinates":"135.44934,34.536058"},"Property":
{"WeatherAreaCode":6200,"WeatherList":{"Weather":[
{"Type":"observation","Date":"201906291810","Rainfall":0.65},
{"Type":"forecast","Date":"201906291820","Rainfall":0.00},
{"Type":"forecast","Date":"201906291830","Rainfall":0.00},
{"Type":"forecast","Date":"201906291840","Rainfall":0.55},
{"Type":"forecast","Date":"201906291850","Rainfall":0.65},
{"Type":"forecast","Date":"201906291900","Rainfall":1.85},
{"Type":"forecast","Date":"201906291910","Rainfall":0.00}]}}}]}

ソースコード

ソースコードを公開します。ご参考まで。

#include <Arduino.h>
#include <ESP32_SPIFFS_ShinonomeFNT.h>
#include <ESP32_SPIFFS_UTF8toSJIS.h>
#include <WiFiClientSecure.h>
#include <time.h>
#include <stdio.h>
#define ARDUINOJSON_DECODE_UNICODE 1
#include <ArduinoJson.h>

#define JST     3600* 9

//ポート設定
#define PORT_SE_IN 13
#define PORT_AB_IN 27
#define PORT_A3_IN 23
#define PORT_A2_IN 21
#define PORT_A1_IN 25
#define PORT_A0_IN 26
#define PORT_DG_IN 19
#define PORT_CLK_IN 18
#define PORT_WE_IN 17
#define PORT_DR_IN 16
#define PORT_ALE_IN 22

#define PANEL_NUM     2   //パネル枚数
#define R             1   //赤色
#define O             2   //橙色
#define G             3   //緑色

//これらのファイルをSPIFFS領域へコピーしておくこと
const char* UTF8SJIS_file         = "/Utf8Sjis.tbl";  //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* Shino_Zen_Font_file   = "/shnmk16.bdf";   //全角フォントファイル名を定義
const char* Shino_Half_Font_file  = "/shnm8x16.bdf";  //半角フォントファイル名を定義

const char* ssid        = "xxxx";   //AP SSID
const char* password    = "xxxx";    //AP Pass Word

const char* appid       = "xxxx";//Yahoo! APP ID
const char* zipcode     = "xxx-xxxx";//郵便番号
const char* output      = "json";//出力形式
const char* server      = "map.yahooapis.jp";

const char* yahooapi_root_ca= \
     "-----BEGIN CERTIFICATE-----\n" \
     "MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
     "RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\n" \
     "VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\n" \
     "DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\n" \
     "ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\n" \
     "VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\n" \
     "mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\n" \
     "IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\n" \
     "mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\n" \
     "XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\n" \
     "dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\n" \
     "jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\n" \
     "BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\n" \
     "DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n" \
     "9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\n" \
     "jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\n" \
     "Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\n" \
     "ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\n" \
     "R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n" \
     "-----END CERTIFICATE-----\n";

ESP32_SPIFFS_ShinonomeFNT SFR;  //東雲フォントをSPIFFSから取得するライブラリ
WiFiClientSecure client;

SemaphoreHandle_t xMutex = NULL;

//LEDマトリクスの書き込みアドレスを設定するメソッド
void setRAMAdder(uint8_t lineNumber){
  uint8_t A[4] = {0};
  uint8_t adder = 0;

  adder = lineNumber;

  for(int i = 0; i < 4; i++){    
    A[i] = adder % 2;
    adder /= 2;
  }

  digitalWrite(PORT_A0_IN, A[0]);
  digitalWrite(PORT_A1_IN, A[1]);
  digitalWrite(PORT_A2_IN, A[2]);
  digitalWrite(PORT_A3_IN, A[3]);

}

////////////////////////////////////////////////////////////////////////////////////
//データをLEDマトリクスへ1行だけ書き込む
//
//iram_addr:データを書き込むアドレス(0~15)
//ifont_data:フォント表示データ(32*PANEL_NUM bit)
//color_data:フォント表示色配列(32*PANEL_NUM bit)Red:1 Orange:2 Green:3 
////////////////////////////////////////////////////////////////////////////////////
void send_line_data(uint8_t iram_adder, uint8_t ifont_data[], uint8_t color_data[]){

  uint8_t font[8]   = {0};
  uint8_t tmp_data  = 0;
  int k = 0;
  for(int j = 0; j < 4 * PANEL_NUM; j++){
    //ビットデータに変換
    tmp_data = ifont_data[j];   
    for(int i = 0; i < 8; i++){    
      font[i] = tmp_data % 2;
      tmp_data /= 2;
    }

    for(int i = 7; i >= 0; i--){
      digitalWrite(PORT_DG_IN, LOW);
      digitalWrite(PORT_DR_IN, LOW);
      digitalWrite(PORT_CLK_IN, LOW);

      if(font[i] == 1){
        if(color_data[k] == R ){
          digitalWrite(PORT_DR_IN, HIGH);
        }

        if(color_data[k] == G){
          digitalWrite(PORT_DG_IN, HIGH);
        }

        if(color_data[k] == O){
          digitalWrite(PORT_DR_IN, HIGH);
          digitalWrite(PORT_DG_IN, HIGH);
        }
      }else{
          digitalWrite(PORT_DR_IN, LOW);
          digitalWrite(PORT_DG_IN, LOW);
      }

      delayMicroseconds(1);
      digitalWrite(PORT_CLK_IN, HIGH);
      delayMicroseconds(1);

      k++;
    }
  }
  //アドレスをポートに入力
  setRAMAdder(iram_adder);
  //ALE Highでアドレスセット
  digitalWrite(PORT_ALE_IN, HIGH);
  //WE Highでデータを書き込み
  digitalWrite(PORT_WE_IN, HIGH);
  //WE Lowをセット
  digitalWrite(PORT_WE_IN, LOW);
  //ALE Lowをセット
  digitalWrite(PORT_ALE_IN, LOW);
}

///////////////////////////////////////////////////////////////
//配列をnビット左へシフトする関数
//
//dist:格納先の配列
//src:入力元の配列
//len:配列の要素数
//n:一度に左シフトするビット数
///////////////////////////////////////////////////////////////
void shift_bit_left(uint8_t dist[], uint8_t src[], int len, int n){
  uint8_t mask = 0xFF << (8 - n);
  for(int i = 0; i < len; i++){
    if(i < len - 1){
      dist[i] = (src[i] << n) | ((src[i + 1] & mask) >> (8 - n));
    }else{
      dist[i] = src[i] << n;
    }
  }
}

void shift_color_left(uint8_t dist[], uint8_t src[], int len){
  for(int i = 0; i < len * 8; i++){
    if(i < len * 8 - 1){
      dist[i] = src[i + 1];
    }else{
      dist[i] = 0;
    }
  }
}

////////////////////////////////////////////////////////////////////
//フォントをスクロールしながら表示するメソッド
//
//sj_length:半角文字数
//font_data:フォントデータ(東雲フォント)
//color_data:フォントカラーデータ(半角毎に設定する)
//intervals:スクロール間隔(ms)
////////////////////////////////////////////////////////////////////
void scrollLEDMatrix(int16_t sj_length, uint8_t font_data[][16], uint8_t color_data[], uint16_t intervals){
  uint8_t src_line_data[sj_length] = {0};
  uint8_t dist_line_data[sj_length] = {0};
  uint8_t tmp_color_data[sj_length * 8] = {0};
  uint8_t tmp_font_data[sj_length][16] = {0};
  uint8_t ram = LOW;

  int n = 0;
  for(int i = 0; i < sj_length; i++){
  
    //8ビット毎の色情報を1ビット毎に変換する
    for(int j = 0; j < 8; j++){
      tmp_color_data[n++] = color_data[i];
    }
  
    //フォントデータを作業バッファにコピー
    for(int j = 0; j < 16; j++){
      tmp_font_data[i][j] = font_data[i][j];
    }

  }

  for(int k = 0; k < sj_length * 8 + 2; k++){
    ram = ~ram;
    digitalWrite(PORT_AB_IN, ram);//RAM-A/RAM-Bに書き込み
    for(int i = 0; i < 16; i++){
      for(int j = 0; j < sj_length; j++){       
        //フォントデータをビットシフト元バッファにコピー
        src_line_data[j] = tmp_font_data[j][i];
      }

      send_line_data(i, src_line_data, tmp_color_data);
      shift_bit_left(dist_line_data, src_line_data, sj_length, 1);

      //font_dataにシフトしたあとのデータを書き込む
      for(int j = 0; j < sj_length; j++){
        tmp_font_data[j][i] = dist_line_data[j];
      }
    }
    shift_color_left(tmp_color_data, tmp_color_data, sj_length);
    delay(intervals);
  }
}

////////////////////////////////////////////////////////////////////
//フォントを静的に表示するメソッド
//
//sj_length:半角文字数
//font_data:フォントデータ(東雲フォント)
//color_data:フォントカラーデータ(半角毎に設定する)//
////////////////////////////////////////////////////////////////////
void printLEDMatrix(int16_t sj_length, uint8_t font_data[][16], uint8_t color_data[]){
  uint8_t src_line_data[sj_length] = {0};
  uint8_t tmp_color_data[sj_length * 8] = {0};
  uint8_t tmp_font_data[sj_length][16] = {0};
  uint8_t ram = LOW;

  int n = 0;
  for(int i = 0; i < sj_length; i++){
  
    //8ビット毎の色情報を1ビット毎に変換する
    for(int j = 0; j < 8; j++){
      tmp_color_data[n++] = color_data[i];
    }
  
    //フォントデータを作業バッファにコピー
    for(int j = 0; j < 16; j++){
      tmp_font_data[i][j] = font_data[i][j];
    }

  }

  for(int k = 0; k < sj_length * 8 + 2; k++){
    ram = ~ram;
    digitalWrite(PORT_AB_IN, ram);//RAM-A/RAM-Bに書き込み
    for(int i = 0; i < 16; i++){
      for(int j = 0; j < sj_length; j++){       
        //フォントデータをビットシフト元バッファにコピー
        src_line_data[j] = tmp_font_data[j][i];
      }
      send_line_data(i, src_line_data, tmp_color_data);
    }
  }
}

void setAllPortOutput(){
  pinMode(PORT_SE_IN, OUTPUT);
  pinMode(PORT_AB_IN, OUTPUT);
  pinMode(PORT_A3_IN, OUTPUT);
  pinMode(PORT_A2_IN, OUTPUT);
  pinMode(PORT_A1_IN, OUTPUT);
  pinMode(PORT_A0_IN, OUTPUT);
  pinMode(PORT_DG_IN, OUTPUT);
  pinMode(PORT_CLK_IN, OUTPUT);
  pinMode(PORT_WE_IN, OUTPUT);
  pinMode(PORT_DR_IN, OUTPUT);
  pinMode(PORT_ALE_IN, OUTPUT);
}

void setAllPortLow(){
  digitalWrite(PORT_SE_IN, LOW);
  digitalWrite(PORT_AB_IN, LOW);
  digitalWrite(PORT_A3_IN, LOW);
  digitalWrite(PORT_A2_IN, LOW);
  digitalWrite(PORT_A1_IN, LOW);
  digitalWrite(PORT_A0_IN, LOW);
  digitalWrite(PORT_DG_IN, LOW);
  digitalWrite(PORT_CLK_IN, LOW);
  digitalWrite(PORT_WE_IN, LOW);
  digitalWrite(PORT_DR_IN, LOW);
  digitalWrite(PORT_ALE_IN, LOW);
}

void setAllPortHigh(){
  digitalWrite(PORT_SE_IN, HIGH);
  digitalWrite(PORT_AB_IN, HIGH);
  digitalWrite(PORT_A3_IN, HIGH);
  digitalWrite(PORT_A2_IN, HIGH);
  digitalWrite(PORT_A1_IN, HIGH);
  digitalWrite(PORT_A0_IN, HIGH);
  digitalWrite(PORT_DG_IN, HIGH);
  digitalWrite(PORT_CLK_IN, HIGH);
  digitalWrite(PORT_WE_IN, HIGH);
  digitalWrite(PORT_DR_IN, HIGH);
  digitalWrite(PORT_ALE_IN, HIGH);
}

void PrintTime(String &str, int flag)
{
  char tmp_str[10] = {0};
  time_t t;
  struct tm *tm;

  t = time(NULL);
  tm = localtime(&t);

  if(flag == 0){
    sprintf(tmp_str, "  %02d:%02d ", tm->tm_hour, tm->tm_min);
  }else{
    sprintf(tmp_str, "  %02d %02d ", tm->tm_hour, tm->tm_min);
  }

  str = tmp_str;
}

void printTimeLEDMatrix(){
  //フォントデータバッファ
  uint8_t time_font_buf[8][16] = {0};
  String str;

  static int flag = 0;

  flag = ~flag;
  PrintTime(str, flag);

  //フォント色データ str(半角文字毎に設定する)
  uint8_t time_font_color[8] = {G,G,G,G,G,G,G,G};
  uint16_t sj_length = SFR.StrDirect_ShinoFNT_readALL(str, time_font_buf);
  printLEDMatrix(sj_length, time_font_buf, time_font_color);
}

void makeHostStr(String &hostStr){
  hostStr = "Host: ";
  hostStr += server;
}

void makeAgentStr(String &agentStr){
  agentStr = "User-Agent: Yahoo AppID: ";
  agentStr += appid;
}

void getYahooApiJsonInfo(String httpRequest, String &resultJson){
  String getStr;
  String hostStr;
  String agentStr;

  client.setCACert(yahooapi_root_ca);

  Serial.println("\nStarting connection to server...");
  if (!client.connect(server, 443)){
    Serial.println("Connection failed!");
  }else {
    Serial.println("Connected to server!");

    delay(500);

    client.println(httpRequest);
    client.println();

    delay(500);

    while (client.connected()) {
      String line = client.readStringUntil('\n');
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }

    delay(500);

    while (client.available()) {
      resultJson += (char)client.read();
    }

    client.stop();
  }
}

void makeGetZipCodeStr(String zipcode, String &getStr){
    getStr = "GET /search/zip/V1/zipCodeSearch?query=";
    getStr += zipcode;
    getStr += "&output=";
    getStr += output;
    getStr += " HTTP/1.1";
} 

void makeZipCodeHttpRequestStr(String &httpRequest){
  String getStr;
  String hostStr;
  String agentStr;
  String coordinates;

  makeGetZipCodeStr(zipcode, getStr);
  makeHostStr(hostStr);
  makeAgentStr(agentStr);

  httpRequest = getStr;
  httpRequest += "\n";
  httpRequest += hostStr;
  httpRequest += "\n";
  httpRequest += agentStr;
  httpRequest += "\n";
  httpRequest += "Connection: close";
}

void getCoordinatesFromZipcode(String zipcode, String &coordinates){
  String httpRequest;
  String resultJson;
  
  //httpRequestを作成する
  makeZipCodeHttpRequestStr(httpRequest);

  Serial.println(httpRequest);

  getYahooApiJsonInfo(httpRequest, resultJson);

  Serial.println(resultJson);

  const size_t capacity = JSON_ARRAY_SIZE(0) + JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(2) + JSON_ARRAY_SIZE(3) + 6*JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(7) + 2*JSON_OBJECT_SIZE(8) + 3*JSON_OBJECT_SIZE(9) + 990;

  DynamicJsonDocument doc(capacity);

  deserializeJson(doc, resultJson);

  JsonObject Feature_0 = doc["Feature"][0];

  const char* Feature_0_Geometry_Coordinates = Feature_0["Geometry"]["Coordinates"];

  Serial.printf("Coordinates = %s\n", Feature_0_Geometry_Coordinates);

  coordinates = Feature_0_Geometry_Coordinates;
}

void makeGetStr(String coordinates, String &getStr){
  getStr = "GET /weather/V1/place?coordinates="; 
  getStr += coordinates;
  getStr += "&output=";
  getStr += output;
  getStr += " HTTP/1.1";
}

void makeWeatherHttpRequestStr(String &httpRequest){
  String getStr;
  String hostStr;
  String agentStr;
  String coordinates;

  getCoordinatesFromZipcode(zipcode, coordinates);

  makeGetStr(coordinates, getStr);
  makeHostStr(hostStr);
  makeAgentStr(agentStr);

  httpRequest = getStr;
  httpRequest += "\n";
  httpRequest += hostStr;
  httpRequest += "\n";
  httpRequest += agentStr;
  httpRequest += "\n";
  httpRequest += "Connection: close";
}

void getWeatherStrings(JsonArray &i_weather, int i_index, String &o_type, String &o_date, float &o_rainfall){
  JsonObject Feature_0_Property_WeatherList_Weather_0 = i_weather[i_index];
  const char* Type = Feature_0_Property_WeatherList_Weather_0["Type"];
  const char* Date = Feature_0_Property_WeatherList_Weather_0["Date"];
  float Rainfall = Feature_0_Property_WeatherList_Weather_0["Rainfall"];

  o_type = Type;
  o_date = Date;
  o_rainfall = Rainfall;

}

//雨降りの状態
#define RAINFALL_END    0x01  //降雨が終わる時点
#define RAINFALL_NO     0x02  //雨が降っていない状態 
#define RAINFALL_START  0x03  //降雨が開始する時点
#define RAINFALL_NOW    0x04  //雨が継続的に降っている状態

uint16_t getWeatherInfo(int &forcast_time){

  String weatherJsonInfo;
  String httpRequest;

  String type;
  String date;
  float rainfall = 0;
  
  uint16_t weatherInfo = 0;

  makeWeatherHttpRequestStr(httpRequest);

  Serial.println(httpRequest);

  getYahooApiJsonInfo(httpRequest, weatherJsonInfo);

  Serial.println(weatherJsonInfo);

  const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(7) + JSON_OBJECT_SIZE(1) + 3*JSON_OBJECT_SIZE(2) + 7*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(7) + 660;
  DynamicJsonDocument doc(capacity);

  deserializeJson(doc, weatherJsonInfo);
  JsonObject Feature_0 = doc["Feature"][0];

  JsonArray WeatherList = Feature_0["Property"]["WeatherList"]["Weather"];

  getWeatherStrings(WeatherList, 0, type, date, rainfall);

  //現在雨が降っていない
  if(rainfall == 0.00){
    for(int i = 1; i < 7; i++){
      getWeatherStrings(WeatherList, i, type, date, rainfall);
      if(rainfall > 0.00){
        //(i*10-10)分後に雨が降ります。
        forcast_time = i * 10 - 10;
        weatherInfo = RAINFALL_START;
        break;
      }
      else{
        weatherInfo = RAINFALL_NO;
      }
    } 
  }else{//現在雨が降っている
    for(int i = 1; i < 7; i++){
      getWeatherStrings(WeatherList, i, type, date, rainfall);
      if(rainfall == 0.00){
        //(i*10-10)分後に雨が止みます。
        forcast_time = i * 10 - 10;
        weatherInfo = RAINFALL_END;
        break;
      }else{
        //しばらく雨が降ります。60分後も降雨状態
        weatherInfo = RAINFALL_NOW;
      }
    }
  }

  return weatherInfo;
}

portTickType Delay1000 = 1000 / portTICK_RATE_MS; //freeRTOS 用の遅延時間定義
TaskHandle_t hClock;
TaskHandle_t hWeatherInfo;

void ClockTask(void *pvParameters) {
  Serial.printf("ClockTask coreID = %d, ClockTask priority = %d\n", xPortGetCoreID(), uxTaskPriorityGet(hClock));

  BaseType_t xStatus;
  const TickType_t xTicksToWait = 500UL;
  xSemaphoreGive(xMutex);
 
  while(1){
      xStatus = xSemaphoreTake(xMutex, xTicksToWait);

      //Serial.println("check for mutex (ClockTask)");

      if(xStatus == pdTRUE){
        time_t t;
        struct tm *tm;

        t = time(NULL);
        tm = localtime(&t);

        if(tm->tm_min % 10 == 0){
          Serial.println("Give Semaphore(ClockTask)");
          xSemaphoreGive(xMutex);
        }else{
          printTimeLEDMatrix();
        }
      }

      delay(500);
  }
}

void printConnecting(void){
  //フォントデータバッファ
  uint8_t font_buf[8][16] = {0};
  //フォント色データ(半角文字毎に設定する)
  uint8_t font_color1[8] = {G,G,G,G,G,G,G,G};

  uint16_t sj_length = SFR.StrDirect_ShinoFNT_readALL("...     ", font_buf);
  printLEDMatrix(sj_length, font_buf, font_color1);
}

void WeatherInfoTask(void *pvParameters){
  Serial.printf("WeatherInfoTask coreID = %d, WeatherInfoTask priority = %d\n", xPortGetCoreID(), uxTaskPriorityGet(hWeatherInfo));

  BaseType_t xStatus;
  const TickType_t xTicksToWait = 1000UL;
  xSemaphoreGive(xMutex);

  uint16_t sj_length = 0;//半角文字数 
    
  //フォントデータバッファ
  uint8_t font_buf[100][16] = {0};
  //フォント色データ str1(半角文字毎に設定する)
  uint8_t font_color1[100] = {G,G,G,G,G,G,G,G,O,O,
                              O,O,O,O,O,O,O,O,O,O,
                              O,O,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G,
                              G,G,G,G,G,G,G,G,G,G};
  
  char tmp_str[100] = {0};

  int forcast_time;

  while(1){

    xStatus = xSemaphoreTake(xMutex, xTicksToWait);

    //Serial.println("check for mutex (WeatherInfoTask)");

    if(xStatus == pdTRUE ){
      
      printConnecting();

      uint16_t weather_state = getWeatherInfo(forcast_time);

      switch(weather_state){
        case RAINFALL_END:
          if(forcast_time != 0){
            sprintf(tmp_str, "        Yahoo!天気情報  %d分後に雨が止む予報です。", forcast_time);          
          }else{
            sprintf(tmp_str, "        Yahoo!天気情報  すぐに雨が止む予報です。");          
          }
        break;
        case RAINFALL_NO://雨は降っていない。60分後の予報もない
          sprintf(tmp_str, "        Yahoo!天気情報 現在、雨が降る予報はありません。");          
        break;
        case RAINFALL_START:
          if(forcast_time != 0){
            sprintf(tmp_str, "        Yahoo!天気情報  %d分後に雨が降る予報です。", forcast_time);          
          }else{
            sprintf(tmp_str, "        Yahoo!天気情報  すぐに雨が降る予報です。");          
          }
        break;
        case RAINFALL_NOW:
          sprintf(tmp_str, "        Yahoo!天気情報  現在、雨が降っています。しばらく雨が続きます。");    
        break;
        default:
          ;//nothing
      }
      Serial.printf("%s\n", tmp_str);
      sj_length = SFR.StrDirect_ShinoFNT_readALL(tmp_str, font_buf);
      scrollLEDMatrix(sj_length, font_buf, font_color1, 30); 
    }

    xSemaphoreGive(xMutex);
    delay(10);

  }
}

void setup() {

  uint16_t sj_length = 0;//半角文字数 

  delay(1000);
  Serial.begin(115200);
  setAllPortOutput();
  setAllPortLow();

  WiFi.begin(ssid, password);
  while(WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println();
  Serial.printf("Connected, IP address: ");
  Serial.println(WiFi.localIP());
  Serial.print("Connected to ");
  Serial.println(ssid);

  configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");

  //手動で表示バッファを切り替える
  digitalWrite(PORT_SE_IN, HIGH);

  //フォントデータバッファ
  uint8_t font_buf[32][16] = {0};
  //フォント色データ str1(半角文字毎に設定する)
  uint8_t font_color1[32] = {G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G,G};

  SFR.SPIFFS_Shinonome_Init3F(UTF8SJIS_file, Shino_Half_Font_file, Shino_Zen_Font_file);
  sj_length = SFR.StrDirect_ShinoFNT_readALL("  OK", font_buf);
  scrollLEDMatrix(sj_length, font_buf, font_color1, 30);

  xMutex = xSemaphoreCreateMutex();

  if( xMutex != NULL ){
    xTaskCreatePinnedToCore(ClockTask, "ClockTask", 4096, NULL, 1, &hClock, 1); //ClockTask開始
    xTaskCreatePinnedToCore(WeatherInfoTask, "WeatherInfoTask", 8192, NULL, 2, &hWeatherInfo, 0); //WeatherInfoTask開始
  }else{
    while(1){
        Serial.println("rtos mutex create error, stopped");
        delay(1000);
    }
  }
}

void loop() {
  ;
}

Macを使ってSSL化しているWebサイトのルート証明書をpem形式で保存する方法

現在、ESP32を使ってYahoo!の気象情報APIを使いたいなぁと思っています。
developer.yahoo.co.jp

Arduino core for the ESP32を使っているので、YahooのWebAPIにアクセスするにはWifiClientSecureライブラリを使います。
色々調べると、YahooのWebAPIは常時SSLになっています。WifiClientSecureを使うにはサイトのルート証明書のデータを設定しなければなりません。

Macを使ってSSL化しているWebサイトのルート証明書をpem形式で保存する方法

f:id:riraosan:20190608155747j:plain

  • デベロッパーツールから「Security」を選択する
  • Securityから「View certificate」ボタンを押下する

f:id:riraosan:20190608155917j:plain

  • 「Baltimore CyberTrust Root」から発行されていることを確認

f:id:riraosan:20190608160006j:plain

  • 「キーチェーン」を起動して「Baltimore CyberTrust Root」を検索
  • 「Baltimore CyberTrust Root」のコンテキストメニューを表示して、「"Baltimore CyberTrust Root"を書き出す...」を選択

f:id:riraosan:20190608160029j:plain

  • フォーマットを「pem形式」に変更

f:id:riraosan:20190608160053j:plain

  • 保存ボタンを押下する

→ファイルの中身はbase64エンコードされている。

f:id:riraosan:20190608160145p:plain

こいつをソースコードに埋め込んでWifiClientSecureライブラリに渡してやるとSSL通信が出来ます。

const char* yahooapi_root_ca= \
     "-----BEGIN CERTIFICATE-----\n" \
     "MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
     "RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\n" \
     "VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\n" \
     "DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\n" \
     "ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\n" \
     "VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\n" \
     "mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\n" \
     "IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\n" \
     "mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\n" \
     "XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\n" \
     "dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\n" \
     "jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\n" \
     "BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\n" \
     "DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n" \
     "9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\n" \
     "jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\n" \
     "Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\n" \
     "ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\n" \
     "R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n" \
     "-----END CERTIFICATE-----\n";
WiFiClientSecure client;

setup(){
   client.setCACert(yahooapi_root_ca);