Skip to content

Commit ebb3bd8

Browse files
committed
Add unconfirmed PDU handling to MMS connection
-- - Introduce `UnconfirmedServiceHandler` utility class for dispatching specific unconfirmed MMS services - Extend `MMS_Connection` constructor with new parameter `unconfirmed_cb` to register one or more unconfirmed PDU callbacks. - Update `MMS_Connection.recv()` loop to intercept unconfirmed PDUs and route them to callbacks. - Add new conversion model for Data objects using Python Dicts - Change tpktsock implementation to handle cut-off packets
1 parent 9bb1aa5 commit ebb3bd8

4 files changed

Lines changed: 550 additions & 29 deletions

File tree

src/icspacket/proto/mms/connection.py

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
#
1414
# You should have received a copy of the GNU General Public License
1515
# along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
1617
import logging
1718

18-
from typing_extensions import override
19+
from collections.abc import Iterable
20+
from typing_extensions import Callable, override
1921

2022
from icspacket.core.connection import connection
2123
from icspacket.proto.mms._mms import (
2224
GetNamedVariableListAttributes_Request,
2325
GetNamedVariableListAttributes_Response,
26+
Unconfirmed_PDU,
27+
UnconfirmedService,
2428
)
2529
from icspacket.proto.tpkt import tpktsock
2630
from icspacket.proto.cotp.connection import COTP_Connection
@@ -62,7 +66,6 @@
6266
GetNameList_Request,
6367
GetVariableAccessAttributes_Request,
6468
GetVariableAccessAttributes_Response,
65-
Identifier,
6669
Identify_Request,
6770
Initiate_RequestPDU,
6871
MMSpdu,
@@ -74,7 +77,6 @@
7477
ServiceError,
7578
Status_Request,
7679
StatusResponse,
77-
VariableAccessSpecification,
7880
VMDReset_Request,
7981
VMDStop_Request,
8082
Write_Request,
@@ -98,6 +100,109 @@
98100
logger = logging.getLogger(__name__)
99101

100102

