飛行情報投稿bot

### 目的
羽田新ルートをmisskey上で投稿にて見ることができるようにした。マイコンボードを用いて常時運用としたかった。

### 必要構成
- ESP32(ESP32-DevKitC-VE　秋月購入)
- circuitpython 8.2.3
    - [smtp_circuitpython.py](https://github.com/scogswell/SMTP-for-Circuitpython)
    - adafruit_ntp.mpy
    - adafruit_requests.mpy
- Misskeyアカウトとアクセストークン
- メールアドレスアカウント
- linux mint 22
- python
- wifi環境　2.4GHz
  
### 内容
[flightrader24](https://www.flightradar24.com)のAPIによるJson情報を使ってMisskey上でノートするというものである。
監視する空域は経緯緯度で指定しないと全情報を取得してしまうので注意が必要である。今回は羽田新ルートを対象にした。

### 導入
ESP32の開発環境の準備、misskeyの準備、あればメールアカウントの準備を順にしておく。

## ESP32の準備
ESP32のベースとして秋月でDevKitCを購入した。microUSBケーブルと適当なUSBACアダプタをつなげる。
開発環境として[circuitpython](https://circuitpython.org/)を採用した。
ESP32のファームとしてadafruit-circuitpython-doit_esp32_devkit_v1-ja-8.2.3.binを[公式](https://circuitpython.org/)よりダウンロードして、esp32に書き込む。
書き込みはブラウザ上からできるためchrom系列で書き込んだ。[ESP Web Flasherを使用して書き込み](https://poporon-lab.hatenablog.com/entry/2022/09/22/012737)が参考になる。ただし、circuitpyとしてドライフが表示されない。esp32のベースとして表示されない場合もある。
[circuitpythonはwebブラウザだけでファイルのアップロードや編集が行える。](https://steam-tokyo.com/esp32-c3-circuitpython-web-workflow/)
WIFI環境は個人によって異なるため　2.4G帯を利用できるようにしておく。

## MisskeyのAPI利用の準備
Misskeyのアカウトとアクセストークンは公式にしたがって取得しておく。
Misskeyのモジュールをインストールしておく。

## メールアカウントの準備
あればでいいが、メールアカウントがあると飛行情報の確保とESP32の情報が取得できる。今回は自分はメールアカウントを持っており、取得データをメールに飛ばすようにした。
- エラーログはesp32_logのメールアドレス
- 取得データはdataのメールアドレス
とした。

### プログラム本体
- secrets.py
wifiアクセスにおいて自身で管理しているSSID,パスワードを保管しておく。

- code.py
ブート時に自動的に読みこれまれる

```python:code.py
import airplane
```
- airplane.py

```python:airplane.py

#
#羽田新ルートの侵入を検知
#ESP32にて判定
#緯度(北35.71878,南35.61535）経度(西139.64919,東139.74154)


import gc
import board
import digitalio
import time
import json
import ssl
import wifi
import socketpool
import smtp_circuitpython
import adafruit_requests
import rtc
import adafruit_ntp
import supervisor


SMTP_SERVER = "契約しているSMTPサーバー"
SMTP_PORT = 465
TZ_OFFSET = 9  #日本のUTCから時差
# Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and
# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other
# source control.
# pylint: disable=no-name-in-module,wrong-import-order
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

print("\n===============================")
print("Connecting to WiFi...")

while not wifi.radio.ipv4_address:
    try:
        wifi.radio.connect(secrets["ssid"], secrets["password"])
    except ConnectionError as e:
        print("Connection Error:", e)
        print("Retrying in 10 seconds")
    time.sleep(10)
    gc.collect()
print("Connected!\n")

FR_URL= "https://data.flightradar24.com/zones/fcgi/feed.js?bounds=35.71878,35.61535,139.64919,139.74154&adsb=1&mlat=1&faa=1&flarm=1&estimated=1&air=1&gnd=1&vehicles=1&gliders=1&array=1"

misskey_api_url = "https://misskey.io/api/notes/create"
misskey_token = "misskeyで取得した投稿用のトークン"
header = {
               'Content-Type' : 'application/json',"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 Firefox/47.0",
               }

check_airplanename_before = []  #前回の機体名
no_airplane_countdown_default = 36  #3分飛行情報をカウント出来ない場合
no_airplane_countdown = no_airplane_countdown_default
interval_time = 10  #監視間隔時間sec

pool = socketpool.SocketPool(wifi.radio)
context = ssl.create_default_context()
    
def send_mail(mail_to, mail_subject, mail_body):
    try:
        smtp = smtp_circuitpython.SMTP(host=SMTP_SERVER, port=SMTP_PORT,
                                       pool=pool, ssl_context=context, use_ssl = True,
                                       username=secrets['mail_user'],password=secrets['mail_password'],
                                       debug = True)
        mail_to += "メールアドレス"
        smtp.to(mail_to)
        smtp.body("Subject: "+ mail_subject +"\r\n\r\n"+ mail_body)
        smtp.quit()
        del smtp
        gc.collect
        
    except Exception as e:
        gc.collect
        print("SMTP Error:"+ str(e))
        #supervisor.reload()
        
        
        
def set_now_time():
    """NTPサーバーから現時刻を設定する"""
    try:
        ntp = adafruit_ntp.NTP(pool, tz_offset=TZ_OFFSET)
        rtc.RTC().datetime = ntp.datetime
        del ntp
        gc.collect
    except Exception as e:
        del ntp
        gc.collect()
        print("NTPサーバーエラー:" + str(e))
        send_mail("esp32_log","", "NTPサーバーエラー:"+ str(e))
        
        
def response(method, url, data):
    """httpのリクエスト"""
    requests = adafruit_requests.Session(pool, context)
    if(method == "get"):
        try:
            response = requests.get(url=url,headers=header)
            body = response.json()
            response.close()
            del response,requests
            gc.collect()
            print(body)
            return body
        except Exception as e:
            print("APIエラー:"+ str(e))
            del requests
            gc.collect()
            send_mail("esp32_log","","getエラー:" + str(e))
            no_airplane_countdown = no_airplane_countdown_default
    if(method == "post"):
        try:
            res = requests.post(url = url, data = json.dumps(data).encode("utf-8"), headers= header)
            body = res.json()
            if(body['error'] != []):
                res.close()
                del res
                time.sleep(60)
                raise("サーバー制限")
            res.close()
            del res,requests
            gc.collect
            print(body)
            return body
        except Exception as e:
            print("APIエラー:"+ str(e))
            time.sleep(60)
            del requests
            gc.collect()
            send_mail("esp32_log","","postエラー:" + str(e))
    

set_now_time()

gc.collect()

while True:
    try:
        body = response("get", FR_URL, "")
        check_airplanename = []
        infos = []
        for info in body['aircraft']:
            infos.append(info)
            check_airplanename.append(info[-2])  #機体名取得
            approach_airport = info[-6]  #着陸空港
            print(check_airplanename)
        del body
        gc.collect
        if ( (check_airplanename != []) and (set(check_airplanename) ^ set(check_airplanename_before)) and (approach_airport == "HND") ):
            if( (check_airplanename_before == []) or  (check_airplanename_before[-1] != infos[-1][-2])): #note_before != ("羽田新ルート情報実験中:"+ (','.join(map(str,body['aircraft'][-1]))) + "[FlightRadar24](https://www.flightradar24.com/airport/hnd)")
                data = {"i": misskey_token,"text":"羽田新ルート情報実験中:"+ " 出発:"+ str(infos[-1][-7]) + " #" + str(infos[-1][-10]) + " " + str(infos[-1][-2]) + " " +  "[FlightRadar24](https://www.flightradar24.com/"+str(infos[-1][-2])+"/"+str(infos[-1][0])+")"}
                try:
                    interval_time = 10
                    res = response("post", misskey_api_url, data)
                    del res,data
                    gc.collect()
                    raw_airplane_info = ','.join(map(str,infos[-1]))
                    send_mail("data","", raw_airplane_info)
                    del raw_airplane_info
                    check_airplanename_before.clear()
                    gc.collect()
                    for tmp_info in check_airplanename:
                        check_airplanename_before.append(tmp_info)
                        
                except Exception as e:
                    print(str(e))
                    check_airplanename_before.clear()
                    del data
                    gc.collect()
                    end_mail("esp32_log","","投稿エラー:"+ str(e))
                
        time.sleep(interval_time)
        del infos
        gc.collect()
        now = time.localtime()
        if((check_airplanename == [])):
            no_airplane_countdown = no_airplane_countdown - 1
            print("飛行機情報なし:"+str(no_airplane_countdown)+"残り確認回数")
            if( no_airplane_countdown == 0):
                if( interval_time == 10):
                    send_mail("esp32_log","","飛行機情報なしのため2分間隔に変更")
                    interval_time = 120
                if( now.tm_hour == 0):
                    send_mail("esp32_log","","０時のため６時間投稿中止")
                    time.sleep(21600)
                    set_now_time()
                    gc.collect
                no_airplane_countdown = no_airplane_countdown_default                
        else:  #ゴーアラの場合はカウントダウンをリセット
            no_airplane_countdown = no_airplane_countdown_default
        if(now.tm_hour == 14):
            interval_time = 10
            print("間隔を10秒にしました")
        del now

    except Exception as e:
        print("異常終了:" + str(e))
        gc.collect
        send_mail("esp32_log","","異常終了:" + str(e))
        no_airplane_countdown = no_airplane_countdown_default
        time.sleep(10)
        supervisor.reload()
        
    finally:
        gc.collect()
        

        
```

### まとめ
空域に侵入を確認するとmisskeyにノートをしてくれることを確認できた。
エラー処理と動作確認としてesp32_logのメールアドレスに原因を送信された。

### 問題点
- 空域に複数機体が侵入した場合jsonに依存する
- 更新頻度の問題

### 課題
- 情報精度の向上
- さらなる低消費電力の追求

### 感想
- esp32は常時サーバーとしては低消費電力で昨今のように電力代高騰の折にはうってつけである
- 小型なのも利便性が高い
- 運用に際して一月ほど連続稼働を目指したため稼働実験に３ヶ月くらい試行錯誤していた

### 参考リンク
- ESP32のチュートリアル(https://learn.adafruit.com/circuitpython-with-esp32-quick-start/overview)
- [smtp_circuitpython.py](https://github.com/scogswell/SMTP-for-Circuitpython)
- [ＥＳＰ３２－ＤｅｖＫｉｔＣ　ＥＳＰ－ＷＲＯＯＭ－３２開発ボード](https://akizukidenshi.com/catalog/g/gM-11819/)