Đặc tả message, handshake, field, enum, heartbeat và quy tắc tương thích của HLP/1.
Đây là data contract giữa bên phát (Waze mod Gateway → HudLink) và bên nhận (HUD ESP32/Arduino hoặc renderer bất kỳ). Giao thức không phụ thuộc transport: cùng một chuỗi byte có thể chạy qua Bluetooth SPP, BLE có phân mảnh, TCP hoặc cáp serial. Chỉ cần định nghĩa một lần và triển khai ở cả hai phía.
Mục tiêu thiết kế: rất dễ parse trên MCU, tương thích về sau, tự mô tả và chịu được kết nối mất gói. Mỗi message độc lập; mất một message chỉ khiến HUD cũ hơn một chút.
\n (0x0A). Có thể có \r ngay trước \n. \r là khoảng trắng hợp lệ trong JSON nên parser chuẩn sẽ bỏ qua; bên nhận C dùng char buffer PHẢI xóa \r cuối dòng trước khi thêm null terminator, không chỉ bỏ qua nó lúc nhận.alrs, rút ngắn st/st2 và bỏ các field tùy chọn cho tới khi frame vừa giới hạn. Bên nhận nên dùng buffer 512 byte và bỏ dòng quá dài.\n kế tiếp.\n. Framing dựa trên byte stream nên việc chia packet là trong suốt với protocol.Mọi message đều có key loại t và phiên bản protocol v:
{"v":1,"t":"s", ...fields...}
t | Ý nghĩa | Hướng | Thời điểm |
|---|---|---|---|
dev | Khai báo thiết bị | bên nhận → bên phát | Một lần khi kết nối; thiết bị tự giới thiệu |
hi | Khai báo bên phát | bên phát → bên nhận | Trả lời dev hoặc sau timeout ở §2.5 |
s | Cập nhật trạng thái | bên phát → bên nhận | Khi field thay đổi và theo heartbeat ở §5 |
ping | Kiểm tra kết nối sống | hai chiều | Bất kỳ lúc nào; peer nên trả pong ngay |
pong | Phản hồi kết nối sống | hai chiều | Trả lời ping |
bye | Chuẩn bị ngắt | hai chiều | Dừng dẫn đường, đóng app hoặc bên nhận rời đi |
cfg_begin / cfg_item / cfg_end | Schema cấu hình thiết bị | bên nhận → bên phát | Tùy chọn, sau hi (§2.6) |
cfg_set_begin / cfg_set / cfg_set_commit | Transaction cấu hình | bên phát → bên nhận | Sau khi người dùng bấm Áp dụng (§2.6) |
cfg_ack | Kết quả cấu hình | bên nhận → bên phát | Sau khi kiểm tra và lưu bền vững (§2.6) |
Quy tắc tương thích về sau — cả hai phía PHẢI tuân theo:
t chưa biết.v. Bên nhận bỏ qua message có v không hỗ trợ.Ở mỗi kết nối mới qua SPP, BLE hoặc TCP, hai phía khai báo trước khi stream để biết phiên bản protocol, capability và tùy chọn của nhau. Bên phát có quyền quyết định cuối cùng và echo tham số đã thương lượng trong hi; bên nhận thích ứng theo kết quả đó.
kết nối được thiết lập
bên nhận ──► dev (identity, phiên bản protocol, màn hình, capability, nhu cầu)
bên phát ──► hi (identity, protocol, capability và rate/field/protocol ĐÃ CHẤP NHẬN)
bên phát ──► s … (stream ở rate đã chấp nhận, chỉ mang field đã chấp nhận)
...ping/pong, heartbeat...
một trong hai ──► bye
Fallback cho bên nhận đơn giản: thiết bị không gửi được declaration có thể chỉ lắng nghe. Nếu bên phát không nhận dev trong 500 ms sau khi kết nối, nó dùng mặc định proto:1, rate mặc định của transport và toàn bộ field baseline gọn, sau đó gửi hi rồi s. Binding BLE hiện tại chấp nhận rate 4 trong fallback này. Mảng tùy chọn alrs vẫn bị bỏ vì có thể chiếm phần lớn budget 512 byte. Nhờ vậy firmware chỉ đọc dòng và parse vẫn hoạt động mà không cần logic handshake.
hi có tính idempotent và CÓ THỂ được gửi lại. Bên nhận PHẢI chấp nhận hi mới và áp dụng tham số mới. Implementation hiện tại dùng việc này cho một trường hợp cụ thể:
dev đến muộn, sau hi mặc định ở mốc 500 ms: bên phát thương lượng lại và gửi hi đã cập nhật. Không có race condition; hi cuối cùng thắng.Android hiện tại không phát lại capability khi dữ liệu Waze xuất hiện hoặc biến mất. Hãy xem caps là nhóm field đã thương lượng cho session; việc widget hiện có dữ liệu hay không phải dựa vào giá trị mặc định của field bị thiếu trong từng state.
Danh tính session (sess). hi chứa một sess riêng cho từng session. Mỗi khi sess thay đổi, kể cả sau khi kết nối lại, bên nhận PHẢI coi đây là session mới: xóa state cache và gửi lại dev nếu có hỗ trợ declaration.
dev — khai báo của bên nhận{
"v": 1,
"t": "dev",
"name": "WazeHUD-ESP32",
"fw": "1.0.0",
"proto": [1],
"disp": { "w": 240, "h": 240, "color": 1 },
"can": ["speed", "limit", "turn", "street", "eta", "avgzone", "alerts"],
"want": {
"rate": 4,
"fields": ["spd", "lim", "trn", "trn2", "dst", "exit", "st", "eta", "rmin"]
}
}
| Key | Kiểu | Ý nghĩa |
|---|---|---|
name | string | Tên thiết bị/model |
fw | string | Phiên bản firmware bên nhận |
proto | int[] | Các phiên bản HLP bên nhận hỗ trợ, ví dụ [1] hoặc [1,2] |
disp | object | Capability màn hình: w, h theo pixel và color 0/1; chỉ để tham khảo, bên phát không render |
can | string[] | Capability token bên nhận có thể render, xem §2.5.3 |
want | object | Tùy chọn rate tối đa và tập con fields ở §3; bỏ qua nghĩa là nhận tập baseline |
Mọi key ngoài t và v đều không bắt buộc. Khi thiếu, bên phát dùng mặc định.
hi — khai báo bên phát và tham số được chấp nhận{
"v": 1,
"t": "hi",
"app": "waze",
"appv": "5.20.90.901",
"name": "WazeHUD",
"sess": 48213,
"proto": 1,
"caps": ["speed", "limit", "turn", "street", "eta", "avgzone", "alerts"],
"rate": 4,
"fields": ["spd", "lim", "trn", "trn2", "dst", "exit", "st", "eta", "rmin"]
}
| Key | Kiểu | Ý nghĩa |
|---|---|---|
app | string | ID ứng dụng bên phát |
appv | string | Phiên bản ứng dụng bên phát |
name | string | Tên link của bên phát |
sess | int | ID duy nhất mỗi kết nối để phát hiện restart |
proto | int | Phiên bản HLP được chấp nhận; bên phát hiện tại chỉ hỗ trợ 1 |
caps | string[] | Capability bên phát thực sự cung cấp trong session |
rate | int | Update rate đã chấp nhận, tính bằng Hz |
fields | string[] hoặc * | Field state đã chấp nhận; * legacy là toàn bộ baseline, không gồm opt-in như alrs |
can / caps)Các nhóm tính năng cấp cao, độc lập với từng field:
| Token | Bao gồm field | Ý nghĩa |
|---|---|---|
speed | spd,over | Tốc độ hiện tại |
limit | lim | Biển giới hạn tốc độ |
turn | trn,trn2,dst,exit | Hướng rẽ và khoảng cách |
street | st,st2 | Tên đường |
eta | eta,rmin,rkm | Giờ đến và phần đường còn lại |
avgzone | avg,avgL,avgR,avgP | Vùng cấm vượt tại Việt Nam |
alerts | alr,alrD,alrV, cộng alrs nếu opt-in | Biển báo/cảnh báo sắp tới |
device_config | message cfg_* | Android hiển thị cài đặt do thiết bị sở hữu |
lanes | dành cho phiên bản tương lai | Hướng dẫn làn đường |
Capability
alertsbao gồm biển gần nhất quaalr/alrD/alrV. Mảng đầy đủalrsmặc định không thuộcalerts; bên nhận phải ghi thêmalrstrongdev.want.fields. HUD chỉ khai báocan:["alerts"]vẫn nhận message nhỏ. Thiết bị không gửi declaration sẽ không nhậnalrs.
Quy tắc thương lượng — bên phát quyết định và echo trong hi:
dev.proto có mặt nhưng không chứa 1, Android gửi UNSUPPORTED_VERSION rồi đóng session.rate bị giới hạn trong 1..4 Hz với BLE và 1..10 Hz với Classic SPP. Khi thiếu, mặc định là 4 Hz cho BLE và 8 Hz cho SPP.fields là giao của dev.want.fields và field được hỗ trợ. Nếu thiếu hoặc là "*", bên phát nhận toàn bộ field baseline nhưng vẫn loại alrs; muốn nhận mảng này phải ghi rõ alrs. Message s chỉ chứa các key đã chấp nhận cộng v, t, ts. Tên field lạ trong want.fields bị bỏ qua im lặng.can và disp hiện chỉ mang tính thông tin. caps được suy ra từ nhóm field đã chấp nhận; nó không được lọc động theo dữ liệu tuyến hiện tại hoặc theo dev.can.Thiết bị HLP có thể công bố cài đặt riêng trong Cài đặt Mod → Thiết bị. Đây là phần bổ sung tương
thích ngược của HLP/1, nên peer cũ sẽ bỏ qua message lạ. Android không lưu hoặc tự tạo schema. Nếu
không nhận đủ schema sau hi, vùng cấu hình thiết bị hoàn toàn không xuất hiện.
Thiết bị gửi một schema nguyên tử, tối đa 32 mục:
{"v":1,"t":"cfg_begin","rev":7,"count":2,"title":"Cấu hình HUD"}
{"v":1,"t":"cfg_item","rev":7,"id":"brightness","kind":"slider","label":"Độ sáng","value":70,"min":10,"max":100,"step":5}
{"v":1,"t":"cfg_item","rev":7,"id":"theme","kind":"selection","label":"Giao diện","value":"auto","options":[{"value":"auto","label":"Tự động"},{"value":"night","label":"Ban đêm"}]}
{"v":1,"t":"cfg_end","rev":7}
Hỗ trợ toggle (boolean), slider (số nguyên có min/max/step), selection (chuỗi và 1–32
options), integer (số nguyên có min/max) và text (UTF-8, maxLength tùy chọn, tối đa 128
code point). id phải khớp [A-Za-z0-9_.-]{1,40}; label tối đa 64 code point và description
tối đa 160. Mỗi frame vẫn bị giới hạn 512 byte. Android chỉ publish schema đủ item, cùng revision và
không trùng id.
Control chỉ sửa bản nháp cục bộ. Android không gửi gì cho tới khi bấm Áp dụng, sau đó enqueue nguyên tử một transaction chứa toàn bộ giá trị:
{"v":1,"t":"cfg_set_begin","tx":12,"rev":7,"count":2}
{"v":1,"t":"cfg_set","tx":12,"id":"brightness","value":80}
{"v":1,"t":"cfg_set","tx":12,"id":"theme","value":"night"}
{"v":1,"t":"cfg_set_commit","tx":12}
Thiết bị PHẢI giữ bản staging riêng, kiểm tra tx, rev, count, id, kiểu và range, chỉ thay đổi state
đang chạy/NVS sau commit hợp lệ, rồi trả một trong hai dạng:
{"v":1,"t":"cfg_ack","tx":12,"ok":true,"rev":8}
{"v":1,"t":"cfg_ack","tx":12,"ok":false,"field":"brightness","error":"out of range"}
Android chỉ cho một transaction đang chờ, timeout sau 10 giây và chỉ nhận giá trị mới khi ACK thành
công đúng tx. Link vật lý mới sẽ xóa schema của thiết bị trước. Project ESP-IDF mẫu đã có staging
và NVS trong shared/hlp_device_config.c.
Xem Cấu hình thiết bị động để đọc hướng dẫn triển khai, bảng control, checklist production và kết quả kiểm thử trên thiết bị thật.
sKey được viết ngắn để tiết kiệm băng thông và RAM MCU. Mọi field đều tùy chọn; dùng mặc định khi thiếu.
| Key | Kiểu | Đơn vị / khoảng | Mặc định | Ý nghĩa |
|---|---|---|---|---|
nav | int | 0/1 | 0 | 1 khi đang dẫn đường theo tuyến |
spd | int | km/h, ≥0 | 0 | Tốc độ GPS hiện tại |
lim | int | km/h, 0 = chưa biết | 0 | Giới hạn tốc độ; 0 thì ẩn biển |
over | int | 0/1 | 0 | 1 khi vượt quá giới hạn và margin của mod |
trn | int | enum hướng rẽ §4 | 0 | Hướng rẽ kế tiếp |
trn2 | int | enum hướng rẽ §4 | 0 | Hướng rẽ sau hướng kế tiếp |
dst | int | mét, -1 = không có | -1 | Khoảng cách tới hướng rẽ kế tiếp |
exit | int | ≥0, 0 = không áp dụng | 0 | Số lối ra vòng xuyến |
st | string | ≤40 ký tự | "" | Tên đường hiện tại |
st2 | string | ≤40 ký tự | "" | Tên đường sau hướng rẽ |
eta | string | HH:MM, 24 giờ | "" | Giờ đến dự kiến |
rmin | int | phút, ≥0 | 0 | Thời gian còn lại |
rkm | number | km, làm tròn 1 chữ số thập phân | 0 | Quãng đường còn lại; JSON có thể ghi 6 thay vì 6.0 |
avg | int | 0/1 | 0 | Đang trong vùng cấm vượt |
avgL | int | mét | 0 | Quãng đường còn lại trong vùng |
avgR | int | km/h | 0 | Tốc độ khuyến nghị trong vùng |
avgP | int | 0..100 | 0 | Tiến độ qua vùng theo phần trăm |
alr | int | enum cảnh báo §4.2 | 0 | Loại biển/cảnh báo gần nhất, mirror alrs[0].k |
alrD | int | mét, -1 = không có | -1 | Khoảng cách tới cảnh báo gần nhất |
alrV | int | km/h | không có | Giá trị của cảnh báo gần nhất; chỉ dùng khi loại cảnh báo có giá trị |
alrs | array | tối đa 4 object, opt-in | không có | Danh sách cảnh báo gần→xa: [{"k":code,"d":meters,"v":opt}] |
ts | int | ms | 0 | Uptime của bên phát để kiểm tra thứ tự và độ cũ |
Thứ tự và độ cũ: dùng
(sess, ts)cùng nhau, không dùng riêngts.tslà uptime chứ không phải wall clock. Nếu process bên phát được tạo lại trong cùng transport,tsquay về giá trị nhỏ. Message mới hơn khisesskhác—luôn chấp nhận session mới—hoặc khisessgiống vàtslớn hơn.sesslấy từhiở §2.5.
Định dạng số. Bên phát làm tròn
rkmtới một chữ số thập phân trước khi đưa vào JSON. Bên nhận phải đọc nó như số; serializer có thể bỏ.0khi kết quả là số nguyên.
avg= vùng cấm vượt. Waze giao dữ liệu dưới tên “average-speed-camera zone”, nhưng tại Việt Nam geometry này được editor dùng cho vùng cấm vượt. HUD phải vẽ biển cấm vượt cùng đếm ngượcavgL, không vẽ camera.avgRlà tốc độ Waze khuyến nghị.
alr/alrD/alrVvàalrs. Ba field đầu mô tả cảnh báo gần nhất và mirroralrs[0], nên firmware không parse mảng vẫn dùng được.alrslà danh sách đầy đủ tối đa bốn cảnh báo, gần tới xa, và chỉ được gửi khi bên nhận ghialrstrongdev.want.fields. Bên trong mảng, key loại làk, không phảit, để không nhầm với loại message ở envelope.kdùng cùng enum vớialr;vlà tùy chọn tương ứngalrV.
Ví dụ tối thiểu sau khi thương lượng một tập field nhỏ, khi không dẫn đường:
{ "v": 1, "t": "s", "spd": 0, "lim": 50, "st": "Nguyễn Trãi", "ts": 123456 }
Ví dụ đầy đủ khi đang dẫn đường:
{
"v": 1,
"t": "s",
"nav": 1,
"spd": 47,
"lim": 50,
"over": 0,
"trn": 8,
"trn2": 2,
"dst": 47,
"exit": 0,
"st": "Nguyễn Trãi",
"st2": "Khuất Duy Tiến",
"eta": "20:01",
"rmin": 23,
"rkm": 6.7,
"avg": 0,
"alr": 2,
"alrD": 300,
"alrs": [
{ "k": 2, "d": 300 },
{ "k": 8, "d": 800, "v": 40 }
],
"ts": 124890
}
trn, trn2Đây là mã HUD ổn định. Bên phát map Instruction$Type nội bộ của Waze sang các mã này; bên nhận vẽ bitmap mũi tên cục bộ. Với mã lạ, không vẽ gì và xử lý như 0.
| Mã | Giá trị / ý nghĩa | Mã | Giá trị / ý nghĩa |
|---|---|---|---|
| 0 | NONE, không có | 10 | ROUNDABOUT, dùng exit |
| 1 | CONTINUE, đi thẳng | 11 | ROUNDABOUT_LEFT, dùng exit |
| 2 | LEFT, rẽ trái | 12 | ROUNDABOUT_RIGHT, dùng exit |
| 3 | RIGHT, rẽ phải | 13 | KEEP_LEFT, giữ trái |
| 4 | SLIGHT_LEFT, chếch trái | 14 | KEEP_RIGHT, giữ phải |
| 5 | SLIGHT_RIGHT, chếch phải | 15 | EXIT_LEFT, ra nhánh trái |
| 6 | SHARP_LEFT, rẽ gắt trái | 16 | EXIT_RIGHT, ra nhánh phải |
| 7 | SHARP_RIGHT, rẽ gắt phải | 17 | ARRIVE, tới nơi |
| 8 | U_TURN, hiện dùng cho cả hai phía | 18 | FERRY, dành trước, hiện chưa phát |
| 9 | U_TURN_RIGHT, dành trước, hiện chưa phát |
alr| Mã | Giá trị / ý nghĩa | Mã | Giá trị / ý nghĩa |
|---|---|---|---|
| 0 | NONE, không có | 5 | ACCIDENT, tai nạn |
| 1 | POLICE, cảnh sát | 6 | TRAFFIC_JAM, kẹt xe |
| 2 | CAMERA_SPEED, camera tốc độ | 7 | ROAD_CLOSED, đường đóng |
| 3 | CAMERA_REDLIGHT, camera đèn đỏ | 8 | SPEED_DROP, giảm giới hạn tốc độ |
| 4 | HAZARD, nguy hiểm | 9 | NO_PASSING, cấm vượt |
hi khi kết nối, sau đó gửi message s.s khi bất kỳ field nào thay đổi và ít nhất một heartbeat s mỗi 1000 ms nếu state không đổi. BLE phát theo rate đã thương lượng, giới hạn trong 1–4 Hz. Heartbeat chỉ cần khi không có thay đổi trong ít nhất 1000 ms; khi dữ liệu đang đổi, rate đã thương lượng tự đảm bảo nhịp.ping mỗi 5000 ms trên cả hai transport. HUD nên gửi pong ngay, trước khi render hoặc làm tác vụ chậm khác. Android ghi nhận hoạt động của peer để hiển thị trạng thái/chẩn đoán, nhưng hiện không tự ngắt chỉ vì thiếu pong; kết nối lại được kích hoạt bởi transport disconnect hoặc write lỗi/timeout.0 nghĩa là không có đối với lim, exit, avg*, alr; -1 nghĩa là không có đối với dst, alrD, vì khoảng cách 0 m vẫn hợp lệ.alrV và alrs bị bỏ hoàn toàn khi không áp dụng, không dùng sentinel. alrV chỉ xuất hiện cho loại cảnh báo mang giá trị như SPEED_DROP.v:1. Thay đổi không tương thích phải chuyển sang HLP/2 với v:2; trong giai đoạn chuyển tiếp bên phát có thể hỗ trợ cả hai. Field bổ sung vẫn thuộc v1.Trạng thái: ĐÃ TRIỂN KHAI v1. SPP và BLE vận chuyển cùng một message logic. Codec tham chiếu Android và ESP32 đều áp dụng giới hạn frame 512 byte. Lần sửa protocol tiếp theo phải tăng v nếu thay đổi envelope hoặc ý nghĩa field.
Protocol ứng dụng giống nhau trên Classic RFCOMM SPP và BLE GATT.
Classic SPP dùng Bluetooth SIG Serial Port UUID 00001101-0000-1000-8000-00805F9B34FB. Android là RFCOMM client bảo mật; thiết bị ngoại vi publish SDP service.
BLE dùng các UUID 128-bit cố định:
| UUID | Hướng / công dụng |
|---|---|
8a7e0001-4d6e-4c48-9a9d-484c504c0001 | HLP service |
8a7e0002-4d6e-4c48-9a9d-484c504c0001 | Android → thiết bị, TX write |
8a7e0003-4d6e-4c48-9a9d-484c504c0001 | Thiết bị → Android, RX notification |
8a7e0004-4d6e-4c48-9a9d-484c504c0001 | Capability tùy chọn |
8a7e0005-4d6e-4c48-9a9d-484c504c0001 | Trạng thái tùy chọn |
Ranh giới packet BLE không mang ý nghĩa protocol. Một frame có thể bị chia qua nhiều write hoặc notification; bên nhận ghép byte cho tới LF. Android yêu cầu MTU 247 nhưng vẫn chạy với MTU 23, chia frame thành chunk tối đa MTU - 3 và tuần tự hóa write with response. Chunk mới chỉ được gửi sau callback của chunk trước; mỗi chunk có timeout ba giây. Firmware phải copy byte nhận vào queue giới hạn rồi thoát nhanh khỏi callback GATT.
Toàn bộ frame đã mã hóa, gồm LF cuối, dài tối đa 512 byte. Khi tràn, bên nhận bỏ byte tới LF kế tiếp rồi tiếp tục framing. UTF-8 lỗi, JSON lỗi, phiên bản không hỗ trợ, thương lượng không hợp lệ và các lỗi protocol khác có thể được báo bằng:
{ "v": 1, "t": "error", "code": "FRAME_TOO_LARGE", "detail": "..." }
Message lỗi chỉ phục vụ chẩn đoán và không bắt buộc với bên nhận legacy. Loại message chưa biết vẫn phải được bỏ qua để giữ tương thích về sau.