techrmc’s blog

ICT大好きな中小企業診断士のブログです

Raspberry Pi 3BとESP-WROOM-02を用いた自作の環境ログシステム(その2)

Raspberry Pi 3BとESP-WROOM-02を用いた自作の環境ログシステム(その1)」では、環境ログシステムの全体概要と、ESP-WROOM-02による測定クライアントについて説明しました。
techrmc.hatenablog.com
ここでは、Raspberry Pi 3Bによるデータ蓄積サーバについて説明します。

データ蓄積サーバ

Raspberry PiのHDD化

Raspberry PiではOSがmicroSDカード上に構築されます。しかし環境ログシステムではサイズの小さなセンサデータを高頻度でDBに書き込む必要があるため、書き込み頻度に限界のあるmicroSDカードでは寿命が短くなってしまいます。そこで長期運用を可能とするため、OSをUSB接続のHDDにインストールし、microSDカードへのアクセスはブート時のみとなるように構成変更しました。

MQTT

MQTTブローカとしてmosquittoを使います。mosquittoについては次の記事を参考にしました。
monoist.itmedia.co.jp
ちなみにESP-WROOM-02の測定クライアントでは、MQTTパブリッシャとしてPubSubClientを使っています。PubSubClientはArduinoのライブラリマネージャによってインストールできます。Raspberry Pi上のMQTTサブスクライバで受信した、測定クライアントのMQTTメッセージ例を下図に示します。

測定クライアントからのMQTTメッセージ例

SQLサーバ

SQLサーバとしてMariaDBを使います。MariaDBについては次の記事を参考にしました。
www.raspberrypirulo.net
nort-wmli.blogspot.com
SQLサーバのデータテーブルの例を下図に示します。

MariaDBのデータテーブル例

Node-RED

センサデータをWebブラウザで簡単に可視化できるツールとして、Node-REDを使っています。Node-REDでは、MQTTサブスクライバで受信したデータをグラフ化するとともに、SQLサーバにデータを蓄積するフローを組みます。このフローを下図に示します。

Node-REDのフロー

上記フローの中のSQLクエリ生成のJavaScriptは下記の通りです。

SQLクエリ生成

Node-REDへデータ閲覧用スマホからアクセスした画面例を下図に示します。スマホ画面には、測定クライアントからpublishされたセンサデータがグラフ表示されます。なお、右側のアグリセンサが今回の環境ログシステムのグラフであり、左側のBME280は環境ログシステムとは別のセンサのグラフ(画面キャプチャ時はデータ無し)です。

スマホ画面

以上、Raspberry PiESP-WROOM-02を用いた環境ログシステムを紹介しました。センサデータは、スマホにグラフ表示されるとともに、SQLサーバに蓄積されています。SQLのDBを操作することで、任意のデータ分析やデータ加工が可能です。

ESP-WROOM-02を用いた自作のWiFiドアセンサ 〜LINE通知版〜

以前の記事(IFTTT版)では、扉が開いたことを検知すると、IFTTTサーバ上のアプレットを叩き、IFTTTのアプレットスマホへメール送信していました。
techrmc.hatenablog.com
ここでは、IFTTTを利用せず、スマホのLINEアプリへメッセージを表示する仕組みに改造します。
LINEアプリへのメッセージングには、Notify APIを用います。
Notify APIについては、ネット検索で多数の記事が見つかるので、先達の記事を参考にさせて頂きました。参考にした記事例を2つほど。

burariweb.info
qiita.com

LINE Notifyを利用するには、まずLINEにログインしてトークンを取得する必要があります。トークンの取得方法は先達の記事をご覧ください。

検知回路

検知回路は以前の記事(IFTTT版)から改造せず、そのまま使用します。

プログラム

LINEのNotity APIを叩くよう、IFTTT版を改造しました。
Esp_Line_Notifyというライブラリを利用すると、もっとシンプルに記述できるようですが、ここではEsp_Line_Notifyは利用しない方式としました。改造箇所は次の通りです。

Notify APIの固定パラメータ

LINE Notify APIを叩くための固定パラメータをセットします。

// LINE
const char* host = "notify-api.line.me";
const int httpPort = 443;
const char* token = "xxxxxxxx";
const char* message = "PostBox: Opened";

上記ではtokenを"xxxxxxxx"と伏せてありますが、実際には取得したトークンの文字列で置き換えます。
messageは、LINEアプリに表示されるメッセージ文です。ここでは、"PostBox: Opened"が表示されます。

clientオブジェクト

IFTTT版ではWiFiClientクラスを使っていましたが、Notify APIではHTTPS接続が必要なため、WiFiClientSecureクラスを使います。

WiFiClientSecure client;

Notify APIのキック

IFTTT版のsend2IFTTT()の代わりに、次のsend2LINE()を用います。
client.setInsecure()は証明書の検証を省くためのコードです。

void send2LINE(){ 
    client.setInsecure(); 
    if (!client.connect(host, httpPort)) {
        return;
    }
    
    String content = String("message=") + String(message);
    client.print(String("POST /api/notify HTTP/1.1\r\n")
                   + "Host: " + host + "\r\n"
                   + "Authorization: Bearer " + token + "\r\n"
                   + "Content-Length: " + String(content.length()) + "\r\n"
                   + "Content-Type: application/x-www-form-urlencoded\r\n\r\n"
                   + content + "\r\n"); 
    unsigned long timeout = millis();
    while (client.available() == 0) {
        if (millis() - timeout > 5000) {
            client.stop();
            return;
        }
    }
  
    // Read all the lines of the reply from server
    while(client.available()){
        String line = client.readStringUntil('\r');
    }
}

スマホ画面

スマホのLINE画面を次に示します。扉を開ける度に"PostBox: Opened"というメッセージが表示されているのがわかります。ちなみに、"IoT"はトークン発行時に登録したトークン名で、トークン発行時に任意の文字列が設定可能です。

LINE Notify画面

最新MacOSにおけるVisual Studio Code Arduino拡張機能のコンパイルエラー{"code",null}への対処法

MacOSにおいて、Visual Studio CodeArduino拡張機能を用いて、Arduinoのソフト開発を行おうとしたのですが、Verify(コンパイル)しようとすると、{"code",null}というエラーが起きました。ちなみに、Windows11のVisual Studio Codeでは、同一のArduinoのコードが問題なくVerifyできています。また、同じMacでも、Arduino IDE 2.2.1では問題なくVerifyできています。このことから、MacOSVisual Studio Code周りに問題がありそうとerror要因が切り分けできます。

