Skip to content

"Add UDP receive callback feature and example for implicit messaging event processing #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 117 additions & 72 deletions eeip/eipclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
72 changes: 72 additions & 0 deletions examples/sample2_implicit_udp_callback.py
Original file line number Diff line number Diff line change
@@ -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()