99L向けのWindows/Linux Ground Station prototypeです。CREATE-ROCKET/Avi_tenkatenn_boardのUSB line protocolと、Vaultで定義した99L LoRa application packetを入力として扱います。
Control roll protocol sourceはVault commit 2a6fa974a9b7a50a9b9d574174262068e2e5b8bfに固定しています。A7はControlRollTelemetryV2、Mission link fallbackはA8です。
本projectはまず、デザイン確定より先に以下を検証することを目的とします。
rocket_fin_test.glbの姿勢・動翼表示- 2 Hz telemetryを追加500 ms遅延なしで滑らかに表示するcausal prediction
- 打上げ後0〜15 s固定graph
- application起動後からのsystem history graph
- 全packet / 全field / raw bytesの表示
- USB Serialの手動port選択
- packetごとの即時保存と
fsync - RendererのF5再読込中もUSB受信・保存を継続
- 完全offline map画像の読込み
- 離床時の枠flash + 単音2回
Full HD 1920×1080を基準にしています。最小windowは1440×810です。
- 01 ATTITUDE / 3D
- shadow無効
- multi-direction fill light
- orthographic camera
- oblique-front default view
- vehicle bounding sphereを原点へ再配置して、どの姿勢でも画面外へ出にくい構造
COPY VIEWでcamera position / quaternion / target / zoomをJSONとして取得PREDICTとRAW / HOLDを切替
- 02 FLIGHT / BIRD'S-EYE MAP
- Launcher-relative East/North
- 全raw GNSS点を保存・描画
- 1 s以上のgapを跨いで線を接続しない
- local offline image読込み
- 03 TELEMETRY / ALL VALUES
- overview
- 全known field一覧
- raw packet monitor
- 04 FLIGHT DYNAMICS / FIXED 0–15 s
- 全trackが同一x軸
- roll / tilt
- roll rate / fin rate
- fin angle
- requested torque
- airspeed
- absolute height
- static pressure
- 各track内に最新値badge
- 05 SYSTEM HISTORY / ALL RUN TIME
- logic/motor voltage
- RSSI
- pressure/temperature
- application起動後からの全期間
- source packetが途絶えた区間は線を接続しない
- EVENT / COMMAND CONSOLE
- state transition、invalid packet、USB event
- line-based command送信
全known packet fieldはALL VALUESへ表示され、全application bytesはRAW PACKETSとdisk logへ保存されます。
World座標は次です。
+X = East
+Y = Up
-Z = North
telemetryのtilt magnitude、tilt direction、v1 liftoff-relative rollから作る表示専用wrapped orientationで表示Quaternionを作ります。v1 roll自体の意味は変更せず、Control reference/deviationへ再解釈しません。
PREDICT modeでは未来packetを待ちません。
- roll: telemetryのroll rateで最大500 msだけ予測
- tilt/direction:直近2 packetからrateを推定
- 新packetとの誤差:約85 msの短いSLERP correction
- last periodic RX age >= 1.0 s:予測を停止し、最後の姿勢でfreezeしてgray表示
予測表示の補正値は保存しません。受信raw packetに加え、wrapped orientationとunwrapped Control reference/deviationを別columnにしたdecoded sampleを保存します。Control deviationを表示範囲へ合わせてwrapしません。
画面上でcameraを調整してCOPY VIEWを押すと、次のJSONがclipboardへ入ります。
{
"schema": 1,
"camera": {
"projection": "orthographic",
"position": [0, 0, 0],
"quaternion": [0, 0, 0, 1],
"up": [0, 1, 0],
"zoom": 1
},
"target": [0, 0, 0],
"model": {
"center": [0, 0, 0],
"radius": 1,
"display_frame_quaternion": [0, 0, 0, 1]
}
}production UIでcamera操作を固定する際、このJSONをdefault camera configへ移します。
VaultのCREATE/99L Ground Station/00_USBシリアル通信仕様.mdに従い、machine-readableな@ recordをparseします。
@RX usb_v=1 seq=9 board_ms=5000 dt_ms=500 rssi_present=1 rssi_raw=172 rssi_dbm=-84 valid=1 header=0xB1 len=3 error=NONE raw=B107B6
@TX usb_v=1 board_ms=6500 ok=1 kind=0 id=42 command=0x13 prefix=000004 len=11 raw=55002A1385FF0000000016 error=NONE
@FRAG usb_v=1 seq=1 board_ms=6100 reason=UNKNOWN_HEADER len=3 raw=7F12A0
@SYS usb_v=1 board_ms=12 event=BOOT
USBは115200 bps、8N1、flow controlなしです。portはVID/PIDから自動選択せず、画面で選びます。byte-based framerはLF/CRLF、任意chunk分割、1 byteずつの受信を処理し、1 lineを2048 bytesへ制限します。上限超過時と非ASCII入力は次のLFまで破棄してparser errorとして保存します。reconnect時には途中lineを破棄します。
@RX valid=1だけを既存LoRa application decoderへ渡します。valid=0、@FRAG、構造不正recordはpacket monitorとsessionへ残しますがcurrent telemetryは更新しません。SSC_NOT_INITIALIZED、GNSS_NO_FIX等のsemantic unavailableはwire packetが正しければvalidであり、0等へ補正しません。#で始まるpretty-printはtelemetry sourceにせず、ESP ROM出力等のunclassified lineもraw sessionから失いません。
USB v1への移行はbreaking changeです。旧Ground Boardの自由文pretty-printだけをtelemetryとしてparseするPC softwareとは互換ではありません。一方、PCからGround Boardへ送るg、ae、le、local、time、release、helpのcommand text形式は維持します。
詳細はdocs/usb-v1.mdを参照してください。
application起動ごとに新しいsession directoryを作ります。
Windows例:
Documents\CREATE 99L Ground Station\logs\2026-08-13T12-34-56-789Z\
内容:
session.json
events.jsonl
serial.bin
roll-telemetry.csv
events.jsonlは接続、全serial chunk/line、parsed record、parser error、command lifecycle、Renderer latency、application decoder mismatch、roll semantic recordを受信順で保存します。roll-telemetry.csvはwrapped orientation、v1 liftoff-relative unwrapped roll、V2 reference/deviation/corrective errorを別columnでexportします。各eventにはPC UTC、string化したmonotonic nanoseconds、portを付けます。serial.binはRX/TXのraw byteを保存します。各appendをfsyncし、RendererのF5中もElectron Main processが受信・保存を継続します。disk errorはそのsession中stickyなfailureとして表示し、telemetry受信を可能な限り継続します。
Rendererのpacket monitor/historyはboundedです。全履歴の基準はdisk sessionです。
Windows PowerShell:
npm install
npm run devLinux:
npm install
npm run devNixOS/Linuxではrepository rootで次を使用できます。
nix develop path:.
bash scripts/nix-deps.sh
npm test
npm run test:pty
npm run build:renderer
npm run dev未commitのflake.nixも確実に入力へ含めるため、現在のworking treeではpath:.を指定します。flakeをcommitした後は通常のnix developでも起動できます。shellはNode.js 22とElectronを固定し、NixOS上ではnpm同梱binaryの代わりにNix packageのElectronを使用します。nix-deps.shは実行bitを保持しないfilesystem上でもnpm package binaryを使えるよう、lockfile hash別のdependencyをuser cacheへ置き、repositoryのignored node_modulesからsymlinkします。既存の異なるnode_modulesは削除しません。
2回目以降とCIでは、commit済みlockfileを使ってnpm ciを実行してください。
Linuxでserial portへアクセスできない場合は、対象distributionのdialout等のgroupまたはudev ruleを設定してください。アプリはportを自動選択せず、ユーザーが選択します。
Windows portable executable:
npm run build:winLinux AppImage:
npm run build:linux
npm run verify:linux-package生成binaryはElectron/Node/Three.js/SerialPortを同梱します。利用者PCへNode.jsやnpmを要求しません。build時はlockfileから一時的なphysical production dependency treeを作ってpackするため、Nix用node_modules cache symlinkでもSerialPortのtransitive package/native addonを欠落させません。native addonはNixの新しいglibcで再buildせず、配布済みlinux-x64 glibc prebuildを使用します。verify:linux-packageはappimage-runで生成AppImageそのものを起動し、そのruntimeからserialportとnative bindingをloadして実際にport列挙まで行います。
GitHub ActionsのBuild desktop artifactsはpush、pull request、手動実行でtest後にWindows portable .exeとLinux AppImageをartifactとして出力します。
npm test共有golden vectorで既知11 packet type、length、XOR、strict USB v1 parser、framer、command lifecycle、session error/order、bounded storeを確認します。A7用vectorはGround Board repositoryとbyte-identicalで、+380 deg=760、+720 deg=1440、-720 deg=-1440、OUT_OF_RANGE、no-shortest-pathを検証します。PTY integrationは実際のOS pseudo terminalとserialportを通し、任意chunk、partial disconnect、10回reconnect、single listener、TelemetryStore/sessionまでを確認します。
GUIを開かずにport確認、timestamp付きcapture、console command送信、session保存を行えます。serial owner、byte framer、strict parser、session writerはElectron Main processと同じ実装です。同一portをGUIや別monitorと同時に開かないでください。
npm run cli -- --help
npm run cli -- --list
npm run cli -- --port /dev/ttyUSB0 --duration-ms 10000 --send "help"
npm run cli -- --port /dev/ttyUSB0 --duration-ms 10000 --session-dir /tmp/99l-cliCH340のDTR/RTS状態によりESP32がdownload bootへ入っている場合は、同一processでportを保持したままrun modeへresetできます。
npm run cli -- --port /dev/ttyUSB0 --reset-to-run --duration-ms 10000 --send "help"--reset-to-runはDTRをdeassertしたままRTSを100 ms pulseし、送信前にESP32を通常bootへ戻します。通常のread-only captureでは指定しないため、CLI接続だけで基板を自動resetしません。
--sendはport open直後の取こぼしを避けるため既定で250 ms待ってから、入力文字列をそのままGround Boardへ送ります。必要なら--settle-msで変更できます。actuator commandの安全確認はoperator側で行います。接続中はstdinからもcommandを送信できます。
- PASS (host): USB v1共通golden vector 22 record、strict parser、byte framer、既存application decoder、command lifecycle、session/store unit test。
- PASS (PTY service path): pseudo terminal上の実
serialportからMain process service、parser、TelemetryStore、session writerまでを通し、10回reconnect後もlistener重複なし。PARTIAL (PTY full path): このtest単体はElectron IPC / preload / Renderer paintを起動していません。この区間は下記development GUI / AppImage実port試験で別途確認しました。 - PASS (build/package):
nix develop path:.でtest、PTY、renderer、Linux AppImage buildを実行しました。生成AppImage自身のFHS runtimeでpackagedserialportloadと7 port列挙まで成功しています。実機試験artifactは132,502,094 bytes、SHA-2568cd4dd13b529a375b99193ef9449fc1f4328619b4d1d467b5bee8d5ffd999435です。 - PASS (development GUI):
/dev/ttyUSB0を画面選択と同じUI経路で開き、151@RX(A0 149 / B0 2)をGUIとsessionへ151件ずつ保存しました。invalid、parser error、duplicate、sequence gapは0です。受信→storeは最小3 / 平均4.477 / p95 6 / p99 7 / 最大8 ms、受信→paintは9 / 25.470 / 29 / 37 / 48 msでした。10回disconnect/connect後もlistenerは各1、切断中commandは無効、RSSは773860→772168 KiB、threadは104→104です。証跡は/tmp/99l_usb_v1_9JOjDs71/gui_dev_actual_v2/cdp.jsonです。 - PASS (AppImage USB/parser/session/GUI): 600.133秒で
@RX/ disk session / GUIを816件ずつ一致させ、parser error、duplicate、USB sequence gapは0でした。受信→storeは最小2 / 平均3.675 / p95 5 / p99 6 / 最大14 ms、受信→paintは9 / 26.993 / 30 / 45 / 63 msで、engineering targetのp95 50/100 ms、paint最大200 msを満たしました。invalid 1件はraw付きpacket monitor/sessionだけへ残り、telemetry更新に使っていません。この10分区間はMission CANが停止していたため、USB/App/RF受信経路の検証であり3基板telemetry E2Eには数えません。証跡は/tmp/99l_usb_v1_9JOjDs71/gui_appimage_final_8cd_10min/combined_analysis.jsonです。 - FAIL (10分RF rate target): A0は目標約1200件に対して816件(valid 815 / invalid 1、1.361 Hz)でした。1秒超gap 97、5秒超gap 5、RSSI最小-139 / 平均-131.705 / 最大-125 dBmで、weak-RF受信欠落が支配的です。USB sequence gap 0、session/GUI count一致のため、この不足をparserやRenderer dropへ置き換えていません。
- PARTIAL (10分memory): crash、freeze、listener増加はなく、threadは111〜112でした。process RSSは969896→978768 KiB、先頭20 sample平均969910.6→末尾20 sample平均977612.6 KiBであり、10分を超える長期plateauまでは立証していません。
- PARTIAL (A0-only command): 55 UI試行(
g 0x7F28 /le27)に対して@TX55、final 50でした。gfinal 27件はUI→final最小527 / 平均721.556 / p95 989 / p99・最大998 ms、lefinal 23件は531 / 740.826 / 986 / 1001 / 1001 msです。final outcomeはそれぞれRejected/NotSupported、Rejected/InvalidStateで、TX→final平均は225.667 / 225.565 msです。RFでfinalを失った5件をfailureへ推測せず、auto retry 0、operatorがID 49 / 34 / 61 / 65 / 67をmanual releaseしました。ID 67の@SYSはport open途中lineへ重なってunclassified保存され、# releasedは確認したもののapp側で成功相関していません。各50回の計画数は未達です。集計は/tmp/99l_usb_v1_9JOjDs71/a0_command_aggregate.jsonです。 - PARTIAL (A0+B1 command): 10 UI試行(
g5 /le5)に対して@TX9、final 7でした。成功したg3件のUI→finalは最小827 / 平均1035.333 / p95・最大1294 ms、le4件は711 / 1215.5 / 2703 / 2703 msです。2.703秒finalを1秒でtimeout/retryせず同じtransactionへ相関しました。残りはRF送信前boundary timeout 1件と@TX後final欠落2件で、operator release以外の自動処理はありません。集計は/tmp/99l_usb_v1_9JOjDs71/mixed_command_aggregate.jsonです。 - PASS (TimeRequest/TimeResponse): B1 ID 7をUIへ表示し、
time 7 ...から@TX kind=4 id=7 command=0x02 ok=1を生成しました。PC側でIDを再採番せず、送信前発行済みのB1 ID 8以後はA0 128件が継続しB1は停止しました。証跡は/tmp/99l_usb_v1_9JOjDs71/gui_appimage_time_response_current/です。 - PASS (final three-board passive) / FAIL→PASS (safe command): Mission productionをclean build/uploadしてCAN正常を確認した後、最終productionの69.565秒sessionはA0 129件、valid 129、session/GUI一致、parser error、duplicate、sequence gap 0でした。最初の
g 0x7Fは@TX ok=1後のfinalをRFで失いましたが自動再送せず、別sessionでoperatorが再試行したg 0x7Fは713 ms、leは950 msで期待したfinalへ到達しました。証跡は/tmp/99l_usb_v1_9JOjDs71/gui_appimage_final_production_65s/、gui_appimage_final_g_retry1/、gui_appimage_final_le_retry1/です。 - PASS (GUI reconnect) / PARTIAL (board reset) / BLOCKED (physical unplug): GUI disconnect/connect 10回はcrash、二重listener、二重packet表示なしでした。Ground Board resetも10回実行し最終valid A0復帰を確認しましたが、全10回をGUI接続中に観測してはいません。物理USB抜去は自動化環境から実施していません。
Serial hardwareがなくてもSYNTHETICで以下を確認できます。
- CommandReceive
- LiftoffDetection
- EngineBurn
- Control
- Descent
- 3D姿勢とfin
- graph
- map
- liftoff flash / double beep
F9でも離床alertを手動試験できます。
requested torquescaleはVault上でsimulation確定待ちのため、decoderではTEMPORARY_SCALEと明示します。- 実機上のfin正方向とGLB正方向の対応は後で符号確認が必要です。
- voltageは、それを含むpacketが届いた時点だけsampleを追加します。Flight packetに電圧が含まれない区間を補間・捏造しません。
- A7は9 byte
ControlRollTelemetryV2、Mission link fallbackはA8/24 byteです。旧A7 fallback packetをv2 Control rollとして再解釈せず、length/schema不一致でrejectします。 - offline map画像のpixelとENU座標の厳密なgeoreference metadataは今後追加します。現prototypeは画像をpanel背景として読込みます。
- current
CommandReceivepackingはVaultの22 byte v1を前提にしています。変更時はgolden vectorとdecoderを同時更新してください。 - serial sessionは順序と即時永続化を優先してMain processで同期write/fsyncします。高packet rateでlatency targetを超える場合は、bounded utility process化を別途評価します。
- disk write failureは同じsession内で自動的に「回復済み」へ戻しません。新しいsessionで明示的に再試行します。
- B0 Accepted後もpendingを維持し、Completed/Rejected/Failedでのみfinalにします。1秒timeout、自動retry、自動releaseは行いません。
- Ground BoardがUSBをresetした
@SYS event=BOOTではcurrent telemetryとsequence基準をclearしますが、pending commandを成功/失敗へ推測しません。 - Ground BoardがE220へ書く前にboundary timeoutした場合、USB v1には対応するmachine-readable failure recordがありません。
#をtelemetry sourceにしないためcommandはUSB_WRITTENのまま保持し、operatorが基板状態を確認して解決します。自動failure/retryへは変換しません。