動作確認環境

CPU:Apple M2

OS:macOS Sonoma 14.1

Visual Studio Codeのバージョン:1.84.0

Arduino拡張機能の設定:Arduino拡張機能にバンドルされるArduino CLIを使用

バイスArduino UNO R4 Minima

対処法

ネット上の次の記事通りMacOSのシステム設定を変更したところ、無事、Verifyできるようになりました。

https://github.com/microsoft/vscode-arduino/issues/1681

具体的には、次の手順です。なお、「プライバシーとセキュリティ」をいじるので、システム脆弱性を招く可能性があります。あくまでご自分でセキュリティリスクを判断してください。

  1. 「システム設定」から「プライバシーとセキュリティ」を選択。
  2. 「プライバシーとセキュリティ」の「フルディスクアクセス」を選択。
  3. フルディスクアクセスを許可するアプリケーションとして「Visual Studio Code」を追加。(「フルディスクアクセス」の画面下部にある「+」を押してから、「Visual Studio Code」を選択。)
  4. 3と同様に、「プライバシーとセキュリティ」の「アプリ管理」に「Visual Studio Code」を追加。
  5. 3と同様に、「プライバシーとセキュリティ」の「デベロッパツール」に「Visual Studio Code」を追加。
  6. ここでVisual Studio Codeを起動すると、Verifyの{"code":null}エラーが消え、Verifyできるようになっているはずです。
  7. Verifyで{"code":null}エラーが消えたことを確認したら、脆弱性を少しでもリカバーするため、「プライバシーとセキュリティ」の「フルディスクアクセス」と「アプリ管理」の"Visual Studio Code"のトグルスイッチを切っておきます。
  8. デベロッパツール」のトグルスイッチも切りたいところですが、「デベロッパツール」の"Visual Studio Code"のトグルスイッチは入れたままにしておく必要がありそうです。(トグルスイッチを切ると、{"code":null}エラーが復活してしまいました。)

以上、{"code",null}エラーへの暫定対処でした。Visual Studio CodeArduino拡張機能の今後のアップデートで、上記の暫定対処が不要になることを期待します。

WAKWAKホームページへの問い合わせフォーム(POST-MAIL CGI)設置

概要

NTT-MEのインターネットプロバイダ「WAKWAK」の個人ホームページに、問い合わせフォームを設置しました。Web閲覧者が問い合わせフォームに入力すると、あらかじめ指定しておいたメールアドレスへ入力内容がメールで届きます。問い合わせフォームのCGIは、www.kent-web.comさんのPOST-MAILを利用させて頂きました。

www.kent-web.com

WAKWAKへのCGI設置

WAKWAKCGIを設置する場合、CGIを呼び出すフォームのhtmlファイルは/public_html配下に設置し、CGIは/cgi-bin配下に設置するのがWAKWAKの流儀です。そこで、postmail.zipを解凍して出来るディレクトリ構造のうち、postmail配下のindex.htmlとstyle.cssのみを/public_htmlへ設置し、他は/cgi-binへ設置します。

/public_html 

index.htmlとstyle.cssを設置。index.htmlのファイル名は適宜変更します。(そのまま変更しなくても構いません)

/cgi-bin 

index.htmlやstyle.css「以外」のファイルやディレクトリを設置。ディレクトリはディレクトリ構造を壊さず、そのまま/cgi-bin配下へ設置します。

注意点

注意点は次の通りです。

  1. ファイルの改行コードはLFとします。LFになっていないと、CGIエラーになリます。
  2. init.cgiを修正する際、$sendmailはデフォルトのまま(/usr/lib/sendmail)でOKです。また、閲覧者が第三者のメールアドレスを入力した場合にその第三者への迷惑メールとならないよう、$cf{auto_res}は0に設定した方が良いです。
  3. ファイルをWebサーバへアップロードする際は、asciiモードでftp転送します。
  4. Webサーバにアップロードしたファイルやディレクトリのパーミッションは、セキュリティを考慮して、下記のように設定します。KENT-WEBさんのPOST-MAILのWebサイトに記載されているパーミッションとは異なるのですが、トライ&エラーの結果、WAKWAKの場合は下記の設定が良さそうでした。
    • /public_html配下のhtmlファイルとcssファイルは644
    • /cgi-bin直下のpostmail.cgiとcheck.cgiは755
    • /cgi-bin配下のディレクトリはサブディレクトリを含めて700
    • /cgi-bin配下の他のファイルは(拡張子が.cgiや.pmのファイルを含めて)600

Gmailアドレスをメール送信先とする場合の個別課題

POST-MAILでは、init.cgiで設定したメール送信先($cf{mailto})へメール送信します。この時、メールのfrom行には閲覧者がフォーム入力したメールアドレスが設定されます。そのため、メール送信先Gmailのメールアドレスとした場合、閲覧者が入力したメールアドレスによってはGmailがメール受信を拒否することがあります。これは、Gmailがセキュリティ上、SPFもしくはDKIMで認証した送り主からしかメール受信しないためです。詳細は下記のgoogle記事をご覧ください。

https://support.google.com/mail/answer/81126#authentication

この現象を防ぐためには、postmail.cgiの342行目辺りを修正し、from行をSPFもしくはDKIM認証されたメールアドレスに固定します。

SPF or DKIMSPF or DKIM

Raspberry Pi 3BとESP-WROOM-02を用いた自作の環境ログシステム(その1)

全体概要

Raspberry Pi 3BとESP-WROOM-02をベースとした自作の環境ログシステムです。特徴は次の通りです。

  • 屋外に設置される計測センサは単三乾電池3本で長時間駆動可能
  • 将来のデータ分析に備えてセンサデータをデータベースへ蓄積
  • ファイルシステムのハードディスク実装によりRaspberry Piを長寿命化

システム構成図は次図の通りです。

システム構成図


環境ログシステムは、気温・湿度・気圧・土壌湿度を計測する測定クライアントと、測定クライアントから送信されたセンサデータをMySQLデータベースに記録するデータ蓄積サーバから構成されます。
測定クライアントはESP-WROOM-02をベースとしており、屋外のウォールボックスに設置して、屋内設置のデータ蓄積サーバとWiFi接続します。センサデータの通信には、MQTTを用いています。
一方、データ蓄積サーバはRaspberry Pi 3Bをベースとしており、測定クライアントからMQTTで受信したセンサデータをMySQLデータベースに記録します。MySQLのセンサデータはNode-REDによって可視化し、パソコンやスマホWebブラウザに表示します。

