M5StickC Plus2で作る！DJIドローンのリモートID(Wi-Fi Beacon)レコーダー

## はじめに
- M5StickC Plus2を使って、iPhoneでは確認しづらいDJIドローンのWi-Fi BeaconリモートIDを可視化・記録する装置を作った

## 背景
DJI製ドローンを飛ばしていたら、リモートIDが本当に発信されているのか気になった。
手持ちのiPhoneでは、以下の理由でリモートIDを見ることが出来なかった。
- リモートIDは、Bluetooth 5.x、又はWi-Fi Aware、又はWi-Fi Beaconのどれかで発信する必要がある
    [リモート ID 技術規格書](https://www.mlit.go.jp/koku/content/001444589.pdf)
    抜粋
    ```
    2. RID 機器等の性能要件
    (1) リモート ID 信号（以下「RID 信号」という。）は、Bluetooth 5.x Long
    Range（以下「Bluetooth 5.x」という。）、Wi-Fi Neighbor Awareness
    Networking（以下「Wi-Fi Aware」という。）又は Wi-Fi Beacon の無線方
    式による直接放送方式（RID 機器等から発信された RID 信号を受信機能を
    有する端末が直接受信する通信方式）により発信されるものでなければ
    ならない。
    ```
- DJIドローンはWi-Fi BeaconでリモートIDを発信しているとされている
    - [OpenDroneIDのdrones](https://github.com/opendroneid/receiver-android/blob/master/transmitter-devices.md#drones)
    抜粋

    | Device         | BT 4 | BT 5 | Wi-Fi Beacon | Wi-Fi NAN | Link                               | Notes                             |
    |----------------|------|------|--------------|-----------|------------------------------------|-----------------------------------|
    | DJI Mavic 3    | ❌   | ❌   | ✅           | ❌        | https://www.dji.com/cz/mavic-3     | Range < 500 meters via smartphone |
    | DJI Mini 3 Pro | ❌   | ❌   | ✅           | ❌        | https://www.dji.com/cz/mini-3-pro  | Range < 500 meters via smartphone |
- iPhoneアプリはOS制約の関係で、Wi-Fi Beaconを見ることが出来ないらしい
    [OpenDroneID の opendroneid-core-c#ios](https://github.com/opendroneid/opendroneid-core-c#ios)
    抜粋(google翻訳)
    ```
    iOS向けDroneTagアプリケーションは、iOSでBluetooth 4 Legacy Advertising信号を受信できます。
    AppStoreからダウンロードできます。
    ソースコードはhttps://github.com/dronetag/drone-scannerで公開されており、iOSとAndroidの両方でビルドできます。

    iOS 用の別の受信機アプリケーションは、こちらでベータテストされています: https://testflight.apple.com/join/HA3EL4zw。

    Appleは現在、BT4レガシーアドバタイジング以外のドローンID信号送信方法を受信するための適切なAPIを公開していません。
    つまり、iOSの現在のバージョン（15まで）は、BT5 Long Range + Extended Advertising、Wi-Fi NaN、Wi-Fiビーコンの受信をサポートしていません。
    ```
- 従って、iPhoneアプリでは、DJIのリモートIDを見ることが出来ない

そこで、手持ちのM5StickC Plus2を使ってDJIのリモートIDを見ようと思った。
ただし、作った後にOpenDroneIDのESP32実装の存在を知ったので、次回があればそちらを利用したい。
- [OpenDroneIDのESP32実装](https://github.com/opendroneid/opendroneid-core-c#esp32)
    抜粋(google翻訳)
    ```
    ESP32送信機のサンプル コードには、ESP32 HW でドローン ID 信号を受信するためのコードも含まれています。
    ```

## 開発環境

*   Windows 11
    *   バージョン 24H2 (OS ビルド 26100.2605)
*   Arduino IDEバージョン ：2.3.6
    *   日付：2025-04-09T11:26:55.498Z
    *   CLIバージョン：1.2.0
*   Arduinoライブラリ:
    *   `M5GFX` 0.2.9
    *   `M5Unified` 0.2.7
    *   `M5StickCPlus2` 1.0.1
    *   `ArduinoJson` 7.4.1
*   M5StickCPlus2

## 構成

アプリケーションは主に以下のコンポーネントから構成される。

1.  **メインスケッチ (`drone_remote_id.ino`)**:
    *   Wi-Fiスニッフィングの初期化とパケット処理コールバック (`wifi_sniffer_packet_handler`) の設定
    *   ボタン入力処理、Wi-Fiチャンネル制御、定期的な画面更新
    *   `RemoteIDDataManager` と `M5CanvasTextDisplayController` のインスタンス化と制御
    *   RTCからの時刻取得とシステム時刻設定
2.  **RemoteIDDataManagerクラス**:
    *   受信したリモートIDデータをRID（機体識別子、通常はシリアルナンバー）ごとに整理・格納
    *   データは時系列のリングバッファ (`std::deque`) で管理し、古いものから削除
    *   ターゲットRIDのデータはより多く保持
    *   各種クエリ機能（最新データ取得、RSSI順ソート、JSON出力など）を提供
    *   `ArduinoJson` ライブラリを利用してJSON文字列を生成
    *   複数タスクからのアクセスを考慮し、セマフォでデータ整合性を保護
3.  **M5CanvasTextDisplayControllerクラス**:
    *   M5GFXの `M5Canvas` を利用したダブルバッファリングによる、ちらつきのない画面表示を実現
    *   文字単位のグリッドベースでのテキスト配置
    *   Arduino `Print` クラスライクなインターフェースを提供し、`print`/`println` で簡単に描画可能
    *   画面回転や文字サイズ変更にも対応

```plantuml
@startuml
!theme spacelab

title Arduino Remote ID Project - Class Diagram

package M5StackLibs {
    class M5GFX <<Library>> {
        + setRotation(uint8_t rotation)
        + width() : int
        + height() : int
        + fillScreen(uint16_t color)
        + getRotation() : uint8_t
        + {abstract} ... // Other methods
    }

    class M5Canvas <<Library>> {
        + M5Canvas(M5GFX* gfx)
        + createSprite(int16_t width, int16_t height) : bool
        + deleteSprite()
        + pushSprite(int32_t x, int32_t y)
        + pushSprite(M5Canvas* spr, int32_t x, int32_t y)
        + fillSprite(uint32_t color)
        + setTextSize(int size)
        + setTextFont(int font)
        + fontHeight() : int16_t
        + fontWidth() : int16_t
        + setTextDatum(int datum)
        + setTextColor(uint16_t fgcolor, uint16_t bgcolor)
        + setTextColor(uint16_t fgcolor)
        + fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color)
        + drawString(const String& string, int32_t x, int32_t y)
        + drawChar(uint16_t uniCode, int32_t x, int32_t y, uint8_t font)
        + setCursor(int32_t x, int32_t y)
        + {abstract} ... // Other methods
    }
}

package ArduinoLibs {
    class String <<Library>>
    class Print <<Library>>
    note right of Print : Arduino Print class (used by Serial, M5.Log)
}

package StdCppLibs {
    class "std::vector<T>" as StdVector <<Template>>
    class "std::deque<T>" as StdDeque <<Template>>
    class "std::map<K,V>" as StdMap <<Template>>
    class "std::pair<T1,T2>" as StdPair <<Template>>
    class "time_t" as TimeT <<TypeAlias>>
}

package ArduinoJsonLib {
  class "ArduinoJson::DynamicJsonDocument" as JsonDoc <<Library>>
  class "ArduinoJson::JsonObject" as JsonObject <<Library>>
  class "ArduinoJson::JsonArray" as JsonArray <<Library>>
}

package CustomCode {
    class M5CanvasTextDisplayController {
        + M5CanvasTextDisplayController(M5GFX& display)
        + ~M5CanvasTextDisplayController()
        + begin(initialTextSize: int, initialLineWrap: bool, textColor: uint16_t, bgColor: uint16_t, initialRotation: uint8_t) : bool
        + setRotation(rotation: uint8_t, doShow: bool) : bool
        + setTextSize(size: int, doShow: bool)
        + setText(row: int, col: int, const String& text)
        + setText(row: int, col: int, const char* text)
        + setCursor(row: int, col: int)
        + print(const String& text) : size_t
        + print(const char* text) : size_t
        + print(char c) : size_t
        + print(int val, int base) : size_t
        + print(double val, int decimalPlaces) : size_t
        + println(const String& text) : size_t
        + println(...) ' Other overloads for println
        + show()
        + setLineWrap(wrap: bool)
        + clearDrawingCanvas()
        + fillScreen(color: uint16_t)
        + fillScreen()
        + setTextColor(color: uint16_t)
        + setBgColor(color: uint16_t)
        + getRows() : int
        + getCols() : int
        + getPrintCursorRow() : int
        + getPrintCursorCol() : int
        + getTextSize() : int
        + getRotation() : uint8_t
        -- Private Members --
        - _lcd: M5GFX&
        - _canvas1: M5Canvas*
        - _canvas2: M5Canvas*
        - _activeCanvas: M5Canvas*
        - _drawingCanvas: M5Canvas*
        - _rows: int
        - _cols: int
        - _textSize: int
        - _fontHeight: int
        - _fontWidth: int
        - _lineWrap: bool
        - _textColor: uint16_t
        - _bgColor: uint16_t
        - _printCursorRow: int
        - _printCursorCol: int
        -- Private Methods --
        - _deleteCanvases()
        - _recreateCanvases() : bool
        - _clearAndResetDrawingCanvas()
        - _printChar(c: char) : bool
    }

    struct RemoteIDEntry {
        + rssi: int
        + timestamp: time_t
        + beaconTimestamp: uint64_t
        + channel: int
        + registrationNo: String
        + latitude: float
        + longitude: float
        + pressureAltitude: float
        + gpsAltitude: float
        + RemoteIDEntry()
        + RemoteIDEntry(r: int, ts: time_t, bTs: uint64_t, ch: int, regNo: const String&, lat: float, lon: float, pa: float, ga: float)
    }

    ' Define RIDDataContainer outside and before RemoteIDDataManager
    struct RIDDataContainer {
        + entries: std::deque<RemoteIDEntry>
        + max_size: size_t
        + latest_rssi: int
        + latest_timestamp: time_t
        + RIDDataContainer(maxSize: size_t)
        + addEntry(const RemoteIDEntry& entry)
    }

    class RemoteIDDataManager {
        + RemoteIDDataManager(const String& targetRid)
        + addData(rid: const String&, rssi: int, timestamp: time_t, beaconTimestamp: uint64_t, channel: int, registrationNo: const String&, lat: float, lon: float, pAlt: float, gAlt: float)
        + getRIDsWithDataInLastMinute(currentTime: time_t) : std::vector<String>
        + getAllDataForRID(rid: const String&, max_entries: size_t) : std::vector<RemoteIDEntry>
        + getRIDCount() : int
        + getDataByIndex(index: int) : std::vector<RemoteIDEntry>
        + getRIDStringByIndex(index: int) : String
        + hasRID(rid: const String&) : bool
        + getLatestEntryForRID(rid: const String&, entry: RemoteIDEntry&) : bool
        + clearAllData()
        + clearDataForRID(rid: const String&)
        + getSortedRIDsByRSSI() : std::vector<std::pair<int, String>>
        + getJsonForTopRSSI(count: int, max_log_entries: size_t) : String
        + getJsonForRegistrationNo(regNo: const String&, max_log_entries: size_t) : String
        + getLatestChannelForTopRSSI() : int
        + getLatestChannelForRegistrationNo(const String& regNo) : int
        -- Private Members --
        - _target_rid_value: String
        - {static} TARGET_RID_MAX_DATA: const size_t = 1200
        - {static} OTHER_RID_MAX_DATA: const size_t = 600
        - {static} ONE_MINUTE_IN_SECONDS: const time_t = 60
        - _data_store: std::map<String, RIDDataContainer> ' This now refers to the external struct
        ' No more "-- Private Nested Struct --" section here
        -- Private Methods --
        - isTargetRID(const String& rid) : bool
        - _populateJsonEntry(jsonObj: JsonObject, entry: const RemoteIDEntry&)
    }
}

package SketchFile {
    class "drone_remote_id.ino" as InoSketch <<Sketch>> {
        Global Variables:
        ~ dataManager: RemoteIDDataManager
        ~ displayController_ptr: M5CanvasTextDisplayController*
        ~ dataManagerSemaphore: SemaphoreHandle_t ' FreeRTOS type
        ~ channel: int
        ~ {static} wifi_country: wifi_country_t ' esp_wifi_types.h
        ~ ...
        Functions:
        + setup()
        + loop()
        + wifi_sniffer_packet_handler(buf: void*, type: wifi_promiscuous_pkt_type_t)
        + wifi_sniffer_init()
        -- Helper Structs (used in wifi_sniffer_packet_handler) --
        # struct Message_head
        # struct Seq_ctl
        # struct wifi_mac_hdr_t
        # struct element_head_t
        # struct Status_flag
        # struct H_V_accuracy
        # struct B_S_accuracy
        # struct RID_Data
    }
    note right of InoSketch
        This represents the main Arduino sketch (.ino) file.
        It initializes and uses M5CanvasTextDisplayController
        and RemoteIDDataManager.
        It also contains Wi-Fi sniffing logic and helper structs
        (marked with # as internal to the sketch's context)
        for parsing Wi-Fi packet data.
        'M5.Log' (and thus Serial) is used for logging.
    end note
}

' --- Relationships ---
M5CanvasTextDisplayController o-- "1" M5GFX : uses (ref)
M5CanvasTextDisplayController *-- "2" M5Canvas : creates/owns
M5CanvasTextDisplayController ..> String : uses
M5CanvasTextDisplayController ..> Print : uses (via Serial.println for debug)

' RemoteIDDataManager uses RIDDataContainer in its _data_store map.
' Since _data_store is a member, PlantUML should infer this composition.
' Explicitly: RemoteIDDataManager *-- "RIDDataContainer" : _data_store (map value)
' However, the map itself implies a "uses" or "contains" relationship.
' Let's use a more direct composition for the map's value type if needed,
' but often the member declaration is sufficient.
' The previous "RemoteIDDataManager *-- " " RemoteIDDataManager.RIDDataContainer" was for nested.
' Now it should be:
RemoteIDDataManager *-- " " RIDDataContainer : _data_store (value in map)

RemoteIDDataManager ..> RemoteIDEntry : uses (param, return)
RemoteIDDataManager ..> String : uses (key in map, params, returns)
RemoteIDDataManager ..> StdVector : uses (return type)
RemoteIDDataManager ..> StdPair : uses (return type)
RemoteIDDataManager ..> TimeT : uses
RemoteIDDataManager ..> JsonDoc : uses
RemoteIDDataManager ..> JsonObject : uses
RemoteIDDataManager ..> JsonArray : uses

RIDDataContainer *-- "*" RemoteIDEntry : contains in deque
RIDDataContainer ..> TimeT : uses

RemoteIDEntry ..> String : uses
RemoteIDEntry ..> TimeT : uses


InoSketch ..> M5CanvasTextDisplayController : uses instance (via ptr)
InoSketch ..> RemoteIDDataManager : uses instance
InoSketch ..> Print : uses (via M5.Log)
InoSketch ..> RemoteIDEntry : wifi_sniffer_packet_handler creates
InoSketch ..> M5GFX : uses (via M5.Display, M5.begin)
InoSketch ..> M5Canvas : uses (indirectly via M5.Display or custom controller)


' Grouping for clarity (optional visual aid)
CustomCode -[hidden]down- SketchFile
@enduml
```
[PlantUMLクラス図](https://cdn-0.plantuml.com/plantuml/png/bHbjSzks-Rj-1QRTrL8ckqsszNQwB5V7iXDljfEprAHtknqF8X-9Q4201K3BMY_zxSC3K30eIAph3nQ8v_qT17_GXYhJL7pm9rD21KJND0TEbuE1OOO3ELT5muGazr190-HwHZug-JFaXfoI2qwr9ZD6rufMWu6b_4BNG6x_FZTsUSEMclmw8FOlHqIx_-xg9_BwjGKeghPlthHG_FiBqM3kfQ66IJ6o4iq_CaDKkp6Ey3QiCELeJAR48aNx9R1rQGu0LeppUQu0FD-Ntrl6kUHInLpNaNHauJM8sVnAbzeecfklvEpiZ7pxBVbWIb2a0bFAGZlChuEEkHTKF51zpEA0C7AEUK7MgyTOfLm1DJ2l5JCmQXNtra_8xj4RZGelfUGHSG4S0d7CjMvq-SJphwyiaqVavvVR8wWxLLyGNIj4xwNqBlUKJOkGkbo3MS2ZcRF_ELM8jei3y2ifZ8Elx2A6u_FxJj3H9Ni8dz5VCJmLCAEcgPo40bS7LBX0rP-IPxLsjan8s5dwdMUJxllg7d9pC1nFoytJqiR_k5SBHJTpevXOZt8fj27-uHkYtMzlx0A3Yv96IZU2NSW23fDEsb99uwF1N3HAIzMVQCykhQ-x3XEQKj9XlArnmKN0ZnPc4fZ0hgOmYuXSjJZJdG3_tDAF6Wq5MMx971IZV6Bhz-n6hiUnOdDJNDHrejTGcs8wVO3SIFLwyMP8g4RKJsx3ghI0gkRKWDUfGrJ0Bmr4D3DyxYUfQFtwtvDFWUIMrlq4DMLMhPUJnQj0yv6ouuePLa5c7Eh2BXU8kAtXd3EgBMOQg7zfAQnJh4y2ZmWodSwsWbOinuUPp9iAXEVURiH1Ey81_tzOubpoXE7ncRJdIj6j8mrFCMLiqqMZZQom8j2UfDsxbiAqTTlsGWgZ9EUWuhxVZ-c7mJUay82uJ7xh9unHbx1cOiG4CupoNPTrFN12sjqR9k2pehKV7HDYTjrg6lhQn3QsPAibtqtBC2hJ8TGPwMeVVq8AEI_bnbDrgDFve8FwNQeKVQGGY9ZO6yCgxeHewH-Zp4kgNWIwjC6bb35UZKrabEg0DgDXcKcHOx7zg8X4yXu4x60FsAzmiQGQUd0BsImvFA4NaBEAye-Svg2Fqt7HRrwARllyc9oqVLu-WEAI5fgifDgXn8ws0H_jUNwNlgDDoE4O8-T0rKpHZLMdFMMD3vy9ysw27y4wDjfxo17lxReNAvmyDpezk2B4KX-1u6Hgqy-I_owEPNGG9vHOW3x_J7nwYlFnWHeWjr0jGMcxzGGb6S-BAV7zBDxENJnUJaauLQRGLqUWD3Vi0JpW24hXezwBewp7k-QYPCdJJTFw9m4y7JyJK3Xu9X3UQRen96s_7SBbkWTOTt8X4LftqY20qnZYQMi_Xlv5mhiGuY92BqAkWCPGaXhcAl5S5FUWmUnNPA8b7ZppgUjy4RFmPgMDQdBJlenUpow5KTlER5LQi_qynkEADhIgfxZizgCbq5oAHS1m_lt-Rp4EAYC4y7ss2jPC6-NguqvEszuN8N0BCaq1KxBYafeO8iNw2AXMe7MZu9mV8rxN-ZYmuvdH-2X4jVFAwDOfEEmNEjYFewpqE6WesjVfy1EqhXK_GNl2kgPXkTujnt4CJyWCLam0kR-UpQYXU8IXzbaHsHZD2Y1K56G9AwaWQ8s8jrJGDQWe4L8MSIw0C8g1jIawI7Uyy2P2hkXZXjCgdLbeAMYJfQaLG3qPbgWuifBcaQ0uHhGeV7YyhplAVaC0VyRfHKFgeuuV3i7tnpLLsEnPiQSCKemKAvBW1rzCqkgQ72-gIQUM9aa9FJV5plaJO1qUah5wFTEVcIdHYcjnGxMvPQ8nCCeRfK0OL72dD3QR-2NDw_2co_2SS-Hr9PLbVT0lc4KXxJ2-AUTk1YOQNyX6cCF366M_tLwB0Xv73FyZpX_bxrKzo2RjO2NLXorD6hrZV-EgmGdkyH4WVDfDxaCC_U3mBXybPxlUGEmbmbmg0mMcmzlj_Nn-FThtsTDRi4lBDlXxpiCNGojn8Ml78yT0ETVvi7EvJa8V-pJbTD_9_j7XDliCrca8BdoLHTec2PKY9mfrDF5J8B1vxX7GTvPCiI9xeBo1HEjJygl6CsR-bIpExzzTBZ8Rf-ps_ATiThuutxd36qt-ILw--kwxGxGV5kylx_j9lpz2UNUPtLxV_RYupAxlildbnOUxsRmbzit14tSEHTJGJ1kfO4h2bOitR98q_3VaX2nAfecG6w9WXKuoafWI23mQK89oFyw2Mot1dIGLphzXvE4xq0OACdUuriz3eY77M94I53pxICVqWYhV3FP3p8f39xXQrWqEEanRFvv-zjShq-YU9TJreT4rJg_mvb_0vEKLuz2zAIkK590fHvwnuem9wQvbheNq9FPQnYywj-dlk5nIJZvHnUYIWvu6o6-aU9fxqqF3C4RTlrN9QgEc_JSsBmwBceDDYjAbI5Y-fwBWc50dv4e1t2y-p8dPrX1nI0wRVZkawuQjM8Qz1vtTUN9iGTUPsqIs-gnyOe5Ng-7fgX4kS_Iq-xxQr9rEowNiR7XvMh3L2bI6eGIJbSueDLesgobvaAnuCN56jThLIbPCvuriT5P_CKwnp1nbYjTBet6SpE-1rw3QjDzTmpBHfyiuplW_xmwBjw0rNKDM0YrIw1n-oNB34u2NKz4yAmkLcHGE7F32qh4z19yRQgpjAqxNAV1zzYcZUTuecczJwDjitWF5ReTft1PNUfiTQYRGkZwae5QWGHZjcb15cGWNtjhNs0ZhRemN8F2KDDTcT_bdMwfsf_H6W-wlZa2Dw0UAhyEUSYr9xXkd9f_PwHKZBi9ugy_bckMEJUapmhj11mQZYgelKDY0cT9wYMh2XE-lktxhhJlHAADpc-SldgZIACZBzHdbAlf9nnD_vtz2HgY7AHlTVWqO4wRH6mLoGbNNbfEd0r6uk63tEx69UMhTmAchlfBL6dS6lIuaqj8CNmxR3wXJx_MHWjNuTmXV8E6hOOYGfVLlxFfRkH6wdzesZ3VjQF0oduFlhfo2YW-CjluvQs_rd7iAM3RhCNhZG48uqcI8Oi4pe-FHIofQdraUSoPow02O9fHKxZmo8Hyv5UR7snkyHMmuTeuLA6Am479PrL8p30KokdoiESkPuTk3Oy9xSr-n8PbsP8-iLiGTTSQMvNkvWGVKmXTQZJO0Nn5MML60QWxHt26HYWntYJyaAaeGrEm6p8bBBxGBfHHCGMvY0z2nEoadsclWsZ1XAo802YWcbjEomSvWG7XSz1768-VK7_lGTxgn1PSp40PbBonUhU21oKQJuL6_aE71waxTjQ7QwIhSQGObtCaDOMONdoLC1xrIzZZk-zzRpWGwOZn8MVb4xHvGGiBMLD4AtqjDeyGu9Kwh0gc-mBOLE26EWzwnqCTvXEz_WOqdSK7h9Vj8sHyayb_fFCanfF2nhHyjEl8z1zD_IMiH1qdiN51V39DeX1d0174NDmbfQjUWooI9KmhVen-4UOd0th4M_C-4DbJau7jTRTHuZqlgd8HsZo9jeNxe99oxFkix3-swVfT3Pvu4IQsf4roxpuZZ8tu9X4puTiItfCi3krhklfQIF7XkFC36_qx9fiROO2l0xiFCbenaZJs8Sij7D_Q7uerKzBtrz3ybAmeG_otaHfn6xn237q0KJSN_3m00)

## 使い方
1. Gitの[drone_remote_id](https://github.com/igaponr/NTigapon/tree/545323e5c718ff864c03a6dc90ea588fd2e16d8d/arduino/sketch/M5StickCPlus2_drone_remote_id/drone_remote_id.ino/drone_remote_id)にある以下の4つのファイルをダウンロードする
    - [drone_remote_id.ino](https://github.com/igaponr/NTigapon/blob/545323e5c718ff864c03a6dc90ea588fd2e16d8d/arduino/sketch/M5StickCPlus2_drone_remote_id/drone_remote_id.ino/drone_remote_id/drone_remote_id.ino)
    - [RemoteIDDataManager.h](https://github.com/igaponr/NTigapon/blob/545323e5c718ff864c03a6dc90ea588fd2e16d8d/arduino/sketch/M5StickCPlus2_drone_remote_id/drone_remote_id.ino/drone_remote_id/RemoteIDDataManager.h)
    - [RemoteIDDataManager.cpp](https://github.com/igaponr/NTigapon/blob/545323e5c718ff864c03a6dc90ea588fd2e16d8d/arduino/sketch/M5StickCPlus2_drone_remote_id/drone_remote_id.ino/drone_remote_id/RemoteIDDataManager.cpp)
    - [M5CanvasTextDisplayController.h](https://github.com/igaponr/NTigapon/blob/545323e5c718ff864c03a6dc90ea588fd2e16d8d/arduino/sketch/M5StickCPlus2_drone_remote_id/drone_remote_id.ino/drone_remote_id/M5CanvasTextDisplayController.h)
1. Arduino IDEで ダウンロードしたdrone_remote_id.ino を開き、必要なライブラリがインストールされていることを確認する
1. 次章「設定」の値を、必要に応じて変更する
1. M5StickCPlus2をPCに接続し、適切なボードとシリアルポートを選択する
1. スケッチをM5StickCPlus2に書き込む
1. 起動すると、画面にヘッダ情報（チャンネル、検出RID数、空きヒープ）と、検出されたRIDの情報がRSSIの高い順に表示される
**※ドローンは電源ONしただけでは、リモートIDを発信しないので、アーム状態にする必要がある**
※ドローンの「アーム状態」とは、ドローンのモーターが回転して、飛行可能(プロペラが回る準備完了)状態になることである
**※DJIドローンの場合、コンビネーション スティック コマンド（CSC）によって、モーターを始動すると、アーム状態になる様だ**
![view01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/ccb02141-40b2-44fb-a32c-f36645e0a9c3.jpeg)↑ RIDが検出されていない時、(S)サーチモード
![view04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/8109534e-d335-41e1-a4a6-e5da5d502c7a.jpeg)↑ RIDが一つ検出された時、(S)サーチモード
![view05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/6911bc3d-3cf5-46ab-9ca9-5bb6845bc41b.jpeg)↑ RIDが一つ検出された時、(L)ロックモード
![view06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/fe9afb96-315a-458f-b07c-3710921f22f8.jpeg)↑ GNSS(=GPS)が有効な時
※リモートIDを発信できるドローンを一台しか持っていないため、二台以上の動作が確認できていない
   - 各RIDエントリには、RID文字列(シリアル番号)、RSSI、チャンネル、登録記号、緯度経度、気圧高度、GPS高度、最終受信時刻などが表示される
   ※DJIのドローンが衛星を10個ぐらい認識してGNSSのアイコンがグリーンにならないと、緯度と経度は0が返る様だ
   ※気圧高度とGPS高度は受信した値をただ表示しているだけなので、正確な高度を出すには何かしらの補正が必要と思う
   - チャンネルは自動で1chから13chまでスキャンする。表示は Ch:XX(S) のように(S)canモードを示す
1. ボタン操作:
   - ボタンA (M5.BtnA / 本体正面ボタン): JSON送信
      - 押すと、シリアルモニター経由でJSONデータをPCに送信する
      ![view07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201344/cee4773a-f58e-4acf-9d46-d257f0e651df.png)
      - 送信モードはスケッチ内の #define SEND_MODE_TOP_RSSI で切り替えられる
         - 1 (デフォルト): RSSIが最も高い1つのRIDの履歴データを送信
         - 0: スケッチ内の TARGET_REG_NO_FOR_JSON で指定された登録記号を持つRIDの履歴データを送信
   - ボタンB (M5.BtnB / 本体側面ボタン): チャンネル固定モード切り替え
      - 押すごとに、チャンネル固定モードの有効/無効がトグルする
      - 有効にすると、SEND_MODE_TOP_RSSI の設定に応じて、ターゲットとなるRIDが受信された最後のチャンネルに固定しようと試みる。表示は Ch:XX(L) のように(L)ockモードを示します。ターゲットが見つからない場合は Ch:Lock? と表示され、スキャンを継続する
   - ボタンC (M5.BtnPWR / 電源ボタン): リセット
      - 押すとM5StickCPlus2が再起動する
1. シリアルモニターを開く (ボーレート 115200) と、デバッグログやJSONデータを確認できる

## 設定
初期状態では少しのデータしか記録しない。必要に応じて調整すること。
ただし、ヒープ領域不足でPanicになるかもしれない。
1. 対象のリモートIDを指定する
    探すRIDのシリアル番号がわかっているなら以下のコード(drone_remote_id.ino の40行目あたり)で指定する
    ```
    RemoteIDDataManager dataManager(""); 
    ```
    以下の様に指定する
    ```
    RemoteIDDataManager dataManager("1581F00LB000C000000R"); 
    ```
1. RIDのデータエントリ数を調整する
    指定されたRIDと、それ以外のRIDで、保持するデータエントリ数を以下のコード(RemoteIDDataManager.hの180行目あたり)で定義しているので調整する
    ```
    static const size_t TARGET_RID_MAX_DATA = 1200;
    static const size_t OTHER_RID_MAX_DATA = 10;
    ```
    ※ビーコンは1秒に一回以上発信される
1. JSONデータの最大エントリ数を調整する
    Aボタンクリック時にJSONデータを作る最大エントリー数を、以下のコード(drone_remote_id.ino の39行目あたり)で定義しているので調整する
    ```
    const size_t MAX_ENTRIES_IN_JSON = 600;
    ```
    以下の様に変更する(私の環境では40で安定した)
    ```
    const size_t MAX_ENTRIES_IN_JSON = 40;
    ```
    **※私の環境では、50だとAボタン押すタイミングでメモリを使い果たしてPanicになったw**
    ※40だとPanicにならなくなったが、40秒未満のデータしかJSONで送れない微妙。JSONデータを見直すか、バイナリデータへの変更を検討したい

## おわりに
iPhoneアプリでWi-Fi Beaconを見ることが出来ない点には驚いた。
リモートIDは三種類(Bluetooth 5.x、Wi-Fi Aware、Wi-Fi Beacon)のいずれかで発信されるので、汎用的に使えるように作るのは大変そうだ。
作った後にOpenDroneIDのESP32実装の存在を知ったので、次回があればそちらを利用したい。
今後は、PCで受け取ったjsonデータからKMLファイルを作って、Google Earthなどで表示させてみたい。
でもデータ数を増やすとメモリが足りなくなるので、メモリの無駄を見直したい。

## 参考サイト
- [ESP32 Wi-Fi Sniffer](https://lang-ship.com/blog/work/esp32-wifi-sniffer/)
- [M5StackでWi-Fiパケットキャプチャ](https://qiita.com/kobatan/items/dac5d4696d631003e037)
- [国土交通省 無人航空機の登録制度](https://www.mlit.go.jp/koku/koku_ua_registration.html)
   - [5.2.周辺機体の発信情報を確認する](https://www.dips-reg.mlit.go.jp/app/page/manual_5_2.html)
- [Open Drone ID Core C Library](https://github.com/opendroneid/opendroneid-core-c)