[Python]アマチュア無線のログをCSVやADIFに変換してみた（3）ログCSVからADIFへ

## はじめに

&emsp;本稿は以下2本の続きです。

https://qiita.com/Hagian/items/5fde6bbe525bd71569ce

https://qiita.com/Hagian/items/d1198d5e34d2fa10e54e

## 概要

&emsp;本稿では、アマチュア無線のログデータを、CSVからADIFに変換して出力するPythonプログラムを紹介します。

### ADIFとは

&emsp;ADIFは、 <ins>**Amateur Data Interchange Format**</ins> の略で、アマチュア無線のログデータをやり取りするための共通形式です。ロギングソフトではTurbo HAMLOGやZLOGなどで扱えるほか、eQSL、LoTW、QRZ.com、IOTA/SOTA/POTAなどへのログアップロードにも用いられる形式です（2023年10月時点での最新版はVersion 3.14）。

https://adif.org/

ADIF形式のファイルですが、WindowsではADIF Masterというソフトがあり、以下リンク先のように直接扱うことができます。

https://ja4joe.livedoor.blog/archives/20993496.html

&emsp;しかし、MacではADIFを扱えるソフトが見つけられませんでした ~~（探し方が悪い）~~ 。そこで、ログデータからADIFへの変換プログラムの製作を思い立ちました。

## 目的

&emsp;今回ADIFファイルを生成する主な目的は、POTAへのログアップロードです。POTAの説明は割愛しますが、端的に説明すれば自然公園に関する世界的なアクティビティプログラムです。

https://pota.app/#/

## 実装

### 開発環境

* macOS Ventura 13.4
* Python 3.11.4
* Visual Studio Code 1.80.1

### 元データ

