EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Network / ใช้ NVR เป็นประตูเข้าหากล้องที่ยิงตรงไม่ถึง ด้วย ISAPI และ SDK
Network

ใช้ NVR เป็นประตูเข้าหากล้องที่ยิงตรงไม่ถึง ด้วย ISAPI และ SDK

โดย Ton-Or ·16 ก.ย. 2026·อ่าน 11 นาที
ใช้ NVR เป็นประตูเข้าหากล้องที่ยิงตรงไม่ถึง ด้วย ISAPI และ SDK

NVR ไม่ได้เป็นแค่ที่เก็บคลิป มันเป็นเครื่องเดียวในระบบที่คุยกับกล้องทุกตัวได้ ตอนที่เราเข้าถึงกล้องตรงไม่ได้เลย NVR เลยกลายเป็นประตูบานเดียวที่เหลืออยู่ และประตูบานนี้เปิดได้กว้างกว่าที่หน้าเว็บมันยอมให้เห็นมาก

พอดีผมรับตรวจระบบกล้องชุดหนึ่งที่คนอื่นวางไว้ก่อน เปิดผังมาดูแล้วเจอว่ากล้องสามสิบกว่าตัวไม่ได้อยู่วงเดียวกับเครื่องที่ผมใช้ทำงาน มันอยู่หลังขาที่สองของ NVR ซึ่งเป็นวงปิดที่ไม่มีเราเตอร์ ไม่มีเส้นทางกลับมาหาวงผม ping ยังไม่ถึงเลยสักตัวเดียว งานที่รับมาคือบอกให้ได้ว่ากล้องตัวไหนตายไปตั้งแต่เมื่อไหร่ ผมเลยต้องทำงานทั้งหมดผ่าน NVR ตัวเดียว

ปกติงานออดิตกล้องผมจะยิงถามตัวกล้องตรงเสมอ เหตุผลกับวิธีทำผมเขียนแยกไว้แล้วที่ ออดิตกล้อง CCTV ในวงเองด้วย ISAPI กับ ffprobe RTSP เล่มนี้เลยขอข้ามเรื่องนั้นไปทั้งดุ้น แล้วเริ่มที่จุดที่ยิงตรงไม่ถึงเลยแม้แต่ตัวเดียว

เลข IP ชื่อเครื่อง และรหัสผ่านในบทความเป็นค่าตัวอย่างทั้งหมด ส่วน path ของ ISAPI ชื่อฟังก์ชัน SDK และตัวเลขที่เจอคือของจริงจากเครื่องที่ผมดูแลอยู่ ด้วยบัญชีที่ผมถือสิทธิ์เอง ไม่ใช่การเดารหัสของใคร

กล้องอยู่หลังขาที่สอง วงเราไม่มีทางถึง

โครงที่เจอเป็นแบบนี้ NVR มีสองขา ขาแรกเสียบเข้าวงสำนักงานที่ผมเข้าถึงได้ ขาที่สองเป็นวงกล้องล้วนที่แยกออกไปเลย ไม่มีเราเตอร์คั่น ไม่มีเส้นทาง ไม่มี NAT อะไรทั้งนั้น กล้องคุยกับ NVR ได้อย่างเดียว

เส้นทางที่เหลืออยู่เครื่องผม192.168.10.9->NVR ขา 1192.168.10.20 พอร์ต 80->NVR ขา 2วงกล้องปิด->กล้อง 32 ตัวยิงตรงไม่ถึง

ทางเลือกที่คนส่วนใหญ่คิดถึงคือไปเสียบโน้ตบุ๊กเข้าวงกล้องที่หน้างาน ซึ่งทำได้ถ้าอยู่ที่นั่น แต่งานตรวจย้อนหลังที่ต้องรันซ้ำทุกเดือนจะไปนั่งเสียบทุกรอบไม่ไหว อีกทางคือขอให้เขาเดินเส้นทางกลับมาให้ อันนั้นก็เท่ากับพังการแยกวงที่เขาตั้งใจทำไว้ ผมเลยเลือกทางที่สาม คือใช้สิ่งที่ NVR เปิดไว้ให้อยู่แล้ว

