コンテンツにスキップ
Picossci 2 Conta™ Baseで工作してみよう!

Picossci 2 Conta™ Baseで工作してみよう!

こんにちは、アルバイトの田中です。

今回は、Picossci 2 Conta™ Base(ESP-WROOM-02/RP2350A搭載)をご紹介します。

スイッチサイエンスでは様々な国のモジュールを仕入れて販売をしていますが、スイッチサイエンス自身でもモジュール開発を行っているのはご存知だったでしょうか?

まずはスイッチサイエンスで提唱している「Conta規格」と、Conta規格に対応したマイコンボード「Picossci 2 Conta™ Base」についてご紹介していこうと思います。ちなみにPicossciは、「ピコッシィ」と呼びます!

目次

  1. Conta規格とは??
  2. Picossci 2 Conta™ Baseとは??
  3. IoTなものを作ってみた(第二弾)
  4. 最後に
  5. サンプルコード

Conta規格とは??

詳しくはConta規格ページに定義されていますので、ここでは簡単に概要を紹介していこうと思います。主な規格の内容は以下の通りです。

  • 外形は 2 ㎝×2 ㎝の正方形
  • 電源は3.3 Vを基本とし、必要に応じて5.0 V
  • 利用する信号は、I2Cバス、SPIバス、汎用信号
  • コネクタは、I2Cバス、SPIバス、汎用信号を主体とした3個のコネクタで構成
  • 基板の固定穴は2つ

次に、規格の特徴を少し紹介します。

特徴その1「I2Cバス、SPIバス両方使える」

数珠つなぎに接続するタイプの規格(例えばQwiic等)はどちらかの通信規格に制限されます。Contaでは両方使えるので、使うセンサに合わせて選択可能です。

接続先に決まりのないGPIOに接続されるピンも4つあるため、拡張性が高いです。

特徴その2「サイズによる制約を受けにくい」

実装サイズが大きいモジュールの場合、以下の写真のようにサイズを倍々にして開発が可能です。実装する回路のサイズによってモジュールサイズがバラバラとなり、統合に困る心配がありません。
※倍々にしたサイズのモジュールは細かな制約がありますので、詳しくはConta規格ページを参照ください

特徴その3「見映えがいい」

私の主観ですが、2 ㎝×2 ㎝の外形で正方形のモジュールが並んでいたら統一感あって見映えがいいですよね。基板色を変えて自作のConta規格センサモジュールを並べたら可愛い感じになりそうです。

 

以上、微妙に私見の入ったConta規格の特徴紹介でした。

規格の説明を終えたところで、次にConta規格のモジュールを搭載可能なマイコンボード「Picossci 2 Conta™ Base」について紹介します。

Picossci 2 Conta™ Baseとは??

Picossci 2 Conta™ Baseは、Conta規格のモジュールを4つ搭載可能なRP2350A、ESP-WROOM-02を搭載したマイコンボードです。
※RP2350はRaspberry Pi財団が開発した高性能マイコンです。Arm Cortex-M33コアとRISC-V Hazard3コアのデュアルコア構成が特徴的です。
※ESP-WROOM-02はEspressif Systems社が開発したWi-Fi機能搭載のマイコンです。Wi-Fiの2.4 GHz帯(IEEE 802.11 b/g/n)を使用できます。2015年頃に発売された古いマイコンではありますが、安価かつ最低限の機能があることから、発売から10年経った今でも使用されています。

Picossci 2 Conta™ BaseはRP2350Aがメインマイコンで、ESP-WROOM-02とはシリアル接続されており、ATコマンドを用いてWi-Fi機能を利用できます。Picossci「2」ではない以前のバージョンで、RP2040を搭載した「Picossci Conta™ Base」があります。
※ATコマンドとは、ESP-WROOM-02を制御するためのコマンドセットです。シリアル通信(UART)を介して送信することで、Wi-Fi接続やデータ送受信などを簡単に行えます。詳細な文法に関してはこちら