データ蓄積サーバは常時起動状態となるため、このままインターネットに晒すと外部から攻撃を受ける可能性が高くなります。そこで、データ蓄積サーバはインターネットから切り離したクローズドなネットワーク(システム構成図の点線内)に配置することで、セキュリティリスクを軽減しています。このクローズドなネットワークにアクセスするためのアクセスポイントがデータアップロード用アクセスポイントです。測定クライアントがセンサーデータを送信する際は、このデータアップロード用アクセスポイントに接続し、データ蓄積サーバへセンサデータを送信します。
一方、インターネット用アクセスポイントは、測定クライアントがデータアップロード用アクセスポイントにWiFi接続できない場合など異常検知した時に用いられ、測定クライアントはインターネット上の異常監視スマホに対してLINE Notifyで異常を通知します。LINE Notifyが呼ばれると、スマホのLINEアプリに受信メッセージが表示されます。
インターネット用アクセスポイントは、測定クライアントの初期化時にインターネット上のNTPサーバと時刻同期する際にも用いられます。

測定クライアント

測定クライアントは、

で構成します。BME-280は気温・湿度・気圧を測定するセンサであり、ESP-WROOM-02にI2Cで接続します。土壌湿度センサは土壌の湿り気を測定するセンサであり、ESP-WROOM-02のアナログ入力端子に接続します。リアルタイムクロックはESP-WROOM-02とI2Cで接続し、NTPサーバと同期が取れた時刻の保持と、ESP-WROOM-02の起動制御を行います。
測定クライアントの回路図と、ユニバーサル基板に実装した写真は次の通りです。
なお、回路図には記載していませんが、電源は単3乾電池3本で、3端子レギュレータで3.3Vまで降圧しています。ESP-WROOM-02WiFi接続時に大きな電流を必要とするので、それに耐え得る3端子レギュレータを選定します。

測定クライアント回路図
測定クライアント実装写真

写真では、測定クライアントのユニバーサル基板の下に、別記事
techrmc.hatenablog.com
で紹介したWiFiドアセンサのESP-WROOM-02が写っていますが、環境ログシステムとは無関係です。

電力消費量を削減するため、データ測定時以外は、ESP-WROOM-02をスリープ状態にしておきます。スリープ状態のESP-WROOM-02は非常に小さな電力消費となるため、乾電池駆動の長期化に役立ちます。
スリープ状態のESP-WROOM-02は、リアルタイムクロックRTC-8564NBのアラーム割り込みによって起動トリガをかけます。ESP-WROOM-02は測定が終わるとスリープ状態に入っていますが、所定のアラーム時刻にRTC-8564NBからトリガがかかると、ESP-WROOM-02はスリープ状態から復帰し、BME280と土壌湿度センサからデータを読み取ります。読み取ったセンサデータはMQTTでpublishし、次の起動時刻をRTC-8564NBのアラーム時刻としてセットしたのち再びスリープ状態に入ります。測定クライアントの初期化時には、RTC-8564NBをインターネット上のNTPサーバと時刻同期しておきます。
なお、土壌湿度センサをVCCとGNDに直接接続すると、土壌湿度測定時以外も常時、土壌湿度センサに電流が流れるため、乾電池の寿命が短くなリます。そこで、測定時以外は消費電流を抑制できるよう、土壌湿度センサへの電流供給をトランジスタ2SC1815によってスイッチングしています。

Arduinoで作成したESP-WROOM-02のプログラムを次に示します。なお、BME280用のプログラムは、スイッチサイエンスのサンプルプログラム
BME280 – スイッチサイエンス
をほぼそのまま利用させて頂きましたので、そちらをご覧ください。

メインプログラムmain.ino

/*
 * 測定クライアントのメインプログラム
 * 
 * 動作モード:DIP-SW(SW1)で切り替え
 *    キャリブレーションモード:WiFi接続テスト・NTP同期・土壌湿度センサを水に浸して半固定抵抗を調整
 *    通常モード:定期的にBME280と土壌湿度センサで温度・湿度・気圧・土壌湿度を測定し、MQTTブローカへpublish
 * 異常時の挙動: 
 *    動作モードがキャリブレーションモードの時:
 *      WiFiエラー(インターネットアクセス用):キャリブレーション無限ループ
 *      WiFiエラー(データアップロード用):キャリブレーション無限ループ
 *      NTPエラー:キャリブレーション無限ループ
 *    動作モードが通常モードの時:
 *      BME280エラー:ディープスリープ
 *      WiFiエラー(インターネットアクセス用):ディープスリープ
 *      WiFiエラー(データアップロード用):LINE通知&ディープスリープ
 *      MQTTエラー:LINE通知&ディープスリープ
 *    
 */
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <time.h> 
extern "C" {
#include "user_interface.h"         // ESP8266用の拡張IFライブラリ
}
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define ESP8266_PLATFORM

// IOピン番号
#define PIN_MODE 12                 // 動作モード(HIGH:通常, LOW:キャリブレーション)
#define PIN_LED 13                  // LED点灯制御(HIGH:点灯, LOW:消灯)
#define PIN_SPWR 14                 // 土壌湿度センサ電源制御(HIGH:ON, LOW:OFF)

// LED点灯回数
#define NOERR 3                     // 1秒間隔で3回点灯:NTP同期した後(同期成功・不成功には無関係)
#define WIFIERRM 5                  // 1秒間隔で5回点灯:WiFi接続エラー(MQTTデータアップロード用アクセスポイント)
#define NTPERR 7                    // 1秒間隔で7回点灯:NTP同期エラー(年が2020未満)
#define WIFIERRU 9                  // 1秒間隔で9回点灯:WiFi接続エラー(インターネット用アクセスポイント)

// 測定定数
#define SLEEP_P 0                   // スリープ周期(0は無期限)
#define CYCLE_M 60                  // 測定周期(分単位)

// キャリブレーションモードでは、土壌湿度センサを水に浸し、
// 上限値と下限値の間に設定する。
#define STH_H 930                   // 土壌湿度センサの上限値
#define STH_M 900                   // 土壌湿度センサの標的値
#define STH_L 870                   // 土壌湿度センサの下限値

// 動作モード(HIGH:通常, LOW:キャリブレーション)
int mode;

