สวัสดีจ่ะ ตอนที่แล้ว เราไล่ Signal Chain ของ sensor ชายฝั่งกันไปหนึ่งรอบ ตั้งแต่ขา RS-232/RS-422 ผ่าน serial device server จนถึงการตัดบรรทัดกับเช็ก checksum ฝั่ง server แล้วจบที่ AIS ซึ่งเป็นตัวที่ต่อง่ายที่สุดในกลุ่ม และแน่นอน ไม่เป็นอีกตอนที่จะ Geek สุดๆ ไม่ต้องถามหาป้าแล้ว
ตอนที่ 2 นี้ขยับขึ้นอีกระดับ เพราะสอง sensor ที่จะเล่าต่อไปนี้ไม่ได้ใจดีแบบ AIS — เรดาร์ชายฝั่งมีทางออกข้อมูลให้เลือกตั้งสามแบบและคนละโลกกันเลย ส่วนกล้อง EO/IR ต้องคำนวณให้ถูกก่อน มันถึงจะหันไปถูกทาง ทั้งคู่ยังอยู่ในโจทย์เดิมของ Maritime Domain Awareness คือ "ตอนนี้ในทะเลมีอะไรอยู่บ้างวะ" และยังยืนบนกฎข้อเดียวกับตอนที่แล้ว คือถ้ามันไม่เป็นมาตรฐาน มันคือกับดัก
ผมบ่นเรื่องเรดาร์ชายฝั่งไว้ยาวแล้วที่ Radar on the Rock ตอนนี้เลยขอข้ามเรื่อง "เรดาร์คืออะไร" ไปที่เรื่องที่คนทำระบบเจ็บจริง คือ เรดาร์ตัวเดียวกันอาจพ่นข้อมูลออกมาได้สามแบบ และแต่ละแบบคนละโลกกันเลย
Message Format ที่ 1: NMEA 0183 — ง่ายสุด ได้น้อยสุด
ที่ใช้กันคือ $RATTM (Tracked Target Message) คือเป้าที่เรดาร์ track ให้แล้ว กับ $RATLL (Target Latitude/Longitude) ที่ใจดีแปลงเป็นพิกัดมาให้เลย
text $RATTM,492,0.75,281.7,T,1.2,352.7,T,0.72,5.6,N,,T,,074054,M*10
$RATLL,12,1244.7890,N,10038.1234,E,TGT12,074054,T,*4A 1 2
ผมแกะ TTM ทีละฟิลด์ไว้ในบทความนั้นแล้ว ที่อยากเสริมคือข้อจำกัดของมัน: ช่องสัญญาณ 4800 บอด (หรือ 38400 ถ้าตั้ง HS) แคบมากเมื่อเทียบกับจำนวนเป้าที่เรดาร์ชายฝั่งตัวเดียวเห็นได้ พอเป้าเยอะ ของที่เกิดคือเป้าถูกตัดทิ้งหรืออัปเดตช้าลง แล้วเราจะนั่งงงว่าทำไมเป้าบนจอกระตุกทั้งที่เรดาร์เห็นครบ — คอขวดอยู่ที่สายครับ ไม่ใช่ที่จาน
จุดที่ต้องระวังอีกอย่าง: TTM ให้ ระยะกับแบริ่งเทียบจากตัวเรดาร์ ไม่ใช่พิกัดโลก เพราะฉะนั้นตำแหน่งเสาเรดาร์ต้องแม่นระดับเมตร และต้องรู้ว่า "ศูนย์องศา" ของจานมันตรงกับทิศเหนือจริงหรือไม่ ถ้าติดตั้งแล้วเบี้ยวไปสององศา เป้าที่ระยะ 10 ไมล์ทะเลจะเพี้ยนไปหลายร้อยเมตร แล้วมันจะไม่มีวันจับคู่กับ AIS ได้เลย
แปลว่าเราต้อง แปลงเป็นตำบลที่ เอง โดยเอาพิกัดของเสาเรดาร์เป็นจุดตั้งต้น — ค่านี้ได้จาก GPS ที่ติดไว้ที่สถานี หรือจาก survey ตอนติดตั้ง แล้วเดินออกไปตามแบริ่งและระยะที่ TTM บอก ด้วยการคำนวณแบบวงใหญ่ (great-circle) ถึงจะได้ lat,lon ออกมาให้ปักหมุดได้ โค้ดอยู่ท้ายหัวข้อเรดาร์ครับ ฟังก์ชัน polar_to_latlon ตัวเดียวใช้ได้ทั้ง TTM และ ASTERIX
Message Format ที่ 2: NMEA 2000 — CAN bus ที่คนทำเว็บไม่คุ้น
NMEA 2000 (วงการเรียกย่อว่า N2K — 2K คือ 2000) ไม่ใช่ 0183 เวอร์ชันใหม่นะครับ มันคนละสายพันธุ์เลย และรากของมันมาจากวงการยานยนต์เต็มๆ ชั้นล่างสุดคือ CAN bus ที่ Bosch ทำไว้ให้รถยนต์ (CAN 2.0B ความเร็ว 250 kbit/s) ส่วนชั้นบนอย่างรูปแบบข้อความกับการจอง address ยกมาจาก SAE J1939 ซึ่งเป็นโปรโตคอลที่รถบรรทุก เครื่องจักรหนัก และเครื่องยนต์ดีเซลใช้คุยกันอยู่ก่อนแล้ว NMEA แค่มานิยามชุดข้อความของตัวเองสำหรับเรือทับลงไป ข้อความเรียกว่า PGN (Parameter Group Number) ฝังอยู่ใน CAN ID ขนาด 29 บิต และหนึ่งเฟรมบรรทุกข้อมูลได้แค่ 8 ไบต์ ของที่ยาวกว่านั้นต้องหั่นส่งแบบ fast-packet
ของอีกอย่างที่ 0183 ไม่มีเลยคือ ขาสั่ง — 0183 เป็น talker เดียวพูดทางเดียวให้ listener หลายตัวนั่งฟัง แต่ N2K เป็น multi-talker สองทาง อุปกรณ์บนบัส "สั่ง" กันเองได้ ตัวอย่างที่เป็นมาตรฐานคือ PGN 126208 (Group Function) ที่มีทั้ง Request ขอให้อีกฝั่งส่ง PGN ที่เราอยากได้ และ Command สั่งเปลี่ยนค่าในอุปกรณ์ เช่น ตั้ง offset ของ depth sounder กับ PGN 127502 (Switch Bank Control) ที่สั่งเปิดปิดสวิตช์หรือรีเลย์ในระบบ digital switching ได้ตรงๆ แถมอุปกรณ์เสียบปุ๊บก็จอง address กันเองด้วย ISO Address Claim ไม่ต้องไล่ตั้ง talker ID ให้ชนกันแบบ 0183 งานเรดาร์ชายฝั่งของเราแทบไม่ได้ใช้ขาสั่งนี้ ส่วนใหญ่ฟังอย่างเดียว แต่ต้องรู้ไว้ เพราะมันคือเหตุผลที่บัส N2K มี traffic ประเภท request/command วิ่งปนอยู่ด้วย ไม่ได้ไหลทางเดียวเหมือนที่เราคุ้น
python def decode_can_id (can_id: int ) -> dict :
prio = (can_id >> 26 ) & 0x 07
dp = (can_id >> 24 ) & 0x 03 # data page
pf = (can_id >> 16 ) & 0x FF # PDU Format
ps = (can_id >> 8 ) & 0x FF # PDU Specific
src = can_id & 0x FF # source address ของอุปกรณ์บนบัส
if pf < 240 : # PDU1: ส่งเจาะจงปลายทาง PS คือ destination address
pgn, dst = (dp << 16 ) | (pf << 8 ), ps
else : # PDU2: broadcast PS เป็นส่วนหนึ่งของ PGN
pgn, dst = (dp << 16 ) | (pf << 8 ) | ps, 255
return { "prio" : prio, "pgn" : pgn, "src" : src, "dst" : dst}
# ตัวอย่าง PGN ที่เป็นมาตรฐานเปิดและได้ใช้บ่อย
# 127250 heading เรือ · 129025 ตำแหน่งแบบ rapid update · 129026 COG/SOG
# 129029 ข้อมูล GNSS · 129038 / 129039 AIS position report class A / B 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
ทีนี้มาถึงตัวอย่างชั้นดีของหัวข้อ "มาตรฐาน vs proprietary": N2K กันช่วง PGN ไว้ให้ผู้ผลิตใส่ของตัวเองโดยเฉพาะ คือ 65280–65535 กับ 130816–131071 และ 126720 สำหรับข้อความแบบระบุปลายทาง ผลคือของสวยๆ อย่างภาพเรดาร์หรือรายละเอียดเป้าของหลายเจ้า ไปนั่งอยู่ในช่วงที่ไม่มีเอกสารสาธารณะ ส่วนที่เปิดจริงจะเป็นของพื้นฐานอย่างตำแหน่ง เข็ม ความเร็ว และ AIS
แปลว่าถ้าวางแผนจะดูดเป้าเรดาร์ผ่าน N2K โดยไม่ถามให้ชัดก่อนซื้อ มีสิทธิ์ได้ของที่ถอดไม่ออกสูงมาก คำถามที่ต้องถามตอนคุยสเปกมีข้อเดียว — "เป้าออกมาเป็น PGN มาตรฐานหมายเลขอะไร มีเอกสารเปิดไหม" ถ้าคำตอบคือ "ใช้ SDK เราสิครับ" ให้ยิ้มแล้วเดินไปถามเจ้าถัดไป
ส่วนวิธีเอา N2K เข้า server ต้องผ่าน gateway ที่แปลง CAN เป็น USB/serial/Ethernet บางตัวพ่น raw frame เป็น ASCII บรรทัดละเฟรม บางตัวเป็นไบนารี อีกทางคือใช้ตัวแปลง N2K → 0183 ซึ่งง่ายกว่าแต่ข้อมูลหายเยอะ
Message Format ที่ 3: ASTERIX CAT048 — ไบนารีแท้ ยิงมาทาง UDP
ตัวนี้คือของโปรดผม ASTERIX เป็นมาตรฐานของ EUROCONTROL ที่ออกแบบมาสำหรับข้อมูลเป้าจากเรดาร์ โดย CAT048 คือ monoradar target report คือรายงานเป้าจากเรดาร์ตัวเดียว (ถ้าเห็น CAT034 คู่กันมาด้วย นั่นคือข้อความบอกสถานะและจังหวะกวาดของสถานี)
รากมันมาจากฝั่งการบิน แต่ผู้ผลิตเรดาร์ตรวจการณ์จำนวนมากเลือกใช้เป็นทางออกมาตรฐาน เพราะเป็นไบนารีที่กระชับ ส่งเป้าได้เยอะในแบนด์วิดท์น้อย และมีเอกสารเปิด ปกติมาทาง UDP หรือ multicast ให้หลายระบบฟังพร้อมกัน — ไม่ต้องมีสาย serial ให้ปวดหัวเลย
และเพราะรากมาจากการบิน CAT048 เลยมี data item สำหรับ ความสูง ติดมาด้วย คือ I048/090 (Flight Level ที่ได้จาก transponder Mode C) กับ I048/110 (ความสูงที่เรดาร์ 3D วัดเอง) เรดาร์ชายฝั่งที่มองเป้าผิวน้ำมักไม่ส่งสองตัวนี้ หรือส่งมาเป็นศูนย์ แต่ parser ต้องรู้จักมันอยู่ดี เพราะมันมีที่นั่งของมันอยู่ใน UAP และกินไบต์ตามลำดับ ถ้าข้ามไม่เป็นไบต์ก็เลื่อน แล้ว record เละทั้งอัน ส่วนใครที่โจทย์เป็นเป้าอากาศยานบินต่ำอย่างโดรน ช่องพวกนี้คือของที่หยิบมาใช้ได้เลย ไม่ต้องไปหาที่อื่น
โครงสร้างมันเป็นแบบนี้: หนึ่ง data block = CAT 1 ไบต์ + LEN 2 ไบต์ + record หลายอัน แต่ละ record ขึ้นต้นด้วย FSPEC ซึ่งเป็นแถวบิตบอกว่า "ใน record นี้มี data item อะไรมาบ้าง" เรียงตามลำดับที่มาตรฐานกำหนดไว้ (เรียกว่า UAP) แล้วข้อมูลจริงก็ต่อกันมาติดๆ โดยไม่มีตัวคั่น
พูดง่ายๆ คือ ถ้าคุณอ่าน item ใดพลาดไปหนึ่งไบต์ ทั้ง record หลังจากนั้นเละหมด ไม่มี \r\n มาช่วยให้ตั้งหลักใหม่เหมือน NMEA
python import struct
def parse_cat048 (b: bytes ) -> list[ dict ]:
if not b or b[ 0 ] != 48 : # CAT ต้องเป็น 048
return []
length = struct.unpack_from( ">H" , b, 1 )[ 0 ] # ความยาวทั้ง data block รวมหัว 3 ไบต์
p, out = 3 , []
while p < length:
# FSPEC: อ่านต่อไปเรื่อยๆ ตราบใดที่บิตสุดท้าย (FX) ยังเป็น 1
fspec = []
while True :
fspec.append(b[p]); p += 1
if not fspec[ - 1 ] & 0x 01 :
break
# แต่ละ octet เก็บได้ 7 FRN เพราะบิตสุดท้ายถูกจองไว้เป็น FX
def has (frn, fspec = fspec):
o, bit = divmod (frn - 1 , 7 )
return o < len (fspec) and bool (fspec[o] & ( 0x 80 >> bit))
rec = {}
if has( 1 ): # I048/010 ใครส่งมา
rec[ "sac" ], rec[ "sic" ] = b[p], b[p + 1 ]
p += 2
if has( 2 ): # I048/140 เวลา
tod = (b[p] << 16 ) | (b[p + 1 ] << 8 ) | b[p + 2 ]
rec[ "tod_sec" ] = tod / 128 # วินาทีนับจากเที่ยงคืน UTC
p += 3
if has( 3 ): # I048/020 ยาวผันแปร
while b[p] & 0x 01 :
p += 1
p += 1
if has( 4 ): # I048/040 ตำแหน่งเชิงขั้ว
rho, theta = struct.unpack_from( ">HH" , b, p)
rec[ "rho_nm" ] = rho / 256 # LSB = 1/256 ไมล์ทะเล
rec[ "theta_deg" ] = theta * 360 / 65536 # LSB = 360/2^16 องศา
p += 4
# item ที่เหลือต้องไล่ให้ครบตามลำดับ UAP เช่น I048/161 track number, I048/200 ความเร็วเชิงขั้ว
out.append(rec)
return out 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
กับดักสองอันที่ต้องรู้ล่วงหน้า
เวลาใน I048/140 คือวินาทีนับจากเที่ยงคืน UTC ของวันนั้น มันวนกลับเป็นศูนย์ทุกเที่ยงคืน ถ้าไม่จัดการ ระบบคุณจะมีอาการ "เป้ากระโดดย้อนอดีตวันละครั้ง" ตอนตีเจ็ดบ้านเรา วิธีแก้คือเทียบกับนาฬิกา server แล้วเติมวันเอง และอย่าลืมว่าเครื่องปลายทางทุกตัวต้องเดิน NTP ให้ตรงได้เชิงขั้วมา ไม่ได้พิกัด เหมือน TTM เลย ต้องแปลงเองด้วยตำแหน่งสถานีเรดาร์โค้ดแปลงนี่ใช้ได้กับทั้ง TTM และ CAT048 เลยครับ ตัวเดียวจบ
python import math
R = 6371008.8 # รัศมีโลกเฉลี่ย (เมตร)
def polar_to_latlon (site_lat, site_lon, rho_nm, theta_deg, north_offset_deg = 0.0 ):
d = (rho_nm * 1852 ) / R # ระยะเชิงมุม
brg = math.radians(theta_deg + north_offset_deg) # north_offset = ค่าชดเชยให้จานตรงเหนือจริง
f1, l1 = math.radians(site_lat), math.radians(site_lon)
f2 = math.asin(math.sin(f1) * math.cos(d) + math.cos(f1) * math.sin(d) * math.cos(brg))
l2 = l1 + math.atan2(math.sin(brg) * math.sin(d) * math.cos(f1),
math.cos(d) - math.sin(f1) * math.sin(f2))
return math.degrees(f2), (math.degrees(l2) + 540 ) % 360 - 180 1 2 3 4 5 6 7 8 9 10 11 12 13 14
ส่วนเรื่องจะเอาเป้าพวกนี้ไปวาดบนแผนที่ยังไง เก็บไว้ตอนหน้าครับ
EO/IR — คำนวณให้ถูก แล้วกล้องหมุนไปเอง
มาถึงตัวที่สนุกที่สุด กล้อง EO/IR (Electro-Optical / Infrared — กล้องตาเปล่ากับกล้องความร้อน) หน้าที่ของมันคือ พิสูจน์ทราบ เรดาร์บอกว่า "มีของ" AIS บอกว่า "ฉันชื่อนี้" แต่คนที่ตอบได้ว่าของที่เห็นตรงกับที่มันอ้างหรือเปล่า คือตาคนที่มองผ่านกล้อง
ปัญหาคือกล้องพวกนี้ซูมไกล และมุมมองตอนซูมสุดแคบจนเหมือนมองผ่านหลอดกาแฟ ให้คนนั่งหมุน joystick หาเรือกลางทะเลเอง กว่าจะเจอเรือก็ไปไกลแล้ว ของที่ต้องมีจึงเป็น slew-to-cue — คลิกเป้าบนแผนที่ปุ๊บ กล้องหันไปเองปั๊บ
มาตรฐานที่ทำให้ไม่ต้องแต่งงานกับยี่ห้อ: ONVIF
ONVIF คือข้อตกลงกลางของวงการกล้องวงจรปิด สิ่งที่เราสนมีสองส่วน คือ Profile S สำหรับสตรีมภาพ (ขอ URI แล้วไปดึง RTSP เอา) กับ PTZ service สำหรับสั่งหมุน (Pan/Tilt/Zoom) การคุยกันเป็น SOAP ผ่าน HTTP ครับ โบราณหน่อยแต่เปิดและมีเอกสาร
ลำดับที่ใช้จริงคือ ค้นหากล้องด้วย WS-Discovery (ยิง UDP multicast ไปที่ 239.255.255.250 พอร์ต 3702 แล้วรอกล้องขานรับ) → GetCapabilities เอา URL ของแต่ละ service → GetProfiles เอา ProfileToken → แล้วค่อยสั่ง AbsoluteMove
python # ใช้ไลบรารี ONVIF ทั่วไปของฝั่ง Python (ตระกูล onvif-zeep) — เบื้องหลังคือ SOAP over HTTP ล้วนๆ
# ถ้าไม่อยากพึ่งไลบรารี ก็ POST XML เองด้วย requests ได้เหมือนกัน ผลลัพธ์ไม่ต่างกัน
from onvif import ONVIFCamera
SPACE_PT = "http://www.onvif.org/ver10/tptz/PanTiltSpaces/SphericalPositionSpaceDegrees"
SPACE_Z = "http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace"
cam = ONVIFCamera(host, 80 , user, password)
media, ptz = cam.create_media_service(), cam.create_ptz_service()
profile = media.GetProfiles()[ 0 ]
# อ่านก่อนว่ากล้องตัวนี้รองรับ space ไหน ช่วงค่าเท่าไหร่ แล้วค่อยตัดสินใจว่าจะส่งองศาหรือส่ง -1..1
opts = ptz.GetConfigurationOptions({ "ConfigurationToken" : profile.PTZConfiguration.token})
def slew_to (pan_deg, tilt_deg, zoom01):
req = ptz.create_type( "AbsoluteMove" )
req.ProfileToken = profile.token
req.Position = {
"PanTilt" : { "x" : pan_deg, "y" : tilt_deg, "space" : SPACE_PT },
"Zoom" : { "x" : zoom01, "space" : SPACE_Z },
}
ptz.AbsoluteMove(req) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
ข้อสำคัญที่คนพลาดบ่อย: space ของค่าพิกัดมีหลายแบบ ถ้ากล้องรองรับ SphericalPositionSpaceDegrees คุณส่งองศาไปตรงๆ ได้เลย ชีวิตง่ายมาก แต่ถ้ามันรองรับแค่ PositionGenericSpace ค่าจะเป็น −1 ถึง 1 แล้วคุณต้องไปแมปเอง โดยอ่านช่วงจริงจาก GetConfigurationOptions ห้าม hard-code ว่า 1.0 เท่ากับ 180 องศา เพราะแต่ละรุ่นช่วงไม่เท่ากัน วันที่เปลี่ยนกล้องแล้วมันหันผิดทางทุกครั้ง คุณจะนึกถึงย่อหน้านี้
คณิตศาสตร์ของการชี้ให้ถูก
โจทย์คือ รู้พิกัดกล้อง (และความสูงเสา) รู้พิกัดเป้า ต้องหาว่าจะหันไปทิศไหน กดลงเท่าไหร่
python import math
R = 6371008.8
K = 1.14 # สัมประสิทธิ์การหักเหของแสงในบรรยากาศ (ฝั่งเรดาร์นิยมใช้ 4/3)
def cue (cam_lat, cam_lon, cam_height_m, tgt_lat, tgt_lon):
f1, f2 = math.radians(cam_lat), math.radians(tgt_lat)
dl = math.radians(tgt_lon - cam_lon)
# แบริ่งจริงจากกล้องไปเป้า
bearing = (math.degrees(math.atan2(
math.sin(dl) * math.cos(f2),
math.cos(f1) * math.sin(f2) - math.sin(f1) * math.cos(f2) * math.cos(dl))) + 360 ) % 360
# ระยะทางแนวราบ (haversine)
a = math.sin((f2 - f1) / 2 ) ** 2 + math.cos(f1) * math.cos(f2) * math.sin(dl / 2 ) ** 2
d = 2 * R * math.asin(math.sqrt(a))
# มุมกด = เรขาคณิตล้วน ลบด้วยผลของความโค้งโลก + การหักเหของแสง (ยิ่งไกลยิ่งสำคัญ)
down = math.atan(cam_height_m / d) - d / ( 2 * K * R)
return { "bearing" : bearing, "tilt_deg" : - math.degrees(down), "range_m" : d} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
พอได้เลขแล้วยังไม่จบ ของจริงมีอีกสามเรื่องที่ต้องเผื่อ
Boresight offset — "ศูนย์องศา" ของกล้องไม่เคยตรงกับทิศเหนือจริงตอนติดตั้ง วิธีแก้บ้านๆ ที่ได้ผลคือเล็งจุดที่รู้พิกัดแน่ๆ สักสามสี่จุด (ปลายแหลม เสาไฟ ทุ่น) จดค่าที่กล้องบอกเทียบกับค่าที่คำนวณได้ แล้วเก็บค่าชดเชยเป็น config ของกล้องตัวนั้น ห้ามฝังในโค้ด เพราะช่างขึ้นไปขยับทีเดียวค่าก็เปลี่ยนซูมเท่าไหร่ถึงจะเห็น ONVIF ให้ค่าซูมเป็น 0–1 ซึ่งไม่ได้บอกว่ากี่มิลลิเมตรหรือกี่องศา ต้องทำตารางเทียบเองว่าค่าซูมเท่านี้ได้มุมมองกว้างกี่องศา แล้วคำนวณย้อนจากความยาวเรือกับระยะทางว่าอยากให้เป้ากินพื้นที่จอสักหนึ่งในสี่ ต้องแคบแค่ไหนเป้าวิ่งหนีระหว่างที่กล้องกำลังหมุน กล้องใหญ่ๆ กวาดไปอีกฝั่งใช้เวลาหลายวินาที ถ้าส่งพิกัด ณ วินาทีที่คลิก พอกล้องหันถึงเรือก็เลื่อนไปแล้ว ทางแก้คือประมาณเวลาหมุนจากมุมที่ต้องกวาด แล้วยิงเป้าล่วงหน้าตาม COG/SOG — คิดซะว่า "เล็งนำ" เหมือนส่งบอลให้คนกำลังวิ่งสรุปตอนที่ 2 — สอง sensor ที่ต้องออกแรงกว่าจะได้หมุดสักหมุด เรดาร์ตัวเดียวอาจมีสาม Message Format เลือก Message Format ให้ตรงกับของที่อยากได้ตั้งแต่ตอนคุยสเปก ไม่ใช่ตอนไปยืนต่อสายที่หน้างาน ทั้ง TTM และ CAT048 ให้ของมาเป็นเชิงขั้ว แปลว่าตำแหน่งเสากับ "ศูนย์องศา" ต้องแม่นก่อน ไม่งั้นเป้าจะไม่มีวันไปจับคู่กับ AIS ได้ EO/IR ไม่ได้ยากที่การหมุน แต่ยากที่การชี้ให้ถูก — boresight offset, ตารางเทียบค่าซูม และการเล็งนำเป้าที่กำลังวิ่ง คือสามเรื่องที่ต้องเผื่อไว้ตั้งแต่แรก ตอนที่ 3 เป็นตอนจบของชุด "เรือที่ยอมให้เราเห็น" เริ่มด้วยการเอาเป้าที่เราแกะมาได้ทั้งสองตอน ทั้ง AIS เรดาร์ และกล้อง ไปพล็อตลงบนแผนที่ GIS จริงๆ ว่าจะจัด layer ยังไง ใช้สัญลักษณ์แบบไหน วาดหางเป้ากับกรวยมุมมองของกล้องยังไงให้ operator เหลือบตาเดียวแล้วรู้เรื่อง — เจอกันตอนหน้าครับ พวกแกร์