RP2350のピンアサインはRaspberry Pi Pico2と機能互換のため、Arduino IDEでの開発ではRaspberry Pi Pico2と同じ環境で開発できます。

商品ページはこちら

ボード上の配置図は以下の通りです。評価ボードとしての役割以外にも、簡単なIoTな工作にも使えます。Qwiicコネクタも搭載されているので、数珠繫ぎでAdafruitやSparkFunのモジュールも使用できます。

サーボモータが5個とグラフィック液晶が搭載可能な点が特徴的ですね。なお、サーボモータを使用する際は、USB Type-Cからの給電では電流不足になる可能性があるため、外部電源入力端子または外部DCジャックの取付が必要となります。

グラフィック液晶の商品ページはこちら

規格とマイコンボードの説明をしたところで、実際にPicossciとConta規格モジュールをしようして工作をしてみようと思います。

 

IoTなものを作ってみた(第二弾)

第一弾はこちら

今回もPicossci 2 Conta™ Baseを活用すべく、IoTな機器を製作していこうと思います。

製作したのは、自宅のインターネットにつながっている端末から確認が可能な環境監視機器です。ESP-WROOM-02をサーバーとして、RP2350がBME680からのデータを一定時間ごとに更新します。

今回の製作は過去のスイッチサイエンスのブログを参考にしました。リンクは以下の通りです。

使用するモジュール

開発環境と使用言語

  • Arduino IDE 2.3.4
  • Arduino言語
  • html
  • css
  • javascript

作品紹介

ハードウェアはPicossci 2にConta規格のBME680モジュールを搭載したシンプルな構成です。

ESP-WROOM-02のサーバに接続すると以下のような環境データを示すサイトへ接続できます。

環境データのログは気温・湿度・気圧を1分おきに取得し、30分間分のデータを保持します。ページをリロードしても、ログはローカルストレージに保存されるためデータは消えません。データクリアボタンでローカルストレージのログをすべて消すことができます。

気温・湿度・気圧のグラフの隣には不快指数を示す棒グラフを載せてみました。

ちなみに不快指数とは、気象庁の「湿度・気圧・日照時間について」によると

気温と湿度から求められる「蒸し暑さ」の指数で、日本人の場合、不快指数85で93%の人が蒸し暑さのため不快感を感じるとされていますが、体に感じる蒸し暑さは気温と湿度以外に風速等の条件によっても左右されるため、不快指数だけでは必ずしも体感とは一致しないと言われています。気象庁の統計種目にはなっていません

とのこと。具体的な計算式は

(不快指数)= 0.81 x(気温)+ 0.01 x(湿度)x(0.99 x(気温)-14.3)+ 46.3

です。

今回は室内環境を想定しているので、風速等の条件はなく、不快指数はそこそこ当てになるでしょう。試しにセンサに温かくなったACアダプタを置いて、疑似的に暑い環境にしてみました。結果が以下の写真です。

不快指数の棒グラフは不快指数が高くなるほど赤色のグラフになり、低くなるほど青色のグラフになります。湿度11%で気温35℃の時は、「やや暑い」らしいです。本当でしょうか...。湿度が低いと意外と不快ではないのでしょうか?

「http://○○○.○○○.○○○.○○○/data」に接続すると、以下のようなセンサのデータと不快指数を格納したjson形式のログを確認することができます。

{
  "temperature": 34.61,
  "humidity": 10.9,
  "pressure": 995.92,
  "gas_resistance": 28.18,
  "altitude": 145.21,
  "discomfort_index": 76.51
}

システムの仕組み

ESP-WROOM-02がサーバで、アクセス先に応じてRP2350に保存されたデータを送信しています。「http://○○○.○○○.○○○.○○○/」へのアクセスでWebページのindexを送信し、「/data」へのアクセスでjson形式のデータを送信します。

BME680のセンサデータおよび不快指数は「/data」への要求ごとに更新され、Indexページに埋め込まれたJavaScriptでは1分毎に「/data」への要求を行っています。