// WiFi定数
const char ssid4mqtt[] = "xxxx";         // MQTTデータアップロード用アクセスポイントのSSID
const char pass4mqtt[] = "xxxx";         // MQTTデータアップロード用アクセスポイントのパスワード
const char ssid4util[] = "xxxx";         // インターネット用アクセスポイント(NTP同期、LINEエラー通知)のSSID
const char pass4util[] = "xxxx";         // インターネット用アクセスポイント(NTP同期、LINEエラー通知)のパスワード

// MQTT定数
#define MQTTCONNECTWAIT 10000       // MQTTブローカへの接続リトライ待ち時間(msec)
#define MQTTMAXRETRY 2              // MQTT接続リトライの最大回数
const char mqttAddress[] = "xxx.xx.xx.xx";
const char mqttTopic[] = "xxxx";
const char mqttClientid[] = "xxxx";
const int mqttPort = 1883;

// LINE
const char* host = "notify-api.line.me";
const int httpPort = 443;
const char* token = "xxxx";

// Use WiFiClient class for MQTT connections
WiFiClient client;
PubSubClient mqttClient(client);

// Use WiFiClientSecure class for LINE connections
WiFiClientSecure utilclient;

// タイムスタンプ(ISO8601形式)
#define ATLEN 26      // timestampの文字列長+1(末尾のNULLをカウント) 定義変更時はrtc8564nb.inoの定義も要変更
char at[ATLEN];       // timestamp ISO8601形式

// 測定値をMQTTブローカへpublishする際のJSONバッファ
// https://arduinojson.org/v6/assistant/でのバッファサイズの計算式は、
// const size_t capacity = JSON_OBJECT_SIZE(5) + 78;
StaticJsonDocument<200> jsondoc;
char msg[200];

////////////////////////////////////////
//メインsetup
////////////////////////////////////////
void setup() {

  ////////////////////////////////////////
  // モード共通初期処理
  ////////////////////////////////////////
  Serial.begin(115200);
   
  //IO PIN 設定
  pinMode(PIN_LED,OUTPUT);        // PIN_LEDを出力用にセットして、LEDを消灯しておく
  digitalWrite(PIN_LED,LOW);
  pinMode(PIN_SPWR,OUTPUT);       // PIN_SPWRを出力用にセットして、土壌湿度センサを電源OFFにしておく
  digitalWrite(PIN_SPWR,LOW);
  pinMode(PIN_MODE,INPUT_PULLUP); // PIN_MODEのデフォルトは、HIGH(動作モード:通常)

  Wire.begin();                   // マスタとしてI2Cバスに接続

  mode = digitalRead(PIN_MODE);           // モードSWを読み取り

  if (mode==LOW) {  // 土壌センサキャリブレーションモード(WiFi接続テスト、NTP時刻同期を兼ねる)
    if (connectWiFi(ssid4mqtt, pass4mqtt)==false) {  // MQTTデータアップロード用アクセスポイントへの接続がエラーの場合
      longLightMulti(WIFIERRM);
      digitalWrite(PIN_SPWR,HIGH);
      delay(5000);                      // 土壌センサの安定を待つ
      return;                           // setup()を抜けて、loop()へ入る
                                        // WiFiエラー時は、NTP同期せず、土壌センサのキャリブレーションのみ実行 
    } else if (connectWiFi(ssid4util, pass4util)==false) { // インターネット用アクセスポイントがエラーの場合
      longLightMulti(WIFIERRU);
      digitalWrite(PIN_SPWR,HIGH);
      delay(5000);                      // 土壌センサの安定を待つ
      return;                           // setup()を抜けて、loop()へ入る
                                        // WiFiエラー時は、NTP同期せず、土壌センサのキャリブレーションのみ実行       
    }

    rtc_init();                
    
    //キャリブレーションモードでは、rtc_init()での低電圧検知に関係なくNTP同期に入る
    if (rtc_sync_ntp() < 2020) { // 現在の西暦2020より小さいとき、間違いなくNTP同期エラー
      longLightMulti(NTPERR);
    } else { // 2020以上の場合、NTP同期エラーの可能性は低い。ただし、NTP同期成功とは限らない。
      longLightMulti(NOERR);
    }  

    // 現在のRTCレジスタを取得    
    rtc_read_regtbl();  
    
    digitalWrite(PIN_SPWR,HIGH);
    delay(5000);                          // 土壌センサの安定を待つ

  } else {  //キャリブレーションモードはここまで。ここから通常モード
    // RTC初期設定
    if (rtc_init()==true) { // 低電圧検知時
      if (connectWiFi(ssid4util, pass4util) == false) { // インターネット用アクセスポイントへの接続がエラーの場合
        // 現在のRTCレジスタを取得    
        rtc_read_regtbl();
        
        // WiFi接続エラー時はRTCアラームをCYCLE_M分後にセット
        // RTC低電圧が起きているので、不定時刻のCYCLE_M分後となる
        rtc_set_alarm_min(CYCLE_M);  
        sleep();                          
      }

      // WiFi接続成功
       
      // NTP同期処理
      rtc_sync_ntp();
      
      // 低電圧検知時の処理終了
      
    }
    
    // 現在のRTCレジスタを取得
    rtc_read_regtbl();
  
    // CYCLE_M分後に新アラームをセット
    // RTC低電圧が起きていると、不定時刻のCYCLE_M分後となる
    rtc_set_alarm_min(CYCLE_M);  
    rtc_read_regtbl();
    
    digitalWrite(PIN_SPWR,HIGH);
    delay(5000);                          // 土壌センサの安定を待つ
 
    bme280_Setup();    
  
    // 土壌センサ・BME280 読み取り・送信
    soilbme280_mqtt();

    sleep();   
  }
}

////////////////////////////////////////
//メインloop
////////////////////////////////////////
void loop() {
  int value;
  
  if (mode==LOW) {
    value = analogRead(A0);
    if (value > STH_H) {  // 上限閾値超えの場合
      intLight(100);
    } else if (value > STH_M) {   // 標的閾値と上限閾値の中間の場合
      intLight(70);
    } else if (value > STH_L) {   // 下限閾値と標的閾値の中間の場合
      intLight(40);
    } else {                      // 下限閾値以下の場合
      intLight(10);
    }   
  } else {    //ダミー。mode=HIGH(通常モード)の時はloop()は呼ばれないはず。
    sleep();
  }
}

////////////////////////////////////////
//ディープスリープに入る
// ・bme280をスリープ
// ・土壌センサを電源OFF
// ・LEDを消灯
////////////////////////////////////////
void sleep(){
  bme280_Sleep();
  digitalWrite(PIN_SPWR,LOW);
  digitalWrite(PIN_LED,LOW);
  delay(100);
  ESP.deepSleep(SLEEP_P,WAKE_RF_DEFAULT);  // スリープモードへ移行する
  delay(1000);                             // スリープモードに入るのに時間を要すためダミーのdelayを入れる
}

