Welcome to telecotele.com » Projects

802.11 LRとLoRaとBLE Coded PHYを試す

長距離で使えるらしい無線通信を試し到達距離と消費電力量を確認します。

Author: lrks

tags: embedded radio

802.11 LRとLoRaとBLE Coded PHYを試すのカバー画像

はじめに

直接見通しなし、鉄筋コンクリート造の建物越しという環境で、室内と屋外のそれぞれに設置したデバイス間で通信したくなりました。 有線通信は無理なので無線通信が前提です。 デバイス間の直線距離は50mから100mほどの予定で、普段使っているWi-FiやBLEだと届きません。 ここでいうデバイスの、特に屋外側はマイコンなどと組み合わせたセンサを想定しており、何らかの電池で駆動するので省電力にしたいとも思っています。

最近では(電源や費用を考えなければ)衛星通信も使えて、空が見えればどうとでもなりそうですが……そこまで大げさにはしたくありません。 携帯電話回線など公衆無線回線に接続するのは月額費用がかかるので嫌。 1NCEを使う?そもそも通信モジュールが高価なので別の方法が使えるならそちらにしたい。

ということで、長距離で使えるらしい802.11 LRとLoRaとBLE Coded PHYを試し、到達距離と消費電力量を確認します。

802.11 LR

802.11 LRは、中国のEspressif Systems社が提供するWi-Fiの拡張モードです。 同社のWi-Fi機能を備えたESP32系デバイス同士の通信で利用できます1。 L2以上?より上?は変更されないようで、APやSTAといった仕組みや、もちろんその上のIP、TCP、UDPなどのレイヤもそのままで使えます。

802.11 LRでは、IEEE 802.11bと比べ受信感度は4dB改善されるようです2。 その代わりデータレートは遅くなり、250 kbpsまたは500 kbpsとなります。

“802.11”とありますがこれは独自の規格です。 IEEEとは一言も書かれていません。 “LR”についても、“Low Rate”だったり、“Long Range”だったりよく分かりません。 ほか技術の詳細も公開されておらず、特許があるらしいですがそれも見つけられず謎です。 後述するBLE Coded PHYのように、既存PHYのパラメータを調整したものじゃないかという気はしますが……。

補足: ESP-NOW

検証の中で、ESP-NOWも利用するので補足しておきます。

ESP-NOWは、Espressif Systems社が提供するデータリンク層プロトコルです。 コネクションレスのプロトコルで、同社のWi-Fi機能を備えたデバイス同士の通信に利用できます3

ESP-NOWでは長距離で通信できることが謳われていますが4、これをESP-NOW自体の特徴として良いかは微妙です。 というのも、ESP-NOWの実態はIEEE 802.11の管理フレーム (vendor-specific action frame) を利用しています5。 フレームの使い方の話であって、おそらく「802.11 LRを使えば長距離」ということでしょう。 既存のIEEE 802.11bなどを使っているなら通信距離も変わりません。

802.11 LRの利用はさておき既存のWi-Fi規格を使うなら、ESP-NOWなんて使わず普通にその辺のAPに接続して通信させれば良いのでは? デバイス同士で通信させたいならどちらかでSoftAPを立ててもう一方から接続すれば良い。 コネクションレスが利点?UDPを使えば良いだけでしょう……と思うかも知れません。

ESP-NOWの(個人的に思う大きな)特徴はフレームを直で使うNon-IP6であることです。

もしESP-NOWを使わないとすると、まずどこかのAPに接続する必要があります。 接続方式によっても多少変わりますが、まずAPをスキャンして認証と接続要求を出すことになるでしょう。 APに接続後、たとえばどこかのサーバにUDPでデータを送信したくとも、まずはDHCPでIPアドレスを取得したり、ARPパケットを出して宛先のMACアドレスを取得する必要があります。 これらが済んでようやくデータを流せるようになります。

これ以降継続的にデータを流すなら気になりません。 ただ、もし電池駆動でDeepSleepと組み合わせてたまに起きてデータを流すような構成だったら厄介です。 実際にデータを流すまでいくら時間がかかるのか、電力消費が大きいRF機能をどれだけ動かさないといけないのか。 APの接続情報をキャッシュするとか、DHCPを使わないとか、Static ARPを使うとか多少の工夫はできるかも知れませんが……。

これがESP-NOWだと、1発目からデータを流せます。良かったですね。 対向デバイス(に到達してユニキャストだった場合)はフレームのACKを返してくれるはずですが、この受信を待たずに打ち逃げでRF機能をOFFにしたり、DeepSleepしたりは自由です。

脱線: ESP-NOWのセキュリティ

ここからは補足に留まらない脱線として、ESP-NOWのセキュリティについて述べます。

ESP-NOWではフレームの暗号化も可能です。 これはWPA2で使用されるCCMPを用いるもので、事前にデバイス同士で鍵を共有することで実現します。 良さそうですね。

ただ、注意が必要なこととして 受信者は「暗号化されたデータ」も「平文のデータ」も受け取ってしまう ことが挙げられます。 平文であれば鍵は不要なため、これは第三者を含む任意の送信者が受信者に受理される7データを送信できるという意味です。

当初は何か勘違いしているのかと思いましたが、実際に試してみるとそのとおりの挙動でした。 下記のWebサイトとそのコメントでも言及されており、結構ぶち当たる問題なのかも知れません。

ESP32: ESP-NOW Encrypted Messages | Random Nerd Tutorials
https://randomnerdtutorials.com/esp32-esp-now-encrypted-messages/
The receiver has the code for encryption but the sender doesn’t: the receiver gets the messages anyway. I don’t think this is the behavior we expected. Since we add the encryption code on both boards, one would expect that if the receiver board got a message that is not encrypted, it would ignore it. But that’s not what happens. It receives all messages, encrypted and not encrypted. At the moment, there isn’t a way to know if the received message is encrypted or not, which seems like a limitation at the moment.

ということで、暗号化が有効な状態で送信者が送ったデータを第三者が復号できない(または現実的に困難)なことは実現できても、受信者が受け取ったデータが正当な送信者から送られたデータかどうかは分からない、となりました。 これはアプリケーション側でメッセージ認証 (Message Authentication Code, MAC) を入れたくなりますね。 さらに実用上はリプレイ攻撃の防止・緩和のため、また再送による重複排除、リオーダリングを実現するため乱数列やシーケンス番号を付与することになるでしょう。

いやでもどうやってシーケンス番号の同期を取るんですか、予測可能なら意味ないですよ。 とか考えるとデバイス同士でコネクションを確立することとなり、それは結局コネクションレスの利点を活かせずESP-NOWでなくとも良いのでは……?という気になってきますね。 ESP-NOWの上でTCP/IPを動かすのは本末転倒です。 現実的には、デバイス動作時に毎回コネクションを張るのではなく、事前に一度だけペアリングをしておきそこでシーケンス番号や乱数シードの同期を取るとかでしょうか。

