Please take it easy on me, I only started learning Python last week, and have cobbled this together, so I am fully aware that it will look a pile of crap to the proper programmers on here!
This is a Python3 script running on Ubuntu Server on a Raspberry Pi 3. It is emulates a Modbus TCP slave (server). This code runs in it's own thread, the main thread pulls data from an ASCII TCP stream and puts the required values into variable, which the Modbus thread then maps into a virtual register space, and responds to Modbus requests from the Master (client). The requests come in about 6 times a second, there is only one client, and every request is the same. (This whole bodge job is to get live data into a a proprietary control system that only talks Modbus TCP)
It does work, and seems to work well initially. (I know I am playing with fire by assuming that the complete Modbus request is received on a single recv() call)
When it starts, Python consumes around 3% of one CPU core, but this creeps up and up and up. (I am seeing this via htop, with tree view enabled. This child thread is using the CPU - if the parent shows 10% this thread show 9% etc)
After about 10 hours it's at 90% and things fall apart.
Whilst a total beginner with Python and socket programming, I cannot understand why this happens over time. The loop is doing exactly the same thing after the first minute than it is after 10 hours, so why the increase in CPU usage? Memory usage does not increase.
Anyway, here is the monstrosity. At this point, please don't comment on the rubbish way I'm doing things, just on why the CPU usage is increasing! I'm sure it's a schoolboy error with my handling of the sockets. (As an aside, I am extremely impressed with Python, in that with nothing more than a PDF tutorial downloaded for free, Mr Google and a couple of days, I had something running that does the job, if only for a few hours!
class modbusThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
### Now we define what will actually run as that thread - the Modbus server itself
def run(self):
global modbus_outer_loops
global modbus_inner_loops
global modbus_requests_served
# create 64 kbyte address space for Modbus registers
# each register is 16 bits wide, we are using 32 bit Floats, so each value takes two registers
# registers thus must be addressed 0, 2, 4 etc...
# any registers read that have not had WITS data placed in them will read -777.1
register_address_space = bytearray(struct.pack(">f", -777.1)) * 16384
wanted_record_items = [ [] ] * 100 # create empty list of lists
############################## WHAT DATA ITEMS DO WE WANT? ###############################
wanted_record_items[1] = [40,41]
wanted_record_items[66] = [8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]
host = socket.gethostname() #MODBUS_IP_ADDRESS
print ("MODBUS- Hostname:", host)
port = 502
sinewave_degrees = 0.0
while True: ### main outer loop. Should only run to initially bind to the TCP port.
modbus_outer_loops += 1
hard_restart_required = False
print ("MODBUS- Creating modbus_socket")
modbus_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print ("MODBUS- Binding to",host,port)
try: # try to bind to the TCP port, will fail if network not up...
modbus_socket.bind((host,port))
except:
print ("MODBUS- Cannot bind to the Modbus TCP port! Is the network up?")
time.sleep(RESTART_WAIT)
continue
modbus_socket.listen(0)
while True: ############# Inner loop, deals with connections from client
modbus_inner_loops += 1
print ("MODBUS- Waiting for incoming connection on",host,port,"...")
client_socket,addr = modbus_socket.accept()
client_socket.settimeout(TCP_TIMEOUT)
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops,", Incoming connection from %s" % str(addr))
while True:
sinewave_degrees += 0.25
if sinewave_degrees >= 360:
sinewave_degrees = 0
sinewave_float = math.sin(math.radians(sinewave_degrees)) * 50
sinewave_float += 50
sinewave_int = int(sinewave_float)
# Read the wanted data into the register space
register = 0 # Register 0 gets the test signal (sine wave)
register_address_space[0:3] = bytearray(struct.pack(">f", sinewave_float))
register += 2 # We use TWO 16 bit registers per float!
for record in list(range(100)): # loop through all records ( 0 - 99 )
for item in list(wanted_record_items[record]):
register_address_space[register * 2:(register * 2)+3] = bytearray(struct.pack(">f", record_item[record][item]))
register += 2
try:
request = client_socket.recv(SOCKET_BUFFER)
except:
print ("MODBUS- Connection failed.")
print ("MODBUS- Closing socket and restarting listener.")
break
if not request:
print ("MODBUS- Connection established but no data received. (Connection closed by peer?)")
print ("MODBUS- Hard restarting. Killing server socket and restarting listener from scratch")
hard_restart_required = True
break
start_time = timer()
# Build the Modbus response
# print ("MODBUS- -Request:"," ".join("%02x" % b for b in request))
transactionID0 = request[0]
transactionID1 = request[1]
protocol0 = request[2]
protocol1 = request[3]
unitID = request[6]
function_code = request[7]
start_register_hi_byte = request [8]
start_register_lo_byte = request[9]
number_of_registers_hi_byte = request[10]
number_of_registers_lo_byte = request[11]
function_code = 3 # function code 3 is all you get! take it or leave it!
start_register = start_register_lo_byte + (start_register_hi_byte * 256)
number_of_registers = number_of_registers_lo_byte + (start_register_hi_byte * 256)
byte_count = number_of_registers * 2 # registers are 16 bit
length = byte_count + 3 # data length plus, byte count, func code and unit ID
length_hi_byte = length >> 8
length_lo_byte = length & 255
response_list = [transactionID0, transactionID1, protocol0, protocol1, length_hi_byte, length_lo_byte, unitID, function_code, byte_count] # add all that shit together as a list
response_list += register_address_space[start_register * 2 : (start_register * 2) + (number_of_registers * 2)] # add the data onto the end of the response
response_bytes = bytes(response_list) # turn it into a byte string
# print ("MODBUS- Response:"," ".join("%02x" % b for b in response_bytes))
end_time = timer()
client_socket.send(response_bytes)
modbus_requests_served += 1
print ("MODBUS- ",modbus_outer_loops,", ",modbus_inner_loops," - ",modbus_requests_served," requests served. Response time = ", end_time - start_time, sep="")
time.sleep(0.025)
print ("MODBUS- EXITING INNER LOOP-------------------------------")
client_socket.close()
if hard_restart_required:
hard_restart_required = False
break
time.sleep(MODBUS_RESTART_WAIT)
print ("MODBUS- EXITING OUTER LOOP---------------------")
modbus_socket.close()
The sinewave stuff is just as a test signal.
I have moved the block that moves the data into the register space to before the recv() call to see if that helps. It was done after the recv(), and takes no more than 0.01 seconds to execute.
Many thanks, and I promise I will do a much better job with v2, but why oh why the steady increase in CPU usage?