////////////////////////////////////////
//長く点灯して消灯
////////////////////////////////////////
void longLight(){
  digitalWrite(PIN_LED,HIGH);
  delay(1000);  // light on for 1 sec
  digitalWrite(PIN_LED,LOW);
  delay(100);  // light off for 100 msec
}

////////////////////////////////////////
//長く点灯して消灯を、n回繰り返す
////////////////////////////////////////
void longLightMulti(int n) {
  for(int i=0;i<n;i++) longLight();
}

////////////////////////////////////////
//duty比率で点灯&消灯
// duty=100 1秒ずっと点灯
// duty=50 0.5秒点灯して0.5秒消灯
// duty=20 0.2秒点灯して0.8秒消灯
////////////////////////////////////////
void intLight(int duty){
  digitalWrite(PIN_LED,HIGH);
  delay(10*duty);             // light on
  digitalWrite(PIN_LED,LOW);
  delay(10*(100-duty)+10);    // light off
}

////////////////////////////////////////
// WiFiアクセスポイントに接続
// 戻り値
//  true:接続成功
//  false:接続エラー
////////////////////////////////////////
bool connectWiFi(const char *ssid, const char *pass){
  int waiting=0;

  if (WiFi.status() == WL_CONNECTED) {  // 接続済みの時、一旦、接続を切る
    WiFi.disconnect();
  }

  digitalWrite(PIN_LED,LOW);
  WiFi.mode(WIFI_STA);                  // STA mode
  WiFi.begin(ssid, pass);            
  while(WiFi.status() != WL_CONNECTED){ // wait until WiFi connected
    delay(100);                         // wait 100msec
    waiting++;                          // counter increment
    if(waiting > 300) {                 // max waiting counter. approx.300*100 msec= 30sec
      digitalWrite(PIN_LED,LOW);
      return false;                         
    }
  }
  digitalWrite(PIN_LED,LOW);
  return true;
}

////////////////////////////////////////
// LINEへメッセージ送信
// 戻り値
//  true: LINEメッセージ送信完了
//  false: WiFi接続エラー or LINE接続エラー or LINEレスポンス無し
////////////////////////////////////////
bool send_to_LINE(char *value1, char *value2, char *value3) {
  utilclient.setInsecure();
  if (connectWiFi(ssid4util, pass4util) == false) { // インターネットアクセス用アクセスポイントへの接続がエラーの場合
    return false;   
  }

  if (!utilclient.connect(host, httpPort)) {
    return false;
  }
    
  // LINEサーバへメッセージ送信
  String message = String("value1=") + String(value1)
                 + String(", value2=") + String(value2)
                 + String(", value3=") + String(value3);

  String content = String("message=") + message;
  utilclient.print(String("POST /api/notify HTTP/1.1\r\n")
                 + "Host: " + host + "\r\n"
                 + "Authorization: Bearer " + token + "\r\n"
                 + "Content-Length: " + String(content.length()) + "\r\n"
                 + "Content-Type: application/x-www-form-urlencoded\r\n\r\n"
                 + content + "\r\n");   
  unsigned long timeout = millis();
  while (utilclient.available() == 0) {
    if (millis() - timeout > 5000) {
      utilclient.stop();
      return false;
    }
  }
  
  // Read all the lines of the reply from server and print them to Serial
  while(utilclient.available()){
    String line = utilclient.readStringUntil('\r');
  }
  
  return true;
}

////////////////////////////////////////
//測定してmqtt送信
//測定値5回のうち最大・最小以外の3回の平均を取る
////////////////////////////////////////
void soilbme280_mqtt(){        
  float temperature[5], max_temperature, min_temperature;
  float humidity[5], max_humidity, min_humidity;
  float pressure[5], max_pressure, min_pressure;
  int moisture[5], max_moisture, min_moisture;

  readData();   
  temperature[0] = readTemperature();
  humidity[0] = readHumidity();
  pressure[0] = readPressure();
  moisture[0] = analogRead(A0);
  // Check if any reads failed and exit early (to try again).
  if (isnan(temperature[0]) || isnan(humidity[0]) || isnan(pressure[0])) {
    sleep();
  }
  delay(100);

  max_temperature = temperature[0];
  min_temperature = temperature[0];
  max_humidity = humidity[0];
  min_humidity = humidity[0];
  max_pressure = pressure[0];
  min_pressure = pressure[0];
  max_moisture = moisture[0];
  min_moisture = moisture[0];

  for(int i=1;i<5;i++) {  
    readData();      
    temperature[i] = readTemperature();
    humidity[i] = readHumidity();
    pressure[i] = readPressure();
    moisture[i] = analogRead(A0);
    // Check if any reads failed and exit early (to try again).
    if (isnan(temperature[i]) || isnan(humidity[i]) || isnan(pressure[i])) {
      sleep();
    }

    if (temperature[i] > max_temperature) 
      max_temperature = temperature[i];
    if (temperature[i] < min_temperature) 
      min_temperature = temperature[i];
    if (humidity[i] > max_humidity) 
      max_humidity = humidity[i];
    if (humidity[i] < min_humidity) 
      min_humidity = humidity[i];
    if (pressure[i] > max_pressure) 
      max_pressure = pressure[i];
    if (pressure[i] < min_pressure) 
      min_pressure = pressure[i];
    if (moisture[i] > max_moisture) 
      max_moisture = moisture[i];
    if (moisture[i] < min_moisture) 
      min_moisture = moisture[i];
    
    delay(100);
  }

  float ave_temperature=0;
  float ave_humidity=0;
  float ave_pressure=0;
  int ave_moisture=0;

  for(int j=0;j<5;j++) {
    ave_temperature += temperature[j];
    ave_humidity += humidity[j];
    ave_pressure += pressure[j];
    ave_moisture += moisture[j];
  }
  // remove abnormal (min, max) values
  ave_temperature = (ave_temperature-max_temperature-min_temperature)/3;
  ave_humidity = (ave_humidity-max_humidity-min_humidity)/3;
  ave_pressure = (ave_pressure-max_pressure-min_pressure)/3;
  ave_moisture = (ave_moisture-max_moisture-min_moisture)/3;

  // タイムスタンプ取得
  rtc_get_timestamp(at);

  if (connectWiFi(ssid4mqtt, pass4mqtt) == false) { // データアップロード用アクセスポイントへの接続がエラーの場合    
    send_to_LINE((char *)mqttClientid,"error","WiFi_connection");   
    sleep(); // WiFi接続エラー時はsetup()でアラームセットした時刻(CYCLE_M分後)に再起動      
  }

  // データアップロード用アクセスポイントに接続成功するとここまで来る

  //
  //時刻・温度・湿度・大気圧・土壌湿度のMQTT送信
  //

  mqttClient.setServer(mqttAddress, mqttPort);
  int cnt = 0;
  while (!mqttClient.connected()) {
    if (mqttClient.connect(mqttClientid)){
      jsondoc["datetime"] = at;
      jsondoc["temperature"] = ave_temperature;
      jsondoc["humidity"] = ave_humidity;
      jsondoc["pressure"] = ave_pressure;
      jsondoc["soil_moisture"] = ave_moisture;
      serializeJson(jsondoc,msg);
      mqttClient.publish(mqttTopic, msg);
      jsondoc.clear();    // jsondocのメモリリーク防止のためclearする
    } else {
      cnt++;
      if (cnt <= MQTTMAXRETRY ) { // 最大リトライ回数以下のとき
        delay(MQTTCONNECTWAIT);   // wait for MQTTCONNECTWAIT msec before retrying
      } else {                    // 最大リトライ回数を超えた時
        send_to_LINE((char *)mqttClientid,"error", "MQTT_connection");
        sleep();
      }
    }
  }
  mqttClient.disconnect();

}