103+
UnconfirmedServiceCallback = Callable[["MMS_Connection", UnconfirmedService], None]
104+
"""
105+
Type alias for callbacks that process unconfirmed MMS service elements.
106+
107+
:param conn: Active MMS connection instance.
108+
:type conn: MMS_Connection
109+
:param service: The unconfirmed MMS service payload.
110+
:type service: UnconfirmedService
111+
112+
.. versionadded:: 0.2.4
113+
"""
114+
115+
UnconfirmedPDUCallback = Callable[["MMS_Connection", Unconfirmed_PDU], None]
116+
"""
117+
Type alias for callbacks that process full unconfirmed MMS PDUs.
118+
119+
:param conn: Active MMS connection instance.
120+
:type conn: MMS_Connection
121+
:param pdu: The unconfirmed MMS PDU as received from the remote peer.
122+
:type pdu: Unconfirmed_PDU
123+
124+
.. versionadded:: 0.2.4
125+
"""
126+
127+
128+
class UnconfirmedServiceHandler:
129+
"""
130+
Utility class for dispatching unconfirmed MMS service elements.
131+
132+
Instances of this handler can be registered to react to specific
133+
unconfirmed MMS services. It acts as a callable object that can be
134+
directly invoked with an :class:`Unconfirmed_PDU`.
135+
136+
.. code-block:: python
137+
:caption: Example
138+
139+
def on_status(conn, service):
140+
print("Received status report:", service)
141+
142+
handler = UnconfirmedServiceHandler(
143+
UnconfirmedService.PRESENT.PR_XXX,
144+
func=on_status
145+
)
146+
147+
# later inside MMS_Connection
148+
handler(conn, unconfirmed_pdu)
149+
150+
:param service:
151+
The :class:`UnconfirmedService.PRESENT` discriminator that this
152+
handler should filter on.
153+
:type service: UnconfirmedService.PRESENT
154+
:param func:
155+
Optional callback invoked when a matching unconfirmed service is received.
156+
:type func: UnconfirmedServiceCallback | None
157+
158+
.. versionadded:: 0.2.4
159+
"""
160+
161+
def __init__(
162+
self,
163+
service: UnconfirmedService.PRESENT,
164+
func: UnconfirmedServiceCallback | None = None,
165+
) -> None:
166+
self.target_service = service
167+
self.func = func
168+
169+
def on_pdu(self, conn: "MMS_Connection", service: UnconfirmedService) -> None:
170+
"""
171+
Dispatch a matching unconfirmed service to the configured callback.
172+
173+
:param conn:
174+
Active MMS connection instance.
175+
:type conn: MMS_Connection
176+
:param service:
177+
The unconfirmed MMS service element matching the target type.
178+
:type service: UnconfirmedService
179+
180+
.. versionadded:: 0.2.4
181+
"""
182+
if self.func:
183+
self.func(conn, service)
184+
185+
def __call__(self, conn: "MMS_Connection", pdu: Unconfirmed_PDU) -> None:
186+
"""
187+
Make this handler instance directly callable with an unconfirmed PDU.
188+
189+
If the PDU contains the configured service type, the internal
190+
:meth:`on_pdu` method is invoked.
191+
192+
:param conn:
193+
Active MMS connection instance.
194+
:type conn: MMS_Connection
195+
:param pdu:
196+
An unconfirmed MMS PDU as received from the peer.
197+
:type pdu: Unconfirmed_PDU
198+
199+
.. versionadded:: 0.2.4
200+
"""
201+
service = pdu.service
202+
if service and service.present == self.target_service:
203+
self.on_pdu(conn, service)
204+
205+
101206
class MMS_Connection(connection):
102207
"""
103208
Implementation of the MMS (Manufacturing Message Specification) connection
@@ -152,6 +257,15 @@ class MMS_Connection(connection):
152257
:type presentation_config: ISO_PresentationSettings | None
153258
:param auth: Optional ACSE :class:`Authenticator` instance for authentication handling.
154259
:type auth: Authenticator | None
260+
:param unconfirmed_cb:
261+
Callback or iterable of callbacks that will be invoked whenever
262+
an unconfirmed PDU is received from the peer. Each callback
263+
must conform to :data:`UnconfirmedPDUCallback`.
264+
:type unconfirmed_cb: UnconfirmedPDUCallback | Iterable[UnconfirmedPDUCallback] | None
265+
266+
.. versionchanged:: 0.2.4
267+
Added the ``unconfirmed_cb`` parameter for registering unconfirmed
268+
PDU callbacks.
155269
"""
156270

