add UBX-MON-VER support and surface device identification in CLI/GUI

Add the Ver message parser and a fixed UbxQuery (was unused and had a
broken semaphore/state bug) to poll MON-VER once on connect. The CLI
prints hw/sw version and extensions; the GUI polls it per receiver,
pre-fills the read-only ID field with a connection counter and
rewrites it from the MOD= extension once the device answers, and
propagates the resolved display name to the Sky and Plot tabs so they
no longer show the stale placeholder ID.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XM4FNUjhu9x8df5Dp1hHvd
This commit is contained in:
2026-07-16 20:22:25 +02:00
co-authored by Claude Sonnet 5
parent d506f9afe0
commit 2dba3a9a2f
8 changed files with 176 additions and 27 deletions
+28 -4
View File
@@ -1,4 +1,4 @@
import sys, os import sys, os, itertools
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_gui = os.path.dirname(os.path.abspath(__file__)) _gui = os.path.dirname(os.path.abspath(__file__))
@@ -19,6 +19,8 @@ from backend.file_backend import source_id_from_path
BAUD_RATES = ['4800', '9600', '19200', '38400', '57600', '115200', BAUD_RATES = ['4800', '9600', '19200', '38400', '57600', '115200',
'230400', '460800', '921600'] '230400', '460800', '921600']
_connection_counter = itertools.count(1)
class _StatusDot(QLabel): class _StatusDot(QLabel):
def __init__(self): def __init__(self):
@@ -45,10 +47,11 @@ class ConnectionRow(QWidget):
layout.setContentsMargins(4, 2, 4, 2) layout.setContentsMargins(4, 2, 4, 2)
layout.setSpacing(6) layout.setSpacing(6)
# ── Receiver ID ────────────────────────────────────────────────────── # ── Receiver ID (read-only: pre-filled with a counter, rewritten from
# MON-VER once the device answers, or derived from the file path) ──
layout.addWidget(QLabel("ID:")) layout.addWidget(QLabel("ID:"))
self._id_edit = QLineEdit() self._id_edit = QLineEdit()
self._id_edit.setPlaceholderText("receiver-id") self._id_edit.setReadOnly(True)
self._id_edit.setFixedWidth(110) self._id_edit.setFixedWidth(110)
layout.addWidget(self._id_edit) layout.addWidget(self._id_edit)
@@ -125,6 +128,11 @@ class ConnectionRow(QWidget):
layout.addWidget(self._stack) layout.addWidget(self._stack)
layout.addStretch() layout.addStretch()
# ── Device identification (read-only, from MON-VER on connect) ────────
self._version_label = QLabel("")
self._version_label.setStyleSheet("color: #888888; font-size: 11px;")
layout.addWidget(self._version_label)
# ── Status / buttons ───────────────────────────────────────────────── # ── Status / buttons ─────────────────────────────────────────────────
self._dot = _StatusDot() self._dot = _StatusDot()
layout.addWidget(self._dot) layout.addWidget(self._dot)
@@ -141,6 +149,8 @@ class ConnectionRow(QWidget):
if config: if config:
self.apply_config(config) self.apply_config(config)
else:
self._id_edit.setText(f"conn-{next(_connection_counter)}")
# ── File browse ────────────────────────────────────────────────────────── # ── File browse ──────────────────────────────────────────────────────────
@@ -199,10 +209,17 @@ class ConnectionRow(QWidget):
self._connected = connected self._connected = connected
self._dot.set_connected(connected) self._dot.set_connected(connected)
self._conn_btn.setText("Disconnect" if connected else "Connect") self._conn_btn.setText("Disconnect" if connected else "Connect")
for w in [self._id_edit, self._type_combo, self._host_edit, for w in [self._type_combo, self._host_edit,
self._port_spin, self._ser_combo, self._baud_combo, self._port_spin, self._ser_combo, self._baud_combo,
self._file_edit, self._delay_spin]: self._file_edit, self._delay_spin]:
w.setEnabled(not connected) w.setEnabled(not connected)
if not connected:
self._version_label.setText("")
def set_version_info(self, device_id: str, sw_version: str):
if device_id:
self._id_edit.setText(device_id)
self._version_label.setText(sw_version)
# ── Slots ──────────────────────────────────────────────────────────────── # ── Slots ────────────────────────────────────────────────────────────────
@@ -287,6 +304,7 @@ class ConnectionTab(QWidget):
outer.addLayout(bottom) outer.addLayout(bottom)
model.connection_changed.connect(self._on_connection_changed) model.connection_changed.connect(self._on_connection_changed)
model.version_update.connect(self._on_version_update)
# ── History combo ───────────────────────────────────────────────────────── # ── History combo ─────────────────────────────────────────────────────────
@@ -332,3 +350,9 @@ class ConnectionTab(QWidget):
break break
if connected: if connected:
self._refresh_history_combo() self._refresh_history_combo()
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
for row in self._rows:
if row.get_receiver_id() == rid:
row.set_version_info(device_id, sw_version)
break
+46 -1
View File
@@ -1,4 +1,4 @@
import sys, os, json, dataclasses, time import sys, os, json, dataclasses, time, threading
from dataclasses import dataclass from dataclasses import dataclass
from PyQt5.QtCore import QObject, QTimer, pyqtSignal from PyQt5.QtCore import QObject, QTimer, pyqtSignal
@@ -13,6 +13,8 @@ from nmea.packetizer import NmeaPacketizer
from ubx.packetizer import UbxPacketizer from ubx.packetizer import UbxPacketizer
from backend.network_backend import NetworkBackend from backend.network_backend import NetworkBackend
from ubx.ubx import frame_create from ubx.ubx import frame_create
from ubx.query import UbxQuery
from ubx.messages import Ver
GNSS_NAMES = { GNSS_NAMES = {
0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou', 0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou',
@@ -35,6 +37,14 @@ LRU_PATH = os.path.expanduser('~/.nmea_client_gui.json')
LRU_MAX = 20 LRU_MAX = 20
def _device_id_from_ver(ver: dict) -> str:
for ext in ver.get('extensions', []):
s = ext.get('extension', '')
if s.startswith('MOD='):
return s[len('MOD='):]
return ver.get('hw_version', '')
@dataclass @dataclass
class SatelliteData: class SatelliteData:
gnss_id: int gnss_id: int
@@ -85,12 +95,15 @@ class _ReceiverState:
self.backend = None self.backend = None
self.transceiver = None self.transceiver = None
self.poll_timer = None self.poll_timer = None
self.device_id: str = ''
self.sw_version: str = ''
class ReceiverManager(QObject): class ReceiverManager(QObject):
satellite_update = pyqtSignal(str, list) # rid, [SatelliteData] satellite_update = pyqtSignal(str, list) # rid, [SatelliteData]
pdop_update = pyqtSignal(str, float, float) # rid, pdop, timestamp pdop_update = pyqtSignal(str, float, float) # rid, pdop, timestamp
connection_changed = pyqtSignal(str, bool) # rid, connected connection_changed = pyqtSignal(str, bool) # rid, connected
version_update = pyqtSignal(str, str, str) # rid, device_id, sw_version
receiver_added = pyqtSignal(str) receiver_added = pyqtSignal(str)
receiver_removed = pyqtSignal(str) receiver_removed = pyqtSignal(str)
_connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread _connection_lost = pyqtSignal(str) # internal: backend thread -> GUI thread
@@ -101,6 +114,7 @@ class ReceiverManager(QObject):
self._configs: dict = {} self._configs: dict = {}
self._lru: list = self._load_lru() self._lru: list = self._load_lru()
self._connection_lost.connect(self._handle_connection_lost) self._connection_lost.connect(self._handle_connection_lost)
self.version_update.connect(self._on_version)
# ── LRU ────────────────────────────────────────────────────────────────── # ── LRU ──────────────────────────────────────────────────────────────────
@@ -197,6 +211,11 @@ class ReceiverManager(QObject):
GsaQtListener(rid, lambda p, t, r=rid: self._on_pdop(r, p, t)) GsaQtListener(rid, lambda p, t, r=rid: self._on_pdop(r, p, t))
) )
ver_query = None
if config.conn_type != 'file':
ver_query = UbxQuery(xcvr, Ver())
ubx_sink.register_listener(ver_query)
backend.connect() backend.connect()
backend.start() backend.start()
@@ -211,9 +230,18 @@ class ReceiverManager(QObject):
timer.start() timer.start()
state.poll_timer = timer state.poll_timer = timer
threading.Thread(target=self._poll_version, args=(rid, ver_query),
daemon=True).start()
self._push_lru(config) self._push_lru(config)
self.connection_changed.emit(rid, True) self.connection_changed.emit(rid, True)
def _poll_version(self, rid: str, query: UbxQuery):
ver = query.poll()
if ver is None:
return
self.version_update.emit(rid, _device_id_from_ver(ver), ver['sw_version'])
def _handle_connection_lost(self, rid: str): def _handle_connection_lost(self, rid: str):
state = self._states.get(rid) state = self._states.get(rid)
if not state or not state.connected: if not state or not state.connected:
@@ -237,7 +265,10 @@ class ReceiverManager(QObject):
state.backend = None state.backend = None
state.transceiver = None state.transceiver = None
state.satellites.clear() state.satellites.clear()
state.device_id = ''
state.sw_version = ''
self.connection_changed.emit(rid, False) self.connection_changed.emit(rid, False)
self.version_update.emit(rid, '', '')
def _poll_ubx(self, rid: str): def _poll_ubx(self, rid: str):
state = self._states.get(rid) state = self._states.get(rid)
@@ -263,6 +294,14 @@ class ReceiverManager(QObject):
def get_config(self, rid: str): def get_config(self, rid: str):
return self._configs.get(rid) return self._configs.get(rid)
def get_version_info(self, rid: str):
s = self._states.get(rid)
return (s.device_id, s.sw_version) if s else ('', '')
def display_name(self, rid: str) -> str:
s = self._states.get(rid)
return (s.device_id if s and s.device_id else rid)
def get_satellites(self, rids: list) -> list: def get_satellites(self, rids: list) -> list:
result = [] result = []
for rid in rids: for rid in rids:
@@ -288,3 +327,9 @@ class ReceiverManager(QObject):
return return
state.pdop = pdop state.pdop = pdop
self.pdop_update.emit(rid, pdop, ts) self.pdop_update.emit(rid, pdop, ts)
def _on_version(self, rid: str, device_id: str, sw_version: str):
state = self._states.get(rid)
if state:
state.device_id = device_id
state.sw_version = sw_version
+11 -1
View File
@@ -316,6 +316,7 @@ class SinglePlotWidget(QWidget):
model.receiver_removed.connect(self._on_receiver_removed) model.receiver_removed.connect(self._on_receiver_removed)
model.satellite_update.connect(self._on_sat_update) model.satellite_update.connect(self._on_sat_update)
model.pdop_update.connect(self._on_pdop_update) model.pdop_update.connect(self._on_pdop_update)
model.version_update.connect(self._on_version_update)
# ── UI ──────────────────────────────────────────────────────────────────── # ── UI ────────────────────────────────────────────────────────────────────
@@ -425,7 +426,7 @@ class SinglePlotWidget(QWidget):
for rid in selected: for rid in selected:
ts_dq, v_dq = self._data.get(rid, ([], [])) ts_dq, v_dq = self._data.get(rid, ([], []))
if ts_dq: if ts_dq:
snapshot[rid] = (list(ts_dq), list(v_dq)) snapshot[self._model.display_name(rid)] = (list(ts_dq), list(v_dq))
w, h = self._canvas.width(), self._canvas.height() w, h = self._canvas.width(), self._canvas.height()
if w > 0 and h > 0: if w > 0 and h > 0:
self._thread.submit(snapshot, self._y_cb.currentText(), w, h) self._thread.submit(snapshot, self._y_cb.currentText(), w, h)
@@ -485,6 +486,15 @@ class SinglePlotWidget(QWidget):
self._rebuild_val_combo() self._rebuild_val_combo()
self._submit_render() self._submit_render()
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
label = self._model.display_name(rid)
for i in range(self._src_list.count()):
item = self._src_list.item(i)
if item.data(Qt.UserRole) == rid:
item.setText(f" {label} ")
break
self._submit_render()
def _on_sat_update(self, rid: str, sats: list): def _on_sat_update(self, rid: str, sats: list):
selected = set(self._selected_rids()) selected = set(self._selected_rids())
if rid not in selected: if rid not in selected:
+18 -1
View File
@@ -34,6 +34,7 @@ class SkyTab(QWidget):
self._pdop: dict = {} # rid -> float self._pdop: dict = {} # rid -> float
self._info_cards: dict = {} # rid -> QFrame self._info_cards: dict = {} # rid -> QFrame
self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used) self._info_labels: dict = {} # rid -> (lbl_pdop, lbl_visible, lbl_used)
self._name_labels: dict = {} # rid -> QLabel (receiver display name)
self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)] self._trail: dict = {} # (rid, gnss_id, sv_id) -> deque[(elev, azim)]
self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer self._hi_timers: dict = {} # (rid, gnss_id, sv_id) -> QTimer
@@ -113,6 +114,7 @@ class SkyTab(QWidget):
model.receiver_removed.connect(self._on_receiver_removed) model.receiver_removed.connect(self._on_receiver_removed)
model.satellite_update.connect(self._on_sat_update) model.satellite_update.connect(self._on_sat_update)
model.pdop_update.connect(self._on_pdop_update) model.pdop_update.connect(self._on_pdop_update)
model.version_update.connect(self._on_version_update)
# ── Source list management ──────────────────────────────────────────────── # ── Source list management ────────────────────────────────────────────────
@@ -136,6 +138,7 @@ class SkyTab(QWidget):
cl.addWidget(w) cl.addWidget(w)
self._info_cards[rid] = card self._info_cards[rid] = card
self._info_labels[rid] = (lbl_pdop, lbl_visible, lbl_used) self._info_labels[rid] = (lbl_pdop, lbl_visible, lbl_used)
self._name_labels[rid] = name_lbl
self._info_layout.insertWidget(self._info_layout.count() - 1, card) self._info_layout.insertWidget(self._info_layout.count() - 1, card)
def _on_receiver_removed(self, rid: str): def _on_receiver_removed(self, rid: str):
@@ -151,6 +154,20 @@ class SkyTab(QWidget):
self._info_layout.removeWidget(card) self._info_layout.removeWidget(card)
card.deleteLater() card.deleteLater()
self._info_labels.pop(rid, None) self._info_labels.pop(rid, None)
self._name_labels.pop(rid, None)
self._refresh_immediate()
def _on_version_update(self, rid: str, device_id: str, sw_version: str):
label = self._model.display_name(rid)
for i in range(self._src_list.count()):
item = self._src_list.item(i)
if item.data(Qt.UserRole) == rid:
item.setText(f" {label} ")
break
lbl = self._name_labels.get(rid)
if lbl:
lbl.setText(f"<b>{label}</b>")
self._table_keys = [] # force a full rebuild so the Receiver column updates
self._refresh_immediate() self._refresh_immediate()
def _selected_rids(self) -> set: def _selected_rids(self) -> set:
@@ -297,7 +314,7 @@ class SkyTab(QWidget):
self._table.setItem(row, 3, cell(f"{sat.azimuth:.0f}")) self._table.setItem(row, 3, cell(f"{sat.azimuth:.0f}"))
self._table.setItem(row, 4, cell(f"{sat.cno:.1f}")) self._table.setItem(row, 4, cell(f"{sat.cno:.1f}"))
self._table.setItem(row, 5, cell("" if sat.used_in_fix else "")) self._table.setItem(row, 5, cell("" if sat.used_in_fix else ""))
self._table.setItem(row, 6, cell(rid)) self._table.setItem(row, 6, cell(self._model.display_name(rid)))
msg_item = QTableWidgetItem("") msg_item = QTableWidgetItem("")
msg_item.setTextAlignment(Qt.AlignCenter) msg_item.setTextAlignment(Qt.AlignCenter)
msg_item.setBackground(brush) msg_item.setBackground(brush)
+31 -6
View File
@@ -3,6 +3,7 @@ import time
import json import json
import signal import signal
import argparse import argparse
import threading
from backend.network_backend import NetworkBackend from backend.network_backend import NetworkBackend
from backend.serial_backend import SerialBackend from backend.serial_backend import SerialBackend
@@ -16,8 +17,9 @@ from nmea.messages import Gll, Gsa, Gsv, Gga, Rmc
from ubx.ubx import frame_create from ubx.ubx import frame_create
from ubx.msg_types import * from ubx.msg_types import *
from ubx.packetizer import UbxPacketizer from ubx.packetizer import UbxPacketizer
from ubx.messages import Sig, Sat, RawX, UbxMsg from ubx.messages import Sig, Sat, RawX, Ver, UbxMsg
from ubx.listener import UbxListener from ubx.listener import UbxListener
from ubx.query import UbxQuery
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -87,7 +89,8 @@ def _make_listeners(name: str):
def wire_source(name: str, backend, log_dir: str | None, def wire_source(name: str, backend, log_dir: str | None,
loggers: list, transceivers: dict, pacer_delay: float = 0.0): loggers: list, transceivers: dict, ver_queries: dict,
pacer_delay: float = 0.0):
xcvr = Transceiver(name) xcvr = Transceiver(name)
backend.register_xcvr(xcvr) backend.register_xcvr(xcvr)
transceivers[name] = xcvr transceivers[name] = xcvr
@@ -104,6 +107,10 @@ def wire_source(name: str, backend, log_dir: str | None,
nmea_rx, ubx_rx = _make_listeners(name) nmea_rx, ubx_rx = _make_listeners(name)
ver_query = UbxQuery(xcvr, Ver())
ubx_rx.append(ver_query)
ver_queries[name] = ver_query
if pacer_delay > 0: if pacer_delay > 0:
nmea_pacer = ReplayPacer(pacer_delay) nmea_pacer = ReplayPacer(pacer_delay)
ubx_pacer = ReplayPacer(pacer_delay) ubx_pacer = ReplayPacer(pacer_delay)
@@ -157,6 +164,7 @@ if __name__ == '__main__':
backends = [] backends = []
loggers = [] loggers = []
transceivers = {} transceivers = {}
ver_queries = {}
# Network sources → one shared NetworkBackend # Network sources → one shared NetworkBackend
net_sources = [s for s in sources if s['type'] == 'network'] net_sources = [s for s in sources if s['type'] == 'network']
@@ -165,13 +173,14 @@ if __name__ == '__main__':
for s in net_sources]) for s in net_sources])
backends.append(net_be) backends.append(net_be)
for s in net_sources: for s in net_sources:
wire_source(s['name'], net_be, log_dir, loggers, transceivers) wire_source(s['name'], net_be, log_dir, loggers, transceivers, ver_queries)
# Serial sources → one SerialBackend each # Serial sources → one SerialBackend each
for s in [s for s in sources if s['type'] == 'serial']: serial_sources = [s for s in sources if s['type'] == 'serial']
for s in serial_sources:
ser_be = SerialBackend(s['device'], s.get('baudrate', 115200)) ser_be = SerialBackend(s['device'], s.get('baudrate', 115200))
backends.append(ser_be) backends.append(ser_be)
wire_source(s['name'], ser_be, log_dir, loggers, transceivers) wire_source(s['name'], ser_be, log_dir, loggers, transceivers, ver_queries)
# File sources → one shared FileBackend (no secondary logging) # File sources → one shared FileBackend (no secondary logging)
file_sources = [s for s in sources if s['type'] == 'file'] file_sources = [s for s in sources if s['type'] == 'file']
@@ -182,7 +191,8 @@ if __name__ == '__main__':
name = source_id_from_path(s['path']) name = source_id_from_path(s['path'])
if name: if name:
wire_source(name, file_be, log_dir=None, loggers=loggers, wire_source(name, file_be, log_dir=None, loggers=loggers,
transceivers=transceivers, pacer_delay=delay) transceivers=transceivers, ver_queries=ver_queries,
pacer_delay=delay)
signal.signal(signal.SIGINT, signal.signal(signal.SIGINT,
lambda sig, frm: handler(sig, frm, backends, loggers)) lambda sig, frm: handler(sig, frm, backends, loggers))
@@ -191,6 +201,21 @@ if __name__ == '__main__':
be.connect() be.connect()
be.start() be.start()
# Poll MON-VER once per live (network/serial) source, off the main thread
# since UbxQuery.poll() blocks until the response frame arrives.
def report_version(name: str, query: UbxQuery):
ver = query.poll()
if ver is None:
return
ext = '; '.join(e['extension'] for e in ver['extensions'])
print(f"{name}: device hw={ver['hw_version']} sw={ver['sw_version']}"
+ (f" ({ext})" if ext else ""))
for s in net_sources + serial_sources:
name = s['name']
threading.Thread(target=report_version, args=(name, ver_queries[name]),
daemon=True).start()
# Poll loop only meaningful for network/serial sources # Poll loop only meaningful for network/serial sources
net_names = [s['name'] for s in net_sources] net_names = [s['name'] for s in net_sources]
if net_names: if net_names:
+2 -1
View File
@@ -1,3 +1,4 @@
from ubx.messages.msg import UbxMsg from ubx.messages.msg import UbxMsg
from ubx.messages.nav import Sig, Sat from ubx.messages.nav import Sig, Sat
from ubx.messages.rxm import RawX from ubx.messages.rxm import RawX
from ubx.messages.mon import Ver
+32
View File
@@ -0,0 +1,32 @@
import struct
from ubx.messages.msg import UbxMsg
class Ver(UbxMsg):
def __init__(self):
UbxMsg.__init__(self, 0x0A, 0x04)
class Group1:
@classmethod
def from_bytes(cls, data: bytes):
obj = Ver.Group1()
obj.extension = data[0:30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
return obj.__dict__
@classmethod
def from_bytes(cls, data: bytes):
obj = Ver()
obj.sw_version = data[0:30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
obj.hw_version = data[30:40].split(b"\x00", 1)[0].decode("ascii", errors="replace")
obj.extensions = []
offset = 40
remain = len(data) - offset
group_size = 30
n = offset
while remain >= group_size:
extension = cls.Group1.from_bytes(data[n:n + group_size])
n += group_size
remain -= group_size
obj.extensions.append(extension)
if remain:
print(f"Ver.from_bytes: {remain} trailing bytes unparsed (payload size mismatch)")
return obj.__dict__
+8 -13
View File
@@ -3,30 +3,25 @@ from ubx.ubx import frame_parse, frame_create
from ubx.messages.msg import UbxMsg from ubx.messages.msg import UbxMsg
from transceiver import Transceiver from transceiver import Transceiver
from ubx.listener import UbxListener from ubx.listener import UbxListener
from threading import BoundedSemaphore from threading import Event
class UbxQuery(UbxListener): class UbxQuery(UbxListener):
def __init__(self, transceiver: Transceiver, msg: UbxMsg): def __init__(self, transceiver: Transceiver, msg: UbxMsg):
UbxListener.__init__(self, msg) UbxListener.__init__(self, msg)
self.transceiver = transceiver self.transceiver = transceiver
self.sema = BoundedSemaphore() self.result = None
self.event = Event()
def on_recv(self, msg: MsgContainer): def on_recv(self, msg: MsgContainer):
if self.is_msg(msg): if self.is_msg(msg):
data, hdr = frame_parse(msg.data) data, hdr = frame_parse(msg.data)
msg_parsed = self.msg.from_bytes(data) self.result = self.msg.from_bytes(data)
self.msg = msg_parsed self.event.set()
self.sema.release()
def poll(self): def poll(self):
self.event.clear()
frame = frame_create(self.msg.mclass, self.msg.mid, b'') frame = frame_create(self.msg.mclass, self.msg.mid, b'')
self.transceiver.send(frame) self.transceiver.send(frame)
self.sema.acquire() self.event.wait()
print(self.msg) return self.result
def query(self, data: bytes):
frame = frame_create(0x01, 0x35, b'')
self.transceiver.send(frame)
self.sema.acquire()
print(self.msg)