脱線: ESP32-C3のMAC計算時間

さらなる脱線です。

前述のとおりメッセージ認証を入れたいところですが、組み込み機器での利用を考えるとあまり負荷がかかる方式は採用できません。 幸い、今回注目しているESP32のデバイス、特に後述の検証で利用する予定のESP32-C3ではメッセージ認証の計算にハードウェア支援が利用できる場合があります。 そこで、いくつかのメッセージ認証について実際の処理時間を計測し、どの方式が現実的か検討してみます。

計測に使用したソースコードはこちら(長いので折りたたみ)
#include <esp_wifi.h>
#include <esp_hmac.h>
#include <hal/efuse_hal.h>
#include <tinycrypt/aes.h>
#include <tinycrypt/constants.h>
#include <tinycrypt/cmac_mode.h>
#include <tinycrypt/hmac.h>
#include <tinycrypt/sha256.h>

#include "mbedtls/ecp.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/md.h"
#include "mbedtls/aes.h"
#include "mbedtls/bignum.h"
#include "mbedtls/pkcs5.h"
#include "mbedtls/cmac.h"
#include "mbedtls/nist_kw.h"
#include "mbedtls/des.h"
#include "mbedtls/ccm.h"
#include "mbedtls/esp_config.h"
#include "mbedtls/sha1.h"

uint16_t udp_checksum(const uint8_t *data, size_t len) {
  uint32_t sum = 0;
  while (len >= 2) {
      uint16_t word = (data[0] << 8) | data[1];
      sum += word;
      data += 2;
      len -= 2;
  }
  if (len > 0) sum += (uint16_t)(data[0] << 8);

  sum = (sum & 0xFFFF) + (sum >> 16);
  sum = (sum & 0xFFFF) + (sum >> 16);
  uint16_t result = ~sum;
  return (result == 0x0000) ? 0xFFFF : result;
}

uint16_t calculateCRC(const uint8_t *data, size_t length) {
  // https://github.com/espressif/arduino-esp32/blob/de184bd0cbb6f8d6c68497236657a1cb591f5ce4/libraries/SD/src/sd_diskio_crc.c
  const uint16_t m_CRC16Table[256] = {
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273,
    0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7,
    0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B,
    0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF,
    0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B,
    0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6,
    0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C,
    0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
    0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0,
    0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676,
    0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D,
    0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D,
    0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9,
    0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
  };

  uint16_t crc = 0;
  for (size_t i=0; i<length; i++) {
    crc = (crc << 8) ^ m_CRC16Table[((crc >> 8) ^ data[i]) & 0x00FF];
  }
  return crc;
}

void tinycrypt_cmac(const uint8_t *key, const uint8_t *message, size_t len, uint8_t *mac) {
  struct tc_aes_key_sched_struct sched = {0};
  struct tc_cmac_struct state = {0};
  if (tc_cmac_setup(&state, key, &sched) == TC_CRYPTO_FAIL) return;
  if (tc_cmac_update(&state, message, len) == TC_CRYPTO_FAIL) return;
  if (tc_cmac_final(mac, &state) == TC_CRYPTO_FAIL) return;
}

void global_omac1_aes_128(const uint8_t *key, const uint8_t *message, size_t len, uint8_t *mac) {
  g_wifi_default_wpa_crypto_funcs.omac1_aes_128(key, message, len, mac);
}

void mbedtls_cmac(const uint8_t *key, const uint8_t *message, size_t len, uint8_t *mac) {
  // based on https://github.com/espressif/esp-idf/blob/e9bdd395995f60878e01cece10525987bacc8a5f/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c#L887
  const mbedtls_cipher_info_t *cipher_info;
  mbedtls_cipher_context_t ctx;

  cipher_info = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB);
  if (cipher_info == NULL) return;

  mbedtls_cipher_init(&ctx);
  if (mbedtls_cipher_setup(&ctx, cipher_info) != 0) {
    goto cleanup;
  }
  if (mbedtls_cipher_cmac_starts(&ctx, key, 128) != 0) {
    goto cleanup;
  }
  if (mbedtls_cipher_cmac_update(&ctx, message, len) != 0) {
    goto cleanup;
  }
  mbedtls_cipher_cmac_finish(&ctx, mac);
cleanup:
  mbedtls_cipher_free(&ctx);
}

void hardware_hmac(const uint8_t *message, size_t len, uint8_t *mac) {
  // 
  // $ espefuse.py --port xxx burn_key BLOCK_KEY3 hmac_key.bin HMAC_UP
  esp_hmac_calculate(HMAC_KEY3, message, len, mac);
}

void global_hmac_sha256(const uint8_t *key, const uint8_t *message, size_t len, uint8_t *mac) {
  g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(key, 32, 1, &message, (const int*)&len, mac);
}

void mbedtls_hmac(const uint8_t *key, const uint8_t *message, size_t len, uint8_t *mac) {
  // based on https://github.com/espressif/esp-idf/blob/e9bdd395995f60878e01cece10525987bacc8a5f/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c#L307
  mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
  const mbedtls_md_info_t *md_info;
  mbedtls_md_context_t md_ctx;

  mbedtls_md_init(&md_ctx);
  md_info = mbedtls_md_info_from_type(md_type);
  if (!md_info) return;

  if (mbedtls_md_setup(&md_ctx, md_info, 1) != 0) {
    return;
  }
  if (mbedtls_md_hmac_starts(&md_ctx, key, 32) != 0) {
    return;
  }
  if (mbedtls_md_hmac_update(&md_ctx, message, len) != 0) {
    return;
  }
  mbedtls_md_hmac_finish(&md_ctx, mac);
  mbedtls_md_free(&md_ctx);
}

void setup() {
  // CDC
  delay(1000);
  Serial.begin();
  Serial.setDebugOutput(true);
  for (int i=0; i<10 && !Serial; i++) {
    delay(100);
  }
}

