diff --git a/eeip/eipclient.py b/eeip/eipclient.py index e027b6f..ce8c217 100644 --- a/eeip/eipclient.py +++ b/eeip/eipclient.py @@ -9,7 +9,17 @@ import time, datetime +class SequenceDetails: + def __init__(self): + self.sequence_count = 0 + self.sequence = 0 + class EEIPClient: + """ + EtherNet/IP Client for Class 1 (implicit) messaging. + By default, UDP packets are sent automatically at the RPI interval (auto-send enabled). + Set self.udp_auto_send_enabled = False to disable auto-send and use send_udp_once() for manual sending. + """ def __init__(self): self.__tcpClient_socket = None self.__session_handle = 0 @@ -47,6 +57,13 @@ def __init__(self): self.__udp_client_receive_closed = False + self.sequence_details = SequenceDetails() + + # Controls whether UDP packets are sent automatically at the RPI interval (default: True) + self.udp_auto_send_enabled = True + + # User-defined callback for received UDP data + self.udp_receive_callback = None def ListIdentity(self): """ @@ -540,9 +557,19 @@ def forward_open(self, large_forward_open = False): self.__udp_recv_thread = threading.Thread(target=self.__udp_listen, args=()) self.__udp_recv_thread.daemon = True self.__udp_recv_thread.start() - self.__udp_send_thread = threading.Thread(target=self.__send_udp, args=()) - self.__udp_send_thread.daemon = True - self.__udp_send_thread.start() + if self.udp_auto_send_enabled: + self.__udp_send_thread = threading.Thread(target=self.__send_udp, args=()) + self.__udp_send_thread.daemon = True + self.__udp_send_thread.start() + + def set_udp_receive_callback(self, callback): + """ + Set a callback function to be called when a UDP message is received. + The callback should accept one argument: the received data as bytes. + """ + self.udp_receive_callback = callback + self.udp_auto_send_enabled = False # Ensure auto-send is disabled when a callback is set + def forward_close(self): @@ -687,6 +714,11 @@ def __udp_listen(self): self.__t_o_iodata = list() for i in range(0, len(__receivedata_udp)-20-header_offset): self.__t_o_iodata.append(__receivedata_udp[20+i+header_offset]) + + # Handle callback if set + if self.udp_receive_callback is not None: + self.udp_receive_callback(self.__t_o_iodata) + self.__lock_receive_data.release() self.__last_received_implicit_message = datetime.datetime.utcnow() #(self.__t_o_iodata) @@ -697,80 +729,93 @@ def __udp_listen(self): self.__receivedata = bytearray() - def __send_udp(self): - sequence_count = 0 - sequence = 0 - while not self.__stoplistening_udp: - message = list() - #-------------------Item Count - message.append(2) + def SendUDPExplicitly(self, out_data = None): + """ + Sends a UDP packet explicitly + """ + if self.__udp_server_socket is not None and not self.__stoplistening_udp: + self.__send_udp_packet(out_data=out_data) + + + def __send_udp(self,out_data = None): + while not self.__stoplistening_udp: + self.__send_udp_packet() + + def __send_udp_packet(self, out_data = None): + message = list() + + #-------------------Item Count + message.append(2) + message.append(0) + # -------------------Item Count + + # -------------------Type ID + message.append(0x02) + message.append(0x80) + # -------------------Type ID + + # -------------------Length + message.append(0x08) + message.append(0x00) + # -------------------Length + + # -------------------Connection ID + message.append(self.__connection_id_o_t & 0xFF) + message.append((self.__connection_id_o_t & 0xFF00) >> 8) + message.append((self.__connection_id_o_t & 0xFF0000) >> 16) + message.append((self.__connection_id_o_t & 0xFF000000) >> 24) + # -------------------Connection ID + + # -------------------sequence count + self.sequence_details.sequence_count += 1 + message.append(self.sequence_details.sequence_count & 0xFF) + message.append((self.sequence_details.sequence_count & 0xFF00) >> 8) + message.append((self.sequence_details.sequence_count & 0xFF0000) >> 16) + message.append((self.sequence_details.sequence_count & 0xFF000000) >> 24) + # -------------------sequence count + + # -------------------Type ID + message.append(0xB1) + message.append(0x00) + # -------------------Type ID + + header_offset = 0 + if self.__o_t_realtime_format == RealTimeFormat.HEADER32BIT: + header_offset = 4 + o_t_length = self.__o_t_length + header_offset + 2 + + # -------------------Length + message.append(o_t_length & 0xFF) + message.append((o_t_length & 0xFF00) >> 8) + # -------------------Length + + # -------------------Sequence count + self.sequence_details.sequence += 1 + if self.__o_t_realtime_format != RealTimeFormat.HEARTBEAT: + message.append(self.sequence_details.sequence & 0xFF) + message.append((self.sequence_details.sequence & 0xFF00) >> 8) + # -------------------Sequence count + + if self.__o_t_realtime_format == RealTimeFormat.HEADER32BIT: + message.append(1) message.append(0) - # -------------------Item Count - - # -------------------Type ID - message.append(0x02) - message.append(0x80) - # -------------------Type ID - - # -------------------Length - message.append(0x08) - message.append(0x00) - # -------------------Length - - # -------------------Connection ID - message.append(self.__connection_id_o_t & 0xFF) - message.append((self.__connection_id_o_t & 0xFF00) >> 8) - message.append((self.__connection_id_o_t & 0xFF0000) >> 16) - message.append((self.__connection_id_o_t & 0xFF000000) >> 24) - # -------------------Connection ID - - # -------------------sequence count - sequence_count += 1 - message.append(sequence_count & 0xFF) - message.append((sequence_count & 0xFF00) >> 8) - message.append((sequence_count & 0xFF0000) >> 16) - message.append((sequence_count & 0xFF000000) >> 24) - # -------------------sequence count - - # -------------------Type ID - message.append(0xB1) - message.append(0x00) - # -------------------Type ID - - header_offset = 0 - if self.__o_t_realtime_format == RealTimeFormat.HEADER32BIT: - header_offset = 4 - o_t_length = self.__o_t_length + header_offset + 2 - - # -------------------Length - message.append(o_t_length & 0xFF) - message.append((o_t_length & 0xFF00) >> 8) - # -------------------Length - - # -------------------Sequence count - sequence += 1 - if self.__o_t_realtime_format != RealTimeFormat.HEARTBEAT: - message.append(sequence & 0xFF) - message.append((sequence & 0xFF00) >> 8) - # -------------------Sequence count - - if self.__o_t_realtime_format == RealTimeFormat.HEADER32BIT: - message.append(1) - message.append(0) - message.append(0) - message.append(0) - # -------------------write data - #self.o_t_iodata[0] = self.o_t_iodata[0] + 1 - for i in range(0, self.__o_t_length): + message.append(0) + message.append(0) + # -------------------write data + #self.o_t_iodata[0] = self.o_t_iodata[0] + 1 + for i in range(0, self.__o_t_length): + if out_data is not None: + message.append(out_data[i]) + else: message.append(self.o_t_iodata[i]) - # -------------------write data + # -------------------write data - sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP + sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # UDP - self.__udp_server_socket.sendto(bytearray(message), (self.__ip_address, self.__target_udp_port)) - time.sleep(float(self.__requested_packet_rate_o_t)/1000000.0) + self.__udp_server_socket.sendto(bytearray(message), (self.__ip_address, self.__target_udp_port)) + time.sleep(float(self.__requested_packet_rate_o_t)/1000000.0) def __listen(self): diff --git a/examples/sample2_implicit_udp_callback.py b/examples/sample2_implicit_udp_callback.py new file mode 100644 index 0000000..2f9290e --- /dev/null +++ b/examples/sample2_implicit_udp_callback.py @@ -0,0 +1,72 @@ +""" +Example: Using EEIPClient with UDP receive callback and auto UDP sending (Sensor Event/Acknowledgment) + +This example demonstrates how to use the EEIPClient class to connect to an EtherNet/IP PLC, set up a callback function for receiving UDP data (implicit messaging), and send an acknowledgment back to the PLC when a sensor event is detected. + +Scenario: +- The PLC is configured to send implicit messages to this client when a sensor triggers (e.g., digital input changes). +- The client receives the UDP message, processes the sensor event, and sends an acknowledgment UDP packet back to the PLC. + +How it works: +- The EEIPClient is configured to connect to the PLC. +- A user-defined callback function is registered using set_udp_receive_callback(). +- When the PLC sends a UDP message (e.g., on sensor event), the callback is invoked with the received bytes. +- The client processes the data and sends an acknowledgment UDP packet back to the PLC. +""" +import time +from eeip.eipclient import EEIPClient + + +class MessageProcessor: + """ + Processes incoming UDP messages from the PLC and sends acknowledgments if a sensor event is detected. + """ + def __init__(self, eeipclient): + self.eeipclient = eeipclient + + def process(self, data): + print(f"[CALLBACK] Received UDP data from PLC: {data.hex()}") + # Example: Check if a sensor bit is set (e.g., first byte, bit 0) + if data and (data[0] & 0x01): + print("Sensor event detected! Sending acknowledgment...") + ack = bytes([0xAC]) + self.eeipclient.send_udp_explicitly(ack) + print("Acknowledgment sent.") + else: + print("No sensor event in this packet.") + + +def main(): + # Replace with your PLC's IP address + target_ip = "192.168.1.10" + + eeipclient = EEIPClient() + processor = MessageProcessor(eeipclient) + + # Set the UDP receive callback to the processor's process method + eeipclient.set_udp_receive_callback(processor.process) + + print(f"Registering session with {target_ip}...") + session_handle = eeipclient.register_session(target_ip) + print(f"Session handle: {session_handle}") + + print("Opening implicit messaging connection (Forward Open)...") + eeipclient.forward_open() + print("Connection established. Waiting for sensor events from PLC...") + + try: + # Run for 30 seconds, receiving UDP data via callback + for i in range(30): + # Optionally print the latest input data + print(f"Input bytes: {list(eeipclient.t_o_iodata[:8])}") + time.sleep(1) + except KeyboardInterrupt: + print("Interrupted by user.") + finally: + print("Closing connection and unregistering session...") + eeipclient.forward_close() + eeipclient.unregister_session() + print("Done.") + +if __name__ == "__main__": + main()