เครื่องผมNVRขา 1 วงสำนักงานขา 2 วงกล้องปิดกล้อง 32 ตัวอยู่หลังขาที่สองISAPI พอร์ต 80สถานะ ภาพนิ่ง ค้นคลิปRTSP พอร์ต 554ภาพสดผ่าน NVRHCNetSDK พอร์ต 8000อ่าน log ของเครื่องNVR คุยกับกล้องได้ยิงตรงไปที่กล้อง ไม่มีเส้นทาง
NVR เป็นประตูบานเดียวที่ถึงกล้องได้ ทางเข้ามีสามทาง ส่วนการยิงตรงไปที่กล้องไม่มีเส้นทางเลย
ขอบเขตของงานแบบนี้

ทุกคำสั่งในบทความยิงไปที่เครื่องที่ผมรับดูแลอยู่ ด้วยบัญชีที่เจ้าของให้สิทธิ์มา ไม่ใช่การไปสแกนหาเครื่องคนอื่นหรือเดารหัส และเกือบทั้งหมดเป็นการอ่านค่าอย่างเดียว ตัวสำรวจ endpoint ที่ผมใช้ยิงแต่ GET กับ POST เฉพาะที่เป็นการค้นหา ไม่มี PUT ไม่มี DELETE เพราะระบบกล้องที่รันอยู่จริงพลาดทีเดียวคือภาพหาย

สำรวจก่อนว่ารุ่นนี้เปิดอะไรไว้บ้าง

ISAPI คือ REST บน HTTP ธรรมดาที่พอร์ต 80 ใช้ digest auth ฝั่ง Python เลยไม่ต้องมีไลบรารีพิเศษอะไร requests กับ HTTPDigestAuth จบ แต่สิ่งที่ต้องรู้ก่อนลงมือคือ NVR แต่ละรุ่นแต่ละ firmware เปิด endpoint ไม่เท่ากัน เดาไม่ได้ ต้องยิงดู

ผมเขียนตัวสำรวจไว้ตัวหนึ่ง ยิง GET ไล่ตามรายการ path ที่รู้จัก แล้วสรุปว่าตัวไหน 200 ตัวไหนตอบ 403 พร้อม subStatusCode

GETS = [
    "/ISAPI/System/status", "/ISAPI/System/capabilities", "/ISAPI/System/time",
    "/ISAPI/System/Network/interfaces", "/ISAPI/System/Network/ipFilter",
    "/ISAPI/System/Network/virtualHost", "/ISAPI/System/Network/mapping",
    "/ISAPI/Security/users", "/ISAPI/Security/adminAccesses",
    "/ISAPI/ContentMgmt/InputProxy/channels",
    "/ISAPI/ContentMgmt/InputProxy/channels/status",
    "/ISAPI/ContentMgmt/InputProxy/search",
    "/ISAPI/ContentMgmt/StreamingProxy/channels",
    "/ISAPI/ContentMgmt/Storage", "/ISAPI/ContentMgmt/record/tracks",
    "/ISAPI/Streaming/channels", "/ISAPI/Streaming/status",
    "/ISAPI/Event/triggers", "/ISAPI/Event/notification/httpHosts",
]

def brief(text, n=120):
    t = re.sub(r"<\?xml[^>]*\?>", "", text)
    t = re.sub(r"\s+", " ", t).strip()
    root = re.match(r"<([A-Za-z]+)", t)
    sub = re.search(r"<subStatusCode>([^<]+)</subStatusCode>", text)
    tag = root.group(1) if root else "?"
    return f"<{tag}>" + (f" {sub.group(1)}" if sub else "") + " " + t[:n]

a = HTTPDigestAuth(args.user, args.password)
B = f"http://{args.ip}"
ok = []
for path in GETS:
    r = requests.get(B + path, auth=a, timeout=15)
    if r.status_code == 200:
        ok.append(path)
        print(f"200  {path}\n        {brief(r.text)}")
    else:
        sub = re.search(r"<subStatusCode>([^<]+)</subStatusCode>", r.text)
        print(f"{r.status_code}  {path}   ({sub.group(1) if sub else r.reason})")
