diff --git a/database/schema.sql b/database/schema.sql
index 9fcceba..33a772c 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -3,6 +3,7 @@
-- Date: October 26, 2025
-- Drop existing tables if they exist (for clean setup)
+DROP TABLE IF EXISTS protocol_mappings CASCADE;
DROP TABLE IF EXISTS audit_log CASCADE;
DROP TABLE IF EXISTS emergency_stop_events CASCADE;
DROP TABLE IF EXISTS failsafe_events CASCADE;
@@ -29,6 +30,41 @@ CREATE TABLE pump_stations (
COMMENT ON TABLE pump_stations IS 'Metadata about pump stations';
COMMENT ON COLUMN pump_stations.timezone IS 'Timezone for the pump station (default: Europe/Rome for Italian utilities)';
+-- Create protocol_mappings table
+CREATE TABLE protocol_mappings (
+ mapping_id VARCHAR(100) PRIMARY KEY,
+ station_id VARCHAR(50) NOT NULL,
+ pump_id VARCHAR(50) NOT NULL,
+ protocol_type VARCHAR(20) NOT NULL, -- 'opcua', 'modbus_tcp', 'modbus_rtu', 'rest_api'
+ protocol_address VARCHAR(500) NOT NULL, -- Node ID, register address, endpoint URL
+ data_type VARCHAR(50) NOT NULL, -- 'setpoint', 'status', 'control', 'safety'
+ db_source VARCHAR(100) NOT NULL, -- Database field name
+
+ -- Metadata
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW(),
+ created_by VARCHAR(100),
+ enabled BOOLEAN DEFAULT TRUE,
+
+ FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id),
+
+ -- Constraints
+ CONSTRAINT valid_protocol_type CHECK (protocol_type IN ('opcua', 'modbus_tcp', 'modbus_rtu', 'rest_api')),
+ CONSTRAINT valid_data_type CHECK (data_type IN ('setpoint', 'status', 'control', 'safety', 'alarm', 'configuration')),
+ CONSTRAINT unique_protocol_address UNIQUE (protocol_type, protocol_address)
+);
+
+COMMENT ON TABLE protocol_mappings IS 'Protocol-agnostic mappings between database fields and protocol addresses';
+COMMENT ON COLUMN protocol_mappings.protocol_type IS 'Protocol type: opcua, modbus_tcp, modbus_rtu, rest_api';
+COMMENT ON COLUMN protocol_mappings.protocol_address IS 'Protocol-specific address (OPC UA node ID, Modbus register, REST endpoint)';
+COMMENT ON COLUMN protocol_mappings.data_type IS 'Type of data: setpoint, status, control, safety, alarm, configuration';
+COMMENT ON COLUMN protocol_mappings.db_source IS 'Database field name that this mapping represents';
+
+-- Create indexes for protocol mappings
+CREATE INDEX idx_protocol_mappings_station_pump ON protocol_mappings(station_id, pump_id);
+CREATE INDEX idx_protocol_mappings_protocol_type ON protocol_mappings(protocol_type, enabled);
+CREATE INDEX idx_protocol_mappings_data_type ON protocol_mappings(data_type, enabled);
+
-- Create pumps table
CREATE TABLE pumps (
pump_id VARCHAR(50) NOT NULL,
diff --git a/docs/PROTOCOL_MAPPING_IMPLEMENTATION_PLAN.md b/docs/PROTOCOL_MAPPING_IMPLEMENTATION_PLAN.md
new file mode 100644
index 0000000..83f68aa
--- /dev/null
+++ b/docs/PROTOCOL_MAPPING_IMPLEMENTATION_PLAN.md
@@ -0,0 +1,514 @@
+# Protocol Mapping - Phase 1 Implementation Plan
+
+## Overview
+This document outlines the detailed implementation plan for Phase 1 of the Protocol Mapping UI feature, supporting Modbus, OPC UA, and other industrial protocols.
+
+## π― Phase 1 Goals
+- Enable basic configuration of database-to-protocol mappings through unified dashboard interface
+- Replace hardcoded protocol mappings with configurable system
+- Support multiple protocols (Modbus, OPC UA) through single Protocol Mapping tab
+- Provide protocol-specific validation within unified interface
+- Implement protocol switching within single dashboard tab
+
+## π Detailed Task Breakdown
+
+### Task 1: Extend Configuration Manager with Protocol Mapping Support
+**Priority**: High
+**Estimated Effort**: 3 days
+
+#### Implementation Details:
+```python
+# File: src/dashboard/configuration_manager.py
+
+class ProtocolMapping(BaseModel):
+ """Protocol mapping configuration for all protocols"""
+ id: str
+ protocol_type: str # modbus_tcp, opcua, custom
+ station_id: str
+ pump_id: str
+ data_type: str # setpoint, status, power, etc.
+ protocol_address: str # register address or OPC UA node
+ db_source: str
+ transformation_rules: List[Dict] = []
+
+ # Protocol-specific configurations
+ modbus_config: Optional[Dict] = None
+ opcua_config: Optional[Dict] = None
+
+class ConfigurationManager:
+ def __init__(self):
+ self.protocol_mappings: List[ProtocolMapping] = []
+
+ def add_protocol_mapping(self, mapping: ProtocolMapping) -> bool:
+ """Add a new protocol mapping with validation"""
+
+ def get_protocol_mappings(self,
+ protocol_type: str = None,
+ station_id: str = None,
+ pump_id: str = None) -> List[ProtocolMapping]:
+ """Get mappings filtered by protocol/station/pump"""
+
+ def validate_protocol_mapping(self, mapping: ProtocolMapping) -> Dict[str, Any]:
+ """Validate mapping for conflicts and protocol-specific rules"""
+```
+
+### Task 2: Create Protocol Mapping API Endpoints
+**Priority**: High
+**Estimated Effort**: 2 days
+
+#### Implementation Details:
+```python
+# File: src/dashboard/api.py
+
+@dashboard_router.get("/protocol-mappings")
+async def get_protocol_mappings(
+ protocol_type: Optional[str] = None,
+ station_id: Optional[str] = None,
+ pump_id: Optional[str] = None
+):
+ """Get all protocol mappings"""
+
+@dashboard_router.post("/protocol-mappings")
+async def create_protocol_mapping(mapping: ProtocolMapping):
+ """Create a new protocol mapping"""
+
+@dashboard_router.put("/protocol-mappings/{mapping_id}")
+async def update_protocol_mapping(mapping_id: str, mapping: ProtocolMapping):
+ """Update an existing protocol mapping"""
+
+@dashboard_router.delete("/protocol-mappings/{mapping_id}")
+async def delete_protocol_mapping(mapping_id: str):
+ """Delete a protocol mapping"""
+
+@dashboard_router.post("/protocol-mappings/validate")
+async def validate_protocol_mapping(mapping: ProtocolMapping):
+ """Validate a protocol mapping without saving"""
+
+# Protocol-specific endpoints
+@dashboard_router.get("/protocol-mappings/modbus")
+async def get_modbus_mappings():
+ """Get all Modbus mappings"""
+
+@dashboard_router.get("/protocol-mappings/opcua")
+async def get_opcua_mappings():
+ """Get all OPC UA mappings"""
+```
+
+### Task 3: Build Multi-Protocol Configuration Form UI
+**Priority**: High
+**Estimated Effort**: 3 days
+
+#### Implementation Details:
+```html
+
+
+// Add Protocol Mapping section to dashboard
+function createProtocolMappingSection() {
+ return `
+
+
Protocol Mapping Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+```
+
+### Task 4: Implement Protocol Mapping Grid View
+**Priority**: Medium
+**Estimated Effort**: 2 days
+
+#### Implementation Details:
+```javascript
+// File: static/dashboard.js
+
+function renderMappingGrid(mappings) {
+ const grid = document.getElementById('mapping-grid');
+ grid.innerHTML = `
+
+
+
+ | Protocol |
+ Station |
+ Pump |
+ Data Type |
+ Address |
+ Database Source |
+ Actions |
+
+
+
+ ${mappings.map(mapping => `
+
+ | ${mapping.protocol_type} |
+ ${mapping.station_id} |
+ ${mapping.pump_id} |
+ ${mapping.data_type} |
+ ${mapping.protocol_address} |
+ ${mapping.db_source} |
+
+
+
+ |
+
+ `).join('')}
+
+
+ `;
+}
+```
+
+### Task 5: Add Protocol-Specific Validation Logic
+**Priority**: High
+**Estimated Effort**: 2 days
+
+#### Implementation Details:
+```python
+# File: src/dashboard/configuration_manager.py
+
+class ConfigurationManager:
+ def validate_protocol_mapping(self, mapping: ProtocolMapping) -> Dict[str, Any]:
+ """Validate protocol mapping configuration"""
+ errors = []
+ warnings = []
+
+ # Protocol-specific validation
+ if mapping.protocol_type == 'modbus_tcp':
+ # Modbus validation
+ try:
+ address = int(mapping.protocol_address)
+ if not (0 <= address <= 65535):
+ errors.append("Modbus register address must be between 0 and 65535")
+ except ValueError:
+ errors.append("Modbus address must be a valid integer")
+
+ # Check for address conflicts
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == 'modbus_tcp' and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+
+ elif mapping.protocol_type == 'opcua':
+ # OPC UA validation
+ if not mapping.protocol_address.startswith('ns='):
+ errors.append("OPC UA Node ID must start with 'ns='")
+
+ # Check for node conflicts
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == 'opcua' and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+
+ return {
+ 'valid': len(errors) == 0,
+ 'errors': errors,
+ 'warnings': warnings
+ }
+```
+
+### Task 6: Integrate Configuration Manager with Protocol Servers
+**Priority**: High
+**Estimated Effort**: 2 days
+
+#### Implementation Details:
+```python
+# File: src/protocols/modbus_server.py
+
+class ModbusServer:
+ def __init__(self, setpoint_manager, configuration_manager):
+ self.setpoint_manager = setpoint_manager
+ self.configuration_manager = configuration_manager
+
+ async def _update_registers(self):
+ """Update registers using configured mappings"""
+ modbus_mappings = self.configuration_manager.get_protocol_mappings('modbus_tcp')
+ for mapping in modbus_mappings:
+ try:
+ # Get value from database/setpoint manager
+ value = await self._get_mapped_value(mapping)
+ # Apply transformations
+ transformed_value = self._apply_transformations(value, mapping.transformation_rules)
+ # Write to register
+ self._write_register(mapping.protocol_address, transformed_value, mapping.modbus_config['register_type'])
+ except Exception as e:
+ logger.error(f"Failed to update mapping {mapping.id}: {str(e)}")
+
+# File: src/protocols/opcua_server.py
+
+class OPCUAServer:
+ def __init__(self, configuration_manager):
+ self.configuration_manager = configuration_manager
+
+ async def update_nodes(self):
+ """Update OPC UA nodes using configured mappings"""
+ opcua_mappings = self.configuration_manager.get_protocol_mappings('opcua')
+ for mapping in opcua_mappings:
+ try:
+ # Get value from database/setpoint manager
+ value = await self._get_mapped_value(mapping)
+ # Apply transformations
+ transformed_value = self._apply_transformations(value, mapping.transformation_rules)
+ # Write to node
+ await self._write_node(mapping.protocol_address, transformed_value)
+ except Exception as e:
+ logger.error(f"Failed to update mapping {mapping.id}: {str(e)}")
+```
+
+### Task 7: Create Database Schema for Protocol Mappings
+**Priority**: Medium
+**Estimated Effort**: 1 day
+
+#### Implementation Details:
+```sql
+-- File: database/schema.sql
+
+CREATE TABLE IF NOT EXISTS protocol_mappings (
+ id VARCHAR(50) PRIMARY KEY,
+ protocol_type VARCHAR(20) NOT NULL, -- modbus_tcp, opcua, custom
+ station_id VARCHAR(50) NOT NULL,
+ pump_id VARCHAR(50) NOT NULL,
+ data_type VARCHAR(50) NOT NULL,
+ protocol_address VARCHAR(200) NOT NULL, -- register address or OPC UA node
+ db_source VARCHAR(200) NOT NULL,
+ transformation_rules JSONB,
+
+ -- Protocol-specific configurations
+ modbus_config JSONB,
+ opcua_config JSONB,
+
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (station_id, pump_id) REFERENCES pumps(station_id, pump_id)
+);
+
+CREATE INDEX idx_protocol_mappings_type ON protocol_mappings(protocol_type);
+CREATE INDEX idx_protocol_mappings_station_pump ON protocol_mappings(station_id, pump_id);
+CREATE INDEX idx_protocol_mappings_address ON protocol_mappings(protocol_address);
+```
+
+### Task 8: Add Protocol-Specific Unit Tests
+**Priority**: Medium
+**Estimated Effort**: 1.5 days
+
+#### Implementation Details:
+```python
+# File: tests/unit/test_protocol_mapping.py
+
+class TestProtocolMapping(unittest.TestCase):
+ def test_modbus_address_conflict_detection(self):
+ """Test that Modbus address conflicts are properly detected"""
+ config_manager = ConfigurationManager()
+
+ mapping1 = ProtocolMapping(
+ id="test1", protocol_type="modbus_tcp", station_id="STATION_001", pump_id="PUMP_001",
+ data_type="setpoint", protocol_address="40001", db_source="pump_plans.speed_hz"
+ )
+
+ mapping2 = ProtocolMapping(
+ id="test2", protocol_type="modbus_tcp", station_id="STATION_001", pump_id="PUMP_002",
+ data_type="setpoint", protocol_address="40001", db_source="pump_plans.speed_hz"
+ )
+
+ config_manager.add_protocol_mapping(mapping1)
+ result = config_manager.validate_protocol_mapping(mapping2)
+
+ self.assertFalse(result['valid'])
+ self.assertIn("Modbus address 40001 already used", result['errors'][0])
+
+ def test_opcua_node_validation(self):
+ """Test OPC UA node validation"""
+ config_manager = ConfigurationManager()
+
+ mapping = ProtocolMapping(
+ id="test1", protocol_type="opcua", station_id="STATION_001", pump_id="PUMP_001",
+ data_type="setpoint", protocol_address="invalid_node", db_source="pump_plans.speed_hz"
+ )
+
+ result = config_manager.validate_protocol_mapping(mapping)
+ self.assertFalse(result['valid'])
+ self.assertIn("OPC UA Node ID must start with 'ns='", result['errors'][0])
+```
+
+### Task 9: Add Single Protocol Mapping Tab to Dashboard
+**Priority**: Low
+**Estimated Effort**: 0.5 days
+
+#### Implementation Details:
+```javascript
+// File: static/dashboard.js
+
+// Update tab navigation - Add single Protocol Mapping tab
+function updateNavigation() {
+ const tabButtons = document.querySelector('.tab-buttons');
+ tabButtons.innerHTML += `
+
+ `;
+}
+
+// Add Protocol Mapping tab content
+function addProtocolMappingTab() {
+ const tabContainer = document.querySelector('.tab-container');
+ tabContainer.innerHTML += `
+
+
+
Protocol Mapping Configuration
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+// Protocol switching within the single tab
+function selectProtocol(protocol) {
+ // Update active protocol button
+ document.querySelectorAll('.protocol-btn').forEach(btn => btn.classList.remove('active'));
+ event.target.classList.add('active');
+
+ // Load protocol-specific content
+ loadProtocolMappings(protocol);
+}
+```
+
+### Task 10: Implement Protocol Discovery Features
+**Priority**: Medium
+**Estimated Effort**: 2 days
+
+#### Implementation Details:
+```python
+# File: src/dashboard/api.py
+
+@dashboard_router.post("/protocol-mappings/modbus/discover")
+async def discover_modbus_registers():
+ """Auto-discover available Modbus registers"""
+ try:
+ # Scan for available registers
+ discovered_registers = await modbus_client.scan_registers()
+ return {"success": True, "registers": discovered_registers}
+ except Exception as e:
+ logger.error(f"Failed to discover Modbus registers: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Discovery failed: {str(e)}")
+
+@dashboard_router.post("/protocol-mappings/opcua/browse")
+async def browse_opcua_nodes():
+ """Browse OPC UA server for available nodes"""
+ try:
+ # Browse OPC UA server
+ nodes = await opcua_client.browse_nodes()
+ return {"success": True, "nodes": nodes}
+ except Exception as e:
+ logger.error(f"Failed to browse OPC UA nodes: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Browse failed: {str(e)}")
+```
+
+## π Integration Points
+
+### Existing System Integration
+1. **Configuration Manager**: Extend existing class with unified protocol mapping support
+2. **Protocol Servers**: Inject configuration manager and use configured mappings (Modbus, OPC UA)
+3. **Dashboard API**: Add unified protocol mapping endpoints alongside existing configuration endpoints
+4. **Dashboard UI**: Add single Protocol Mapping tab with protocol switching
+5. **Database**: Add unified table for persistent storage of all protocol mappings
+
+### Data Flow Changes
+```
+Current: Database β Setpoint Manager β Hardcoded Mapping β Protocol Servers
+New: Database β Setpoint Manager β Unified Configurable Mapping β Protocol Servers
+ β
+ Unified Configuration Manager
+```
+
+### Dashboard Integration
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β DASHBOARD NAVIGATION β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β [Status] [Config] [SCADA] [Signals] [Protocol Mapping] [Logs] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+Within Protocol Mapping Tab:
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PROTOCOL MAPPING β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β [Modbus] [OPC UA] [All Protocols] β Protocol Selector β
+β β
+β Unified Mapping Grid & Configuration Forms β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+## π§ͺ Testing Strategy
+
+### Test Scenarios
+1. **Protocol Configuration Validation**: Test address conflicts, data type compatibility across protocols
+2. **Integration Testing**: Test that configured mappings are applied correctly to all protocol servers
+3. **Protocol-Specific Testing**: Test Modbus register mapping and OPC UA node mapping separately
+4. **Performance Testing**: Test impact on protocol server performance
+
+### Test Data
+- Create test mappings for different protocols and scenarios
+- Test edge cases (address boundaries, data type conversions, protocol-specific rules)
+- Test cross-protocol conflict scenarios
+
+## π Success Metrics
+
+### Functional Requirements
+- β
Users can configure database-to-protocol mappings through dashboard
+- β
System uses configured mappings for all supported protocols
+- β
Protocol-specific validation prevents configuration conflicts
+- β
Mappings are persisted across application restarts
+- β
Support for multiple protocols (Modbus, OPC UA) with unified interface
+
+### Performance Requirements
+- β±οΈ Mapping configuration response time < 500ms
+- β±οΈ Protocol server update performance maintained
+- πΎ Memory usage increase < 15MB for typical multi-protocol configurations
+
+## π¨ Risk Mitigation
+
+### Technical Risks
+1. **Performance Impact**: Monitor protocol server update times, optimize if needed
+2. **Configuration Errors**: Implement comprehensive protocol-specific validation
+3. **Protocol Compatibility**: Ensure consistent behavior across different protocols
+
+### Implementation Risks
+1. **Scope Creep**: Stick to Phase 1 requirements only
+2. **Integration Issues**: Test thoroughly with existing protocol servers
+3. **Data Loss**: Implement backup/restore for mapping configurations
+
+## π
Estimated Timeline
+
+**Total Phase 1 Effort**: 18.5 days
+
+| Week | Tasks | Deliverables |
+|------|-------|--------------|
+| 1 | Tasks 1-3 | Configuration manager, API endpoints, multi-protocol UI |
+| 2 | Tasks 4-6 | Grid view, protocol-specific validation, server integration |
+| 3 | Tasks 7-10 | Database schema, tests, navigation, discovery features |
+
+## π― Next Steps After Phase 1
+
+1. **User Testing**: Gather feedback from operators on multi-protocol interface
+2. **Bug Fixing**: Address any issues discovered in production
+3. **Phase 2 Planning**: Begin design for enhanced features (drag & drop, templates, bulk operations)
+
+---
+
+*This implementation plan provides a detailed roadmap for delivering Phase 1 of the Protocol Mapping feature, supporting multiple industrial protocols with a unified interface. Each task includes specific implementation details and integration points with the existing system.*
\ No newline at end of file
diff --git a/docs/PROTOCOL_MAPPING_UI_DESIGN.md b/docs/PROTOCOL_MAPPING_UI_DESIGN.md
new file mode 100644
index 0000000..2019742
--- /dev/null
+++ b/docs/PROTOCOL_MAPPING_UI_DESIGN.md
@@ -0,0 +1,389 @@
+# Protocol Mapping Configuration UI Design
+
+## Overview
+This document outlines the comprehensive UI design for configuring database-to-protocol mappings through the dashboard interface, supporting Modbus, OPC UA, and other industrial protocols.
+
+## π― Design Goals
+- **Intuitive**: Easy for both technical and non-technical users
+- **Visual**: Clear representation of database-to-protocol data flow
+- **Configurable**: Flexible mapping configuration without code changes
+- **Validated**: Real-time conflict detection and validation
+- **Scalable**: Support for multiple stations, pumps, and protocols
+- **Protocol-Agnostic**: Unified interface for Modbus, OPC UA, and other protocols
+
+## ποΈ Architecture
+
+### Data Flow
+```
+Database Sources β Mapping Configuration β Protocol Endpoints
+ β β β
+pump_plans.speed_hz β Setpoint mapping β Modbus: Holding register 40001
+pumps.status_code β Status mapping β OPC UA: ns=2;s=Station.Pump.Status
+safety.flags β Safety mapping β Modbus: Coil register 0
+flow_meters.rate β Flow mapping β OPC UA: ns=2;s=Station.Flow.Rate
+```
+
+### Component Structure
+```javascript
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## π UI Components
+
+### 1. Main Dashboard Layout
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PROTOCOL MAPPING CONFIGURATION β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β [Protocols] [Stations] [Pumps] [Mapping View] [Templates] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### 2. Visual Protocol Mapping View
+
+#### **Layout**:
+```
+βββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββ
+β β β PROTOCOL MAPPING β
+β PUMP LIST β β βββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββ β
+β β β β # β DATA TYPE β DB SOURCE β ADDRESS β β
+β STATION_001 β β βββββΌββββββββββββββΌββββββββββββββΌββββββββββ€ β
+β ββ PUMP_001 β β β 0 β Setpoint β speed_hz β 40001 β β
+β ββ PUMP_002 β β β 1 β Status β status_code β 40002 β β
+β ββ PUMP_003 β β β 2 β Power β power_kw β 40003 β β
+β β β β 3 β Level β level_m β 40004 β β
+β STATION_002 β β β 4 β Flow β flow_m3h β 40005 β β
+β ββ PUMP_004 β β β 5 β Safety β safety_flag β 40006 β β
+β β β βββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββ β
+βββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### 3. Multi-Protocol Configuration Form
+
+#### **Modal/Form Layout**:
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β CONFIGURE PROTOCOL MAPPING β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β Protocol: [Modbus TCP βΌ] [OPC UA βΌ] [Custom Protocol] β
+β β
+β Station: [STATION_001 βΌ] Pump: [PUMP_001 βΌ] β
+β β
+β Data Type: [Setpoint βΌ] Protocol Address: β
+β β
+β MODBUS: [40001] (Holding Register) β
+β OPC UA: [ns=2;s=Station.Pump.Setpoint] β
+β β
+β Database Source: β
+β [β] pump_plans.suggested_speed_hz β
+β [ ] pumps.default_setpoint_hz β
+β [ ] Custom SQL: [___________________________] β
+β β
+β Data Transformation: β
+β [β] Direct value [ ] Scale: [Γ10] [Γ·10] β
+β [ ] Offset: [+___] [ ] Clamp: [min___] [max___] β
+β β
+β Validation: β
No conflicts detected β
+β β
+β [SAVE MAPPING] [TEST MAPPING] [CANCEL] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### 4. Protocol-Specific Address Configuration
+
+#### **Modbus Configuration**:
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β MODBUS ADDRESS CONFIGURATION β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β Register Type: [β Holding β Input β Coil β Discrete] β
+β β
+β Address: [40001] β
+β Size: [1 register] β
+β Data Type: [16-bit integer] β
+β β
+β Byte Order: [Big Endian] [Little Endian] β
+β Word Order: [High Word First] [Low Word First] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+#### **OPC UA Configuration**:
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β OPC UA NODE CONFIGURATION β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β Node ID: [ns=2;s=Station.Pump.Setpoint] β
+β β
+β Namespace: [2] β
+β Browse Name: [Setpoint] β
+β Display Name: [Pump Setpoint] β
+β β
+β Data Type: [Double] [Float] [Int32] [Int16] [Boolean] β
+β Access Level: [CurrentRead] [CurrentWrite] [HistoryRead] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### 5. Drag & Drop Interface
+
+#### **Visual Design**:
+```
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+β DATABASE β β MAPPING β β PROTOCOL β
+β SOURCES β β WORKSPACE β β ENDPOINTS β
+β β β β β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β β pump_plans β β β β Setpoint β β β β Modbus β β
+β β speed_hz ββββββββΆβ speed_hz ββββββββΆβ 40001 β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β β β β β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β β pumps β β β β Status β β β β OPC UA β β
+β β status ββββββββΆβ status_code ββββββββΆβ ns=2;s=... β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β β β β β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+β β safety β β β β Safety β β β β Modbus β β
+β β flags ββββββββΆβ safety_flag ββββββββΆβ Coil 0 β β
+β βββββββββββββββ β β βββββββββββββββ β β βββββββββββββββ β
+βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
+```
+
+### 6. Real-time Preview Panel
+
+#### **Layout**:
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β REAL-TIME PREVIEW β
+βββββββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββββββββ€
+β Database Value β Transform β Protocol β Current Value β
+βββββββββββββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββββββββ€
+β 42.3 Hz β Γ10 β β Modbus 40001β 423 β
+β Running β Direct β OPC UA Node β 1 β
+β 15.2 kW β Direct β Modbus 40003β 15 β
+β 2.1 m β Γ100 β β OPC UA Node β 210 β
+βββββββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββββββββ
+```
+
+### 7. Protocol-Specific Templates
+
+#### **Template Gallery**:
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PROTOCOL TEMPLATES β
+βββββββββββββββββββ¬ββββββββββββββββββ¬ββββββββββββββββββββββββββββββ€
+β Modbus Standard β OPC UA Standard β Custom Template β
+β β β β
+β β’ Holding Regs β β’ Analog Items β β’ Import from file β
+β β’ Input Regs β β’ Digital Items β β’ Export current β
+β β’ Coils β β’ Complex Types β β’ Save as template β
+β β’ Discrete β β’ Methods β β
+β β β β
+β [APPLY] β [APPLY] β [CREATE] β
+βββββββββββββββββββ΄ββββββββββββββββββ΄ββββββββββββββββββββββββββββββ
+```
+
+## π§ Technical Implementation
+
+### Data Models
+```typescript
+interface ProtocolMapping {
+ id: string;
+ protocolType: 'modbus_tcp' | 'opcua' | 'custom';
+ stationId: string;
+ pumpId: string;
+ dataType: 'setpoint' | 'status' | 'power' | 'flow' | 'level' | 'safety';
+ protocolAddress: string; // Register address or OPC UA node
+ dbSource: string;
+ transformation: TransformationRule[];
+
+ // Protocol-specific properties
+ modbusConfig?: {
+ registerType: 'holding' | 'input' | 'coil' | 'discrete';
+ size: number;
+ dataType: 'int16' | 'int32' | 'float' | 'boolean';
+ byteOrder: 'big_endian' | 'little_endian';
+ };
+
+ opcuaConfig?: {
+ namespace: number;
+ browseName: string;
+ displayName: string;
+ dataType: string;
+ accessLevel: string[];
+ };
+}
+
+interface TransformationRule {
+ type: 'scale' | 'offset' | 'clamp' | 'round';
+ parameters: any;
+}
+```
+
+### API Endpoints
+```
+GET /api/v1/dashboard/protocol-mappings
+POST /api/v1/dashboard/protocol-mappings
+PUT /api/v1/dashboard/protocol-mappings/{id}
+DELETE /api/v1/dashboard/protocol-mappings/{id}
+POST /api/v1/dashboard/protocol-mappings/validate
+POST /api/v1/dashboard/protocol-mappings/test
+GET /api/v1/dashboard/protocol-mappings/templates
+POST /api/v1/dashboard/protocol-mappings/import
+GET /api/v1/dashboard/protocol-mappings/export
+
+# Protocol-specific endpoints
+GET /api/v1/dashboard/protocol-mappings/modbus
+GET /api/v1/dashboard/protocol-mappings/opcua
+POST /api/v1/dashboard/protocol-mappings/modbus/discover
+POST /api/v1/dashboard/protocol-mappings/opcua/browse
+```
+
+### Integration Points
+
+#### 1. Configuration Manager Integration
+```python
+class ConfigurationManager:
+ def __init__(self):
+ self.protocol_mappings: List[ProtocolMapping] = []
+
+ def add_protocol_mapping(self, mapping: ProtocolMapping) -> bool:
+ # Validate and add mapping
+ pass
+
+ def get_protocol_mappings(self,
+ protocol_type: str = None,
+ station_id: str = None,
+ pump_id: str = None) -> List[ProtocolMapping]:
+ # Filter mappings by protocol/station/pump
+ pass
+```
+
+#### 2. Protocol Server Integration
+```python
+# Modbus Server Integration
+class ModbusServer:
+ def __init__(self, configuration_manager: ConfigurationManager):
+ self.configuration_manager = configuration_manager
+
+ async def _update_registers(self):
+ modbus_mappings = self.configuration_manager.get_protocol_mappings('modbus_tcp')
+ for mapping in modbus_mappings:
+ value = self._get_database_value(mapping.dbSource)
+ transformed_value = self._apply_transformations(value, mapping.transformation)
+ self._write_register(mapping.protocolAddress, transformed_value, mapping.modbusConfig.registerType)
+
+# OPC UA Server Integration
+class OPCUAServer:
+ def __init__(self, configuration_manager: ConfigurationManager):
+ self.configuration_manager = configuration_manager
+
+ async def update_nodes(self):
+ opcua_mappings = self.configuration_manager.get_protocol_mappings('opcua')
+ for mapping in opcua_mappings:
+ value = self._get_database_value(mapping.dbSource)
+ transformed_value = self._apply_transformations(value, mapping.transformation)
+ await self._write_node(mapping.protocolAddress, transformed_value)
+```
+
+## π¨ Visual Design System
+
+### Color Scheme by Protocol
+- **Modbus**: Blue (#2563eb)
+- **OPC UA**: Green (#16a34a)
+- **Custom Protocols**: Purple (#9333ea)
+- **Success**: Green (#16a34a)
+- **Warning**: Yellow (#d97706)
+- **Error**: Red (#dc2626)
+
+### Icons
+- π Modbus
+- π OPC UA
+- βοΈ Custom Protocol
+- β
Valid mapping
+- β οΈ Warning
+- β Error
+- π Active/live data
+- π Data preview
+
+## π Validation Rules
+
+### Protocol-Specific Validation
+
+#### Modbus Validation:
+- Register addresses: 0-65535
+- Address ranges must not overlap
+- Data type compatibility with register type
+- Valid byte/word order combinations
+
+#### OPC UA Validation:
+- Valid Node ID format
+- Namespace exists and accessible
+- Data type compatibility
+- Access level permissions
+
+### Cross-Protocol Validation
+- Database source must exist and be accessible
+- Transformation rules must be valid
+- No duplicate mappings for same data point
+
+## π Performance Considerations
+
+### Protocol-Specific Optimizations
+- **Modbus**: Batch register writes for efficiency
+- **OPC UA**: Use subscription model for frequent updates
+- **All**: Cache transformed values and mapping configurations
+
+## π Security Considerations
+
+### Protocol Security
+- **Modbus**: Validate register access permissions
+- **OPC UA**: Certificate-based authentication
+- **All**: Role-based access to mapping configuration
+
+## π Implementation Phases
+
+### Phase 1: Core Protocol Mapping
+- Basic mapping configuration for all protocols
+- Protocol-specific address configuration
+- Real-time preview and validation
+- Integration with existing protocol servers
+
+### Phase 2: Enhanced Features
+- Drag & drop interface
+- Protocol templates
+- Bulk operations
+- Advanced transformations
+
+### Phase 3: Advanced Features
+- Protocol discovery and auto-configuration
+- Mobile responsiveness
+- Performance optimizations
+- Advanced security features
+
+## π Testing Strategy
+
+### Protocol-Specific Testing
+- **Modbus**: Register read/write operations, address validation
+- **OPC UA**: Node browsing, data type conversion, security
+- **Cross-Protocol**: Data consistency, transformation accuracy
+
+## π Documentation
+
+### Protocol-Specific Guides
+- Modbus Mapping Configuration Guide
+- OPC UA Node Configuration Guide
+- Custom Protocol Integration Guide
+
+---
+
+*This document provides the comprehensive design for the Protocol Mapping UI, supporting multiple industrial protocols with a unified interface.*
\ No newline at end of file
diff --git a/src/dashboard/api.py b/src/dashboard/api.py
index 38ef219..a1f35eb 100644
--- a/src/dashboard/api.py
+++ b/src/dashboard/api.py
@@ -13,7 +13,7 @@ from pydantic import BaseModel, ValidationError
from config.settings import Settings
from .configuration_manager import (
configuration_manager, OPCUAConfig, ModbusTCPConfig, PumpStationConfig,
- PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType
+ PumpConfig, SafetyLimitsConfig, DataPointMapping, ProtocolType, ProtocolMapping
)
from datetime import datetime
@@ -821,4 +821,212 @@ async def export_signals():
except Exception as e:
logger.error(f"Error exporting signals: {str(e)}")
- raise HTTPException(status_code=500, detail=f"Failed to export signals: {str(e)}")
\ No newline at end of file
+ raise HTTPException(status_code=500, detail=f"Failed to export signals: {str(e)}")
+
+# Protocol Mapping API Endpoints
+
+@dashboard_router.get("/protocol-mappings")
+async def get_protocol_mappings(
+ protocol_type: Optional[str] = None,
+ station_id: Optional[str] = None,
+ pump_id: Optional[str] = None
+):
+ """Get protocol mappings with optional filtering"""
+ try:
+ # Convert protocol_type string to enum if provided
+ protocol_enum = None
+ if protocol_type:
+ try:
+ protocol_enum = ProtocolType(protocol_type)
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid protocol type: {protocol_type}")
+
+ mappings = configuration_manager.get_protocol_mappings(
+ protocol_type=protocol_enum,
+ station_id=station_id,
+ pump_id=pump_id
+ )
+
+ return {
+ "success": True,
+ "mappings": [mapping.dict() for mapping in mappings],
+ "count": len(mappings)
+ }
+ except Exception as e:
+ logger.error(f"Error getting protocol mappings: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to get protocol mappings: {str(e)}")
+
+@dashboard_router.post("/protocol-mappings")
+async def create_protocol_mapping(mapping_data: dict):
+ """Create a new protocol mapping"""
+ try:
+ # Convert protocol_type string to enum
+ if "protocol_type" not in mapping_data:
+ raise HTTPException(status_code=400, detail="protocol_type is required")
+
+ try:
+ protocol_enum = ProtocolType(mapping_data["protocol_type"])
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid protocol type: {mapping_data['protocol_type']}")
+
+ # Create ProtocolMapping object
+ import uuid
+ mapping = ProtocolMapping(
+ id=mapping_data.get("id") or f"{mapping_data.get('protocol_type')}_{mapping_data.get('station_id', 'unknown')}_{mapping_data.get('pump_id', 'unknown')}_{uuid.uuid4().hex[:8]}",
+ protocol_type=protocol_enum,
+ station_id=mapping_data.get("station_id"),
+ pump_id=mapping_data.get("pump_id"),
+ data_type=mapping_data.get("data_type"),
+ protocol_address=mapping_data.get("protocol_address"),
+ db_source=mapping_data.get("db_source"),
+ transformation_rules=mapping_data.get("transformation_rules", []),
+ modbus_config=mapping_data.get("modbus_config"),
+ opcua_config=mapping_data.get("opcua_config")
+ )
+
+ success = configuration_manager.add_protocol_mapping(mapping)
+
+ if success:
+ return {
+ "success": True,
+ "message": "Protocol mapping created successfully",
+ "mapping": mapping.dict()
+ }
+ else:
+ raise HTTPException(status_code=400, detail="Failed to create protocol mapping")
+
+ except ValidationError as e:
+ logger.error(f"Validation error creating protocol mapping: {str(e)}")
+ raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
+ except HTTPException:
+ # Re-raise HTTP exceptions
+ raise
+ except Exception as e:
+ logger.error(f"Error creating protocol mapping: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to create protocol mapping: {str(e)}")
+
+@dashboard_router.put("/protocol-mappings/{mapping_id}")
+async def update_protocol_mapping(mapping_id: str, mapping_data: dict):
+ """Update an existing protocol mapping"""
+ try:
+ # Convert protocol_type string to enum if provided
+ protocol_enum = None
+ if "protocol_type" in mapping_data:
+ try:
+ protocol_enum = ProtocolType(mapping_data["protocol_type"])
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid protocol type: {mapping_data['protocol_type']}")
+
+ # Create updated ProtocolMapping object
+ updated_mapping = ProtocolMapping(
+ id=mapping_id, # Use the ID from URL
+ protocol_type=protocol_enum or ProtocolType(mapping_data.get("protocol_type")),
+ station_id=mapping_data.get("station_id"),
+ pump_id=mapping_data.get("pump_id"),
+ data_type=mapping_data.get("data_type"),
+ protocol_address=mapping_data.get("protocol_address"),
+ db_source=mapping_data.get("db_source"),
+ transformation_rules=mapping_data.get("transformation_rules", []),
+ modbus_config=mapping_data.get("modbus_config"),
+ opcua_config=mapping_data.get("opcua_config")
+ )
+
+ success = configuration_manager.update_protocol_mapping(mapping_id, updated_mapping)
+
+ if success:
+ return {
+ "success": True,
+ "message": "Protocol mapping updated successfully",
+ "mapping": updated_mapping.dict()
+ }
+ else:
+ raise HTTPException(status_code=404, detail=f"Protocol mapping {mapping_id} not found")
+
+ except ValidationError as e:
+ logger.error(f"Validation error updating protocol mapping: {str(e)}")
+ raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error updating protocol mapping: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to update protocol mapping: {str(e)}")
+
+@dashboard_router.delete("/protocol-mappings/{mapping_id}")
+async def delete_protocol_mapping(mapping_id: str):
+ """Delete a protocol mapping"""
+ try:
+ success = configuration_manager.delete_protocol_mapping(mapping_id)
+
+ if success:
+ return {
+ "success": True,
+ "message": f"Protocol mapping {mapping_id} deleted successfully"
+ }
+ else:
+ raise HTTPException(status_code=404, detail=f"Protocol mapping {mapping_id} not found")
+
+ except Exception as e:
+ logger.error(f"Error deleting protocol mapping: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to delete protocol mapping: {str(e)}")
+
+@dashboard_router.post("/protocol-mappings/{mapping_id}/validate")
+async def validate_protocol_mapping(mapping_id: str, mapping_data: dict):
+ """Validate a protocol mapping without saving it"""
+ try:
+ # Convert protocol_type string to enum
+ if "protocol_type" not in mapping_data:
+ raise HTTPException(status_code=400, detail="protocol_type is required")
+
+ try:
+ protocol_enum = ProtocolType(mapping_data["protocol_type"])
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid protocol type: {mapping_data['protocol_type']}")
+
+ # Create temporary ProtocolMapping object for validation
+ temp_mapping = ProtocolMapping(
+ id=mapping_id,
+ protocol_type=protocol_enum,
+ station_id=mapping_data.get("station_id"),
+ pump_id=mapping_data.get("pump_id"),
+ data_type=mapping_data.get("data_type"),
+ protocol_address=mapping_data.get("protocol_address"),
+ db_source=mapping_data.get("db_source"),
+ transformation_rules=mapping_data.get("transformation_rules", []),
+ modbus_config=mapping_data.get("modbus_config"),
+ opcua_config=mapping_data.get("opcua_config")
+ )
+
+ validation_result = configuration_manager.validate_protocol_mapping(temp_mapping)
+
+ return {
+ "success": True,
+ "valid": validation_result["valid"],
+ "errors": validation_result["errors"],
+ "warnings": validation_result["warnings"]
+ }
+
+ except ValidationError as e:
+ logger.error(f"Validation error in protocol mapping: {str(e)}")
+ raise HTTPException(status_code=400, detail=f"Validation error: {str(e)}")
+ except Exception as e:
+ logger.error(f"Error validating protocol mapping: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to validate protocol mapping: {str(e)}")
+
+@dashboard_router.get("/protocol-mappings/protocols")
+async def get_available_protocols():
+ """Get list of available protocol types"""
+ try:
+ protocols = [
+ {
+ "value": protocol.value,
+ "label": protocol.value.replace("_", " ").upper(),
+ "description": f"Configure {protocol.value.replace('_', ' ').title()} mappings"
+ }
+ for protocol in ProtocolType
+ ]
+
+ return {
+ "success": True,
+ "protocols": protocols
+ }
+ except Exception as e:
+ logger.error(f"Error getting available protocols: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Failed to get available protocols: {str(e)}")
\ No newline at end of file
diff --git a/src/dashboard/configuration_manager.py b/src/dashboard/configuration_manager.py
index d24f785..1b94d2f 100644
--- a/src/dashboard/configuration_manager.py
+++ b/src/dashboard/configuration_manager.py
@@ -113,6 +113,52 @@ class DataPointMapping(BaseModel):
protocol_address: str # OPC UA node, Modbus register, etc.
data_type_specific: Dict[str, Any] = {}
+class ProtocolMapping(BaseModel):
+ """Unified protocol mapping configuration for all protocols"""
+ id: str
+ protocol_type: ProtocolType
+ station_id: str
+ pump_id: str
+ data_type: str # setpoint, status, power, flow, level, safety, etc.
+ protocol_address: str # register address or OPC UA node
+ db_source: str # database table and column
+ transformation_rules: List[Dict[str, Any]] = []
+
+ # Protocol-specific configurations
+ modbus_config: Optional[Dict[str, Any]] = None
+ opcua_config: Optional[Dict[str, Any]] = None
+
+ @validator('id')
+ def validate_id(cls, v):
+ if not v.replace('_', '').isalnum():
+ raise ValueError("Mapping ID must be alphanumeric with underscores")
+ return v
+
+ @validator('protocol_address')
+ def validate_protocol_address(cls, v, values):
+ if 'protocol_type' in values:
+ if values['protocol_type'] == ProtocolType.MODBUS_TCP:
+ try:
+ address = int(v)
+ if not (0 <= address <= 65535):
+ raise ValueError("Modbus address must be between 0 and 65535")
+ except ValueError:
+ raise ValueError("Modbus address must be a valid integer")
+ elif values['protocol_type'] == ProtocolType.MODBUS_RTU:
+ try:
+ address = int(v)
+ if not (0 <= address <= 65535):
+ raise ValueError("Modbus RTU address must be between 0 and 65535")
+ except ValueError:
+ raise ValueError("Modbus RTU address must be a valid integer")
+ elif values['protocol_type'] == ProtocolType.OPC_UA:
+ if not v.startswith('ns='):
+ raise ValueError("OPC UA Node ID must start with 'ns='")
+ elif values['protocol_type'] == ProtocolType.REST_API:
+ if not v.startswith(('http://', 'https://')):
+ raise ValueError("REST API endpoint must start with 'http://' or 'https://'")
+ return v
+
class HardwareDiscoveryResult(BaseModel):
"""Result from hardware auto-discovery"""
success: bool
@@ -124,12 +170,55 @@ class HardwareDiscoveryResult(BaseModel):
class ConfigurationManager:
"""Manages comprehensive system configuration through dashboard"""
- def __init__(self):
+ def __init__(self, db_client=None):
self.protocol_configs: Dict[ProtocolType, SCADAProtocolConfig] = {}
self.stations: Dict[str, PumpStationConfig] = {}
self.pumps: Dict[str, PumpConfig] = {}
self.safety_limits: Dict[str, SafetyLimitsConfig] = {}
self.data_mappings: List[DataPointMapping] = []
+ self.protocol_mappings: List[ProtocolMapping] = []
+ self.db_client = db_client
+
+ # Load mappings from database if available
+ if self.db_client:
+ self._load_mappings_from_db()
+
+ def _load_mappings_from_db(self):
+ """Load protocol mappings from database"""
+ try:
+ query = """
+ SELECT mapping_id, station_id, pump_id, protocol_type,
+ protocol_address, data_type, db_source, enabled
+ FROM protocol_mappings
+ WHERE enabled = true
+ ORDER BY station_id, pump_id, protocol_type
+ """
+
+ results = self.db_client.execute_query(query)
+
+ logger.info(f"Database query returned {len(results)} rows")
+
+ for row in results:
+ try:
+ # Convert protocol_type string to enum
+ protocol_type = ProtocolType(row['protocol_type'])
+ mapping = ProtocolMapping(
+ id=row['mapping_id'],
+ station_id=row['station_id'],
+ pump_id=row['pump_id'],
+ protocol_type=protocol_type,
+ protocol_address=row['protocol_address'],
+ data_type=row['data_type'],
+ db_source=row['db_source']
+ )
+ self.protocol_mappings.append(mapping)
+ logger.debug(f"Loaded mapping {row['mapping_id']}: {protocol_type}")
+ except Exception as e:
+ logger.error(f"Failed to create mapping for {row['mapping_id']}: {str(e)}")
+
+ logger.info(f"Loaded {len(self.protocol_mappings)} protocol mappings from database")
+ except Exception as e:
+ logger.error(f"Failed to load protocol mappings from database: {str(e)}")
def configure_protocol(self, config: SCADAProtocolConfig) -> bool:
"""Configure a SCADA protocol"""
@@ -198,6 +287,224 @@ class ConfigurationManager:
logger.error(f"Failed to map data point for {mapping.pump_id}: {str(e)}")
return False
+ def add_protocol_mapping(self, mapping: ProtocolMapping) -> bool:
+ """Add a new protocol mapping with validation"""
+ try:
+ # Validate the mapping
+ validation_result = self.validate_protocol_mapping(mapping)
+ if not validation_result['valid']:
+ raise ValueError(f"Mapping validation failed: {', '.join(validation_result['errors'])}")
+#
+# # Verify pump exists
+# if mapping.pump_id not in self.pumps:
+# raise ValueError(f"Pump {mapping.pump_id} does not exist")
+#
+# # Verify station exists
+# if mapping.station_id not in self.stations:
+# raise ValueError(f"Station {mapping.station_id} does not exist")
+
+ # Save to database if available
+ if self.db_client:
+ query = """
+ INSERT INTO protocol_mappings
+ (mapping_id, station_id, pump_id, protocol_type, protocol_address, data_type, db_source, created_by, enabled)
+ VALUES (:mapping_id, :station_id, :pump_id, :protocol_type, :protocol_address, :data_type, :db_source, :created_by, :enabled)
+ ON CONFLICT (mapping_id) DO UPDATE SET
+ station_id = EXCLUDED.station_id,
+ pump_id = EXCLUDED.pump_id,
+ protocol_type = EXCLUDED.protocol_type,
+ protocol_address = EXCLUDED.protocol_address,
+ data_type = EXCLUDED.data_type,
+ db_source = EXCLUDED.db_source,
+ enabled = EXCLUDED.enabled,
+ updated_at = CURRENT_TIMESTAMP
+ """
+ params = {
+ 'mapping_id': mapping.id,
+ 'station_id': mapping.station_id,
+ 'pump_id': mapping.pump_id,
+ 'protocol_type': mapping.protocol_type.value,
+ 'protocol_address': mapping.protocol_address,
+ 'data_type': mapping.data_type,
+ 'db_source': mapping.db_source,
+ 'created_by': 'dashboard',
+ 'enabled': True
+ }
+ self.db_client.execute(query, params)
+
+ self.protocol_mappings.append(mapping)
+ logger.info(f"Added protocol mapping {mapping.id}: {mapping.protocol_type} for {mapping.station_id}/{mapping.pump_id}")
+ return True
+ except Exception as e:
+ logger.error(f"Failed to add protocol mapping {mapping.id}: {str(e)}")
+ return False
+
+ def get_protocol_mappings(self,
+ protocol_type: Optional[ProtocolType] = None,
+ station_id: Optional[str] = None,
+ pump_id: Optional[str] = None) -> List[ProtocolMapping]:
+ """Get mappings filtered by protocol/station/pump"""
+ filtered_mappings = self.protocol_mappings.copy()
+
+ if protocol_type:
+ filtered_mappings = [m for m in filtered_mappings if m.protocol_type == protocol_type]
+
+ if station_id:
+ filtered_mappings = [m for m in filtered_mappings if m.station_id == station_id]
+
+ if pump_id:
+ filtered_mappings = [m for m in filtered_mappings if m.pump_id == pump_id]
+
+ return filtered_mappings
+
+ def update_protocol_mapping(self, mapping_id: str, updated_mapping: ProtocolMapping) -> bool:
+ """Update an existing protocol mapping"""
+ try:
+ # Find the mapping to update
+ for i, mapping in enumerate(self.protocol_mappings):
+ if mapping.id == mapping_id:
+ # Validate the updated mapping (exclude current mapping from conflict check)
+ validation_result = self.validate_protocol_mapping(updated_mapping, exclude_mapping_id=mapping_id)
+ if not validation_result['valid']:
+ raise ValueError(f"Mapping validation failed: {', '.join(validation_result['errors'])}")
+
+ # Update in database if available
+ if self.db_client:
+ query = """
+ UPDATE protocol_mappings
+ SET station_id = :station_id,
+ pump_id = :pump_id,
+ protocol_type = :protocol_type,
+ protocol_address = :protocol_address,
+ data_type = :data_type,
+ db_source = :db_source,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE mapping_id = :mapping_id
+ """
+ params = {
+ 'mapping_id': mapping_id,
+ 'station_id': updated_mapping.station_id,
+ 'pump_id': updated_mapping.pump_id,
+ 'protocol_type': updated_mapping.protocol_type.value,
+ 'protocol_address': updated_mapping.protocol_address,
+ 'data_type': updated_mapping.data_type,
+ 'db_source': updated_mapping.db_source
+ }
+ self.db_client.execute(query, params)
+
+ self.protocol_mappings[i] = updated_mapping
+ logger.info(f"Updated protocol mapping {mapping_id}")
+ return True
+
+ raise ValueError(f"Protocol mapping {mapping_id} not found")
+ except Exception as e:
+ logger.error(f"Failed to update protocol mapping {mapping_id}: {str(e)}")
+ return False
+
+ def delete_protocol_mapping(self, mapping_id: str) -> bool:
+ """Delete a protocol mapping"""
+ try:
+ initial_count = len(self.protocol_mappings)
+ self.protocol_mappings = [m for m in self.protocol_mappings if m.id != mapping_id]
+
+ if len(self.protocol_mappings) < initial_count:
+ # Delete from database if available
+ if self.db_client:
+ query = "DELETE FROM protocol_mappings WHERE mapping_id = :mapping_id"
+ self.db_client.execute(query, {'mapping_id': mapping_id})
+
+ logger.info(f"Deleted protocol mapping {mapping_id}")
+ return True
+ else:
+ raise ValueError(f"Protocol mapping {mapping_id} not found")
+ except Exception as e:
+ logger.error(f"Failed to delete protocol mapping {mapping_id}: {str(e)}")
+ return False
+
+ def validate_protocol_mapping(self, mapping: ProtocolMapping, exclude_mapping_id: Optional[str] = None) -> Dict[str, Any]:
+ """Validate protocol mapping for conflicts and protocol-specific rules"""
+ errors = []
+ warnings = []
+
+ # Check for ID conflicts (exclude current mapping when updating)
+ for existing in self.protocol_mappings:
+ if existing.id == mapping.id and existing.id != exclude_mapping_id:
+ errors.append(f"Mapping ID '{mapping.id}' already exists")
+ break
+
+ # Protocol-specific validation
+ if mapping.protocol_type == ProtocolType.MODBUS_TCP:
+ # Modbus validation
+ try:
+ address = int(mapping.protocol_address)
+ if not (0 <= address <= 65535):
+ errors.append("Modbus address must be between 0 and 65535")
+
+ # Check for address conflicts within same protocol
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == ProtocolType.MODBUS_TCP and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"Modbus address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+ break
+
+ except ValueError:
+ errors.append("Modbus address must be a valid integer")
+
+ elif mapping.protocol_type == ProtocolType.OPC_UA:
+ # OPC UA validation
+ if not mapping.protocol_address.startswith('ns='):
+ errors.append("OPC UA Node ID must start with 'ns='")
+
+ # Check for node conflicts within same protocol
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == ProtocolType.OPC_UA and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"OPC UA node {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+ break
+
+ elif mapping.protocol_type == ProtocolType.MODBUS_RTU:
+ # Modbus RTU validation (same as Modbus TCP)
+ try:
+ address = int(mapping.protocol_address)
+ if not (0 <= address <= 65535):
+ errors.append("Modbus RTU address must be between 0 and 65535")
+
+ # Check for address conflicts within same protocol
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == ProtocolType.MODBUS_RTU and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"Modbus RTU address {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+ break
+
+ except ValueError:
+ errors.append("Modbus RTU address must be a valid integer")
+
+ elif mapping.protocol_type == ProtocolType.REST_API:
+ # REST API validation
+ if not mapping.protocol_address.startswith(('http://', 'https://')):
+ errors.append("REST API endpoint must start with 'http://' or 'https://'")
+
+ # Check for endpoint conflicts within same protocol
+ for existing in self.protocol_mappings:
+ if (existing.id != mapping.id and
+ existing.protocol_type == ProtocolType.REST_API and
+ existing.protocol_address == mapping.protocol_address):
+ errors.append(f"REST API endpoint {mapping.protocol_address} already used by {existing.station_id}/{existing.pump_id}")
+ break
+
+ # Check database source format
+ if '.' not in mapping.db_source:
+ warnings.append("Database source should be in format 'table.column'")
+
+ return {
+ 'valid': len(errors) == 0,
+ 'errors': errors,
+ 'warnings': warnings
+ }
+
def auto_discover_hardware(self) -> HardwareDiscoveryResult:
"""Auto-discover connected hardware and SCADA systems"""
result = HardwareDiscoveryResult(success=True)
@@ -266,18 +573,30 @@ class ConfigurationManager:
if not self.data_mappings:
validation_result["warnings"].append("No data point mappings configured")
+ # Check protocol mappings
+ if not self.protocol_mappings:
+ validation_result["warnings"].append("No protocol mappings configured")
+
# Check safety limits
pumps_without_limits = set(self.pumps.keys()) - set(limit.pump_id for limit in self.safety_limits.values())
if pumps_without_limits:
validation_result["warnings"].append(f"Pumps without safety limits: {', '.join(pumps_without_limits)}")
+ # Validate individual protocol mappings
+ for mapping in self.protocol_mappings:
+ mapping_validation = self.validate_protocol_mapping(mapping)
+ if not mapping_validation['valid']:
+ validation_result['errors'].extend([f"Mapping {mapping.id}: {error}" for error in mapping_validation['errors']])
+ validation_result['warnings'].extend([f"Mapping {mapping.id}: {warning}" for warning in mapping_validation['warnings']])
+
# Create summary
validation_result["summary"] = {
"protocols_configured": len(self.protocol_configs),
"stations_configured": len(self.stations),
"pumps_configured": len(self.pumps),
"safety_limits_set": len(self.safety_limits),
- "data_mappings": len(self.data_mappings)
+ "data_mappings": len(self.data_mappings),
+ "protocol_mappings": len(self.protocol_mappings)
}
return validation_result
@@ -289,7 +608,8 @@ class ConfigurationManager:
"stations": {sid: station.dict() for sid, station in self.stations.items()},
"pumps": {pid: pump.dict() for pid, pump in self.pumps.items()},
"safety_limits": {key: limits.dict() for key, limits in self.safety_limits.items()},
- "data_mappings": [mapping.dict() for mapping in self.data_mappings]
+ "data_mappings": [mapping.dict() for mapping in self.data_mappings],
+ "protocol_mappings": [mapping.dict() for mapping in self.protocol_mappings]
}
def import_configuration(self, config_data: Dict[str, Any]) -> bool:
@@ -301,6 +621,7 @@ class ConfigurationManager:
self.pumps.clear()
self.safety_limits.clear()
self.data_mappings.clear()
+ self.protocol_mappings.clear()
# Import protocols
for pt_str, config_dict in config_data.get("protocols", {}).items():
@@ -333,6 +654,11 @@ class ConfigurationManager:
mapping = DataPointMapping(**mapping_dict)
self.data_mappings.append(mapping)
+ # Import protocol mappings
+ for mapping_dict in config_data.get("protocol_mappings", []):
+ mapping = ProtocolMapping(**mapping_dict)
+ self.protocol_mappings.append(mapping)
+
logger.info("Configuration imported successfully")
return True
diff --git a/src/dashboard/templates.py b/src/dashboard/templates.py
index 823cb88..ddb118c 100644
--- a/src/dashboard/templates.py
+++ b/src/dashboard/templates.py
@@ -140,6 +140,71 @@ DASHBOARD_HTML = """
margin-top: 20px;
text-align: center;
}
+
+ /* Protocol Selector Styles */
+ .protocol-selector {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+ }
+
+ .protocol-btn {
+ padding: 8px 16px;
+ background: #f8f9fa;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: normal;
+ }
+
+ .protocol-btn.active {
+ background: #007acc;
+ color: white;
+ border-color: #007acc;
+ font-weight: bold;
+ }
+
+ .protocol-btn:hover {
+ background: #e9ecef;
+ }
+
+ .protocol-btn.active:hover {
+ background: #005a9e;
+ }
+
+ /* Modal Styles */
+ .modal {
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0,0,0,0.5);
+ }
+
+ .modal-content {
+ background-color: white;
+ margin: 10% auto;
+ padding: 20px;
+ border-radius: 8px;
+ width: 80%;
+ max-width: 600px;
+ position: relative;
+ }
+
+ .close {
+ color: #aaa;
+ float: right;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+ }
+
+ .close:hover {
+ color: black;
+ }
.logs-container {
max-height: 400px;
overflow-y: auto;
@@ -178,6 +243,7 @@ DASHBOARD_HTML = """
+
@@ -433,6 +499,113 @@ DASHBOARD_HTML = """
+
+
+
Protocol Mapping Configuration
+
+
+
+
+
Protocol Selection
+
+
+
+
+
+
+
+
+
+
+
+
Protocol Mappings
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Protocol |
+ Station |
+ Pump |
+ Data Type |
+ Protocol Address |
+ Database Source |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
Add Protocol Mapping
+
+
+
+
+
System Actions
@@ -456,6 +629,7 @@ DASHBOARD_HTML = """
+