
無重力の宙の電波を読む記事④:AX.25 UIフレームを1バイトずつ読む実践編 RSP-03の rsp03.ksy を例に、デコーダの構造を見ていく
はじめに|今回は実際の rsp03.ksy を読む
前回の記事②-3では、KISS、G3RUH、TNC、SatNOGS、KSYの境界を整理しました。
そこで確認したのは、KSYが電波そのものを復調するものではない、ということでした。
SatNOGS Clientでは、衛星から届いた電波は、アンテナで受信され、SDRでIQデータとして取り込まれます。
その後、Raspberry PiやPC上のSatNOGS ClientがGNU Radio Flowgraphを使って、復調、NRZI処理、G3RUHデスクランブル、フレーム同期などを行います。
つまり、Kaitai Structの処理が始まる時点では、すでに電波そのものではなく、AX.25フレーム、またはそのPayloadとして扱えるバイト列になっています。
では、そのAX.25フレームを、実際のデコーダではどのように読むのでしょうか。
今回は、私がRSP-03向けに作成した rsp03.ksy を例に、AX.25 UIフレームからPayloadへ入り、さらにPacket1、Packet2、Packet3へ分岐していく構造を見ていきます。
この記事は、AX.25の仕様を完全に説明するものではありません。
むしろ、SatNOGS向けデコーダを作るときに、実際のKSYファイルの中でどのように構造を切り出していったのかを、実装例として整理するものです。
1. rsp03.ksy 全体の考え方
RSP-03のGMSKテレメトリは、AX.25 UIフレームとして扱います。
そのため、rsp03.ksy では大きく次のような流れでデータを読みます。
AX.25 Frame
↓
AX.25 Header
↓
RSP-03 Payload
↓
RSP-03 fixed header
↓
time
↓
packet_type
↓
Packet1 / Packet2 / Packet3
KSYでやっていることを一言で言えば、
AX.25のバイト列を、RSP-03のテレメトリ項目へ変換する
ということです。
ここで重要なのは、KSYは復調処理を書いているのではない、という点です。
GMSK復調、NRZI処理、G3RUHデスクランブルなどは、SatNOGSのFlowgraph側の仕事です。
rsp03.ksy が担当するのは、その後に得られたAX.25フレームとPayloadを、意味のあるフィールドに分解する部分です。
2. meta でデコーダの名前と前提を決める
rsp03.ksy の先頭では、まずこのファイルが何のデコーダなのかを定義します。
meta:
id: rsp03
title: RSP-03 GMSK decoder
endian: le
ここで id はデコーダの識別名、title は説明用のタイトルです。
endian: le は、基本的にリトルエンディアンで値を読むという指定です。
RSP-03のテレメトリでは、複数バイトの数値が出てくるため、エンディアンの指定は重要です。
また、doc-ref にはRSP-03のHK Beacon Format仕様書への参照を入れています。
これは、後から見返したときに「このKSYはどの仕様書に基づいて書いたのか」を明確にするためです。
デコーダは一度書いて終わりではなく、レビューや修正を受けながら育っていきます。
そのため、仕様書との対応関係を残しておくことは、とても大事だと感じました。
3. SatNOGSで表示したい項目を :field で整理する
rsp03.ksy では、doc の中に多くの :field を書いています。
たとえば、次のような項目です。
dest_callsign
src_callsign
src_ssid
dest_ssid
ctl
pid
packet_type
telemetry_id
cobc_boot_count
cobc_temperature
satellite_operation_mode
antenna_deployment_status
これは、単にKSY内部で値を読むだけでなく、SatNOGS側でテレメトリ項目として扱えるようにするための対応です。
ここで面白いのは、フィールド名が単なるプログラム変数ではなく、後から人間が見るテレメトリ項目にもつながっていくことです。
つまり、KSYを書く作業は、単にバイナリを読む作業ではありません。
どの値を、どの名前で、どのように見せるか
を決める作業でもあります。
RSP-03では項目数が多いため、仕様書※の番号に合わせて no005_telemetry_id のような名前を付けています。
※RSP-03RSP-03 HK Beacon Format(JP)
https://rsp03.rymansat.com/assets/pdfs/RSP-03_HK_Beacon_Format(JP)_Rev1R2.pdf
これは少し長い名前になりますが、仕様書と照合しやすいというメリットがあります。
正直、短時間でKSYを作るのにKSY記述の見やすさより、正確性、デバッグのしやすさを求めました。
4. 先頭でCWとAX.25を切り替える
rsp03.ksy では、最初に first_tag を読み、先頭の値によって処理を切り替える構造にしています。
seq:
- id: ax25_frame
type:
switch-on: first_tag
cases:
0x47: cw_g # 'G'
0x48: cw_h # 'H'
0x49: cw_i # 'I'
_: ax25_frame_body
instances:
first_tag:
pos: 0
type: u1
ここでは、先頭1バイトが G、H、I の場合はCW系として扱い、それ以外はAX.25フレームとして扱う、という考え方にしています。
0x47 はASCIIの G、0x48 は H、0x49 は I です。
このように switch-on を使うと、1つのKSYファイルの中で複数の形式を切り替えて扱うことができます。
RSP-03は複数の通信方式やフォーマットを持つため、最初に「これはどの形式か」を見分ける入口を作る必要がありました。
5. AX.25フレーム本体を読む
AX.25として読む場合、ax25_frame_body に入ります。
構造は非常に素直です。
ax25_frame_body:
seq:
- id: ax25_header
type: ax25_header
- id: payload
type: rsp03_payload
つまり、
AX.25 Header
RSP-03 Payload
の順に読む、という設計です。
ここが、②-3で整理した「KSYはAX.25以降を読む」という話の実装部分です。
前回は考え方の整理でした。
今回は、それが実際のKSYファイルの中でこのような形になっている、ということを確認できます。
6. AX.25 Headerを1バイトずつ分解する
AX.25ヘッダは、次のように定義しています。
ax25_header:
seq:
- id: dest_callsign_raw
type: callsign_raw
- id: dest_ssid_raw
type: ssid_mask
- id: src_callsign_raw
type: callsign_raw
- id: src_ssid_raw
type: ssid_mask
- id: ctl
type: u1
- id: pid
type: u1
ここでは、次の順番で読んでいます。
宛先コールサイン
宛先SSID
送信元コールサイン
送信元SSID
Control
PID
AX.25 UIフレームでは、Controlが 0x03、PIDが 0xF0 になることがあります。
つまり、AX.25ヘッダの最後にある 03 F0 が、Payloadへ入る直前の目印になります。
RSP-03のPayloadを正しく読むためには、この 03 F0 の位置を正しく見つけることが重要です。
1バイトずれると、Payloadの先頭がずれます。
Payloadの先頭がずれると、packet_typeも、時刻も、温度も、電圧も、すべて別の値として読まれてしまいます。
7. AX.25コールサインはそのままASCIIではない
AX.25で最初に混乱しやすいのが、コールサインの読み方です。
AX.25のコールサインは、そのままASCII文字として入っているわけではありません。
各文字が1ビット左にシフトされた形で格納されています。
そのため、rsp03.ksy では次のように process: ror(1) を使っています。
callsign_raw:
seq:
- id: callsign_ror
process: ror(1)
size: 6
type: callsign
ror(1) は、各バイトを1ビット右回転して読む処理です。
これにより、AX.25で左シフトされていたコールサインを、人間が読めるASCII文字列に戻しています。
さらに、RSP-03では想定されるコールサインとして、次のような値を検証対象にしています。
valid:
any-of:
- '"JS1YOY"'
- '"JS1***"'
ここでは、実運用上のコールサイン候補に合わせて、正しく読めているかを確認できるようにしています。
この部分は、AX.25フレームをKSYで扱ううえで非常に重要な実装例です。
8. SSIDを読む
AX.25のアドレス部には、コールサインだけでなくSSIDも含まれます。
rsp03.ksy では、SSIDを次のように取り出しています。
ssid_mask:
seq:
- id: ssid_mask
type: u1
instances:
ssid:
value: (ssid_mask & 0x1f) >> 1
ここでは、SSID情報を含む1バイトから、必要なbitだけを取り出しています。
AX.25のSSID byteには、SSID以外のbitも含まれます。
そのため、単純に1バイトをそのまま数値として読むのではなく、bit maskを使って必要な部分だけを抜き出しています。
このように、KSYでは単に順番に読むだけでなく、instances を使って「読んだ値から計算した値」を定義できます。
9. RSP-03 Payloadへ入る
AX.25ヘッダを読み終えると、次はRSP-03固有のPayloadです。
rsp03.ksy では、Payloadの先頭を次のように読んでいます。
rsp03_payload:
seq:
- id: no001_header
type: u4
- id: no001_header_1
type: u1
- id: no002_time
type: u4
- id: no003_time
type: u2
- id: no004_packet_type
type: no004_packet_type_t
ここで最初に出てくるのが、RSP-03 Payloadの固定ヘッダです。
RSP-03では、Packet1、Packet2、Packet3で、それぞれ既知の5バイトヘッダを持つ構造になっています。
コメントでは、次のような値を想定しています。
0x0018AD8001
0x00184A8001
0x0018DF8001
この5バイトヘッダを読むために、KSYでは u4 と u1 に分けて読んでいます。
Kaitai Structでは、5バイト整数をそのまま扱うよりも、4バイトと1バイトに分けて読み、後で instances で再構成する方が扱いやすい場合があります。
10. 5バイトヘッダを再構成する
rsp03.ksy では、Payload先頭の5バイトヘッダを no001_header_u40 として再構成しています。
no001_header_u40:
value: >
((no001_header & 0xff) << 32) +
(((no001_header >> 8) & 0xff) << 24) +
(((no001_header >> 16) & 0xff) << 16) +
(((no001_header >> 24) & 0xff) << 8) +
no001_header_1
ここは、実装上かなり工夫した部分です。
meta では endian: le としているため、4バイト値の読み方はリトルエンディアンになります。
一方で、ヘッダとして確認したい値は、仕様書上の並びで5バイトのまとまりとして見たい。
そのため、一度 u4 と u1 で読み込んだ値を、bit shiftを使って5バイトの値として再構成しています。
そのうえで、次のようにヘッダが想定値のどれかに一致するかを確認しています。
header_valid:
value: >
(no001_header_u40 == 0x0018AD8001 or
no001_header_u40 == 0x00184A8001 or
no001_header_u40 == 0x0018DF8001)
これは、Payloadの入口が正しく読めているかを確認するためのチェックポイントになります。
もしここが一致しなければ、AX.25ヘッダの切り出し位置がずれている、Payload先頭を間違えている、あるいは想定と違うパケットを読んでいる可能性があります。
11. packet_typeでPacket1/2/3を切り替える
RSP-03のPayloadでは、packet_typeによって後続の構造が変わります。
rsp03.ksy では、次のように switch-on を使って切り替えています。
no004_packet_type_t:
seq:
- id: packet_type
type: u1
- id: type_check
type:
switch-on: packet_type
cases:
0x01: packet1
0x02: packet2
0x03: packet3
ここが、RSP-03デコーダの中心部分です。
packet_typeが 0x01 ならPacket1として読む。
packet_typeが 0x02 ならPacket2として読む。
packet_typeが 0x03 ならPacket3として読む。
このようにすることで、1つのKSYファイルで複数種類のテレメトリ構造を扱えます。
衛星のテレメトリでは、HKデータ、ミッションデータ、姿勢制御データなど、種類によってPayload構造が異なることがあります。
そのような場合に、switch-on は非常に便利です。
12. Packet1:衛星の基本状態を読む
Packet1では、COBCの起動回数、経過時間、衛星システム時刻、温度、運用モード、アンテナ展開状態などを読みます。
たとえば、先頭付近は次のような構造です。
packet1:
seq:
- id: no005_telemetry_id
type: u2
- id: no006_cobc_boot_count
type: u4
- id: no007_cobc_elapsed_time
doc: sec
type: u8
- id: no008_satellite_system_time
doc: ms
type: u8
- id: no009_cobc_temperature
doc: mc
type: s1
- id: no010_satellite_operation_mode
type: u1
- id: no011_antenna_deployment_status
type: u1
ここで、u1、u2、u4、u8 は符号なし整数、s1 は符号付き1バイト整数です。
また、doc: sec や doc: ms のように単位をコメントとして残しています。
このようにしておくと、後で見たときに、その値が秒なのか、ミリ秒なのか、温度なのかが分かりやすくなります。
13. bit fieldをflagに分解する
RSP-03のテレメトリには、1バイトの中に複数の状態をbitで持つ項目があります。
たとえば、アンテナ展開状態は1バイトのbit fieldとして扱われます。
rsp03.ksy では、このような状態を instances でflagに分解しています。
no011_flag_0:
value: ( no011_antenna_deployment_status & 1) != 0
no011_flag_1:
value: ((no011_antenna_deployment_status >> 1) & 1) != 0
no011_flag_2:
value: ((no011_antenna_deployment_status >> 2) & 1) != 0
no011_flag_3:
value: ((no011_antenna_deployment_status >> 3) & 1) != 0
このように書くことで、1バイトの値をそのまま見るだけでなく、bitごとの意味に分けて確認できます。
衛星のステータス情報では、このbit fieldがよく出てきます。
たとえば、
bit0 = +Xアンテナ
bit1 = -Xアンテナ
bit2 = +Yアンテナ
bit3 = -Yアンテナ
のように、1つのバイトに複数の状態が詰め込まれている場合です。
KSYでは、こうしたbitごとの状態も、読みやすいフィールドとして取り出せます。
14. Packet2とPacket3も同じ考え方で読む
Packet2では、ミッション系のコマンド実行結果、STT状態、取得座標、画像撮影時刻、最近のコマンド履歴などを読みます。
Packet3では、姿勢制御モード、リアクションホイール、磁気トルカ、IMU、GNSS、太陽センサなど、より姿勢制御やADCSに近い項目を読みます。
構造は大きく違いますが、考え方は同じです。
仕様書の順番に従う
各項目の型を決める
単位をdocに残す
enumで意味を付ける
bit fieldはinstancesでflag化する
この繰り返しです。
最初は長いファイルに見えますが、実際には仕様書の表を1行ずつKSYに写しているような作業です。
ただし、1バイトでもずれると後続のすべてが崩れるため、Kaitai IDEやサンプルデータで確認しながら進める必要があります。
15. enumで数字に意味を付ける
rsp03.ksy では、単なる数値をそのまま表示するだけでなく、enumを使って意味を付けています。
たとえば、衛星の運用モードやコマンド結果、STT状態、姿勢制御モードなどです。
数値だけを見ると、
0
1
2
3
にしか見えません。
しかしenumを使うと、
standby
mission
safe
commissioning
のように、人間が理解しやすい状態として扱えます。
テレメトリは、人間が衛星の状態を判断するための情報です。
そのため、単に数値を読むだけでなく、意味を付けることが大切です。
16. この実装で学んだこと
RSP-03の rsp03.ksy を作成して学んだことは、KSYは単なる変換ファイルではないということです。
KSYには、衛星の通信仕様、テレメトリ設計、運用で見たい項目、SatNOGSで共有したい情報がすべて反映されます。
AX.25ヘッダを読む部分では、衛星通信のプロトコル理解が必要になります。
Payloadを読む部分では、衛星のHK仕様書を正確に読む力が必要になります。
Packet1、Packet2、Packet3を切り替える部分では、データ構造を整理する力が必要になります。
bit fieldやenumでは、衛星の状態を人間に分かりやすく見せる工夫が必要になります。
つまり、KSYを書く作業は、衛星のバイト列を「人間が読める言葉」に翻訳する作業でした。
まとめ|AX.25からPayloadへ、Payloadから衛星の状態へ
この記事では、RSP-03向けに作成した rsp03.ksy を例に、AX.25 UIフレームからPayloadを読み、packet_typeによってPacket1、Packet2、Packet3へ分岐する構造を見てきました。
全体の流れは、次のように整理できます。
AX.25 Header
↓
Destination callsign
Source callsign
SSID
Control
PID
↓
RSP-03 Payload
↓
5-byte fixed header
time
packet_type
↓
Packet1 / Packet2 / Packet3
↓
Telemetry fields
②-3では、KSYがどこから処理を始めるのかを整理しました。
②-4では、その境界の先にある実際の構造を見ました。
AX.25の 03 F0 の後ろには、ただのバイト列ではなく、衛星の状態が詰まっています。
そのバイト列を、仕様書に従って1つずつ意味のある項目へ変換する。
それが、Kaitai Structで衛星デコーダを書くということでした。
次回は、この rsp03.ksy をSatNOGSのデコーダとして登録するために、GitLab上でどのように整理し、レビューを受け、修正していったのかを振り返ります。