測定モードと運用仕様¶
DS-MDA001 のアルコール測定には、仕様として存在するモード、アプリに実装されているモード、コードは残っているが到達できないモードの 3 層があります。ここを整理しておかないと、プロトコルを叩くときに「コマンドはあるのに何も起きない」で悩むことになります。
1. 規格が要求する 2 モード¶
本機は中国国家標準 GB/T 21254-2017「呼出气体酒精含量检测仪」 に適合する取り締まり用検知器として開発されており、次の 2 モードを備えることが求められています。
| モード | 規格上の記述 | 内容 |
|---|---|---|
| 正常测试 (通常測定) |
具备正常酒精气体呼出检测功能,插入一次性防倒吸标准吹管 | 使い捨ての逆流防止マウスピースを装着し、規定の呼気量・呼気時間を満たして測定する。証拠能力のある本測定 |
| 快速筛查 (迅速スクリーニング) |
具备快速筛查功能,不用安装吹管,只需被测人员轻吹一口气就能快速判断是否饮酒 | マウスピース不要。軽く一息吹きかけさせ、飲酒の有無だけを即座に判定する |
検問では全車両を快速筛查で流し、反応した相手だけマウスピースを装着して正常测试に進む、という運用になります。詳しくは 端末の素性と出自 を参照してください。
2. アプリに実装されているモード¶
com.hikvision.alcoholtest の起動画面 (SplashActivity) が提供するのは次の 3 項目だけです(res/values/arrays.xml)。
| メニュー | 遷移先 | 内容 |
|---|---|---|
| 明眸联动模式(連携モード) | LoginActivity → MainActivity |
Hikvision の顔認証端末「明眸 (MinMoe)」と連携し、測定者を紐づける |
| 单机模式(スタンドアロン) | NormalTestModeActivity |
端末単体での測定 |
| 记录查询(記録閲覧) | LogInfoActivity |
ローカル DB の測定記録閲覧 |
測定を行う Activity は NormalTestModeActivity ただ 1 つです。AndroidManifest に宣言された Activity も上記 5 つのみで、Quick / Rapid / Screening / Passive といった名前の Activity は存在しません。
通常測定シーケンス¶
sequenceDiagram
participant App as NormalTestModeActivity
participant Board as アルコールセンサー基板
App->>Board: startTest (0x01/0x01)
Board-->>App: ACK (0x21)
loop ゼロ点クリア確認
App->>Board: findAlcoholAD (0x02/0x00)
Board-->>App: AD値 (<=200 でクリア完了)
end
App->>Board: blow (0x04/0x01)
Note over App,Board: 「请装好吹管吹气」表示・吹込待機
loop 吹込中
Board-->>App: 進捗通知 (0x24 / 0x01〜0x04)
end
Board-->>App: 吹込完了 (0x24 / 0x05)
App->>Board: exitBlow (0x04/0x02)
App->>Board: AlcoholContent (0x08/0x01)
Board-->>App: 濃度 Float (非同期・最大24秒)
アプリが実際に送信するコマンドは 8 種類だけです: startTest / findAlcoholAD / blow / exitBlow / AlcoholContent / demarcate / passiveTest / exitTest。プロトコル上定義されている残りのコマンドは、アプリからは一切発行されません。
各コマンドの詳細は シリアル通信プロトコル、実測の非同期挙動は 実測挙動と非同期仕様 を参照してください。
3. 到達できないコード¶
日本流通版では、取り締まり用の機能がまとめて殺されています。センサー基板側のファームウェアは対応している可能性が高いので、自作ツール から直接コマンドを叩けば挙動を確認できます。
3.1 快速筛查(迅速スクリーニング) — 完全な Dead Code¶
// OrderTools.java — 3つとも完全に同じフレーム (cmd 0x04 / sub 0x03)
public static String QuickBlow = "FE020C00020101040309F0FE";
public static String blowQuick = "FE020C00020101040309F0FE";
public static String quickTestStartAutoPump = "FE020C00020101040309F0FE";
| 確認項目 | 結果 |
|---|---|
| 上記 3 定数を参照しているコード | APK 全体で 0 箇所 |
専用ヘルパークラス util/RapidScreening.java |
存在するが一度も new されない(OrderTools に未初期化フィールドがあるだけ) |
CmdType.ExitTestForQuick 列挙定数 |
代入も比較もされない |
| UI 上の導線 | 無し(モードメニューは 3 項目のみ) |
| レイアウト・文字列・画像リソース | quick / rapid / 筛查 の参照は 0 件 |
blow が 0x04/0x01、exitBlow が 0x04/0x02 なので、0x04/0x03 は「吹込モード開始」の第 3 のバリエーションにあたります。
3.2 被動測定 passiveTest (0x10) — こちらも到達不能¶
マウスピースなしで周囲の気体を吸って判定するモードです。起動しようとするコードはありますが、条件が永久に成立しません。
// NormalTestModeActivity.java:381 — ExitTestForWarm はどこからも代入されない
if (SampleBoradState == OrderTools.CmdType.ExitTestForWarm) {
SampleBoradState = OrderTools.CmdType.PassiveTest;
SerialHelper.sendHex(OrderTools.passiveTest);
加えて、起動ボタンであるはずの btn_passive_test は findViewById されておらず(レイアウト XML にボタン ID が 1 つも存在しない)、参照すれば NPE になります。
ハンドラ側の実装自体は完成しており、レスポンス仕様は以下の通りです。
| 条件 | 意味 |
|---|---|
mRecvBuf[7] == 0x22 |
NAK。1回リトライ後「失败请重新点击被动」 |
mRecvBuf[7] == 0x23, mRecvBuf[8] == 0 |
「正在气体分析」=分析中(暫定応答) |
mRecvBuf[7] == 0x23, mRecvBuf[8] ∈ {1,2,3} |
確定値。bytes 9-12 が Float 濃度 |
| その他 | 「气体收集失败,请重试」→ exitTest |
TestType は "被动测试" としてタグ付けされます。快速筛查 (0x04/0x03) とは別コマンドである点に注意してください。
3.3 工場出荷校正モード¶
FactoryTestFlag という Intent Extra を見て校正フロー (demarcate 送信など) に入るコードがありますが、この Extra を put している箇所が存在しません。
4. 判定ロジックと単位¶
4.1 単位は mg/L 固定¶
// Constant.java
g_100mL = "g/100mL(BAC)"
mg_100mL = "mg/100mL(BAC)"
mg_L = "mg/L(BrAC)" // ← これが使われる
mg_mL = "mg/mL(BAC)"
ug_100mL = "ug/100ml(BrAC)"
NormalTestModeActivity で this.unit = Constant.mg_L; とハードコードされており、切り替える UI はありません。センサーが返す Float 値はそのまま mg/L (呼気中アルコール濃度, BrAC) です。
変換は Utils.formatDensityString() が担当し、血中濃度への換算比は 220(2200:1 の分配係数相当)です。
| 単位指定 | 処理 |
|---|---|
mg/L(BrAC) |
生値をそのまま 0.000 書式で返す |
mg/100mL(BAC) |
値 × 220。ただし 4.0 未満は 0 に丸められる |
4.2 中国式の 20 / 80 判定は動作していない¶
// NormalTestModeActivity.java:655-691
float f2 = Float.parseFloat(Utils.formatDensityString(Constant.mg_L, fBytesToFloat2));
if (f2 < 20.0f) → 未超标 (緑 #006400)
else if (f2 >= 20.0f && f2 < 80.0f) → 饮酒后驾车 (黄 #DAA520)
else if (f2 >= 80.0f) → 醉酒后驾车 (赤)
単位を換算せずに閾値と比較している
f2 は mg_L 指定で生成されるため mg/L の生値です。一方 20 / 80 は GB 19522 が定める mg/100mL (BAC) の数値です。
実際の呼気濃度は 0.15 mg/L といったオーダーなので、f2 が 20 を超えることは現実的にありません。この判定は常に「未超标」に落ちます。
同じ計算をしているアラートリングの描画 (UpdateAlcoholColorRing) も常に < 5.0 の分岐に入り、ゲージが動きません。
皮肉なことに、この不具合が別のクラッシュを防いでいます。20〜80 および 80 以上の分岐の中では btn_continue / btn_exit などの未バインドな Button フィールドを参照しており、到達すれば NPE になります。
4.3 実際に効いている判定は 0.15 mg/L¶
// NormalTestModeActivity.java:711-726 (スタンドアロンモード時)
if (d >= 0.15d) → "NG"
else if (0.0f <= f3 && d < 0.15d) → "OK"
これは日本の道路交通法の呼気アルコール基準(0.15 mg/L)です。中国向け本体に日本向け判定を後付けしたビルドであることが、ここからも分かります。
レシート印字の境界値が DB と食い違う
| 実装 | 条件 | 0.15 ちょうどのとき |
|---|---|---|
PrintUtil.print() |
d > 0.15 で NG / d <= 0.15 で OK |
OK |
PrintUtil.printAll() / DB 記録 |
d >= 0.15 で NG / d < 0.15 で OK |
NG |
ちょうど 0.15 mg/L のとき、画面・DB と一部の印字で判定が逆転します。
4.4 その他の定数¶
| 定数 | 値 | 意味 |
|---|---|---|
Constant.ADC_THRESHOLD_VALUE |
200 |
ゼロ点クリア完了とみなす AD 値の上限 |
blowTime |
3.0f |
吹込の規定秒数 |
5. 結局どのモードが使えるのか¶
| モード | 公式アプリ | プロトコル直叩き |
|---|---|---|
| 正常测试(マウスピース有り本測定) | ✅ | ✅ |
| 快速筛查(スクリーニング) | ❌ Dead Code | ⚠️ 未検証(0x04/0x03 を送れば試せる) |
| 被动测试(受動吸引測定) | ❌ 到達不能 | ✅ 動作確認済み(実測挙動 参照) |
工場校正 (demarcate) |
❌ 到達不能 | ✅ 応答取得済み(プロトコル §5.3) |
AlcoholTestDebugger の TCP ブリッジを使えば、PC から任意のフレームを投げて 3 行目以降を自分で確かめられます。