print(f"\n# ใช้ได้ {len(ok)}/{len(GETS)} endpoint")

รหัสผ่านผมไม่ฝังในไฟล์ครับ อ่านจาก environment แล้วส่งเข้าไปเป็นอาร์กิวเมนต์ตอนรัน

export HIK_PASS='<PASSWORD>'
python isapi_probe.py 192.168.10.20 admin "$HIK_PASS" --out out/isapi_nvr.txt

ผลที่ได้จากเครื่องที่ผมทดสอบ เป็น NVR 32 ช่อง firmware V5.04 คือ ใช้ได้ 26 จาก 38 endpoint ที่ลอง ซึ่งมากกว่าที่คิดไว้ตอนแรกเยอะ

26/38endpoint ที่ผ่าน
พอร์ต 80ISAPI digest
พอร์ต 8000HCNetSDK
32 ช่องกล้องหลัง NVR

endpoint ที่ใช้ได้จริง กับที่รุ่นนี้ปิดตาย

ตอบ 200

อ่านสถานะได้เกือบทั้งเครื่อง

  • /ISAPI/ContentMgmt/InputProxy/channels กล้องทุกช่องที่ตั้งไว้ พร้อม IP พอร์ต โปรโตคอล และ user ที่ใช้ต่อ
  • .../channels/status online จริงหรือไม่ พร้อม chanDetectResult เช่น netUnreachable
  • /ISAPI/ContentMgmt/InputProxy/search คือการสั่งให้ NVR สแกนหาอุปกรณ์แทนเรา เป็น GET ธรรมดา คืน ip mac รุ่น serial firmware
  • /ISAPI/System/Network/interfaces เห็นทุกขา ทั้ง IP MAC และสถานะลิงก์
  • /ISAPI/Security/adminAccesses บอกว่าเปิด HTTP RTSP SDK ไว้อันไหนบ้าง
ตอบ 403

อย่าเสียเวลาลองซ้ำ

  • Network/virtualHost และ Network/mapping ไม่มี แปลว่าเปิดหน้าเว็บของกล้องผ่าน NVR ไม่ได้
  • passthrough ทุกรูปแบบ ทั้ง InputProxy/channels/<id>/passthrough และ /ISAPI/RemoteChannel/...
  • Event/schedules, Event/notification/mailing
  • Security/onlineUsers, ContentMgmt/record/quota, System/diskManagement
  • ContentMgmt/logSearch มีอยู่แต่หา body ที่ถูกไม่ได้ ตอบ 400 ตลอด

สองตัวแรกในคอลัมน์ซ้ายคือของที่ผมใช้ทุกวัน เอามาชนกันด้วย id ของช่อง แล้วได้ตารางที่บอกครบว่าช่องไหนคือกล้อง IP อะไร ต่อด้วยโปรโตคอลอะไร และตอนนี้ NVR มองเห็นมันอยู่ไหม

cfg, _ = h.xml("/ISAPI/ContentMgmt/InputProxy/channels")
st,  _ = h.xml("/ISAPI/ContentMgmt/InputProxy/channels/status")

status = {}
for s in as_list(st.get("InputProxyChannelStatus")):
    status[s.get("id")] = {"online": s.get("online"),
                           "why": s.get("chanDetectResult")}

off = []
for ch in as_list(cfg.get("InputProxyChannel")):
    src = ch.get("sourceInputPortDescriptor") or {}
    s = status.get(ch.get("id"), {})
    mark = "ONLINE" if s.get("online") == "true" else "OFFLINE"
    print(f"{ch.get('id'):<4}{mark:<10}{str(src.get('ipAddress')):<16}{ch.get('name')}")
    if s.get("online") != "true":
        off.append((ch.get("id"), src.get("ipAddress"), s.get("why")))

for cid, ip, why in off:
    print(f"    ch{cid} {ip} เหตุที่ NVR บอก: {why}")   # เช่น netUnreachable