void loop() {
  unsigned long start, diff;
  uint8_t cmac_key[16] = {
    0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x01, 0x89, 0x98, 0x23, 0x76, 0x30
  };
  uint8_t cmac[16];
  uint8_t hmac_key[32] = {
    0x05, 0xb6, 0xbe, 0x3b, 0x6f, 0x47, 0x33, 0x5b, 0x41, 0xcf, 0xe0, 0x70, 0x39, 0x67, 0x1d, 0x8b, 
    0x73, 0x11, 0x8a, 0xbd, 0x71, 0xe3, 0x5d, 0x1b, 0x96, 0x97, 0x17, 0x77, 0x74, 0x55, 0x34, 0x93
  };
  uint8_t hmac[32];

  // msg1:6bytes(48bits), cmac:a80311670dfea6567ebb8c6433073e15, hmac:f5f5e7d1637ceb986040757c38f8c8e781aa792e87389501c407c5ce7601da9b
  uint8_t msg1[] = "Hello1";
  size_t msg1_len = strlen((char *)msg1);
  // msg2:16bytes(128bits), cmac:2df0ee7779412b19eb871e4a6b5a78e8, hmac:5ad3093c3ba1a9002987c6dc5be0df1286be0e297209646ab202199ddbfb958c
  uint8_t msg2[] = "Hello2Hello2Hell";
  size_t msg2_len = strlen((char *)msg2);
  // msg3:18bytes(144bits), cmac:26300c57a10b5a3417f7c28a879fb3fc, hmac:092acc0577a9ee2aa385498ca3f7be001fe8e1b5de6574f9b18d08c0ad1ccfaf
  uint8_t msg3[] = "Hello3Hello3Hello3";
  size_t msg3_len = strlen((char *)msg3);
  // msg4:42bytes(336bits), cmac:3abd45c10c9e779a0510cee9224f72d2, hmac:d0410a865d0231bbfa00ba7c85c998c84bda6e2072330df8899d394a53ade209
  uint8_t msg4[] = "Hello4Hello4Hello4Hello4Hello4Hello4Hello4";
  size_t msg4_len = strlen((char *)msg4);
  // msg5:250bytes, cmac:4b3df02bbe040a04ddf81964fe4b7df8, hmac:f246f1a6e1f454f6601aab737220500a3399c36f5b76acd8ecbda6faa644428a
  uint8_t msg5[] = "Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hello5Hell";
  size_t msg5_len = strlen((char *)msg5);
  
  Serial.println("------");

  // noop
  for (int i=0; i<5; i++) {
    volatile int hoge;
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) hoge = i;
      if (i >= 1) hoge = i;
      if (i >= 2) hoge = i;
      if (i >= 3) hoge = i;
      if (i >= 4) hoge = i;
    }
    diff = micros() - start;
    Serial.printf("noop(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }
  Serial.println("--");

  // udp_checksum
  for (int i=0; i<5; i++) {
    volatile uint16_t hoge;
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) hoge = udp_checksum(msg1, msg1_len);
      if (i >= 1) hoge = udp_checksum(msg2, msg2_len);
      if (i >= 2) hoge = udp_checksum(msg3, msg3_len);
      if (i >= 3) hoge = udp_checksum(msg4, msg4_len);
      if (i >= 4) hoge = udp_checksum(msg5, msg5_len);
    }
    diff = micros() - start;
    Serial.printf("udp_checksum(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }

  // calculateCRC
  for (int i=0; i<5; i++) {
    volatile uint16_t hoge;
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) hoge = calculateCRC(msg1, msg1_len);
      if (i >= 1) hoge = calculateCRC(msg2, msg2_len);
      if (i >= 2) hoge = calculateCRC(msg3, msg3_len);
      if (i >= 3) hoge = calculateCRC(msg4, msg4_len);
      if (i >= 4) hoge = calculateCRC(msg5, msg5_len);
    }
    diff = micros() - start;
    Serial.printf("calculateCRC(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }
  Serial.println("--");

  // tinycrypt_cmac
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) tinycrypt_cmac(cmac_key, msg1, msg1_len, cmac);
      if (i >= 1) tinycrypt_cmac(cmac_key, msg2, msg2_len, cmac);
      if (i >= 2) tinycrypt_cmac(cmac_key, msg3, msg3_len, cmac);
      if (i >= 3) tinycrypt_cmac(cmac_key, msg4, msg4_len, cmac);
      if (i >= 4) tinycrypt_cmac(cmac_key, msg5, msg5_len, cmac);
    }
    diff = micros() - start;
    Serial.printf("tinycrypt_cmac(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }

  // global_omac1_aes_128
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) global_omac1_aes_128(cmac_key, msg1, msg1_len, cmac);
      if (i >= 1) global_omac1_aes_128(cmac_key, msg2, msg2_len, cmac);
      if (i >= 2) global_omac1_aes_128(cmac_key, msg3, msg3_len, cmac);
      if (i >= 3) global_omac1_aes_128(cmac_key, msg4, msg4_len, cmac);
      if (i >= 4) global_omac1_aes_128(cmac_key, msg5, msg5_len, cmac);
    }
    diff = micros() - start;
    Serial.printf("global_omac1_aes_128(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }
  
  // mbedtls_cmac
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) mbedtls_cmac(cmac_key, msg1, msg1_len, cmac);
      if (i >= 1) mbedtls_cmac(cmac_key, msg2, msg2_len, cmac);
      if (i >= 2) mbedtls_cmac(cmac_key, msg3, msg3_len, cmac);
      if (i >= 3) mbedtls_cmac(cmac_key, msg4, msg4_len, cmac);
      if (i >= 4) mbedtls_cmac(cmac_key, msg5, msg5_len, cmac);
    }
    diff = micros() - start;
    Serial.printf("mbedtls_cmac(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }
  Serial.println("--");

  // hardware_hmac
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) hardware_hmac(msg1, msg1_len, hmac);
      if (i >= 1) hardware_hmac(msg2, msg2_len, hmac);
      if (i >= 2) hardware_hmac(msg3, msg3_len, hmac);
      if (i >= 3) hardware_hmac(msg4, msg4_len, hmac);
      if (i >= 4) hardware_hmac(msg5, msg5_len, hmac);
    }
    diff = micros() - start;
    Serial.printf("hardware_hmac(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }

  // global_hmac_sha256
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) global_hmac_sha256(hmac_key, msg1, msg1_len, hmac);
      if (i >= 1) global_hmac_sha256(hmac_key, msg2, msg2_len, hmac);
      if (i >= 2) global_hmac_sha256(hmac_key, msg3, msg3_len, hmac);
      if (i >= 3) global_hmac_sha256(hmac_key, msg4, msg4_len, hmac);
      if (i >= 4) global_hmac_sha256(hmac_key, msg5, msg5_len, hmac);
    }
    diff = micros() - start;
    Serial.printf("global_hmac_sha256(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }

  // mbedtls_hmac
  for (int i=0; i<5; i++) {
    start = micros();
    for (int j=0; j<100; j++) {
      if (i >= 0) mbedtls_hmac(hmac_key, msg1, msg1_len, hmac);
      if (i >= 1) mbedtls_hmac(hmac_key, msg2, msg2_len, hmac);
      if (i >= 2) mbedtls_hmac(hmac_key, msg3, msg3_len, hmac);
      if (i >= 3) mbedtls_hmac(hmac_key, msg4, msg4_len, hmac);
      if (i >= 4) mbedtls_hmac(hmac_key, msg5, msg5_len, hmac);
    }
    diff = micros() - start;
    Serial.printf("mbedtls_hmac(%d): %lu.%lu[ms]\r\n", i, diff/1000, diff%1000);
  }

  delay(10000);
}

