0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5Stack Tab5を使い倒す(ネットワーク編)

Last updated at Posted at 2025-12-30

1. 導入・概要

この記事はM5StackTab5をArduino環境で使い倒す(願望)ための情報を共有するものです。
私と同じようにこのパワフルなマシンの様々な機能&大きな画面を使いたいと思っている方向けに情報を共有していければと思っています。

この記事は「Tab5を使い倒す(1_基礎編)」の続きとなる2_Network編です。それぞれプロジェクトは独立しているので、どこから始めていただいてもかまいません。
1_基礎編では環境構築などについても触れていますので、初めてトライする方は順を追って確認いただいたほうがわかりやすいと思います。
私のGitHub上にソースを公開していますので、MITライセンスで活用してください。

No 記事 コード
1 基礎編 Github
2 ネットワーク編 Github
3 GUI編 Github

2. 前提条件

  • コードはGithubにMITライセンスとして公開しています。それぞれのサンプルはフォルダ分けしてプログラムと解説を入れていますので、参考にしてください。

  • 開発環境はAruduinoIDEを使っての開発を想定しています。

  • プログラムはAruduinoIDE環境でコンパイル、転送までできますが。ライブラリは個別にインストールが必要です。

シークレットファイルの扱い
多くのサンプルで、Wi-Fi SSID / パスワードや API キーを secrets.h に分離しています。テンプレートをコピーして利用環境に合わせてください。

  • テンプレート: secrets.h.example
  • 実ファイル: secrets.h(ユーザーがコピーして編集)

3. プログラム解説

Wi-Fi基本機能(3個)

No. プログラム名 主要機能
1 Wifi_Antenna Wi-Fiアンテナ感度テスト
2 Wifi_Scan Wi-Fiアクセスポイントスキャン
3 Wifi_Connect Wi-Fi接続テスト

Web/API通信(4個)

No. プログラム名 主要機能
4 RSSReader RSSフィード取得・表示(Yahoo News)
5 WebAPI_Wether 天気情報API取得
6 WebServer シンプルWebサーバー
7 ChatGPT ChatGPT API連携

IoT通信(2個)

No. プログラム名 主要機能
8 MQTT MQTTブローカー接続・Pub/Sub
9 SNTP 時刻同期(NTPサーバー)

ESP-C4を搭載しているので、BLEも使えるはずですがサンプルプログラムではエラーが出て使えませんでした。
どうしたら使えるのでしょうか?


プログラム解説

1. Wifi_Antenna - Wi-Fiアンテナ感度テスト

このスケッチは M5Tab5 の IO エキスパンダを制御し、Wi-Fi アンテナの接続を内部(本体アンテナ)と外部(外付けアンテナ)で切り替えます。同時にディスプレイへ現在のアンテナの状態(Wi-Fi アンテナの感度・アンテナのモード)を確認するテストです。

image.png

このスケッチは M5Tab5 の IOエキスパンダを制御し、Wi-Fi アンテナの接続を内部(本体アンテナ)と外部(外付けアンテナ)で切り替えます。同時にディスプレイへ現在のアンテナモードを表示します。ハードウェア的な切替ラインを直接叩くだけなので、ループ処理や追加のロジックは不要です。

ソース
#include "M5Unified.h"

void setup()
{
    auto cfg = M5.config();
    cfg.output_power = true;  // バッテリーユニット使用時にディスプレイ電源を確保
    M5.begin(cfg);

    M5.Display.setTextSize(2);
    M5.Display.setCursor(0, 0);
    M5.Display.println("Wi-Fi Antenna");

    // 内蔵Wi-Fiアンテナを使用
    auto& ioe = M5.getIOExpander(0);
    ioe.digitalWrite(0, false);
    M5.Display.println("Mode: Internal");

    // 外部Wi-Fiアンテナを使用
    // auto& ioe = M5.getIOExpander(0);
    // ioe.digitalWrite(0, true);
    // M5.Display.println("Mode: External");
}

void loop()
{
} 

解説

  1. 設定取得と電源出力の有効化
   auto cfg = M5.config();
   cfg.output_power = true;
   M5.begin(cfg);
  • M5.config() でデフォルト設定を取得し、output_powertrue に設定。
  • バッテリ駆動ユニットでディスプレイや IO 周辺へ電力を供給するためのフラグ。
  • M5.begin(cfg) で本体・LCD・IOエキスパンダを初期化。
  1. アンテナ切替ラインの制御
   auto& ioe = M5.getIOExpander(0);
   ioe.digitalWrite(0, false);
  • IO エキスパンダ 0 番のポート 0 を LOWfalse)に設定 → 内蔵アンテナを選択。
  1. 外部アンテナ用コード(コメント)
   // ioe.digitalWrite(0, true);
   // M5.Display.println("Mode: External");
  • true を書き込むと外部アンテナ経路へ切り替わる。

2. Wifi_Scan - Wi-Fiアクセスポイントスキャン

このプログラムは、M5Stack Tab5のWiFi機能を使用して、周囲のWiFiアクセスポイントをスキャンし、SSID、RSSI(信号強度)、チャンネル、暗号化方式などの情報を画面に表示します。

  • WiFiスキャン: 周囲のWiFiアクセスポイントを検出
  • 情報表示: SSID、RSSI、チャンネル、暗号化方式を表示
  • 自動再スキャン: 5秒ごとに自動的に再スキャン
  • SDIO2ピン設定: Tab5のSDIO2ピンを使用したWiFi制御
ソース
#include <M5Unified.h>
#include <WiFi.h>

void setup()
{
    M5.begin();
    Serial.begin(115200);

    M5.Display.setFont(&fonts::FreeMonoBoldOblique9pt7b);

    // Arduino IDEでM5Tab5ボードを選択した場合、定義済みのデフォルトピンを使用できます
    WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
                  BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
                  BOARD_SDIO_ESP_HOSTED_RESET);
}

void loop()
{
    M5.Display.setCursor(0, 0);
    M5.Display.println("Scan start");

    // WiFi.scanNetworksは検出されたネットワークの数を返します
    int n = WiFi.scanNetworks();
    M5.Display.println("Scan done");
    if (n == 0) {
        M5.Display.println("no networks found");
    } else {
        M5.Display.print(n);
        M5.Display.println(" networks found");
        M5.Display.println("Nr | SSID                             | RSSI | CH | Encryption");
        for (int i = 0; i < n; ++i) {
            // 検出された各ネットワークのSSIDとRSSIを表示
            M5.Display.printf("%2d", i + 1);
            M5.Display.print(" | ");
            M5.Display.printf("%-32.32s", WiFi.SSID(i).c_str());
            M5.Display.print(" | ");
            M5.Display.printf("%4ld", WiFi.RSSI(i));
            M5.Display.print(" | ");
            M5.Display.printf("%2ld", WiFi.channel(i));
            M5.Display.print(" | ");
            switch (WiFi.encryptionType(i)) {
                case WIFI_AUTH_OPEN:
                    M5.Display.print("open");
                    break;
                case WIFI_AUTH_WEP:
                    M5.Display.print("WEP");
                    break;
                case WIFI_AUTH_WPA_PSK:
                    M5.Display.print("WPA");
                    break;
                case WIFI_AUTH_WPA2_PSK:
                    M5.Display.print("WPA2");
                    break;
                case WIFI_AUTH_WPA_WPA2_PSK:
                    M5.Display.print("WPA+WPA2");
                    break;
                case WIFI_AUTH_WPA2_ENTERPRISE:
                    M5.Display.print("WPA2-EAP");
                    break;
                case WIFI_AUTH_WPA3_PSK:
                    M5.Display.print("WPA3");
                    break;
                case WIFI_AUTH_WPA2_WPA3_PSK:
                    M5.Display.print("WPA2+WPA3");
                    break;
                case WIFI_AUTH_WAPI_PSK:
                    M5.Display.print("WAPI");
                    break;
                default:
                    M5.Display.print("unknown");
            }
            M5.Display.println();
            delay(10);
        }
    }
    M5.Display.println("");
    // スキャン結果を削除してメモリを解放
    WiFi.scanDelete();

    // 次のスキャンまで少し待機
    delay(5000);
} 

解説

1.WiFiピンの設定
Tab5では、Arduino IDEでM5Tab5ボードを選択すると、デフォルトピンが定義されているため、以下のように簡略化できます:

WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
             BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
             BOARD_SDIO_ESP_HOSTED_RESET);

2.WiFiスキャン

int n = WiFi.scanNetworks();

3.アクセスポイント情報の取得

WiFi.SSID(i);              // SSID
WiFi.RSSI(i);              // 信号強度(dBm)
WiFi.channel(i);           // チャンネル
WiFi.encryptionType(i);    // 暗号化方式

4.暗号化方式の判定

switch (WiFi.encryptionType(i)) {
    case WIFI_AUTH_OPEN:
        M5.Display.print("open");
        break;
    case WIFI_AUTH_WEP:
        M5.Display.print("WEP");
        break;
    case WIFI_AUTH_WPA_PSK:
        M5.Display.print("WPA");
        break;
    case WIFI_AUTH_WPA2_PSK:
        M5.Display.print("WPA2");
        break;
    // ... その他の暗号化方式
}

カスタマイズ

1.スキャンモードの変更

WiFi.scanNetworks(true);  // 非同期スキャン
// または
WiFi.scanNetworks(false, true);  // 非表示ネットワークもスキャン

2.RSSIによるフィルタリング

if (WiFi.RSSI(i) > -70) {  // -70dBm以上の信号のみ表示
    // 表示処理
}

3. Wifi_Connect - Wi-Fi接続テスト

