100g以上の無人航空機に義務化されたリモートIDを自作できないかやってみた。
まず、リモートIDの条件として、Bluetooth 5.x LongRange又は、Wi-Fiの無線方式により発信されるものでなければならず、
飛距離は300m以上で1秒間に1回以上、登録番号や位置情報などのデータを発信しなければなりません。
今回はBluthoothで発信するものを作っていきます。
その前にあなたのスマホがBLE LongRange に対応しているか確認しなければなりません。
スマホに nRF Connect for mobile アプリを検索してインストールします。
起動したら左上の'三'マークをタップして[Device information]を選択し、次の項目がSupportedになってればOKです。
- Long range(PHY Coded)
- Extended advertisement
しかし、スマホによっては、ハードは対応していても、ソフトが対応していないものもあるようなので、実際にやってみないと分からないです。
<追記:こちらにLongRange非対応のスマホでも、受信確認できる装置を自作する方法をUPしました。>
用意するもの
① マイコン Seeed XIAO ESP32C3 ( 秋月電子 940円) (スイッチサイエンス 902円)
② GPSモジュール BN-180 (HELIMONStER 2220円)
又は Flywoo GOKU GM10 Nano V3 GPS (HELIMONStER 2890円)
③ USB-C電源 又は 1Sリポバッテリー
④ バッテリコネクタケーブル (USBモバイルバッテリーで運用する場合は必要ありません)
⑤ BLE LongRange を受信できるスマホ(Google Pixel5, OPPO Find X3 Pro では受信確認済み)
⑥ スマホ受信用アプリ( nRF Connect for Mobile 又は LightBlue と OpenDroneID ) をスマホで検索してインストールして下さい。
XIAO ESP32C3 は Wifi,Bluetooth5(BLE) 内蔵の超小型マイコンです(外付けアンテナ付き)技適もバッチリついています。Arduinoでプログラムできます。
背面には1Sリポバッテリーの接続端子がありそこからの起動もできます。なおかつ充電回路も内蔵してるのでUSBからバッテリーを充電できます。
BLEの最大出力は15dBあり、私の測定では940m飛ぶことを確認しています。(2階の窓に置いて、そこからの見通し出来る距離が最高940mでした。それ以上は障害物があり測定できませんでした。もしかしたらもっと飛ぶかもしれません。1.5kmぐらい真っすぐな道路を見つけたので、割りばしの先に発信機を取り付けて道端に刺し測定したら300mぐらいしか飛びませんでいた。地上から約20cmぐらいでは、道路の反射などが影響するのでしょうか?良く分かりませんが距離が出ませんでした。)
配線は以下のようにして下さい。
BN-180の電源は5Vでも3.3Vでもいいのですが、リポバッテリーでも起動できるように3.3Vの方に接続します。
大きさ比較です。
下がFlywoo GOKUの方です。かなり小さいです。重さも2.2gです。BN-180は4.9gでした。
配線は私が検証用に付けたもので、付属はしていません。 箱の底にケーブルと銅板シールが入ってました。ピンは付属していません。
リモートIDの仕様書
Bluetooth(BLE)基礎知識
ASTM国際規格書 <F3411.40165 UAS Remote ID.pdf> ←いつサイトが閉ざされるか分からないのでダウンロードして置くのがお勧めです。
国土交通省 リモート ID 技術規格書
国土交通省のRID規格書をよ~く読んでいくと、RIDの機体登録番号などの設定書き込みは、国土交通省のホームページ経由でなければならとなっています。ガ~(゚д゚lll)~ン
なので~、正規のスマホから書き込むプロセスはすっ飛ばして、固定のRIDの情報を発信するだけのプログラムにします。
RIDの規格書を熟読し、最小限の情報のみを発信するフォーマットにしましたが、どこか解釈があやふやなところもあります。
ま~取り合えず位置情報と登録番号と名前が発信できれば良しとします。
スマホのOpenDroneIDアプリ(Androidのみ)を使えば位置情報がマップ上に表示されます。
これは、無人航空機だけでなく、小さな子供や徘徊老人に持たせれば、どこをうろついているかが分かるんじゃないですか?
イノシシや熊や猿につけれれば居場所もわかる。
ブザーもつければ、近くに来て見つけられない時にスマホから信号を送り、音がでるようにもできるし、いろんな可能性が考えられます。
Arduino IDE の準備
ArduinoでXIAO ESP32C3 が使えるように準備します。下記のサイトを参考にしました。
Seeed Studio XIAO ESP32C3をさっそく試してみた
次にGNSSモジュールBN-180用のライブラリーをインストールします。
Arduino IDEの [ツール] 内の [ライブラリを管理] を選択するとライブラリマネージャ画面が表示されます。
検索に「TinyGPS」と入力すると「TinyGPSPlus」が候補として表示されます。そこの[インストール]ボタンを押します。
これでプログラムの準備ができました。
プログラムの書き込み方法
BOOTボタンを押下しながら、RESETボタンを押すと書き込みモードになるので、
メニューの[スケッチ]→[マイコンボードに書き込む]を押すか、(→)ボタンを押します。
書き込みが終わったら一度電源を入れ直します。するとプログラムが起動します。
プログラムは以下です。これを書き込んでみてください。
電源を入れて、BN-180のLEDが赤く点滅すれば衛星を受信しているので、スマホのOpenDroneIDを起動すれば現在地がマップに表示されるはずです。
BN-180の更新レートを10Hz,ボーレートは115200に設定したので、1秒間に10回情報を発信します。
機体の登録記号や名前などはプログラムの最初の方で定義してるので、好きなように書き換えてください。
(プログラム領域の右上のアイコンをクリックすると全コピーできます。)
プログラム
#include <BLEDevice.h>
#include <BLEAdvertising.h>
#include <TinyGPSPlus.h>
#define SERIAL_NO "ABC0123456789" // 製造番号
#define REG_SYMBOL "JA.JU012345ABCD" // 登録記号
#define AUTHENTICATION {0} // 認証情報
#define LOCAL_NAME "My DRONE" // 名前
#define BAUDRATE 115200 // GNSS ボーレート
#define RATE 100 // GNSS 更新レート 100ms(10Hz)
#define RX_PIN 20
#define TX_PIN 21
// ADタイプ
#define AD_TYPE_FLAG 0x01 // 発信モード
#define AD_TYPE_CMP_LOCAL_NAME 0x09 // 名前
#define AD_TYPE_TX_POWER 0x0A // 送信出力値
#define AD_TYPE_SERVICE_DATA 0x16 // サービスデータ型 [16-bit UUID]
// メッセージ種別
#define MSG_TYPE_BASIC_ID 0x00 // 製造番号、登録記号
#define MSG_TYPE_LOCATION 0x10 // 位置情報
#define MSG_TYPE_AUTH 0x20 // 認証情報
#define MSG_TYPE_SELF_ID 0x30 // 操作目的 [文字列]
#define MSG_TYPE_SYSTEM 0x40 // リモートパイロットの位置情報
#define MSG_TYPE_OPERATOR_ID 0x50 // 操縦者ID [文字列]
#define MSG_TYPE_PACK 0xF0 // パッケージ
// UAS ID Type
#define ID_TYPE_SerialNo 0x10 // 製造番号種別
#define ID_TYPE_ASSIGED_REG 0x20 // 登録番号種別
#define ID_TYPE_UUID 0x30 // UTM Assigned UUID
// 機体種別
#define UA_TYPE_NON 0 // なし
#define UA_TYPE_AEROPLANE 1 // 飛行機
#define UA_TYPE_HELICOPTER 2 // ヘリコプター
#define UA_TYPE_GYROPLANE 3 // ジャイロプレーン
#define UA_TYPE_HYBRID_LIFT 4 // ハイブリッドリフト/垂直に離陸できる固定翼機
#define UA_TYPE_ORINITHOPTER 5 // 羽ばたき機(オルニソプター)
#define UA_TYPE_GLIDER 6 // グライダー(滑空機)
#define UA_TYPE_KITE 7 // カイト(凧)
#define UA_TYPE_FREE_BALLOON 8 // 自由気球
#define UA_TYPE_CAPITIVE_BALLOON 9 // 係留気球
#define UA_TYPE_AIRSHIP 10 // 飛行船
#define UA_TYPE_FREE_FALL_PARACHUTE 11 // パラシュート
#define UA_TYPE_ROCKET 12 // ロケット
#define UA_TYPE_TETHERED_POWERED_AIRCRAFT 13 // テザー式動力航空機
#define UA_TYPE_GROUND_OBSTACLE 14 // 地上障害物
#define UA_TYPE_OTHER 15 // その他
// 位置情報
#define LOC_STA_NON 0x00 // 不明
#define LOC_STA_GROUND 0x10 // 地面
#define LOC_STA_AIRBRONE 0x20 // 飛行中
#define LOC_FLAG_HT 0x04 // Height Type 0:Above Takeoff 1:AGL(Above Ground Level)地面からの高度
#define LOC_FLAG_EW 0x02 // Eeast/West Direction 0:<180 1>=180
#define LOC_FLAG_SM 0x01 // Speed Multiplier 0:x0.25 1:x0.75
// ADV データ
#define N 4 // メッセージ個数
#pragma pack(push,1) // データを1バイト単位に詰めて配置
typedef struct{
uint8_t AD_leg = 5+3+N*25; // 全体の長さ
uint8_t AD_type = AD_TYPE_SERVICE_DATA; // Service Data
uint16_t AD_UUID = 0xFFFA; // ASTM
uint8_t App_code = 0x0D; // Open Drone ID
uint8_t counter; // カウンター:1回送るごとにインクリメントする。
//--- package ---------------------------
uint8_t type_pack = MSG_TYPE_PACK; // パッケージデータ
uint8_t msg_size = 25; // 1ブロック25バイト
uint8_t n = N; // ブロックの個数
//--- Basic ID (25byte)--------------------------
uint8_t type_basic1 = MSG_TYPE_BASIC_ID; // Basic ID
uint8_t ID_UA_type1 = (ID_TYPE_SerialNo | UA_TYPE_HYBRID_LIFT); // UA[b3-b0]に機体種別
char UAS_ID1[20] = SERIAL_NO; // 製造番号
uint8_t resv1[3];
//--- Basic ID (25byte)--------------------------
uint8_t type_basic2 = MSG_TYPE_BASIC_ID; // Basic ID
uint8_t ID_UA_type2 = ID_TYPE_ASSIGED_REG;
char UAS_ID2[20] = REG_SYMBOL; // 登録記号 (例: JA.JU012345ABCDE)
uint8_t resv2[3];
//--- Location (25byte)--------------------------
uint8_t type_loc = MSG_TYPE_LOCATION; // Location
uint8_t status; // 飛行中、方角E/W、速度倍率などの状態
uint8_t dir; // 方角
uint8_t speed; // 速度
uint8_t Ver_speed; // 垂直速度
uint32_t lat; // 緯度
uint32_t lng; // 経度
uint16_t Pressur_Altitude; // 気圧高度
uint16_t Geodetic_Altitude; // GPS高度
uint16_t Height; // 地面からの高さ
uint8_t V_H_Accuracy = 0xBB; // 垂直、水平速度精度(垂直、水平<3m)
uint8_t B_S_Accuracy = 0x04; // 気圧、速度精度(速度<0.3m/s)
uint16_t timestamp = 0; // 現在時刻の分以下の小数点1までの秒数x10
uint8_t T_Accuracy = 1; // 時間精度(0.1s)
uint8_t resv3;
//--- Page0 (25byte)-----------------------------
uint8_t type_auth = MSG_TYPE_AUTH; // 認証情報
uint8_t Auth_Type = 0x30; // Message Set Signatur
uint8_t page_count = 0; // Page0
uint8_t Length = 17; // headからのサイズ
uint32_t timestamp_auth = 0; // 現在時刻(2019.1.1からの秒数)
uint8_t auth_head = 0; // ヘッダ 0:AES-128bit-CCM
char auth_data[12] = AUTHENTICATION; // 認証データ(AES-128bit-CCM ってなんだ? 解からないので無視)
uint8_t resv4[4];
//---- local name -----------------------------
uint8_t l = 29; // Byte数
uint8_t t = AD_TYPE_CMP_LOCAL_NAME; // AD_type = Complete local name
char local_name[28] = LOCAL_NAME; // 名前
} Adv_Data;
#pragma pack(pop)
TinyGPSPlus gps;
BLEMultiAdvertising advert(1); // max number of advertisement data
Adv_Data adv;
time_t tm2019; // 1900.1.1から2019.1.1 までの秒数
void adv_data_init(){
struct tm stm;
stm.tm_year = 2019 - 1900;
stm.tm_mon = 0;
stm.tm_mday = 1;
stm.tm_hour = 0;
stm.tm_min = 0;
stm.tm_sec = 0;
tm2019 = mktime( &stm );
adv.status = LOC_STA_GROUND;
adv.lat =(uint32_t)(35.6807068 * 10000000); // 初期値(皇居広場)
adv.lng =(uint32_t)(139.7572909 * 10000000);
}
void set_speed(float s){
uint8_t v=0;
if(s <= 255*0.25){
v = (uint8_t)(s/0.25);
adv.status &= ~LOC_FLAG_SM;
}
else if(s >225*0.25 && s<254.25){
v = (uint8_t)((s-225*0.25)/0.75);
adv.status |= LOC_FLAG_SM;
}
else{
v = 254;
adv.status |= LOC_FLAG_SM;
}
adv.speed = v;
}
void set_direction(uint16_t way){
uint8_t d;
if(way<180){
d =(uint8_t)way;
adv.status &= ~LOC_FLAG_EW;
}
else{
d = (uint8_t)(way-180);
adv.status |= LOC_FLAG_EW;
}
adv.dir = d;
}
bool gps_read(void){
float lat,lng;
struct tm stm;
bool ok = false;
uint32_t time;
if (gps.location.isValid())
{
lat = gps.location.lat();
lng = gps.location.lng();
adv.lat = (uint32_t)(lat * 10000000);
adv.lng = (uint32_t)(lng * 10000000);
}
if (gps.date.isValid())
{
stm.tm_year = gps.date.year()-1900;
stm.tm_mon = gps.date.month()-1;
stm.tm_mday = gps.date.day();
}
if (gps.time.isValid())
{
stm.tm_hour = gps.time.hour();
stm.tm_min = gps.time.minute();
stm.tm_sec = gps.time.second();
time = mktime( &stm ) - tm2019; // 2019.1.1からの秒数
adv.timestamp = ( gps.time.minute()*60 + gps.time.second() ) * 10 + gps.time.centisecond()/10;
adv.timestamp_auth = time;
ok = true;
}
if(ok){
set_speed(gps.speed.mps()); // 速度
set_direction(gps.course.deg()); // 方向
adv.Geodetic_Altitude = (gps.altitude.meters()+1000)*2; // GPS高度
}
return ok;
}
/*
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED (0 << 0) // Non-Connectable and Non-Scannable Undirected advertising
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_CONNECTABLE (1 << 0) // Connectable advertising
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_SCANNABLE (1 << 1) // Scannable advertising
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_DIRECTED (1 << 2) // Directed advertising
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_HD_DIRECTED (1 << 3) // High Duty Cycle Directed Connectable advertising (<= 3.75 ms Advertis- ing Interval)
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_LEGACY (1 << 4) // Use legacy advertising PDUs
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_ANON_ADV (1 << 5) // Omit advertiser's address from all PDUs ("anonymous advertising")
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_INCLUDE_TX_PWR (1 << 6) // Include TxPower in the extended header of the advertising PDU
#define ESP_BLE_GAP_SET_EXT_ADV_PROP_MASK (0x7F) // Reserved for future use
*/
esp_ble_gap_ext_adv_params_t ext_adv_params_coded = {
// .type = ESP_BLE_GAP_SET_EXT_ADV_PROP_SCANNABLE | ESP_BLE_GAP_SET_EXT_ADV_PROP_INCLUDE_TX_PWR, // こっちが正しい気がするが、これだと受信できませんでした。
.type = ESP_BLE_GAP_SET_EXT_ADV_PROP_NONCONN_NONSCANNABLE_UNDIRECTED | ESP_BLE_GAP_SET_EXT_ADV_PROP_INCLUDE_TX_PWR,
.interval_min = 0x50,
.interval_max = 0x50,
.channel_map = ADV_CHNL_ALL,
.own_addr_type = BLE_ADDR_TYPE_RANDOM,
.filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
.tx_power = ESP_PWR_LVL_P18, // P6,P9,P12,P15,P18
.primary_phy = ESP_BLE_GAP_PHY_CODED,
// .primary_phy = ESP_BLE_GAP_PHY_1M, // 受信機が Google Pixel 5 の場合はこちらでないと受信できませんでした。受信できない場合こちらを試してみてください。
.max_skip = 0,
.secondary_phy = ESP_BLE_GAP_PHY_CODED,
.sid =0,
.scan_req_notif = false,
};
uint8_t addr_coded[6] = {0xc0, 0xde, 0x52, 0x00, 0x00, 0x01};
#define SERVICE_UUID "f9ed6165-faa8-4f2d-8b82-dc67d3444b0f"
#define AUTH_UUID "aacf388f-0e69-4802-8067-3508b1b50c3a"
#define COMMAND_UUID "2d67083e-5291-4dfa-a357-8ae4317413f5"
#define RESPONSE_UUID "d98c42d8-3013-462e-8d35-2b5b61eea94d"
//----- GNSS( GPS ) 設定用 ---------------------------------------------
#pragma pack(push,1)
typedef struct{
uint8_t sync1 = 0xB5;
uint8_t sync2 = 0x62;
uint8_t cl = 0x06;
uint8_t id = 0x00;
uint16_t len = 20;
uint8_t port = 1;
uint8_t rs = 0;
uint16_t txReady = 0x0000;
uint32_t mode = 0x000008D0; // 8bit 1stopbit noPlity
uint32_t baudrate = BAUDRATE; // ボーレート
uint16_t inProtoMaske = 0x0007; // RTCM2,NMEA,UBX
uint16_t outProtoMaske = 0x0003; // NMEA,UBX
uint16_t flags = 0;
uint16_t rs2 = 0;
uint8_t ckA,ckB;
}UBX_CFG_PRT;
typedef struct {
uint8_t sync1 = 0xB5;
uint8_t sync2 = 0x62;
uint8_t cl = 0x06;
uint8_t id = 0x00;
uint16_t len = 1;
uint8_t d = 1;
uint8_t ckA = 0x08;
uint8_t ckB = 0x22;
}UBX_CFG_PRT_POLL;
typedef struct{
uint8_t sync1 = 0xB5;
uint8_t sync2 = 0x62;
uint8_t cl = 0x06;
uint8_t id = 0x08;
uint16_t len = 6;
uint16_t measRate = RATE; // 更新時間(ms)
uint16_t navRate = 1; // 比率
uint16_t timeRef = 1; // GPS time
uint8_t ckA,ckB;
}UBX_CFG_RATE;
#pragma pack(pop)
int set_cs(uint8_t *cmd){ // チェックサムを計算
int i,n;
uint16_t len;
uint8_t ca=0;
uint8_t cb=0;
len = cmd[4]+cmd[5]*16;
for(i=2; i<len+4+2; i++){
ca = (ca + cmd[i]) & 0xFF;
cb = (cb + ca) & 0xFF;
}
cmd[i++]= ca;
cmd[i] = cb;
return len+8; // 全体の長さを返す
}
void send_UBX(uint8_t *buf){
int len;
len = set_cs(buf); // チェックサムを計算
for(int i=0; i<len; i++){
Serial1.write(buf[i]);
}
Serial1.println("");
delay(300);
}
void send_NMEA(String cmd){
int i;
uint8_t c,cs=0;
char cksum[3];
Serial1.print("$");
for(i=0;i< cmd.length(); i++){
c = cmd.charAt(i);
Serial1.write(c);
cs ^= c;
}
sprintf(cksum,"*%02X",cs);
Serial1.println(cksum);
}
UBX_CFG_PRT cfg_prt;
UBX_CFG_PRT_POLL cfg_prt_poll;
UBX_CFG_RATE cfg_rate;
const uint32_t init_speed[5] = { 115200, 57600, 38400, 19200, 9600 };
void gnss_setup(){
// GNSSのボーレートを設定する
for (int i = 0; i < 5; i++){ // 現在の速度が判らないので、可能性のある速度でコマンドを送信する。どれかで通じるはず。
Serial1.begin(init_speed[i], SERIAL_8N1, RX_PIN, TX_PIN);
while(!Serial1){}
send_UBX((uint8_t *)&cfg_prt); // ボーレート設定コマンド
send_UBX((uint8_t *)&cfg_prt_poll); // 確認用コマンド
// send_NMEA("PUBX,41,1,0007,0003,115200,0"); // NMEAコマンドはダメだった
Serial1.end();
delay(300);
}
Serial1.begin(BAUDRATE, SERIAL_8N1, RX_PIN, TX_PIN);
while(!Serial1){}
send_UBX((uint8_t *)&cfg_rate); // 更新レートを10Hz(100ms間隔)に設定
}
//-------------------------------------------------------------------------------------------
void setup() {
gnss_setup(); // GNSSモジュール(BN-180)の設定
BLEDevice::init("");
advert.setAdvertisingParams(0, &ext_adv_params_coded);
advert.setInstanceAddress(0, addr_coded);
advert.setDuration(0);
adv_data_init();
advert.setAdvertisingData(0, sizeof(Adv_Data), (uint8_t *)&adv);
delay(1000);
advert.start();
}
void loop() {
while(Serial1.available()){ // UARTにGNSSのデータが来てたら全て読み込む
if(gps.encode(Serial1.read())){ // GPSデータに変換
if(gps_read()){ // GPSのデータをADV構造体に書き込む
advert.setAdvertisingData(0, sizeof(Adv_Data), (uint8_t *)&adv); // アドバタイズデータ書き換え
adv.counter++;
}
}
}
}
補足
今回の実験でBLEのこととかGNSS(今までGPSと呼んでいたがこれは米国の衛星のことだった)のこととか、色々勉強しましたが、説明すると長~くなるので、プログラムを見て理解してください。
ちょっとだけ説明すると、拡張アドバタイジングで指定するデータは図の赤枠の所だけでいいです。あとはライブラリーで勝手に送信してくれます。
最後にお願い
これを作成又は使用したことにより、何らかの問題が発生したとしても、私は一切責任を負いませんので、あくまで自己責任でお願い致します。