ค่า chanDetectResult ที่ติดมากับสถานะมีประโยชน์กว่าคำว่า offline เฉยๆ เยอะ เพราะมันแยกให้ว่า NVR ยิงไปแล้วไม่มีเส้นทาง กับยิงถึงแต่ล็อกอินไม่ผ่าน เป็นคนละเรื่องกันและแก้คนละแบบ

จุดที่ทำให้แผนแรกของผมพัง คือ virtualHost ไม่มี ผมตั้งใจจะให้ NVR ทำตัวเป็น reverse proxy เปิดหน้าเว็บของกล้องแต่ละตัวออกมาที่พอร์ตต่างกัน รุ่นที่รองรับทำได้จริง แต่รุ่นนี้ตอบ notSupport ทั้ง virtualHost และ mapping ตามด้วย passthrough ที่ปิดหมดทุกทาง แปลว่าผมจะไม่มีวันได้คุยกับ ISAPI ของตัวกล้องผ่าน NVR เลย สิ่งที่ทำได้คือขอข้อมูลที่ NVR รู้จักอยู่แล้วเท่านั้น

ภาพนิ่งกับ RTSP ของกล้องที่เข้าไม่ถึง

ข่าวดีคือถึงจะคุยกับกล้องตรงไม่ได้ แต่ ดูภาพของกล้องได้ เพราะสตรีมมันวิ่งผ่าน NVR อยู่แล้ว กุญแจอยู่ที่การแปลงเลขช่องเป็น trackID ด้วยสูตร ch * 100 + 1 ช่อง 17 เลยกลายเป็น 1701

# ภาพนิ่ง JPEG ของช่อง 17 ผ่าน NVR ได้ทันที ไม่ต้องถึงตัวกล้อง
curl -s --digest -u "admin:$HIK_PASS" \
  "http://192.168.10.20/ISAPI/Streaming/channels/1701/picture" -o ch17.jpg
ls -l ch17.jpg      # ของจริงที่ผมได้อยู่ราว 38-52 KB ต่อภาพ

# สตรีมสดก็ผ่าน NVR ได้เหมือนกัน เอา ffprobe อ่านค่าจริงของภาพ
ffprobe -v error -rtsp_transport tcp \
  -select_streams v:0 -show_entries stream=codec_name,width,height,avg_frame_rate \
  -of default=nw=1 "rtsp://admin:[email protected]:554/Streaming/Channels/1701"

ตรงนี้มีประโยชน์กว่าที่คิดครับ เพราะการเห็นภาพจริงกับการเห็นคำว่า online บนหน้าเว็บเป็นคนละเรื่องกัน ผมเคยเจอช่องที่ขึ้นออนไลน์แต่ภาพเป็นจอดำสนิทเพราะเลนส์โดนอะไรไม่รู้บัง กับช่องที่ขึ้นออนไลน์แต่ภาพค้างเป็นเฟรมเดิม การดึงภาพนิ่งมาเก็บวันละครั้งแล้วเปิดดูเรียงกันทีเดียวจับได้ทั้งสองแบบ

ภาพนิ่งที่ดึงมาคือข้อมูลของเจ้าของระบบ

ไฟล์ JPEG ที่ได้เป็นภาพในพื้นที่จริงของเขา ไม่ใช่ผลตรวจทางเทคนิคเฉยๆ ผมเก็บไว้ในเครื่องที่เข้ารหัสดิสก์ ตั้งอายุไฟล์ให้ลบทิ้งเองภายในไม่กี่วัน และไม่เอาออกนอกเครื่องเด็ดขาด เวลาส่งรายงานให้ลูกค้า ส่งเป็นตารางสถานะ ไม่ส่งภาพ เว้นแต่เขาขอเอง

ถามว่าช่องนี้บันทึกล่าสุดเมื่อไหร่

คำถามที่ต้องตอบให้ได้คือกล้องตัวนี้ตายตั้งแต่เมื่อไหร่ วิธีที่ตรงที่สุดคือค้นในไฟล์บันทึกว่าช่องนั้นมีคลิปล่าสุดถึงวันไหน ใช้ POST /ISAPI/ContentMgmt/search โดยส่ง XML เข้าไป