最小構成のWi-Fi接続テストです。HTTP通信のテストとして、httpbin.orgにアクセスします。

ソース
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "secrets.h"

void setup() {
  // M5Unifiedの初期化
  auto cfg = M5.config();
  M5.begin(cfg);
  // ディスプレイの初期化
  M5.Display.setTextSize(2);
  M5.Display.setTextColor(WHITE, BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.println("WiFi Connecting...");
  M5.Display.println(ssid);
  
  // アクセスポイントに接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Display.print(".");
  }
  // ディスプレイにIPアドレスを表示
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.setTextSize(5);
  M5.Display.println("WiFi Connected!");
  M5.Display.println();
  M5.Display.setTextSize(3);
  M5.Display.print("IP: ");
  M5.Display.println(WiFi.localIP());
  
  delay(2000);

  // ディスプレイをクリア
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.setTextSize(5);
  M5.Display.println("HTTP Test");
  M5.Display.println();
  M5.Display.setTextSize(3);
  M5.Display.println("Connecting...");
  
  // HTTPクライアントの準備
  HTTPClient http;
  
  // HTTPリクエストの例(httpbin.orgのテストAPI)
  if (http.begin("http://httpbin.org/get")) {
    M5.Display.println("Requesting...");
    int status = http.GET();
    
    if (status > 0) {
      if (status == HTTP_CODE_OK) {
        // ディスプレイに成功メッセージを表示
        M5.Display.fillScreen(BLACK);
        M5.Display.setCursor(0, 0);
        M5.Display.setTextSize(5);
        M5.Display.println("Success!");
        M5.Display.println();
        M5.Display.setTextSize(3);
        M5.Display.println("HTTP OK");
        M5.Display.print("Size: ");
        M5.Display.println(response.length());
        M5.Display.println();
        M5.Display.setTextSize(2);
        M5.Display.println("Check Serial");
        M5.Display.println("for details");
      }
      else {
        // ディスプレイにエラーを表示
        M5.Display.fillScreen(BLACK);
        M5.Display.setCursor(0, 0);
        M5.Display.setTextSize(5);
        M5.Display.println("HTTP Error");
        M5.Display.setTextSize(3);
        M5.Display.print("Status: ");
        M5.Display.println(status);
      }
    }
    else {
      // ディスプレイにエラーを表示
      M5.Display.fillScreen(BLACK);
      M5.Display.setCursor(0, 0);
      M5.Display.setTextSize(3);
      M5.Display.println("Get Failed");
    }
    http.end();
  }
  else {
    // ディスプレイにエラーを表示
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.setTextSize(3);
    M5.Display.println("Connect Error");
  }
}

void loop() {
  M5.update();
  delay(100);
}


4. RSSReader - RSSフィード取得・表示

このプログラムは、M5Stack Tab5のWiFi機能を使用して、Yahoo NewsのRSSフィードを取得し、ニュースタイトルを画面に表示します。HTTPS通信でRSSフィードを読み込み、XMLを解析してタイトルを抽出します。

必要なライブラリ

  • WiFi: ESP32標準ライブラリ(Arduino IDEに含まれています)
  • HTTPClient: ESP32標準ライブラリ(Arduino IDEに含まれています)

主要機能

  • HTTPS接続: Yahoo News RSSフィードサーバーへのセキュアな接続
  • RSS解析: XMLからニュースタイトルを抽出
  • 日本語表示: 日本語フォントでタイトルを表示
  • 複数記事表示: 最大5件のニュースを表示

プログラムの動作

  1. 初期化: M5Stack初期化、WiFi接続、HTTPクライアント設定
  2. RSS取得: Yahoo News RSSフィードへのHTTPS接続、XMLレスポンス受信
  3. 表示: XMLから最大5件のニュースタイトルを抽出し、日本語フォントで表示

HTTPクライアントの初期化

HTTPClient http;
http.setTimeout(15000);  // タイムアウトを15秒に設定

HTTPリクエストの送信

// Yahoo News RSSフィード(HTTPS)
if (http.begin("https://news.yahoo.co.jp/rss/topics/top-picks.xml")) {
  int status = http.GET();
  if (status == HTTP_CODE_OK) {
    String xml = http.getString();
    // XMLを解析
  }
  http.end();
}

XMLからタイトルを抽出

String extractTag(String xml, String tagName, int startPos = 0) {
  String openTag = "<" + tagName + ">";
  String closeTag = "</" + tagName + ">";
  int start = xml.indexOf(openTag, startPos);
  int end = xml.indexOf(closeTag, start);
  return xml.substring(start + openTag.length(), end);
}

日本語フォントの設定

M5.Display.setFont(&fonts::lgfxJapanGothic_16);
M5.Display.println("日本語のタイトル");

RSSフィードの情報

ソース
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>

// WiFi認証情報は secrets.h から読み込みます
// secrets.h.example をコピーして secrets.h を作成し、実際のSSIDとパスワードを設定してください
#include "secrets.h"

void setup() {
  // M5Unifiedの初期化
  auto cfg = M5.config();
  M5.begin(cfg);
  
  delay(500);
  
  // ディスプレイの初期化
  M5.Display.setFont(&fonts::lgfxJapanGothic_16);
  M5.Display.setTextColor(WHITE, BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.println("WiFi接続中...");
  M5.Display.println(ssid);
  
  // アクセスポイントに接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Display.print(".");
  }
  
  // ディスプレイにIPアドレスを表示
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.setFont(&fonts::lgfxJapanGothic_20);
  M5.Display.println("WiFi接続完了!");
  M5.Display.println();
  M5.Display.setFont(&fonts::lgfxJapanGothic_16);
  M5.Display.print("IP: ");
  M5.Display.println(WiFi.localIP());
  
  delay(2000);

  // ディスプレイをクリア
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.setFont(&fonts::lgfxJapanGothic_20);
  M5.Display.println("RSSフィードに");
  M5.Display.println("接続中...");
  M5.Display.println();
  
  // メモリ情報を表示
  M5.Display.setFont(&fonts::lgfxJapanGothic_12);
  M5.Display.print("空きメモリ: ");
  M5.Display.print(ESP.getFreeHeap());
  M5.Display.println(" bytes");
  M5.Display.println();
  
  // HTTPクライアントの準備
  HTTPClient http;
  
  // タイムアウトを長めに設定(デフォルトは5秒)
  http.setTimeout(15000);  // 15秒
  
  // Yahoo News RSSフィード(HTTPS)
  // HTTPClient.h は内部で適切にHTTPS通信を処理します
  if (http.begin("https://news.yahoo.co.jp/rss/topics/top-picks.xml")) {
    M5.Display.println("Requesting...");
    int status = http.GET();
    
    // デバッグ情報を表示
    M5.Display.print("Status code: ");
    M5.Display.println(status);
    
    if (status > 0) {
      if (status == HTTP_CODE_OK) {
        // 得られたXMLを取得する
        String xml = http.getString();
        
        // RSSニュースをディスプレイに表示
        displayRSSNews(xml);
      }
      else {
        // ディスプレイにエラーを表示
        M5.Display.fillScreen(BLACK);
        M5.Display.setCursor(0, 0);
        M5.Display.setFont(&fonts::lgfxJapanGothic_16);
        M5.Display.println("HTTP Error");
        M5.Display.print("Status: ");
        M5.Display.println(status);
      }
    }
    else {
      // ディスプレイにエラーを表示
      M5.Display.fillScreen(BLACK);
      M5.Display.setCursor(0, 0);
      M5.Display.setFont(&fonts::lgfxJapanGothic_16);
      M5.Display.println("Get Failed");
      M5.Display.println();
      M5.Display.print("Error code: ");
      M5.Display.println(status);
      M5.Display.println();
      
      // エラーコードの説明
      if (status == -1) {
        M5.Display.println("Connection failed");
      } else if (status == -11) {
        M5.Display.println("Timeout");
      } else {
        M5.Display.println("Unknown error");
      }
    }
    http.end();
  }
  else {
    // ディスプレイにエラーを表示
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.setFont(&fonts::lgfxJapanGothic_16);
    M5.Display.println("Connect Error");
  }
}

void loop() {
  M5.update();
  delay(100);
}

// XMLから指定タグの内容を抽出する関数
String extractTag(String xml, String tagName, int startPos = 0) {
  String openTag = "<" + tagName + ">";
  String closeTag = "</" + tagName + ">";
  
  int start = xml.indexOf(openTag, startPos);
  if (start == -1) return "";
  start += openTag.length();
  
  int end = xml.indexOf(closeTag, start);
  if (end == -1) return "";
  
  return xml.substring(start, end);
}

// RSSのニュースタイトルを抽出して表示する関数
void displayRSSNews(String xml) {
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  
  // 日本語フォントを設定
  M5.Display.setFont(&fonts::lgfxJapanGothic_16);
  M5.Display.setTextColor(YELLOW, BLACK);
  M5.Display.println("=== Yahoo News ===");
  M5.Display.setTextColor(WHITE, BLACK);
  M5.Display.println();
  
  int itemCount = 0;
  int searchPos = 0;
  
  // <item>タグを順に検索
  while (itemCount < 5) {  // 最大5件表示
    int itemStart = xml.indexOf("<item>", searchPos);
    if (itemStart == -1) break;
    
    int itemEnd = xml.indexOf("</item>", itemStart);
    if (itemEnd == -1) break;
    
    // <item>内の<title>を抽出
    String title = extractTag(xml, "title", itemStart);
    if (title.length() > 0) {
      itemCount++;
      M5.Display.setTextColor(CYAN, BLACK);
      M5.Display.printf("%d. ", itemCount);
      M5.Display.setTextColor(WHITE, BLACK);
      
      // タイトルを表示(日本語フォントで表示)
      M5.Display.println(title);
      M5.Display.println();
    }
    
    searchPos = itemEnd + 7;  // </item>の後から検索
  }
  
  M5.Display.println();
  M5.Display.setTextColor(GREEN, BLACK);
  M5.Display.printf("合計: %d 件\n", itemCount);
}


