7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

リモートID受信機を自作する (パート2)

Last updated at Posted at 2023-02-20

リモートID(BLE形式)を付けたけれども、本当に電波が発信されてるのか確認したくありませんか?
 確認するには、BLE5.x LongRange対応のスマホがあれば、[Open Drone ID]と言う、無料アプリを検索してインストールすることで確認できます(Androidのみ)。しかし、今市場に出回っているスマホはBLE LongRangeに対応している機種が限られています。
そこで、LongRange 非対応のスマホでも確認できる装置を作りたいと思います。

用意するもの

① マイコン Seeed Studio XIAO ESP32C3 (スイッチサイエンス 902円)
② USB-C モバイルバッテリー (又は 1S リポバッテリー)
③ 普通のスマホ

 前回Arduino IDEでプログラムしましたが、今回は [Visual Studio Code] + [PlatformIO] の開発環境でプログラムします。
何故かと言うと、ESP32C3のSPIFFS領域にファイルを書き込む方法がArduino IDEでは見つけられなかったからです。(ESP32 や ESP8266用のアップローダはあった。)
前回作った受信情報に、マップを追加して、現在位置をマークできるようにして見たかったからです。

[VS Code]+[PlatformIO]のインストールはこちらを参考にしました。
新規プロジェクトでは下図のように設定して下さい。

platformio.iniの中身は以下のようにして下さい。
追加ライブラリーやシリアルモニタの速度、パーテッションの指定をしています。

platformio.ini
[env:seeed_xiao_esp32c3]
platform = espressif32
board = seeed_xiao_esp32c3
framework = arduino
lib_deps = 
	links2004/WebSockets@^2.3.7
	ottowinter/ESPAsyncWebServer-esphome@^3.0.0
monitor_speed = 115200
board_build.partitions = huge_app.csv

dataフォルダを新たに作成します。
[RID_Receiver]のところを右クリックして、[新しいフォルダ]を選択し、フォルダ名を[data]にします。

下の2つの画像を右クリックして「名前を付けて画像を保存」してください。
名前は[map.png]と[mark.png]にします。
map.png
mark.png
保存した[map.png]と[mark.png]を、先ほど作った[data]フォルダーにドラッグ&ドロップしてコピーします。

次に[data]フォルダ内に新たに[index.html]ファイルを作ります。
[data]フォルダ名の上でマウスの右クリックをして、「新しいファイル」を選択し、ファイル名を[index.html]にします。中身は以下です。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="content-type" charset="UTF-8">
<title>RID Receiver</title>
<style>
	body	{background-color:rgba(128,128,128,0.322);}
	h1		{font-size: 2em;color:rgb(156,5,5); background-color: whitesmoke;}
	h2		{font-size: 1.5em;color:black;}
	span	{color:blue;}
</style>
</head>
<!-----------------------------------------HTML----------------------------------------->
<body>
	<h1>RID受信情報</h1>
	<h2>
		名前: <span style="color:rgb(216, 3, 3)" id="name"> </span><br>
		RSSI: <span id="rssi">0</span><br>
		Primary PHY: <span id="pPhy"></span><br> 
		Secondary PHY: <span id="sPhy"></span><br>
		Tx Power: <span id="pw"></span><span>dBm</span><br>
		Counter: <span id="cnt"></span><br>
		---------------------------------<br>
		製造番号: <span id="SN"></span><br>
		登録記号: <span id="RN"></span><br>
		機体種別: <span id="UA"></span><br>
		緯度: <span id="lat"></span><br>
		経度: <span id="lng"></span><br>
		速度: <span id="speed"></span><br>
		高度: <span id="alti"></span><br>
		方角: <span id="dir"></span><br>		
		時刻:<span id="time"></span><br>
	</h2>
	<div style="position:relative;">
 		<img src="map" width="800" height="800" alt="map.png">
		<div style="position:absolute; left: 0px; top:0px;">
			<canvas id="cvs" width="800" height="800"></canvas>
		</div>
		<div id="mark" style="position:absolute;">
		 <img src="mark" alt="mark.png">
		</div>
	</div>
