From c0d7cbc91a99211d86a34e2ab2b4901c54ca1e44 Mon Sep 17 00:00:00 2001 From: Miguel Date: Thu, 17 Jul 2025 16:23:18 +0200 Subject: [PATCH] =?UTF-8?q?Se=20ampli=C3=B3=20el=20soporte=20para=20tipos?= =?UTF-8?q?=20de=20datos=20y=20=C3=A1reas=20de=20memoria=20en=20la=20aplic?= =?UTF-8?q?aci=C3=B3n,=20incluyendo=20MW=20(Memory=20Words),=20PEW=20(Proc?= =?UTF-8?q?ess=20Input=20Words),=20PAW=20(Process=20Output=20Words)=20y=20?= =?UTF-8?q?direccionamiento=20de=20bits=20individuales.=20Se=20mejor=C3=B3?= =?UTF-8?q?=20la=20validaci=C3=B3n=20de=20configuraciones=20de=20variables?= =?UTF-8?q?=20y=20se=20implement=C3=B3=20un=20sistema=20de=20edici=C3=B3n?= =?UTF-8?q?=20de=20variables=20con=20interfaz=20modal.=20Adem=C3=A1s,=20se?= =?UTF-8?q?=20integraron=20nuevas=20funcionalidades=20en=20la=20API=20para?= =?UTF-8?q?=20la=20gesti=C3=B3n=20de=20variables,=20permitiendo=20una=20ex?= =?UTF-8?q?periencia=20de=20usuario=20m=C3=A1s=20fluida=20y=20completa.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .doc/MemoriaDeEvolucion.md | 102 +++++++++ application_events.json | 267 +++++++++++++++++++++- main.py | 437 +++++++++++++++++++++++++++++++++++-- plc_streamer.lock | 1 + plc_variables.json | 6 + system_state.json | 2 +- templates/index.html | 389 ++++++++++++++++++++++++++++++++- 7 files changed, 1176 insertions(+), 28 deletions(-) create mode 100644 plc_streamer.lock diff --git a/.doc/MemoriaDeEvolucion.md b/.doc/MemoriaDeEvolucion.md index d24eb56..fb4d2be 100644 --- a/.doc/MemoriaDeEvolucion.md +++ b/.doc/MemoriaDeEvolucion.md @@ -4,6 +4,108 @@ ### Latest Modifications (Current Session) +#### Expanded Data Types and Memory Areas Support +**Decision**: Extended the application to support MW (Memory Words), PEW (Process Input Words), PAW (Process Output Words), and individual bit addressing (E5.1, A3.7, M10.0) plus additional Siemens PLC data types. + +**Rationale**: The original implementation was limited to Data Blocks (DB) only, which restricted access to other important PLC memory areas commonly used in industrial applications. MW (Markers/Memory), PEW (Process Inputs), and PAW (Process Outputs) are essential for monitoring peripheral I/O and internal PLC memory, providing comprehensive visibility into the complete PLC system state. + +**Implementation**: + +**New Memory Areas Supported**: +- **MW/M (Memory Words/Markers)**: Internal PLC memory for flags, intermediate calculations, and program logic +- **PEW/PE (Process Input Words)**: Direct access to analog and digital input peripherals +- **PAW/PA (Process Output Words)**: Direct access to analog and digital output peripherals +- **DB (Data Blocks)**: Existing support maintained for backward compatibility + +**Additional Data Types Added**: +- **word**: 16-bit unsigned integer (0-65535) +- **byte**: 8-bit unsigned integer (0-255) +- **uint**: 16-bit unsigned integer (same as word, alternative naming) +- **udint**: 32-bit unsigned integer (0-4294967295) +- **sint**: 8-bit signed integer (-128 to 127) +- **usint**: 8-bit unsigned integer (same as byte, alternative naming) + +**Technical Architecture Changes**: + +**Enhanced Variable Configuration**: +- Added `area` field to variable configuration with validation for supported area types +- Modified `add_variable()` method signature to include area parameter: `add_variable(name, area, db, offset, var_type)` +- Backward compatibility maintained for existing DB-based configurations +- DB parameter now optional and only required for DB area type + +**Smart Area Detection**: +- Automatic area type validation with descriptive error messages +- Support for both short and long area names (e.g., "mw"/"m", "pew"/"pe", "paw"/"pa") +- Case-insensitive area specification for user convenience + +**snap7 Library Integration**: +- Utilized snap7's dedicated functions for optimal performance: + - `mb_read()` for Memory/Markers access + - `eb_read()` for Process Input access + - `ab_read()` for Process Output access + - `db_read()` for Data Block access (existing) +- Each area uses appropriate snap7 function rather than generic `read_area()` for better performance + +**Enhanced Variable Display**: +- Dynamic area description generation for logging and display +- Format examples: "MW100", "PEW256", "PAW64", "DB1.20" +- Clear identification of memory area in event logs and status displays + +**Individual Bit Addressing Support**: +- Added support for individual bit monitoring using Siemens standard notation +- **E (Process Input Bits)**: E5.1 = Input byte 5, bit 1 (sensors, limit switches) +- **A (Process Output Bits)**: A3.7 = Output byte 3, bit 7 (actuators, indicator lamps) +- **MB (Memory Bits)**: M10.0 = Memory byte 10, bit 0 (internal flags, state variables) +- Uses `snap7.util.get_bool()` for reliable bit extraction from byte arrays +- Web interface automatically restricts data type to BOOL for bit areas +- Dynamic bit position selector (0-7) appears only for bit areas +- Format examples: "E5.1", "A3.7", "M10.0" + +**Variable Editing Functionality**: +- Added comprehensive variable editing system with modal interface +- **Edit Button**: ✏️ Edit button added alongside Remove button in variables table +- **Modal Form**: Professional modal dialog with form validation and dynamic field visibility +- **API Support**: GET endpoint for fetching variable configuration, PUT endpoint for updates +- **Data Preservation**: Maintains streaming state when variables are modified +- **Name Changes**: Supports changing variable names with duplicate validation +- **Field Validation**: Dynamic UI that adapts to memory area type (DB fields, bit selectors) +- **Seamless UX**: Modal closes on successful update with automatic page refresh + +**API Enhancements**: +- Updated REST API validation to include all new data types and areas +- Comprehensive input validation with descriptive error messages +- Support for all area types in variable addition endpoint + +**Error Handling**: +- Robust validation for unsupported area types with clear error messages +- Data type validation against complete supported type list +- Graceful fallback for invalid configurations + +**Industrial Benefits**: +- **Comprehensive I/O Monitoring**: Direct access to process inputs and outputs without requiring DB mapping +- **Memory Diagnostics**: Ability to monitor internal PLC flags and calculation results +- **Flexible Data Types**: Support for various integer sizes optimizes memory usage and precision +- **Complete System Visibility**: Monitor entire PLC memory space including peripherals and internal state + +**Configuration Examples**: +``` +# Memory Word (internal flags) +Area: MW, Offset: 100, Type: word + +# Process Input (temperature sensor) +Area: PEW, Offset: 256, Type: int + +# Process Output (valve control) +Area: PAW, Offset: 64, Type: word + +# Data Block (recipe data) +Area: DB, DB: 1, Offset: 20, Type: real +``` + +**Migration Support**: Existing configurations automatically default to "db" area type, ensuring seamless upgrade without configuration loss. + +**Impact**: Technicians and engineers now have complete access to all PLC memory areas, enabling comprehensive monitoring of inputs, outputs, internal memory, and data blocks from a single interface. This eliminates the need for multiple monitoring tools and provides complete system visibility for troubleshooting and process optimization. + #### Persistent Application Events Log **Decision**: Implemented comprehensive event logging system with persistent storage and real-time web interface. diff --git a/application_events.json b/application_events.json index ca3f853..9ed9846 100644 --- a/application_events.json +++ b/application_events.json @@ -251,8 +251,271 @@ "slot": 2 } } + }, + { + "timestamp": "2025-07-17T16:02:11.949781", + "level": "info", + "event_type": "Application started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-17T16:02:11.964986", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2 + } + }, + { + "timestamp": "2025-07-17T16:02:11.966271", + "level": "info", + "event_type": "csv_started", + "message": "CSV recording started for 4 variables", + "details": { + "variables_count": 4, + "output_directory": "records\\17-07-2025" + } + }, + { + "timestamp": "2025-07-17T16:02:11.967664", + "level": "info", + "event_type": "streaming_started", + "message": "Streaming started with 4 variables", + "details": { + "variables_count": 4, + "streaming_variables_count": 4, + "sampling_interval": 0.1, + "udp_host": "127.0.0.1", + "udp_port": 9870 + } + }, + { + "timestamp": "2025-07-17T16:08:42.495109", + "level": "info", + "event_type": "Application started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-17T16:08:42.524816", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2 + } + }, + { + "timestamp": "2025-07-17T16:08:42.527387", + "level": "info", + "event_type": "csv_started", + "message": "CSV recording started for 4 variables", + "details": { + "variables_count": 4, + "output_directory": "records\\17-07-2025" + } + }, + { + "timestamp": "2025-07-17T16:08:42.529631", + "level": "info", + "event_type": "streaming_started", + "message": "Streaming started with 4 variables", + "details": { + "variables_count": 4, + "streaming_variables_count": 4, + "sampling_interval": 0.1, + "udp_host": "127.0.0.1", + "udp_port": 9870 + } + }, + { + "timestamp": "2025-07-17T16:15:14.523187", + "level": "info", + "event_type": "Application started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-17T16:15:14.534443", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2 + } + }, + { + "timestamp": "2025-07-17T16:15:14.536431", + "level": "info", + "event_type": "csv_started", + "message": "CSV recording started for 4 variables", + "details": { + "variables_count": 4, + "output_directory": "records\\17-07-2025" + } + }, + { + "timestamp": "2025-07-17T16:15:14.538424", + "level": "info", + "event_type": "streaming_started", + "message": "Streaming started with 4 variables", + "details": { + "variables_count": 4, + "streaming_variables_count": 4, + "sampling_interval": 0.1, + "udp_host": "127.0.0.1", + "udp_port": 9870 + } + }, + { + "timestamp": "2025-07-17T16:15:43.961033", + "level": "info", + "event_type": "variable_added", + "message": "Variable added: PEW302 -> PEW302 (real)", + "details": { + "name": "PEW302", + "area": "pew", + "db": null, + "offset": 302, + "bit": null, + "type": "real", + "total_variables": 5 + } + }, + { + "timestamp": "2025-07-17T16:15:43.965019", + "level": "info", + "event_type": "csv_file_created", + "message": "New CSV file created after variable modification: _16_15_43.csv", + "details": { + "file_path": "records\\17-07-2025\\_16_15_43.csv", + "variables_count": 5, + "reason": "variable_modification" + } + }, + { + "timestamp": "2025-07-17T16:16:05.447969", + "level": "info", + "event_type": "variable_removed", + "message": "Variable removed: PEW302", + "details": { + "name": "PEW302", + "removed_config": { + "area": "pew", + "offset": 302, + "type": "real", + "streaming": false + }, + "total_variables": 4 + } + }, + { + "timestamp": "2025-07-17T16:16:05.452456", + "level": "info", + "event_type": "csv_file_created", + "message": "New CSV file created after variable modification: _16_16_05.csv", + "details": { + "file_path": "records\\17-07-2025\\_16_16_05.csv", + "variables_count": 4, + "reason": "variable_modification" + } + }, + { + "timestamp": "2025-07-17T16:16:05.460154", + "level": "error", + "event_type": "streaming_error", + "message": "Error in streaming loop: dictionary changed size during iteration", + "details": { + "error": "dictionary changed size during iteration", + "consecutive_errors": 1 + } + }, + { + "timestamp": "2025-07-17T16:16:21.389136", + "level": "info", + "event_type": "variable_added", + "message": "Variable added: PEW302 -> PEW302 (word)", + "details": { + "name": "PEW302", + "area": "pew", + "db": null, + "offset": 302, + "bit": null, + "type": "word", + "total_variables": 5 + } + }, + { + "timestamp": "2025-07-17T16:16:21.395123", + "level": "info", + "event_type": "csv_file_created", + "message": "New CSV file created after variable modification: _16_16_21.csv", + "details": { + "file_path": "records\\17-07-2025\\_16_16_21.csv", + "variables_count": 5, + "reason": "variable_modification" + } + }, + { + "timestamp": "2025-07-17T16:16:21.400619", + "level": "error", + "event_type": "streaming_error", + "message": "Error in streaming loop: dictionary changed size during iteration", + "details": { + "error": "dictionary changed size during iteration", + "consecutive_errors": 1 + } + }, + { + "timestamp": "2025-07-17T16:22:46.797675", + "level": "info", + "event_type": "Application started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-07-17T16:22:46.830234", + "level": "info", + "event_type": "plc_connection", + "message": "Successfully connected to PLC 10.1.33.11", + "details": { + "ip": "10.1.33.11", + "rack": 0, + "slot": 2 + } + }, + { + "timestamp": "2025-07-17T16:22:46.832226", + "level": "info", + "event_type": "csv_started", + "message": "CSV recording started for 5 variables", + "details": { + "variables_count": 5, + "output_directory": "records\\17-07-2025" + } + }, + { + "timestamp": "2025-07-17T16:22:46.834244", + "level": "info", + "event_type": "streaming_started", + "message": "Streaming started with 5 variables", + "details": { + "variables_count": 5, + "streaming_variables_count": 5, + "sampling_interval": 0.1, + "udp_host": "127.0.0.1", + "udp_port": 9870 + } } ], - "last_updated": "2025-07-17T15:43:38.917840", - "total_entries": 22 + "last_updated": "2025-07-17T16:22:46.834244", + "total_entries": 46 } \ No newline at end of file diff --git a/main.py b/main.py index 81a6814..e51c46a 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ from flask import ( send_from_directory, ) import snap7 +import snap7.util import json import socket import time @@ -350,27 +351,95 @@ class PLCDataStreamer: config_details, ) - def add_variable(self, name: str, db: int, offset: int, var_type: str): + def add_variable( + self, name: str, area: str, db: int, offset: int, var_type: str, bit: int = None + ): """Add a variable for polling""" - self.variables[name] = { - "db": db, + area = area.lower() + + # Validate area type - ahora incluye áreas de bits individuales + if area not in ["db", "mw", "m", "pew", "pe", "paw", "pa", "e", "a", "mb"]: + raise ValueError( + f"Unsupported area type: {area}. Supported: db, mw, m, pew, pe, paw, pa, e, a, mb" + ) + + # Validate data type + valid_types = [ + "real", + "int", + "bool", + "dint", + "word", + "byte", + "uint", + "udint", + "sint", + "usint", + ] + if var_type not in valid_types: + raise ValueError( + f"Invalid data type: {var_type}. Supported: {', '.join(valid_types)}" + ) + + # Para áreas de bits individuales, el tipo debe ser bool y bit debe estar especificado + if area in ["e", "a", "mb"] and var_type != "bool": + raise ValueError(f"For bit areas ({area}), data type must be 'bool'") + + if area in ["e", "a", "mb"] and bit is None: + raise ValueError( + f"For bit areas ({area}), bit position must be specified (0-7)" + ) + + if bit is not None and (bit < 0 or bit > 7): + raise ValueError("Bit position must be between 0 and 7") + + # Create variable configuration + var_config = { + "area": area, "offset": offset, "type": var_type, "streaming": False, } + + # Add DB number only for DB area + if area == "db": + var_config["db"] = db + + # Add bit position for bit areas + if area in ["e", "a", "mb"]: + var_config["bit"] = bit + + self.variables[name] = var_config self.save_variables() variable_details = { "name": name, - "db": db, + "area": area, + "db": db if area == "db" else None, "offset": offset, + "bit": bit, "type": var_type, "total_variables": len(self.variables), } + + # Updated area description to include bit addresses + area_description = { + "db": f"DB{db}.{offset}", + "mw": f"MW{offset}", + "m": f"M{offset}", + "pew": f"PEW{offset}", + "pe": f"PE{offset}", + "paw": f"PAW{offset}", + "pa": f"PA{offset}", + "e": f"E{offset}.{bit}", + "a": f"A{offset}.{bit}", + "mb": f"M{offset}.{bit}", + } + self.log_event( "info", "variable_added", - f"Variable added: {name} -> DB{db}.{offset} ({var_type})", + f"Variable added: {name} -> {area_description[area]} ({var_type})", variable_details, ) self.create_new_csv_file_for_variable_modification() @@ -681,23 +750,181 @@ class PLCDataStreamer: def read_variable(self, var_config: Dict[str, Any]) -> Any: """Read a specific variable from the PLC""" try: - db = var_config["db"] + area_type = var_config.get("area", "db").lower() offset = var_config["offset"] var_type = var_config["type"] + bit = var_config.get("bit") # Extract bit position for bit areas + + if area_type == "db": + # Data Block access (existing functionality) + db = var_config["db"] + if var_type == "real": + raw_data = self.plc.db_read(db, offset, 4) + value = struct.unpack(">f", raw_data)[0] + elif var_type == "int": + raw_data = self.plc.db_read(db, offset, 2) + value = struct.unpack(">h", raw_data)[0] + elif var_type == "bool": + raw_data = self.plc.db_read(db, offset, 1) + value = bool(raw_data[0] & 0x01) + elif var_type == "dint": + raw_data = self.plc.db_read(db, offset, 4) + value = struct.unpack(">l", raw_data)[0] + elif var_type == "word": + raw_data = self.plc.db_read(db, offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "byte": + raw_data = self.plc.db_read(db, offset, 1) + value = struct.unpack(">B", raw_data)[0] + elif var_type == "uint": + raw_data = self.plc.db_read(db, offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "udint": + raw_data = self.plc.db_read(db, offset, 4) + value = struct.unpack(">L", raw_data)[0] + elif var_type == "sint": + raw_data = self.plc.db_read(db, offset, 1) + value = struct.unpack(">b", raw_data)[0] + elif var_type == "usint": + raw_data = self.plc.db_read(db, offset, 1) + value = struct.unpack(">B", raw_data)[0] + else: + return None + + elif area_type == "mw" or area_type == "m": + # Memory Words / Markers access + if var_type == "real": + raw_data = self.plc.mb_read(offset, 4) + value = struct.unpack(">f", raw_data)[0] + elif var_type == "int": + raw_data = self.plc.mb_read(offset, 2) + value = struct.unpack(">h", raw_data)[0] + elif var_type == "bool": + raw_data = self.plc.mb_read(offset, 1) + value = bool(raw_data[0] & 0x01) + elif var_type == "dint": + raw_data = self.plc.mb_read(offset, 4) + value = struct.unpack(">l", raw_data)[0] + elif var_type == "word": + raw_data = self.plc.mb_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "byte": + raw_data = self.plc.mb_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + elif var_type == "uint": + raw_data = self.plc.mb_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "udint": + raw_data = self.plc.mb_read(offset, 4) + value = struct.unpack(">L", raw_data)[0] + elif var_type == "sint": + raw_data = self.plc.mb_read(offset, 1) + value = struct.unpack(">b", raw_data)[0] + elif var_type == "usint": + raw_data = self.plc.mb_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + else: + return None + + elif area_type == "pew" or area_type == "pe": + # Process Input Words access + if var_type == "real": + raw_data = self.plc.eb_read(offset, 4) + value = struct.unpack(">f", raw_data)[0] + elif var_type == "int": + raw_data = self.plc.eb_read(offset, 2) + value = struct.unpack(">h", raw_data)[0] + elif var_type == "bool": + raw_data = self.plc.eb_read(offset, 1) + value = bool(raw_data[0] & 0x01) + elif var_type == "dint": + raw_data = self.plc.eb_read(offset, 4) + value = struct.unpack(">l", raw_data)[0] + elif var_type == "word": + raw_data = self.plc.eb_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "byte": + raw_data = self.plc.eb_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + elif var_type == "uint": + raw_data = self.plc.eb_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "udint": + raw_data = self.plc.eb_read(offset, 4) + value = struct.unpack(">L", raw_data)[0] + elif var_type == "sint": + raw_data = self.plc.eb_read(offset, 1) + value = struct.unpack(">b", raw_data)[0] + elif var_type == "usint": + raw_data = self.plc.eb_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + else: + return None + + elif area_type == "paw" or area_type == "pa": + # Process Output Words access + if var_type == "real": + raw_data = self.plc.ab_read(offset, 4) + value = struct.unpack(">f", raw_data)[0] + elif var_type == "int": + raw_data = self.plc.ab_read(offset, 2) + value = struct.unpack(">h", raw_data)[0] + elif var_type == "bool": + raw_data = self.plc.ab_read(offset, 1) + value = bool(raw_data[0] & 0x01) + elif var_type == "dint": + raw_data = self.plc.ab_read(offset, 4) + value = struct.unpack(">l", raw_data)[0] + elif var_type == "word": + raw_data = self.plc.ab_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "byte": + raw_data = self.plc.ab_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + elif var_type == "uint": + raw_data = self.plc.ab_read(offset, 2) + value = struct.unpack(">H", raw_data)[0] + elif var_type == "udint": + raw_data = self.plc.ab_read(offset, 4) + value = struct.unpack(">L", raw_data)[0] + elif var_type == "sint": + raw_data = self.plc.ab_read(offset, 1) + value = struct.unpack(">b", raw_data)[0] + elif var_type == "usint": + raw_data = self.plc.ab_read(offset, 1) + value = struct.unpack(">B", raw_data)[0] + else: + return None + + elif area_type == "e": + # Process Input Bits access (E5.1 format) + if var_type == "bool": + raw_data = self.plc.eb_read(offset, 1) + # Use snap7.util.get_bool for proper bit extraction + value = snap7.util.get_bool(raw_data, 0, bit) + else: + return None + + elif area_type == "a": + # Process Output Bits access (A3.7 format) + if var_type == "bool": + raw_data = self.plc.ab_read(offset, 1) + # Use snap7.util.get_bool for proper bit extraction + value = snap7.util.get_bool(raw_data, 0, bit) + else: + return None + + elif area_type == "mb": + # Memory Bits access (M10.0 format) + if var_type == "bool": + raw_data = self.plc.mb_read(offset, 1) + # Use snap7.util.get_bool for proper bit extraction + value = snap7.util.get_bool(raw_data, 0, bit) + else: + return None - if var_type == "real": - raw_data = self.plc.db_read(db, offset, 4) - value = struct.unpack(">f", raw_data)[0] - elif var_type == "int": - raw_data = self.plc.db_read(db, offset, 2) - value = struct.unpack(">h", raw_data)[0] - elif var_type == "bool": - raw_data = self.plc.db_read(db, offset, 1) - value = bool(raw_data[0] & 0x01) - elif var_type == "dint": - raw_data = self.plc.db_read(db, offset, 4) - value = struct.unpack(">l", raw_data)[0] else: + self.logger.error(f"Unsupported area type: {area_type}") return None return value @@ -1064,14 +1291,57 @@ def add_variable(): try: data = request.get_json() name = data.get("name") + area = data.get("area") db = int(data.get("db")) offset = int(data.get("offset")) var_type = data.get("type") + bit = data.get("bit") # Added bit parameter - if not name or var_type not in ["real", "int", "bool", "dint"]: + valid_types = [ + "real", + "int", + "bool", + "dint", + "word", + "byte", + "uint", + "udint", + "sint", + "usint", + ] + valid_areas = ["db", "mw", "m", "pew", "pe", "paw", "pa", "e", "a", "mb"] + + if ( + not name + or not area + or var_type not in valid_types + or area.lower() not in valid_areas + ): return jsonify({"success": False, "message": "Invalid data"}), 400 - streamer.add_variable(name, db, offset, var_type) + if area.lower() in ["e", "a", "mb"] and bit is None: + return ( + jsonify( + { + "success": False, + "message": "Bit position must be specified for bit areas", + } + ), + 400, + ) + + if area.lower() in ["e", "a", "mb"] and (bit < 0 or bit > 7): + return ( + jsonify( + { + "success": False, + "message": "Bit position must be between 0 and 7", + } + ), + 400, + ) + + streamer.add_variable(name, area, db, offset, var_type, bit) return jsonify({"success": True, "message": f"Variable {name} added"}) except Exception as e: @@ -1085,6 +1355,133 @@ def remove_variable(name): return jsonify({"success": True, "message": f"Variable {name} removed"}) +@app.route("/api/variables/", methods=["GET"]) +def get_variable(name): + """Get a specific variable configuration""" + try: + if name not in streamer.variables: + return ( + jsonify({"success": False, "message": f"Variable {name} not found"}), + 404, + ) + + var_config = streamer.variables[name].copy() + var_config["name"] = name + var_config["streaming"] = name in streamer.streaming_variables + + return jsonify({"success": True, "variable": var_config}) + + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 400 + + +@app.route("/api/variables/", methods=["PUT"]) +def update_variable(name): + """Update an existing variable""" + try: + data = request.get_json() + new_name = data.get("name", name) + area = data.get("area") + db = int(data.get("db", 1)) + offset = int(data.get("offset")) + var_type = data.get("type") + bit = data.get("bit") # Added bit parameter + + valid_types = [ + "real", + "int", + "bool", + "dint", + "word", + "byte", + "uint", + "udint", + "sint", + "usint", + ] + valid_areas = ["db", "mw", "m", "pew", "pe", "paw", "pa", "e", "a", "mb"] + + if ( + not new_name + or not area + or var_type not in valid_types + or area.lower() not in valid_areas + ): + return jsonify({"success": False, "message": "Invalid data"}), 400 + + if area.lower() in ["e", "a", "mb"] and bit is None: + return ( + jsonify( + { + "success": False, + "message": "Bit position must be specified for bit areas", + } + ), + 400, + ) + + if area.lower() in ["e", "a", "mb"] and (bit < 0 or bit > 7): + return ( + jsonify( + { + "success": False, + "message": "Bit position must be between 0 and 7", + } + ), + 400, + ) + + # Remove old variable if name changed + if name != new_name and name in streamer.variables: + # Check if new name already exists + if new_name in streamer.variables: + return ( + jsonify( + { + "success": False, + "message": f"Variable {new_name} already exists", + } + ), + 400, + ) + + # Preserve streaming state + was_streaming = name in streamer.streaming_variables + streamer.remove_variable(name) + + # Add updated variable + streamer.add_variable(new_name, area, db, offset, var_type, bit) + + # Restore streaming state if it was enabled + if was_streaming: + streamer.toggle_streaming_variable(new_name, True) + else: + # Update existing variable + if name not in streamer.variables: + return ( + jsonify( + {"success": False, "message": f"Variable {name} not found"} + ), + 404, + ) + + # Preserve streaming state + was_streaming = name in streamer.streaming_variables + + # Remove and re-add with new configuration + streamer.remove_variable(name) + streamer.add_variable(new_name, area, db, offset, var_type, bit) + + # Restore streaming state if it was enabled + if was_streaming: + streamer.toggle_streaming_variable(new_name, True) + + return jsonify({"success": True, "message": f"Variable updated successfully"}) + + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 400 + + @app.route("/api/variables//streaming", methods=["POST"]) def toggle_variable_streaming(name): """Toggle streaming for a specific variable""" diff --git a/plc_streamer.lock b/plc_streamer.lock new file mode 100644 index 0000000..6683c21 --- /dev/null +++ b/plc_streamer.lock @@ -0,0 +1 @@ +41384 \ No newline at end of file diff --git a/plc_variables.json b/plc_variables.json index b07c92b..2000f9c 100644 --- a/plc_variables.json +++ b/plc_variables.json @@ -22,5 +22,11 @@ "offset": 18, "type": "real", "streaming": true + }, + "PEW302": { + "area": "pew", + "offset": 302, + "type": "word", + "streaming": true } } \ No newline at end of file diff --git a/system_state.json b/system_state.json index 8d0c3eb..5684b41 100644 --- a/system_state.json +++ b/system_state.json @@ -5,5 +5,5 @@ "should_record_csv": true }, "auto_recovery_enabled": true, - "last_update": "2025-07-17T15:42:38.054690" + "last_update": "2025-07-17T16:22:46.833236" } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4844204..81f3953 100644 --- a/templates/index.html +++ b/templates/index.html @@ -360,6 +360,88 @@ flex-direction: column; } } + + /* Modal Styles */ + .modal { + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + } + + .modal-content { + background-color: white; + margin: 5% auto; + padding: 0; + border-radius: 15px; + width: 90%; + max-width: 600px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + animation: modalShow 0.3s ease; + } + + @keyframes modalShow { + from { + opacity: 0; + transform: translateY(-50px); + } + + to { + opacity: 1; + transform: translateY(0); + } + } + + .modal-header { + background: linear-gradient(135deg, #6b7280, #4b5563); + color: white; + padding: 20px; + border-radius: 15px 15px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + } + + .modal-header h3 { + margin: 0; + font-size: 1.5rem; + } + + .close { + color: white; + font-size: 28px; + font-weight: bold; + cursor: pointer; + line-height: 1; + } + + .close:hover { + color: #f1f5f9; + } + + .modal-body { + padding: 30px; + } + + .modal-footer { + padding: 20px 30px; + border-top: 1px solid #e5e7eb; + display: flex; + justify-content: flex-end; + gap: 10px; + } + + .btn-secondary { + background: linear-gradient(135deg, #9ca3af, #6b7280); + } + + .btn-secondary:hover { + background: linear-gradient(135deg, #6b7280, #4b5563); + } @@ -461,6 +543,18 @@
+ + +
+
@@ -468,12 +562,31 @@ +
@@ -486,7 +599,7 @@ Name - Data Block + Memory Area Offset Type Stream to PlotJuggler @@ -497,7 +610,25 @@ {% for name, var in variables.items() %} {{ name }} - DB{{ var.db }} + + {% if var.area == 'db' or var.get('db') %} + DB{{ var.get('db', 'N/A') }}.{{ var.offset }} + {% elif var.area == 'mw' or var.area == 'm' %} + MW{{ var.offset }} + {% elif var.area == 'pew' or var.area == 'pe' %} + PEW{{ var.offset }} + {% elif var.area == 'paw' or var.area == 'pa' %} + PAW{{ var.offset }} + {% elif var.area == 'e' %} + E{{ var.offset }}.{{ var.bit }} + {% elif var.area == 'a' %} + A{{ var.offset }}.{{ var.bit }} + {% elif var.area == 'mb' %} + M{{ var.offset }}.{{ var.bit }} + {% else %} + DB{{ var.get('db', 'N/A') }}.{{ var.offset }} + {% endif %} + {{ var.offset }} {{ var.type.upper() }} @@ -506,6 +637,7 @@ + @@ -514,6 +646,78 @@ + + +