入力データは下記の5つを用意しました。 これらの各データに対してそれぞれ100回メッセージ認証の計算を行います。

メッセージID長さ
msg16 bytes
msg216 bytes
msg318 bytes
msg442 bytes
msg5250 bytes

……と、その前に比較対象として、メッセージ認証とは関係なく単純に100回分forループを回した8とき (noop) 、UDPチェックサム (udp_checksum)、CRC16 (calculateCRC) で計算しました。 結果はこれです。

メッセージIDnoopudp_checksumcalculateCRC
msg10.13 [ms]0.78 [ms]0.369 [ms]
msg20.7 [ms]0.201 [ms]0.777 [ms]
msg30.9 [ms]0.323 [ms]1.247 [ms]
msg40.10 [ms]0.602 [ms]1.976 [ms]
msg50.9 [ms]2.114 [ms]5.62 [ms]

なぜかudp_checksumがnoopよりも高速なときがありますがこれは誤差として、別に割り込み禁止とかやっていないんで……くらいのゆるふわ検証として見てください。

次にCMAC (Cipher-based MAC)によるメッセージ認証を計算させてみます。 内部で使われているブロック暗号の計算にハードウェア支援が使われる可能性があり、効率的に計算できるかな?と期待しています。 TinyCryptの実装 (tinycrypt_cmac)、WPAで使われている実装 (global_omac1_aes_128)、Mbed TLSの実装 (mbedtls_cmac) での結果がこちら。

メッセージIDtinycrypt _cmacglobal_omac1 _aes_128mbedtls _cmac
msg114.830 [ms]9.688 [ms]9.496 [ms]
msg229.456 [ms]18.954 [ms]18.852 [ms]
msg350.926 [ms]31.468 [ms]31.335 [ms]
msg479.152 [ms]46.995 [ms]46.820 [ms]
msg5195.85 [ms]101.701 [ms]101.502 [ms]

global_omac1_aes_128とmbedtls_cmacは実質同じに見えますね。

次にHMAC (Hash-based MAC) によるメッセージ認証も計算させてみます。 HMACに関しては直接ハードウェア支援を利用できる (hardware_hmac) のですが、他にWPAで使われている実装 (global_hmac_sha256)、Mbed TLSの実装 (mbedtls_hmac) も合わせて計算してみた結果がこちら。

メッセージIDhardware _hmacglobal_hmac _sha256mbedtls _hmac
msg11.852 [ms]10.871 [ms]10.737 [ms]
msg23.650 [ms]21.291 [ms]21.225 [ms]
msg35.474 [ms]32.0 [ms]31.893 [ms]
msg47.287 [ms]42.578 [ms]42.437 [ms]
msg510.228 [ms]56.568 [ms]56.388 [ms]

さすがハードウェア支援があるhardware_hmacは高速ですね。

ただし、hardware_hmacを使うにはあらかじめeFuseに鍵を書き込んでおく必要があります。 eFuseは変更不可のため、鍵ローテーションが不可能です。 そして、ここで計算しているは256 bit (32 bytes) のHMACであり、もし数バイトのデータを送ることを想定しているならオーバーヘッドが大きいと感じるかも知れません。 そこで、半分に切り詰めて128 bitとしてみましょう。 やや古いですがCRYPTRECによるガイド9によれば(128 bitのCMACにおける文脈として)1つの鍵でMAC作成を行う場合のメッセージ数の推奨値は2482^{48}個までとされています。 もし1ms毎にMACを作成するなら8925年、1us毎に作成するなら8.925年で到達します。 これが製品寿命となるでしょう。 現実的なのでこれで良いかも知れませんが、これを信用して実製品に採用されてもそこで発生したリスクについては負えません。

LoRa

次はLoRaです。 機械学習におけるファインチューニングの文脈で使われるLoRAではありません。 ウフフ、オッケーですか?

さて、LoRaはアメリカのSemtech社による独自の変調方式です。 これはFSK(周波数変調)やPSK(位相偏移変調)などデジタル変調の一種で、長距離通信を目的に設計されています。 長距離通信ということでLoRa (Long Range)という名前にはなっていますが、コア技術としてはチャープスペクトラム拡散という変調方式が利用されている模様。 これは周波数を連続的に変化させてシンボルを表現するため、見かけは周波数変調っぽいですね10

このLoRaを利用して公衆無線回線を提供するための規格として、LoRaWANも策定されています。 LoRaWANでは利用周波数やフレーム形式などの仕様が定められており、もしこれを使うのであれば適切な通信モジュールやアプリケーションの実装が必要です。 もちろん回線提供業者との契約も必要で、契約金や月額料金もかかるでしょう。

しかし、LoRaWANとは関係なくLoRaを利用している(と主張する)通信モジュールも販売されており、単にLoRaが使いたいだけならこうしたモジュールが使えます。 その辺の独自FSK通信モジュールを使うのと何も変わりません。 モジュールを複数買って、そのモジュール同士で通信させるだけです。

こうしたLoRaの利用については、LoRaWANとの違いを明確にするため「Private LoRa」などと呼ばれる模様。 周波数帯についてはLoRaそのものでは規定されておらず自由ですが、たとえば日本国内で利用可能として販売されている通信モジュールだとまず920MHz帯を使うものがあります。 このほか426MHzや429MHzを使うものや、2.4GHzを使うもの11もありました。

とはいえ、手軽に利用できる通信モジュールは920MHz帯のものです。 今回は、その中でも入手しやすく安価で、もちろん技適も取得済みの「株式会社クレアリンクテクノロジー E220-900T22S(JP)」を利用して、検証を行ってみます。

920MHz帯のルール

LoRaで920MHzを利用するにあたって、下記の資料を参照し各種ルールを確認します。

総務省 電波利用ポータル|免許関係|920MHz帯の無線局の利用
https://www.tele.soumu.go.jp/j/adm/system/ml/920mhz/index.htm

920MHz帯はLoRa以外にも様々な無線システムが混在するためややこしい12ですね。 ここではLoRaの通信モジュールが使うであろう「アクティブ系:テレメータ用・テレコントロール用及びデータ伝送用(センサー通信系)」内の「陸上移動局13」「特定小電力無線局 中出力型(キャリアセンス要)14」のみに注目してみます。

まず、無線局の種類による違いは下記のとおり。

陸上移動局特定小電力無線局 中出力型(キャリアセンス要)
登録不要
空中線電力250mW以下20mW以下
周波数920.5MHz ~ 923.5MHz920.5MHz ~ 928.1MHz
チャネル200kHz間隔 計15ch200kHz間隔 計38ch
同時使用可能チャネル数1 ~ 51 ~ 20