一般的に今回のような機能を実装する際は、ESP32やESP-WROOM-02をメインマイコンにし、ESPのフラッシュメモリにhtml情報を書き込む手法が多く取られています。今回のようにESPとは別のマイコンにhtml情報を保存する手法は少ないのではないでしょうか。

使い方

簡単に使い方を紹介しようと思います。

  1. プログラムが書き込み済みのPicossciをPCに接続する
  2. Arduino IDEでシリアルモニタを開く
  3. 以下の通りに表示されるので、任意のブラウザで「http://○○○.○○○.○○○.○○○/」に接続する
    Attempting to connect to SSID: 「自宅のWi-FiのSSID」
    Wi-Fi connected.
    IP address: 192.168.235.125
    Server started. Access in browser: http://○○○.○○○.○○○.○○○/
    Attempting to connect to SSID: 「自宅のWi-FiのSSID」

以上でセンサのログを確認することができます。

 

最後に

今回はPicossci 2 Conta™ Base(ESP-WROOM-02/RP2350A搭載)を紹介しました。「IoTなものを作ってみた(第二弾)」では、一つのモジュールのみ使用しましたが実際はより多くのモジュールを接続して開発が可能です。

今回使用した「Conta™ 環境+ガスセンサ BME680搭載」以外にも数多くのモジュールとマイコンボードがあります。

※2025年2月現在のラインナップと価格です。

ぜひ気になった方はPicossciおよびContaモジュールを使って工作をしてみてください!私は自作のConta規格モジュールを近いうちに作ってみたいと思います。

Picossciで工作の第二弾があれば、またお会いしましょう。最後まで読んでいただきありがとうございました。

Contaシリーズ特集ページはこちら

サンプルコード

本ブログで紹介した制作物のコードを掲載します。

以下にサンプルコードを掲載します。同じコードをGitHub上でも公開しております。GitHubを普段使いしている方はこちらからコードの取得も可能です。

使用したライブラリ

  • Adafruit BME680 Library 2.0.5
  • Adafruit Unified Sensor Driver(BME680 Libraryと共にインストール)
  • WiFiEspAT 1.5.0

JavaScript APIの取得先

  • Chart.js
    (https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.2.0/chart.min.js)
  • date-fnsライブラリ
    (https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@next/dist/chartjs-adapter-date-fns.bundle.min.js

ともにMITライセンスです。

注意点

picossci_secrets.hでSSIDやパスワードのdefineをしています。
index_html.hとpicossci_secrets.hはpicossci.inoと同じ階層に配置してください。


// picossci.inoファイル
#include <Wire.h>

// 追加ライブラリ
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>
#include <WiFiEspAT.h>

// .inoと同じ階層に保存
#include "picossci_secrets.h"
#include "index_html.h"

// BME680の設定
Adafruit_BME680 bme(&Wire1);
#define SEALEVELPRESSURE_HPA (1013.25)

// I2C通信ピン設定
#define SDA_PIN 6
#define SCL_PIN 7

// Wi-Fi、ESPの設定
#define ESP_SERIAL Serial2
const long AT_BAUD_RATE = 115200;
char ssid[] = SECRET_SSID;
char password[] = SECRET_PASS; 
WiFiServer server(80);
const int MAX_CLIENTS = 3;
const int CLIENT_CONN_TIMEOUT = 3600;

void setup() {
  Serial.begin(115200);
  while (!Serial) ;
  Wire1.setSDA(SDA_PIN);
  Wire1.setSCL(SCL_PIN);
  ESP_SERIAL.begin(AT_BAUD_RATE);
  WiFi.init(ESP_SERIAL);
  if (WiFi.status() == WL_NO_MODULE) {
    Serial.println("Communication with Wi-Fi module failed!");
    while (true);
  }
  if (!bme.begin()) {
    Serial.println("Could not find a valid BME680 sensor, check wiring!");
    while (1);
  }
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);
  bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme.setGasHeater(320, 150);
  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print('.');
  }
  Serial.println();
  Serial.println("Wi-Fi connected.");
  IPAddress ip = WiFi.localIP();
  Serial.print("IP address: ");
  Serial.println(ip);
  server.begin(MAX_CLIENTS, CLIENT_CONN_TIMEOUT);
  Serial.println("Server started. Access in browser: http://" + ip.toString() + "/");
}

