diff --git a/.github/workflows/scripts/adj-tester/main.py b/.github/workflows/scripts/adj-tester/main.py index e561a99..6971900 100755 --- a/.github/workflows/scripts/adj-tester/main.py +++ b/.github/workflows/scripts/adj-tester/main.py @@ -1,4 +1,12 @@ -"""Entrypoint script for validating project JSON files against schemas. +""" + +ADJ Validator + +Version: 1.1.0 + +JavierRibaldelRio + +Entrypoint script for validating project JSON files against schemas. This script coordinates several checks over files in the test/ directory using JSON Schema validation and project-specific rules @@ -15,6 +23,7 @@ is_valid_ipv4, print_results, logError, + info_message, validate_with_schema, ) @@ -268,7 +277,7 @@ def check_board_json(path: str): except RuntimeError as e: error_list.append(logError(path, "", str(e))) is_valid = False - print_results(path, is_valid, error_list) + print_results(path, is_valid, error_list, type="(Board)") if is_valid: return board @@ -368,11 +377,11 @@ def check_measurement_json(path: str, previous_ids=None): except RuntimeError as e: error_list.append(logError(path, "", str(e))) is_valid = False - print_results("\t" + path, is_valid, error_list) + print_results(path, is_valid, error_list, type="(Measurements)", prefix="\t") return is_valid -def check_packet_json(path: str, measurement_ids=None): +def check_packet_json(path: str, sockets: set, measurement_ids=None): """Validate a packet JSON file. Ensures schema conformance, global uniqueness of packet IDs @@ -421,10 +430,127 @@ def check_packet_json(path: str, measurement_ids=None): ) is_valid = False + # Ensure that socket is defined in the sockets + socket_name = pkt.get("socket", "") + if socket_name != "" and socket_name not in sockets: + error_list.append( + logError( + path, + f"id {pkt_id}", + f"socket '{socket_name}' in id {pkt_id} is not defined in sockets", + ) + ) + is_valid = False + + except RuntimeError as e: + error_list.append(logError(path, "", str(e))) + is_valid = False + print_results(path, is_valid, error_list, type="(Packets/Orders)", prefix="\t") + return is_valid + + +def check_socket_json(path: str, sockets_name: set): + + error_list = [] + is_valid = True + try: + # Open json and Schema + sockets = open_json(path) + schema = open_schema("socket.schema.json") + + # Schema validation + is_valid, schema_errors = validate_with_schema(sockets, schema, path) + if not is_valid: + error_list.extend(schema_errors) + + for socket in sockets: + # ensure socket name is unique across all sockets + socket_name = socket["name"] + if socket_name in sockets_name: + error_list.append( + logError( + path, + "name", + f"Duplicate socket name '{socket_name}' across sockets", + ) + ) + is_valid = False + else: + sockets_name.add(socket_name) + + # check that remote_ip is valid (remote_ip is optional) and it is defined only for type=="DatagramSocket" || type=="Socket" + if ( + socket["type"] not in ["DatagramSocket", "Socket"] + and "remote_ip" in socket + ): + error_list.append( + logError( + path, + f"name {socket_name}", + f"remote_ip is only allowed for type 'DatagramSocket' or 'Socket', but socket '{socket_name}' has type '{socket['type']}'", + ) + ) + is_valid = False + + else: + socket_ip = socket.get("remote_ip", "") + if socket_ip != "" and not is_valid_ipv4(socket_ip): + error_list.append( + logError( + path, + f"name {socket_name}", + f"Invalid IPv4 address: {socket_ip}", + ) + ) + is_valid = False + + # check that port is only definied if type=="ServerSocket" || type=="DatagramSocket" + + if socket.get("port", None) is not None and socket["type"] not in [ + "ServerSocket", + "DatagramSocket", + ]: + error_list.append( + logError( + path, + f"name {socket_name}", + f"Port is only allowed for type 'ServerSocket' or 'DatagramSocket', but socket '{socket_name}' has type '{socket['type']}'", + ) + ) + is_valid = False + + # check that local_port is only used for type=="Socket" + if ( + socket.get("local_port", None) is not None + and socket["type"] != "Socket" + ): + error_list.append( + logError( + path, + f"name {socket_name}", + f"local_port is only allowed for type 'Socket', but socket '{socket_name}' has type '{socket['type']}'", + ) + ) + is_valid = False + + # checck thath remote_port is only used for type=="Socket" + if ( + socket.get("remote_port", None) is not None + and socket["type"] != "Socket" + ): + error_list.append( + logError( + path, + f"name {socket_name}", + f"remote_port is only allowed for type 'Socket', but socket '{socket_name}' has type '{socket['type']}'", + ) + ) + is_valid = False + except RuntimeError as e: error_list.append(logError(path, "", str(e))) is_valid = False - print_results("\t" + path, is_valid, error_list) + print_results(path, is_valid, error_list, type="(Socket)", prefix="\t") return is_valid @@ -466,6 +592,18 @@ def main(): # measurements are unique within a board measurement_ids = set() + # sockets are unique + sockets_name = set() + + # Validate socket JSON files + for socket_path in board.get("sockets", []): + valid = ( + check_socket_json( + f"boards/{board_name}/{socket_path}", sockets_name + ) + and valid + ) + # Validate measurements JSON files for measurement_path in board.get("measurements", []): valid = ( @@ -481,6 +619,7 @@ def main(): valid = ( check_packet_json( f"boards/{board_name}/{packets_path}", + sockets_name, measurement_ids, ) and valid diff --git a/.github/workflows/scripts/adj-tester/schema/packet.schema.json b/.github/workflows/scripts/adj-tester/schema/packet.schema.json index 7abd0ab..83786e2 100644 --- a/.github/workflows/scripts/adj-tester/schema/packet.schema.json +++ b/.github/workflows/scripts/adj-tester/schema/packet.schema.json @@ -36,6 +36,17 @@ "minLength": 1 }, "description": "Measurement IDs included in this packet" + }, + "period_ms": { + "type": [ + "integer", + "number" + ], + "description": "Packet transmission period in milliseconds" + }, + "socket": { + "type": "string", + "description": "Name of the socket used for this packet" } } } diff --git a/.github/workflows/scripts/adj-tester/schema/socket.schema.json b/.github/workflows/scripts/adj-tester/schema/socket.schema.json new file mode 100644 index 0000000..2648836 --- /dev/null +++ b/.github/workflows/scripts/adj-tester/schema/socket.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Socket configuration schema", + "description": "Array of socket definitions for network communication", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "ServerSocket", + "DatagramSocket", + "Socket" + ], + "description": "Socket type" + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Socket name" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Port number for ServerSocket or DatagramSocket" + }, + "local_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Local port number for Socket" + }, + "remote_ip": { + "type": "string", + "format": "ipv4", + "description": "Remote IPv4 address" + }, + "remote_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "Remote port number for Socket" + } + } + } +} \ No newline at end of file diff --git a/.github/workflows/scripts/adj-tester/utils.py b/.github/workflows/scripts/adj-tester/utils.py index f8b9afa..eb01ab5 100644 --- a/.github/workflows/scripts/adj-tester/utils.py +++ b/.github/workflows/scripts/adj-tester/utils.py @@ -27,7 +27,7 @@ def print_header(header: str): print("=" * header_len) -def print_status(message: str, validated: bool = False): +def format_status(message: str, validated: bool = False): """Print a single-line status message. The `validated` flag controls whether the status is shown as @@ -45,7 +45,13 @@ def print_status(message: str, validated: bool = False): elif validated: result = "\033[32mPassed\033[0m" # green - print(f"{message} {result}") + return f"{message} {result}" + + +def info_message(message: str): + """format to blue color and return the message string.""" + + return f"\033[34m{message}\033[0m" def log_message(message: str, status: int = 0): @@ -106,7 +112,7 @@ def is_valid_ipv4(address: str) -> bool: return False -def print_results(file, is_valid, error_list): +def print_results(file, is_valid, error_list, type=None, prefix=""): """Print validation results for a file. The function prints an overall status line and then any detailed @@ -118,7 +124,13 @@ def print_results(file, is_valid, error_list): error_list: Iterable of error message strings. """ - print_status(file, is_valid) + out = format_status(file, is_valid) + + if type != None: + out = f"{out} {info_message(type)}" + + print(prefix + out) + for error in error_list: # Indent errors for readability in CLI output print("\t|\t" + error) diff --git a/README.md b/README.md index f289f82..4531829 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ adj/ ├── {board_name}.json # Main board configuration ├── {board_name}_measurements.json # Board measurements ├── packets.json # Data packets - └── orders.json # Order packets + ├── orders.json # Order packets + └── sockets.json # Board sockets ``` ## Data Types @@ -84,14 +85,15 @@ Mapping of board names to their configuration file paths. ### Board Configuration (`{board_name}.json`) -Main board configuration including ID, IP address, and references to measurements and packets files. +Main board configuration including ID, IP address, and references to measurements, packets and sockets files. ```json { "board_id": "number", "board_ip": "string", "measurements": ["string"], - "packets": ["string"] + "packets": ["string"], + "sockets": ["string"] } ``` @@ -107,7 +109,8 @@ Main board configuration including ID, IP address, and references to measurement "board_id": 1001, "board_ip": "192.168.1.10", "measurements": ["brake_board_measurements.json"], - "packets": ["packets.json", "orders.json"] + "packets": ["packets.json", "orders.json"], + "sockets":["sockets.json"] } ``` @@ -173,7 +176,10 @@ Array of packet definitions for network communication. Packets are separated by "id": "number?", "type": "string", "name": "string", - "variables": ["string"] + "variables": ["string"], + "period_type": "us/ms?", + "period": "number?", + "socket": "string?" } ] ``` @@ -183,6 +189,10 @@ Array of packet definitions for network communication. Packets are separated by - `type`: Packet type string (e.g., "data", "order", "status") - `name`: Human-readable packet name - `variables`: Array of variable names/measurement IDs included in this packet +- `period_type`: Optional string specifying the type of measurement for period, either microseconds +or milliseconds +- `period`: Optional number specifying the transmission period in milliseconds or microseconds for periodic packets. Can be an integer or floating-point value +- `socket`: Optinal string that should match the name of the socket you want to transmit this packet trhough **Example:** ```json @@ -191,7 +201,10 @@ Array of packet definitions for network communication. Packets are separated by "id": 2001, "type": "data", "name": "Brake Telemetry", - "variables": ["brake_pressure", "brake_status", "brake_temperature"] + "variables": ["brake_pressure", "brake_status", "brake_temperature"], + "period_type":"ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "order", @@ -201,6 +214,56 @@ Array of packet definitions for network communication. Packets are separated by ] ``` +### Sockets (`sockets.json`) + +Array of socket definitions for network communication. + +```json +{ + "type": "string", + "name": "string", + "port": "number?", + "local_port":"number?", + "remote_ip": "string?", + "remote_port":"number?" + +} +``` + +**Field Descriptions:** +- `type`: Socket type string(e.g., "ServerSocket","DatagramSocket","Socket") +- `name`: Socket name +- `port`: Optional number that describes the port number used for a ServerSocket or a DatagramSocket +- `local_port`: Optional number that describes the local port number used for a Socket +- `remote_ip`: Optional string that describes the remote ip you want to connect to in a DatagramSocket or Socket +- `remote_port`: Optional number that describes the remote port number used for a Socket + +**Example:** +```json +[ + { + "type": "ServerSocket", + "name": "control_station_tcp", + "port": 50500 + }, + + { + "type": "DatagramSocket", + "name": "control_station_udp", + "remote_ip":"192.168.0.9", + "port": 50400 + }, + + { + "type": "Socket", + "name": "pcu_tcp", + "local_port": 50501, + "remote_ip": "192.168.1.5", + "remote_port": 50500 + } +] +``` + ## Configuration Rules 1. **Naming Convention**: Board directories and main configuration files must use the same name as the board key in `boards.json` @@ -213,6 +276,8 @@ Array of packet definitions for network communication. Packets are separated by 5. **ID Uniqueness**: Board IDs must be unique across all boards. Packet IDs should be unique within their type category. +6. **Socket Naming**: The sockets referenced inside the packet definitions must use exact names as defined in `socket.json`. + ## Validation Notes - All numeric fields use standard JSON number format diff --git a/boards/VCU/VCU.json b/boards/VCU/VCU.json index 86c75b8..58b8546 100644 --- a/boards/VCU/VCU.json +++ b/boards/VCU/VCU.json @@ -7,5 +7,9 @@ "packets": [ "orders.json", "packets.json" + ], + "sockets":[ + "sockets.json" ] + } \ No newline at end of file diff --git a/boards/VCU/packets.json b/boards/VCU/packets.json index f98bc4f..13f9476 100644 --- a/boards/VCU/packets.json +++ b/boards/VCU/packets.json @@ -6,7 +6,10 @@ "general_state", "operational_state" ], - "id": 249 + "id": 249, + "period_type": "ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -18,7 +21,10 @@ "reed4", "all_reeds" ], - "id": 251 + "id": 251, + "period_type": "us", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -27,7 +33,10 @@ "flow1", "flow2" ], - "id": 250 + "id": 250, + "period_type": "ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -36,7 +45,10 @@ "regulator_1_pressure", "regulator_2_pressure" ], - "id": 252 + "id": 252, + "period_type": "us", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -47,7 +59,10 @@ "pressure_brakes", "pressure_capsule" ], - "id": 253 + "id": 253, + "period_type": "ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -56,7 +71,10 @@ "tape_enable_output", "emergency_tape" ], - "id": 254 + "id": 254, + "period_type": "ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -64,7 +82,10 @@ "variables": [ "SDC" ], - "id": 255 + "id": 255, + "period_type": "ms", + "period": 16.67, + "socket": "control_station_udp" }, { "type": "data", @@ -72,6 +93,9 @@ "variables": [ "state" ], - "id": 65 + "id": 65, + "period_type": "ms", + "period": 16.67, + "socket": "pcu_udp" } ] \ No newline at end of file diff --git a/boards/VCU/sockets.json b/boards/VCU/sockets.json new file mode 100644 index 0000000..c9ae043 --- /dev/null +++ b/boards/VCU/sockets.json @@ -0,0 +1,55 @@ +[ + { + "type": "ServerSocket", + "name": "control_station_tcp", + "port": 50500 + }, + + { + "type": "DatagramSocket", + "name": "control_station_udp", + "remote_ip":"192.168.0.9", + "port": 50400 + }, + + { + "type": "Socket", + "name": "pcu_tcp", + "local_port": 50501, + "remote_ip": "192.168.1.5", + "remote_port": 50500 + + + }, + + { + "type": "DatagramSocket", + "name": "pcu_udp", + "remote_ip":"192.168.1.5", + "port": 50402 + }, + + + { + "type": "Socket", + "name": "hvscu_tcp", + "local_port": 50502, + "remote_ip": "192.168.1.7", + "remote_port": 50500 + + + }, + + { + "type": "DatagramSocket", + "name": "hvscu_udp", + "remote_ip":"192.168.1.7", + "port": 50403 + } + + + + + + +]