周波数は一部被っています。 そして、登録の要/不要と空中線電力の違いが大きい。 今回の検証で利用する通信モジュールは特定小電力無線局に該当し免許や登録は不要ですが、高出力版のE220-900T22L(JP)15などは陸上移動局に該当するため登録が必要です。

そして、周波数の範囲によって送信時間や休止時間、キャリアセンス(送信前に他の無線局が居ないか確認することで混信を防止する)時間のルールが変わります。 あくまで「アクティブ系」の「陸上移動局」と「特定小電力無線局 中出力型(キャリアセンス要)」のことだけを考えてまとめると下記のとおり。

920.5MHz ~ 923.5MHz923.5MHz ~ 928.1MHz
1回の送信時間4秒以下400ms以下
送信時間の総和規定なし1時間あたり360秒以下
送信後の休止時間50ms以上2ms以上
キャリアセンス時間5ms以上128us以上

陸上移動局は920.5MHz ~ 923.5MHz側しか選べませんが、中出力型(キャリアセンス要)はどちらも選べる。 これらのルールは通信モジュール側で遵守してくれるため、ユーザ側で制御する必要はありません。

とはいえ、ユーザ側でも923.5MHz ~ 928.1MHzの送信時間が厳しめなどを意識しておくことは必要です。 E220-900T22S(JP)では、この制約を満たすために(該当するチャネルを使う場合)送信後にその送信時間の10倍休止時間を設けるよう制御16されています。 これが嫌なら920.5MHz ~ 923.5MHzのチャネルを使ってねということらしい。 この辺は使用する通信モジュールによって制約を満たす実装方法が変わるかも知れませんが、こうした仕様を把握したり想定したりしながら使うことになるでしょう。

それでも休止時間やキャリアセンス時間はあるので、単純に考えると1分1秒1msを争うような即時性のあるデータや、ストリームで音声・映像を流すような用途は厳しそうですね。 LoRaだとデータレートが低いのでそもそも映像は厳しい17としても、即時性とかストリームでデータを流すということは考えられるのでは?

これは「特定小電力無線局 中出力型 (FH)」や「特定小電力無線局 中出力型 (LDC)」という種類の無線局を使えばなんとかなるかも知れません。 FHは周波数ホッピング (Frequency Hopping)を表しチャネルを変えながら通信することで特定チャネルの占有を防ぎ他の無線との干渉を軽減するという方式、LDCはLow Duty Cycleの意味で送信を低頻度にすることで他の無線との干渉を軽減する方式です。 干渉を軽減する仕組みが別で用意されているため、キャリアセンスをしなくても良いとされています。 これらを利用するにあたってのルールをまとめてみると下記のとおり。

中出力型 (FH)中出力型 (LDC)
登録不要不要
空中線電力20mW以下20mW以下
周波数920.5MHz ~ 925.1MHz920.5MHz ~ 923.5MHz
チャネル200kHz間隔 計23ch200kHz間隔 計15ch
同時使用可能チャネル数11
1回の送信時間400ms以下4秒以下
送信時間の総和【全体】1時間あたり720秒以下
【チャネル単位】1時間あたり36秒以下
1時間あたり36秒以下
送信後の休止時間同一周波数の場合4秒以上なし or 50ms以上?
キャリアセンス時間なしなし

微妙に周波数も変わっています。 この方式に対応した通信モジュールがあるか、入手性や価格などは調べていませんが、やっぱりキャリアセンス時間なしというのが大きな特徴ですね。

ただ、FHを使ったとしても常時ストリームでデータを流すというのは難しそうです。 2秒分を圧縮して400ms以内に流す、たとえば映像なら0.5fpsならいけそう?というのはデータレート的にいけるかどうかといった感じです。 同時使用チャネル数が1つで200kHzの帯域しか使えないので厳しそうですが……。 通信モジュールを複数台用意して束ねます、というのは倫理的や法的に許されるかは分かりません。 結局ストリームで流すのが難しいなら、キャリアセンスを必要とする方式のほうが同時に複数チャネルを使用できデータレートを上げられる可能性があるのでそっちのほうが良いのかな……という気になってきます。

一方で、LDCは低頻度だけど即時にデータを送りたい場合に使えそうです。 ただ、これはキャリアセンスが無いことの裏返しですが、送信しても他の無線局と干渉してしまい届かない可能性があるので、重要なデータの場合は何回か再送することになるのかなと思います。 そうすると、「送信後の休止時間」が気になってきますが……これはよく分かりません。 冒頭に示した総務省電波利用ポータルの情報では「―」となっています。 しかし、下記で引用する告示を見ると「50ms以上(ただし、送信時間内の再送信は休止時間を設けずでOK)」と読み取れます。

平成元年郵政省告示第四十九号(無線設備規則第四十九条の十四の規定に基づく特定小電力無線局の無線設備の一の筐体に体に収めることを要しない装置等) https://www.tele.soumu.go.jp/horei/law_honbun/72138100.html 二2(5)
九二〇・五MHz以上九二三・五MHz以下の周波数の電波を使用する無線設備(設備規則第四十九条の十四第七号ニ(2)に規定する無線局のものに限る。)の送信時間制限装置は、当該無線設備の一時間当たりの送信時間の総和が三十六秒以下であって、電波を発射してから四秒以内にその発射を停止し、かつ、〇・〇五秒の送信休止時間を経過した後でなければその後の送信を行わないものであること。ただし、最初に電波を発射してから四秒以内に再送信(当該時間内に停止する再送信に限る。)を行う場合に限り、当該送信休止時間を設けずに送信を行うことができる。

送信時間内の再送信は他方式でも休止時間を設けずでOKのようで、それに合わせると「LDCにおける送信後の休止時間は50ms」となるのでは? 自分が誤解しているのか、法改正があって古い情報を参照したのかも知れませんが、よく分かりませんでした!いかがでしたか?

E220-900T22S(JP)

今回の検証で使う通信モジュールについても簡単に述べておきます。

ここでは株式会社クレアリンクテクノロジーが販売するE220-900T22S(JP)モジュールを使います。 これは中国のEBYTE社が販売しているE220シリーズの通信モジュール18を日本向けにカスタマイズして技適を取得したモジュールのようです。

動作モードとしては通常の送受信や、パラメータ設定を行うモードなどがあります。 モードの切り替えは外部からM1/M0ピンを操作することで可能。

パラメータ設定モードで、GUIから設定を行うPythonスクリプトは「LoRaモジュール評価ボード ご利用方法」19としてクレアリンクテクノロジーから配布されています。 設定項目の詳細については割愛しますが、GUIの設定画面としては下記のとおり。 (アドレスや暗号化キーは10進数で指定)

E220-900T22S(JP)のGUI設定画面 E220-900T22S(JP)のGUI設定画面

