polymech - fw latest | web ui
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Multi-Client Modbus Test - Tests how many concurrent Modbus clients the ESP32 can handle
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
import statistics
|
||||
from datetime import datetime
|
||||
from multiprocessing import Process, Queue, Value
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from pymodbus.exceptions import ConnectionException
|
||||
|
||||
# --- Configuration ---
|
||||
MODBUS_PORT = 502
|
||||
MB_BATTLE_COUNTER_REG = 20 # Counter register address
|
||||
MB_BATTLE_TIMESTAMP_REG = 21 # Timestamp register address
|
||||
MB_CLIENT_COUNT_REG = 22 # Current number of connected clients
|
||||
MB_CLIENT_MAX_REG = 23 # Maximum number of clients seen
|
||||
MB_CLIENT_TOTAL_REG = 24 # Total client connections since start
|
||||
OUTPUT_DIR = "tmp"
|
||||
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "multi-client-test.json")
|
||||
LOG_LEVEL = logging.INFO
|
||||
|
||||
# --- Setup Logging ---
|
||||
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# --- Argument Parsing ---
|
||||
parser = argparse.ArgumentParser(description='Multi-Client Modbus Test - Test concurrent client connections')
|
||||
parser.add_argument('--ip-address', type=str, default='192.168.1.250',
|
||||
help='IP address of the Modbus TCP server (ESP32), defaults to 192.168.1.250')
|
||||
parser.add_argument('--clients', type=int, default=4,
|
||||
help='Number of concurrent clients to test (default: 4)')
|
||||
parser.add_argument('--operations', type=int, default=100,
|
||||
help='Number of operations per client (default: 100)')
|
||||
parser.add_argument('--delay', type=float, default=0.01,
|
||||
help='Delay in seconds between client operations (default: 0.01)')
|
||||
parser.add_argument('--sequential', action='store_true',
|
||||
help='Run sequential client test')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create output directory
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# --- Client Process Function ---
|
||||
def client_process(client_id, ip_address, operations, delay, results_queue, stop_flag):
|
||||
"""Run a client process that connects and performs operations"""
|
||||
client = None
|
||||
operations_count = 0
|
||||
successful_operations = 0
|
||||
failed_operations = 0
|
||||
connection_attempts = 0
|
||||
successful_connections = 0
|
||||
failed_connections = 0
|
||||
write_times = []
|
||||
read_times = []
|
||||
round_trip_times = []
|
||||
|
||||
try:
|
||||
logging.info(f"Client {client_id}: Starting")
|
||||
|
||||
# Keep trying to connect and perform operations until we reach the total
|
||||
while operations_count < operations and not stop_flag.value:
|
||||
# Connect if not connected
|
||||
if client is None or not client.is_socket_open():
|
||||
connection_attempts += 1
|
||||
try:
|
||||
client = ModbusTcpClient(ip_address, port=MODBUS_PORT)
|
||||
connected = client.connect()
|
||||
if connected:
|
||||
successful_connections += 1
|
||||
logging.info(f"Client {client_id}: Connected")
|
||||
else:
|
||||
failed_connections += 1
|
||||
logging.warning(f"Client {client_id}: Connection failed")
|
||||
time.sleep(1) # Wait a bit before retrying
|
||||
continue
|
||||
except Exception as e:
|
||||
failed_connections += 1
|
||||
logging.error(f"Client {client_id}: Connection error: {e}")
|
||||
time.sleep(1) # Wait a bit before retrying
|
||||
continue
|
||||
|
||||
# Perform a read-write-read operation
|
||||
try:
|
||||
operations_count += 1
|
||||
|
||||
# Read current counter value
|
||||
read_start = time.time()
|
||||
read_response = client.read_holding_registers(address=MB_BATTLE_COUNTER_REG, count=1)
|
||||
read_end = time.time()
|
||||
read_time = (read_end - read_start) * 1000 # Convert to ms
|
||||
|
||||
if read_response.isError():
|
||||
failed_operations += 1
|
||||
logging.error(f"Client {client_id}: Read error: {read_response}")
|
||||
continue
|
||||
|
||||
current_value = read_response.registers[0]
|
||||
|
||||
# Write incremented value
|
||||
write_start = time.time()
|
||||
write_response = client.write_register(address=MB_BATTLE_COUNTER_REG, value=current_value+1)
|
||||
write_end = time.time()
|
||||
write_time = (write_end - write_start) * 1000 # Convert to ms
|
||||
|
||||
if write_response.isError():
|
||||
failed_operations += 1
|
||||
logging.error(f"Client {client_id}: Write error: {write_response}")
|
||||
continue
|
||||
|
||||
# Verify the write
|
||||
verify_start = time.time()
|
||||
verify_response = client.read_holding_registers(address=MB_BATTLE_COUNTER_REG, count=1)
|
||||
verify_end = time.time()
|
||||
verify_time = (verify_end - verify_start) * 1000 # Convert to ms
|
||||
|
||||
round_trip_time = (verify_end - read_start) * 1000 # Full operation time in ms
|
||||
|
||||
if verify_response.isError():
|
||||
failed_operations += 1
|
||||
logging.error(f"Client {client_id}: Verification error: {verify_response}")
|
||||
continue
|
||||
|
||||
new_value = verify_response.registers[0]
|
||||
if new_value != current_value + 1:
|
||||
logging.warning(f"Client {client_id}: Value mismatch: expected {current_value+1}, got {new_value}")
|
||||
|
||||
# Success - record times
|
||||
successful_operations += 1
|
||||
write_times.append(write_time)
|
||||
read_times.append((read_time + verify_time) / 2) # Average of both reads
|
||||
round_trip_times.append(round_trip_time)
|
||||
|
||||
# Progress reporting
|
||||
if operations_count % 10 == 0:
|
||||
logging.info(f"Client {client_id}: Completed {operations_count}/{operations} operations")
|
||||
|
||||
# Add delay between operations
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
except ConnectionException as ce:
|
||||
failed_operations += 1
|
||||
logging.error(f"Client {client_id}: Connection error during operation: {ce}")
|
||||
client = None # Will try to reconnect
|
||||
except Exception as e:
|
||||
failed_operations += 1
|
||||
logging.error(f"Client {client_id}: Operation error: {e}")
|
||||
# Continue with next operation
|
||||
|
||||
# Clean up
|
||||
if client and client.is_socket_open():
|
||||
client.close()
|
||||
|
||||
# Report results
|
||||
results = {
|
||||
"client_id": client_id,
|
||||
"operations": {
|
||||
"total": operations_count,
|
||||
"successful": successful_operations,
|
||||
"failed": failed_operations
|
||||
},
|
||||
"connections": {
|
||||
"attempts": connection_attempts,
|
||||
"successful": successful_connections,
|
||||
"failed": failed_connections
|
||||
},
|
||||
"times_ms": {
|
||||
"write": {
|
||||
"min": min(write_times) if write_times else None,
|
||||
"max": max(write_times) if write_times else None,
|
||||
"mean": statistics.mean(write_times) if write_times else None,
|
||||
"median": statistics.median(write_times) if write_times else None
|
||||
},
|
||||
"read": {
|
||||
"min": min(read_times) if read_times else None,
|
||||
"max": max(read_times) if read_times else None,
|
||||
"mean": statistics.mean(read_times) if read_times else None,
|
||||
"median": statistics.median(read_times) if read_times else None
|
||||
},
|
||||
"round_trip": {
|
||||
"min": min(round_trip_times) if round_trip_times else None,
|
||||
"max": max(round_trip_times) if round_trip_times else None,
|
||||
"mean": statistics.mean(round_trip_times) if round_trip_times else None,
|
||||
"median": statistics.median(round_trip_times) if round_trip_times else None
|
||||
}
|
||||
},
|
||||
"all_times": {
|
||||
"write": write_times,
|
||||
"read": read_times,
|
||||
"round_trip": round_trip_times
|
||||
}
|
||||
}
|
||||
results_queue.put(results)
|
||||
logging.info(f"Client {client_id}: Finished - {successful_operations}/{operations_count} successful operations")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {client_id}: Unexpected error: {e}")
|
||||
results_queue.put({
|
||||
"client_id": client_id,
|
||||
"error": str(e),
|
||||
"operations": {
|
||||
"total": operations_count,
|
||||
"successful": successful_operations,
|
||||
"failed": failed_operations
|
||||
}
|
||||
})
|
||||
finally:
|
||||
if client and client.is_socket_open():
|
||||
client.close()
|
||||
|
||||
def main():
|
||||
# Reset the counter before starting the test
|
||||
try:
|
||||
reset_client = ModbusTcpClient(args.ip_address, port=MODBUS_PORT)
|
||||
if reset_client.connect():
|
||||
reset_client.write_register(address=MB_BATTLE_COUNTER_REG, value=0)
|
||||
reset_client.write_register(address=MB_CLIENT_COUNT_REG, value=0)
|
||||
reset_client.write_register(address=MB_CLIENT_MAX_REG, value=0)
|
||||
reset_client.write_register(address=MB_CLIENT_TOTAL_REG, value=0)
|
||||
reset_client.close()
|
||||
logging.info("Reset counter and client statistics")
|
||||
else:
|
||||
logging.error("Failed to connect for reset")
|
||||
except Exception as e:
|
||||
logging.error(f"Error during reset: {e}")
|
||||
|
||||
# Add a sequential client test instead of parallel processing
|
||||
if args.clients > 0 and args.operations == 1:
|
||||
run_sequential_test(args.ip_address, args.clients, args.delay)
|
||||
return
|
||||
|
||||
# Create shared objects for normal parallel test
|
||||
results_queue = Queue()
|
||||
stop_flag = Value('i', 0)
|
||||
|
||||
# Start client processes
|
||||
processes = []
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
logging.info(f"Starting {args.clients} client processes...")
|
||||
for i in range(args.clients):
|
||||
p = Process(target=client_process,
|
||||
args=(i+1, args.ip_address, args.operations, args.delay, results_queue, stop_flag))
|
||||
processes.append(p)
|
||||
p.start()
|
||||
# Small delay between starting clients to avoid connection race
|
||||
time.sleep(0.1)
|
||||
|
||||
# Wait for all processes to complete
|
||||
for p in processes:
|
||||
p.join()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Test interrupted by user")
|
||||
stop_flag.value = 1
|
||||
for p in processes:
|
||||
if p.is_alive():
|
||||
p.join(timeout=1)
|
||||
if p.is_alive():
|
||||
p.terminate()
|
||||
|
||||
# Calculate total test time
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Collect results
|
||||
results = []
|
||||
while not results_queue.empty():
|
||||
results.append(results_queue.get())
|
||||
|
||||
# Get final client statistics
|
||||
client_stats = {
|
||||
"count": 0,
|
||||
"max": 0,
|
||||
"total": 0
|
||||
}
|
||||
|
||||
try:
|
||||
stats_client = ModbusTcpClient(args.ip_address, port=MODBUS_PORT)
|
||||
if stats_client.connect():
|
||||
response = stats_client.read_holding_registers(address=MB_CLIENT_COUNT_REG, count=3)
|
||||
if not response.isError():
|
||||
client_stats["count"] = response.registers[0]
|
||||
client_stats["max"] = response.registers[1]
|
||||
client_stats["total"] = response.registers[2]
|
||||
stats_client.close()
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting client stats: {e}")
|
||||
|
||||
# Aggregate statistics
|
||||
total_operations = sum(r["operations"]["total"] for r in results)
|
||||
successful_operations = sum(r["operations"]["successful"] for r in results if "operations" in r and "successful" in r["operations"])
|
||||
failed_operations = sum(r["operations"]["failed"] for r in results if "operations" in r and "failed" in r["operations"])
|
||||
|
||||
# Calculate operations per second
|
||||
ops_per_second = successful_operations / total_time if total_time > 0 else 0
|
||||
|
||||
# Prepare summary
|
||||
summary = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"test_parameters": {
|
||||
"ip_address": args.ip_address,
|
||||
"clients": args.clients,
|
||||
"operations_per_client": args.operations,
|
||||
"delay": args.delay
|
||||
},
|
||||
"results": {
|
||||
"total_time_seconds": total_time,
|
||||
"operations": {
|
||||
"total": total_operations,
|
||||
"successful": successful_operations,
|
||||
"failed": failed_operations,
|
||||
"success_rate": (successful_operations / total_operations * 100) if total_operations > 0 else 0
|
||||
},
|
||||
"performance": {
|
||||
"operations_per_second": ops_per_second,
|
||||
"operations_per_second_per_client": ops_per_second / args.clients if args.clients > 0 else 0
|
||||
},
|
||||
"client_stats": client_stats
|
||||
},
|
||||
"client_details": results
|
||||
}
|
||||
|
||||
# Save results to file
|
||||
with open(OUTPUT_FILE, 'w') as f:
|
||||
json.dump(summary, f, indent=2)
|
||||
|
||||
# Print summary
|
||||
print("\n--- Multi-Client Modbus Test Results ---")
|
||||
print(f"Clients: {args.clients}")
|
||||
print(f"Operations per client: {args.operations}")
|
||||
print(f"Total test time: {total_time:.2f} seconds")
|
||||
print(f"Total operations: {total_operations}")
|
||||
print(f"Successful operations: {successful_operations}")
|
||||
print(f"Failed operations: {failed_operations}")
|
||||
print(f"Success rate: {summary['results']['operations']['success_rate']:.2f}%")
|
||||
print(f"Operations per second (total): {ops_per_second:.2f}")
|
||||
print(f"Operations per second (per client): {summary['results']['performance']['operations_per_second_per_client']:.2f}")
|
||||
print(f"Server client stats - current: {client_stats['count']}, max: {client_stats['max']}, total: {client_stats['total']}")
|
||||
print("--------------------------------------\n")
|
||||
print(f"Full results saved to {OUTPUT_FILE}")
|
||||
|
||||
def run_sequential_test(ip_address, max_clients, delay_between_clients):
|
||||
"""Test how many clients the server can handle by connecting them one by one"""
|
||||
logging.info(f"Starting sequential client test with up to {max_clients} clients...")
|
||||
|
||||
# Array to hold all clients
|
||||
clients = []
|
||||
clients_connected = 0
|
||||
max_connected = 0
|
||||
client_data = {}
|
||||
|
||||
try:
|
||||
# Try to connect clients one by one
|
||||
for i in range(1, max_clients + 1):
|
||||
logging.info(f"Connecting client {i}...")
|
||||
|
||||
try:
|
||||
# Create and connect a new client
|
||||
client = ModbusTcpClient(ip_address, port=MODBUS_PORT)
|
||||
connected = client.connect()
|
||||
|
||||
if connected:
|
||||
clients.append(client)
|
||||
clients_connected += 1
|
||||
max_connected = max(max_connected, clients_connected)
|
||||
|
||||
# Read counter to confirm connection is working
|
||||
response = client.read_holding_registers(address=MB_BATTLE_COUNTER_REG, count=1)
|
||||
if not response.isError():
|
||||
value = response.registers[0]
|
||||
logging.info(f"Client {i} connected successfully. Read value: {value}")
|
||||
else:
|
||||
logging.warning(f"Client {i} connected but read failed: {response}")
|
||||
|
||||
# Wait between connections
|
||||
time.sleep(delay_between_clients)
|
||||
else:
|
||||
logging.error(f"Client {i} failed to connect")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error connecting client {i}: {e}")
|
||||
break
|
||||
|
||||
# Get stats
|
||||
if clients_connected > 0:
|
||||
# Try to read with the first client
|
||||
try:
|
||||
response = clients[0].read_holding_registers(address=MB_CLIENT_COUNT_REG, count=3)
|
||||
if not response.isError():
|
||||
client_data["count"] = response.registers[0]
|
||||
client_data["max"] = response.registers[1]
|
||||
client_data["total"] = response.registers[2]
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading client stats: {e}")
|
||||
|
||||
# Hold connections for a moment
|
||||
logging.info(f"Successfully connected {clients_connected} clients. Waiting 5 seconds...")
|
||||
time.sleep(5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Test interrupted by user")
|
||||
finally:
|
||||
# Close all clients
|
||||
for i, client in enumerate(clients):
|
||||
if client.is_socket_open():
|
||||
try:
|
||||
client.close()
|
||||
logging.info(f"Disconnected client {i+1}")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Report results
|
||||
print("\n--- Sequential Client Test Results ---")
|
||||
print(f"Maximum clients attempted: {max_clients}")
|
||||
print(f"Clients successfully connected: {clients_connected}")
|
||||
print(f"Success rate: {(clients_connected / max_clients * 100) if max_clients > 0 else 0:.2f}%")
|
||||
|
||||
if client_data:
|
||||
print(f"Server reported - clients: {client_data.get('count', 'N/A')}, max: {client_data.get('max', 'N/A')}, total: {client_data.get('total', 'N/A')}")
|
||||
|
||||
print("--------------------------------------\n")
|
||||
|
||||
# Save results
|
||||
results = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"test_type": "sequential",
|
||||
"parameters": {
|
||||
"ip_address": ip_address,
|
||||
"max_clients": max_clients,
|
||||
"delay": delay_between_clients
|
||||
},
|
||||
"results": {
|
||||
"clients_connected": clients_connected,
|
||||
"success_rate": (clients_connected / max_clients * 100) if max_clients > 0 else 0,
|
||||
"client_stats": client_data
|
||||
}
|
||||
}
|
||||
|
||||
with open(OUTPUT_FILE, 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
logging.info(f"Results saved to {OUTPUT_FILE}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Override for sequential test
|
||||
if args.sequential:
|
||||
args.operations = 1
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user