RTC-8564NB周りのプログラムrtc8564nb.ino

#include <Wire.h>
#include <time.h> 
#include <TZ.h>
/*
 * bool rtc_init() -- RTC初期化
 * void rtc_sync_ntp() --- NTP時刻をRTCに設定
 * void rtc_read_regtbl() --- RTC内のレジスタからレジスタテーブルRegTblに読み込み
 * void rtc_set_alarm_min(int m) --- RTC内のレジスタにm分後のアラーム時分を書き込み
 * void rtc_set_alarm(int h, int m) --- RTC内のレジスタにアラーム時分を書き込み
 * int rtc_get_hour() --- レジスタテーブルRegTblから現在のhourを返す
 * int rtc_get_min() --- レジスタテーブルRegTblから現在のminを返す
 * void rtc_get_timestamp(char *at) --- レジスタテーブルからtimestampを生成 
 * 
 * byte BCDtoDec(byte value) --- BCDを10進数に変換
 * byte DectoBCD(int value) --- 10進数をBCDに変換
 */

#define RTC_ADDRESS 0x51                  // RTCのI2Cアドレス

#define NTP_SERVER1   "xxx.xxx.xxx"       // NTPサーバー
#define NTP_SERVER2   "xxx.xxx.xxx"       // NTPサーバー
#define NTP_SERVER3   "ntp.nict.jp"       // NTPサーバー
#define NTPMAXREPEAT  60                  // NTP同期ウェイト(1000ms)の最大リピート数

#define RTCREGLEN 16
int RegTbl[RTCREGLEN];  // RTCのレジスタテーブル(16byte)
static const char *pszWDay[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};  // 曜日表現

#define ATLEN 26      // timestampの文字列長+1(末尾のNULLをカウント)

/*
 * bool rtc_init() -- RTC初期化。
 * 低電圧が起きたかチェック(check VL bit of Seconds register)し、アラームINTをリセット。
 * アラームはダミー設定し(ダミー値:0時・0分・1日・日曜)、全てアラーム対象外ビットを立てる。
 * 
 * Return:
 *    true: 低電圧検知
 *    false: 低電圧非検知
 */
bool rtc_init(){ 
  bool isVL;  // 低電圧検知フラグ
      
  // VL bitのあるSeconds registerを取得
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x02);   // [02]Seconds レジスタを指定
  Wire.endTransmission();
  Wire.requestFrom(RTC_ADDRESS,1);
  while (Wire.available() == 0 ){}
  RegTbl[2] = Wire.read();

  // 低電圧検知ビットVLをチェック
  if((RegTbl[2] & 0x80) != 0){ //低電圧検知
    isVL = true;
  } else { // 低電圧非検知
    isVL = false;
  }

  // アラーム発生時に1となっているAFビット(bit3)をリセット
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x01);   // [01]Control2 レジスタを指定  
  Wire.write(0x02);   // [01]Control2 0x02:アラーム割り込み時に3ピン[INT]をLOWにする&アラームリセット
  Wire.endTransmission();

  // アラームはダミー値を設定。先頭bitに1がある場合(0x80)はアラーム対象外
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x09);   // [09]Minutes Alarm レジスタを指定  
  Wire.write(0x00 | 0x80);  // [09]Minutes Alarm, ダミーで0分
  Wire.write(0x00 | 0x80);  // [0A]Hours Alarm, ダミーで0時
  Wire.write(0x01 | 0x80);  // [0B]Days Alarm, ダミーで1日
  Wire.write(0x00 | 0x80);  // [0C]Weekdays Alarm, ダミーで日曜
                              // 0:日 1:月 2:火 3:水 4:木 5:金 6:土
  //  クロック出力レジスタ
  // [0D]CLKOUT Frequency
  // ・クロック出力機能を有効にする。
  //   ※有効:0x80 無効 :0x00
  // ・クロック周波数は1Hz(1秒間に1回)とする
  //   ※0x00:32768Hz, 0x01:1024Hz, 0x02:32Hz, 0x03:1Hz
  // 次の設定だと2ピン[CLKOUT]から1秒に1回、クロック出力される     
  //    Wire.write(0x80 | 0x03);
  Wire.write(0x00 | 0x03);    // クロック周波数1Hz(0x03)だが、クロック出力無効(0x00)
  Wire.endTransmission();

  return(isVL);
}

/*
 * int rtc_sync_ntp() --- NTP時刻をRTCに設定。
 * 同期エラー時は不正値に設定される。
 * 
 * Return
 *    西暦年
 */