💾 CSV Recording Control

@@ -694,16 +898,59 @@ }); }); + // Toggle DB and Bit field visibility based on memory area selection + function toggleFields() { + const area = document.getElementById('var-area').value; + const dbField = document.getElementById('db-field'); + const dbInput = document.getElementById('var-db'); + const bitField = document.getElementById('bit-field'); + const typeSelect = document.getElementById('var-type'); + + // Handle DB field + if (area === 'db') { + dbField.style.display = 'block'; + dbInput.required = true; + } else { + dbField.style.display = 'none'; + dbInput.required = false; + dbInput.value = 1; // Default value for non-DB areas + } + + // Handle Bit field and data type restrictions + if (area === 'e' || area === 'a' || area === 'mb') { + bitField.style.display = 'block'; + // For bit areas, force data type to bool + typeSelect.value = 'bool'; + // Disable other data types for bit areas + Array.from(typeSelect.options).forEach(option => { + option.disabled = (option.value !== 'bool'); + }); + } else { + bitField.style.display = 'none'; + // Re-enable all data types for non-bit areas + Array.from(typeSelect.options).forEach(option => { + option.disabled = false; + }); + } + } + // Add variable document.getElementById('variable-form').addEventListener('submit', function (e) { e.preventDefault(); + const area = document.getElementById('var-area').value; const data = { name: document.getElementById('var-name').value, - db: parseInt(document.getElementById('var-db').value), + area: area, + db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1, offset: parseInt(document.getElementById('var-offset').value), type: document.getElementById('var-type').value }; + // Add bit parameter for bit areas + if (area === 'e' || area === 'a' || area === 'mb') { + data.bit = parseInt(document.getElementById('var-bit').value); + } + fetch('/api/variables', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -946,6 +1193,138 @@ updateStatus(); loadStreamingStatus(); refreshEventLog(); + + // Edit Variable Functions + let currentEditingVariable = null; + + function editVariable(name) { + currentEditingVariable = name; + + // Fetch current variable data + fetch(`/api/variables/${name}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + populateEditForm(data.variable); + document.getElementById('edit-modal').style.display = 'block'; + } else { + showMessage(data.message, 'error'); + } + }) + .catch(error => { + showMessage(`Error fetching variable data: ${error}`, 'error'); + }); + } + + function populateEditForm(variable) { + document.getElementById('edit-var-name').value = variable.name; + document.getElementById('edit-var-area').value = variable.area; + document.getElementById('edit-var-offset').value = variable.offset; + document.getElementById('edit-var-type').value = variable.type; + + if (variable.db) { + document.getElementById('edit-var-db').value = variable.db; + } + + if (variable.bit !== undefined) { + document.getElementById('edit-var-bit').value = variable.bit; + } + + // Update field visibility based on area + toggleEditFields(); + } + + function toggleEditFields() { + const area = document.getElementById('edit-var-area').value; + const dbField = document.getElementById('edit-db-field'); + const dbInput = document.getElementById('edit-var-db'); + const bitField = document.getElementById('edit-bit-field'); + const typeSelect = document.getElementById('edit-var-type'); + + // Handle DB field + if (area === 'db') { + dbField.style.display = 'block'; + dbInput.required = true; + } else { + dbField.style.display = 'none'; + dbInput.required = false; + dbInput.value = 1; // Default value for non-DB areas + } + + // Handle Bit field and data type restrictions + if (area === 'e' || area === 'a' || area === 'mb') { + bitField.style.display = 'block'; + // For bit areas, force data type to bool + typeSelect.value = 'bool'; + // Disable other data types for bit areas + Array.from(typeSelect.options).forEach(option => { + option.disabled = (option.value !== 'bool'); + }); + } else { + bitField.style.display = 'none'; + // Re-enable all data types for non-bit areas + Array.from(typeSelect.options).forEach(option => { + option.disabled = false; + }); + } + } + + function closeEditModal() { + document.getElementById('edit-modal').style.display = 'none'; + currentEditingVariable = null; + } + + // Handle edit form submission + document.getElementById('edit-variable-form').addEventListener('submit', function (e) { + e.preventDefault(); + + if (!currentEditingVariable) { + showMessage('No variable selected for editing', 'error'); + return; + } + + const area = document.getElementById('edit-var-area').value; + const data = { + name: document.getElementById('edit-var-name').value, + area: area, + db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1, + offset: parseInt(document.getElementById('edit-var-offset').value), + type: document.getElementById('edit-var-type').value + }; + + // Add bit parameter for bit areas + if (area === 'e' || area === 'a' || area === 'mb') { + data.bit = parseInt(document.getElementById('edit-var-bit').value); + } + + fetch(`/api/variables/${currentEditingVariable}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + showMessage(data.message, data.success ? 'success' : 'error'); + if (data.success) { + closeEditModal(); + location.reload(); + } + }) + .catch(error => { + showMessage(`Error updating variable: ${error}`, 'error'); + }); + }); + + // Close modal when clicking outside of it + window.onclick = function (event) { + const modal = document.getElementById('edit-modal'); + if (event.target === modal) { + closeEditModal(); + } + } + + // Initialize field visibility on page load + toggleFields();