Se amplió el soporte para tipos de datos y áreas de memoria en la aplicación, incluyendo MW (Memory Words), PEW (Process Input Words), PAW (Process Output Words) y direccionamiento de bits individuales. Se mejoró la validación de configuraciones de variables y se implementó un sistema de edición de variables con interfaz modal. Además, se integraron nuevas funcionalidades en la API para la gestión de variables, permitiendo una experiencia de usuario más fluida y completa.

This commit is contained in:
Miguel 2025-07-17 16:23:18 +02:00
parent 5e3b1ae76e
commit c0d7cbc91a
7 changed files with 1176 additions and 28 deletions

View File

@ -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.

View File

@ -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
}

437
main.py
View File

@ -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/<name>", 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/<name>", 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/<name>/streaming", methods=["POST"])
def toggle_variable_streaming(name):
"""Toggle streaming for a specific variable"""

1
plc_streamer.lock Normal file
View File

@ -0,0 +1 @@
41384

View File

@ -22,5 +22,11 @@
"offset": 18,
"type": "real",
"streaming": true
},
"PEW302": {
"area": "pew",
"offset": 302,
"type": "word",
"streaming": true
}
}

View File

@ -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"
}

View File

@ -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);
}
</style>
</head>
@ -461,6 +543,18 @@
<input type="text" id="var-name" placeholder="temperature" required>
</div>
<div class="form-group">
<label>Memory Area:</label>
<select id="var-area" required onchange="toggleFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</div>
<div class="form-group" id="db-field">
<label>Data Block (DB):</label>
<input type="number" id="var-db" min="1" max="9999" value="1" required>
</div>
@ -468,12 +562,31 @@
<label>Offset:</label>
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
</div>
<div class="form-group" id="bit-field" style="display: none;">
<label>Bit Position:</label>
<select id="var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</div>
<div class="form-group">
<label>Data Type:</label>
<select id="var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit)</option>
<option value="dint">DINT (32-bit)</option>
<option value="int">INT (16-bit Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</option>
<option value="bool">BOOL</option>
</select>
</div>
@ -486,7 +599,7 @@
<thead>
<tr>
<th>Name</th>
<th>Data Block</th>
<th>Memory Area</th>
<th>Offset</th>
<th>Type</th>
<th>Stream to PlotJuggler</th>
@ -497,7 +610,25 @@
{% for name, var in variables.items() %}
<tr>
<td>{{ name }}</td>
<td>DB{{ var.db }}</td>
<td>
{% 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 %}
</td>
<td>{{ var.offset }}</td>
<td>{{ var.type.upper() }}</td>
<td>
@ -506,6 +637,7 @@
<label for="stream-{{ name }}">Enable</label>
</td>
<td>
<button class="btn btn-primary" onclick="editVariable('{{ name }}')">✏️ Edit</button>
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
</td>
</tr>
@ -514,6 +646,78 @@
</table>
</div>
<!-- Edit Variable Modal -->
<div id="edit-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>✏️ Edit Variable</h3>
<span class="close" onclick="closeEditModal()">&times;</span>
</div>
<div class="modal-body">
<form id="edit-variable-form">
<div class="form-row">
<div class="form-group">
<label>Variable Name:</label>
<input type="text" id="edit-var-name" required>
</div>
<div class="form-group">
<label>Memory Area:</label>
<select id="edit-var-area" required onchange="toggleEditFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</div>
<div class="form-group" id="edit-db-field">
<label>Data Block (DB):</label>
<input type="number" id="edit-var-db" min="1" max="9999" value="1" required>
</div>
<div class="form-group">
<label>Offset:</label>
<input type="number" id="edit-var-offset" min="0" max="8192" value="0" required>
</div>
<div class="form-group" id="edit-bit-field" style="display: none;">
<label>Bit Position:</label>
<select id="edit-var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</div>
<div class="form-group">
<label>Data Type:</label>
<select id="edit-var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</option>
<option value="bool">BOOL (Boolean)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">❌ Cancel</button>
<button type="submit" class="btn btn-success">💾 Update Variable</button>
</div>
</form>
</div>
</div>
</div>
<!-- CSV Recording Control -->
<div class="card">
<h2>💾 CSV Recording Control</h2>
@ -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();
</script>
</body>