BODY = """<?xml version='1.0'?><CMSearchDescription>
<searchID>{A1B2C3D4-0017-0000-0000-000000000001}</searchID>
<trackIDList><trackID>1701</trackID></trackIDList>
<timeSpanList><timeSpan>
  <startTime>2026-09-01T00:00:00Z</startTime>
  <endTime>2026-09-17T23:59:59Z</endTime>
</timeSpan></timeSpanList>
<maxResults>1</maxResults><searchResultPostion>0</searchResultPostion>
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/CMR</metadataDescriptor>
</metadataList></CMSearchDescription>"""

r = requests.post(f"http://{ip}/ISAPI/ContentMgmt/search", data=BODY,
                  auth=HTTPDigestAuth(user, pw), timeout=30)
print(r.status_code, r.text[:600])
searchID ต้องเป็นรูปแบบ GUID เท่านั้น

ผมเสียเวลาไปเกือบชั่วโมงกับตรงนี้ ใส่ searchID เป็นข้อความสั้นๆ อย่าง C1 แล้วได้ 400 badXmlContent กลับมาทุกรอบ โดยที่ error ไม่ได้บอกเลยว่าผิดตรงไหน พอเปลี่ยนเป็น GUID เต็มรูปแบบในวงเล็บปีกกาแบบ {A1B2C3D4-0017-0000-0000-000000000001} ก็ผ่านทันที และสังเกตด้วยว่า field ชื่อ searchResultPostion สะกดตกตัว i มาแบบนั้นตั้งแต่ต้นทาง อย่าไปแก้ให้ถูก ไม่งั้นมันไม่รู้จัก

กับดักเรื่องเวลา และ SADP ที่มองไม่เห็นวงที่สอง

สามเรื่องนี้ผมเจอตอนทำจริง จดไว้กันคนอื่นเสียเวลาซ้ำ

เรื่องแรก เวลาที่ ISAPI คืนมาลงท้ายด้วย Z เหมือนเป็น UTC แต่จริงๆ มันคือเวลาของเครื่องซึ่งตั้งเป็น +07 อยู่ ถ้าเอาไปแปลงเป็นเวลาไทยอีกรอบจะเพี้ยนไปเจ็ดชั่วโมง เทียบกับ /ISAPI/System/time ก่อนสรุปทุกครั้ง

เรื่องที่สอง endpoint InputProxy/search ที่สั่งให้ NVR สแกนหาอุปกรณ์แทนเรา มันสแกนเฉพาะขาที่ตั้งเป็น defaultConnection เท่านั้น ซึ่งคือขาที่หันมาทางวงสำนักงาน ส่วนวงกล้องที่อยู่ขาที่สองมันมองไม่เห็น ผมลองยัดพารามิเตอร์เพิ่มทั้ง netInterfaceID interfaceID และ subnet แล้วไม่มีอันไหนมีผลเลย

เรื่องที่สาม อันนี้เฉพาะคนใช้ git-bash บน Windows path ที่ขึ้นต้นด้วย /ISAPI/... จะโดนแปลงเป็น path ของ Windows โดยอัตโนมัติ กลายเป็นยิงไปที่ C:/Program Files/... แล้วงงว่าทำไม 404

# git-bash แปลง path ให้เองจนพัง ต้องปิดด้วยตัวแปรนี้
MSYS_NO_PATHCONV=1 curl -s --digest -u "admin:$HIK_PASS" \
  "http://192.168.10.20/ISAPI/System/time"

# เทียบเวลาเครื่องกับเวลาที่ค้นได้ ก่อนสรุปว่ากล้องตายกี่โมง
MSYS_NO_PATHCONV=1 curl -s --digest -u "admin:$HIK_PASS" \
  "http://192.168.10.20/ISAPI/System/time" | grep -o '<localTime>[^<]*'

เมื่อ ISAPI จนแต้ม ก็ต้องลงไปที่ HCNetSDK

สิ่งที่ผมต้องการที่สุดคือ log ของเครื่อง เพราะมันตอบได้ตรงที่สุดว่ากล้องหลุดตอนไหน ใครล็อกอินมาทำอะไร แต่ ContentMgmt/logSearch ตอบ 400 ทุกท่าที่ลอง ทางที่เหลือคือ Device Network SDK ซึ่งเป็นโปรโตคอลส่วนตัวของยี่ห้อนี้ วิ่งที่พอร์ต 8000 และมาเป็นไลบรารีไบนารี

