SimCore
Shared transport-layer plumbing for the ADR-029 Train/IL/CTC RBC simulators
Toggle main menu visibility
Loading...
Searching...
No Matches
safecomm.py
Go to the documentation of this file.
1
"""SafeCommLink - the CommServer-facing client link for a sim (train).
2
3
This is the Python client side of the safe-communication path introduced
4
with the C ``safeCommFreamwork`` / ``CommServer`` product:
5
6
TrainSim --(this module, plain TCP)--> CommServer --> RBC role-C gateway
7
8
CommServer owns the WEST/EAST dual-homing, so from the sim's point of view
9
there is a single logical link. It is a drop-in alternative to
10
``simcore.dual_link.DualHomedLink`` - same ``ensure_connected()`` /
11
``send_all()`` / ``poll_recv()`` surface - selected by ``train_sim`` when
12
the config carries a ``commServer`` block.
13
14
Stub, matching the C side: the Safety Functional Module (authentication,
15
per-message MAC, sequence numbering - ``safecomm::safety``) is
16
NOT_IMPLEMENTED, so this link is a transparent byte pipe with fixed
17
``message_size`` framing. When the SaF PDU layout lands in
18
``safeCommFreamwork`` this module gets the matching wrapper/unwrapper,
19
kept byte-identical the same way ``simcore.rbc_wire`` mirrors the C codec.
20
"""
21
22
import
socket
23
import
time
24
25
# CommServer hides which real site (WEST/EAST) answered - it owns the
26
# dual-homing. The sim's per-site bookkeeping (was_connected, log tags,
27
# `for site in SITES`) still expects a site name, so this link presents
28
# everything under the WEST slot and leaves EAST permanently unused.
29
_SITE =
"WEST"
30
_UNUSED_SITE =
"EAST"
31
32
# Reconnect policy: this is a CLIENT link - if CommServer restarts, its
33
# host goes away, or the stream errors, the sim must recover on its own.
34
# ensure_connected() is called once per sim cycle; it makes at most ONE
35
# non-blocking connect() attempt per call and never spins/sleeps inside a
36
# single call (a blocking retry loop here stalled the whole sim - control
37
# port, status, other trains' M136 - whenever CommServer was down).
38
_CONNECT_TIMEOUT_S = 0.75
# bound one connect() attempt
39
_RECONNECT_MIN_INTERVAL_S = 1.0
# don't hammer connect() every ~0.3 s cycle
40
41
42
class
SafeCommLink
:
43
"""Single TCP link to a CommServer, framed as fixed-size messages.
44
Auto-reconnecting: any send/recv error drops the socket, and the next
45
ensure_connected() re-dials (rate-limited)."""
46
47
def
__init__
(self, log, host, port, message_size):
48
self.
_log
= log
49
self.
_host
= host
50
self.
_port
= int(port)
51
self.
message_size
= int(message_size)
52
self.
_sock
=
None
53
self.
_rx
= bytearray()
54
self.
_last_attempt
= 0.0
55
self.
_ever_connected
=
False
56
57
# -- lifecycle --------------------------------------------------------
58
def
ensure_connected
(self):
59
"""One non-blocking (re)connect attempt, rate-limited. Safe to call
60
every cycle - a no-op once connected. Returns True if connected."""
61
if
self.
_sock
is
not
None
:
62
return
True
63
now = time.monotonic()
64
if
(now - self.
_last_attempt
) < _RECONNECT_MIN_INTERVAL_S:
65
return
False
66
self.
_last_attempt
= now
67
try
:
68
s = socket.create_connection((self.
_host
, self.
_port
), timeout=_CONNECT_TIMEOUT_S)
69
s.setblocking(
False
)
70
try
:
71
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
72
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
73
except
OSError:
74
pass
75
self.
_sock
= s
76
self.
_rx
.clear()
77
level = self.
_log
.info
if
not
self.
_ever_connected
else
self.
_log
.warning
78
level(
"SafeCommLink %s to CommServer %s:%d"
,
79
"connected"
if
not
self.
_ever_connected
else
"RECONNECTED"
,
80
self.
_host
, self.
_port
)
81
self.
_ever_connected
=
True
82
return
True
83
except
OSError
as
exc:
84
# DEBUG, not WARNING: this fires every retry interval while
85
# CommServer is down and would otherwise flood the log.
86
self.
_log
.debug(
"SafeCommLink connect %s:%d failed (%s) - will retry"
,
87
self.
_host
, self.
_port
, exc)
88
return
False
89
90
@property
91
def
sockets
(self):
92
"""DualHomedLink-compatible view: WEST slot carries the one TCP
93
socket, EAST slot is always None."""
94
return
{_SITE: self.
_sock
, _UNUSED_SITE:
None
}
95
96
def
connected_sites
(self):
97
return
[_SITE]
if
self.
_sock
is
not
None
else
[]
98
99
def
disconnect
(self, site=None):
# noqa: ARG002 - parity with DualHomedLink
100
self.
_close
()
101
102
def
_close
(self, site=None):
# noqa: ARG002
103
if
self.
_sock
is
not
None
:
104
try
:
105
self.
_sock
.close()
106
except
OSError:
107
pass
108
self.
_sock
=
None
109
self.
_rx
.clear()
110
111
# -- I/O ------------------------------------------------------------
112
def
_frame
(self, payload):
113
b = bytes(payload)
114
if
len(b) < self.
message_size
:
115
b = b + b
"\x00"
* (self.
message_size
- len(b))
116
elif
len(b) > self.
message_size
:
117
b = b[: self.
message_size
]
118
return
b
119
120
def
send_all
(self, payload):
121
"""Send one message to the CommServer (padded/truncated to
122
``message_size``). Silently drops if not connected - the caller's
123
own ``ensure_connected()`` loop re-establishes."""
124
if
self.
_sock
is
None
:
125
return
126
try
:
127
self.
_sock
.sendall(self.
_frame
(payload))
128
except
OSError
as
exc:
129
self.
_log
.warning(
"SafeCommLink send failed (%s) - dropping link"
, exc)
130
self.
_close
()
131
132
def
poll_recv
(self, timeout=0.0):
133
"""Yield ``(site, message_bytes)`` for every whole framed message
134
available within ``timeout`` seconds."""
135
if
self.
_sock
is
None
:
136
return
137
end = time.monotonic() + max(0.0, timeout)
138
first =
True
139
while
first
or
time.monotonic() < end:
140
first =
False
141
try
:
142
chunk = self.
_sock
.recv(4096)
143
except
BlockingIOError:
144
if
timeout <= 0.0:
145
break
146
time.sleep(0.005)
147
continue
148
except
OSError
as
exc:
149
self.
_log
.warning(
"SafeCommLink recv failed (%s) - dropping link"
, exc)
150
self.
_close
()
151
return
152
if
not
chunk:
153
self.
_log
.warning(
"SafeCommLink: CommServer closed the connection"
)
154
self.
_close
()
155
return
156
self.
_rx
.extend(chunk)
157
while
len(self.
_rx
) >= self.
message_size
:
158
msg = bytes(self.
_rx
[: self.
message_size
])
159
del self.
_rx
[: self.
message_size
]
160
yield
_SITE, msg
161
162
163
def
link_from_config
(log, config, message_size, sim_config):
164
"""Return a SafeCommLink if the config has a ``commServer`` block,
165
else None (caller falls back to DualHomedLink)."""
166
block = config.get(
"commServer"
)
if
isinstance(config, dict)
else
None
167
if
not
block:
168
return
None
169
host = sim_config.require(config,
"commServer"
,
"host"
)
170
port = sim_config.require(config,
"commServer"
,
"port"
)
171
return
SafeCommLink
(log, host, port, message_size)
simcore.safecomm.SafeCommLink
Definition
safecomm.py:42
simcore.safecomm.SafeCommLink.ensure_connected
ensure_connected(self)
Definition
safecomm.py:58
simcore.safecomm.SafeCommLink.__init__
__init__(self, log, host, port, message_size)
Definition
safecomm.py:47
simcore.safecomm.SafeCommLink.poll_recv
poll_recv(self, timeout=0.0)
Definition
safecomm.py:132
simcore.safecomm.SafeCommLink._close
_close(self, site=None)
Definition
safecomm.py:102
simcore.safecomm.SafeCommLink._frame
_frame(self, payload)
Definition
safecomm.py:112
simcore.safecomm.SafeCommLink._last_attempt
float _last_attempt
Definition
safecomm.py:54
simcore.safecomm.SafeCommLink._rx
_rx
Definition
safecomm.py:53
simcore.safecomm.SafeCommLink._log
_log
Definition
safecomm.py:48
simcore.safecomm.SafeCommLink._sock
_sock
Definition
safecomm.py:52
simcore.safecomm.SafeCommLink.message_size
message_size
Definition
safecomm.py:51
simcore.safecomm.SafeCommLink.disconnect
disconnect(self, site=None)
Definition
safecomm.py:99
simcore.safecomm.SafeCommLink.sockets
sockets(self)
Definition
safecomm.py:91
simcore.safecomm.SafeCommLink._host
_host
Definition
safecomm.py:49
simcore.safecomm.SafeCommLink._ever_connected
bool _ever_connected
Definition
safecomm.py:55
simcore.safecomm.SafeCommLink.connected_sites
connected_sites(self)
Definition
safecomm.py:96
simcore.safecomm.SafeCommLink.send_all
send_all(self, payload)
Definition
safecomm.py:120
simcore.safecomm.SafeCommLink._port
_port
Definition
safecomm.py:50
simcore.safecomm.link_from_config
link_from_config(log, config, message_size, sim_config)
Definition
safecomm.py:163
src
simcore
safecomm.py
Generated by
1.18.0