void loop() {
  WiFiClient client = server.available();
  if (client) {
    IPAddress clientIP = client.remoteIP();
    Serial.print("New client: ");
    Serial.println(clientIP);

    boolean currentLineIsBlank = false;
    String request = "";

    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        request += c;

        // リクエストヘッダの解析
        if (c == '\n') {
          if (currentLineIsBlank) {
            break;
          }
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }

    // リクエストの解析
    Serial.println("Parsing request...");
    String requestLine = getRequestLine(request);
    String method = getMethod(requestLine);
    String path = getPath(requestLine);
    Serial.print("Method: ");
    Serial.println(method);
    Serial.print("Path: ");
    Serial.println(path);

    if (method == "GET") {
      if (path == "/") {
        // インデックスページを送信
        sendIndexPage(client);
      }
      else if (path == "/data") {
        // センサーデータをJSON形式で送信
        sendSensorData(client);
      }
      else {
        // 404 Not Found
        sendNotFound(client);
      }
    }
    else {
      // 405 Method Not Allowed
      sendMethodNotAllowed(client);
    }

    // クライアントとの接続終了
    client.stop();
    Serial.println("Client disconnected");
  }
}

// リクエストから最初のリクエストラインを取得
String getRequestLine(String request) {
  int pos = request.indexOf('\n');
  if (pos != -1) {
    return request.substring(0, pos);
  }
  return "";
}

// リクエストラインからメソッドを取得
String getMethod(String requestLine) {
  int pos = requestLine.indexOf(' ');
  if (pos != -1) {
    return requestLine.substring(0, pos);
  }
  return "";
}

// リクエストラインからパスを取得
String getPath(String requestLine) {
  int start = requestLine.indexOf(' ');
  if (start != -1) {
    int end = requestLine.indexOf(' ', start + 1);
    if (end != -1) {
      return requestLine.substring(start + 1, end);
    }
  }
  return "";
}

// インデックスページを送信
void sendIndexPage(WiFiClient client) {
  Serial.println("Sending index page in chunks...");
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: text/html");
  client.println("Connection: close");
  client.println();

  const size_t chunkSize = 512;
  size_t totalLength = strlen(INDEX_HTML);
  
  for (size_t i = 0; i < totalLength; i += chunkSize) {
    size_t bytesToSend = ((totalLength - i) < chunkSize) ? (totalLength - i) : chunkSize;
    char buffer[chunkSize];
    memcpy(buffer, INDEX_HTML + i, bytesToSend);
    client.write(buffer, bytesToSend);
    delay(1);
  }
  
  Serial.println("Index page sent.");
}

void sendSensorData(WiFiClient client) {
  Serial.println("Sending sensor data as JSON...");
  float temp = 0.0, hum = 0.0, pres = 0.0, gas_kohms = 0.0, alt = 0.0, disidx = 0.0;
  bool hasData = false;
  if (bme.performReading()) {
    temp      = bme.temperature;
    hum       = bme.humidity;
    pres      = bme.pressure / 100.0;     // hPa
    gas_kohms = bme.gas_resistance / 1000.0;
    alt       = bme.readAltitude(SEALEVELPRESSURE_HPA);
    hasData   = true;
  }
  disidx = 0.81*temp + 0.01*hum*(0.99*temp - 14.3) + 46.3;

  // JSONレスポンスの作成
  String json = "{";
  if (hasData) {
    json += "\"temperature\":" + String(temp, 2) + ",";
    json += "\"humidity\":" + String(hum, 2) + ",";
    json += "\"pressure\":" + String(pres, 2) + ",";
    json += "\"gas_resistance\":" + String(gas_kohms, 2) + ",";
    json += "\"altitude\":" + String(alt, 2) + ",";
    json += "\"discomfort_index\":" + String(disidx, 2);
  }
  else {
    json += "\"error\":\"Failed to read from BME680 sensor.\"";
  }
  json += "}";

  // HTTPレスポンス送信
  client.println("HTTP/1.1 200 OK");
  client.println("Content-Type: application/json");
  client.println("Connection: close");
  client.println();
  client.println(json);
}

