RON-V 連載リンク
← 前の記事:#14 — 統合前編:L0/L1/L2と自動配送
→ 次の記事:#16 — Google Sheets・Looker Studioの準備
L0〜L4の定義追記(2026-10-25)
本文中のコード・試験結果は執筆当時の記録として残しています。画像AI・音声を関連付ける現在の定義は次のとおりです。
| レベル | 残す内容 |
|---|---|
| L0 | Meshtastic向けの23バイト短報。画像AIの候補通知は任意設定 |
| L1 | 個別の観測・撮影/録音/認識の状態と、詳細への参照 |
| L2 | 一定時間の観測件数・分類別候補などの集計 |
| L3 | 画像AIの詳細、録音情報、文字起こし全文 |
| L4 | 元画像JPEG、元音声WAV、LTEのraw logなど |
L0の警報欄は1バイト=8ビットです。新しいversion=2の定義は、倒木 0x01、冠水 0x02、煙 0x04、道路上の障害物 0x08、路面の穴 0x10。残り3ビットは未割り当てです。0x03は倒木と冠水の組み合わせで、煙の番号ではありません。0x00はL0上では未判定で、安全を示しません。 候補ありでは元イベントの時刻・位置を使い、現在地と混同しません。
発信元はnode_uid、根拠は画像・音声などです。「処理成功」「災害の候補」「人の確認済み」を分けます。初期の確認状態はunreviewed。画像AIの検出0件を「危険なし」とせず、音声の否定も全文へ残します。人が補足・訂正する画面と、音声からの災害自動分類は今後の実装です。
10月25日にL1の関連付け・L2の集計・音声L3/L4の転送処理をコードへ追加しました。新連携の実機確認はこれからです。音声転送とL0候補通知は初期設定では無効で、個別に有効化します。従来のL0 version=1へ、この分類の意味を遡って付けません。
Looker Studioで使用中のSheets summaryはRF測定から直接出力する既存の10列で、L2とは別です。今回の追加で列名・順序・値の意味、既存Grafanaの参照項目は変えません。L1のMesh送信、写真・音声のMesh転送、自動公開は実装していません。
詳細と設定手順:データレベルの定義(リポジトリの閲覧権限が必要です)。
[!NOTE]
2026-09-23 現行GitHub同期メモ
#15執筆時点ではL3/L4を将来拡張としていましたが、現在のGitHubmainでは実装済みです。LTEはapp/integration/lte_evidence.pyにより L3=解析済みJSON / L4=cell_searchraw log、Vehicle Cameraはvehicle_evidence.pyにより L3=manifest JSON / L4=JPEG evidence を生成します。rclone_delivery_worker.pyがL3/L4をsync_deliveries経由で非同期転送します。したがって本文中の「L3/L4は未実装」は当時の開発段階を示す記録として読み、現在仕様はこの注記とGitHubmainを優先してください。
はじめに
前編では、RON-Vの観測データを、
L0
L1
L2
へ整理し、
sync_objects
sync_deliveries
sync_cursors
を用いた同期・配送管理を実装しました。
さらに、
ronv-controller.timer
↓
Integration Cycle
↓
L0 / L1 / L2
↓
Delivery登録
↓
ronv-delivery.timer
↓
Meshtastic
↓
920MHz
↓
real ACK
までを自動化しました。
前編終了時点では、一晩の継続運転とControllerの約600秒周期化まで確認できています。
後編では、ここからRON-V統合の最終仕上げを行います。
今回確認するのは、
統合状態確認コマンド
↓
L3 / L4の設計境界
↓
最終バックアップ
↓
reboot前検査
↓
Raspberry Pi reboot
↓
service / timer自動復帰
↓
reboot後の新しいL0
↓
Delivery自動処理
↓
Meshtastic real ACK
です。
最終的に、
電源投入
↓
RON-V自動起動
↓
観測
↓
構造化
↓
配送
↓
920MHz
↓
real ACK
まで人手を介さず復帰できれば、RON-V #15の統合完成とします。
現行statusの追加点
現在の app/integration/status.py / bin/ronv-status は、当時のVehicle/Web/Mesh/Controller/Deliveryに加え、RF測定の最新状態と ronv-rf-measure.timer も確認対象に含みます。
154. 統合状態確認コマンドを作る
これまでRON-Vの確認では、
systemctl
sqlite3
curl
journalctl
を個別に実行していました。
しかし、統合後は確認対象が増えています。
例えば、
Vehicle
Web
Mesh
gpsd
Grafana
Controller
Delivery
L0
L1
L2
sync_deliveries
mesh_messages
SQLite integrity
があります。
そこで、RON-V全体を一度に確認する、
ronv-status
を作ります。
このコマンドは、
read only
とします。
つまり、
DB更新しない
USBを開かない
serviceをrestartしない
状態確認専用コマンドです。
155. status.pyを作る
ファイル:
app/integration/status.py
を作ります。
既存ファイルがある場合に備えて、先にバックアップします。
cd /home/ronv/ronv
STAMP=$(date +%Y%m%d_%H%M%S)
if [ -f app/integration/status.py ]; then
cp -a \
app/integration/status.py \
"app/integration/status.py.bak_${STAMP}"
fi
echo "STAMP=$STAMP"
156. status.py
cd /home/ronv/ronv
cat > app/integration/status.py <<'PY'
from __future__ import annotations
import json
import sqlite3
import subprocess
import urllib.error
import urllib.request
from app.common.config import load_config
from app.integration.l0_packet import unpack_l0
def systemctl_value(
unit: str,
command: str,
) -> str:
try:
result = subprocess.run(
[
"systemctl",
command,
unit,
],
capture_output=True,
text=True,
timeout=3,
check=False,
)
value = (
result.stdout.strip()
or result.stderr.strip()
or "unknown"
)
return value
except subprocess.TimeoutExpired:
return "timeout"
except Exception as exc:
return (
"error:"
+ type(exc).__name__
)
def systemd_result(
unit: str,
) -> str:
try:
result = subprocess.run(
[
"systemctl",
"show",
unit,
"--property=Result",
"--value",
],
capture_output=True,
text=True,
timeout=3,
check=False,
)
return (
result.stdout.strip()
or "unknown"
)
except subprocess.TimeoutExpired:
return "timeout"
except Exception as exc:
return (
"error:"
+ type(exc).__name__
)
def web_health(
host: str,
port: int,
) -> str:
if host in (
"0.0.0.0",
"::",
):
host = "127.0.0.1"
url = (
f"http://{host}:{port}/health"
)
try:
with urllib.request.urlopen(
url,
timeout=3,
) as response:
body = (
response
.read(512)
.decode(
"utf-8",
errors="replace",
)
.strip()
)
return (
f"HTTP {response.status} "
f"{body}"
)
except urllib.error.URLError as exc:
return (
"ERROR "
+ str(exc.reason)
)
except Exception as exc:
return (
"ERROR "
+ type(exc).__name__
)
def mask_node_id(
value: str | None,
) -> str:
if not value:
return "-"
value = str(value)
if len(value) <= 5:
return "***"
return (
value[:3]
+ "***"
+ value[-2:]
)
def open_database_ro(
db_path: str,
) -> sqlite3.Connection:
con = sqlite3.connect(
f"file:{db_path}?mode=ro",
uri=True,
timeout=5,
)
con.row_factory = sqlite3.Row
return con
def print_system_status(
config: dict,
) -> None:
print(
"===== SYSTEM ====="
)
continuous_units = (
"ronv-vehicle.service",
"ronv-web.service",
"ronv-mesh.service",
"gpsd.socket",
"grafana-server.service",
)
for unit in continuous_units:
active = systemctl_value(
unit,
"is-active",
)
enabled = systemctl_value(
unit,
"is-enabled",
)
print(
f"{unit:28s}"
f" active={active:10s}"
f" enabled={enabled}"
)
print()
timer_units = (
"ronv-controller.timer",
"ronv-delivery.timer",
)
for unit in timer_units:
active = systemctl_value(
unit,
"is-active",
)
enabled = systemctl_value(
unit,
"is-enabled",
)
print(
f"{unit:28s}"
f" active={active:10s}"
f" enabled={enabled}"
)
print()
oneshot_units = (
"ronv-controller.service",
"ronv-delivery.service",
)
for unit in oneshot_units:
active = systemctl_value(
unit,
"is-active",
)
result = systemd_result(
unit
)
print(
f"{unit:28s}"
f" active={active:10s}"
f" last_result={result}"
)
def print_config(
config: dict,
) -> None:
print(
"===== CONFIG ====="
)
print(
"project :",
config[
"project"
][
"name"
],
)
print(
"version :",
config[
"project"
][
"version"
],
)
print(
"cycle_sec :",
config[
"controller"
][
"cycle_sec"
],
)
print(
"integration.node_id :",
config.get(
"integration",
{},
).get(
"node_id",
"-",
),
)
print(
"mesh_destination :",
mask_node_id(
config.get(
"integration",
{},
).get(
"mesh_destination"
)
),
)
print(
"sqlite_db :",
config[
"paths"
][
"sqlite_db"
],
)
def print_database_status(
config: dict,
) -> None:
db_path = (
config[
"paths"
][
"sqlite_db"
]
)
con = open_database_ro(
db_path
)
try:
print(
"===== DATABASE ====="
)
integrity = con.execute(
"PRAGMA integrity_check;"
).fetchone()[0]
print(
"integrity_check :",
integrity,
)
print()
print(
"===== L0 LATEST ====="
)
row = con.execute(
"""
SELECT
id,
object_uid,
created_at_jst,
sequence_u16,
latitude_deg,
longitude_deg,
network_flags_u8,
packet_bytes,
packet_hex
FROM l0_beacons
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
if row is None:
print(
"no L0 beacon"
)
else:
crc_ok = False
try:
packet = bytes.fromhex(
row[
"packet_hex"
]
)
decoded = unpack_l0(
packet
)
crc_ok = bool(
decoded[
"crc_ok"
]
)
except Exception:
crc_ok = False
position = (
"yes"
if (
row[
"latitude_deg"
]
is not None
and
row[
"longitude_deg"
]
is not None
)
else "no"
)
print(
"id :",
row["id"],
)
print(
"object_uid :",
row["object_uid"],
)
print(
"created_at_jst :",
row["created_at_jst"],
)
print(
"sequence :",
row["sequence_u16"],
)
print(
"position_available :",
position,
)
print(
"network_flags :",
row[
"network_flags_u8"
],
)
print(
"packet_bytes :",
row[
"packet_bytes"
],
)
print(
"crc_ok :",
crc_ok,
)
print()
print(
"===== L1 COUNTS ====="
)
rows = con.execute(
"""
SELECT
data_type,
COUNT(*) AS rows
FROM sync_objects
WHERE level=1
GROUP BY data_type
ORDER BY data_type
"""
).fetchall()
if not rows:
print(
"no L1 objects"
)
else:
for item in rows:
print(
f"{item['data_type']:24s}:",
item["rows"],
)
print()
print(
"===== L2 LATEST ====="
)
row = con.execute(
"""
SELECT
object_uid,
created_at_jst,
bytes,
payload_json
FROM sync_objects
WHERE level=2
AND data_type='cycle_summary'
ORDER BY created_at_jst DESC
LIMIT 1
"""
).fetchone()
if row is None:
print(
"no L2 summary"
)
else:
print(
"object_uid :",
row["object_uid"],
)
print(
"created_at_jst :",
row["created_at_jst"],
)
print(
"bytes :",
row["bytes"],
)
try:
payload = json.loads(
row[
"payload_json"
]
)
window = payload.get(
"window",
{},
)
vehicle = payload.get(
"vehicle",
{},
)
lte = payload.get(
"lte",
{},
)
mesh = payload.get(
"mesh",
{},
)
print(
"cycle_sec :",
window.get(
"cycle_sec",
"-",
),
)
print(
"vehicle.count :",
vehicle.get(
"count",
"-",
),
)
print(
"vehicle.shock :",
vehicle.get(
"shock_count",
"-",
),
)
print(
"lte.count :",
lte.get(
"count",
"-",
),
)
print(
"lte.unique_pci :",
lte.get(
"unique_pci_count",
"-",
),
)
print(
"mesh.tx_count :",
mesh.get(
"tx_count",
"-",
),
)
print(
"mesh.tx_acked :",
mesh.get(
"tx_acked",
"-",
),
)
except Exception as exc:
print(
"payload_parse :",
"ERROR",
type(exc).__name__,
)
print()
print(
"===== DELIVERY LATEST ====="
)
row = con.execute(
"""
SELECT
id,
object_uid,
transport,
state,
attempt_count,
created_at_jst,
acked_at_jst,
remote_id,
last_latency_ms,
last_error
FROM sync_deliveries
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
if row is None:
print(
"no delivery"
)
else:
print(
"id :",
row["id"],
)
print(
"object_uid :",
row["object_uid"],
)
print(
"transport :",
row["transport"],
)
print(
"state :",
row["state"],
)
print(
"attempt_count :",
row["attempt_count"],
)
print(
"created_at_jst :",
row["created_at_jst"],
)
print(
"acked_at_jst :",
row["acked_at_jst"],
)
print(
"remote_id :",
row["remote_id"],
)
print(
"last_latency_ms :",
row["last_latency_ms"],
)
print(
"last_error :",
row["last_error"],
)
print()
print(
"===== DELIVERY STATES ====="
)
rows = con.execute(
"""
SELECT
state,
COUNT(*) AS rows
FROM sync_deliveries
GROUP BY state
ORDER BY state
"""
).fetchall()
for item in rows:
print(
f"{item['state']:12s}:",
item["rows"],
)
print()
print(
"===== MESH TX LATEST ====="
)
row = con.execute(
"""
SELECT
id,
timestamp_jst,
state,
packet_id,
ack_type,
ack_error,
last_error
FROM mesh_messages
WHERE direction='TX'
ORDER BY id DESC
LIMIT 1
"""
).fetchone()
if row is None:
print(
"no Mesh TX"
)
else:
print(
"id :",
row["id"],
)
print(
"timestamp_jst :",
row["timestamp_jst"],
)
print(
"state :",
row["state"],
)
print(
"packet_id :",
row["packet_id"],
)
print(
"ack_type :",
row["ack_type"],
)
print(
"ack_error :",
row["ack_error"],
)
print(
"last_error :",
row["last_error"],
)
finally:
con.close()
def main() -> None:
config = load_config()
print(
"========================================"
)
print(
"RON-V INTEGRATION STATUS"
)
print(
"========================================"
)
print()
print_config(
config
)
print()
print_system_status(
config
)
print()
print(
"===== WEB HEALTH ====="
)
print(
web_health(
str(
config[
"web"
][
"host"
]
),
int(
config[
"web"
][
"port"
]
),
)
)
print()
print_database_status(
config
)
print()
print(
"========================================"
)
print(
"RON-V STATUS END"
)
print(
"========================================"
)
if __name__ == "__main__":
main()
PY
157. 構文確認
cd /home/ronv/ronv
/home/ronv/.venvs/ronv/bin/python \
-m py_compile \
app/integration/status.py
何も表示されなければOKです。
158. status.pyを実行する
cd /home/ronv/ronv
RONV_CONFIG=/home/ronv/ronv/config/ronv.toml \
/home/ronv/.venvs/ronv/bin/python \
-m app.integration.status
実機では、
===== SYSTEM =====
ronv-vehicle.service active=active enabled=enabled
ronv-web.service active=active enabled=enabled
ronv-mesh.service active=active enabled=enabled
gpsd.socket active=active enabled=enabled
grafana-server.service active=active enabled=enabled
ronv-controller.timer active=active enabled=enabled
ronv-delivery.timer active=active enabled=enabled
ronv-controller.service active=inactive last_result=success
ronv-delivery.service active=inactive last_result=success
となりました。
ControllerとDeliveryは、
Type=oneshot
なので、
active=inactive
でも正常です。
重要なのは、
last_result=success
です。
159. Web / SQLite / L0も同時確認する
実機:
===== WEB HEALTH =====
HTTP 200 {"status":"ok"}
===== DATABASE =====
integrity_check : ok
最新L0:
sequence : 76
position_available : no
network_flags : 4
packet_bytes : 23
crc_ok : True
室内だったためGNSSは、
position_available : no
でした。
これは異常ではありません。
160. L1 / L2も確認する
L1:
lte_cell_observation : 1
vehicle_event : 7
L2:
cycle_sec : 600
vehicle.count : 0
vehicle.shock : 0
lte.count : 0
lte.unique_pci : 0
mesh.tx_count : 1
mesh.tx_acked : 1
L1 COUNTSは累積です。
一方L2は、
直近600秒
なので、値の意味は異なります。
例えば、
vehicle_event累積 = 7
直近600秒
vehicle.count = 0
という状態は正常です。
161. Delivery状態も確認する
実機:
===== DELIVERY LATEST =====
state : acked
attempt_count : 1
last_error : None
さらに、
===== DELIVERY STATES =====
acked : 64
でした。
この時点でDBに存在していた64配送はすべて、
acked
でした。
162. Mesh real ACKも確認する
===== MESH TX LATEST =====
state : acked
ack_type : real
ack_error : NONE
last_error : None
つまり、
Delivery
↓
Meshtastic
↓
real ACK
までstatusコマンドだけで確認できます。
163. ronv-statusコマンドを作る
毎回、
python -m app.integration.status
を打つのは面倒です。
そこで、
ronv-status
だけで実行できるようにします。
164. 起動スクリプトを作る
cd /home/ronv/ronv
STAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p bin
if [ -f bin/ronv-status ]; then
cp -a \
bin/ronv-status \
"bin/ronv-status.bak_${STAMP}"
fi
cat > bin/ronv-status <<'SH'
#!/bin/bash
set -e
export RONV_CONFIG=/home/ronv/ronv/config/ronv.toml
cd /home/ronv/ronv
exec \
/home/ronv/.venvs/ronv/bin/python \
-m app.integration.status
SH
chmod +x bin/ronv-status
165. /usr/local/binへリンクする
sudo ln -sfn \
/home/ronv/ronv/bin/ronv-status \
/usr/local/bin/ronv-status
確認:
command -v ronv-status
結果:
/usr/local/bin/ronv-status
166. ronv-statusを実行する
ronv-status
これだけで、
service
timer
Web
SQLite
L0
L1
L2
Delivery
Mesh
をまとめて確認できます。
このコマンドは、この後のreboot試験でも使用します。
167. 現在のL3 / L4実装
#15の初期段階ではL3/L4を将来拡張としていましたが、現在のGitHub main ではEvidence層として実装済みです。
168. LTE Evidence
lte_measurements
│
├─ L3 /var/lib/ronv/l3/lte/*.json
│ 解析済み cell_search evidence
│
└─ L4 /var/lib/ronv/l4/lte/*.log
raw cell_search log
実装は、
app/integration/lte_evidence.py
です。L3/L4はSHA-256を保持して sync_objects へ登録されます。
169. Vehicle / Camera Evidence
Vehicle EventとCameraは、
app/integration/vehicle_evidence.py
でEvidence化します。
L3 = Vehicle Event / Camera manifest JSON
L4 = captured JPEG evidence
RingCamのSHOCK Eventでは現在、イベント時刻を中心に選択した複数画像をL4として扱える実装へ発展しています。
170. L3 / L4の非同期配送
L3/L4ファイルは sync_deliveries に transport=rclone_drive として登録し、
app/integration/rclone_delivery_worker.py
がGoogle Drive等へ非同期転送します。外部通信が失敗しても測定処理そのものを止めない設計です。
したがって現在のレベル構成は、
L0 Node Beacon
L1 Sensor Fact (Vehicle / LTE / RF)
L2 Cycle Summary
L3 Structured Evidence / Manifest
L4 Raw Evidence File / Raw Log
となります。
171. reboot前の最終バックアップ
ここからRaspberry Piの再起動試験へ進みます。
その前に、現在のRON-Vを保存します。
今回保存するのは、
integration source
config
systemd
ronv-status
SQLite DB
です。
172. pre_rebootバックアップを作る
cd /home/ronv/ronv
STAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p backups
FINAL_DIR="backups/pre_reboot_${STAMP}"
mkdir -p "$FINAL_DIR"
echo "FINAL_DIR=$FINAL_DIR"
cp -a \
app/integration \
"$FINAL_DIR/integration"
cp -a \
config/ronv.toml \
"$FINAL_DIR/ronv.toml"
cp -a \
systemd \
"$FINAL_DIR/systemd"
cp -a \
bin/ronv-status \
"$FINAL_DIR/ronv-status"
sqlite3 /var/lib/ronv/ronv.db \
".backup '$FINAL_DIR/ronv.db'"
SQLiteはファイルコピーではなく、
SQLite .backup
を使います。
173. バックアップDBを検査する
sqlite3 \
"$FINAL_DIR/ronv.db" \
'PRAGMA integrity_check;'
実機:
ok
174. バックアップ内容を確認する
find "$FINAL_DIR" \
-maxdepth 3 \
-type f \
-printf '%p %s bytes\n' \
| sort
du -sh "$FINAL_DIR"
実機では、
ronv.db
integration/
ronv.toml
systemd/
ronv-status
などが保存されました。
バックアップサイズは、
684K
でした。
175. SHA-256 manifestを作る
バックアップ後の改変検出用に、
SHA256SUMS
を作ります。
cd /home/ronv/ronv
FINAL_DIR="backups/pre_reboot_<TIMESTAMP>"
cd "$FINAL_DIR"
find . \
-type f \
! -name SHA256SUMS \
-print0 \
| sort -z \
| xargs -0 sha256sum \
> SHA256SUMS
確認:
wc -l SHA256SUMS
実機:
36 SHA256SUMS
176. SHA-256を検証する
sha256sum -c SHA256SUMS
実機では36ファイルすべて、
OK
となりました。
例えば、
./integration/cycle.py: OK
./integration/delivery_worker.py: OK
./integration/status.py: OK
./integration/sync_store.py: OK
./ronv.db: OK
./ronv-status: OK
./ronv.toml: OK
./systemd/ronv-controller.service: OK
./systemd/ronv-controller.timer: OK
./systemd/ronv-delivery.service: OK
./systemd/ronv-delivery.timer: OK
です。
177. バックアップDB integrityを再確認
sqlite3 \
/home/ronv/ronv/backups/pre_reboot_<TIMESTAMP>/ronv.db \
'PRAGMA integrity_check;'
結果:
ok
これで、
source
config
systemd
SQLite
hash
まで保存できました。
178. reboot前にPythonを全検査する
cd /home/ronv/ronv
/home/ronv/.venvs/ronv/bin/python \
-m py_compile \
app/integration/sync_store.py \
app/integration/l0_packet.py \
app/integration/l0_store.py \
app/integration/l1_store.py \
app/integration/l2_summary.py \
app/integration/delivery_worker.py \
app/integration/cycle.py \
app/integration/status.py
echo "Python compile: OK"
実機:
Python compile: OK
179. systemd unitもverifyする
systemd-analyze verify \
systemd/ronv-vehicle.service \
systemd/ronv-web.service \
systemd/ronv-mesh.service \
systemd/ronv-lte-measure.service \
systemd/ronv-controller.service \
systemd/ronv-controller.timer \
systemd/ronv-delivery.service \
systemd/ronv-delivery.timer
echo "systemd verify: OK"
実機:
systemd verify: OK
180. project版とinstalled版のunitを比較する
for UNIT in \
ronv-vehicle.service \
ronv-web.service \
ronv-mesh.service \
ronv-controller.service \
ronv-controller.timer \
ronv-delivery.service \
ronv-delivery.timer
do
if cmp -s \
"systemd/$UNIT" \
"/etc/systemd/system/$UNIT"
then
echo "$UNIT : MATCH"
else
echo "$UNIT : DIFFER"
fi
done
実機:
ronv-vehicle.service : MATCH
ronv-web.service : MATCH
ronv-mesh.service : MATCH
ronv-controller.service : MATCH
ronv-controller.timer : MATCH
ronv-delivery.service : MATCH
ronv-delivery.timer : MATCH
つまり、
repository側のunit
と、
実際にsystemdへインストールされているunit
が一致しています。
181. reboot前baselineを取る
再起動前に、比較用の状態を記録します。
まずboot IDです。
echo -n "boot_id : "
cat /proc/sys/kernel/random/boot_id
echo -n "boot_since : "
uptime -s
公開記事では値そのものは伏せます。
boot_id : <PRE_REBOOT_BOOT_ID>
boot_since : 2026-09-04 21:13:37
182. timer状態
systemctl is-enabled \
ronv-controller.timer \
ronv-delivery.timer
systemctl is-active \
ronv-controller.timer \
ronv-delivery.timer
実機:
enabled
enabled
active
active
183. ronv-statusでreboot前確認
ronv-status
主要service:
ronv-vehicle.service active=active enabled=enabled
ronv-web.service active=active enabled=enabled
ronv-mesh.service active=active enabled=enabled
gpsd.socket active=active enabled=enabled
grafana-server.service active=active enabled=enabled
timer:
ronv-controller.timer active=active enabled=enabled
ronv-delivery.timer active=active enabled=enabled
Web:
HTTP 200 {"status":"ok"}
SQLite:
integrity_check : ok
184. reboot前の最終L0
実機:
id : 82
sequence : 82
created_at_jst : 2026-09-05T10:59:32+09:00
packet_bytes : 23
crc_ok : True
つまりreboot前baselineは、
L0 sequence = 82
です。
185. reboot前のDelivery
実機:
id : 70
state : acked
attempt_count : 1
last_error : None
この時点では、
Delivery id = 70
まで完了していました。
186. reboot前のMesh
実機:
id : 77
state : acked
ack_type : real
ack_error : NONE
reboot前baseline:
L0 sequence 82
Delivery id 70
Mesh id 77
とします。
187. SQLite integrity
sqlite3 /var/lib/ronv/ronv.db \
'PRAGMA integrity_check;'
結果:
ok
これでreboot前の条件が揃いました。
188. Raspberry Piを再起動する
最後に、
sync
を実行します。
sync
その後、
sudo reboot
します。
SSH接続が切れますが正常です。
189. reboot後のboot IDを確認する
再接続後、何もserviceを手動startせず確認します。
echo "===== POST-REBOOT BOOT ====="
echo -n "boot_id : "
cat /proc/sys/kernel/random/boot_id
echo -n "boot_since : "
uptime -s
echo
echo "===== UPTIME ====="
uptime
実機:
boot_id : <POST_REBOOT_BOOT_ID>
boot_since : 2026-09-05 11:10:29
reboot前とは異なるboot IDになりました。
つまり、
実際に新しいbootへ移行
したことを確認できます。
190. service / timerが自動復帰したか確認する
重要なのは、
systemctl start
を一度も実行しないことです。
確認:
systemctl is-enabled \
ronv-vehicle.service \
ronv-web.service \
ronv-mesh.service \
ronv-controller.timer \
ronv-delivery.timer \
gpsd.socket \
grafana-server.service
実機:
enabled
enabled
enabled
enabled
enabled
enabled
enabled
191. active状態
systemctl is-active \
ronv-vehicle.service \
ronv-web.service \
ronv-mesh.service \
ronv-controller.timer \
ronv-delivery.timer \
gpsd.socket \
grafana-server.service
実機:
active
active
active
active
active
active
active
つまり、
Vehicle
Web
Mesh
Controller timer
Delivery timer
gpsd
Grafana
がすべて自動復帰しました。
192. Controllerはまだ動いていない
reboot直後、
systemctl list-timers \
ronv-controller.timer \
ronv-delivery.timer \
--all \
--no-pager
を確認しました。
Controllerは、
LAST -
でした。
つまり、
reboot後まだControllerは一度も動いていない
状態です。
これは狙いどおりです。
Controller timerは、
OnActiveSec=10min
なので、boot後約10分で最初の自動実行になります。
193. reboot直後に興味深い状態が発生した
ここでronv-statusを確認すると、
L0 sequence = 83
になっていました。
しかし時刻を見ると、
created_at_jst
2026-09-05T11:09:33+09:00
でした。
一方rebootは、
11:10:29
です。
つまり、
sequence 83
はreboot前に生成されたL0です。
194. sequence 83はreboot前に生成されていた
時系列:
11:09:33
L0 sequence 83生成
↓
Delivery登録
↓
11:10:29
Raspberry Pi reboot
つまりreboot時点で、
未配送Delivery
がSQLiteに残っていました。
これは偶然ですが、非常に良い試験条件になりました。
195. reboot後に未配送データが復元された
reboot後のronv-status:
===== DELIVERY LATEST =====
id : 71
state : acked
attempt_count : 1
acked_at_jst : 2026-09-05T11:11:43+09:00
last_error : None
つまりreboot後、
Delivery timer
が自動復帰し、reboot前に残っていたDeliveryを処理しました。
196. Mesh real ACK
Mesh側:
===== MESH TX LATEST =====
id : 78
timestamp_jst : 2026-09-05T11:11:43+09:00
state : acked
ack_type : real
ack_error : NONE
last_error : None
つまり、
reboot前
L0生成
Delivery pending
↓
reboot
↓
Delivery timer復帰
↓
Mesh Gateway復帰
↓
920MHz送信
↓
real ACK
が成立しました。
197. SQLiteを配送queueとして使った効果
今回のsequence 83は、RON-Vの設計上かなり重要な実証です。
もしDelivery queueがRAMだけに存在していた場合、
reboot
で失われます。
しかしRON-Vでは、
sync_deliveries
mesh_messages
をSQLiteへ保存しています。
そのため、
reboot前の未配送データ
も残ります。
今回、
seq83
が実際にreboot後へ持ち越され、
real ACK
まで到達しました。
198. Delivery全件もackedになった
この時点のstatus:
===== DELIVERY STATES =====
acked : 71
つまりDBに存在した71配送はすべて、
acked
でした。
199. 次はController自身の自動復帰を確認する
ここまでで、
service自動復帰
Delivery自動復帰
未配送データ再開
real ACK
は確認できました。
残る条件は、
reboot後にController自身が
新しいL0を生成する
ことです。
reboot直後、
journalctl -b \
-u ronv-controller.service \
--no-pager
を確認すると、
-- No entries --
でした。
つまりまだ一度も動いていません。
200. Controller timerの次回実行を待つ
systemctl list-timers \
ronv-controller.timer \
ronv-delivery.timer \
--all \
--no-pager
実機では、
ronv-controller.timer
LEFT 約2分
LAST -
でした。
ここでは手動で、
systemctl start ronv-controller.service
を実行してはいけません。
完全自動復帰を確認するため、そのまま待ちます。
201. reboot後のController初回自動実行
数分後、
journalctl -b \
-u ronv-controller.service \
--no-pager
を確認しました。
実機:
Sep 05 11:20:37
Starting ronv-controller.service
続いて、
Sep 05 11:20:42
===== RON-V INTEGRATION CYCLE =====
が出ました。
202. reboot後の新しいL0
実機:
l0_object_uid : ronv-l0-0001-...-00084
l0_sequence : 84
l0_gps_status : no_fix
l0_crc_ok : True
l0_delivery_id : 72
vehicle_l1_count : 0
lte_l1_count : 0
cycle_sec : 600
elapsed_ms : 5349
つまり、
reboot
↓
約10分
↓
Controller timer
↓
L0 sequence 84
が人手なしで生成されました。
203. GNSS no_fixでも正常
今回も室内だったため、
l0_gps_status : no_fix
です。
しかし、
l0_crc_ok : True
なのでL0 packet自体は正常です。
RON-VではGNSSが取得できない場合も、
GPS unknown
としてpacketを生成できる設計にしています。
204. L2も自動生成された
同じCycleで、
l2_object_uid
も新しく生成されました。
つまりreboot後、
L0
L1処理
L2
Delivery登録
までControllerが正常復帰しています。
205. Delivery id 72が自動登録された
sequence 84に対応する、
l0_delivery_id : 72
が作られました。
つまり、
Controller
↓
L0
↓
sync_objects
↓
sync_deliveries
まで自動で進みました。
206. Delivery timerがsequence 84を処理した
その後ronv-statusを確認しました。
===== DELIVERY LATEST =====
id : 72
state : acked
attempt_count : 1
created_at_jst : 2026-09-05T11:20:42+09:00
acked_at_jst : 2026-09-05T11:21:26+09:00
last_error : None
sequence 84のDeliveryは、
attempt_count = 1
で成功しています。
207. sequence 84もreal ACK
Mesh:
===== MESH TX LATEST =====
id : 79
timestamp_jst : 2026-09-05T11:21:26+09:00
state : acked
ack_type : real
ack_error : NONE
last_error : None
つまり、
reboot
↓
Controller timer
↓
L0 seq84
↓
Delivery id72
↓
Delivery timer
↓
Mesh id79
↓
920MHz
↓
real ACK
まで完全自動で成立しました。
208. SQLite integrityも正常
同じronv-statusで、
===== DATABASE =====
integrity_check : ok
でした。
つまり、
reboot
SQLite reopen
service復帰
Delivery再開
新Cycle
を経てもDB integrityに問題はありません。
209. Webも自動復帰
===== WEB HEALTH =====
HTTP 200 {"status":"ok"}
なので、
ronv-web.service
もreboot後に自動復帰しています。
210. reboot後の最終service状態
最終状態:
ronv-vehicle.service active / enabled
ronv-web.service active / enabled
ronv-mesh.service active / enabled
gpsd.socket active / enabled
grafana-server.service active / enabled
ronv-controller.timer active / enabled
ronv-delivery.timer active / enabled
oneshot:
ronv-controller.service
inactive
last_result=success
ronv-delivery.service
inactive
last_result=success
です。
211. reboot試験で確認できた2種類の復旧
今回のreboot試験では、偶然ではありますが、非常に良い2種類の実証ができました。
1. reboot前の未配送データ
L0 seq83
↓
Delivery登録
↓
reboot
↓
Delivery timer復帰
↓
Mesh
↓
real ACK
2. reboot後に新規生成されたデータ
reboot
↓
Controller timer復帰
↓
L0 seq84
↓
Delivery id72
↓
Mesh id79
↓
real ACK
です。
212. RON-Vのreboot耐性
今回確認できたことは、
rebootしても
queueが消えない
だけではありません。
reboot
↓
service復帰
↓
timer復帰
↓
未配送再開
↓
新規観測開始
↓
無線配送
↓
ACK確認
まで成立しています。
213. 最終的なRON-V構成
Raspberry Pi 5
│
┌───────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
AtomS3R RTL-SDR gpsd
│ │ │
▼ ▼ │
vehicle_events lte_measurements │
│ │ │
▼ ▼ ▼
L1 Fact L1 Fact L0 Beacon
│ │ │
└─────────┬─────────┘ │
│ │
▼ │
L2 Summary │
│ │
└─────────────┬──────────────┘
│
▼
sync_objects
│
▼
sync_deliveries
│
▼
ronv-delivery.timer
│
▼
mesh_messages
│
▼
ronv-mesh.service
│
▼
Meshtastic 920MHz
│
▼
real ACK
表示系:
SQLite
├─ ronv-status
├─ RON-V Web UI
└─ Grafana
です。
214. systemd構成
常駐service:
ronv-vehicle.service
ronv-web.service
ronv-mesh.service
gpsd.socket
grafana-server.service
oneshot:
ronv-controller.service
ronv-delivery.service
timer:
ronv-controller.timer
ronv-delivery.timer
です。
215. ControllerとDeliveryを分離した理由
Controllerが、
観測
集約
無線送信
ACK待ち
retry
を全部行う設計にはしませんでした。
RON-Vでは、
Controller
L0 / L1 / L2
Delivery登録
Delivery Worker
配送管理
Mesh Gateway
USB / 無線
へ分離しています。
その結果、
Meshtasticが一時的に送れない
場合でも、
観測処理
まで止める必要がありません。
216. SQLite中心の構成
RON-VではSQLiteを、
履歴DB
としてだけでなく、
永続queue
としても使っています。
今回のreboot試験で、
seq83
が再起動をまたいで配送されたことで、この設計が実際に機能することを確認できました。
217. 最終確認一覧
[OK] ronv-status
[OK] Web health
[OK] SQLite integrity
[OK] Vehicle service
[OK] Web service
[OK] Mesh service
[OK] gpsd
[OK] Grafana
[OK] Controller timer
[OK] Delivery timer
[OK] L0
[OK] CRC16
[OK] L1 Vehicle
[OK] L1 LTE
[OK] L2 Summary
[OK] sync_objects
[OK] sync_deliveries
[OK] sync_cursors
[OK] Delivery retry設計
[OK] 二重送信抑止設計
[OK] Meshtastic real ACK
[OK] 最終バックアップ
[OK] SHA-256検証
[OK] backup DB integrity
[OK] Python compile
[OK] systemd verify
[OK] installed unit MATCH
[OK] Raspberry Pi reboot
[OK] service自動復帰
[OK] timer自動復帰
[OK] reboot前未配送データの再開
[OK] seq83 real ACK
[OK] reboot後Controller自動起動
[OK] seq84生成
[OK] seq84 Delivery
[OK] seq84 real ACK
[OK] reboot後SQLite integrity
[OK] reboot後Web HTTP 200
218. RON-V #15完成判定
今回の完成条件は、
reboot
↓
service / timer自動復帰
↓
Controller自動実行
↓
新規L0生成
↓
Delivery自動処理
↓
Meshtastic 920MHz
↓
real ACK
でした。
今回、
seq84
でこの全経路を確認できました。
したがって、
RON-V #15 Integration
は完成とします。
219. #15でRON-Vがどう変わったか
#15開始前のRON-Vは、
Vehicle
Camera
LTE
GNSS
Mesh
Web
Grafana
という個別機能が動作するRaspberry Piでした。
#15では、それらを、
観測
↓
構造化
↓
同期管理
↓
配送管理
↓
無線
↓
ACK
という1つのシステムへ接続しました。
220. RON-Vの現在地
現在のRON-Vは、
Radio Observation Node – Vehicle
という名前のとおり、
車載観測
通信状態観測
GNSS
イベント取得
無線配送
可視化
を行うノードになりました。
さらに、
rebootしても自動復帰
できるところまで確認しています。
221. ただし、まだ「製品」ではない
今回完成したのは、
RON-V software integration
です。
今後、車載装置として使うには、
電源
電圧降下
ACC連動
安全shutdown
筐体
温度
SDカード寿命
アンテナ配置
LTE観測周期
カメラEvidence
なども検討する必要があります。
つまり、
Software Integration
は完成しましたが、
Vehicle Hardware Integration
は今後も続きます。
222. 次の課題
今回のPiでは、ここまで多くの作業を手動で行いました。
例えば、
apt install
Python venv
pip install
directory作成
config作成
SQLite初期化
systemd登録
Meshtastic設定
Web設定
Grafana設定
などです。
次に同じRON-Vをもう1台作るとき、これを最初から繰り返すのは現実的ではありません。
そこで次は、
完成したRON-V
↓
GitHub
↓
install.sh
↓
新しいRaspberry Pi
↓
RON-V構築
へ進みます。
223. 次回
次回は、
RON-V #16 — GitHubからRON-Vを一括インストールする
を行います。
最初に、
GitHubへ入れるもの
GitHubへ入れないもの
を整理します。
例えば、
入れる
app/
systemd/
config example
install script
requirements
README
入れない
実DB
実GPS位置
実Mesh Node ID
SSID
runtime log
JPEG
backup
__pycache__
という整理が必要です。
その後、
git clone
↓
install.sh
↓
venv
↓
dependencies
↓
config
↓
SQLite
↓
systemd
↓
ronv-status
まで自動化します。
まとめ
#15前編・後編を通して、RON-Vは、
複数の観測機能を持つRaspberry Pi
から、
自律して観測し、
情報を構造化し、
配送状態を永続管理し、
920MHz Meshへ送信し、
real ACKを確認し、
reboot後も自動復帰する
車載観測ノード
になりました。
今回特に重要だったのは、
SQLiteによる永続状態管理
Controller / Delivery / Meshの責務分離
Meshtastic USBの単一所有
systemdによる自動復帰
です。
reboot試験では、
reboot前の未配送L0
と、
reboot後に新しく生成されたL0
の両方がreal ACKまで到達しました。
これで、
RON-V #15 — RON-V統合
を完了とします。