(Add) ntrip_client.py GGA sentence generation and sending
This commit is contained in:
@@ -8,11 +8,65 @@ from rclpy.node import Node
|
||||
from rclpy.qos import QoSProfile, HistoryPolicy, ReliabilityPolicy, DurabilityPolicy
|
||||
from mavros_msgs.msg import RTCM
|
||||
|
||||
# TODO: ROS_DOMAIN_ID 要補一下
|
||||
|
||||
class GGA_stream():
|
||||
|
||||
@classmethod
|
||||
def nmea_checksum(cls, body: str) -> str:
|
||||
"""body 不含 '$' 與 '*checksum',例如 'GPGGA,123519,...'"""
|
||||
value = 0
|
||||
for ch in body:
|
||||
value ^= ord(ch)
|
||||
return f"{value:02X}"
|
||||
|
||||
@classmethod
|
||||
def decimal_to_nmea_dm(cls, deg: float, *, is_latitude: bool) -> tuple[str, str]:
|
||||
"""十進位度 → NMEA 的 (d)dmm.mmmm 與半球字元。"""
|
||||
if is_latitude:
|
||||
hemi = "N" if deg >= 0 else "S"
|
||||
deg_width = 2
|
||||
else:
|
||||
hemi = "E" if deg >= 0 else "W"
|
||||
deg_width = 3
|
||||
deg = abs(deg)
|
||||
d = int(deg)
|
||||
m = (deg - d) * 60.0
|
||||
return f"{d:0{deg_width}d}{m:07.4f}", hemi
|
||||
|
||||
@classmethod
|
||||
def build_gga_sentence(cls, lat_deg: float, lon_deg: float, alt_m: float = 100.0) -> bytes:
|
||||
"""
|
||||
組一筆 $GPGGA 句子(含 checksum) 回傳 bytes 可直接 sock.sendall。
|
||||
|
||||
lat_deg / lon_deg : 十進位經緯度(北、東為正)。
|
||||
測試時請改成 mount 服務範圍內的近似位置;正式使用應來自 GNSS 真實定位。
|
||||
"""
|
||||
utc = time.gmtime()
|
||||
t_str = f"{utc.tm_hour:02d}{utc.tm_min:02d}{utc.tm_sec:02d}.00"
|
||||
|
||||
lat_dm, ns = cls.decimal_to_nmea_dm(lat_deg, is_latitude=True)
|
||||
lon_dm, ew = cls.decimal_to_nmea_dm(lon_deg, is_latitude=False)
|
||||
|
||||
# quality=1(GPS fix)、8 顆星、HDOP=1.0 僅供測試示意
|
||||
body = (
|
||||
f"GPGGA,{t_str},{lat_dm},{ns},{lon_dm},{ew},"
|
||||
f"1,08,1.0,{alt_m:.1f},M,0.0,M,,"
|
||||
)
|
||||
sentence = f"${body}*{cls.nmea_checksum(body)}\r\n"
|
||||
return sentence.encode("ascii")
|
||||
|
||||
@classmethod
|
||||
def send_gga(cls, sock: socket.socket, lat_deg: float, lon_deg: float, alt_m: float = 100.0) -> str:
|
||||
"""送出 GGA,回傳可讀句子供列印。"""
|
||||
payload = cls.build_gga_sentence(lat_deg, lon_deg, alt_m)
|
||||
sock.sendall(payload)
|
||||
return payload.decode("ascii").strip()
|
||||
|
||||
class NtripClientNode(Node):
|
||||
"""連線 NTRIP caster,接收 RTCM3 資料流並發布至 ROS2 topic。"""
|
||||
"""連線 NTRIP caster, 接收 RTCM3 資料流並發布至 ROS2 topic """
|
||||
|
||||
RECONNECT_BASE_SEC = 2.0
|
||||
RECONNECT_BASE_SEC = 10.0
|
||||
RECONNECT_MAX_SEC = 60.0
|
||||
|
||||
def __init__(self):
|
||||
@@ -21,9 +75,15 @@ class NtripClientNode(Node):
|
||||
self.declare_parameter('host', 'rtk2go.com')
|
||||
self.declare_parameter('port', 2101)
|
||||
self.declare_parameter('mountpoint', '')
|
||||
self.declare_parameter('username', '')
|
||||
self.declare_parameter('username', 'template@mail.com')
|
||||
self.declare_parameter('password', '')
|
||||
|
||||
self.declare_parameter('gga_on', False)
|
||||
self.declare_parameter('gga_lat', None)
|
||||
self.declare_parameter('gga_lon', None)
|
||||
self.declare_parameter('gga_alt_m', 100)
|
||||
self.declare_parameter('gga_interval_sec', 300)
|
||||
|
||||
rtcm_qos = QoSProfile(
|
||||
history=HistoryPolicy.KEEP_LAST,
|
||||
depth=1,
|
||||
@@ -43,28 +103,36 @@ class NtripClientNode(Node):
|
||||
def _receive_loop(self):
|
||||
backoff = self.RECONNECT_BASE_SEC
|
||||
|
||||
while self._running:
|
||||
host = self.get_parameter('host').value
|
||||
port = self.get_parameter('port').value
|
||||
mount = self.get_parameter('mountpoint').value
|
||||
user = self.get_parameter('username').value
|
||||
pwd = self.get_parameter('password').value
|
||||
host = self.get_parameter('host').value
|
||||
port = self.get_parameter('port').value
|
||||
mountpoint = self.get_parameter('mountpoint').value
|
||||
user = self.get_parameter('username').value
|
||||
pwd = self.get_parameter('password').value
|
||||
|
||||
if not mount:
|
||||
self.get_logger().error('mountpoint 參數未設定,等待重試...')
|
||||
time.sleep(backoff)
|
||||
continue
|
||||
if not mountpoint:
|
||||
self.get_logger().error('mountpoint 參數未設定...')
|
||||
return
|
||||
|
||||
while self._running:
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
|
||||
self.gga_lat = self.get_parameter('gga_lat').value
|
||||
self.gga_lon = self.get_parameter('gga_lon').value
|
||||
self.gga_alt_m = self.get_parameter('gga_alt_m').value
|
||||
self.gga_interval_sec = self.get_parameter('gga_interval_sec').value
|
||||
self.gga_on = self.get_parameter('gga_on').value
|
||||
self.gga_on = self.gga_lat is not None and self.gga_lon is not None and self.gga_on
|
||||
|
||||
|
||||
try:
|
||||
self.get_logger().info(f'正在連線 {host}:{port}/{mount} ...')
|
||||
self.get_logger().info(f'正在連線 {host}:{port}/{mountpoint} ...')
|
||||
sock.connect((host, port))
|
||||
|
||||
auth = base64.b64encode(f'{user}:{pwd}'.encode()).decode()
|
||||
request = (
|
||||
f'GET /{mount} HTTP/1.0\r\n'
|
||||
f'GET /{mountpoint} HTTP/1.0\r\n'
|
||||
f'User-Agent: NTRIP PythonClient\r\n'
|
||||
f'Authorization: Basic {auth}\r\n'
|
||||
f'Connection: close\r\n\r\n'
|
||||
@@ -78,7 +146,9 @@ class NtripClientNode(Node):
|
||||
)
|
||||
raise ConnectionError('handshake failed')
|
||||
|
||||
self.get_logger().info(f'已連接至 {mount},開始接收 RTCM 資料流')
|
||||
self.get_logger().info(f'已成功連接至 {mountpoint}')
|
||||
|
||||
|
||||
backoff = self.RECONNECT_BASE_SEC
|
||||
|
||||
self._read_stream(sock)
|
||||
@@ -93,14 +163,21 @@ class NtripClientNode(Node):
|
||||
|
||||
def _read_stream(self, sock: socket.socket):
|
||||
buf = b''
|
||||
last_gga_time = 0.0
|
||||
|
||||
while self._running:
|
||||
now = time.time()
|
||||
if self.gga_on and (now - last_gga_time >= self.gga_interval_sec):
|
||||
line = GGA_stream.send_gga(sock, self.gga_lat, self.gga_lon, self.gga_alt_m)
|
||||
self.get_logger().info(f"[GGA] {line}")
|
||||
last_gga_time = now
|
||||
|
||||
try:
|
||||
chunk = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
continue
|
||||
if not chunk:
|
||||
break
|
||||
raise ConnectionError("stream ended. Empty Chunk")
|
||||
buf += chunk
|
||||
|
||||
while len(buf) >= 6:
|
||||
|
||||
Reference in New Issue
Block a user