ほか便利ツールとして「LoRaデータ送信時間計算表」も配布されています。 時間がかかるからデータ削ろうかな……HMACは256bitじゃなくて128bitにしようかな……といった判断に使えそうです。

なお、パラメータ設定モードはDeepSleepとしても扱われます。 そのため、ノーマルモードからの切り替え、またはパラメータ設定モード(DeepSleep)からの復帰を考えて、実運用でもM1/M0のピンを外部のMCUか何かから切り替えられるようにしておく必要がありそうです。 ここで、AUXというモジュールのビジー状態を出力ピンもあり、これがHigh出力(非ビジー状態)なら即座にモード変更可能とのこと。 ただ、データ送信時のノーマルモードからパラメータ設定モード(DeepSleep)への移行について考えると、データ送信中はビジー状態ですが、この時点でモード切替を行っても良いらしい。 送信が済んでからモード切り替えを行ってくれる。 それであればAUXはあまり見なくても良いかな……いや一応確認で見れるなら見ておこうかな……くらいの気持ちになっています。

また、E220-900T22S(JP)にはWOR (Wake On Radio)の機能があります。 これは送信側と受信側ともに専用モードにしないと使えませんが、Sleep状態20のモジュールを送信側から起こす機能です。 受信側では500ms~3000ms間隔で一瞬起きてデータの到達を確認し、おそらく送信側ではこのタイミングに入るようにデータを送信し続けるのでしょう。 この動作は920MHz帯における送信時間の制約に引っかかるので使用できるチャネルは限られます。 そして、受信側ではやっぱり動作時は電流をそこそこ消費します。 Sleepの時間に比べて動作している時間はわずかとはいえ、電池駆動の場合はWORの頻度自体を減らして、たとえば特定のタイミングだけ受信できるようにするとかいう工夫が必要かも知れません。 でもタイミングが同期できるならWORを使わずノーマルモードで送受信したら?同期が難しいからWORを使うのでは?というのはそのとおり。

最後にセキュリティについて述べます。 一応データは暗号化された状態で送信され、受信時に復号されるようです。 ただ、暗号化に使われる鍵は16bitと強固ではありません。 暗号化方式も非公開で安全か否かの判断がつかず、機密性の確保に課題があります。 この暗号化はグループ同士のIDやアドレスといった扱いで、信頼できる暗号化が必要なら上位のアプリケーション側で行う想定です。 もともとメッセージ認証の機能はないため、セキュリティに関してはどのみちアプリケーション側で考える必要があります。

SDRによるデコード

SDRを試した日記 その0.1で触れた(でも試さなかった)SDRangelによるLoRaのデコードを試してみます。

何か開発したわけではなく、ただ既存のChirpChat Demodulatorを使っただけです。

sdrangel/plugins/channelrx/demodchirpchat/readme.md at master · f4exb/sdrangel
https://github.com/f4exb/sdrangel/blob/master/plugins/channelrx/demodchirpchat/readme.md

結果は下記のとおり。

SDRangel ChirpChat DemodulatorによるLoRaのデコード SDRangel ChirpChat DemodulatorによるLoRaのデコード

ChirpChat Demodulatorは今回使っているモジュールのLoRaチップ(LLCC68)での動作は確認されていないようですが、それらしくデコードできていそうな気がします。 いや……雰囲気は良いですが、モジュールによる暗号化の影響で正しくデコードできているか分からない。 HF:errHC:errとなっているので、やっぱりデコードできていないかも……。

もう少しパラメータを調整する必要がある? それともLLCC68のためにデコーダのコードから変える必要がある? この辺は暗号化を解いて、送信した平文を確認するまでははっきりしませんね。 任意の暗号化キーを設定でき、任意に設定できる平文21から暗号文(ただし正しくデコードできたかは保証がない)を得られる状態にはあるので、根気があれば復号できるかも知れません。

BLE Coded PHY

次はBluetooth Low Energy (BLE) Coded PHYです。 「Long Rangeモード」とされているようですが……正直それ以外はあまり調べていません。

既存のPHYをベースとしつつも、FECを追加して実質のデータレートを犠牲にする代わりに伝送距離を伸ばすらしいです。 そのデータレートは500kbpsか125kbpsになるとのこと。 あくまで表面的な話ですが、なんとなく802.11 LRを思い出してきますね。

ほかセキュリティについてはBLE自体の話であり、ひとまずCoded PHYのことだけを考えたいので気にしないことにします。 また、実装として検証ではESP32-C3を使いましたが、アドバタイズのタイミングがなかなか制御できず、BLE Coded PHY版のESP-NOWとかあると嬉しいなぁとは思いますが、これはより関係ない話でしょう。

ほか

今回は試しませんが、ほかにも通信規格や通信モジュールがあります。

たとえばZigBeeは入手しやすいのは2.4GHz帯を使うものですが、この帯域は802.11 LRとBLE Coded PHYで検証するしまぁ別に良いかな……。 そしてWi-SUNは920MHz帯だけどモジュールが高価なのでこれも別に良いかな……。 と、検証を見送りました。

ただ、920MHz帯のWi-Fi HaLow22はちょっと検証しても良かったかもという気がしてきます。 周波数の範囲や1,2,4MHzという帯域幅からして「特定小電力無線局 中出力型(キャリアセンス要)」が使われている模様。 利用例として監視カメラの映像伝送があり、キャリアセンスや送信後の休止時間を考慮していけるもんなんですね。 そして、Wi-Fi HaLowではよくDuty比10%ルールについて言及もされている。 1MHz幅で920.5MHz ~ 923.5MHzだけを使うなら送信時間の総和(Duty比)の制約はないと思いますが、そうした使い方は不可で4MHz幅を見越して923.5MHz ~ 928.1MHzを使う前提で実装されている?のかも知れませんが、この辺は分かりません。 2029年から850MHz帯が使える見通しで、そこではDuty比100%も可能なようなので、その際は検証したくなってきました。

また、LTEも使えるなら使ってみたいところでした。 ただやっぱり回線事業者と契約が発生するのと、通信モジュールが高価なことでためらいます。 AliExpressを見ると、ESP32系のマイコンとSIMCOM SIM7080GやSIM7670Gなどがセットになった開発モジュールが6000円ほどで売られていたり、モジュール単体なら3000円程度で入手できるようです。 ただ、ISMバンドではない、公衆回線にそうした機器を接続するのは気が引けます。 偽物や検査不合格品でしたとか、使ってみたら緊急通報に影響を及ぼしました、となったらどうしよう。 秋月電子通商やスイッチサイエンスなどで販売されているモジュールを買おうとすると、1万円くらいとなり高価です。 では中古のモバイルルータやノートPC向けのLTE通信モジュールを使って……というのはマイコンから扱うというよりもLinuxから扱うようになり、それに伴って消費電力も増えてしまいます。 長時間の電池駆動は厳しいでしょう。 ということでなかなか難しいとなり、検証は見送りました。

