ต่อ Hardware/Vendor API ให้รอด: เชื่อ firmware ไม่ใช่ PDF และเตรียม enrich/poll fallback ไว้เสมอ
บทเรียนจริงจากการต่อกล้อง/NVR/NAS/บอร์ด — spec sheet เป็นแค่ hint, push event มักไม่มี identity, upload-then-commit ต้องใช้ server handle และ hardware constraint พลิก architecture ได้ทั้งชุด
อ่าน ~8 นาที
Thesis: vendor doc คือสมมติฐาน ไม่ใช่ contract
ผมต่อ hardware API มาหลายรุ่น — กล้อง IP, NVR, NAS, บอร์ด embedded — และบทเรียนแรกที่แลกมาด้วยเวลาหลายวันคือ PDF spec sheet ของ vendor เป็นแค่ hint ไม่ใช่สัญญา. เอกสารมักเขียนจาก firmware รุ่นใดรุ่นหนึ่ง กับ device class หนึ่ง แล้วถูก copy ข้ามรุ่นโดยไม่มีใคร verify. endpoint ที่เขียนไว้อาจมีจริง อาจ "documented-but-unimplemented" (ยิงไปแล้วตอบ notSupport) หรืออยู่คนละ path บน firmware ที่คุณถืออยู่จริง.
วิธีที่เชื่อถือได้มีทางเดียว: จับ request/response จริงจาก client ของ vendor เอง. เปิด official app หรือ web UI ของอุปกรณ์ แล้วดักดูว่ามันคุยอะไรออกไป — ผ่าน browser DevTools, mitmproxy, หรือ packet capture. สิ่งที่ firmware ตอบกลับจริงคือ ground truth เดียว. อย่าเขียน integration จาก PDF แล้วหวังว่ามันจะตรง; เขียนจากสิ่งที่คุณเห็นกับตา.
Endpoint คนละ device class และกับดักการเดา vendor จาก URL
อุปกรณ์รุ่นเดียวกันแต่คนละ class (กล้องตัวเดียว vs. เครื่องบันทึกหลายช่อง) มักใช้ endpoint คนละชุด แม้ path หน้าตาเหมือนกัน. การ query "channel list" บนตัวหนึ่งอาจเป็น root path, บนอีกตัวต้องระบุ channel id ต่อท้าย, และบางตัวก็ตอบ 200 พร้อม body ว่าง (implement ไม่ครบ) โดยไม่ใช่ error.
กับดักที่แพงที่สุดคือ การเดา vendor หรือสถาปัตยกรรมจากรูปทรงของ URL. ผมเคยเห็น path แบบ /ISAPI/... แล้วสมองลากไปทั้งพวง — สรุปเอาเองว่า "งั้นต้องใช้ binary SDK ของเจ้านั้น, ต้องรันบน x86, ต้องมี native dll". พอลงมือจริงกลายเป็นว่าอุปกรณ์พูด HTTP ธรรมดา ต่อได้ด้วย client ภาษาอะไรก็ได้. false assumption หนึ่งข้อลากสมมติฐานผิดตามมาทั้งชุด. หลักการ: URL shape ไม่ใช่ identity ของ vendor — verify capability ทีละอย่างด้วย request จริง อย่า inference เป็นลูกโซ่.
Isolate validation error ด้วยการ bisect ทีละ field
ปัญหาคลาสสิกของ hardware API คือ error message ที่ opaque — ยิง payload ไป แล้วได้ invalidContent หรือ 4 กลับมา ไม่บอกว่า field ไหนผิด. อย่านั่งเดา. ให้ bisect: เริ่มจาก minimal payload ที่ผ่าน แล้วเติมทีละ field จนกว่ามันจะพัง — field ที่ทำให้พังคือตัวปัญหา.
// จาก payload เต็มที่ error หา field ที่ผิดด้วยการค่อยๆ เติม
const base = { name: "u1", type: "normal" }; // minimal ที่ผ่าน
const candidates = [
{ key: "gender", value: "unknown" }, // enum? ต้อง male/female เท่านั้น
{ key: "userId", value: "AB-12" }, // charset? รับแค่ alphanumeric
{ key: "Valid", value: "true" }, // case? attribute vs element ต่างกัน
];
for (const c of candidates) {
const res = await putConfig({ ...base, [c.key]: c.value });
console.log(c.key, res.statusCode, res.subStatusCode); // ตัวไหนพัง = ตัวนั้น
}
สิ่งที่ error ไม่เคยบอกแต่ทำให้พังบ่อย: charset ที่จำกัด (บาง field รับเฉพาะ ASCII/alphanumeric, มี dash หรือ underscore ไม่ได้), enum ที่ต้องตรงเป๊ะ (ค่าที่ไม่รู้จักต้อง omit ไม่ใช่ส่ง "unknown"), case sensitivity และ attribute vs element ใน XML. อีกเรื่องที่คนพลาดกันเยอะ: binary body ห้าม round-trip ผ่าน string. ถ้ารูปหรือไฟล์ถูก decode เป็น UTF-8 แล้ว re-encode ระหว่างทาง byte จะเพี้ยนทั้งก้อน — จัดการเป็น Buffer/bytes ตั้งแต่ต้นจนจบ อย่าให้ layer ไหนแปลงเป็น text.
Push vs query เป็นคนละ data model — push มักไม่มี identity
อุปกรณ์จำนวนมาก push event เข้ามาแบบ real-time (มีคนเดินผ่าน, ตรวจจับได้) แต่ event ที่ push มา by design มักไม่มี identity ที่คุณต้องการ — มันบอกว่า "เกิดเหตุที่ channel นี้ เวลานี้" แต่ไม่บอกว่า "คนนี้คือใคร" เพราะข้อมูลนั้นอยู่คนละ query. push ถูกออกแบบให้เบาและเร็ว ไม่ใช่ครบ.
ทางแก้คือ enrich: รับ push แล้ว correlate กับข้อมูลจาก query อื่นด้วย key ร่วม — เวลา, รูป, channel — แล้ว cache ผลไว้. แต่ระวัง pitfall ใหญ่: correlation-by-time พังเมื่อ concurrency สูง. ถ้าจับคู่ event กับ record ด้วย "timestamp ใกล้ที่สุด" พอมี event สองอันมาชิดกันในเสี้ยววินาที คุณจะจับคู่สลับกันโดยไม่รู้ตัว.
// correlation-by-time: ใช้ได้ตอน traffic เบา พังตอน burst
function correlate(pushEvent, recentRecords) {
return recentRecords
.filter(r => Math.abs(r.ts - pushEvent.ts) < 2000) // ±2s window
.sort((a, b) => Math.abs(a.ts - pushEvent.ts) - Math.abs(b.ts - pushEvent.ts))[0];
}
// ปลอดภัยกว่า: มี correlation id/sequence จาก payload ก็ใช้ก่อนเสมอ
// ไม่มี id → time window + channel + de-dupe และยอมรับว่ามี ambiguity rate
หลักการ: หา correlation key ที่ deterministic (id, sequence, channel-scoped counter) ก่อนเสมอ. ถ้าไม่มีจริงๆ ค่อย fallback มา time-window แต่ต้อง scope ให้แคบที่สุด (ต่อ channel, ต่อ device) และยอมรับว่ามัน best-effort ไม่ใช่ exact.
Upload → process → commit: echo server handle กลับ ไม่ใช่ filename
flow ยอดฮิตของ API ที่รับไฟล์คือสามจังหวะ: upload ก้อน binary → server ประมวลผล → เรา commit เพื่อผูกไฟล์เข้ากับ record. จุดพลาดคือ ตอน commit ต้องส่ง handle ที่ server generate ให้ กลับไป ไม่ใช่ filename ที่เราส่งขึ้นไป.
// ผิด: commit ด้วยชื่อไฟล์ที่ "เรา" ตั้ง
await commit({ recordId, file: "photo.jpg" }); // server ไม่รู้จัก
// ถูก: อ่าน handle จาก response ของ upload แล้ว echo กลับ
const up = await upload(bytes); // { fileId: "srv-8f2a...", url: "..." }
await commit({ recordId, fileId: up.fileId }); // ผูกด้วย server handle
pattern นี้เหมือน presigned upload บน cloud storageเป๊ะ: ขอ presigned URL → PUT ไฟล์ → ได้ object key → commit ด้วย key นั้น. server เป็นคนถือ namespace ของไฟล์ ไม่ใช่ client. ถ้าคุณ commit ด้วยชื่อที่ตัวเองตั้ง server จะหาไฟล์ไม่เจอ หรือแย่กว่านั้นคือไปชนไฟล์อื่น. ยึด handle ที่ server คืนมาเสมอ.
Hardware/protocol constraint คือ architectural driver
บทเรียนที่เปลี่ยนวิธีคิดผมมากที่สุด: ข้อจำกัดของ hardware กำหนด architecture ทั้งชุด ไม่ใช่แค่รายละเอียด implementation. ตอนที่ผมเลิกยึด binary SDK แล้วหันไปคุยผ่าน HTTP WebAPI แทน สิ่งที่ตามมาคือระบบรันบน ARM (บอร์ดเล็กกินไฟต่ำ) ได้ทันที — เพราะไม่ต้องผูกกับ native library ที่ compile ไว้เฉพาะ x86. constraint เรื่อง CPU arch กับ protocol layer พลิกทั้ง deployment target.
เมื่อต้องรองรับหลาย vendor ที่พูดคนละ dialect ให้สร้าง anti-corruption layer: auto-detect dialect ที่ขอบ parser แล้วซ่อนความต่างไว้หลัง interface เดียว. โครงสร้างนี้ทำให้ "เพิ่ม vendor = เพิ่ม adapter ตัวเดียว" ไม่ต้องแตะ business logic.
// anti-corruption layer: business logic เห็น interface เดียว
interface DeviceAdapter {
listChannels(): Promise<Channel[]>;
enroll(user: User, photo: Buffer): Promise<void>;
onEvent(cb: (e: NormalizedEvent) => void): void;
}
function makeAdapter(probe: ProbeResult): DeviceAdapter {
if (probe.dialect === "isapi-like") return new IsapiAdapter(probe.host);
if (probe.dialect === "webapi-json") return new WebApiAdapter(probe.host);
throw new Error(`unknown dialect: ${probe.dialect}`);
}
// เพิ่ม vendor ใหม่ = เขียน adapter ใหม่ 1 ตัว + เพิ่ม 1 branch ที่ detect
detect dialect จาก response จริงของ probe (header, root endpoint, error shape) ไม่ใช่จากค่า config ที่คนกรอกมือ — เพราะคนกรอกผิดได้ แต่ firmware ไม่โกหก.
สรุป: หลักการที่เอาไปใช้ได้ทุก vendor
- เชื่อ firmware ไม่ใช่ PDF — verify ทุก endpoint ด้วย request/response จริงที่จับจาก client ของ vendor เอง
- URL shape ไม่ใช่ identity ของ vendor — อย่า inference เป็นลูกโซ่ (path → SDK → arch); เช็ค capability ทีละอย่าง
- bisect validation error ทีละ field จาก minimal payload; ระวัง charset/enum/case/attribute ที่ error ไม่บอก และห้าม round-trip binary ผ่าน string
- push ไม่มี identity เป็นเรื่องปกติ — enrich ด้วย query + cache; ใช้ deterministic key ก่อน, correlation-by-time เป็น fallback ที่พังตอน concurrency สูง
- upload-then-commit ต้อง echo server handle ไม่ใช่ filename ที่ส่งไป — คิดแบบ presigned upload
- hardware constraint เป็น architectural driver — HTTP แทน binary SDK เปิดทางไป ARM; anti-corruption layer + auto-detect ทำให้เพิ่ม vendor = เพิ่ม adapter ตัวเดียว