&emsp;元のログCSVはこのような形式です。[初回の記事](https://qiita.com/Hagian/items/5fde6bbe525bd71569ce)でExcelブックからCSVへ変換出力しています。

```log.csv
id,DATE,TIME(JST),frequency,mode,callsign,sent_rst,received_rst,received_qth,received_qra,is_qsl_sent,card_remarks,remarks_1,sent_qth,remarks_2,
1,2023-01-01,12:34,430MHz,FM,JA1RL,59,51,東京都豊島区,鈴木さん,false,Bureau,QSOパーティ,東京都足立区,,
2,2023-04-05,20:16,144MHz,FM,JA7YAB/7,59,59+,山形県天童市,佐藤さん,True,Direct,山形県山形市,偶然,
```

### ADIFに変換

&emsp;ADIFファイルですが、実は中身はテキストデータでできています。フォーマットを以下に示すとこんな感じ。
```ADIF_exp.adi
<EOH>
<STATION_CALLSIGN:6>JP7VAI
<OPERATOR:6>JP7VAI
<QSO_DATE:8>20230611
<TIME_ON:4>0016
<CALL:6>JA7YAB
<MODE:2>FM
<BAND:2>2m
<MY_SIG:4>POTA
<MY_SIG_INFO:7>JA-0110
<SIG_INFO>
<RST_SENT:2>59
<RST_RCVD:2>59
<COMMENT>
<EOR>
```
タグの中でコロンに続く数字は、そのタグの文字数を表しているようです。普段のログデータと違い、注意しなければならない点を示すと、

* 日付は8桁、時刻は4桁の数字で<ins>**UTC表記**</ins>
* 移動局の場合もコールサインのみ（スラッシュ以下はつけない）
* バンドは<ins>周波数ではなく波長表記</ins>（例：7MHz帯 &rarr; 40m；430MHz帯 &rarr; 70cm）
* POTAアクティベーションの場合はPOTAである旨と公園番号を明記

などです。この要件に従い、実装した例が以下です。

```python3:log2POTA.py
# log_convert.pyで加工・出力したcsvをPOTA用にADIF出力するプログラム

import csv
import datetime
import glob
import os
import pathlib
import re


def rst_convert(mode,rst_num):
    # コンテストナンバー等が含まれている場合にRSTレポートのみにする
    if mode == "AM" or "FM" or "SSB":
        if len(rst_num) > 2:
            rst_num = rst_num[:2]
        else:
            pass
    else:
        if len(rst_num) > 3:
            rst_num = rst_num[:3]
        else:
            pass
    return rst_num

# 作業フォルダ内のログCSV一覧取得
logs = glob.glob("../convert_log/prep/*.csv")
logs.sort(reverse=True)
log_path = logs[0]

# ログ読み込み
with open(f"{log_path}", mode="r", encoding="utf-8") as log:
    reader = csv.reader(log)
    qso_body = []
    # ここに自局のコールサインを入力
    my_call = "JP7VAI"
    for i, row in enumerate(reader):
        if i != 0:
            # 自局コールサイン部作成
            # 個人コールの場合、STATION_CALLSIGNとOPERATORは同じ内容
            my_callsign = f"<STATION_CALLSIGN:6>{my_call}"
            operator = f"<OPERATOR:6>{my_call}"
            
            # 日付と時刻を結合
            date_time = f"{row[1]} {row[2]}"
            # 日時の文字列を時刻に変換
            date_time = datetime.datetime.strptime(date_time, "%Y-%m-%d %H:%M")
            # JSTをUTCに変更してから文字列に再変換
            date_time = date_time.astimezone(datetime.timezone.utc).strftime("%Y%m%d%H%M")
            # 日付と時刻を分割し、ADIF仕様にする
            qso_date = f"<QSO_DATE:8>{date_time[:-4]}"
            qso_time = f"<TIME_ON:4>{date_time[-4:]}"
            
            # 相手局コールサイン
            # ポータブルの場合もコールサインのみにする
            if row[5][-2] == "/":
                callsign = row[5][:-2]
            else:
                callsign = row[5]
            callsign = f"<CALL:{len(callsign)}>{callsign}"

            # 電波型式
            mode = f"<MODE:{len(row[4])}>{row[4]}"

            # 周波数帯
            # MHz表記をメーターバンド表記に変換
            band_dict = {"1.9": "160m", "3.5": "80m", "5": "60m", "7": "40m", "10":"30m", 
                        "14": "20m", "18": "17m", "21": "15m", "24": "12m", "28": "10m", 
                        "50": "6m", "144": "2m", "430": "70cm", "1200": "23cm"}
            band = band_dict[row[3]]
            band = f"<BAND:{len(band)}>{band}"

            # 送信したRSTレポート
            rst_sent = rst_convert(row[4],row[6])
            rst_sent = f"<RST_SENT:{len(rst_sent)}>{rst_sent}"

            # 受信したRSTレポート
            rst_received = rst_convert(row[4],row[7])
            rst_received = f"<RST_RCVD:{len(rst_received)}>{rst_received}"
            
            # POTAのログであることを明記
            remarks_pota = "<MY_SIG:4>POTA"

            # 公園番号を入れるフィールドを作成
            # 実際は"****"に公園番号を入力
            park_num = "<MY_SIG_INFO:7>JA-****"

            # P2Pの場合に相手局の公園番号を入力するフィールドを作成
            park_to_park = "<SIG_INFO>"

            remarks = "<COMMENT>"
            footer = "<EOR>\n"
            qso_data = [my_callsign,operator,qso_date,qso_time,callsign,mode,band,remarks_pota,park_num,park_to_park,rst_sent,rst_received,remarks,footer]
            data_l = "\n".join(qso_data)
            qso_body.append(qso_data)
# 作成日時を付加したADIFを作成
datetime_now = datetime.datetime.now()
datetime_now = datetime_now.strftime("%Y%m%d%H%M")
# ディレクトリがなかったら新規作成、すでに存在していてもエラーは出ない
pathlib.Path("../convert_log/POTA/").mkdir(exist_ok=True)
conv_file = f"../convert_log/POTA/{datetime_now}_{my_call}@POTA_formatted.adi"

with open(conv_file, mode="w", encoding="utf-8") as output:
    header = "<EOH>\n\n"
    writer = csv.writer(output, delimiter="\n",quoting=csv.QUOTE_NONE, escapechar="\n")
    output.write(header)
    writer.writerows(qso_body)
```

&emsp;POTAの場合は1アクティベーションごとにADIFを作る必要があるため、少し手間ですが、このプログラムを用いることでだいぶ楽になりました。

## おわりに
&emsp;ADIFに変換するプログラムはまだ試行錯誤を繰り返しています。元のログにPOTAの公園番号を書いておくことで、直接コードで指定することなく、ログCSVからADIFへの変換を実現させてみたいと考えています。