検証

検証として、各方式で到達距離と消費電力量を計測し比較します。

測定の詳細は後述しますが、 到達距離の測定では送信機からデータを流してある地点の受信機に到達したか確認、 消費電力の測定では送信時の電源電圧と消費電流波形をオシロスコープで確認し消費電力量を確認します。

検証機材

まず、今回の検証に利用した機材を記載します。

送信機

まず送信機は下記の一覧表のとおり。 ここで、802.11 LR送信機とBLE Coded PHY送信機は同一のハードウェアです。 LoRa送信機AとLoRa送信機Bは、モジュールや送信時のパラメータは同じですが使用するアンテナが異なります。

802.11 LR送信機LoRa送信機ALoRa送信機BBLE Coded PHY送信機
モジュール名ESP32-C3-WROOM-02E220-900T22S(JP)LoRa送信機Aと同じ802.11 LR送信機と同じ
アンテナ内蔵(パターンアンテナ)外付(ダイポール)外付(FPCアンテナ)802.11 LR送信機と同じ
送信出力20.5dBm (112.2mW)13dBm (20mW)LoRa送信機Aと同じ9dBm (7.94mW)
データレート250kbps or 500kbps1.758kbps(帯域幅125kHz、SF=9)LoRa送信機Aと同じ125kbps
価格500円程度2300円程度(アンテナ込)2000円程度(アンテナ込)802.11 LR送信機と同じ

LoRa送信機A、Bではデータレートの低さが目立ちますね。 これはパラメータで帯域幅を広めてたり、SF(拡散率)を調整すればもう少しデータレートを上げることは可能です。 しかし、今回は到達距離を測定するにあたってデータレートを犠牲に遠くまで届きやすいパラメータとしました。

また、アンテナについては802.11 LR送信機、BLE Coded PHY送信機は基板上のパターンアンテナです。 それに対してLoRa送信機AのアンテナはTX915-JKS-20という(技適登録済みの)ダイポールアンテナが使われておりこちらのほうが有利。 LoRa送信機Bでもダイポールアンテナよりは性能が落ちますが、TX915-FPC-4510という(もちろんこれも技適登録済みの)FPCアンテナを使っています。

受信機

受信機は下記の一覧表のとおり。 ここでも802.11 LR受信機とBLE Coded PHY受信機は同一のハードウェアです。 もっといえば送信機と同じモジュールを使っています。 LoRa受信機は、性能の良いダイポールアンテナを使った1種類のみ用意しました。

802.11 LR受信機LoRa受信機BLE Coded PHY受信機
モジュール名ESP32-C3-WROOM-02E220-900T22S(JP)802.11 LR受信機と同じ
アンテナ内蔵(パターンアンテナ)外付(ダイポール)802.11 LR受信機と同じ
受信感度【IEEE802.11bの場合】-98.0dBm
【推測: 802.11 LRの場合】-102dBm相当
-129dBm(帯域幅125kHz、SF=9の場合)-105dBm
価格500円程度2300円程度(アンテナ込)802.11 LR受信機と同じ

受信感度は基本的に各モジュールのデータシートから確認しました。 802.11 LR受信機の「推測: 802.11 LRの場合」に関しては802.11 LRの主張に基づいて感度を推測しましたが、これはデータシートには記載されていない値です。

受信感度を見ると、LoRa受信機が圧倒的に良いですね。 このモジュールだから?920MHzだから?と思われるかも知れないので、参考として今回使っていないモジュールの情報も載せておきます。

ZigBeeWi-SUNLoRaWAN
モジュール名ESP32-C6-WROOM-1BP35C0-J11A660-900T22
周波数帯域2.4GHz帯920MHz帯920MHz帯
送信出力19.0dBm (79.43mW)13.6dBm (20mW)13dBm (20mW)
データレート250kbps100kbps0.976kbps(最小時)
受信感度-104.0dBm-103.0dBm-140dBm

LoRaがすごいという話でした。 これに加えて920MHz帯での利用では、2.4GHz帯に比べて回折が期待できます。 データレートを無視すればという前提はありますが、すごいですね。

到達距離

これらの機材で到達距離の測定を行います。

方法は、データ送信機を持って屋外を歩き回り、部屋内23に設置した受信機に届くか確認するというものです。 部屋内の受信機は受信データをPCにシリアル通信で送り、そのPCはWebSocket経由でLAN内のWebサーバに受信データを中継します。 このデータが送られる様子は、屋外で歩き回る際に持つスマホから携帯電話回線とVPNを通じて確認可能です。 なんでこんな回りくどいことをしたんでしょうね? 送信機と受信機の役割は逆になりますが、部屋内側が送信機になり、屋外で歩き回る側が受信機としてM5Stackでも使ってリアルタイムにデータが届くか確認すれば良かったんじゃないでしょうか。 だいたいこの辺まで届いたよ的な記録が欲しいなら、スマホで写真取っておけば良い。 しかし、もう計測してしまったのでときすでに遅し🍣です。

なお、到達距離を測定したタイミングの天候はすべて晴れでした。 雨天時だと多少到達距離が短くなるかも知れません。 さらに、各方式の測定は同時に行ったわけではなく、曜日や時間帯のズレはあります。

その計測結果については下記のとおり。

地点 \ 送信元802.11 LR送信機LoRa送信機ALoRa送信機BBLE Coded PHY送信機
部屋内最遠 (~10m)-70dBmほど-40dBmほど-50dBmほど-80dBmほど
屋外A (~50m)受信不可-80dBmほど-110dBmほど受信不可
屋外B (~100m)受信不可-80dBmほど-120dBmほど受信不可

「部屋内最遠」「屋外A」「屋外B」の距離は、それぞれだいたい10m以内、50m以内、100m以内の感じです。 結構近めですが、ここで使っている通信モジュールはほとんどブレッドボードむき出しニックで、「不審物を持ち歩いている全裸の中年男性が居る」と通報されると困るため近場にしか行けませんでした。

ただ、すべての地点で直接見通しがありません。 「部屋内最遠」に関しては、障害物といえばせいぜい軽量鉄骨と石膏ボードで構成された間仕切り壁くらいですが、他の地点では鉄筋コンクリート造の建物に遮られています。

それでもLoRaは届いています。すごいですね。 実はLoRaがすごいというより920MHz帯の回折がすごいんじゃないの?と思っていましたが、特にLoRa送信機Bの場合においてこれだけ低い感度でもデータが受信できるのはLoRaならではだと思います。

消費電力量

次に消費電力量の測定を行います。

測定方法は、送信機からデータを流し続けるようにして、そのときの消費電流を電流センスアンプに通しオシロスコープで観測するというものです。 ただ、この電流センスアンプは10Aなど大電流用で、今回のようなたかだか1Aほどの電流測定ではノイズに埋もれてあまり正確な測定ができません。 それでも各方式の相対的な比較には使えるかも知れない、ということで測定してみました。

