リモートIDを自作する（パート２）

### 100g以上の無人航空機に義務化されたリモートIDを自作できないかやってみた。（パート２）
[前回は](https://qiita.com/kobatan/items/20722dbac3cabb37b5f1#comment-b7849af9dfb942f3e280)、RID情報をプログラム内に記録させていたため、登録記号などの情報を書き換える時、プログラムを書き換える必要がありました。
　今回は、RID情報をSPIFFS領域に記録するようにしたので、プログラムを書き換えることなく変更できます。スマホアプリも必要なく、スマホからWiFiで接続して書き換えます。（このやり方は、正規の方法とは全く異なります。）

あと、ちゃんと送信されているか確認用の[**RID受信機の作り方はこちら**](https://qiita.com/kobatan/items/dc26c28c0e283ba6f4c3)です。
### 用意するもの
①　マイコン　[Seeed XIAO ESP32C3　(スイッチサイエンス　902円）](https://www.switch-science.com/products/8348) 
②　GNSSモジュール　[Flywoo GOKU GM10 Nano V3 GPS　(HELIMONStER 2890円)](https://helimonster.jp/?pid=173344689)
③　USB-C電源 又は 1Sリポバッテリー
④　熱収縮チューブ

[<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/73dffd29-2f20-11a1-dbfd-ca7962f9a9ce.jpeg">](https://www.switch-science.com/products/8348)[<img width="244" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/2cab258a-f3f9-26eb-7852-9aeca120e21a.png">](https://helimonster.jp/?pid=173344689)

　[XIAO ESP32C3](https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/) は　Wifi,Bluetooth5(BLE) 内蔵の超小型マイコンです（外付けアンテナ付き）技適もバッチリついています。
　背面には1Sリポバッテリーの接続端子がありそこからの起動もできます。なおかつ充電回路も内蔵してるのでＵＳＢからバッテリーを充電できます。

　BLEの最大出力は15dBあり、私の測定では940m飛ぶことを確認しています。（2階の窓に置いて、そこからの見通し出来る距離が最高940mでした。それ以上は障害物があり測定できませんでした。もしかしたらもっと飛ぶかもしれません。1.5kmぐらい真っすぐな道路を見つけたので、割りばしの先に発信機を取り付けて道端に刺し測定したら300mぐらいしか飛びませんでした。地上から約20cmぐらいでは、道路の反射などが影響するのでしょうか？良く分かりませんが距離が出ませんでした。）

配線は以下のようにして下さい。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/8369e641-e68c-54d9-d75a-ddc017ef54fd.jpeg">
GNSSの電源は5Vでも3.3Vでもいいのですが、リポバッテリーでも起動できるように3.3Vの方に接続します。
外部電源を3.7Vから取るときは、写真のように1Sリボバッテリー用コネクタを別途取り付けてください。
5Vから電源を取るときは、5V端子にダイオードをかまして5V電源を接続するように[マニュアル](https://wiki.seeedstudio.com/XIAO_ESP32C3_Getting_Started/)には書かれていました。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/6f8a4f0b-4444-6e00-8b2b-2cc57d3bd026.jpeg"><img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/c5ae5979-076b-3cd8-633e-16c40e70d38e.jpeg">

### プログラム
　前回は[**Arduino IDE**]でプログラムしましたが、今回は[**Visual Studio Code**]+[**PlatformIO**]の開発環境でプログラムします。SPIFFS領域を使用したかったからです。

[**VS Code**]+[**PlatformIO**]のインストールは[**こちらを参考**](https://zenn.dev/kotaproj/articles/esp32_vscode_pio)にしました。
新規プロジェクトでは下図のように設定して下さい。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/ef3d3e5a-8019-b29e-3809-efe5e2274e60.jpeg">
[**platformio.ini**]の中身は以下のようにして下さい。
追加ライブラリーやシリアルモニタの速度、パーテッションの指定をしています。
（プログラム領域の右上のアイコン<img width="30" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/19cd08f7-d288-a4c0-e09a-4538e7737892.jpeg">をクリックすると全コピーできます。）
```platformio.ini
[env:seeed_xiao_esp32c3]
platform = espressif32
board = seeed_xiao_esp32c3
framework = arduino
lib_deps = 
	mikalhart/TinyGPSPlus@^1.0.3
	ottowinter/ESPAsyncWebServer-esphome@^3.0.0
	links2004/WebSockets@^2.3.7
	bblanchon/ArduinoJson@^6.20.1
monitor_speed = 115200
board_build.partitions = huge_app.csv
```
dataフォルダを新たに作成します。
[**RemoteID**]のところを右クリックして、[新しいフォルダ]を選択し、フォルダ名を[**data**]にします。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/d9f50958-f1ca-3549-a6d6-986f385d53ec.jpeg">
　次に[data]フォルダ内に新たに[**index.html**]ファイルを作ります。
[data]フォルダ名の上でマウスの右クリックをして、「新しいファイル」を選択し、ファイル名を[index.html]にします。中身は以下です。
```index.html
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>RID登録</title>
	</head>
	<style>
		body	{font-size: 1rem; background-color:rgba(128,128,128,0.322);}
		h2		{font-size: 1.2rem;color:rgb(34, 0, 255); text-align: center; background-color: #aaffff;}
		p		{margin-bottom: 0px;}
		input	{font-size: 1rem;}
		select  {font-size: 1rem;}
		div		{margin-top: 20px; text-align: center}
	</style>
	<!-----------------------HTML----------------------pattern="[A-Z0-9\.]*"  -------------->
	<body>
		<h2>RID情報 書換えフォーム</h2>
		<p>名前：</p>
		<input type="text" id="name" maxlength="28" aria-label="name">
		<p>製造番号：</p>
		<input type="email" id="sn" maxlength="20" aria-label="SN" inputmode="verbatim" autocomplete="off" pattern="[A-Z0-9\.]*" oninput="value = value.toUpperCase();">		
		<p>登録記号：</p>
		<input type="email" id="rn" maxlength="20" aria-label="RN" inputmode="verbatim" autocomplete="off" pattern="[A-Z0-9\.]*" oninput="value = value.toUpperCase();">
		<p>機体種別：</p>
		<select id="type" aria-label="type">
			<option value="0">なし</option>
			<option value="1">飛行機</option>
			<option value="2">ヘリコプター</option>
			<option value="3">ジャイロプレーン</option>
			<option value="4">ハイブリッドリフト</option>
			<option value="5">羽ばたき機</option>		
			<option value="6">グライダー</option>
			<option value="7">カイト（凧）</option>
			<option value="8">自由気球</option>
			<option value="9">係留気球</option>
			<option value="10">飛行船</option>
			<option value="11">パラシュート</option>
			<option value="12">ロケット</option>
			<option value="13">テザー式動力航空機</option>
			<option value="14">地上障害物</option>
			<option value="15">その他</option>			
		</select>
		<div>
			<input type="button" value=" 送 信 " onclick="butotnClick()" style="width: 200px; background-color: aquamarine;" />
		</div>
		<div id="msg" style="color: #00aa00;"></div>
		<div id="time"></div>
    
	</body>
	<!--------------------JavaScript---------------------------------->
	<script>
		var ws = null;
		InitWebSocket();
		
		document.getElementById('rn').value = 'JA.';
		let msg = document.getElementById('msg');

	 	// jsonデータのテンプレート
 		var json_data = {
            name: '',
			sn: '',
			rn: '',
			type: 0
        };

		function butotnClick(){											// 送信ボタンが押されたら
  		// JSONデータの作成
			var send_json = json_data;
            send_json.name = document.getElementById("name").value;
			send_json.sn   = document.getElementById("sn").value;	
			send_json.rn   = document.getElementById("rn").value;
			send_json.type = document.getElementById("type").value;
            send_json = JSON.stringify(send_json);
			if(ws != null)	ws.send(send_json);		                   // websocketでデータ送信
			msg.innerText = '送信しました。';
		};

		function InitWebSocket()										// Websocket設定						
  	    {
			ws = new WebSocket('ws://'+window.location.hostname+':81/');
			
			ws.onmessage = function(evt){					            // データが送られてきた時の処理
				JSONobj = JSON.parse(evt.data);
				document.getElementById('name').value = JSONobj.name;
				document.getElementById('sn').value 	= JSONobj.sn;			
				document.getElementById('rn').value 	= JSONobj.rn;
				document.getElementById('type').value = JSONobj.type;		
			}
			
			ws.onclose = function(evt) {							    // Websocketが閉じたときの処理
  			    ws.close();
				ws = null 
			}
		};
		
		var cnt = 180;
		PassageID = setInterval('countdown()',1000);   // タイマーをセット(1000ms間隔)
		function countdown(){
			cnt--;
			var msg = "残り" + cnt +"秒で終了します。";
			document.getElementById("time").innerHTML = msg;
		}
	</script>
</html>
```
この[data]フォルダーの中身をESP32C3のSPIFFS領域にアップロードします。

PCにESP32C3を接続して、[BOOT]ボタンを押しながら[RESET]ボタンをチョンと押し、[BOOT]ボタンを離します。すると書き込みモードになるので、下図の順番でクリックするだけです。
②を押した後に、ターミナルに"=====[SUCSESS]====="と表示されれば転送完了です。
③を押してエクスプローラーの表示に戻します。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/0489655e-4bb2-40c3-bbfb-2df40d6fd09c.jpeg">
そうしましたら後はプログラムを書くだけです。
[src]の[main.cpp]の中身に下記のコードをコピペして下さい。
```main.cpp
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEAdvertising.h>
#include <TinyGPSPlus.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <BLEScan.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <WebSocketsServer.h>
#include <SPIFFS.h>
#include <FS.h>
#include <ArduinoJson.h>

#define SERIAL_NO	   "1234A0123456789"	// RIDの製造番号 
#define REG_SYMBOL	   "JA.JU012345ABCDE"	// 無人航空機の登録記号
#define AUTHENTICATION {0}					// 認証情報
#define LOCAL_NAME 	   "My DRONE"			// 名前
uint8_t addr_coded[6] = {0xC0, 0xDE, 0x52, 0x00, 0x00, 0x01};	// BLE Address (上位2bit[11] = Random Address：Static Device Address) 他の人と被らない数値を設定して下さい。

const char *ssid="my_RemoteID";				// WiFi AP の名前
const char *pass="12345678";		   		// パスワード（8文字以上）
const IPAddress ip(192,168,6,1);	   		// IPアドレス
const IPAddress subnet(255,255,255,0); 	    // サブネットマスク

#define BAUDRATE	115200		// GNSS ボーレート
#define RATE		100 		// GNSS 更新レート 100ms(10Hz)

#define RX_PIN 20
#define TX_PIN 21
#define BOOT_BUTTON 9

// AD Type
#define AD_TYPE_FLAG			0x01	// 発信モード指定
#define AD_TYPE_SHT_LOCAL_NAME	0x08	// 短縮名
#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	// 操縦者情報
#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)	// データを１バイト単位に詰めて配置
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;
	//--- package ---------------------------	
		uint8_t type_pack = MSG_TYPE_PACK;		// pack
		uint8_t msg_size	= 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 = 0x0B;                // 精度(水平<3m)
			uint8_t B_S_Accuracy = 0x04;                // 精度(速度<0.3m/s)
			uint16_t timestamp = 0;			            // 現在時刻の分以下の小数点１までの秒数ｘ10
			uint8_t T_Accuracy = 1;		                // 時間精度(0.1s)
			uint8_t resv3;
		//--- Page0 (25byte)-----------------------------				
			uint8_t type_auth = MSG_TYPE_AUTH;	        // 認証情報
			uint8_t Auth_Type = 0x30;		            // Authentication　Message [認証情報]
			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[16] = AUTHENTICATION;	    // 認証データ(AES-128bit-CCM ってなんだ？ 解からないので無視)
//---- 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;	     // 名前
//---- Shortened local name -----------------------------
	uint8_t l2 = 4;							// Byte数
	uint8_t t2 = AD_TYPE_SHT_LOCAL_NAME;	// AD_type = Shortened local name
	char sat[3];    						// 補足衛星数	
} Adv_Data;
#pragma pack(pop)

TinyGPSPlus gps;
Adv_Data adv;
time_t tm2019;	             // 1970.1.1から2019.1.1 までの秒数
String rid_file ="/rid.txt"; // SPIFFSのファイルデータ
String jsonTxt;				 // 保存するJson文字列

void setJson(){
	jsonTxt ="{\"name\":\""	 + String(adv.local_name) +	"\",";
	jsonTxt += "\"sn\":\""	 + String(adv.UAS_ID1) +	"\",";
	jsonTxt += "\"rn\":\""	 + String(adv.UAS_ID2) +	"\",";
	jsonTxt += "\"type\":\"" + String(adv.ID_UA_type1 & 0x0f) +	"\"}";
}

void decodeJson(char *data){		// Jsonデータを解析
	StaticJsonDocument<100> doc;	// Jsonオブジェクトの初期化
	uint8_t d;
	int v;

	DeserializationError error = deserializeJson(doc, data);
	if(error){
		Serial.println("JSONエラー");
	}else{
	    const char* name = doc["name"];			// 名前
		const char* sn = doc["sn"];				// 製造番号		
		const char* rn = doc["rn"];				// 登録記号
		memcpy(adv.local_name, name, 28);		
		memcpy(adv.UAS_ID1, sn, 20);				
		memcpy(adv.UAS_ID2, rn, 20);				
		d = adv.ID_UA_type1 & 0xF0;
		v = doc["type"];
		v %= 15;
		adv.ID_UA_type1 = d | (uint8_t)v;		// 機体種別
		setJson();
	}
}

bool file_read(){								// SPIFFSからファイル読み込み
	char buf[100];
	File f = SPIFFS.open(rid_file, "r");
	if(!f){
		return false;
	}
	String data = f.readStringUntil('\n');
	data.toCharArray( buf, data.length());
	decodeJson(buf);
	f.close();
	return true;
}

bool file_write(){								// SPIFFへファイル書き込み
	char buf[100];
	File f = SPIFFS.open(rid_file, "w");
	if(!f){
		return false;
	}
	f.println(jsonTxt);
	f.close();
	return true;
}

void adv_data_init(){							// ADVデータ初期化
  struct tm stm;
  stm.tm_year = 2019 - 1970;
  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);

  file_read();							    	// SPIFFS から保存データを読み込む
}

void set_speed(float  s){					// 速度をセット
	uint8_t v=0;
	if(s <= 255*0.25){
		v = (uint8_t)(s*4);
		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){						// GNSS読み込み
  float lat,lng;
  struct tm stm;
  bool ok = false;
  uint32_t time;

  if( gps.satellites.isValid()){		    // 衛星補足数
	sprintf(adv.sat, "%d", gps.satellites.value());	
  }
  else sprintf(adv.sat, "%d", 0);	 	
  
  if (gps.location.isValid()){
  	lat = gps.location.lat();				// 緯度
  	lng = gps.location.lng();				// 経度
  	adv.lat = (uint32_t)(lat * 10000000);
  	adv.lng = (uint32_t)(lng * 10000000);
    ok = true;
  }

  if (gps.date.isValid()){
 	stm.tm_year = gps.date.year()-1970;		// 年
  	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_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_1M,		// Google Pixel 5 の場合はこちらでないと受信できませんでした。 
    .primary_phy = ESP_BLE_GAP_PHY_CODED,								
    .max_skip = 0,
    .secondary_phy = ESP_BLE_GAP_PHY_CODED,
    .sid =0,
    .scan_req_notif = false,
};

BLEServer *pServer = NULL;
BLECharacteristic *pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
std::string rxValue;
std::string txValue;

BLEMultiAdvertising advert(1); // max number of advertisement data 

//----- 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){	// GNSSに設定コマンドを送る
	int len;
	len = set_cs(buf);			// チェックサムを計算
	for(int i=0; i<len; i++){
		Serial1.write(buf[i]);
	}
	Serial1.println("");
	delay(300);
}

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);		// 確認用コマンド
		Serial1.end();
		delay(300);
	}
	Serial1.begin(BAUDRATE, SERIAL_8N1, RX_PIN, TX_PIN);
	while(!Serial1){}
	send_UBX((uint8_t *)&cfg_rate);				// 更新レートを10Hz(100ms間隔)に設定
}
//-------------------------------------------------------------------------------------------------

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

bool wificonnected = false;
bool sended = false;
uint32_t on_time;

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
	switch(type) {
		case WStype_DISCONNECTED:				// 切断されたとき
			server.end();
			wificonnected = false;
			break;
	
		case WStype_CONNECTED:					// 接続されたとき
			wificonnected = true;
			setJson();
			webSocket.broadcastTXT(jsonTxt);	// 登録データ送信
			on_time = millis();
			break;
	
		case WStype_TEXT:						// テキストデータが受信されたとき
			decodeJson((char *)payload);
			file_write();						// SPIFFSデータ書き換え		
			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 wifi_start(){
  WiFi.softAP(ssid,pass);
  delay(100);
  WiFi.softAPConfig(ip,ip,subnet);
  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html");
  });

  server.begin();
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
}

void wifi_stop(){
  webSocket.close();
  server.end();
  WiFi.mode(WIFI_OFF);
  WiFi.disconnect(true);
}

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

  pinMode(BOOT_BUTTON, INPUT);
  if(!SPIFFS.begin(true)){  // SPIFFSのセットアップ
		return;
  }

  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();
}

bool boot_mode = false;
bool boot_btn = false;

void loop() {
	if(!boot_mode){								    // 書き換えモードチェック
		if(digitalRead(BOOT_BUTTON) == LOW){	    // Bootボタンが押されたら
			if(boot_btn == false){
				on_time = millis();				    // 最初に押された時刻を記憶
				boot_btn = true;
			}
			else{
				if((millis() - on_time) > 2000){	// BOOTボタンが2秒以上押されたら
					wifi_start();					// WiFi AP を開始する
					boot_mode = true;
					on_time = millis();
				}
			}
		}
		else{
			boot_btn = false;
		}
	}else{											// 書き込みモードなら
		webSocket.loop();

		if((millis() - on_time) > 180000){		    // 3分経過したらwifiを止める
			wifi_stop();
			boot_mode = false;
		}
	}
	
	while(Serial1.available()){						// GNSSからデータが来たら
		if(gps.encode(Serial1.read())){
			if(gps_read()){
 				advert.setAdvertisingData(0, sizeof(Adv_Data), (uint8_t *)&adv);	// 拡張アドバタイズの内容を変更する
				adv.counter++;
			}
		}
	}
}
```
最後にプログラムをESP32C3に焼きます。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/40495859-3113-21a5-28c6-0780eb762b01.jpeg">
これでリモートID発信機の出来上がりです。

電源を入れて、衛星を受信すると、GM10 Nanoの緑色のLEDが、点灯から点滅に変わります。
### RID書き込みモードの使用方法
ESP32C3 の電源を入れた状態で、[**BOOT**]ボタンを**２秒以上**押すと、RID書き込みモードになります。
スマホのWiFi接続先を[<font color="Blue">my_RemoteID</font>]にして、パスワードに[<font color="Blue">12345678</font>]と入力します。
（スマホに名前が表示されるまで、結構時間がかかる場合がありますが、待ってれば出てくると思います。）
　機種によってはもっと質の良いWiFiに繋ぎたい見たいなメッセージが出る場合があります。このときは[許可]ではなく[拒否]を選択して下さい。
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/45053df3-7e1f-a429-a6c3-57d5a1256408.jpeg">
スマホのWebブラウザを立ち上げ、URLに[<font color="Blue">192.168.6.1</font>]と入力します。
すると下図のように表示されるはずです。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520758/e5356430-c4ee-c63a-86ca-b79a8f368fe0.jpeg">
内容を書き換えて、[送信]ボタンを押すと書き込まれます。
３分経過すると一応自動でWiFiは閉じるようにしましたが、電源を入れ直した方が早いです。
### 最後にお願い
　これを作成又は使用したことにより、何らかの問題が発生したとしても、私は一切責任を負いませんので、あくまで自己責任でお願い致します。