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+
1617import logging
1718
18- from typing_extensions import override
19+ from collections .abc import Iterable
20+ from typing_extensions import Callable , override
1921
2022from icspacket .core .connection import connection
2123from icspacket .proto .mms ._mms import (
2224 GetNamedVariableListAttributes_Request ,
2325 GetNamedVariableListAttributes_Response ,
26+ Unconfirmed_PDU ,
27+ UnconfirmedService ,
2428)
2529from icspacket .proto .tpkt import tpktsock
2630from icspacket .proto .cotp .connection import COTP_Connection
6266 GetNameList_Request ,
6367 GetVariableAccessAttributes_Request ,
6468 GetVariableAccessAttributes_Response ,
65- Identifier ,
6669 Identify_Request ,
6770 Initiate_RequestPDU ,
6871 MMSpdu ,
7477 ServiceError ,
7578 Status_Request ,
7679 StatusResponse ,
77- VariableAccessSpecification ,
7880 VMDReset_Request ,
7981 VMDStop_Request ,
8082 Write_Request ,
98100logger = 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+
101206class 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