// 404 Not Found を送信
void sendNotFound(WiFiClient client) {
  Serial.println("Sending 404 Not Found...");
  client.println("HTTP/1.1 404 Not Found");
  client.println("Content-Type: text/html");
  client.println("Connection: close");
  client.println();
  client.println("");
  client.println("<title>404 Not Found>/title>");
  client.println("<h1>404 Not Found</h1>");
  client.println("<p>The requested URL was not found on this server.</p>");
  client.println("");
}

// 405 Method Not Allowed を送信
void sendMethodNotAllowed(WiFiClient client) {
  Serial.println("Sending 405 Method Not Allowed...");
  client.println("HTTP/1.1 405 Method Not Allowed");
  client.println("Content-Type: text/html");
  client.println("Connection: close");
  client.println();
  client.println("");
  client.println("<title>405 Method Not Allowed</title>");
  client.println("<h1>405 Method Not Allowed</h1>");
  client.println("<p>The requested method is not allowed for the URL.</p>");
  client.println("");
}

以下はpicossci.inoと同じ階層に配置するindex_html.hの内容です。


// index_html.hファイル
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <title>環境データ</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- Chart.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.2.0/chart.min.js"
          integrity="sha512-VMsZqo0ar06BMtg0tPsdgRADvl0kDHpTbugCBBrL55KmucH6hP9zWdLIWY//OTfMnzz6xWQRxQqsUFefwHuHyg=="
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@next/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
  <style>
    body { 
      font-family: Arial, sans-serif; 
      background-color: #f9f9f9; 
      margin: 0; 
      padding: 20px; 
      text-align: center; 
    }
    h1 { 
      color: #333; 
      margin-bottom: 20px; 
    }
    .chart-container { 
      display: inline-block; 
      vertical-align: top; 
      margin: 10px; 
      background-color: #fff; 
      padding: 10px; 
      border: 1px solid #ddd; 
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    #tempChartContainer { width: 600px; }
    #discomfortChartContainer { width: 300px; }
    #discomfortText { 
      text-align: center; 
      font-size: 1.2em; 
      margin-top: 10px; 
    }
    #clearBtn {
      margin-top: 20px;
      padding: 10px 20px;
      font-size: 1em;
      background-color: #f88;
      border: none;
      color: #fff;
      cursor: pointer;
      border-radius: 5px;
    }
    #clearBtn:hover {
      background-color: #e66;
    }
  </style>