5. WebAPI_Wether - 気象庁天気予報API

M5Stack Tab5のWiFi機能を使用して、気象庁が提供する天気予報APIにアクセスし、今日と明日の天気・気温を取得して表示します。HTTPS通信、JSON解析、時刻同期(SNTP)、グラフィカルな天気アイコン表示など、実用的なIoTアプリケーションの要素を含んでいます。

  • HTTPS通信: SSL/TLSを使用したセキュアな通信で気象庁APIにアクセス
  • JSON解析: ArduinoJsonライブラリを使用して気象庁APIのレスポンスを解析
  • SNTP時刻同期: NTPサーバーから正確な時刻を取得してRTCに設定
  • 天気情報表示: 今日と明日の天気、最高気温・最低気温を表示
  • 天気アイコン: 晴れ・曇り・雨・雪の天気アイコンをグラフィカルに表示
  • 自動更新: 1時間ごとに天気情報を自動更新
  • 日本語表示: 日本語フォント(lgfxJapanMinchoP_16)を使用
  • エラー表示: HTTP通信エラーやJSON解析エラーを画面に表示
ソース
/*
 * M5Stack Tab5 天気予報表示アプリ
 * 気象庁の天気予報APIから天気情報を取得して表示します
 * 
 * 参考: https://bokunimo.net/blog/esp/3426/
 */

#define NTP_TIMEZONE "JST-9"
#define NTP_SERVER1  "0.pool.ntp.org"
#define NTP_SERVER2  "1.pool.ntp.org"
#define NTP_SERVER3  "2.pool.ntp.org"

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_sntp.h>

// WiFi認証情報は secrets.h から読み込みます
#include "secrets.h"

// 気象庁APIのエンドポイント(例: 東京の地域コード130000)
// 地域コードは気象庁のサイトで確認してください
#define WEATHER_API_URL "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"

// 天気コードの定義(気象庁APIの天気コード)
// 100: 晴れ, 200: 曇り, 300: 雨, 400: 雪
enum WeatherCode {
    SUNNY = 100,
    CLOUDY = 200,
    RAIN = 300,
    SNOW = 400
};

// 天気情報構造体
struct WeatherInfo {
    String weather;      // 天気(晴れ、曇り、雨、雪)
    int tempMax;         // 最高気温
    int tempMin;         // 最低気温
    int pop;             // 降水確率(%)
    WeatherCode code;    // 天気コード
};

WeatherInfo todayWeather;
WeatherInfo tomorrowWeather;
bool weatherUpdated = false;
unsigned long lastWeatherUpdate = 0;
const unsigned long WEATHER_UPDATE_INTERVAL = 3600000; // 1時間ごとに更新
String lastError = "";

void setup() {
    M5.begin();
    delay(1000);

    M5.Display.setRotation(3); // 横向き
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.println("Weather App");
    M5.Display.println("Initializing...");

    // WiFi設定
    // Arduino IDEでM5Tab5ボードを選択した場合、定義済みのデフォルトピンを使用できます
    WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
                  BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
                  BOARD_SDIO_ESP_HOSTED_RESET);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    M5.Display.print("Connecting WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        M5.Display.print(".");
    }
    M5.Display.println("\nWiFi Connected!");
    M5.Display.print("IP: ");
    M5.Display.println(WiFi.localIP());

    // NTP時刻同期
    M5.Display.println("Syncing time...");
    configTzTime(NTP_TIMEZONE, NTP_SERVER1, NTP_SERVER2, NTP_SERVER3);
    
    while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) {
        delay(1000);
        M5.Display.print(".");
    }
    M5.Display.println("\nTime synced!");

    // RTCに時刻を設定
    time_t now = time(nullptr);
    if (now > 0) {
        M5.Rtc.setDateTime(gmtime(&now));
    }

    delay(1000);
    M5.Display.fillScreen(BLACK);
    
    // 初回天気情報取得
    updateWeather();
}

void loop() {
    M5.update();
    
    // 1時間ごとに天気情報を更新
    if (millis() - lastWeatherUpdate > WEATHER_UPDATE_INTERVAL) {
        updateWeather();
    }

    // 画面表示を更新(1秒ごと)
    static unsigned long lastDisplayUpdate = 0;
    if (millis() - lastDisplayUpdate > 1000) {
        displayWeather();
        lastDisplayUpdate = millis();
    }

    delay(100);
}

void updateWeather() {
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.println("Updating weather...");

    WiFiClientSecure client;
    client.setInsecure(); // SSL証明書の検証をスキップ(開発用)
    
    HTTPClient http;
    http.begin(client, WEATHER_API_URL);
    http.setTimeout(10000);

    int httpCode = http.GET();
    
    if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();

        // JSONパース
        DynamicJsonDocument doc(16384); // 気象庁APIのレスポンスは大きいのでバッファを拡大
        DeserializationError error = deserializeJson(doc, payload);

        if (!error) {
            // 気象庁APIのJSON構造をパース
            // 構造: [{ "timeSeries": [{ "areas": [{ "weathers": [...] }] }] }]
            if (doc.is<JsonArray>() && doc.size() > 0) {
                JsonObject forecast = doc[0];
                
                if (forecast.containsKey("timeSeries")) {
                    JsonArray timeSeries = forecast["timeSeries"];
                    
                    // 天気情報(timeSeries[0])
                    if (timeSeries.size() > 0 && timeSeries[0].containsKey("areas")) {
                        JsonArray areas = timeSeries[0]["areas"];
                        if (areas.size() > 0 && areas[0].containsKey("weathers")) {
                            JsonArray weathers = areas[0]["weathers"];
                            if (weathers.size() > 0) {
                                String weatherStr = weathers[0].as<String>();
                                todayWeather.weather = weatherStr;
                                todayWeather.code = parseWeatherCode(weatherStr);
                            }
                            if (weathers.size() > 1) {
                                String weatherStr2 = weathers[1].as<String>();
                                tomorrowWeather.weather = weatherStr2;
                                tomorrowWeather.code = parseWeatherCode(weatherStr2);
                            }
                        }
                    }
                    
                    // 気温情報(timeSeries[2])
                    if (timeSeries.size() > 2 && timeSeries[2].containsKey("areas")) {
                        JsonArray tempAreas = timeSeries[2]["areas"];
                        if (tempAreas.size() > 0 && tempAreas[0].containsKey("temps")) {
                            JsonArray temps = tempAreas[0]["temps"];
                            if (temps.size() >= 2) {
                                todayWeather.tempMax = temps[0].as<int>();
                                todayWeather.tempMin = temps[1].as<int>();
                            }
                        }
                    }
                }
            }
            
            weatherUpdated = true;
            lastWeatherUpdate = millis();
        } else {
            M5.Display.setCursor(0, 50);
            M5.Display.print("JSON Error: ");
            M5.Display.println(error.c_str());
        }
    } else {
        lastError = "HTTP Error: " + String(httpCode);
        M5.Display.setCursor(0, 100);
        M5.Display.print("HTTP Error: ");
        M5.Display.println(httpCode);
    }

    http.end();
    delay(1000); // APIへの負荷を軽減
}

WeatherCode parseWeatherCode(String weather) {
    weather.toLowerCase();
    if (weather.indexOf("晴") >= 0) {
        return SUNNY;
    } else if (weather.indexOf("雨") >= 0) {
        return RAIN;
    } else if (weather.indexOf("雪") >= 0) {
        return SNOW;
    } else {
        return CLOUDY;
    }
}

void displayWeather() {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    
    int y = 20;
    
    // 現在時刻表示
    time_t now = time(nullptr);
    struct tm* timeinfo = localtime(&now);
    M5.Display.setCursor(10, y);
    M5.Display.printf("%04d/%02d/%02d %02d:%02d:%02d\n",
                      timeinfo->tm_year + 1900,
                      timeinfo->tm_mon + 1,
                      timeinfo->tm_mday,
                      timeinfo->tm_hour,
                      timeinfo->tm_min,
                      timeinfo->tm_sec);
    y += 30;

    if (!weatherUpdated) {
        M5.Display.setCursor(10, y);
        M5.Display.println("Weather data not available");
        if (lastError.length() > 0) {
            M5.Display.setCursor(10, y + 25);
            M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
            M5.Display.println(lastError);
        }
        return;
    }

    // 今日の天気
    M5.Display.setCursor(10, y);
    M5.Display.print("Today: ");
    M5.Display.println(todayWeather.weather);
    y += 25;

    // 天気アイコン(簡易版)
    M5.Display.setCursor(10, y);
    drawWeatherIcon(10, y, todayWeather.code);
    y += 60;

    // 気温
    M5.Display.setCursor(10, y);
    M5.Display.printf("Temp: %d / %d C\n", todayWeather.tempMax, todayWeather.tempMin);
    y += 25;

    // 明日の天気
    M5.Display.setCursor(10, y);
    M5.Display.print("Tomorrow: ");
    M5.Display.println(tomorrowWeather.weather);
    y += 25;

    // 明日の天気アイコン
    M5.Display.setCursor(10, y);
    drawWeatherIcon(10, y, tomorrowWeather.code);
    y += 60;

    // 更新時刻
    M5.Display.setCursor(10, M5.Display.height() - 30);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
    M5.Display.print("Last update: ");
    unsigned long minutesAgo = (millis() - lastWeatherUpdate) / 60000;
    if (minutesAgo < 60) {
        M5.Display.print(minutesAgo);
        M5.Display.println(" min ago");
    } else {
        unsigned long hoursAgo = minutesAgo / 60;
        M5.Display.print(hoursAgo);
        M5.Display.println(" hours ago");
    }
    
    // WiFi接続状態
    M5.Display.setCursor(10, M5.Display.height() - 15);
    if (WiFi.status() == WL_CONNECTED) {
        M5.Display.setTextColor(GREEN, BLACK);
        M5.Display.print("WiFi: OK");
    } else {
        M5.Display.setTextColor(RED, BLACK);
        M5.Display.print("WiFi: Disconnected");
    }
    M5.Display.setTextColor(WHITE, BLACK);
}