</body>
<!-------------------------------------JavaScript--------------------------------------->
<script>
	let x0=0,y0;
	const mk = document.getElementById('mark');		
	const ctx = document.getElementById('cvs').getContext('2d');

	InitWebSocket();

	function drawLine(x,y){					// 移動経過のラインを描画
		if(x0 != 0){
			ctx.beginPath();
     	    ctx.moveTo(x0, y0);
     	    ctx.lineTo(x, y);									
			ctx.strokeStyle = '#FF0000';	// ラインの色
  		    ctx.lineWidth = 1;  			// ラインの幅
			ctx.stroke();
		}
		x0 = x;
		y0 = y;
	}

	function InitWebSocket(){												// Websocketの初期設定
		websock = new WebSocket('ws://'+window.location.hostname+':81/');	// Websocket開始

		websock.onmessage = function(evt){																// データが送られて来た時の処理
			JSONobj = JSON.parse(evt.data);																	// Json文字列を解析
			document.getElementById('name').innerHTML = JSONobj.name;
			document.getElementById('rssi').innerHTML = JSONobj.rssi;
			document.getElementById('cnt').innerHTML = JSONobj.cnt;
			document.getElementById('SN').innerHTML = JSONobj.SN;
			document.getElementById('RN').innerHTML = JSONobj.RN;
			document.getElementById('UA').innerHTML = JSONobj.UA;		
			document.getElementById('lat').innerHTML = JSONobj.lat;
			document.getElementById('lng').innerHTML = JSONobj.lng;
			document.getElementById('speed').innerHTML = JSONobj.speed;
			document.getElementById('alti').innerHTML = JSONobj.alti;
			document.getElementById('dir').innerHTML = JSONobj.dir;			
			document.getElementById('time').innerHTML = JSONobj.time;                
			document.getElementById('pPhy').innerHTML = JSONobj.pPhy;
			document.getElementById('sPhy').innerHTML = JSONobj.sPhy;
			document.getElementById('pw').innerHTML = JSONobj.pw;

			x = Math.trunc((parseFloat(JSONobj.lng)-122.65) * 28.97);			// 経度をx座標に変換
			y = 800 - Math.trunc((parseFloat(JSONobj.lat)-23.45) * 35.8726);	// 緯度をy座標に変換
			mk.style.left = x + 'px';											// マークの位置を変更	
			mk.style.top = y + 'px';
			drawLine(x+13, y+42);                                               // 軌跡ラインを描画
		}
	}
</script>
</html>

この[data]フォルダーの中身をESP32C3のSPIFFS領域にアップロードします。

PCにESP32C3を接続して、[BOOT]ボタンを押しながら、[RESET]ボタンをチョンと押し、[BOOT]ボタンを離します。すると書き込みモードになるので、あとは下図の順番でクリックするだけです。
②を押した後ターミナルに"=====[SUCSESS]====="と表示されれば転送完了です。
③で表示をエクスプローラーに戻します。

そうしましたら後はプログラムを書くだけです。
[src]の[main.cpp]の中身に下記のコードをコピペして下さい。
(プログラム領域の右上のアイコンをクリックすると全コピーできます。)

プログラム

main.cpp
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <WebSocketsServer.h>
#include <SPIFFS.h>
#include <FS.h>

const char *ssid="RID_Receiver";
const char *pass="12345678";		   // パスワード(8Byte以上)
const IPAddress ip(192,168,5,1);	   // IPアドレス
const IPAddress subnet(255,255,255,0); // サブネットマスク

// 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

#pragma pack(push,1)	// データを1バイト単位に詰めて配置
typedef struct{
	uint8_t type;				// Location(MSG_TYPE_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;		// 垂直、水平速度精度
	uint8_t B_S_Accuracy;		// 気圧、速度精度
	uint16_t timestamp;			// 現在時間の分以下小数点第1位までの秒数X10
	uint8_t T_Accuracy;			// 時間精度
	uint8_t resv3;
} location;

typedef struct{
	uint8_t type;				// 認証情報
	uint8_t Auth_Type;			// Authentication Message [認証情報]
	uint8_t page_count;			// Page0
	uint8_t Length;        		// headからのサイズ
	time_t timestamp_auth;		// 現在時刻(2019.1.1からの秒数)
	uint8_t auth_head;      	// ヘッダは0
	char auth_data[16];			// 認証データ
}AUTH;
#pragma pack(pop)

AsyncWebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);

uint32_t scanTime = 100; //In 10ms (1000ms)
BLEScan* pBLEScan;

String phy[3] ={"1M PHY","2M PHY","Coded PHY"};
String ua[16] ={"なし","飛行機","ヘリコプター","ジャイロプレーン","ハイブリッドリフト","羽ばたき機","グライダー","カイト(凧)","自由気球","係留気球","飛行船","パラシュート","ロケット"," テザー式動力航空機","地上障害物","その他"};
uint8_t msg_size = 25;
location *loc;
AUTH *auth;
uint8_t counter, ua_type;
char name[30],serNo[23],regNo[23];
char s_lat[20],s_lng[20],s_time[22],s_speed[20],s_alti[20],s_dir[6];;
int8_t rssi;
uint8_t primary_phy, secondary_phy, tx_power;
String jsonTxt;
bool dataReady = false;
uint16_t ss = 0;