เลือกก่อนเสมอ

ISAPI

HTTP ธรรมดา ข้ามแพลตฟอร์ม ไม่ต้องลง DLL ดีบักด้วย curl ได้ อุปกรณ์ยุคหลังรองรับเกือบครบ งานอ่านสถานะ ทำ inventory ดึงภาพ ค้นคลิป ใช้ตัวนี้จบ

หยิบมาเมื่อจำเป็น

HCNetSDK

ได้เพิ่มคืออ่าน log เครื่อง คอนฟิกกล้องที่อยู่หลัง NVR แบบให้ NVR รีเลย์ให้ ฟอร์แมตดิสก์ อัปเฟิร์มแวร์ระยะไกล PTZ และ live view แบบ callback แลกมาด้วยการต้องมี DLL ตรงสถาปัตยกรรม

ข่าวดีคือไม่ต้องไปโหลด SDK จากเว็บผู้ผลิต เพราะ DLL มันมากับโปรแกรม SADP ที่ติดเครื่องอยู่แล้ว อยู่ที่ C:\Program Files (x86)\SADP\SADP\HCNetSDK.dll พร้อมโฟลเดอร์ HCNetSDKCom ข้างๆ ข่าวร้ายคือมันเป็น x86 ล้วน Python 64-bit โหลดไม่ได้

แพ็กเกจ pip ชื่อคล้ายกันใช้บน Windows ไม่ได้

ผมลอง pip install hikvision-sdk เสียเวลาไปฟรีๆ เพราะในโค้ดมันฮาร์ดโค้ดหา libhcnetsdk.so กับ LD_LIBRARY_PATH คือทำมาสำหรับ Linux อย่างเดียว ถ้าจะใช้ตัวนี้ต้องรันใน WSL พร้อม SDK ฝั่ง Linux ถอนออกไปแล้วครับ ทางที่ได้ผลบน Windows คือเรียก DLL ตรงด้วย ctypes

วิธีที่ผมใช้คือโหลด Python embeddable 32-bit มาแตกไว้ในโฟลเดอร์ชั่วคราว ไม่ต้องติดตั้งอะไรทับของเดิม แล้วเรียก ctypes จากตัวนั้น สองบรรทัดที่ขาดไม่ได้คือ add_dll_directory กับ chdir เพราะ SDK หาโฟลเดอร์ HCNetSDKCom จาก current directory ไม่ได้หาจาก path ของ DLL

import ctypes as C, os

SDK_DIR = r"C:\Program Files (x86)\SADP\SADP"

def load_sdk():
    if not os.path.isdir(SDK_DIR):
        raise SystemExit(f"[ERR] ไม่พบโฟลเดอร์ SDK: {SDK_DIR}")
    os.add_dll_directory(SDK_DIR)
    os.chdir(SDK_DIR)                       # SDK หา HCNetSDKCom จาก cwd
    return C.WinDLL(os.path.join(SDK_DIR, "HCNetSDK.dll"))

sdk = load_sdk()
sdk.NET_DVR_Login_V30.restype = C.c_long
sdk.NET_DVR_Init()
sdk.NET_DVR_SetConnectTime(3000, 1)
sdk.NET_DVR_SetReconnect(10000, True)

info = NET_DVR_DEVICEINFO_V30()
uid = sdk.NET_DVR_Login_V30(ip.encode(), 8000, user.encode(), pw.encode(), C.byref(info))
if uid < 0:
    raise SystemExit(f"[ERR] login ไม่ผ่าน error={sdk.NET_DVR_GetLastError()}")