void drawWeatherIcon(int x, int y, WeatherCode code) {
    int size = 50;
    switch (code) {
        case SUNNY:
            // 晴れ: 黄色い円
            M5.Display.fillCircle(x + size/2, y + size/2, size/2 - 5, YELLOW);
            break;
        case CLOUDY:
            // 曇り: 灰色の雲
            M5.Display.fillCircle(x + size/3, y + size/2, size/4, DARKGREY);
            M5.Display.fillCircle(x + size*2/3, y + size/2, size/4, DARKGREY);
            M5.Display.fillRect(x + size/4, y + size/2 - size/8, size/2, size/4, DARKGREY);
            break;
        case RAIN:
            // 雨: 灰色の雲 + 青い線
            M5.Display.fillCircle(x + size/3, y + size/3, size/4, DARKGREY);
            M5.Display.fillCircle(x + size*2/3, y + size/3, size/4, DARKGREY);
            M5.Display.fillRect(x + size/4, y + size/3 - size/8, size/2, size/4, DARKGREY);
            for (int i = 0; i < 5; i++) {
                M5.Display.drawLine(x + size/4 + i*size/8, y + size*2/3, 
                                   x + size/4 + i*size/8, y + size*3/4, BLUE);
            }
            break;
        case SNOW:
            // 雪: 灰色の雲 + 白い点
            M5.Display.fillCircle(x + size/3, y + size/3, size/4, DARKGREY);
            M5.Display.fillCircle(x + size*2/3, y + size/3, size/4, DARKGREY);
            M5.Display.fillRect(x + size/4, y + size/3 - size/8, size/2, size/4, DARKGREY);
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    M5.Display.fillCircle(x + size/3 + i*size/6, y + size*2/3 + j*size/8, 3, WHITE);
                }
            }
            break;
        default:
            // 不明: グレーの四角
            M5.Display.fillRect(x, y, size, size, DARKGREY);
            break;
    }
}


必要なライブラリ

  • WiFi: ESP32標準ライブラリ
  • WiFiClientSecure: ESP32標準ライブラリ(HTTPS通信用)
  • HTTPClient: ESP32標準ライブラリ
  • ArduinoJson: JSON解析ライブラリ
  • esp_sntp: ESP32標準ライブラリ(時刻同期用)

プログラムの動作

  1. 初期化: M5Stack初期化、WiFi接続、SNTP時刻同期、初回天気情報取得
  2. メインループ: 1時間ごとに天気情報を更新、1秒ごとに画面表示を更新
  3. 天気情報取得: 気象庁APIにHTTPSリクエスト送信、JSONレスポンス解析、天気・気温を抽出
  4. 画面表示: 現在時刻、今日と明日の天気アイコン、気温、最終更新時刻、WiFi接続状態

NTP設定

#define NTP_TIMEZONE "JST-9"  // 日本時間(UTC+9)
#define NTP_SERVER1  "0.pool.ntp.org"
#define NTP_SERVER2  "1.pool.ntp.org"
#define NTP_SERVER3  "2.pool.ntp.org"

気象庁APIのエンドポイント

#define WEATHER_API_URL "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"

天気情報構造体

struct WeatherInfo {
    String weather;      // 天気(晴れ、曇り、雨、雪)
    int tempMax;         // 最高気温
    int tempMin;         // 最低気温
    int pop;             // 降水確率(%)
    WeatherCode code;    // 天気コード
};

HTTPS通信と証明書検証

WiFiClientSecure client;
client.setInsecure(); // SSL証明書の検証をスキップ(開発用)

HTTPClient http;
http.begin(client, WEATHER_API_URL);
http.setTimeout(10000);
int httpCode = http.GET();

注意: 本番環境では、適切なルートCA証明書を設定し、証明書検証を有効にすることを推奨します。

JSON解析

DynamicJsonDocument doc(16384); // 気象庁APIのレスポンスは大きいのでバッファを拡大
DeserializationError error = deserializeJson(doc, payload);

天気コードの解析

WeatherCode parseWeatherCode(String weather) {
    weather.toLowerCase();
    if (weather.indexOf("晴") >= 0) return SUNNY;
    else if (weather.indexOf("雨") >= 0) return RAIN;
    else if (weather.indexOf("雪") >= 0) return SNOW;
    else return CLOUDY;
}

天気アイコンの描画

void drawWeatherIcon(int x, int y, WeatherCode code) {
    // 晴れ: 黄色い円
    // 曇り: 灰色の雲
    // 雨: 灰色の雲 + 青い線
    // 雪: 灰色の雲 + 白い点
}

気象庁API仕様

エンドポイント

https://www.jma.go.jp/bosai/forecast/data/forecast/{地域コード}.json

レスポンス構造

[
  {
    "timeSeries": [
      {
        "areas": [
          {
            "weathers": ["晴れ", "曇り時々雨"],
            ...
          }
        ]
      },
      ...
      {
        "areas": [
          {
            "temps": ["25", "18"]  // 最高気温、最低気温
          }
        ]
      }
    ]
  }
]

6. WebServer - シンプルWebサーバー

このプログラムは、M5StackデバイスをWiFi経由でWebサーバーとして動作させ、ブラウザからアクセスして音を鳴らすことができるインタラクティブなWebアプリケーションです。HTTPリクエストを受け取り、PWM(Pulse Width Modulation)を使用してスピーカーから音を出力します。

ソース
#include <M5Unified.h>
#include <WiFi.h>
#include <WebServer.h>

// WiFi認証情報は secrets.h から読み込みます
// WebServer ディレクトリ内の secrets.h.example をコピーして secrets.h を作成し、
// 実際の SSID とパスワードを設定してください。
#include "secrets.h"

// 音出力関係の定数
#define LEDC_BIT 13
#define LEDC_FREQ 5000
#define SPEAKER_PIN 5
// 音の情報
const String tones[] = { "c", "d", "e", "f", "g", "a", "b" };
const String tones_jp[] = { "ド", "レ", "ミ", "ファ", "ソ", "ラ", "シ" };
const int freqs[] = { 262, 294, 330, 349, 392, 440, 494 };
// WebServerクラスの変数
WebServer server(80);

void setup() {
  // M5Unifiedの初期化
  auto cfg = M5.config();
  M5.begin(cfg);
  
  // シリアルポートの初期化
  Serial.begin(115200);
  delay(500);
  
  // ディスプレイの初期化
  M5.Display.setTextSize(2);
  M5.Display.setTextColor(WHITE, BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.println("WiFi Connecting...");
  M5.Display.println(ssid);
  
  // アクセスポイントに接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Display.print(".");
  }
  
  // ESP32のIPアドレスを出力
  Serial.println("WiFi Connected.");
  Serial.print("IP = ");
  Serial.println(WiFi.localIP());
  
  // ディスプレイにIPアドレスを表示
  M5.Display.fillScreen(BLACK);
  M5.Display.setCursor(0, 0);
  M5.Display.setTextSize(3);
  M5.Display.println("WiFi Connected!");
  M5.Display.println();
  M5.Display.setTextSize(3);
  M5.Display.print("IP: ");
  M5.Display.println(WiFi.localIP());
  M5.Display.println();
  M5.Display.println("Web Server Ready");
  
  // PWMの初期化
  ledcAttach(SPEAKER_PIN, LEDC_FREQ, LEDC_BIT) ;
  // 処理するアドレスを定義
  server.on("/", handleRoot);
  server.on("/tone", handleTone);
  server.onNotFound(handleNotFound);
  // Webサーバーを起動
  server.begin();
}

void loop() {
  M5.update();
  server.handleClient();
}

void handleRoot() {
  String html;
  int i;
  
  // HTMLを組み立てる
  html = "<!DOCTYPE html>";
  html += "<html>";
  html += "<head>";
  html += "<meta charset=\"utf-8\">";
  html += "<title>音を出す</title>";
  html += "</head>";
  html += "<body>";
  html += "<p>リンクをクリックすると音が出ます</p>";
  html += "<ul>";
  for (i = 0; i < 7; i++) {
    html += "<li><a href=\"/tone?t=";
    html += tones[i];
    html += "\">";
    html += tones_jp[i];
    html += "</a></li>";
  }
  html += "</ul>";
  html += "</body>";
  html += "</html>";
  // HTMLを出力する
  server.send(200, "text/html", html);
}