int rtc_sync_ntp(){
  time_t timeNow;
  struct tm* tmNow;
  int i=0;

  configTzTime(TZ_Asia_Tokyo, NTP_SERVER1, NTP_SERVER2, NTP_SERVER3);   

  // NTP同期を待つ。同期されていないと1970となる様子。2020未満は未同期と判断。   
  do {
    delay(1000);                            // 1秒ごとに繰り返し
    timeNow = time(NULL);
    tmNow = localtime(&timeNow); 
  } while(((tmNow->tm_year+1900) < 2020)&&(i<=NTPMAXREPEAT));    // 繰り返しがNTPMAXREPEATを超えたら同期を諦める

  // NTP取得情報をRTCにセット
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.write(0x00);  // [00]Control1
  Wire.write(0x02);  // [01]Control2 0x02:アラーム割り込み時に3ピン[INT]をLOWにする
  Wire.write(DectoBCD(tmNow->tm_sec));  // [02]Seconds
  Wire.write(DectoBCD(tmNow->tm_min));  // [03]Minutes
  Wire.write(DectoBCD(tmNow->tm_hour));  // [04]Hours
  Wire.write(DectoBCD(tmNow->tm_mday));  // [05]Days
  Wire.write(DectoBCD(tmNow->tm_wday));  // [06]Weekdays(月) 
  Wire.write(DectoBCD(tmNow->tm_mon+1)|0x80);   // [07]Month/Century(21世紀の12月)
                                  // ・Month(01-12) BCD形式
                                  // ・Century(先頭bit 0:20世紀[19xx年代],1(0x80):21世紀[20xx年代])
  Wire.write(DectoBCD(tmNow->tm_year-100));  // [08]Years

  // I2Cスレーブへの送信完了
  Wire.endTransmission(); 

  return(tmNow->tm_year+1900);
}

/*
 * void rtc_set_alarm_min(int m) --- RTC内のレジスタにm分後のアラーム時分を書き込み
 */
void rtc_set_alarm_min(int m) {
  int now_hour, now_min;      //現在時分
  int alarm_hour, alarm_min;  //アラーム時分
  int wh, wm;                 //作業用時分
        
  // アラーム時分をm分後に設定
  wh = m / 60;
  wm = m % 60;
  now_min = rtc_get_min();
  now_hour = rtc_get_hour();
  alarm_min = (now_min+wm)%60;
  if (((now_min+wm)/60)==0) { //分桁上がり無し
    alarm_hour = (now_hour + wh)%24;
  } else { //分桁上がり有り
    alarm_hour = (now_hour + wh + 1)%24;
  }
  rtc_set_alarm(alarm_hour, alarm_min);
}

/*
 * void rtc_set_alarm(int h, int m) --- RTC内のレジスタにアラーム時分を書き込み
 * DaysとWDaysはダミー値を書き込み、アラーム対象外に設定
 */
void rtc_set_alarm(int h, int m){ 
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x09);
  Wire.write(DectoBCD(m));  // [09]Minutes 
  Wire.write(DectoBCD(h));  // [0A]Hours Alarm
  Wire.write(0x01 | 0x80);  // [0B]Days Alarm(1日), 先頭bitに1がある場合(0x80)はアラーム対象外
  Wire.write(0x00 | 0x80);  // [0C]Weekdays Alarm(日曜),先頭bitに1がある場合(0x80)はアラーム対象外
                            // 0:日 1:月 2:火 3:水 4:木 5:金 6:土
  Wire.endTransmission(); 
}

/* 
 * int rtc_get_hour() --- レジスタテーブルRegTblから現在のhourを返す
 * RTCのレジスタから読み出すわけではないので要注意。
 */
int rtc_get_hour(){
  return(BCDtoDec(RegTbl[4] & 0x3F));
}

/*
 * int rtc_get_min() --- レジスタテーブルRegTblから現在のminを返す
 * RTCのレジスタから読み出すわけではないので要注意。
 */
int rtc_get_min(){
  return(BCDtoDec(RegTbl[3] & 0x7F));
}

/*
 * void rtc_get_timestamp(char *at) --- レジスタテーブルからtimestampを生成 
 */
void rtc_get_timestamp(char *at) {
  String wts = String(BCDtoDec(RegTbl[8] & 0xFF)+2000); // year
  wts += '-';
  wts += ((RegTbl[7] >> 4) & 0x01);   // 月の10の位 (0,1)
  wts += (RegTbl[7] & 0x0F);          // 月の1の位 (0,,,,,9)
  wts += '-';
  wts += ((RegTbl[5] >> 4) & 0x03);   // 日の10の位 (0,,3)
  wts += (RegTbl[5] & 0x0F);          // 日の1の位 (0,,,,,9)
  wts += 'T';
  wts += ((RegTbl[4] >> 4) & 0x03);   // 時の10の位 (0,,2)
  wts += (RegTbl[4] & 0x0F);          // 時の1の位 (0,,,,,9)
  wts += ':';
  wts += ((RegTbl[3] >> 4) & 0x07);   // 分の10の位 (0,,,5)
  wts += (RegTbl[3] & 0x0F);          // 分の1の位 (0,,,,,9)
  wts += ':';
  wts += ((RegTbl[2] >> 4) & 0x07);   // 秒の10の位 (0,,,5)
  wts += (RegTbl[2] & 0x0F);          // 秒の1の位 (0,,,,,9)
  wts += "+09:00";                    // JST

  //Serial.println(wts);
  wts.toCharArray(at, ATLEN);
}

/*
 * void rtc_read_regtbl() --- RTC内のレジスタからレジスタテーブルRegTblに読み込み
 */
void rtc_read_regtbl(){
   
  // レジスタのアドレスを先頭にする
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.endTransmission(); 
 
  // I2Cスレーブに16byteのレジスタデータを要求する
  Wire.requestFrom(RTC_ADDRESS,16);
                
  // RTCREGLEN(16byte)のデータを取得する
  for (int i=0; i<RTCREGLEN; i++){
    while (Wire.available() == 0 ){}
    RegTbl[i] = Wire.read();
  }
}

////////////////////////////////////////
// 2進化10進数(BCD)を10進数に変換
////////////////////////////////////////
byte BCDtoDec(byte value){
  return ((value >> 4) * 10) + (value & 0x0F) ;
}

////////////////////////////////////////
// 10進数をBCDに変換
////////////////////////////////////////
byte DectoBCD(int value) {
  return((value/10*0x10) | (value%10));
}

そろそろ記事が長くなってきたので、続きは別記事とします。
続きはこちらへ。
techrmc.hatenablog.com

【書評】いちばんやさしいGoogleアナリティクス4の教本(著者:山浦直宏ほか、インプレス)

Webサイトのアクセス状況を分析できるGoogleアナリティクス4の教科書です。Googleアナリティクス4は、Webサイトでのユーザ行動を計測することでサイト構成の最適化を図ったり、Webサイトでのユーザ行動の変化を通じて広告宣伝やキャンペーンの効果を計測するツールです。本書では、Googleアナリティクス4の導入と活用に必要な手順を、アカウントの作成を含めて易しく解説しています。

