リモートID簡易受信機を作る


以前[リモートIDの送信機を作りました](https://qiita.com/kobatan/items/20722dbac3cabb37b5f1)が、その電波を確認するには、BLE5.x でかつ、Long range(PHY Coded)に対応したスマホが必要でした。しかしPHY Codecはオプションの為か、たとえBLE5.3であっても対応していない機種の方が多いようです。
　そこで、対応していないスマホでも受信できるようにしてみようと思います。

＜追記：[こちらにRID受信機(パート２)マップ付き](https://qiita.com/kobatan/items/dc26c28c0e283ba6f4c3)を作りました。＞

### 用意するもの
① [マイコン XIAO ESP32C3 (秋月電子 940円)](https://akizukidenshi.com/catalog/g/gM-17454/)
② USBモバイルバッテリ
③ USB-C ケーブル
④ 普通のスマホ

### 原理
マイコンは前回送信用に使用したマイコンと同じです。
今回は、これをBLE LongRange 受信機にします。
そして、マイコンからWiFiでスマホに受信データを送ります。
マイコンがアクセスポイントAPに成ります。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/e9f7cc28-7e15-e864-406b-540c8d97eb3b.jpeg">


スマホのWiFi接続先を"RID_Receiver"にして、パスワードに"12345678"を入力してください。
（WiFiの接続先を切り替えるとき、機種によっては、質の良いWiFiに繋ぎたいみたいな内容のメッセージが出る場合がありますが、この時は[許可]ではなく[拒否]を選択してください。）
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/a81941b1-e225-5de4-eea3-53e8e9810213.jpeg">
スマホのブラウザでURLを192.168.5.1と入力すれば、Webページが表示されると言う仕組みです。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/f2c5e045-63b1-2a5f-03c5-0ac05a6e260d.jpeg">


### プログラムの準備
Arduino　IDE　を立ち上げます。
ESP32C3 のボードを使えるようにします。
こちらを参考にしました。
[Seeed Studio XIAO ESP32C3をさっそく試してみた](https://qiita.com/jksoft/items/0a0f9a559e08293194d1)

今回はWebSocketと言う仕組みを使ってWiFiデータを送信するので、そのライブラリーをインストールします。
[ツール]➡[ライブラリを管理]　検索窓に"WebSockets"と入力すると、かなりヒットしますが、下から５番目ぐらにそのままの名前で出てくるのそれをインストールします。

プログラム領域を拡張するために
[ツール]➡[Patition Scheme] を"Huge APP"あたりにして下さい。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/881fbf89-a09b-1de6-0c63-4b63529299e2.jpeg">

### プログラム
```c++:RID_Receiver.ino
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>

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

// ADタイプ
#define AD_TYPE_CMP_LOCAL_NAME	0x09
#define AD_TYPE_SERVICE_DATA	0x16	//16bit 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
#define MSG_TYPE_PACK  			0xF0

// UAS ID Type
#define ID_TYPE_SerialNo 		0x10	// 製造番号種別
#define ID_TYPE_ASSIGED_REG		0x20	// 登録番号種別
#define ID_TYPE_UUID			0x30

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

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

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

String phy[3] ={"1M PHY","2M PHY","Coded PHY"};
uint8_t msg_size = 25;
location *loc;
uint8_t counter;
char name[30],serNo[23],regNo[23];
char s_lat[20],s_lng[20],s_time[20],s_speed[20],s_alti[20];
int8_t rssi;
uint8_t primary_phy, secondary_phy, tx_power;
String jsonTxt;
bool dataReady = false;

// R"====( この中に " 文字をそのまま書けるようにする )====";
// Rプレフィックスを付けた文字列リテラル内の丸カッコ( )で囲まれた部分は、
// エスケープシーケンスが無視される。この機能を「生文字列リテラル (Raw string literals)」という。
// 丸カッコの前後d-char-sequence(=====の部分)には、生文字列の範囲を明確にするための文字列を指定する。これは前後で同じ文字列を指定する必要がある。
// これは、文字列リテラルのなかにダブルクォーテーションや閉じ丸カッコが指定されるような状況で必要となる。
const char webpage[] PROGMEM = 
R"=====(
<!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     {color:rgb(156,5,5); background-color: whitesmoke;}
	h2     {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="lat"></span><br>
		経度: <span id="lng"></span><br>
		速度: <span id="speed"></span><br>
		高度: <span id="alti"></span><br>		
        時刻：<span id="time"></span><br>
	</h2>
</body>
<!-------------------------------------JavaScript--------------------------------------->
<script>
  InitWebSocket()
  function InitWebSocket()
  {
		websock = new WebSocket('ws://'+window.location.hostname+':81/');
		websock.onmessage=function(evt)
		{
			JSONobj = JSON.parse(evt.data);
			
			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('lat').innerHTML = JSONobj.lat;
			document.getElementById('lng').innerHTML = JSONobj.lng;
			document.getElementById('speed').innerHTML = JSONobj.speed;
			document.getElementById('alti').innerHTML = JSONobj.alti;
     	    document.getElementById('time').innerHTML = JSONobj.time;                
			document.getElementById('pPhy').innerHTML = JSONobj.pPhy;
			document.getElementById('sPhy').innerHTML = JSONobj.sPhy;
			document.getElementById('pw').innerHTML = JSONobj.pw
		}
	}
</script>
</html>
)=====";

void setJson(){
	jsonTxt ="{\"name\":\""		+ String(name) +	"\",";
	jsonTxt += "\"rssi\":\""	+ String(rssi) +	"\",";
 	jsonTxt += "\"cnt\":\""		+ String(counter) + "\",";	
	jsonTxt += "\"SN\":\""		+ String(serNo) +	"\",";
	jsonTxt += "\"RN\":\""		+ String(regNo) +	"\",";
	jsonTxt += "\"lat\":\""		+ String(s_lat) + 	"\",";
	jsonTxt += "\"lng\":\""		+ String(s_lng) +	"\",";
	jsonTxt += "\"speed\":\""	+ String(s_speed) +	"\",";
	jsonTxt += "\"alti\":\""	+ String(s_alti) +	"\",";
    jsonTxt += "\"time\":\""	+ String(s_time) +	"\",";
	jsonTxt += "\"pPhy\":\""	+ phy[primary_phy-1] +	"\",";
	jsonTxt += "\"sPhy\":\""	+ phy[secondary_phy-1] +"\",";
	jsonTxt += "\"pw\":\""		+ String(tx_power) +	"\"}";
	dataReady = true;
}
				
uint8_t *decode_msg(uint8_t *data){
	uint8_t *adr = data;	
	uint8_t type,size,n;
	uint8_t id_type,status;
	float lat,lng,alti;
    uint16_t h,m,s,ss;
	uint16_t speed;
	
	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);
				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;
            sprintf(s_time,"%02d分%02d.%01d秒",m,s,ss);
			speed = loc->speed;
			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);
			adr += msg_size;
			break;

		case MSG_TYPE_AUTH:						// 認証情報
			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 handleRoot()
{
  server.send(200,"text/html", webpage);
}

void setup() {
//  Serial.begin(115200);

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

  WiFi.softAP(ssid,pass);
  WiFi.softAPConfig(ip,ip,subnet);
  server.on("/",handleRoot);
  server.begin();
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);  
}

void loop() {
	webSocket.loop();
	server.handleClient();
	if(dataReady){
		webSocket.broadcastTXT(jsonTxt);	// WebSocketでデータ送信
		dataReady = false;
	}
}
```
以上です。