157271
def __init__(
@@ -160,6 +274,9 @@ def __init__(
160274
session_config: ISO_SessionSettings | None = None,
161275
presentation_config: ISO_PresentationSettings | None = None,
162276
auth: Authenticator | None = None,
277+
unconfirmed_cb: UnconfirmedPDUCallback
278+
| Iterable[UnconfirmedPDUCallback]
279+
| None = None,
163280
):
164281
# First, initialize the connection class and invalidate this connection
165282
super().__init__()
@@ -183,6 +300,13 @@ def __init__(
183300
)
184301
self._connected = self.presentation.is_connected()
185302
self.__invoke_id = 1
303+
self.__unconfirmed_cb = []
304+
if unconfirmed_cb:
305+
self.__unconfirmed_cb = (
306+
[unconfirmed_cb]
307+
if not isinstance(unconfirmed_cb, Iterable)
308+
else list(unconfirmed_cb)
309+
)
186310

187311
# ---------------------------------------------------------------------- #
188312
# Properties
@@ -251,6 +375,22 @@ def invoke_id(self) -> int:
251375
"""
252376
return self.__invoke_id
253377

378+
@property
379+
def unconfirmed_cb(self) -> list[UnconfirmedPDUCallback]:
380+
"""
381+
List of registered unconfirmed PDU callbacks.
382+
383+
Each callback is invoked in registration order whenever an
384+
:class:`Unconfirmed_PDU` is received.
385+
386+
:return:
387+
List of registered callback callables.
388+
:rtype: list[UnconfirmedPDUCallback]
389+
390+
.. versionadded:: 0.2.4
391+
"""
392+
return self.__unconfirmed_cb
393+
254394
# ---------------------------------------------------------------------- #
255395
# MMS Connection Operations
256396
# ---------------------------------------------------------------------- #
@@ -415,9 +555,15 @@ def recv_mms_data(self) -> MMSpdu:
415555
:rtype: MMSpdu
416556
:raises TypeError: If the received data is not an MMS PDU.
417557
"""
418-
pdu = self.presentation.recv_encoded_data()
419-
if not isinstance(pdu, MMSpdu):
420-
raise TypeError(f"Received invalid MMS data: {type(pdu)}")
558+
pdu = None
559+
while pdu is None:
560+
pdu = self.presentation.recv_encoded_data()
561+
if not isinstance(pdu, MMSpdu):
562+
raise TypeError(f"Received invalid MMS data: {type(pdu)}")
563+
564+
if pdu.present == MMSpdu.PRESENT.PR_unconfirmed_PDU:
565+
self._handle_unconfirmed_pdu(pdu.unconfirmed_PDU)
566+
pdu = None
421567
return pdu
422568

423569
# ---------------------------------------------------------------------------
@@ -719,7 +865,7 @@ def write_variable(
719865
if response is not None:
720866
write_results = response.write
721867
# this automatically returns None on success
722-
return write_results[0].failure.value
868+
return write_results[0].failure
723869

724870
def variable_attributes(
725871
self, *, name: ObjectName | None = None, address: Address | None = None
@@ -1065,9 +1211,29 @@ def service_request(
10651211
if error is not None:
10661212
raise error
10671213

1214+
if response.present != MMSpdu.PRESENT.PR_confirmed_ResponsePDU:
1215+
raise MMSServiceError(
1216+
f"Received unexpected MMS response: {response.present!r}",
1217+
response=response,
1218+
)
1219+
10681220
return response.confirmed_ResponsePDU.service
10691221

10701222
# --- private ugly code
1223+
def _handle_unconfirmed_pdu(self, pdu: Unconfirmed_PDU) -> None:
1224+
"""
1225+
Internal helper that dispatches unconfirmed PDUs to all
1226+
registered callbacks.
1227+
1228+
:param pdu:
1229+
The unconfirmed PDU to process.
1230+
:type pdu: Unconfirmed_PDU
1231+
1232+
.. versionadded:: 0.2.4
1233+
"""
1234+
for handler in self.__unconfirmed_cb:
1235+
handler(self, pdu)
1236+
10711237
def _error_from_service_response(
10721238
self,
10731239
request: ConfirmedServiceRequest,
@@ -1094,7 +1260,8 @@ def _error_from_service_response(
10941260
case _:
10951261
return MMSServiceError(
10961262
f"Failed service request of type: {request.present!r} with "
1097-
+ f"reason: {reason.confirmed_requestPDU}"
1263+
+ f"reason: {reason.confirmed_requestPDU}",
1264+
response=response,
10981265
)
10991266

11001267
if response.present != MMSpdu.PRESENT.PR_confirmed_ResponsePDU:
@@ -1103,25 +1270,29 @@ def _error_from_service_response(
11031270

11041271
return MMSServiceError(
11051272
f"Received invalid MMS response: {response.present!r} - "
1106-
+ f"expected response for {request.present!r}"
1273+
+ f"expected response for {request.present!r}",
1274+
response=response,
11071275
)
11081276

11091277
pdu = response.confirmed_ResponsePDU
11101278
if not pdu:
11111279
return MMSServiceError(
1112-
f"Failed service request of type: {request.present!r} (empty response)"
1280+
f"Failed service request of type: {request.present!r} (empty response)",
1281+
response=response,
11131282
)
11141283

11151284
if pdu.invokeID.value != self.invoke_id:
11161285
return MMSServiceError(
11171286
f"Response invokeID ({pdu.invokeID}) does not match "
1118-
+ f"request invokeID ({self.invoke_id})"
1287+
+ f"request invokeID ({self.invoke_id})",
1288+
response=response,
11191289
)
11201290

11211291
if pdu.service.present != request.present:
11221292
return MMSServiceError(
11231293
f"Response service type ({pdu.service.present!r}) does not match "
1124-
+ f"request service type ({request.present!r})"
1294+
+ f"request service type ({request.present!r})",
1295+
response=response,
11251296
)
11261297

11271298
return None

0 commit comments

Comments
 (0)