void handleTone() {
  int i, freq;
  String t_name, msg;

  // 「/tone?t=○」のパラメータが指定されているかどうかを確認
  freq = 0;
  if (server.hasArg("t")) {
    // 「○」の値に応じて、
    // 音の名前と周波数を変数t_name/freqに代入
    for (i = 0; i < 7; i++) {
      if (server.arg("t").equals(tones[i])) {
        t_name = tones_jp[i];
        freq = freqs[i];
        break;
      }
    }
  }
  // 音を出す
  ledcWriteTone(SPEAKER_PIN, freq);
  if (freq == 0) {
    // freqが0の場合はエラーメッセージを変数msgに代入
    msg = "パラメータが正しく指定されていません";
  }
  else {
    // freqが0でなければ、「○の音を出しました」を変数msgに代入
    msg = t_name;
    msg += "の音を出しました";
  }
  // 変数msgの文字列を送信する
  server.send(200, "text/plain; charset=utf-8", msg);
  // 1秒待つ
  delay(1000);
  // 音を止める
  ledcWriteTone(SPEAKER_PIN, 0);
}

void handleNotFound(void) {
  server.send(404, "text/plain", "Not Found");
}


#### 主要機能
  • シンプルなHTTP Webサーバー
  • ブラウザからTab5にアクセスして状態を確認
  • PWMによる音出力制御

7. ChatGPT - ChatGPT API連携

このプログラムは、OpenAIのChatGPT APIを使用して、M5Stack Tab5からChatGPTと対話できるアプリケーションです。タッチスクリーンでプロンプトを選択し、ChatGPTからの応答を画面に表示します。

  • WiFi接続: Tab5のSDIO2ピンを使用したWiFi接続
  • ChatGPT API通信: OpenAIのChatGPT APIとHTTPS通信
  • タッチ操作: 画面を4分割して直感的な操作
  • プロンプト選択: 5つのプリセットプロンプトから選択
  • レスポンス表示: ChatGPTからの応答を画面に表示
  • エラーハンドリング: HTTPエラーやJSONパースエラーを処理
ソース
/*
 * M5Stack Tab5 ChatGPT 通信アプリ
 * OpenAI APIを使用してChatGPTと対話するアプリケーション
 * 
 * 参考: https://deviceplus.jp/mc-general/m5stack-chatgpt-01/
 */

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// WiFi認証情報とOpenAI APIキーは secrets.h から読み込みます
#include "secrets.h"

// OpenAI API設定
#define OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
#define MODEL "gpt-3.5-turbo"  // または "gpt-4"

// 画面表示用
String chatHistory = "";
String currentResponse = "";
bool isWaitingResponse = false;
unsigned long lastUpdate = 0;

// プロンプトテンプレート
const char* prompts[] = {
    "面白い短い話をしてください",
    "今日の天気について教えてください",
    "簡単ななぞなぞを出してください",
    "今日の運勢を教えてください",
    "短い詩を作ってください"
};
int promptIndex = 0;

void setup() {
    M5.begin();
    delay(1000);

    M5.Display.setRotation(3); // 横向き
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.println("ChatGPT App");
    M5.Display.println("Initializing...");

    // WiFi設定
    // Arduino IDEでM5Tab5ボードを選択した場合、定義済みのデフォルトピンを使用できます
    WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
        BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
        BOARD_SDIO_ESP_HOSTED_RESET);
    
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    M5.Display.print("Connecting WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        M5.Display.print(".");
    }
    M5.Display.println("\nWiFi Connected!");
    M5.Display.print("IP: ");
    M5.Display.println(WiFi.localIP());

    delay(1000);
    M5.Display.fillScreen(BLACK);
    displayWelcome();
}

void loop() {
    M5.update();

    // タッチ操作の処理
    auto t = M5.Touch.getDetail();
    if (t.wasPressed()) {
        int x = t.x;
        int y = t.y;
        
        // 画面を4分割して操作エリアを定義
        int screenWidth = M5.Display.width();
        int screenHeight = M5.Display.height();
        
        // 左上: 前のプロンプト
        if (x < screenWidth / 2 && y < screenHeight / 2) {
            promptIndex = (promptIndex - 1 + 5) % 5;
            displayPromptSelection();
        }
        // 右上: 次のプロンプト
        else if (x >= screenWidth / 2 && y < screenHeight / 2) {
            promptIndex = (promptIndex + 1) % 5;
            displayPromptSelection();
        }
        // 左下: プロンプト送信
        else if (x < screenWidth / 2 && y >= screenHeight / 2) {
            if (!isWaitingResponse) {
                sendChatGPTRequest(prompts[promptIndex]);
            }
        }
        // 右下: 画面クリア
        else if (x >= screenWidth / 2 && y >= screenHeight / 2) {
            chatHistory = "";
            currentResponse = "";
            displayWelcome();
        }
    }

    // 画面更新(1秒ごと)
    if (millis() - lastUpdate > 1000) {
        displayStatus();
        lastUpdate = millis();
    }

    delay(100);
}

void displayWelcome() {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.println("ChatGPT App");
    M5.Display.println("============");
    M5.Display.println("");
    M5.Display.println("Touch areas:");
    M5.Display.println("Top-Left: Prev prompt");
    M5.Display.println("Top-Right: Next prompt");
    M5.Display.println("Bottom-Left: Send");
    M5.Display.println("Bottom-Right: Clear");
    M5.Display.println("");
    displayPromptSelection();
}

void displayPromptSelection() {
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(CYAN, BLACK);
    M5.Display.setCursor(10, 200);
    M5.Display.print("Prompt [");
    M5.Display.print(promptIndex + 1);
    M5.Display.print("/5]: ");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.println(prompts[promptIndex]);
}

void displayStatus() {
    int y = M5.Display.height() - 30;
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
    M5.Display.setCursor(10, y);
    
    if (WiFi.status() == WL_CONNECTED) {
        M5.Display.setTextColor(GREEN, BLACK);
        M5.Display.print("WiFi: OK  ");
    } else {
        M5.Display.setTextColor(RED, BLACK);
        M5.Display.print("WiFi: NG  ");
    }
    
    if (isWaitingResponse) {
        M5.Display.setTextColor(YELLOW, BLACK);
        M5.Display.print("Waiting...");
    } else {
        M5.Display.setTextColor(WHITE, BLACK);
        M5.Display.print("Ready");
    }
    M5.Display.setTextColor(WHITE, BLACK);
}

void sendChatGPTRequest(String prompt) {
    isWaitingResponse = true;
    currentResponse = "";
    
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextColor(YELLOW, BLACK);
    M5.Display.println("Sending request...");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.print("Prompt: ");
    M5.Display.println(prompt);
    M5.Display.println("");
    
    WiFiClientSecure client;
    client.setInsecure(); // SSL証明書の検証をスキップ(開発用)
    
    HTTPClient http;
    http.begin(client, OPENAI_API_URL);
    http.setTimeout(30000); // 30秒タイムアウト
    
    // ヘッダー設定
    http.addHeader("Content-Type", "application/json");
    http.addHeader("Authorization", String("Bearer ") + openaiApiKey);
    
    // リクエストボディの作成
    DynamicJsonDocument doc(2048);
    doc["model"] = MODEL;
    JsonArray messages = doc.createNestedArray("messages");
    JsonObject message = messages.createNestedObject();
    message["role"] = "user";
    message["content"] = prompt;
    doc["max_tokens"] = 200; // レスポンスの最大トークン数
    doc["temperature"] = 0.7; // 創造性の設定(0.0-2.0)
    
    String requestBody;
    serializeJson(doc, requestBody);
    
    // リクエスト送信
    int httpCode = http.POST(requestBody);
    
    if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        
        // JSONパース
        DynamicJsonDocument responseDoc(8192);
        DeserializationError error = deserializeJson(responseDoc, payload);
        
        if (!error) {
            if (responseDoc.containsKey("choices") && responseDoc["choices"].size() > 0) {
                JsonObject choice = responseDoc["choices"][0];
                if (choice.containsKey("message")) {
                    JsonObject message = choice["message"];
                    if (message.containsKey("content")) {
                        currentResponse = message["content"].as<String>();
                        displayResponse(prompt, currentResponse);
                    }
                }
            } else {
                displayError("No response in JSON");
            }
        } else {
            displayError("JSON parse error");
        }
    } else {
        String errorMsg = "HTTP Error: " + String(httpCode);
        displayError(errorMsg);
    }
    
    http.end();
    isWaitingResponse = false;
}

