リモート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の中身は以下のようにして下さい。
追加ライブラリーやシリアルモニタの速度、パーテッションの指定をしています。
[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]を、先ほど作った[data]フォルダーにドラッグ&ドロップしてコピーします。
次に[data]フォルダ内に新たに[index.html]ファイルを作ります。
[data]フォルダ名の上でマウスの右クリックをして、「新しいファイル」を選択し、ファイル名を[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]の中身に下記のコードをコピペして下さい。
(プログラム領域の右上のアイコンをクリックすると全コピーできます。)
プログラム
#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 には戻れませんね。