Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions scripts/ops/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 車両調査スクリプト

エンジンをかけた状態で、順に実行する。所要時間の目安つき。

## 1. 標準PIDの対応確認 — 30秒・停車で可

```
sudo python3 /opt/pi-obd-meter/scripts/ops/probe-standard-pids.py
```

**大気圧 (0x33) を一度も問い合わせていない。** 生ログに現れた Mode01 の
応答は 05/0B/0F/10/42 の5つだけで、対応PIDビットマップも取っていない。
つまり 0x33 は「非対応」ではなく**未確認**。

対応していれば燃料モデルが直る。`cmd/pi-obd-meter/fuel.go` は
`atmosphericKPa = 101.3` をハードコードしており、Speed-Density 法の
体積効率を `MAP/101.3` で近似している。比叡山 (標高848m) の実大気圧は
約 91.6kPa なので、山では体積効率を1割過大に見積もる。

外気温 (0x46)、エンジン油温 (0x5C)、絶対負荷 (0x43) も同時に確認する。

## 2. ブレーキ信号の探索 — 15分・広い駐車場か交通のない道

```
sudo python3 /opt/pi-obd-meter/scripts/ops/capture-brake.py
```

画面の指示どおりに操作し、各操作の直前に Enter を押す。押した時刻が
ラベルになる。回収して Mac で解析する。

```
scp laurel@<pi>:/var/log/can-verify/brake_*.{log,labels} .
python3 scripts/ops/find-brake-bit.py brake_*.log brake_*.labels
```

**「ブレーキを踏まずにアクセルオフだけで減速する」手順を必ず含めること。**
これが対照群になる。無いと、単に減速と相関するだけのビットを誤って掴む。
手持ちの生ログで 0x200 を調べた際、相関が全部弱く (r=+0.20/-0.30/+0.08)
決着しなかったのは、走行データが最高17km/hの駐車場走行しか無かったため。

## 3. 勾配PIDの探索 — 登坂を含む走行が要る

poll22 が動いていれば追加操作は不要。**連続した登りを走ること**が条件。

```
scp laurel@<pi>:/var/log/can-verify/poll22_*.csv .
python3 scripts/ops/find-grade-pid.py poll22_*.csv
```

FN4A-EL は road load / grade logic で変速線を動かす。登坂で不要な
シフトアップを避け、降坂でエンジンブレーキを残すには、PCM が勾配相当の
量を持っていなければ制御できない。#150 の未同定 PID 17個のどれかである
可能性が高い。

**2026-08-31 時点の絞り込み結果 (平地データのみ、n=1,269,416):**

| PID | 変動幅 | 車速相関 | 判定 |
|---|---|---|---|
| **098E** | 0〜40 | +0.243 | ★停車中は常に0、走行中のみ動く |
| **1746** | 0〜5376 | +0.200 | ★停車中は固定、走行中に動く |

17個から2個に絞れた。**ただし平地のデータなので、これは「性質が合う」
までで同定ではない。** 登坂データで値が跳ねるかを見る必要がある。