void displayResponse(String prompt, String response) {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    
    // プロンプト表示
    M5.Display.setTextColor(CYAN, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.print("Q: ");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.println(prompt);
    
    // 区切り線
    M5.Display.drawLine(10, 50, M5.Display.width() - 10, 50, DARKGREY);
    
    // レスポンス表示
    M5.Display.setTextColor(GREEN, BLACK);
    M5.Display.setCursor(10, 60);
    M5.Display.print("A: ");
    M5.Display.setTextColor(WHITE, BLACK);
    
    // 長いテキストを折り返して表示
    int lineHeight = 25;
    int maxWidth = M5.Display.width() - 20;
    int y = 80;
    int x = 10;
    
    String displayText = response;
    int textLength = displayText.length();
    int charsPerLine = maxWidth / 12; // おおよその文字数
    
    for (int i = 0; i < textLength; i += charsPerLine) {
        int endPos = (i + charsPerLine < textLength) ? (i + charsPerLine) : textLength;
        String line = displayText.substring(i, endPos);
        M5.Display.setCursor(x, y);
        M5.Display.println(line);
        y += lineHeight;
        
        // 画面からはみ出したら終了
        if (y > M5.Display.height() - 50) {
            M5.Display.setCursor(x, y);
            M5.Display.println("...");
            break;
        }
    }
    
    // チャット履歴に追加
    chatHistory += "Q: " + prompt + "\n";
    chatHistory += "A: " + response + "\n\n";
    
    // 操作説明
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
    M5.Display.setCursor(10, M5.Display.height() - 30);
    M5.Display.setTextColor(DARKGREY, BLACK);
    M5.Display.println("Touch Bottom-Left to send, Bottom-Right to clear");
}

void displayError(String errorMsg) {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(RED, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.println("Error!");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.setCursor(10, 40);
    M5.Display.println(errorMsg);
    M5.Display.setCursor(10, 80);
    M5.Display.println("Check Serial Monitor");
    M5.Display.println("for details.");
}

必要なライブラリ

  • ArduinoJson: JSONパースライブラリ
  • WiFi: ESP32標準ライブラリ
  • HTTPClient: ESP32標準ライブラリ
  • WiFiClientSecure: ESP32標準ライブラリ

セットアップ

  1. OpenAI APIキーの取得

    • OpenAI Platformにアクセス
    • アカウントを作成(またはログイン)
    • "Create new secret key"をクリックしてAPIキーを生成
    • 生成されたAPIキーをコピー(再表示できないため注意)
  2. WiFi設定とAPIキー設定

    • secrets.h.exampleをコピーしてsecrets.hを作成してください:
    cp secrets.h.example secrets.h
    
    • secrets.hファイル内のssidpasswordopenaiApiKeyを実際の値に編集してください:
    const char *ssid = "YOUR_WIFI_SSID";
    const char *password = "YOUR_WIFI_PASSWORD";
    const char *openaiApiKey = "sk-..."; // OpenAI APIキー
    

基本操作

  1. WiFiのSSID、パスワード、OpenAI APIキーを設定
  2. プログラムをアップロード
  3. 起動すると自動的にWiFiに接続
  4. 画面をタッチして操作:
操作エリア 動作
左上 前のプロンプトに切り替え
右上 次のプロンプトに切り替え
左下 選択したプロンプトをChatGPTに送信
右下 画面をクリア

プロンプト

デフォルトで以下の5つのプロンプトが用意されています:

  1. "面白い短い話をしてください"
  2. "今日の天気について教えてください"
  3. "簡単ななぞなぞを出してください"
  4. "今日の運勢を教えてください"
  5. "短い詩を作ってください"

プログラムの動作

  1. 初期化: M5Stack初期化、WiFi接続、ウェルカム画面表示
  2. メインループ: タッチ操作の検出、画面更新(1秒ごと)
  3. ChatGPT APIリクエスト: HTTPSでOpenAI APIにアクセス、JSONリクエスト作成、レスポンスパース
  4. 画面表示: プロンプト選択画面、ChatGPTからの応答表示、エラーメッセージ、WiFi接続状態

OpenAI APIへのアクセス

WiFiClientSecure client;
client.setInsecure(); // SSL証明書の検証をスキップ(開発用)

HTTPClient http;
http.begin(client, OPENAI_API_URL);
http.addHeader("Authorization", String("Bearer ") + openaiApiKey);

注意: setInsecure()は開発用です。本番環境では適切なSSL証明書を設定してください。

JSONリクエストの作成

DynamicJsonDocument doc(2048);
doc["model"] = MODEL;
JsonArray messages = doc.createNestedArray("messages");
JsonObject message = messages.createNestedObject();
message["role"] = "user";
message["content"] = prompt;

ChatGPT APIのリクエスト形式に合わせてJSONを作成します。

JSONレスポンスのパース

DynamicJsonDocument responseDoc(8192);
deserializeJson(responseDoc, payload);
String response = responseDoc["choices"][0]["message"]["content"].as<String>();

ChatGPT APIのレスポンスからテキストを抽出します。

タッチ操作の処理

auto t = M5.Touch.getDetail();
if (t.wasPressed()) {
    int x = t.x;
    int y = t.y;
    // 画面を4分割して操作エリアを判定
}

画面を4分割して、タッチ位置に応じて操作を実行します。


8. MQTT - MQTTブローカー接続・Pub/Sub

このプログラムは、M5Stack Tab5のWiFi機能を使用して、MQTTブローカーに接続し、メッセージの送受信を行います。IoTデバイス間の通信や、センサーデータの送信、リモート制御などに活用できます。

ソース
/*
 * M5Stack Tab5 ChatGPT 通信アプリ
 * OpenAI APIを使用してChatGPTと対話するアプリケーション
 * 
 * 参考: https://deviceplus.jp/mc-general/m5stack-chatgpt-01/
 */

#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// WiFi認証情報とOpenAI APIキーは secrets.h から読み込みます
#include "secrets.h"

// OpenAI API設定
#define OPENAI_API_URL "https://api.openai.com/v1/chat/completions"
#define MODEL "gpt-3.5-turbo"  // または "gpt-4"

// 画面表示用
String chatHistory = "";
String currentResponse = "";
bool isWaitingResponse = false;
unsigned long lastUpdate = 0;

// プロンプトテンプレート
const char* prompts[] = {
    "面白い短い話をしてください",
    "今日の天気について教えてください",
    "簡単ななぞなぞを出してください",
    "今日の運勢を教えてください",
    "短い詩を作ってください"
};
int promptIndex = 0;

void setup() {
    M5.begin();
    delay(1000);

    M5.Display.setRotation(3); // 横向き
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.fillScreen(BLACK);
    M5.Display.setCursor(0, 0);
    M5.Display.println("ChatGPT App");
    M5.Display.println("Initializing...");

    // WiFi設定
    // Arduino IDEでM5Tab5ボードを選択した場合、定義済みのデフォルトピンを使用できます
    WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
        BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
        BOARD_SDIO_ESP_HOSTED_RESET);
    
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);

    M5.Display.print("Connecting WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        M5.Display.print(".");
    }
    M5.Display.println("\nWiFi Connected!");
    M5.Display.print("IP: ");
    M5.Display.println(WiFi.localIP());

    delay(1000);
    M5.Display.fillScreen(BLACK);
    displayWelcome();
}

void loop() {
    M5.update();

    // タッチ操作の処理
    auto t = M5.Touch.getDetail();
    if (t.wasPressed()) {
        int x = t.x;
        int y = t.y;
        
        // 画面を4分割して操作エリアを定義
        int screenWidth = M5.Display.width();
        int screenHeight = M5.Display.height();
        
        // 左上: 前のプロンプト
        if (x < screenWidth / 2 && y < screenHeight / 2) {
            promptIndex = (promptIndex - 1 + 5) % 5;
            displayPromptSelection();
        }
        // 右上: 次のプロンプト
        else if (x >= screenWidth / 2 && y < screenHeight / 2) {
            promptIndex = (promptIndex + 1) % 5;
            displayPromptSelection();
        }
        // 左下: プロンプト送信
        else if (x < screenWidth / 2 && y >= screenHeight / 2) {
            if (!isWaitingResponse) {
                sendChatGPTRequest(prompts[promptIndex]);
            }
        }
        // 右下: 画面クリア
        else if (x >= screenWidth / 2 && y >= screenHeight / 2) {
            chatHistory = "";
            currentResponse = "";
            displayWelcome();
        }
    }

    // 画面更新(1秒ごと)
    if (millis() - lastUpdate > 1000) {
        displayStatus();
        lastUpdate = millis();
    }

    delay(100);
}

void displayWelcome() {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.println("ChatGPT App");
    M5.Display.println("============");
    M5.Display.println("");
    M5.Display.println("Touch areas:");
    M5.Display.println("Top-Left: Prev prompt");
    M5.Display.println("Top-Right: Next prompt");
    M5.Display.println("Bottom-Left: Send");
    M5.Display.println("Bottom-Right: Clear");
    M5.Display.println("");
    displayPromptSelection();
}

void displayPromptSelection() {
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(CYAN, BLACK);
    M5.Display.setCursor(10, 200);
    M5.Display.print("Prompt [");
    M5.Display.print(promptIndex + 1);
    M5.Display.print("/5]: ");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.println(prompts[promptIndex]);
}

void displayStatus() {
    int y = M5.Display.height() - 30;
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
    M5.Display.setCursor(10, y);
    
    if (WiFi.status() == WL_CONNECTED) {
        M5.Display.setTextColor(GREEN, BLACK);
        M5.Display.print("WiFi: OK  ");
    } else {
        M5.Display.setTextColor(RED, BLACK);
        M5.Display.print("WiFi: NG  ");
    }
    
    if (isWaitingResponse) {
        M5.Display.setTextColor(YELLOW, BLACK);
        M5.Display.print("Waiting...");
    } else {
        M5.Display.setTextColor(WHITE, BLACK);
        M5.Display.print("Ready");
    }
    M5.Display.setTextColor(WHITE, BLACK);
}

void sendChatGPTRequest(String prompt) {
    isWaitingResponse = true;
    currentResponse = "";
    
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setCursor(10, 10);
    M5.Display.setTextColor(YELLOW, BLACK);
    M5.Display.println("Sending request...");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.print("Prompt: ");
    M5.Display.println(prompt);
    M5.Display.println("");
    
    WiFiClientSecure client;
    client.setInsecure(); // SSL証明書の検証をスキップ(開発用)
    
    HTTPClient http;
    http.begin(client, OPENAI_API_URL);
    http.setTimeout(30000); // 30秒タイムアウト
    
    // ヘッダー設定
    http.addHeader("Content-Type", "application/json");
    http.addHeader("Authorization", String("Bearer ") + openaiApiKey);
    
    // リクエストボディの作成
    DynamicJsonDocument doc(2048);
    doc["model"] = MODEL;
    JsonArray messages = doc.createNestedArray("messages");
    JsonObject message = messages.createNestedObject();
    message["role"] = "user";
    message["content"] = prompt;
    doc["max_tokens"] = 200; // レスポンスの最大トークン数
    doc["temperature"] = 0.7; // 創造性の設定(0.0-2.0)
    
    String requestBody;
    serializeJson(doc, requestBody);
    
    // リクエスト送信
    int httpCode = http.POST(requestBody);
    
    if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        
        // JSONパース
        DynamicJsonDocument responseDoc(8192);
        DeserializationError error = deserializeJson(responseDoc, payload);
        
        if (!error) {
            if (responseDoc.containsKey("choices") && responseDoc["choices"].size() > 0) {
                JsonObject choice = responseDoc["choices"][0];
                if (choice.containsKey("message")) {
                    JsonObject message = choice["message"];
                    if (message.containsKey("content")) {
                        currentResponse = message["content"].as<String>();
                        displayResponse(prompt, currentResponse);
                    }
                }
            } else {
                displayError("No response in JSON");
            }
        } else {
            displayError("JSON parse error");
        }
    } else {
        String errorMsg = "HTTP Error: " + String(httpCode);
        displayError(errorMsg);
    }
    
    http.end();
    isWaitingResponse = false;
}