</head>
<body>
  <h1>環境データ</h1>
  
  <!-- 温度・湿度・大気圧グラフのコンテナ -->
  <div id="tempChartContainer" class="chart-container">
    <canvas id="tempChart"></canvas>
  </div>
  
  <!-- 不快指数グラフのコンテナ -->
  <div id="discomfortChartContainer" class="chart-container">
    <canvas id="discomfortChart"></canvas>
    <div id="discomfortText">不快指数:-</div>
  </div>
  
  <!-- localStorage のデータを削除するボタン -->
  <div>
    <button id="clearBtn" onclick="clearChartData()">データクリア</button>
  </div>
  
  <script>
    const MAX_POINTS = 30;
    
    // 不快指数に応じた色を返す
    function getDiscomfortColor(value) {
      if (value < 55) {
        return '#0000FF';
      } else if (value < 65) {
        let ratio = (value - 55) / 10;
        let r = 0;
        let g = Math.round(255 * ratio);
        let b = Math.round(255 * (1 - ratio));
        return `rgb(${r},${g},${b})`;
      } else if (value < 85) {
        let ratio = (value - 65) / 20;
        let r = Math.round(255 * ratio);
        let g = Math.round(255 * (1 - ratio));
        let b = 0;
        return `rgb(${r},${g},${b})`;
      } else {
        return '#FF0000';
      }
    }
    
    // 不快指数の値に応じた指標を返す
    function getDiscomfortText(value) {
      if (value < 55) {
        return "寒い";
      } else if (value < 60) {
        return "肌寒い";
      } else if (value < 65) {
        return "何も感じない";
      } else if (value < 70) {
        return "快い";
      } else if (value < 75) {
        return "暑くない";
      } else if (value < 80) {
        return "やや暑い";
      } else if (value < 85) {
        return "暑くて汗が出る";
      } else {
        return "暑くてたまらない";
      }
    }
    
    // 温度・湿度・大気圧グラフの初期化
    var tempCtx = document.getElementById('tempChart').getContext('2d');
    var tempChart = new Chart(tempCtx, {
      type: 'line',
      data: {
        labels: [],
        datasets: [
          {
            label: 'Temperature (°C)',
            data: [],
            borderColor: '#f88',
            backgroundColor: 'rgba(255,136,136,0.2)',
            fill: true,
            tension: 0.1,
            yAxisID: 'temp'
          },
          {
            label: 'Humidity (%)',
            data: [],
            borderColor: '#88f',
            backgroundColor: 'rgba(136,136,255,0.2)',
            fill: true,
            tension: 0.1,
            yAxisID: 'humid'
          },
          {
            label: 'Pressure (hPa)',
            data: [],
            borderColor: '#8f8',
            backgroundColor: 'rgba(136,255,136,0.2)',
            fill: true,
            tension: 0.1,
            yAxisID: 'press'
          }
        ]
      },
      options: {
        scales: {
          x: {
            type: 'time',
            time: {
              parser: 'HH:mm',
              unit: 'minute',
              displayFormats: { minute: 'HH:mm' }
            },
            title: { display: true, text: 'Time' }
          },
          temp: {
            type: 'linear',
            position: 'left',
            title: { display: true, text: 'Temperature (°C)' },
            suggestedMin: 15,
            suggestedMax: 30
          },
          humid: {
            type: 'linear',
            position: 'left',
            title: { display: true, text: 'Humidity (%)' },
            suggestedMin: 0,
            suggestedMax: 100,
            grid: { drawOnChartArea: false }
          },
          press: {
            type: 'linear',
            position: 'right',
            title: { display: true, text: 'Pressure (hPa)' },
            suggestedMin: 900,
            suggestedMax: 1100,
            grid: { drawOnChartArea: false }
          }
        }
      }
    });
    
    // 不快指数グラフの初期化
    var dctx = document.getElementById('discomfortChart').getContext('2d');
    var discomfortChart = new Chart(dctx, {
      type: 'bar',
      data: {
        labels: ['Discomfort Index'],
        datasets: [{
          label: 'Discomfort Index',
          data: [0],
          backgroundColor: function(context) {
            const value = context.dataset.data[context.dataIndex];
            return getDiscomfortColor(value);
          },
          borderWidth: 1
        }]
      },
      options: {
        scales: {
          y: {
            min: 0,
            max: 100,
            title: { display: true, text: 'Discomfort Index (0-100)' }
          }
        }
      }
    });
    
    // ローカルストレージからグラフデータを復元する関数
    function loadChartData() {
      const savedLabels = localStorage.getItem('chartLabels');
      const savedTemp = localStorage.getItem('chartTemperature');
      const savedHumid = localStorage.getItem('chartHumidity');
      const savedPress = localStorage.getItem('chartPressure');
      const savedDiscomfort = localStorage.getItem('chartDiscomfort');
      if (savedLabels) {
        try {
          tempChart.data.labels = JSON.parse(savedLabels);
        } catch (e) {
          console.error('Error parsing chartLabels', e);
        }
      }
      if (savedTemp) {
        try {
          tempChart.data.datasets[0].data = JSON.parse(savedTemp);
        } catch (e) {
          console.error('Error parsing chartTemperature', e);
        }
      }
      if (savedHumid) {
        try {
          tempChart.data.datasets[1].data = JSON.parse(savedHumid);
        } catch (e) {
          console.error('Error parsing chartHumidity', e);
        }
      }
      if (savedPress) {
        try {
          tempChart.data.datasets[2].data = JSON.parse(savedPress);
        } catch (e) {
          console.error('Error parsing chartPressure', e);
        }
      }
      if (savedDiscomfort) {
        try {
          discomfortChart.data.datasets[0].data[0] = JSON.parse(savedDiscomfort);
        } catch (e) {
          console.error('Error parsing chartDiscomfort', e);
        }
      }
    }
    
    // グラフデータをローカルストレージに保存する関数
    function saveChartData() {
      localStorage.setItem('chartLabels', JSON.stringify(tempChart.data.labels));
      localStorage.setItem('chartTemperature', JSON.stringify(tempChart.data.datasets[0].data));
      localStorage.setItem('chartHumidity', JSON.stringify(tempChart.data.datasets[1].data));
      localStorage.setItem('chartPressure', JSON.stringify(tempChart.data.datasets[2].data));
      localStorage.setItem('chartDiscomfort', JSON.stringify(discomfortChart.data.datasets[0].data[0]));
    }
    
    // ローカルストレージのデータを削除する関数
    function clearChartData() {
      localStorage.removeItem('chartLabels');
      localStorage.removeItem('chartTemperature');
      localStorage.removeItem('chartHumidity');
      localStorage.removeItem('chartPressure');
      localStorage.removeItem('chartDiscomfort');
      tempChart.data.labels = [];
      tempChart.data.datasets.forEach(ds => ds.data = []);
      discomfortChart.data.datasets[0].data[0] = 0;
      tempChart.update();
      discomfortChart.update();
      document.getElementById('discomfortText').textContent = "不快指数:-";
      console.log('localStorage data cleared.');
    }
    
    async function updateData() {
      try {
        const r = await fetch('/data');
        if (!r.ok) {
          throw new Error(r.statusText);
        }
        const d = await r.json();
        console.log('Fetched data:', d);
        const t = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
        
        // 温度・湿度・大気圧グラフの更新
        if (d.temperature !== undefined && d.humidity !== undefined && d.pressure !== undefined) {
          tempChart.data.labels.push(t);
          tempChart.data.datasets[0].data.push(d.temperature);
          tempChart.data.datasets[1].data.push(d.humidity);
          tempChart.data.datasets[2].data.push(d.pressure);
          if (tempChart.data.labels.length > MAX_POINTS) {
            tempChart.data.labels.shift();
            tempChart.data.datasets.forEach(ds => ds.data.shift());
          }
          tempChart.update();
        } else {
          console.warn('One or more sensor data (temperature, humidity, pressure) are undefined.');
        }
        
        // 不快指数の更新
        if (d.discomfort_index !== undefined) {
          discomfortChart.data.datasets[0].data.push(d.discomfort_index);
          discomfortChart.update();
          document.getElementById('discomfortText').textContent = "不快指数: " + d.discomfort_index + " (" + getDiscomfortText(d.discomfort_index) + ")";
        } else {
          console.warn('Discomfort index data is undefined.');
        }         saveChartData(); } catch (e) { console.error('Error fetching data:', e); } } window.addEventListener('DOMContentLoaded', function() { loadChartData(); updateData(); setInterval(updateData, 60000); }); </script> </body> </html> )rawliteral";

 

前の記事 つくると7に行ってきました2025
次の記事 The RTK Facet mosaic L-Band Hits the Scene