断定するには GPS の標高 (#164) か IMU (#165) との照合が要る。

## 関連 issue

- #164 GPS が一度も測位できていない
- #165 IMU を追加して勾配・横G・加速度を直接測る
- #166 ブレーキ信号が CAN にあるか未確認 — 0x200 も未同定
- #150 未同定の Mode 22 PID 17個
92 changes: 92 additions & 0 deletions scripts/ops/capture-brake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# ブレーキ信号を探すための、操作ラベル付き CAN キャプチャ。
#
# sudo python3 capture-brake.py
#
# 画面の指示どおりに操作し、各操作の開始時に Enter を押す。押した時刻が
# ラベルとして記録されるので、あとから「踏んだ瞬間に変わったビット」を
# 機械的に探せる。
#
# 【なぜラベルが要るか】
# 手持ちの生ログ3本のうち走行を含むのは1本だけで、最高17km/hの駐車場
# 走行しかなかった。0x200 (80,244フレーム流れているのに未デコード) と
# 減速度を照合しても相関が全部弱く (r=+0.20/-0.30/+0.08)、決着しなかった。
#
# 減速そのものではなく「ブレーキ操作」に連動するビットを見分けたい。
# そのために「踏まずにアクセルオフだけで減速する」手順を必ず含める。
# これが無いと、単に減速と相関するだけのビットを誤って掴む。
import os, subprocess, sys, time

if os.geteuid() != 0:
sys.exit("sudo で実行すること")

OUT_DIR = "/var/log/can-verify"
os.makedirs(OUT_DIR, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
dump_path = os.path.join(OUT_DIR, "brake_%s.log" % stamp)
label_path = os.path.join(OUT_DIR, "brake_%s.labels" % stamp)

STEPS = [
("停車・ブレーキを踏む", "踏んだまま5秒待つ"),
("停車・ブレーキを離す", "離したまま5秒待つ"),
("停車・ブレーキを踏む(2回目)", "踏んだまま5秒待つ"),
("停車・ブレーキを離す(2回目)", "離したまま5秒待つ"),
("停車・ブレーキを踏む(3回目)", "踏んだまま5秒待つ"),
("停車・ブレーキを離す(3回目)", "離したまま5秒待つ"),
("★アクセルオフのみで減速", "40km/hから、ブレーキを踏まずに惰行で減速する"),
("フットブレーキで緩やかに減速", "40km/hから、軽くブレーキだけで減速する"),
("★アクセルオフのみで減速(2)", "もう一度、ブレーキを踏まずに惰行"),
("フットブレーキで強めに減速", "安全な範囲で、はっきり踏んで減速する"),
("サイドブレーキを引く", "停車してから。引いたまま5秒"),
("サイドブレーキを解除", "解除して5秒"),
]

print("記録先: %s" % dump_path)
print("ラベル: %s" % label_path)
print()
print("【安全第一】広い駐車場か、交通のない道で行うこと。")
print("同乗者に操作してもらえるなら、そのほうが安全。")
print("途中でやめる場合は Ctrl-C。それまでの記録は残る。")
print()
input("準備ができたら Enter を押す > ")

dump = subprocess.Popen(["candump", "-t", "a", "-l", "can0"], cwd=OUT_DIR)
time.sleep(1.0)

# candump -l は candump-<日時>.log という名前で書く。あとで rename する。
labels = open(label_path, "w", buffering=1)
labels.write("t,label\n")
labels.write("%.3f,記録開始\n" % time.time())

try:
for i, (name, how) in enumerate(STEPS, 1):
print("\n--- %d/%d %s ---" % (i, len(STEPS), name))
print(" %s" % how)
input(" 操作を始める直前に Enter > ")
t = time.time()
labels.write("%.3f,%s\n" % (t, name))
print(" 記録: %s" % time.strftime("%H:%M:%S", time.localtime(t)))
time.sleep(0.5)
except KeyboardInterrupt:
print("\n中断した。ここまでの記録は残る。")

labels.write("%.3f,記録終了\n" % time.time())
labels.close()
time.sleep(1.0)
dump.terminate()
dump.wait(timeout=5)

# candump が作ったファイルを、ラベルと対になる名前に付け替える
made = sorted(
(f for f in os.listdir(OUT_DIR) if f.startswith("candump-")),
key=lambda f: os.path.getmtime(os.path.join(OUT_DIR, f)),
)
if made:
os.rename(os.path.join(OUT_DIR, made[-1]), dump_path)
size = os.path.getsize(dump_path) / 1e6
print("\n完了。%s (%.1f MB)" % (dump_path, size))
else:
print("\ncandump の出力が見つからない。can0 が上がっているか確認すること。")
print("ラベル: %s" % label_path)
print("\nMac へ回収:")
print(" scp laurel@<pi>:%s* ." % os.path.join(OUT_DIR, "brake_%s" % stamp))
121 changes: 121 additions & 0 deletions scripts/ops/find-brake-bit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# capture-brake.py が録ったログから、ブレーキ操作に連動するビットを探す。
#
# python3 find-brake-bit.py brake_YYYYmmdd_HHMMSS.log brake_YYYYmmdd_HHMMSS.labels
#
# Mac 上で走らせる。Pi は不要。
#
# 【判定の考え方】
# 「踏んだ区間で1、離した区間で0」になるビットを探すだけでは足りない。
# 減速中に立つだけのビット (車速依存、ブレーキ以外の何か) が混ざるため。
#
# そこで「★アクセルオフのみで減速」の区間を対照群に使う。ブレーキ信号なら
# その区間では立たないはずで、単に減速と相関するビットはそこでも立つ。
import re, sys, collections

if len(sys.argv) < 3:
sys.exit(__doc__)

pat = re.compile(r"\((\d+\.\d+)\)\s+\S+\s+([0-9A-Fa-f]{3,8})#([0-9A-Fa-f]*)")
frames = []
with open(sys.argv[1], errors="replace") as f:
for line in f:
m = pat.search(line)
if not m:
continue
data = m.group(3)
by = [int(data[i:i + 2], 16) for i in range(0, len(data) - 1, 2)]
frames.append((float(m.group(1)), m.group(2).upper(), by))

labels = []
with open(sys.argv[2], errors="replace") as f:
next(f, None)
for line in f:
p = line.rstrip("\n").split(",", 1)
if len(p) == 2:
labels.append((float(p[0]), p[1]))

print("フレーム %d 件 ラベル %d 件" % (len(frames), len(labels)))
if not frames or not labels:
sys.exit("データが足りない")

ids = collections.Counter(f[1] for f in frames)
print("CAN ID: " + " ".join("%s:%d" % (k, v) for k, v in ids.most_common(12)))


def window(i):
"""ラベル i の開始から次のラベルまで (最後は +6秒)"""
t0 = labels[i][0]
t1 = labels[i + 1][0] if i + 1 < len(labels) else t0 + 6.0
return [f for f in frames if t0 <= f[0] < t1]


ON = [i for i, (_, n) in enumerate(labels) if "踏む" in n]
OFF = [i for i, (_, n) in enumerate(labels) if "離す" in n]
COAST = [i for i, (_, n) in enumerate(labels) if "アクセルオフのみ" in n]
BRAKE_MOVING = [i for i, (_, n) in enumerate(labels) if "フットブレーキ" in n]

print("\n踏む %d区間 / 離す %d区間 / 惰行 %d区間 / 走行中制動 %d区間"
% (len(ON), len(OFF), len(COAST), len(BRAKE_MOVING)))
if not ON or not OFF:
sys.exit("踏む/離す のラベルが無い")


def bitstats(idxs, cid, byte, bit):
"""該当区間で、そのビットが立っていた割合"""
hi = tot = 0
for i in idxs:
for _, c, by in window(i):
if c != cid or len(by) <= byte:
continue
tot += 1
if by[byte] >> bit & 1:
hi += 1
return (hi / tot if tot else None), tot


print("\n=== ブレーキ操作に連動するビット ===")
print(" 条件: 踏む区間で90%以上立ち、離す区間で10%以下")
hits = []
for cid in sorted(ids):
maxlen = max((len(by) for _, c, by in frames if c == cid), default=0)
for byte in range(maxlen):
for bit in range(8):
on, n_on = bitstats(ON, cid, byte, bit)
off, n_off = bitstats(OFF, cid, byte, bit)
if on is None or off is None or n_on < 20 or n_off < 20:
continue
if on >= 0.9 and off <= 0.1:
hits.append((cid, byte, bit, on, off, False))
elif on <= 0.1 and off >= 0.9:
hits.append((cid, byte, bit, on, off, True))

if not hits:
print(" 該当なし。ブレーキは CAN に出ていないか、別のバスにある。")
else:
print(" %-5s %-5s %-4s %8s %8s %s" % ("ID", "byte", "bit", "踏む", "離す", "極性"))
for cid, byte, bit, on, off, inv in hits:
print(" %-5s B%-4d %-4d %7.0f%% %7.0f%% %s"
% (cid, byte, bit, on * 100, off * 100, "反転(0=踏む)" if inv else "1=踏む"))

# --- 対照群でふるいにかける ---
if COAST and BRAKE_MOVING:
print("\n=== 走行中の対照検証 ===")
print(" ブレーキ信号なら「惰行」で立たず「制動」で立つ。")
print(" 両方で立つなら、それは単に減速と相関しているだけ。")
print(" %-5s %-5s %-4s %10s %10s 判定" % ("ID", "byte", "bit", "惰行", "制動"))
for cid, byte, bit, on, off, inv in hits:
c, nc = bitstats(COAST, cid, byte, bit)
b, nb = bitstats(BRAKE_MOVING, cid, byte, bit)
if c is None or b is None or nc < 20 or nb < 20:
print(" %-5s B%-4d %-4d %10s %10s サンプル不足" % (cid, byte, bit, "-", "-"))
continue
if inv:
c, b = 1 - c, 1 - b
ok = c <= 0.2 and b >= 0.6
print(" %-5s B%-4d %-4d %9.0f%% %9.0f%% %s"
% (cid, byte, bit, c * 100, b * 100,
"★ブレーキ信号" if ok else "減速と相関しているだけ"))
else:
print("\n 対照区間 (惰行/制動) が無いため、減速との区別ができない。")
print(" capture-brake.py を最後まで実行すること。")
Loading
Loading