print(f"ช่อง IP {info.byIPChanNum + info.byHighDChanNum * 256} เริ่มที่ {info.byStartDChan}")
  1. เช็กก่อนว่าโปรโตคอล SDK เปิดอยู่จริง ดูที่ /ISAPI/Security/adminAccesses และพอร์ต 8000 ต้องเข้าถึงได้
  2. หา HCNetSDK.dll จากโฟลเดอร์ SADP ที่ติดเครื่องอยู่แล้ว ไม่ต้องไปโหลดชุดใหญ่จากเว็บผู้ผลิต
  3. แตก Python embeddable 32-bit ไว้ในโฟลเดอร์ชั่วคราว เพราะ DLL เป็น x86 อย่าเอาไปติดตั้งทับ Python หลักของเครื่อง
  4. เรียกตามลำดับ Init แล้ว Login_V30 แล้วทำงาน แล้ว Logout แล้ว Cleanup ทุกครั้ง อย่าลืมปิด ไม่งั้น session ค้างที่เครื่องปลายทาง
  5. รันเสร็จลบโฟลเดอร์ชั่วคราวทิ้ง ไม่ทิ้ง DLL ที่ก๊อปมากองไว้ในเครื่องทำงาน

อ่าน log แล้วแยกให้ออกว่ากล้องตาย หรือ NVR ค้าง

ตัวที่ต้องการจริงๆ คือ NET_DVR_FindDVRLog_V30 แล้ววน NET_DVR_FindNextLog_V30 ไปเรื่อยๆ ค่าที่มันคืนคือ 1000 แปลว่าได้หนึ่งรายการ 1001 แปลว่าหมดแล้ว 1002 แปลว่ากำลังค้นอยู่ให้รอ ผมนับรอบ 1002 ไว้ด้วย เกินสองพันรอบก็ตัดจบ เจอมาแล้วว่าบางเครื่องมันค้างอยู่ตรงนั้นไม่ยอมจบ

LOG_OFF = {"major": 24, "minor": 28, "panelUser": 32, "netUser": 48,
           "ipv4": 64, "paraType": 208, "channel": 212, "disk": 216}

def parse_log(buf):
    import struct as _s
    b = buf.raw if hasattr(buf, "raw") else bytes(buf)
    y, mo, d, h, mi, s = _s.unpack_from("<6I", b, 0)
    g = lambda k: _s.unpack_from("<I", b, LOG_OFF[k])[0]
    z = lambda o, n: b[o:o + n].split(b"\x00")[0].decode(errors="replace")
    return {"time": f"{y:04d}-{mo:02d}-{d:02d} {h:02d}:{mi:02d}:{s:02d}",
            "major": g("major"), "minor": g("minor"), "channel": g("channel"),
            "user": z(LOG_OFF["netUser"], 16) or z(LOG_OFF["panelUser"], 16),
            "ip": z(LOG_OFF["ipv4"], 16)}

h = sdk.NET_DVR_FindDVRLog_V30(uid, mode, major or 0, 0, C.byref(t1), C.byref(t2), False)
buf, n, waits = C.create_string_buffer(4096), 0, 0
while n < 20000:
    r = sdk.NET_DVR_FindNextLog_V30(h, buf)
    if r == 1000:
        n, waits = n + 1, 0
        e = parse_log(buf)
        print(f"{e['time']}  major={e['major']} minor=0x{e['minor']:02x} "
              f"ch={e['channel']} user={e['user']} ip={e['ip']}")
    elif r == 1002:                 # กำลังค้น รอแป๊บ
        waits += 1
        if waits > 2000:
            break
    else:                           # 1001 หมดแล้ว หรือ error
        break
sdk.NET_DVR_FindLogClose_V30(h)
อย่าแกะ struct ของ log ด้วย ctypes.Structure

ผมประกาศ NET_DVR_LOG_V30 เป็น Structure ตามเอกสารแล้วเครื่องดับไปเลย เป็น segfault ไม่มีข้อความอะไรบอก เพราะขนาดกับการจัดเรียงไม่ตรงกับที่ DLL รุ่นนี้ใช้จริง ทางที่รอดคือรับเป็น create_string_buffer(4096) ดิบๆ แล้ว unpack ตามออฟเซ็ตเอง อ่านยากกว่านิดเดียวแต่ไม่พังอีกเลย

