R&D · Thiết bị chấm điểm · Protocol · 14/10/2026 · bản thảo v0.1
Protocol tín hiệu xe ↔ máy chủ chấm điểm MỚI
Định nghĩa (1) bộ tín hiệu trên xe (IO thân xe + OBD) cần để chấm các bài sa hình và (2) giao thức giữa bo thu trên xe và máy chủ chấm điểm.
Phạm vi đợt này chỉ là tín hiệu xe. RTK/vị trí làm sau, schema đã giữ chỗ để cắm vào mà không đổi protocol.
Đây là việc "format sự kiện chuẩn nội bộ" mà Thu tín hiệu xe §10 và
Kiến trúc thiết bị §7 đánh dấu 🔴 chốt sớm, vì nếu phải sửa sau khi đã giao thiết bị thì phải cập nhật firmware từng xe ngoài sân.
Phần cứng bo thu: Thu tín hiệu xe.
Bản test nhanh JSON/HTTP: API test tín hiệu xe.
0
Kết luận nhanh
✅ Nguyên tắc nền
- Thiết bị gửi số đo, không gửi phán quyết. Chết máy, số đang gài, xi-nhan đủ lâu chưa, quá giờ… đều do engine tính, nên sửa ngưỡng không phải OTA firmware
- Nghĩa logic chuẩn hoá ở bo thu: bit = 1 luôn là "đang tác động", engine không cần biết dây đấu high/low-side
- Mọi bản ghi mang giờ lấy mẫu (µs), không bao giờ dùng giờ nhận
🔗 Giao thức
- Protobuf, dùng chung một schema cho firmware (nanopb), gateway và server
- Trong xe: UART + COBS + CRC-32. Xe ↔ máy chủ: MQTT 5 / TLS 1.3 mTLS, broker đặt tại sân
- IO gửi theo cạnh (edge) chính xác ms; OBD 10–20Hz; snapshot 1Hz; gom lô 100ms
🛡️ Bằng chứng
- seq liên tục + journal + backfill: mất mạng chỉ làm dữ liệu về muộn, không mất
- Hash chain + checkpoint ký 1Hz bằng khoá trong chip bảo mật; Seal cuối phiên
- Phiên mang cfg_hash: chứng minh xe chạy đúng cấu hình đã kiểm chuẩn
⏸️ Để sau — RTK/vị trí. Các phần cần vị trí (đè vạch, dừng đúng vạch, vào chuồng, geofence vào/ra bài) chưa chấm được ở đợt này. Schema đã giữ sẵn số field cho Pose (§10) và Batch đã có chỗ chứa, nên khi thêm RTK chỉ là bổ sung, không đổi protocol. Trong lúc chưa có RTK, tốc độ tạm lấy từ OBD (PID 0x0D, §3.3) và việc vào/ra bài được đánh dấu bằng tay từ phòng điều hành (§7).
1
Topology
┌─ XE ─────────────────────────────────────────────────────┐
│ dây xe (phanh, côn, phanh tay, xi-nhan, dây an toàn, │
│ cửa, số lùi, khoá điện) ──opto──┐ │
│ cổng OBD-II (RPM, tốc độ) ──CAN/K-line──┤ │
│ ▼ │
│ [BO THU · STM32F401] debounce · đảo cực · lọc xi-nhan │
│ gắn t_us · seq · hash chain · ký (ATECC608B) │
│ │ L0: UART 921600 · COBS + CRC-32 │
│ ▼ │
│ [GATEWAY] journal ≥24h · gom lô 100ms · MQTT client │
│ loa trên xe · (sau này: cổng rover RTK) │
└─────────────┬────────────────────────────────────────────┘
│ L1: MQTT 5 / TLS 1.3 mTLS · Wi-Fi sân, 4G dự phòng
┌─ TRUNG TÂM (on-prem) ──▼──────────────────────────────────┐
│ [Broker] ──┬──▶ [Recorder] ghi thô, KHÔNG parse │
│ ├──▶ [Engine chấm] ──▶ lỗi/điểm ──▶ sh-yard │
│ └──▶ [Phòng điều hành] xem trạng thái xe │
└──────────────────────────────────────────────────────────┘
| Thành phần | Trách nhiệm trong protocol |
| Bo thu | Điều hoà tín hiệu, gắn giờ tại nguồn, đánh seq, nối hash, ký checkpoint. Là nơi duy nhất giữ khoá ký, nên chữ ký chứng minh dữ liệu ra từ đúng con bo đó |
| Gateway | SBC trong cabin. Không sửa nội dung bản ghi từ bo thu, chỉ ghi journal, gom lô, chuyển tiếp, phát loa. Sau này nhận thêm luồng RTK |
| Máy chủ (SS) | On-prem tại trung tâm, kèm broker. Mở/đóng phiên, preflight, chấm, yêu cầu backfill, lưu journal bất biến |
✅ Recorder tách khỏi engine: tiến trình ghi chỉ nhận byte rồi ghi file, không bao giờ chết. Engine có bug cũng không làm mất dữ liệu thí sinh, và luôn chạy lại được từ file.
2
Trục thời gian — không phụ thuộc RTK
Thiết kế cũ lấy PPS từ rover RTK. Vì RTK để sau, bo thu cần nguồn giờ riêng, và khi RTK lắp vào thì cả hai cùng dựa trên giờ GNSS nên tự khớp nhau.
| Nguồn giờ | Sai số | Đánh giá |
| Module GNSS timing rẻ trên bo (u-blox MAX-M10 / NEO-M9, ~15–25 USD) → PPS + giờ UTC | < 1 µs | Khuyến nghị. Độc lập mạng; cùng chuẩn giờ GNSS với RTK sau này. Thêm 1 ăng-ten nhỏ |
| NTP/PTP từ gateway ↔ máy chủ | 1–10 ms | Dự phòng khi trong mái che mất GNSS. Đủ cho tín hiệu xe, không đủ khi ghép với vị trí RTK |
| RTC DS3231 (holdover) | ±2 ppm | Giữ giờ khi mất cả hai nguồn: lệch ~0,1 ms/phút, chấp nhận được trong một lượt thi |
- t_us: int64 micro giây UTC. Cạnh IO được bắt bằng ngắt + timer phần cứng, sai số ≤ 1 ms
- Mỗi snapshot mang clock: LOCKED (có PPS) · NTP · HOLDOVER · FREE. Engine được biết dữ liệu tin được tới đâu
3
Từ điển tín hiệu xe
Tên khoá trùng file config xe (Thu tín hiệu xe §9). Config lo phần dây đấu (pin, high/low-side, active); protocol chỉ mang nghĩa.
3.1 IO thân xe — bitmap io_bits (uint32)
| Bit | Khoá | 1 nghĩa là | Ghi chú |
| 0 | ignition | Khoá điện ON | Bài 1. Bo thu sống bằng nguồn thường trực, nên vẫn ghi được cạnh tắt máy |
| 1 | seatbelt | Đã cài dây an toàn | Chú ý: dây công tắc gốc thường báo "chưa cài", bo phải đảo. Lỗi xuyên suốt khi 1→0 |
| 2 | door | Có cửa đang mở | Low-side (tiếp mass) ở phần cứng, đã đảo về nghĩa chuẩn |
| 3 | handbrake | Phanh tay đang kéo | Bài 1, 3 |
| 4 | brake | Đang đạp phanh chân | Bài 2, 5, 8, tình huống nguy hiểm |
| 5 | clutch | Đang đạp côn | Xe số tự động: luôn 0 và clutch_present=false trong hồ sơ xe |
| 6 | reverse | Đang gài số lùi | Từ công tắc đèn lùi. Bài 7, 10 |
| 7 | left_ind | Xi-nhan trái đang nháy | Kênh xung, bo thu lọc: lên 1 ở xung đầu tiên, về 0 sau 1,0 s không có xung. Bo cũng ghi cạnh thô vào kênh phụ để đối chất |
| 8 | right_ind | Xi-nhan phải đang nháy | Như trên. Hazard = bit 7 & 8, engine suy ra |
| 9–15 | dự phòng | — | Còi, pha, cốt, sương mù, đèn vị trí: để dành cho chấm đường trường. Đặt chỗ, không bao giờ đổi nghĩa bit cũ |
| 16 | tamper | Hộp bo đã bị mở | Reed switch, chốt cứng tới khi kỹ thuật viên xác nhận |
| 17 | obd_ok | ECU đang trả lời OBD | — |
| 18–31 | dự phòng | — | Trạng thái thiết bị |
✅ Lọc ở bo thu, phán quyết ở engine. Bo thu chỉ làm điều hoà tín hiệu: chống rung (20 ms, chỉnh trong config), đảo cực, nhận ra xi-nhan đang nháy. Mọi thứ có ngưỡng sát hạch đều nằm ở engine.
3.2 Động cơ & chuyển động — từ OBD-II
| Khoá | Nguồn · đơn vị | Ghi chú |
| rpm | PID 0x0C · vòng/phút | Poll 10–20Hz. Chết máy, tính gear |
| speed_kmh | PID 0x0D · km/h | Tạm thời, thay cho tốc độ RTK chưa có. Độ phân giải 1 km/h; đủ cho "dừng hẳn" và gear, không đủ đo tụt dốc cm (việc của RTK) |
| valid | bool | false khi ECU không trả lời. Không được hiểu thành RPM = 0, để tránh phạt chết máy oan |
⚠️
Giờ của mẫu OBD: OBD là hỏi-đáp, mất 20–50 ms. Bo ghi
t_req_us (lúc hỏi) và
t_us (lúc nhận); engine lấy điểm giữa. Không tap CKP/ABS (lý do ở
Thu tín hiệu xe §5).
3.3 Engine tự suy ra (không có trên dây)
| Khoá | Cách tính (tham số nằm ở hồ sơ xe / rules, phía server) |
| gear | argmin |rpm/speed − k_i|; clutch=1 hoặc speed≈0 thì N/không xác định; reverse=1 thì R |
| stall | ignition=1 ∧ valid ∧ rpm < ngưỡng trong ≥ X ms (ngưỡng chờ QCVN) |
| hazard | left_ind ∧ right_ind, cạnh lên lệch nhau ≤ 300 ms |
| stopped | speed_kmh = 0 liên tục ≥ X ms (sau này ưu tiên tốc độ RTK) |
4
Map bài → tín hiệu xe
Chỉ phần chấm bằng tín hiệu xe. Cột cuối là phần chờ RTK, đợt này chưa làm.
| # | Bài | Tín hiệu xe — chấm được ngay | Chờ RTK |
| 1 | Xuất phát | ignition · seatbelt · left_ind · handbrake · rpm · thời gian | — |
| 2 | Dừng nhường người đi bộ | brake · stopped | Dừng đúng vạch |
| 3 | Đề-pa lên dốc | stall · handbrake · clutch · brake · thời gian | Đúng vạch, tụt dốc ≤ 50 cm |
| 4 | Vệt bánh xe + vuông góc | left_ind / right_ind | Bánh đúng vệt, đè vạch |
| 5 | Ngã tư đèn tín hiệu | brake · stopped (+ trạng thái đèn từ sân) | Vạch dừng |
| 6 | Chữ S | stall | Đè vạch |
| 7 | Ghép xe dọc | reverse · stall · thời gian | Vào chuồng, đè vạch |
| 8 | Tạm dừng đường sắt | brake · stopped | Đúng vạch |
| 9 | Thay đổi số | rpm · speed_kmh · clutch → gear | Đúng đoạn đường |
| 10 | Ghép xe ngang | reverse · stall | Vào ô, đè vạch |
| 11 | Kết thúc | right_ind | Qua vạch kết thúc |
| + | Tình huống nguy hiểm | t_exec của lệnh · brake · hazard · stopped | — |
| ↺ | Lỗi xuyên suốt | stall · door · seatbelt · speed_kmh · thời gian | Đè vạch |
5
Mô hình lấy mẫu
| Bản ghi | Khi nào | Lý do |
| IoEdge | Mỗi lần một bit logic đổi | Thời điểm chính xác tới ms (bật xi-nhan trước khi xuất phát bao lâu, phanh sau lệnh bao lâu). Lấy mẫu định kỳ 20Hz sẽ sai tới 50 ms |
| Obd | Mỗi phản hồi, 10–20Hz | rpm + speed trong cùng một bản ghi |
| State | 1Hz | Toàn bộ io_bits + điện áp + clock. Để engine tự đồng bộ lại khi bắt đầu giữa chừng, và phát hiện bit kẹt |
✅ Gateway gom thành một Batch mỗi 100ms, tức 10 bản tin/giây/xe: đủ nhanh để loa báo lỗi tức thời, đủ ít để chi phí header không đáng kể. Khi có RTK, Pose 20Hz vào cùng Batch này.
6
Vận chuyển
6.1 L0 — bo thu ↔ gateway
- UART 921600 8N1 (CAN 500k nếu gateway có). Khung: COBS( Envelope ‖ CRC-32 ) + byte 0x00 phân cách. Mất byte chỉ hỏng 1 khung, bắt nhịp lại ngay ở 0x00 kế tiếp
- Hai chiều: bo → gateway gửi Envelope; gateway → bo chuyển tiếp lệnh Down (bo thu là bên ký ACK)
- Bo thu có ring buffer RAM/flash riêng, nên gateway khởi động lại cũng không mất cạnh
6.2 L1 — xe ↔ máy chủ (MQTT 5)
| Topic | QoS | Nội dung |
| ys/{yard}/v/{serial}/up/batch | 1 | Envelope{Batch}, 10/s |
| ys/{yard}/v/{serial}/up/ctl | 1 | SessionAck · CmdAck · Checkpoint · Seal |
| ys/{yard}/v/{serial}/up/health | 0 | Nhiệt độ, dung lượng journal, RSSI, firmware (10s) |
| ys/{yard}/v/{serial}/down | 1 | Down{SessionOpen · SessionClose · Backfill · Announce · HazardTrigger · LessonMark} |
| ys/{yard}/v/{serial}/status | 1 · retain | online / LWT offline |
Vì sao MQTT
- LWT báo xe mất kết nối, không phải tự viết heartbeat
- Fan-out: recorder, engine, phòng điều hành cùng nhận; xe gửi 1 lần
- Broker on-prem nhẹ (Mosquitto/EMQX): rớt internet vẫn thi được
Bảo mật
- mTLS bằng chứng chỉ theo seri, do SUNS cấp qua CP (mô hình 3 tầng)
- ACL: thiết bị chỉ chạm được nhánh v/{serial} của chính nó
- Vẫn ký ở tầng ứng dụng (§9): TLS bảo vệ đường truyền, không tạo bằng chứng lưu trữ
7
Phiên thi
SS XE (bo thu qua gateway)
│── SessionOpen{session_id, exam_id, ───────────────▶│
│ candidate_ref, license_class, │ preflight: clock≠FREE · tamper=0
│ vehicle_profile_ver, rules_ver} │ obd_ok · ignition đọc được
│◀──────────── SessionAck{ok, fail[], fw_ver, ──────│
│ cfg_hash, first_seq} │
│ SS kiểm cfg_hash = bản đã kiểm chuẩn. │
│ Sai → không mở phiên │
│ │
│◀────────────────── Batch ×10/s · Checkpoint ×1/s ──│
│── LessonMark{lesson, enter|exit} ─────────────────▶│ tạm thời: phòng điều hành bấm
│── Announce{text_key, lesson, points} ─────────────▶│ loa trên xe
│── HazardTrigger{cmd_id} ──────────────────────────▶│
│◀──────────────────────── CmdAck{cmd_id, t_exec} ───│ t_exec = mốc tính phản ứng
│ │
│── SessionClose ───────────────────────────────────▶│
│◀──── Seal{first_seq, last_seq, chain_head, sig} ───│
│ thiếu seq → Backfill{from,to} … đủ mới chốt │
LessonMark — tạm thay geofence
- Chưa có RTK nên chưa tự biết xe đang ở bài nào. Vào/ra bài được đánh dấu từ phòng điều hành (hoặc nút trên xe khi chạy thử)
- Bản tin đi xuống xe, được bo thu ACK và đưa vào hash chain, nên mốc bài nằm cùng trục giờ với tín hiệu
- Khi có RTK, engine tự tính geofence và LessonMark chỉ còn dùng để chạy thử
Xe khởi động lại giữa bài
- boot_id tăng, seq về 0; bo nhớ session_id đang mở trong flash
- SS sắp theo (boot_id, seq), ghi khoảng trống là thiết bị gián đoạn, không lẫn với mất mạng
- Xử lý thí sinh (thi lại bài/huỷ lượt) là quy tắc nghiệp vụ theo TT 108
8
Không mất dữ liệu
Cơ chế
- seq uint32 tăng liên tục trong một boot_id, áp cho mọi Envelope
- Gateway ghi journal trước, gửi sau (append-only, ≥ 24h; chỉ xoá vòng khi SS xác nhận phiên đã chốt)
- SS thấy lỗ seq thì gửi Backfill{boot_id, from, to}, xe gửi lại với replay=true
Phân biệt nguyên nhân
- Mất mạng: thiếu seq, backfill lấp đủ
- Thiết bị gián đoạn: nhảy boot_id, hoặc journal cũng thiếu
- ECU không trả lời: seq đủ, Obd.valid=false
⚠️ Quy tắc chốt điểm: SS không phát kết quả cuối khi chưa đủ seq từ first_seq tới Seal.last_seq. Mất mạng giữa bài chỉ làm loa trên xe tạm im; dữ liệu vẫn đủ, kết quả vẫn đúng.
9
Toàn vẹn & ký số
hash_n = SHA-256( hash_{n-1} ‖ serial ‖ boot_id ‖ seq_n ‖ body_bytes_n ) // mỗi Envelope, tính trên bo thu
Checkpoint (1 Hz) = { seq, hash_seq, sig = ECDSA-P256(device_key, serial‖boot_id‖seq‖hash_seq) }
Seal (cuối phiên) = { session_id, first_seq, last_seq, chain_head, n_io, n_obd, sig }
device_key sinh và nằm trong ATECC608B trên bo thu, không bao giờ rời chip.
Public key + seri đăng ký lúc sản xuất → CP cấp xuống WS sát hạch.
Vì sao ký 1Hz, không ký từng lô
- ECDSA trên ATECC ~50 ms/lần, ký 10 lô/s sẽ ngốn ½ thời gian chip
- Hash chain nối mọi lô, nên một chữ ký ở checkpoint đã phủ toàn bộ lô trước nó
Bằng chứng cấu hình
- SessionAck mang fw_ver + cfg_hash (hash của file config xe)
- QCVN cấm tự ý đổi sau kiểm chuẩn, nên hash lệch thì không mở phiên
- Bit tamper trong mọi State: mở hộp giữa phiên thì gắn cờ phiên
10
Schema Protobuf v1
syntax = "proto3";
package suns.ys.v1;
// ===== Uplink: xe → máy chủ =====
message Envelope {
uint32 proto_ver = 1; // = 1
string serial = 2; // seri toàn cục do SUNS cấp
uint32 boot_id = 3;
uint32 seq = 4;
bool replay = 5; // gửi lại từ journal
bytes prev_hash = 6;
oneof body {
Batch batch = 10;
SessionAck session_ack = 11;
CmdAck cmd_ack = 12;
Checkpoint checkpoint = 13;
Seal seal = 14;
Health health = 15;
}
}
message Batch {
string session_id = 1; // rỗng khi ngoài phiên (vẫn ghi, vẫn gửi)
repeated IoEdge io = 2;
repeated Obd obd = 3;
State state = 4; // 1 lần/giây
reserved 5; // repeated Pose pose — RTK, làm sau
}
enum Clock { CLOCK_FREE = 0; HOLDOVER = 1; NTP = 2; LOCKED = 3; }
message IoEdge {
sint64 t_us = 1;
uint32 bit = 2; // vị trí bit trong io_bits
bool value = 3;
bool raw = 4; // true = cạnh thô kênh xung (xi-nhan), để đối chất
}
message Obd {
sint64 t_us = 1; sint64 t_req_us = 2;
uint32 rpm = 3; uint32 speed_kmh = 4; bool valid = 5;
}
message State {
sint64 t_us = 1; uint32 io_bits = 2; uint32 vbat_mv = 3; Clock clock = 4;
}
message SessionAck {
string session_id = 1; bool ok = 2;
repeated string fail = 3; // "CLOCK","TAMPER","OBD","CFG"…
string fw_ver = 4; bytes cfg_hash = 5; uint32 first_seq = 6;
reserved 7; // base_id — RTK, làm sau
}
message CmdAck { uint32 cmd_id = 1; bool ok = 2; sint64 t_exec_us = 3; }
message Checkpoint { uint32 seq = 1; bytes hash = 2; bytes sig = 3; }
message Seal {
string session_id = 1; uint32 first_seq = 2; uint32 last_seq = 3;
bytes chain_head = 4; uint32 n_io = 5; uint32 n_obd = 6; bytes sig = 7;
}
message Health { sint32 temp_c = 1; uint32 journal_free_mb = 2; sint32 rssi = 3; uint32 uptime_s = 4; }
// ===== Downlink: máy chủ → xe =====
message Down {
uint32 proto_ver = 1; uint32 cmd_id = 2;
oneof body {
SessionOpen open = 10;
SessionClose close = 11;
Backfill backfill = 12;
Announce announce = 13;
HazardTrigger hazard = 14;
LessonMark lesson = 15;
}
}
message SessionOpen {
string session_id = 1; string exam_id = 2;
string candidate_ref = 3; // mã tham chiếu — KHÔNG gửi PII xuống xe
string license_class = 4; string vehicle_profile_ver = 5; string rules_ver = 6;
reserved 7, 8; // datum_id, map_ver — RTK, làm sau
}
message SessionClose { string session_id = 1; }
message Backfill { uint32 boot_id = 1; uint32 from_seq = 2; uint32 to_seq = 3; }
message Announce { string text_key = 1; uint32 lesson = 2; sint32 points = 3; }
message HazardTrigger { uint32 kind = 1; }
message LessonMark { uint32 lesson = 1; bool enter = 2; }
Khi chốt, file này thành suns/ys/v1/*.proto, sinh code cho C (nanopb, bo thu), gateway và server từ cùng một nguồn. Các số reserved là chỗ cắm RTK.
11
Phiên bản & băng thông
Quy tắc tiến hoá
- Trong v1: chỉ thêm field/bit; không đổi số field, không đổi nghĩa bit
- Đổi không tương thích thì lên v2; SS đọc song song v1+v2 suốt vòng đời xe ngoài sân
- Ngưỡng chấm, ratios gear, debounce: nằm ở rules_ver / vehicle_profile_ver, không phải OTA
Ước lượng (chưa có RTK)
- Obd ~20 B × 20 = 400 B/s · edge + state ~80 B/s · header, hash, checkpoint ~400 B/s
- ≈ 0,9 KB/s/xe; 12 xe ≈ 11 KB/s, Wi-Fi hay 4G đều thừa
- Journal ≈ 32 MB/xe/ngày 10h. Thêm RTK sau thì khoảng gấp đôi
12
Đang chặn & việc mở
| Việc | Mức | Ghi chú |
| Danh mục lỗi + ngưỡng (TT 108/2026): chết máy bao lâu, xi-nhan trước bao lâu, quá giờ | 🔴 Chặn | Ảnh hưởng rules, không ảnh hưởng protocol |
| OBD-II trên xe sát hạch VN (i10, Morning, Vios, Accent…) có trả PID 0x0C / 0x0D ở 10–20Hz không | 🔴 Đo sớm | Không có → dây tacho + 6N137; tốc độ phải chờ RTK |
| Chọn gateway (SBC) + module GNSS timing + chip ATECC608B | ⚠️ | Gateway cần UART, lưu trữ ≥ 32 GB, Wi-Fi/4G |
| Bộ text_key câu nói loa (theo câu chuẩn của trung tâm sát hạch) | ⚠️ | Lấy từ thiết bị hiện có ở sân |
| Quy tắc khi xe reboot giữa bài | ⚠️ | Theo TT 108 |
| Bộ test tương thích: log mẫu → engine → kết quả kỳ vọng | 🟢 | Ghi log thật từ 1 xe, chạy thử với LessonMark |
| RTK: Pose, datum_id, geofence vào/ra bài, đè vạch | ⏸️ Để sau | Đã giữ chỗ trong schema |
Dựa trên: docs/RD/RTK/thu-tin-hieu-xe-thiet-ke.md · thiet-bi-rover-va-luong-du-lieu.md §8 · khung-phap-ly.md. Bản thảo v0.1, cần review cùng đội firmware trước khi chốt v1.