void setJson(){
	jsonTxt ="{\"name\":\""		+ String(name) +	"\",";
	jsonTxt += "\"rssi\":\""	+ String(rssi) +	"\",";
 	jsonTxt += "\"cnt\":\""		+ String(counter) + "\",";	
	jsonTxt += "\"SN\":\""		+ String(serNo) +	"\",";
	jsonTxt += "\"RN\":\""		+ String(regNo) +	"\",";
	jsonTxt += "\"UA\":\""		+ ua[ua_type] +	    "\",";
	jsonTxt += "\"lat\":\""		+ String(s_lat) + 	"\",";
	jsonTxt += "\"lng\":\""		+ String(s_lng) +	"\",";
	jsonTxt += "\"speed\":\""	+ String(s_speed) +	"\",";
	jsonTxt += "\"alti\":\""	+ String(s_alti) +	"\",";
	jsonTxt += "\"dir\":\""		+ String(s_dir) +	"\",";	
    jsonTxt += "\"time\":\""	+ String(s_time) +	"\",";
	jsonTxt += "\"pPhy\":\""	+ phy[primary_phy-1] +	"\",";
	jsonTxt += "\"sPhy\":\""	+ phy[secondary_phy-1] +"\",";
	jsonTxt += "\"pw\":\""		+ String(tx_power) +	"\"}";
	dataReady = true;
}

time_t tm2019;	// 1900年から2019年までの秒数
void get_tm2019(){
  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 );
}

uint8_t *decode_msg(uint8_t *data){
	uint8_t *adr = data;	
	uint8_t type,size,n;
	uint8_t id_type,status,dir;
	float lat,lng,alti;
    uint16_t h,m,s;
	uint16_t speed;
	time_t tim;
	struct tm t; 
	
	type = data[0] & 0xF0;
	switch(type){
		case MSG_TYPE_PACK:							// パケットデータ
			msg_size = data[1];
			n 	 = data[2];
			adr  = &data[3];
			for(int i=0;i<n;i++){
				adr = decode_msg(adr);
			}
			break;
			
		case MSG_TYPE_BASIC_ID:
			id_type = data[1] & 0xF0;
			if(id_type == ID_TYPE_SerialNo){		// 製造番号
				memcpy(serNo, &data[2], 20);
				ua_type = data[1] & 0x0F;			// 機体種別
				adr += msg_size;
			}
			if(id_type == ID_TYPE_ASSIGED_REG){		// 登録記号
				memcpy(regNo, &data[2], 20);
				adr += msg_size;
			}
			break;
		
		case MSG_TYPE_LOCATION:						// 位置情報
			loc = (location *)&data[0];

			status = loc->status; 
			lat = (float)loc->lat/10000000;
			lng = (float)loc->lng/10000000;
			sprintf(s_lat,"%10.7f",lat);
			sprintf(s_lng,"%10.7f",lng);
			h = loc->timestamp%36000;
			m = h/600;
			s = (h%600)/10;
			ss = h%10;
			s_time[20] = '0' + ss;

			speed = loc->speed;
			if(speed<254){
				if(status & LOC_FLAG_SM){
					speed = speed * 0.75 + 255/4;
				}else{
					speed /= 4; 
				}
			}
			sprintf(s_speed,"%4dm/s",speed);

			alti = loc->Geodetic_Altitude;
			alti = alti/2-1000;
			sprintf(s_alti,"%7.1fm",alti);

            dir = loc->dir;
			if(status & LOC_FLAG_EW) dir += 180;
			sprintf(s_dir,"%3d°", dir);

			adr += msg_size;
			break;

		case MSG_TYPE_AUTH:							// 認証情報
			auth = (AUTH *)&data[0];
			tim = auth->timestamp_auth + tm2019;
			t = *localtime(&tim);
			sprintf(s_time,"%04d/%02d/%02d %02d:%02d:%02d.%1d", t.tm_year + 1900, t.tm_mon +1 , t.tm_mday, (t.tm_hour + 9)%24, t.tm_min, t.tm_sec, ss);
			adr += msg_size;		
			break;

		default:
			adr += msg_size;
			break;			
	}
	return adr;
}


void decode_data(uint8_t *data, uint8_t size){
	uint8_t *adr, len, type;

	adr = data;
	while(adr < data+size){
		len = adr[0];
		if(len==0)break;
		type = adr[1];
		switch(type){
			case AD_TYPE_CMP_LOCAL_NAME:	// 名前データ
				memcpy(name, &adr[2], 28);
				break;
			case AD_TYPE_SERVICE_DATA:		// サービスデータ
		  	    counter = adr[5];
		  	    decode_msg(&adr[6]);		// メッセージをデコードする
				break;
			default:
				break;
		}
		adr += len+1;
	}
}