タイトルは「いちばんやさしい」と銘打っていますが、マーケティング用語をある程度は理解していることが前提のようです。マーケティングについて全くの素人の方は、他の書籍でマーケティングの基本を勉強してから本書を読むのが効果的と思われます。

私自身が理解しづらかった用語は、下記のブログ記事で解説しましたので、参考になさってください。

techrmc.hatenablog.com

 

 

Googleアナリティクス4 超入門

Googleアナリティクス4 (GA4)とは

Googleが提供する、Webサイトの利用状況分析ツールです。Webブラウザでのアクセスだけでなく、iOSAndroidのアプリでのコンテンツ利用も分析できます。デファクトスタンダードと言える著名なツールなので、マーケティング専門企業などが使い方を詳しくネット公開しています。ただ、それらを見ていると、難しい横文字のマーケティング用語が並んでいますし、マーケティング初心者は圧倒されてしまいます。

ここでは「GA4超入門」として、私が理解しづらかった概念を中心に、できるだけ平易に解説します。具体的なGA4の使い方は豊富なネット情報をご覧ください。

GA4の限界

ネットを見ていると、GA4は何でも分析できるように見えてしまいます。マーケティング知識の豊富な方々には当たり前のことかもしれませんが、GA4の限界を列挙します。

  • GA4向けにあらかじめ準備されたコンテンツしか分析出来ない
  • 他社のWebサイトのアクセス状況は分析できない

下図は、GA4が動作する仕組みを模式的に表したものです。

GA4模式図
  1. 分析者はまず、利用状況を分析したいコンテンツに対して、あらかじめGA4用の仕掛けを組み込みます。模式図ではこの仕掛けを「センサ」と表現しました。
  2. ユーザがそのコンテンツにアクセスすると、センサの埋め込まれたコンテンツがユーザ端末にダウンロードされます。
  3. ユーザ端末では、コンテンツに埋め込まれたセンサが発動し、GA4サーバへ通知が行きます。
  4. 分析者はこの通知の膨大な蓄積データを分析することで、多数のユーザの利用状況を把握します。

このような仕組みでGA4は動作するため、コンテンツにGA4用のセンサが仕込まれていなければ、GA4はコンテンツの利用状況を知る由もありません。

また、コンテンツにセンサを仕込むには、コンテンツを改変する権限が必要です。改変権限のない他社コンテンツに対して勝手にセンサを埋め込むことはできないので、GA4での分析対象は自社サイトに限定されます。自社サイトの課題解決のためには、他社サイトと自社サイトを比較検討したいところですが、残念ながらGA4だけでは役者不足なのです。

GA4によるWebサイト改善を検討する際は、これらの限界を理解した上でGA4を利用する必要があります。

理解しづらい用語の解説

GA4の解説記事を読んでいると、理解しづらい横文字用語が並んでいて、マーケティング初心者は面食らってしまいます。ここでは、各用語の正確性はさておき、直感的に腹落ちしてもらえるような解説を試みます。

プロパティ

GA4を利用する際は、まずGA4のアカウントを設定します。このとき「プロパティ」を設定するのですが、「プロパティ」なるものがなかなか理解できませんでした。

GoogleのGA4紹介では「プロパティとは、1 つ以上のウェブサイトまたはアプリ(あるいはその両方)に関連付けられた Google アナリティクスのレポートとデータのセットです。」とありますが、今一つピンと来ません。また、GA4の正式名称は「Googleアナリティクス4プロパティ」なので、「プロパティとは何か?」ますます混迷です。

結局のところ、「プロパティ」は分析対象とする「ひとかたまりのコンテンツとデータ」ということかなと思います。

大企業の場合は、一企業が複数の商品ブランド(洗剤や化粧品など商品カテゴリごとにブランディングしていることが多い)を持っているため、ブランドごとにプロパティを設定した方が分析しやすいでしょう。そのため、分析者のアカウントに対して、複数のプロパティを設定することになります。

一方、中小企業の場合は、特定の商品カテゴリに限定した事業となっていることが多く、自社のWebサイト全体を一つのかたまりとして分析した方が効率的です。そのような場合は、分析者のアカウントに対してプロパティは一つで十分でしょう。

タグ

上述の模式図で説明したように、利用状況を分析したいコンテンツには「センサ」を埋め込みます。この時、特定目的のセンサをコンテンツに埋め込んでしまうと、分析目的が変わったときに、全てのコンテンツのセンサを分析目的に応じて改変しなければなりません。

そこで、コンテンツには分析目的に依存しない汎用的な「タグ」を埋め込んでおき、タグマネージャによって「タグ」から具体的な分析へマッピングします。分析目的が変わっても、コンテンツに埋め込んだタグを改変する必要はなく、タグマネージャ上のマッピング、すなわち、タグの処理を変更するだけです。こうした仕組みによって、GA4では分析作業の生産性を高めています。

コンバージョン

マーケティングに詳しい人と話をしていると、「コンバージョン」という言葉を良く聞きます。GA4でも「コンバージョンの測定」など「コンバージョン」という用語が頻繁に使われます。

「conversion」の一般的な日本語訳は「変換」であり、コンテンツのアクセス分析において「コンバージョン(変換)」と言われても意味不明です。一方、マーケティングの世界では、商品購入、広告閲覧、資料ダウンロードなど、何らかの成果に結びついたことを「コンバージョン」と呼んでいて、「コンバージョン=成果」と理解するのが良さそうです。「商品購入」などの具体的な行動表現では説明が長くなってしまうため、「コンバージョン」という抽象的な表現にしているのでしょう。

ユーザが成果まで辿り着かず途中で閲覧をやめてしまうとコンテンツの提供目的が未達になりますから、ユーザが「コンバージョン」へ辿り着いてくれるようなコンテンツ構成にすることが重要になります。

インサイト

マーケティング用語の「インサイト」は、潜在ニーズと同様にユーザ自身が気づいておらず、さらに潜在ニーズと呼べるまでにも至っていないような心理状態を指しています。一方、GA4でのインサイトは、機械学習で抽出されるなど、何らかのアルゴリズムで導き出される定量的測定値です。つまり、GA4で提示されるインサイトは、マーケティング用語としてのインサイトよりも狭い意味となっているので、この違いを理解した上でGA4のインサイトを利用する必要があります。