void displayResponse(String prompt, String response) {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    
    // プロンプト表示
    M5.Display.setTextColor(CYAN, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.print("Q: ");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.println(prompt);
    
    // 区切り線
    M5.Display.drawLine(10, 50, M5.Display.width() - 10, 50, DARKGREY);
    
    // レスポンス表示
    M5.Display.setTextColor(GREEN, BLACK);
    M5.Display.setCursor(10, 60);
    M5.Display.print("A: ");
    M5.Display.setTextColor(WHITE, BLACK);
    
    // 長いテキストを折り返して表示
    int lineHeight = 25;
    int maxWidth = M5.Display.width() - 20;
    int y = 80;
    int x = 10;
    
    String displayText = response;
    int textLength = displayText.length();
    int charsPerLine = maxWidth / 12; // おおよその文字数
    
    for (int i = 0; i < textLength; i += charsPerLine) {
        int endPos = (i + charsPerLine < textLength) ? (i + charsPerLine) : textLength;
        String line = displayText.substring(i, endPos);
        M5.Display.setCursor(x, y);
        M5.Display.println(line);
        y += lineHeight;
        
        // 画面からはみ出したら終了
        if (y > M5.Display.height() - 50) {
            M5.Display.setCursor(x, y);
            M5.Display.println("...");
            break;
        }
    }
    
    // チャット履歴に追加
    chatHistory += "Q: " + prompt + "\n";
    chatHistory += "A: " + response + "\n\n";
    
    // 操作説明
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_12);
    M5.Display.setCursor(10, M5.Display.height() - 30);
    M5.Display.setTextColor(DARKGREY, BLACK);
    M5.Display.println("Touch Bottom-Left to send, Bottom-Right to clear");
}

void displayError(String errorMsg) {
    M5.Display.fillScreen(BLACK);
    M5.Display.setFont(&fonts::lgfxJapanMinchoP_16);
    M5.Display.setTextColor(RED, BLACK);
    M5.Display.setCursor(10, 10);
    M5.Display.println("Error!");
    M5.Display.setTextColor(WHITE, BLACK);
    M5.Display.setCursor(10, 40);
    M5.Display.println(errorMsg);
    M5.Display.setCursor(10, 80);
    M5.Display.println("Check Serial Monitor");
    M5.Display.println("for details.");
}

必要なライブラリ

  • WiFi.h: WiFi機能

  • WebServer.h: HTTPサーバー機能

  • 音出力定数:

    • LEDC_BIT: PWMの分解能(13ビット = 8192段階)
    • LEDC_FREQ: PWMの基本周波数(5000Hz)
    • SPEAKER_PIN: スピーカー接続ピン(5番ピン)

解説

  1. WiFi接続(35-40行目)
  // アクセスポイントに接続
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    M5.Display.print(".");
  }
  1. loop()関数の処理(69-72行目)
void loop() {
  M5.update();
  server.handleClient();
}
  • M5.update(): M5Stackの状態を更新
  • server.handleClient(): クライアントからのリクエストを処理
  1. ハンドラ関数
    handleRoot()関数(74-100行目)
  • 機能: ルートパス(/)へのアクセス時にHTMLページを返す
  • HTML内容:
    • UTF-8エンコーディング指定
    • 7つの音階へのリンクをリスト表示
    • 各リンクは/tone?t=○形式
  • レスポンス: HTTPステータス200、Content-Type: text/html

handleTone()関数(102-136行目)

void handleTone() {
  int i, freq;
  String t_name, msg;

  // 「/tone?t=○」のパラメータが指定されているかどうかを確認
  freq = 0;
  if (server.hasArg("t")) {
    // 「○」の値に応じて、
    // 音の名前と周波数を変数t_name/freqに代入
    for (i = 0; i < 7; i++) {
      if (server.arg("t").equals(tones[i])) {
        t_name = tones_jp[i];
        freq = freqs[i];
        break;
      }
    }
  }
  // 音を出す
  ledcWriteTone(SPEAKER_PIN, freq);
  if (freq == 0) {
    // freqが0の場合はエラーメッセージを変数msgに代入
    msg = "パラメータが正しく指定されていません";
  }
  else {
    // freqが0でなければ、「○の音を出しました」を変数msgに代入
    msg = t_name;
    msg += "の音を出しました";
  }
  // 変数msgの文字列を送信する
  server.send(200, "text/plain; charset=utf-8", msg);
  // 1秒待つ
  delay(1000);
  // 音を止める
  ledcWriteTone(SPEAKER_PIN, 0);
}
  • 機能: /toneパスへのアクセス時に音を鳴らす
  • 処理フロー:
    1. クエリパラメータtを取得
    2. パラメータ値から対応する周波数を検索
    3. PWMで音を出力(1秒間)
    4. レスポンスを送信
    5. 音を停止
  • エラーハンドリング: 無効なパラメータの場合はエラーメッセージを返す

handleNotFound()関数(138-140行目)

140:webserver/webserver.ino
void handleNotFound(void) {
  server.send(404, "text/plain", "Not Found");
}
  • 機能: 未定義のパスへのアクセス時に404エラーを返す

主要APIの詳細解説

1.WebServer API(ESP32 Arduino Core)

WebServer server(port)

WebServerオブジェクトを生成します。

  • 引数: uint16_t port(HTTPポート番号、通常は80)
  • 使用例:
WebServer server(80);  // ポート80でHTTPサーバーを起動

server.begin()

Webサーバーを起動します。

  • 戻り値: なし(void)
  • 機能: 指定されたポートでHTTPサーバーを開始
  • 注意: setup()内で一度だけ呼び出す

server.handleClient()

クライアントからのリクエストを処理します。

  • 戻り値: なし(void)
  • 機能: 受信したHTTPリクエストを処理し、対応するハンドラ関数を呼び出す
  • 呼び出しタイミング: loop()内で定期的に呼び出す必要がある

server.on(path, handler)

URLパスとハンドラ関数を関連付けます。

  • 引数:
    • const char* path: URLパス(例: "/", "/tone")
    • THandlerFunction handler: ハンドラ関数(戻り値void、引数なし)
  • 戻り値: なし(void)
  • 使用例:
server.on("/", handleRoot);
server.on("/tone", handleTone);

server.onNotFound(handler)

未定義のパスへのアクセス時のハンドラを設定します。

  • 引数: THandlerFunction handler(ハンドラ関数)
  • 戻り値: なし(void)
  • 使用例:
server.onNotFound(handleNotFound);

server.send(code, content_type, content)

HTTPレスポンスを送信します。

  • 引数:
    • int code: HTTPステータスコード(200, 404など)
    • const char* content_type: Content-Typeヘッダー(例: "text/html", "text/plain")
    • const String& content: レスポンスボディ
  • 戻り値: なし(void)
  • 使用例:
server.send(200, "text/html", "<html>...</html>");
server.send(404, "text/plain", "Not Found");

server.hasArg(name)

