@@ -163,18 +163,74 @@ def db_read_multi(self, items: list[tuple[int, int, int]]) -> list[bytes]:
163163 parsed = _parse_read_response (response )
164164 return [r if r is not None else b"" for r in parsed ]
165165
166- def explore (self ) -> bytes :
166+ def read_area (self , area_rid : int , start : int , size : int ) -> bytes :
167+ """Read raw bytes from a controller memory area (M, I, Q, counters, timers).
168+
169+ Args:
170+ area_rid: Native object RID for the area, e.g.
171+ ``Ids.NATIVE_THE_M_AREA_RID`` (82) for Merker.
172+ start: Start byte offset.
173+ size: Number of bytes to read.
174+
175+ Returns:
176+ Raw bytes read from the area.
177+ """
178+ if self ._connection is None :
179+ raise RuntimeError ("Not connected" )
180+
181+ payload = _build_area_read_payload (area_rid , start , size )
182+ response = self ._connection .send_request (FunctionCode .GET_MULTI_VARIABLES , payload )
183+ results = _parse_read_response (response )
184+ if not results or results [0 ] is None :
185+ raise RuntimeError ("Area read failed" )
186+ return results [0 ]
187+
188+ def write_area (self , area_rid : int , start : int , data : bytes ) -> None :
189+ """Write raw bytes to a controller memory area (M, I, Q, counters, timers).
190+
191+ Args:
192+ area_rid: Native object RID for the area.
193+ start: Start byte offset.
194+ data: Bytes to write.
195+ """
196+ if self ._connection is None :
197+ raise RuntimeError ("Not connected" )
198+
199+ payload = _build_area_write_payload (area_rid , start , data )
200+ response = self ._connection .send_request (FunctionCode .SET_MULTI_VARIABLES , payload )
201+ _parse_write_response (response )
202+
203+ def explore (self , explore_id : int = 0 ) -> bytes :
167204 """Browse the PLC object tree.
168205
206+ Args:
207+ explore_id: Object to explore (0 = root).
208+
169209 Returns:
170- Raw response payload
210+ Raw response payload.
171211 """
172212 if self ._connection is None :
173213 raise RuntimeError ("Not connected" )
174214
175- response = self ._connection .send_request (FunctionCode .EXPLORE , b"" )
215+ payload = _build_explore_payload (explore_id )
216+ response = self ._connection .send_request (FunctionCode .EXPLORE , payload )
176217 return response
177218
219+ def set_plc_operating_state (self , state : int ) -> None :
220+ """Set the PLC operating state (start/stop).
221+
222+ Uses INVOKE to call the PLC's operating-state setter.
223+
224+ Args:
225+ state: Target operating state.
226+ 1 = STOP, 2 = RUN, 3 = HOT_RESTART.
227+ """
228+ if self ._connection is None :
229+ raise RuntimeError ("Not connected" )
230+
231+ payload = _build_invoke_payload (state )
232+ self ._connection .send_request (FunctionCode .INVOKE , payload )
233+
178234 def __enter__ (self ) -> "S7CommPlusClient" :
179235 return self
180236
@@ -331,3 +387,72 @@ def _parse_write_response(response: bytes) -> None:
331387 if errors :
332388 err_str = ", " .join (f"item { nr } : error { val } " for nr , val in errors )
333389 raise RuntimeError (f"Write failed: { err_str } " )
390+
391+
392+ def _build_area_read_payload (area_rid : int , start : int , size : int ) -> bytes :
393+ """Build a GetMultiVariables payload for controller memory area access.
394+
395+ Unlike DB access, controller areas (M, I, Q, counters, timers) use a
396+ native RID and the CONTROLLER_AREA_VALUE_ACTUAL sub-area.
397+ """
398+ addr_bytes , field_count = encode_item_address (
399+ access_area = area_rid ,
400+ access_sub_area = Ids .CONTROLLER_AREA_VALUE_ACTUAL ,
401+ lids = [start + 1 , size ],
402+ )
403+
404+ payload = bytearray ()
405+ payload += struct .pack (">I" , 0 )
406+ payload += encode_uint32_vlq (1 )
407+ payload += encode_uint32_vlq (field_count )
408+ payload += addr_bytes
409+ payload += encode_object_qualifier ()
410+ payload += struct .pack (">I" , 0 )
411+ return bytes (payload )
412+
413+
414+ def _build_area_write_payload (area_rid : int , start : int , data : bytes ) -> bytes :
415+ """Build a SetMultiVariables payload for controller memory area access."""
416+ addr_bytes , field_count = encode_item_address (
417+ access_area = area_rid ,
418+ access_sub_area = Ids .CONTROLLER_AREA_VALUE_ACTUAL ,
419+ lids = [start + 1 , len (data )],
420+ )
421+
422+ payload = bytearray ()
423+ payload += struct .pack (">I" , 0 )
424+ payload += encode_uint32_vlq (1 )
425+ payload += encode_uint32_vlq (field_count )
426+ payload += addr_bytes
427+ payload += encode_uint32_vlq (1 ) # item number 1
428+ payload += encode_pvalue_blob (data )
429+ payload += bytes ([0x00 ])
430+ payload += encode_object_qualifier ()
431+ payload += struct .pack (">I" , 0 )
432+ return bytes (payload )
433+
434+
435+ def _build_explore_payload (explore_id : int = 0 ) -> bytes :
436+ """Build an EXPLORE request payload.
437+
438+ Args:
439+ explore_id: Object to explore (0 = root, other values
440+ explore a specific object by RID).
441+ """
442+ if explore_id == 0 :
443+ return b""
444+ payload = bytearray ()
445+ payload += encode_uint32_vlq (explore_id )
446+ return bytes (payload )
447+
448+
449+ def _build_invoke_payload (state : int ) -> bytes :
450+ """Build an INVOKE request payload for SetPlcOperatingState.
451+
452+ The INVOKE function triggers a method on a PLC object.
453+ For operating state changes, this calls the CPU's state setter.
454+ """
455+ payload = bytearray ()
456+ payload += struct .pack (">I" , 0 ) # reserved
457+ payload += encode_uint32_vlq (state )
458+ return bytes (payload )
0 commit comments