/**
* @brief extend adv report parameters
*/
//typedef struct {
//    esp_ble_gap_adv_type_t event_type;              /*!< extend advertising type */
//    uint8_t addr_type;                              /*!< extend advertising address type */
//    esp_bd_addr_t addr;                             /*!< extend advertising address */
//    esp_ble_gap_pri_phy_t primary_phy;              /*!< extend advertising primary phy */
//    esp_ble_gap_phy_t secondly_phy;                 /*!< extend advertising secondary phy */
//    uint8_t sid;                                    /*!< extend advertising sid */
//    uint8_t tx_power;                               /*!< extend advertising tx power */
//    int8_t rssi;                                    /*!< extend advertising rssi */
//    uint16_t per_adv_interval;                      /*!< periodic advertising interval */
//    uint8_t dir_addr_type;                          /*!< direct address type */
//    esp_bd_addr_t dir_addr;                         /*!< direct address */
//    esp_ble_gap_ext_adv_data_status_t data_status;  /*!< data type */
//    uint8_t adv_data_len;                           /*!< extend advertising data length */
//    uint8_t adv_data[251];                          /*!< extend advertising data */
//} esp_ble_gap_ext_adv_reprot_t;

// BLEがアドバタイジングデータを受信したとき呼ばれる
class MyBLEExtAdvertisingCallbacks: public BLEExtAdvertisingCallbacks {
    void onResult(esp_ble_gap_ext_adv_reprot_t report) {
      if(report.event_type & ESP_BLE_GAP_SET_EXT_ADV_PROP_LEGACY){
//        Serial.println("BLE4.2");
      }else{	// BLE 5.x の時
      	rssi = report.rssi;
      	primary_phy = report.primary_phy;
      	secondary_phy = report.secondly_phy; 
      	tx_power = report.tx_power;
		decode_data(report.adv_data, report.adv_data_len);
		setJson();
      }
    }
};

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
	switch(type) {
		case WStype_DISCONNECTED:
			break;
		case WStype_CONNECTED:
			break;
		case WStype_TEXT:
			break;
		case WStype_BIN:
			break;
		case WStype_ERROR:			
		case WStype_FRAGMENT_TEXT_START:
		case WStype_FRAGMENT_BIN_START:
		case WStype_FRAGMENT:
		case WStype_FRAGMENT_FIN:
			break;
	}
}

void setup() {
//  Serial.begin(115200);
  if(!SPIFFS.begin(true)){  // SPIFFSのセットアップ
	return;
  }
  get_tm2019(); // 2019年までの秒数を計算しておく

  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setExtendedScanCallback(new MyBLEExtAdvertisingCallbacks());	// BLE コールバックルーチンを指定
  pBLEScan->setExtScanParams();
  delay(1000);
  pBLEScan->startExtScan(scanTime, 1); // scan duration in n * 10ms, period - repeat after n seconds (period >= duration)

  WiFi.softAP(ssid,pass);
  WiFi.softAPConfig(ip,ip,subnet);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html");
  });
  server.on("/map", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/map.png", "image/png");
  });
  server.on("/mark", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/mark.png", "image/png");
  });
  server.begin();
	
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);

}

void loop() {
	webSocket.loop();
	if(dataReady){
		webSocket.broadcastTXT(jsonTxt);	// WebSocketでデータ送信
		dataReady = false;
	}
}

最後にプログラムをESP32C3に焼きます。

これでRID受信機の出来上がりです。
ESP32C3の[RESET]ボタンを押すと、プログラムが起動します。

使用方法

スマホのWiFi接続先を[RID_Receiver]にして、パスワードに[12345678]と入力します。
(機種によってはもっと質の良いWiFiに繋ぎたい見たいなメッセージが出る場合があります。このときは[許可]ではなく[拒否]を選択して下さい。)

スマホのWebブラウザを立ち上げ、URLに[192.168.5.1]と入力します。
すると下図のように表示されるはずです。

以上です。

感想

 マップはかなり大雑把ですが、緯度経度の数字だけではなんとなく詰まらないので、マップ上に表示させて見ました。
 位置はかなりいい加減です。自分だけの詳細なマップを用意して、さらに精度を上げてみるのも良いかもです。

補足

VSCodeでHTMLファイルを編集する時、プレビュー画面を見ながら編集できるようにする拡張機能があります。<Live Preview>と言う拡張機能です。こちらを参考にインストールして見て下さい。

VSCodeには色んな拡張機能が用意されてて、追加インストールすることにより、より便利に使えるようになります。色々検索してみると良いと思います。
もう Arduino IDE には戻れませんね。

7
6
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?