クエリパラメータまたはPOSTパラメータが存在するか確認します。

  • 引数: const String& name(パラメータ名)
  • 戻り値: bool(存在する場合true
  • 使用例:
if (server.hasArg("t")) {
    // パラメータ"t"が存在する
}

server.arg(name)

クエリパラメータまたはPOSTパラメータの値を取得します。

  • 引数: const String& name(パラメータ名)
  • 戻り値: String(パラメータの値)
  • 使用例:
String value = server.arg("t");

server.method()

HTTPメソッドを取得します。

  • 戻り値: HTTPMethod列挙型
    • HTTP_GET: GETリクエスト
    • HTTP_POST: POSTリクエスト
    • HTTP_PUT: PUTリクエスト
    • HTTP_DELETE: DELETEリクエスト
    • など

server.uri()

リクエストされたURIを取得します。

  • 戻り値: String(URIパス)

server.client()

接続しているクライアントオブジェクトを取得します。

  • 戻り値: WiFiClient(クライアントオブジェクトへの参照)

PWM/LEDC API(ESP32 Arduino Core)

ledcAttach(pin, freq, bit)

PWMチャンネルをピンにアタッチします。

  • 引数:
    • uint8_t pin: ピン番号
    • uint32_t freq: PWM周波数(Hz)
    • uint8_t bit: 分解能(ビット数、8-16)
  • 戻り値: int(チャンネル番号、エラー時は-1)
  • 機能: 指定されたピンにPWM機能を割り当て
  • 使用例:
ledcAttach(5, 5000, 13);  // ピン5に5000Hz、13ビット分解能のPWMを設定

ledcWriteTone(pin, freq)

指定された周波数の音を出力します。

  • 引数:
    • uint8_t pin: ピン番号
    • double freq: 周波数(Hz、0で停止)
  • 戻り値: なし(void)
  • 機能: PWMを使用して指定周波数の音を出力
  • 使用例:
ledcWriteTone(5, 440);  // ピン5から440Hz(ラ)の音を出力
ledcWriteTone(5, 0);   // 音を停止

ledcWrite(pin, duty)

PWMのデューティ比を設定します。

  • 引数:
    • uint8_t pin: ピン番号
    • uint32_t duty: デューティ比(0-最大値)
  • 戻り値: なし(void)
  • 機能: PWMのデューティ比を設定(音の出力にはledcWriteTone()を使用)

WiFi API(ESP32 Arduino Core)

WiFi.begin(ssid, password)

WiFi接続を開始します。

  • 引数:
    • const char* ssid: SSID
    • const char* password: パスワード
  • 戻り値: wl_status_t(接続状態)
  • 注意: 非同期処理のため、接続完了まで待つ必要がある

WiFi.status()

WiFi接続状態を取得します。

  • 戻り値: wl_status_t
    • WL_CONNECTED: 接続済み
    • WL_DISCONNECTED: 未接続
    • WL_CONNECT_FAILED: 接続失敗
    • など

WiFi.localIP()

ローカルIPアドレスを取得します。

  • 戻り値: IPAddress(IPアドレス)

プログラムの特徴

  1. シンプルなWebインターフェース: HTMLページを動的に生成
  2. インタラクティブな操作: ブラウザからクリックで音を鳴らせる
  3. 音階の実装: 7つの音階(ドレミファソラシ)をサポート
  4. エラーハンドリング: 無効なパラメータに対する適切な処理
  5. 視覚的フィードバック: ディスプレイに接続状態とIPアドレスを表示

ブラウザからのアクセス

  1. WiFi接続後、ディスプレイに表示されたIPアドレスを確認

    • 例: 192.168.1.100
  2. ブラウザでアクセス

    • http://192.168.1.100/ にアクセス
    • 音階のリストが表示される
  3. 音を鳴らす

    • リストから音階をクリック
    • 例: 「ド」をクリック → /tone?t=c にアクセス
    • スピーカーから「ド」の音が1秒間鳴る

POSTリクエストの処理

void handlePost() {
  if (server.method() == HTTP_POST) {
    String value = server.arg("freq");
    int freq = value.toInt();
    ledcWriteTone(SPEAKER_PIN, freq);
    server.send(200, "text/plain", "OK");
  }
}

void setup() {
  // ...
  server.on("/post", HTTP_POST, handlePost);
  // ...
}

JSONレスポンスの返却

void handleJSON() {
  String json = "{";
  json += "\"status\":\"ok\",";
  json += "\"ip\":\"" + WiFi.localIP().toString() + "\"";
  json += "}";
  server.send(200, "application/json", json);
}

複数のエンドポイント

void setup() {
  // ...
  server.on("/", handleRoot);
  server.on("/tone", handleTone);
  server.on("/status", handleStatus);
  server.on("/control", handleControl);
  server.onNotFound(handleNotFound);
  // ...
}

9. SNTP

このプログラムは、M5Stack Tab5のWiFi機能を使用して、NTPサーバーから正確な時刻を取得し、デバイスの時刻を同期します。複数のNTPサーバーに対応し、タイムゾーン設定も可能です。

  • SNTP時刻同期: NTPサーバーから正確な時刻を取得
  • 複数サーバー対応: 複数のNTPサーバーに対応(フォールバック機能)
  • タイムゾーン設定: タイムゾーンを設定可能
  • 自動同期: 定期的に時刻を同期
  • 時刻表示: 同期された時刻を画面に表示
ソース
#define NTP_TIMEZONE "UTC-9"
#define NTP_SERVER1  "0.pool.ntp.org"
#define NTP_SERVER2  "1.pool.ntp.org"
#define NTP_SERVER3  "2.pool.ntp.org"

#include <M5Unified.h>
#include <WiFi.h>
#include <esp_sntp.h>

// WiFi認証情報は secrets.h から読み込みます
// secrets.h.example をコピーして secrets.h を作成し、実際のSSIDとパスワードを設定してください
#include "secrets.h"

void setup(void)
{
    M5.begin();
    Serial.begin(115200);

    M5.Display.setFont(&fonts::FreeMonoBoldOblique24pt7b);
    M5.Display.setRotation(3);
    // Arduino IDEでM5Tab5ボードを選択した場合、定義済みのデフォルトピンを使用できます
    WiFi.setPins(BOARD_SDIO_ESP_HOSTED_CLK, BOARD_SDIO_ESP_HOSTED_CMD, BOARD_SDIO_ESP_HOSTED_D0,
                  BOARD_SDIO_ESP_HOSTED_D1, BOARD_SDIO_ESP_HOSTED_D2, BOARD_SDIO_ESP_HOSTED_D3,
                  BOARD_SDIO_ESP_HOSTED_RESET);

    // STAモード(ステーションモード)
    WiFi.mode(WIFI_STA);
    M5.Display.println("WiFi mode set to STA");
    WiFi.begin(ssid, password);
    M5.Display.print("Connecting to ");
    // 接続を待機
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        M5.Display.print(".");
    }
    M5.Display.println("");
    M5.Display.print("Connected to ");
    M5.Display.println(ssid);
    M5.Display.print("IP address: ");
    M5.Display.println(WiFi.localIP());

    if (!M5.Rtc.isEnabled()) {
        Serial.println("RTC not found.");
        M5.Display.println("RTC not found.");
        for (;;) {
            vTaskDelay(500);
        }
    }

    M5.Display.println("RTC found.");
    Serial.println("RTC found.");

    M5.Display.println("SNTP Sync...");

    configTzTime(NTP_TIMEZONE, NTP_SERVER1, NTP_SERVER2, NTP_SERVER3);

    while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED) {
        Serial.print('.');
        delay(1000);
    }

    Serial.println("\r\n NTP Connected.");

    time_t t = time(nullptr) + 1;  // 1秒進める
    while (t > time(nullptr));     // 秒単位で同期
    M5.Rtc.setDateTime(gmtime(&t));
    M5.Display.clear();
}

void loop(void)
{
    static constexpr const char *const wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};

    delay(500);

    auto dt = M5.Rtc.getDateTime();
    Serial.printf("RTC   UTC  :%04d/%02d/%02d (%s)  %02d:%02d:%02d\r\n", dt.date.year, dt.date.month, dt.date.date,
                  wd[dt.date.weekDay], dt.time.hours, dt.time.minutes, dt.time.seconds);
    M5.Display.setCursor(0, 0);
    M5.Display.printf("RTC   UTC  :%04d/%02d/%02d (%s)  %02d:%02d:%02d", dt.date.year, dt.date.month, dt.date.date,
                      wd[dt.date.weekDay], dt.time.hours, dt.time.minutes, dt.time.seconds);

    // ESP32内部タイマー
    auto t = time(nullptr);
    {
        auto tm = gmtime(&t);  // UTC用
        Serial.printf("ESP32 UTC  :%04d/%02d/%02d (%s)  %02d:%02d:%02d\r\n", tm->tm_year + 1900, tm->tm_mon + 1,
                      tm->tm_mday, wd[tm->tm_wday], tm->tm_hour, tm->tm_min, tm->tm_sec);
        M5.Display.setCursor(0, 60);
        M5.Display.printf("ESP32 UTC  :%04d/%02d/%02d (%s)  %02d:%02d:%02d", tm->tm_year + 1900, tm->tm_mon + 1,
                          tm->tm_mday, wd[tm->tm_wday], tm->tm_hour, tm->tm_min, tm->tm_sec);
    }

    {
        auto tm = localtime(&t);  // ローカルタイムゾーン用
        Serial.printf("ESP32 %s:%04d/%02d/%02d (%s)  %02d:%02d:%02d\r\n", NTP_TIMEZONE, tm->tm_year + 1900,
                      tm->tm_mon + 1, tm->tm_mday, wd[tm->tm_wday], tm->tm_hour, tm->tm_min, tm->tm_sec);
        M5.Display.setCursor(0, 120);
        M5.Display.printf("ESP32 %s:%04d/%02d/%02d (%s)  %02d:%02d:%02d", NTP_TIMEZONE, tm->tm_year + 1900,
                          tm->tm_mon + 1, tm->tm_mday, wd[tm->tm_wday], tm->tm_hour, tm->tm_min, tm->tm_sec);
    }
} 

プログラム解説

  1. 初期化

    • M5Stackの初期化
    • WiFi接続
    • SNTP設定(サーバー、タイムゾーン)
  2. 時刻同期

    • NTPサーバーに接続
    • 時刻を取得
    • デバイスの時刻を更新
  3. 時刻表示

    • 同期された時刻を画面に表示
    • シリアルモニターにも出力

NTPサーバーの設定

#define NTP_SERVER1 "0.pool.ntp.org"
#define NTP_SERVER2 "1.pool.ntp.org"
#define NTP_SERVER3 "2.pool.ntp.org"

タイムゾーンの設定

#define NTP_TIMEZONE "UTC-9"  // 日本時間(UTC-9)

SNTPの初期化

sntp_set_time_sync_notification_cb(time_sync_notification_cb);
sntp_servermode_dhcp(1);
sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED);

6. まとめ

謝辞

サンプルプログラムの多くはM5Stack公式からの情報や、先人たちのプログラムをベースに、自分なりに整理したものとなっています。皆様に感謝です。
また、なるべく原典を記載していますので、より詳細に把握したい方はLinkを活用ください。

公式ドキュメント

ライブラリ

サンプル別参考リンク

ChatGPT

MQTT

WebAPI_Wether

SNTP

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?