送信するデータについて、802.11 LR送信機ではESP-NOWを使って32Bytes24のデータを送信します。 受信機は用意せず、受信機が返すであろうフレームACKの処理にかかる消費電力量への影響は考えません。 LoRaではLoRa送信機Bのみでこちらも32Bytesのデータを送信します。 LoRa送信機Aでは送信していません。LoRa送信機Bとアンテナが異なるだけで消費電力量への影響は軽微と考えられるためです。 BLE Coded PHY送信機ではBLEアドバタイズを送信します。 だいたい32Bytesのデータを送信している想定でしたが……後で示す結果を見ると実はそれより少なく確実な測定ではなかったかも知れません。

さて、消費電力量やピーク電流などのデータは後でまとめますが、先に電流波形を示します。 この波形での電圧1Vは電流1A相当です。 まずは802.11 LR送信機の場合。だいたい400mAくらいです。多い。

802.11 LR送信機の電流波形 802.11 LR送信機の電流波形

次はLoRa送信機B。電流は40mAほどですが消費している時間が長い(時間軸は802.11 LR送信機と比べて10倍)。

LoRa送信機Bの電流波形 LoRa送信機Bの電流波形

最後にBLE Coded PHY送信機。200mAほどですが、消費時間が非常に短い。 アドバタイズの長さが思ったより短い(他方式に比べて有利になってしまった)かも知れない……。

BLE Coded PHYの電流波形 BLE Coded PHYの電流波形

これを集計して表にまとめると下記のとおり。

802.11 LR送信機LoRa送信機BBLE Coded PHY送信機
消費電力量155mJ88mJ4.2mJ
ピーク電流400mA40mA200mA
電流消費時間120ms720ms6.7ms

BLE Coded PHY送信機はおかしそう……なので見なかったことにしてください。 802.11 LR送信機とLoRa送信機Bを比べると、LoRa送信機Bのほうが半分くらい省電力のようです。 良いですね。

ついでにDeepSleepの消費電流も見たいところですが……電流が小さすぎて手持ちの測定機材では無理です25。 802.11 LR送信機とLoRa送信機Bは、今後のプロジェクトで使う可能性があるのでカタログスペックを確認して自分のためにまとめておきます。

802.11 LR送信機LoRa送信機B
モジュール名(再掲)ESP32-C3-WROOM-02E220-900T22S(JP)
DeepSleep時の電流5uA2.5uA

十分低いので良いでしょう。

まとめ

802.11 LRとLoRaとBLE Coded PHYを試し、到達距離と消費電力量を確認しました。

到達距離ではLoRaが有利。消費電力でもLoRaが有利。 ただ、データレートが著しく低く、通信モジュールのコストもかかります。 できればESP32-C3だけでいける802.11LR (ESP-NOW)やBLE Coded PHYを使いたかったですね。

おわり。

Footnotes

  1. ESP8266では利用できない。またESP32系としつつも真名がESP8684のESP32-C2でも利用できない。

  2. LR Transmission Distance / Wi-Fi Driver - ESP32 - — ESP-IDF Programming Guide v5.5.4 documentation

  3. ESP8266やESP8684(ESP32-C2)でも利用できます。

  4. 出典: ESP-NOW ワイヤレス通信プロトコル | Espressif Systems(※「遠距離通信」という表記)

  5. Espressif Systems社のデバイスでなくともvendor-specific action frameをやり取りできれば良いため、LinuxでESP-NOWを動作させるプロジェクト Linux-ESPNOW | Hackaday.io もあるようです。

  6. 最近NIDDというのを知りましたが、似た発想だなと思います(大げさ?)

  7. 自己申告のMACアドレスで送信者を判定するのは認証ではありません。また、上位のアプリケーションで期待されるフォーマットになっていないために結局受理されないことは考えられますが、裏を返せばフォーマットさえ判明すれば偽装でき、隠ぺいによるセキュリティとなってしまいます。

  8. volatile を付けてコンパイラによる最適化を抑止しています。

  9. 2008年度版リストガイド(メッセージ認証コード)

  10. (後述する)LoRaを利用した通信モジュールの技適によれば「電波の型式」は周波数変調を示すFとなっていました。技術的に妥当か判断はできませんが、少なくとも手続き上はそのように扱われるようです。

  11. ドローン関連の文脈でExpressLRS (ELRS)と呼ばれるプロトコルがあるようです。一部は技適も取得されているようですが、通信モジュール単体で技適が取られているのか、ドローンやリモコン全体として技適が取られているのか(通信モジュールだけ抜き出して使うのは違反になるか)はよく分かりませんでした。

  12. もっと言えば「アクティブ系」もLoRaだけではなく、Wi-SUNで使われていたり、FSKか何かの変調方式を使った独自通信モジュールも使っていることでしょう。

  13. 免許局と登録局がありますが、アクティブ系では(少なくともいまのところ)区別がない?登録局しかない?模様。そのため免許局だろうが登録局だろうが「陸上移動局」としてまとめて表記します。

  14. 低出力型もあるようですが今回の検証では触れないため割愛します。中出力型 (FH)、中出力型 (LDC)については後述します。

  15. E220-900T22S(JP)とE220-900T22L(JP)の違いに注意。なお、E220-900T22L(JP)の最大送信電力は160mWとのこと。250mWにしなかったのは消費電力増の懸念?国外の法令との兼ね合い?

  16. E220-900T22S(JP) ユーザーが明示的に送信間隔を制御する必要はありません | DRAGON TORCH SUPPORT

  17. 音声はCodec 2、Lyra、Encodecでなんとかなるかな……ならないかな……と考えています。

  18. さらにその内部ではSemtech社のLLCC68 LoRaチップが使われている模様。

  19. LoRa通信モジュール(E220-900T22S(JP)) | DRAGON TORCH Official Site

  20. DeepSleepとは異なり、若干消費電流が増えている。

  21. それと、おそらくモジュール側が付与するヘッダなども付いていそうですが……。

  22. HaLowの読み方は「ヘイロー」です。❗️。以前間違えて「ハロー」と読んでしまったことがあります。次やったらヘイローを壊されても仕方ない。

  23. 一応窓際に設置します。窓ガラスは何枚か?Low-Eか?サッシとスペーサーの材質は?雨戸やシャッター、サンシェード、転落防止用の金属バーとか付いていない?などによっても変わると思いますが……そこまで厳密な測定はしないので無視します。

  24. ESP-NOWのペイロードとして32Bytesです。実際にIEEE 802.11のvendor-specific action frameとして送信されるバイト数はもう少し増えます。

  25. PPK2が欲し……いや、これ以外に使う機会が思い浮かばないので購入に踏み切れない。