กับดักที่สองคือ lSelectMode มันไม่ได้เป็นแค่ตัวกรอง 0 คือค้นตามเวลา 1 คือค้นตามชนิด ซึ่งเมินช่วงเวลาที่เราส่งไปทั้งหมด 2 คือเอาทั้งเวลาและชนิด ส่วน 3 คือทั้งหมด ผมใส่ 1 ครั้งแรกแล้วได้ log ย้อนหลังจนหมด buffer งงอยู่พักใหญ่ว่าทำไมมีของปีที่แล้วโผล่มา ถ้าจะกรองชนิดพร้อมกำหนดช่วงเวลาต้องใส่ 2

กับดักที่สามคือ เลขช่องระหว่าง ISAPI กับ SDK ไม่ตรงกัน SDK เริ่มนับช่อง IP ที่ค่า byStartDChan ซึ่งเครื่องที่ผมเจอเท่ากับ 33 แปลว่า ช่อง 17 ที่เห็นใน ISAPI จะกลายเป็นช่อง 49 ในฝั่ง SDK ถ้าจับคู่ผิดคือได้ข้อสรุปผิดทั้งใบ

0x29IPC ไม่มีลิงก์
0x27net broken
0x22illegal access
0x43 0x41ปิดแล้วเปิดเครื่อง

พอเอา minor code มาเรียงตามเวลาแล้วภาพก็ชัดขึ้นมาทันที ทุกครั้งที่เจอ 0x43 ตามด้วย 0x41 ห่างกันสองนาที นั่นคือไฟดับแล้วเครื่องบูตกลับมา และทุกครั้งที่บูตเสร็จ จะมี 0x29 ของช่องเดิมโผล่ตามมาภายในไม่กี่วินาที แปลว่าบูตกี่รอบกล้องตัวนั้นก็ไม่กลับมา ซึ่งตัดปัญหาว่า NVR ค้างออกไปได้เลย เหลือแค่กล้องเสีย สายขาด หรือไฟที่ปลายทางไม่มา ต้องส่งคนไปดูหน้างานอย่างเดียว รายงานที่ส่งให้เจ้าของก็จบด้วยประโยคเดียวว่าช่องไหนตายตั้งแต่วันไหน

สรุปลำดับที่ผมใช้กับงานแบบนี้

  1. ยิงตัวสำรวจก่อนเสมอ อย่าเดาว่ารุ่นนี้รองรับอะไร รายการที่ผ่านกับที่ตอบ 403 คือขอบเขตของงานทั้งหมด
  2. ดึงรายชื่อช่องกับสถานะจาก InputProxy แล้วเก็บเป็น JSON แยกตามวันที่ เดือนหน้ากลับมา diff ได้
  3. ช่องที่ไม่ออนไลน์ ให้ดูภาพนิ่งกับค้นคลิปย้อนหลังก่อน เพื่อรู้เวลาที่มันหยุดจริง
  4. ถ้าต้องการ log ของเครื่อง ถึงค่อยลงไปที่ SDK ที่พอร์ต 8000 อย่าเริ่มจากตรงนี้เพราะมันหนักกว่ามาก
  5. สรุปเป็นตารางช่อง เวลาที่หยุด และเหตุที่เดาได้จาก minor code ส่งให้เจ้าของระบบตัดสินใจ

ส่วนตัวผมมองว่าระบบกล้องที่แยกวงแน่นแบบนี้ออกแบบมาถูกแล้วครับ ไม่ควรไปเจาะเส้นทางเพิ่มให้ตัวเองสะดวก สิ่งที่ควรทำคือใช้ประตูที่มีอยู่ให้คุ้ม แล้วเขียนเครื่องมือให้รันซ้ำได้ ใครที่เจอรุ่นที่เปิด virtualHost ได้ก็สบายกว่านี้เยอะ ตามที่มีของครับ

ที่ยังค้างในแผนของผมคือทำตัวดึงภาพนิ่งทุกช่องวันละครั้งแล้วเทียบ hash เพื่อจับภาพค้าง กับหาวิธียิง logSearch ของ ISAPI ให้ผ่านสักที จะได้ไม่ต้องพึ่ง Python 32-bit อีก ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย