1206 lines
48 KiB
Python
1206 lines
48 KiB
Python
"""
|
|
DTS controller for watermaker control operations with single state management.
|
|
"""
|
|
|
|
import time
|
|
import threading
|
|
from datetime import datetime
|
|
from flask import Blueprint, jsonify
|
|
from ..services.plc_connection import get_plc_connection
|
|
from ..services.register_writer import RegisterWriter
|
|
from ..services.data_cache import get_data_cache
|
|
from ..services.operation_state import get_operation_state_manager
|
|
from ..utils.logger import get_logger
|
|
from ..utils.error_handler import create_error_response, create_success_response, DTSOperationError
|
|
from ..models.timer_mappings import (
|
|
calculate_timer_progress_percent,
|
|
get_timer_for_dts_mode,
|
|
get_timer_expected_start_value,
|
|
get_dts_flow_sequence,
|
|
get_dts_screen_info,
|
|
get_current_dts_screen_name,
|
|
get_next_screen_in_flow,
|
|
is_screen_skippable,
|
|
is_mode_in_dts_flow
|
|
)
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
# Create blueprint
|
|
dts_bp = Blueprint('dts', __name__)
|
|
|
|
# Initialize services
|
|
plc = get_plc_connection()
|
|
writer = RegisterWriter()
|
|
cache = get_data_cache()
|
|
|
|
|
|
def get_timer_based_progress(mode: int) -> int:
|
|
"""
|
|
Get progress percentage based on the current DTS step timer.
|
|
|
|
Args:
|
|
mode: Current DTS mode (R1000 value)
|
|
|
|
Returns:
|
|
Progress percentage (0-100) based on timer countdown
|
|
"""
|
|
try:
|
|
# Get the timer register for this mode
|
|
timer_address = get_timer_for_dts_mode(mode)
|
|
if not timer_address:
|
|
return 0
|
|
|
|
# Read current timer value
|
|
current_timer_value = plc.read_holding_register(timer_address)
|
|
if current_timer_value is None:
|
|
return 0
|
|
|
|
# Check for invalid timer values (65535 indicates timer not active)
|
|
if current_timer_value == 65535:
|
|
logger.debug(f"Timer R{timer_address} shows max value (65535) - timer not active for mode {mode}")
|
|
return 0
|
|
|
|
# Calculate progress based on timer countdown
|
|
progress = calculate_timer_progress_percent(timer_address, current_timer_value)
|
|
return progress
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get timer-based progress for mode {mode}: {e}")
|
|
return 0
|
|
|
|
|
|
def update_dts_progress_from_timers():
|
|
"""
|
|
Update progress for running DTS operation based on current timer values.
|
|
This should be called periodically to keep progress updated.
|
|
"""
|
|
try:
|
|
state_manager = get_operation_state_manager()
|
|
|
|
if not state_manager.is_running():
|
|
return
|
|
|
|
if not plc.connect():
|
|
return
|
|
|
|
current_state = state_manager.get_current_state()
|
|
|
|
# Read current system mode
|
|
current_mode = plc.read_holding_register(1000)
|
|
if current_mode is None:
|
|
return
|
|
|
|
# Update progress based on current mode and timers
|
|
updates = {"current_mode": current_mode}
|
|
|
|
# Map current mode to DTS screen (based on actual DTS flow)
|
|
mode_to_screen = {
|
|
34: "dts_requested_active", # DTS Requested
|
|
5: "dts_priming_active", # Priming Screen
|
|
6: "dts_init_active", # Init Screen
|
|
7: "dts_production_active", # Production Screen
|
|
8: "dts_flush_active" # Fresh Water Flush Screen
|
|
}
|
|
|
|
# Store previous mode for comparison
|
|
previous_mode = current_state.get("current_mode")
|
|
|
|
# Check if we've moved to a new screen
|
|
expected_screen = mode_to_screen.get(current_mode)
|
|
if expected_screen and current_state["current_step"] != expected_screen:
|
|
screen_name = get_current_dts_screen_name(current_mode)
|
|
|
|
# Check if this change might be external
|
|
external_changes = current_state.get("external_changes", [])
|
|
recent_external = any(
|
|
change.get("new_value") == current_mode
|
|
for change in external_changes[-3:] # Check last 3 changes
|
|
)
|
|
|
|
if recent_external:
|
|
logger.warning(f"DTS Process: Advanced to {screen_name} Screen (mode {current_mode}) - EXTERNAL CHANGE DETECTED")
|
|
updates["external_step_changes"] = current_state.get("external_step_changes", 0) + 1
|
|
else:
|
|
logger.info(f"DTS Process: Advanced to {screen_name} Screen (mode {current_mode})")
|
|
|
|
updates["current_step"] = expected_screen
|
|
|
|
# Add to steps completed
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": f"advanced_to_mode_{current_mode}",
|
|
"mode": current_mode,
|
|
"screen_name": screen_name,
|
|
"previous_mode": previous_mode,
|
|
"external_change": recent_external,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
updates["steps_completed"] = steps_completed
|
|
|
|
# Get timer-based progress for current mode
|
|
timer_address = get_timer_for_dts_mode(current_mode)
|
|
|
|
if timer_address:
|
|
# There's a timer for this mode
|
|
current_timer_value = plc.read_holding_register(timer_address)
|
|
timer_progress = get_timer_based_progress(current_mode)
|
|
|
|
# Enhanced timer info with raw values for debugging
|
|
updates["timer_info"] = {
|
|
"current_mode": current_mode,
|
|
"timer_address": timer_address,
|
|
"timer_progress": timer_progress,
|
|
"raw_timer_value": current_timer_value,
|
|
"timer_active": current_timer_value is not None and current_timer_value != 65535,
|
|
"last_updated": datetime.now().isoformat()
|
|
}
|
|
|
|
# Set progress based on timer status
|
|
if current_timer_value == 65535 or current_timer_value is None:
|
|
# Timer not active - this might indicate the step is transitioning
|
|
updates["progress_percent"] = 0
|
|
logger.debug(f"DTS Process: Timer R{timer_address} not active (value: {current_timer_value}) for mode {current_mode}")
|
|
else:
|
|
updates["progress_percent"] = timer_progress
|
|
|
|
else:
|
|
# No timer for this mode (e.g., production phase)
|
|
updates["progress_percent"] = 100 # Production is "complete" in terms of setup
|
|
updates["timer_info"] = {
|
|
"current_mode": current_mode,
|
|
"timer_address": None,
|
|
"timer_progress": None,
|
|
"production_mode": True,
|
|
"last_updated": datetime.now().isoformat()
|
|
}
|
|
|
|
# Check if DTS process is complete (back to standby mode)
|
|
if current_mode == 2: # Standby mode
|
|
# Check if this was an external stop
|
|
external_changes = current_state.get("external_changes", [])
|
|
recent_external_stop = any(
|
|
change.get("new_value") == 2 and "Process_Stop" in change.get("change_type", "")
|
|
for change in external_changes[-2:] # Check last 2 changes
|
|
)
|
|
|
|
if recent_external_stop:
|
|
logger.warning("DTS Process: EXTERNALLY STOPPED - system returned to standby mode")
|
|
updates["note"] = "DTS process stopped externally via HMI - system in standby mode"
|
|
updates["external_stop"] = True
|
|
updates["current_step"] = "dts_process_complete"
|
|
updates["step_description"] = "DTS process stopped externally - system in standby mode"
|
|
else:
|
|
logger.info("DTS Process: Completed - system returned to standby mode")
|
|
updates["note"] = "DTS process completed successfully - system in standby mode"
|
|
updates["current_step"] = "dts_process_complete"
|
|
updates["step_description"] = "DTS process completed successfully - system in standby mode"
|
|
|
|
# Update the state with current mode and step before completing
|
|
updates["progress_percent"] = 100
|
|
state_manager.update_state(updates)
|
|
|
|
# Complete the operation
|
|
state_manager.complete_operation(success=True)
|
|
return
|
|
|
|
# Apply all updates
|
|
state_manager.update_state(updates)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to update DTS progress from timers: {e}")
|
|
|
|
|
|
def handle_external_dts_change(change_info):
|
|
"""
|
|
Handle externally-initiated DTS process changes.
|
|
This is called when R1000 changes to a DTS mode without an existing API operation.
|
|
"""
|
|
try:
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Determine initial step based on the new R1000 value
|
|
new_mode = change_info.get("new_value")
|
|
mode_to_screen = {
|
|
34: "dts_requested_active", # DTS Requested
|
|
5: "dts_priming_active", # Priming Screen
|
|
6: "dts_init_active", # Init Screen
|
|
7: "dts_production_active", # Production Screen
|
|
8: "dts_flush_active" # Fresh Water Flush Screen
|
|
}
|
|
|
|
initial_step = mode_to_screen.get(new_mode, f"dts_mode_{new_mode}")
|
|
|
|
# If no operation running, start external monitoring
|
|
if state_manager.is_idle():
|
|
success, message, details = state_manager.start_operation("external_monitoring", "external")
|
|
if success:
|
|
screen_descriptions = {
|
|
"dts_requested_active": "DTS Requested - press and hold DTS to START",
|
|
"dts_priming_active": "DTS Priming - flush with shore pressure",
|
|
"dts_init_active": "DTS Init - high pressure pump initialization",
|
|
"dts_production_active": "DTS Production - water flowing to tank",
|
|
"dts_flush_active": "DTS Fresh Water Flush - process ending",
|
|
"dts_process_complete": "DTS process completed successfully"
|
|
}
|
|
|
|
# Add initial step to completed steps
|
|
screen_name = get_current_dts_screen_name(new_mode)
|
|
steps_completed = [{
|
|
"step": "external_dts_detected",
|
|
"mode": new_mode,
|
|
"screen_name": screen_name,
|
|
"external_origin": True,
|
|
"initiating_change": change_info,
|
|
"timestamp": datetime.now().isoformat()
|
|
}]
|
|
|
|
state_manager.update_state({
|
|
"current_step": initial_step,
|
|
"current_mode": new_mode,
|
|
"external_changes": [change_info],
|
|
"steps_completed": steps_completed,
|
|
"screen_descriptions": screen_descriptions,
|
|
"note": f"External DTS process detected - monitoring mode {new_mode}"
|
|
})
|
|
|
|
logger.info(f"External DTS monitoring started for mode {new_mode} ({screen_name})")
|
|
return details["operation_id"]
|
|
else:
|
|
# Add to existing operation's external changes
|
|
current_state = state_manager.get_current_state()
|
|
external_changes = current_state.get("external_changes", [])
|
|
external_changes.append(change_info)
|
|
state_manager.update_state({"external_changes": external_changes})
|
|
logger.info(f"Added external change to existing operation: mode {new_mode}")
|
|
return current_state.get("operation_id")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to handle external DTS change: {e}")
|
|
return None
|
|
|
|
|
|
def execute_dts_sequence():
|
|
"""
|
|
Execute the DTS sequence in background thread.
|
|
Updates operation state as it progresses.
|
|
"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
try:
|
|
if not plc.connect():
|
|
raise DTSOperationError("Failed to connect to PLC")
|
|
|
|
# Initialize screen descriptions
|
|
screen_descriptions = {
|
|
"checking_system_mode": "Checking current system mode",
|
|
"setting_prep_mode": "Setting preparation mode",
|
|
"setting_r71_256": "Activating valve sequence",
|
|
"setting_r71_0": "Completing valve command",
|
|
"waiting_for_valves": "Waiting for valve positioning",
|
|
"starting_dts_mode": "Starting DTS operation",
|
|
"dts_requested_active": "DTS Requested - press and hold DTS to START",
|
|
"dts_priming_active": "DTS Priming - flush with shore pressure",
|
|
"dts_init_active": "DTS Init - high pressure pump initialization",
|
|
"dts_production_active": "DTS Production - water flowing to tank",
|
|
"dts_flush_active": "DTS Fresh Water Flush - process ending",
|
|
"dts_process_complete": "DTS process completed successfully",
|
|
"completed": "DTS sequence completed",
|
|
"failed": "DTS sequence failed"
|
|
}
|
|
|
|
state_manager.update_state({
|
|
"screen_descriptions": screen_descriptions,
|
|
"progress_percent": 0
|
|
})
|
|
|
|
# Step 1: Check value of R1000
|
|
state_manager.update_state({
|
|
"current_step": "checking_system_mode",
|
|
"progress_percent": 5
|
|
})
|
|
logger.info("DTS Start: Checking system mode (R1000)")
|
|
|
|
current_mode = plc.read_holding_register(1000)
|
|
if current_mode is None:
|
|
raise DTSOperationError("Failed to read system mode register R1000")
|
|
|
|
logger.info(f"DTS Start: Current system mode = {current_mode}")
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "read_r1000",
|
|
"value": current_mode,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 15})
|
|
|
|
# Step 2: If not 34, set R1000=34
|
|
if current_mode != 34:
|
|
state_manager.update_state({
|
|
"current_step": "setting_prep_mode",
|
|
"progress_percent": 25
|
|
})
|
|
logger.info("DTS Start: Setting system mode to 34 (prep mode)")
|
|
|
|
if not writer.write_holding_register(1000, 34):
|
|
raise DTSOperationError("Failed to write R1000=34")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_34",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
# Wait 2 seconds
|
|
logger.info("DTS Start: Waiting 2 seconds after mode change")
|
|
time.sleep(2)
|
|
else:
|
|
logger.info("DTS Start: System already in prep mode (34)")
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "r1000_already_34",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 25})
|
|
|
|
# Step 3: Set R71=256
|
|
state_manager.update_state({
|
|
"current_step": "setting_r71_256",
|
|
"progress_percent": 35
|
|
})
|
|
logger.info("DTS Start: Setting R71=256")
|
|
|
|
if not writer.write_holding_register(71, 256):
|
|
raise DTSOperationError("Failed to write R71=256")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_256",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
# Wait 2 seconds
|
|
logger.info("DTS Start: Waiting 2 seconds after R71=256")
|
|
time.sleep(2)
|
|
|
|
# Step 4: Set R71=0
|
|
state_manager.update_state({
|
|
"current_step": "setting_r71_0",
|
|
"progress_percent": 45
|
|
})
|
|
logger.info("DTS Start: Setting R71=0")
|
|
|
|
if not writer.write_holding_register(71, 0):
|
|
raise DTSOperationError("Failed to write R71=0")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_0",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
# Step 5: Wait for R138 to count down (valves moving into position)
|
|
state_manager.update_state({
|
|
"current_step": "waiting_for_valves",
|
|
"progress_percent": 55
|
|
})
|
|
logger.info("DTS Start: Waiting for valve positioning (monitoring R138)")
|
|
|
|
# Monitor R138 for up to 15 seconds, but don't fail if it doesn't reach 0
|
|
valve_timeout = time.time() + 15
|
|
r138_initial = None
|
|
valve_positioning_complete = False
|
|
|
|
while time.time() < valve_timeout:
|
|
r138_value = plc.read_holding_register(138)
|
|
if r138_value is None:
|
|
logger.warning("Failed to read R138 during valve wait")
|
|
time.sleep(0.5)
|
|
continue
|
|
|
|
if r138_initial is None:
|
|
r138_initial = r138_value
|
|
logger.info(f"DTS Start: R138 initial value = {r138_value}")
|
|
|
|
# Update progress based on R138 timer countdown
|
|
if r138_initial and r138_initial > 0:
|
|
timer_progress = calculate_timer_progress_percent(138, r138_value, r138_initial)
|
|
# Scale timer progress to fit in 55-85% range
|
|
scaled_progress = 55 + (timer_progress * 0.3)
|
|
state_manager.update_state({"progress_percent": int(scaled_progress)})
|
|
logger.debug(f"DTS Start: R138 timer progress = {timer_progress}% (current: {r138_value}, initial: {r138_initial})")
|
|
|
|
# When R138 reaches 0, valves are positioned
|
|
if r138_value == 0:
|
|
logger.info("DTS Start: Valve positioning complete (R138 = 0)")
|
|
valve_positioning_complete = True
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "valves_positioned",
|
|
"r138_initial": r138_initial,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 85})
|
|
break
|
|
|
|
time.sleep(0.2) # Check every 200ms
|
|
|
|
# If timeout occurred, log it but continue (PLC may handle valve positioning independently)
|
|
if not valve_positioning_complete:
|
|
logger.warning("DTS Start: R138 did not reach 0 within timeout, but continuing with DTS start")
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "valve_timeout_continue",
|
|
"r138_initial": r138_initial,
|
|
"r138_final": r138_value if 'r138_value' in locals() else None,
|
|
"note": "Valve positioning timeout - continuing with DTS start",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 85})
|
|
|
|
# Step 6: Set R1000=5 (start DTS operation)
|
|
state_manager.update_state({
|
|
"current_step": "starting_dts_mode",
|
|
"progress_percent": 90
|
|
})
|
|
logger.info("DTS Start: Setting R1000=5 (starting DTS mode)")
|
|
|
|
if not writer.write_holding_register(1000, 5):
|
|
raise DTSOperationError("Failed to write R1000=5")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_5",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
|
|
# DTS sequence initialization completed - now transition to timer-based monitoring
|
|
state_manager.update_state({
|
|
"current_step": "dts_priming_active", # Start with priming screen (mode 5)
|
|
"progress_percent": 0, # Reset for actual DTS process
|
|
"steps_completed": steps_completed,
|
|
"note": "DTS initialization complete. Now monitoring DTS process screens via timers.",
|
|
"initialization_complete": True,
|
|
"dts_process_start_time": datetime.now().isoformat()
|
|
})
|
|
|
|
logger.info("DTS Start: Initialization completed successfully - now monitoring DTS process")
|
|
|
|
except Exception as e:
|
|
# Error occurred during sequence
|
|
error_msg = str(e)
|
|
logger.error(f"DTS Start: Error during sequence: {error_msg}")
|
|
|
|
state_manager.complete_operation(success=False, error_msg=error_msg)
|
|
cache.add_error(f"DTS Start Failed: {error_msg}")
|
|
|
|
|
|
def execute_stop_sequence():
|
|
"""
|
|
Execute the watermaker stop sequence in background thread.
|
|
Stop sequence varies based on current system mode (R1000).
|
|
"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
try:
|
|
if not plc.connect():
|
|
raise DTSOperationError("Failed to connect to PLC")
|
|
|
|
# Initialize screen descriptions
|
|
screen_descriptions = {
|
|
"reading_system_mode": "Reading current system mode",
|
|
"stopping_mode_5": "Stopping from Priming screen (5)",
|
|
"stopping_mode_7": "Stopping from Production screen (7)",
|
|
"stopping_mode_8_flush": "Stopping from Fresh Water Flush screen (8)",
|
|
"completed": "Stop sequence completed",
|
|
"failed": "Stop sequence failed"
|
|
}
|
|
|
|
state_manager.update_state({
|
|
"screen_descriptions": screen_descriptions,
|
|
"progress_percent": 5
|
|
})
|
|
|
|
# Step 1: Read current system mode
|
|
state_manager.update_state({
|
|
"current_step": "reading_system_mode",
|
|
"progress_percent": 10
|
|
})
|
|
logger.info("Stop: Reading current system mode (R1000)")
|
|
|
|
current_mode = plc.read_holding_register(1000)
|
|
if current_mode is None:
|
|
raise DTSOperationError("Failed to read system mode register R1000")
|
|
|
|
logger.info(f"Stop: Current system mode = {current_mode}")
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "read_current_mode",
|
|
"value": current_mode,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 20})
|
|
|
|
# Step 2: Execute appropriate stop sequence based on mode
|
|
if current_mode == 7:
|
|
# Mode 7 stop sequence
|
|
state_manager.update_state({
|
|
"current_step": "stopping_mode_7",
|
|
"progress_percent": 30
|
|
})
|
|
logger.info("Stop: Executing mode 7 stop sequence (R71=513)")
|
|
|
|
if not writer.write_holding_register(71, 513):
|
|
raise DTSOperationError("Failed to write R71=513")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_513",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
time.sleep(1)
|
|
state_manager.update_state({"progress_percent": 60})
|
|
|
|
if not writer.write_holding_register(71, 0):
|
|
raise DTSOperationError("Failed to write R71=0 after 513")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_0_after_513",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 80})
|
|
|
|
if not writer.write_holding_register(1000, 8):
|
|
raise DTSOperationError("Failed to write R1000=8")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_8",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
elif current_mode == 5:
|
|
# Mode 5 stop sequence
|
|
state_manager.update_state({
|
|
"current_step": "stopping_mode_5",
|
|
"progress_percent": 30
|
|
})
|
|
logger.info("Stop: Executing mode 5 stop sequence (R71=512)")
|
|
|
|
if not writer.write_holding_register(71, 512):
|
|
raise DTSOperationError("Failed to write R71=512")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_512",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
time.sleep(1)
|
|
state_manager.update_state({"progress_percent": 60})
|
|
|
|
if not writer.write_holding_register(71, 0):
|
|
raise DTSOperationError("Failed to write R71=0 after 512")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_0_after_512",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 80})
|
|
|
|
if not writer.write_holding_register(1000, 8):
|
|
raise DTSOperationError("Failed to write R1000=8")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_8",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
elif current_mode == 8:
|
|
# Mode 8 stop sequence (flush screen)
|
|
state_manager.update_state({
|
|
"current_step": "stopping_mode_8_flush",
|
|
"progress_percent": 30
|
|
})
|
|
logger.info("Stop: Executing mode 8 flush stop sequence (R71=1024)")
|
|
|
|
if not writer.write_holding_register(71, 1024):
|
|
raise DTSOperationError("Failed to write R71=1024")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_1024",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
time.sleep(1)
|
|
state_manager.update_state({"progress_percent": 60})
|
|
|
|
if not writer.write_holding_register(71, 0):
|
|
raise DTSOperationError("Failed to write R71=0 after 1024")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r71_0_after_1024",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 80})
|
|
|
|
if not writer.write_holding_register(1000, 2):
|
|
raise DTSOperationError("Failed to write R1000=2")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_2",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
else:
|
|
raise DTSOperationError(f"Cannot stop from current mode {current_mode}. Valid modes: 5, 7, 8")
|
|
|
|
# Sequence completed successfully
|
|
state_manager.update_state({
|
|
"current_step": "completed",
|
|
"progress_percent": 100
|
|
})
|
|
state_manager.complete_operation(success=True)
|
|
|
|
logger.info(f"Stop: Sequence completed successfully from mode {current_mode}")
|
|
|
|
except Exception as e:
|
|
# Error occurred during sequence
|
|
error_msg = str(e)
|
|
logger.error(f"Stop: Error during sequence: {error_msg}")
|
|
|
|
state_manager.complete_operation(success=False, error_msg=error_msg)
|
|
cache.add_error(f"Stop Failed: {error_msg}")
|
|
|
|
|
|
def execute_skip_sequence():
|
|
"""
|
|
Execute step skip sequence in background thread.
|
|
Automatically determines next step based on current mode.
|
|
"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
try:
|
|
if not plc.connect():
|
|
raise DTSOperationError("Failed to connect to PLC")
|
|
|
|
# Initialize screen descriptions
|
|
screen_descriptions = {
|
|
"reading_system_mode": "Reading current system mode",
|
|
"skipping_priming": "Skipping Priming screen (mode 5 → Production)",
|
|
"skipping_init": "Skipping Init screen (mode 6 → Production)",
|
|
"completed": "Skip sequence completed",
|
|
"failed": "Skip sequence failed"
|
|
}
|
|
|
|
state_manager.update_state({
|
|
"screen_descriptions": screen_descriptions,
|
|
"progress_percent": 5
|
|
})
|
|
|
|
# Step 1: Read current system mode
|
|
state_manager.update_state({
|
|
"current_step": "reading_system_mode",
|
|
"progress_percent": 10
|
|
})
|
|
logger.info("Skip: Reading current system mode (R1000)")
|
|
|
|
current_mode = plc.read_holding_register(1000)
|
|
if current_mode is None:
|
|
raise DTSOperationError("Failed to read system mode register R1000")
|
|
|
|
logger.info(f"Skip: Current system mode = {current_mode}")
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "read_current_mode",
|
|
"value": current_mode,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 20})
|
|
|
|
# Step 2: Execute appropriate skip sequence
|
|
if current_mode == 5:
|
|
# Skip Priming screen: Mode 5 -> Production (Mode 7)
|
|
target_mode = 7
|
|
state_manager.update_state({
|
|
"current_step": "skipping_priming",
|
|
"progress_percent": 40,
|
|
"target_mode": target_mode
|
|
})
|
|
logger.info("Skip: Skipping Priming screen from mode 5 to Production (R67=32841)")
|
|
|
|
if not writer.write_holding_register(67, 32841):
|
|
raise DTSOperationError("Failed to write R67=32841")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r67_32841",
|
|
"from_mode": 5,
|
|
"from_screen": "Priming",
|
|
"to_mode": target_mode,
|
|
"to_screen": "Production",
|
|
"note": "PLC will advance to R1000=6 then 7",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 80})
|
|
|
|
elif current_mode == 6:
|
|
# Skip Init screen: Mode 6 -> Production (Mode 7)
|
|
target_mode = 7
|
|
state_manager.update_state({
|
|
"current_step": "skipping_init",
|
|
"progress_percent": 40,
|
|
"target_mode": target_mode
|
|
})
|
|
logger.info("Skip: Skipping Init screen from mode 6 to Production (R67=32968)")
|
|
|
|
if not writer.write_holding_register(67, 32968):
|
|
raise DTSOperationError("Failed to write R67=32968")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r67_32968",
|
|
"from_mode": 6,
|
|
"from_screen": "Init",
|
|
"to_mode": target_mode,
|
|
"to_screen": "Production",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed})
|
|
|
|
time.sleep(1)
|
|
state_manager.update_state({"progress_percent": 70})
|
|
|
|
if not writer.write_holding_register(1000, 7):
|
|
raise DTSOperationError("Failed to write R1000=7")
|
|
|
|
current_state = state_manager.get_current_state()
|
|
steps_completed = current_state.get("steps_completed", [])
|
|
steps_completed.append({
|
|
"step": "write_r1000_7",
|
|
"from_mode": 6,
|
|
"from_screen": "Init",
|
|
"to_mode": target_mode,
|
|
"to_screen": "Production",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
state_manager.update_state({"steps_completed": steps_completed, "progress_percent": 80})
|
|
|
|
else:
|
|
raise DTSOperationError(f"Cannot skip from current mode {current_mode}. Skip only available from modes 5 (Priming) or 6 (Init)")
|
|
|
|
# Sequence completed successfully
|
|
state_manager.update_state({
|
|
"current_step": "completed",
|
|
"progress_percent": 100
|
|
})
|
|
state_manager.complete_operation(success=True)
|
|
|
|
target_screen_name = get_current_dts_screen_name(target_mode)
|
|
logger.info(f"Skip: Sequence completed successfully from mode {current_mode} to {target_screen_name} screen (mode {target_mode})")
|
|
|
|
except Exception as e:
|
|
# Error occurred during sequence
|
|
error_msg = str(e)
|
|
logger.error(f"Skip: Error during sequence: {error_msg}")
|
|
|
|
state_manager.complete_operation(success=False, error_msg=error_msg)
|
|
cache.add_error(f"Skip Failed: {error_msg}")
|
|
|
|
|
|
def start_dts_sequence_async():
|
|
"""Start DTS sequence asynchronously"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Attempt to start operation
|
|
success, message, details = state_manager.start_operation("dts_start", "api")
|
|
if not success:
|
|
return False, message, details
|
|
|
|
# Start background thread
|
|
thread = threading.Thread(target=execute_dts_sequence, daemon=True)
|
|
thread.start()
|
|
|
|
logger.info(f"DTS Start: Started async operation with ID {details['operation_id']}")
|
|
return True, message, details
|
|
|
|
|
|
def start_stop_sequence_async():
|
|
"""Start watermaker stop sequence asynchronously"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Attempt to start operation
|
|
success, message, details = state_manager.start_operation("dts_stop", "api")
|
|
if not success:
|
|
return False, message, details
|
|
|
|
# Start background thread
|
|
thread = threading.Thread(target=execute_stop_sequence, daemon=True)
|
|
thread.start()
|
|
|
|
logger.info(f"Stop: Started async operation with ID {details['operation_id']}")
|
|
return True, message, details
|
|
|
|
|
|
def start_skip_sequence_async():
|
|
"""Start step skip sequence asynchronously"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Attempt to start operation
|
|
success, message, details = state_manager.start_operation("dts_skip", "api")
|
|
if not success:
|
|
return False, message, details
|
|
|
|
# Start background thread
|
|
thread = threading.Thread(target=execute_skip_sequence, daemon=True)
|
|
thread.start()
|
|
|
|
logger.info(f"Skip: Started async operation with ID {details['operation_id']}")
|
|
return True, message, details
|
|
|
|
|
|
# DTS Control Endpoints
|
|
|
|
@dts_bp.route('/dts/start', methods=['POST'])
|
|
def start_dts():
|
|
"""Start DTS watermaker sequence asynchronously"""
|
|
success, message, details = start_dts_sequence_async()
|
|
|
|
if success:
|
|
return create_success_response(
|
|
message,
|
|
{
|
|
"operation_id": details["operation_id"],
|
|
"status_endpoint": "/api/dts/status",
|
|
"polling_info": {
|
|
"recommended_interval": "1 second",
|
|
"check_status_at": "/api/dts/status"
|
|
}
|
|
},
|
|
202 # 202 Accepted (async operation started)
|
|
)
|
|
else:
|
|
return create_error_response(
|
|
"Conflict",
|
|
message,
|
|
409, # 409 Conflict (operation already running)
|
|
details
|
|
)
|
|
|
|
|
|
@dts_bp.route('/dts/stop', methods=['POST'])
|
|
def stop_watermaker():
|
|
"""Stop watermaker sequence (mode-dependent)"""
|
|
success, message, details = start_stop_sequence_async()
|
|
|
|
if success:
|
|
return create_success_response(
|
|
message,
|
|
{
|
|
"operation_id": details["operation_id"],
|
|
"status_endpoint": "/api/dts/status",
|
|
"polling_info": {
|
|
"recommended_interval": "1 second",
|
|
"check_status_at": "/api/dts/status"
|
|
},
|
|
"note": "Stop sequence varies by current mode (5, 7, or 8)"
|
|
},
|
|
202 # 202 Accepted (async operation started)
|
|
)
|
|
else:
|
|
return create_error_response(
|
|
"Conflict",
|
|
message,
|
|
409, # 409 Conflict (operation already running)
|
|
details
|
|
)
|
|
|
|
|
|
@dts_bp.route('/dts/skip', methods=['POST'])
|
|
def skip_step():
|
|
"""Skip current step automatically (determines next step based on current mode)"""
|
|
success, message, details = start_skip_sequence_async()
|
|
|
|
if success:
|
|
return create_success_response(
|
|
message,
|
|
{
|
|
"operation_id": details["operation_id"],
|
|
"status_endpoint": "/api/dts/status",
|
|
"polling_info": {
|
|
"recommended_interval": "1 second",
|
|
"check_status_at": "/api/dts/status"
|
|
},
|
|
"note": "Auto-skip: Mode 5 (Priming) → Mode 7 (Production), Mode 6 (Init) → Mode 7 (Production)",
|
|
"valid_from_modes": [5, 6]
|
|
},
|
|
202 # 202 Accepted (async operation started)
|
|
)
|
|
else:
|
|
return create_error_response(
|
|
"Conflict",
|
|
message,
|
|
409, # 409 Conflict (operation already running)
|
|
details
|
|
)
|
|
|
|
|
|
@dts_bp.route('/dts/status')
|
|
def get_dts_status():
|
|
"""Get current DTS operation status"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Update progress from timers for running operations
|
|
if state_manager.is_running():
|
|
update_dts_progress_from_timers()
|
|
|
|
current_state = state_manager.get_current_state()
|
|
|
|
# Add user-friendly descriptions
|
|
descriptions = current_state.get("screen_descriptions", {})
|
|
current_state["screen_description"] = descriptions.get(
|
|
current_state["current_step"],
|
|
current_state["current_step"]
|
|
)
|
|
current_state["step_description"] = current_state["screen_description"] # Backward compatibility
|
|
current_state["is_complete"] = current_state["status"] in ["completed", "failed", "cancelled"]
|
|
current_state["is_running"] = current_state["status"] == "running"
|
|
|
|
# Add timer-based progress info if available
|
|
current_state["timer_based_progress"] = "timer_info" in current_state
|
|
|
|
return jsonify({
|
|
"operation": current_state,
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
|
|
|
|
# Backward compatibility endpoint
|
|
@dts_bp.route('/dts/status/<task_id>')
|
|
def get_dts_status_legacy(task_id):
|
|
"""Legacy endpoint for backward compatibility"""
|
|
# Always return current state regardless of task_id
|
|
return get_dts_status()
|
|
|
|
|
|
@dts_bp.route('/dts/cancel', methods=['POST'])
|
|
def cancel_dts_operation():
|
|
"""Cancel the current running DTS operation (if possible)"""
|
|
state_manager = get_operation_state_manager()
|
|
|
|
if not state_manager.is_running():
|
|
return create_error_response(
|
|
"Bad Request",
|
|
"No operation is currently running",
|
|
400
|
|
)
|
|
|
|
# Cancel the operation
|
|
success = state_manager.cancel_operation()
|
|
if success:
|
|
return create_success_response(
|
|
"Operation cancelled successfully",
|
|
{"note": "Background operation may continue briefly"}
|
|
)
|
|
else:
|
|
return create_error_response(
|
|
"Internal Error",
|
|
"Failed to cancel operation",
|
|
500
|
|
)
|
|
|
|
|
|
# Backward compatibility endpoint
|
|
@dts_bp.route('/dts/cancel/<task_id>', methods=['POST'])
|
|
def cancel_dts_task_legacy(task_id):
|
|
"""Legacy cancel endpoint for backward compatibility"""
|
|
# Always try to cancel current operation regardless of task_id
|
|
return cancel_dts_operation()
|
|
|
|
|
|
@dts_bp.route('/dts/current-step-progress')
|
|
def get_current_step_progress():
|
|
"""Get current DTS step progress based on timer values"""
|
|
try:
|
|
if not plc.connect():
|
|
return create_error_response(
|
|
"Connection Error",
|
|
"Failed to connect to PLC",
|
|
503
|
|
)
|
|
|
|
# Read current system mode
|
|
current_mode = plc.read_holding_register(1000)
|
|
if current_mode is None:
|
|
return create_error_response(
|
|
"Read Error",
|
|
"Failed to read system mode register R1000",
|
|
503
|
|
)
|
|
|
|
# Get timer information for current mode
|
|
timer_address = get_timer_for_dts_mode(current_mode)
|
|
if not timer_address:
|
|
return jsonify({
|
|
"current_mode": current_mode,
|
|
"timer_based_progress": False,
|
|
"message": f"No timer mapping for mode {current_mode}",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
|
|
# Read current timer value
|
|
current_timer_value = plc.read_holding_register(timer_address)
|
|
if current_timer_value is None:
|
|
return create_error_response(
|
|
"Read Error",
|
|
f"Failed to read timer register R{timer_address}",
|
|
503
|
|
)
|
|
|
|
# Calculate progress
|
|
progress_percent = calculate_timer_progress_percent(timer_address, current_timer_value)
|
|
expected_start = get_timer_expected_start_value(timer_address)
|
|
|
|
# Get timer info from mappings
|
|
from ..models.timer_mappings import get_timer_info
|
|
timer_info = get_timer_info(timer_address)
|
|
|
|
return jsonify({
|
|
"current_mode": current_mode,
|
|
"timer_address": timer_address,
|
|
"timer_name": timer_info.get("name", f"Timer R{timer_address}"),
|
|
"current_timer_value": current_timer_value,
|
|
"expected_start_value": expected_start,
|
|
"progress_percent": progress_percent,
|
|
"timer_based_progress": True,
|
|
"scale": timer_info.get("scale", "direct"),
|
|
"unit": timer_info.get("unit", ""),
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting current step progress: {e}")
|
|
return create_error_response(
|
|
"Internal Error",
|
|
f"Failed to get current step progress: {str(e)}",
|
|
500
|
|
)
|
|
|
|
|
|
@dts_bp.route('/dts/r1000-monitor')
|
|
def get_r1000_monitor_status():
|
|
"""Get R1000 monitoring status and recent changes"""
|
|
try:
|
|
from ..services.background_tasks import get_task_manager
|
|
|
|
task_manager = get_task_manager()
|
|
r1000_monitor = task_manager.r1000_monitor
|
|
state_manager = get_operation_state_manager()
|
|
|
|
# Get current R1000 value
|
|
current_r1000 = r1000_monitor.get_current_r1000()
|
|
last_change_time = r1000_monitor.get_last_change_time()
|
|
|
|
# Get recent errors that might be R1000 changes
|
|
recent_errors = cache.get_errors(limit=20)
|
|
r1000_changes = [
|
|
error for error in recent_errors
|
|
if "R1000 External Change" in error.get("error", "")
|
|
]
|
|
|
|
# Get current operation status
|
|
current_state = state_manager.get_current_state()
|
|
is_running = state_manager.is_running()
|
|
|
|
# Build operation info for compatibility
|
|
operation_info = None
|
|
if is_running:
|
|
operation_info = {
|
|
"operation_id": current_state.get("operation_id"),
|
|
"status": current_state["status"],
|
|
"current_step": current_state["current_step"],
|
|
"initiated_by": current_state.get("initiated_by"),
|
|
"external_changes": current_state.get("external_changes", []),
|
|
"start_time": current_state.get("start_time"),
|
|
"note": current_state.get("note", "")
|
|
}
|
|
|
|
# Determine if current operation is API or externally initiated
|
|
api_initiated = operation_info and operation_info.get("initiated_by") == "api"
|
|
externally_initiated = operation_info and operation_info.get("initiated_by") == "external"
|
|
|
|
return jsonify({
|
|
"r1000_monitor": {
|
|
"current_value": current_r1000,
|
|
"last_change_time": last_change_time.isoformat() if last_change_time else None,
|
|
"monitoring_active": True
|
|
},
|
|
"recent_changes": r1000_changes[-10:], # Last 10 changes
|
|
"current_operation": operation_info,
|
|
"operation_status": {
|
|
"is_running": is_running,
|
|
"api_initiated": api_initiated,
|
|
"externally_initiated": externally_initiated
|
|
},
|
|
# Legacy compatibility fields
|
|
"running_tasks": {
|
|
"total": 1 if is_running else 0,
|
|
"api_initiated": [operation_info] if api_initiated else [],
|
|
"externally_initiated": [operation_info] if externally_initiated else []
|
|
},
|
|
"affected_tasks": [operation_info] if operation_info else [],
|
|
"change_classifications": {
|
|
"Process_Start": "System starting DTS process (2 → 5 or 34)",
|
|
"Process_Stop": "System stopping DTS process (any → 2)",
|
|
"Step_Skip": "Skipping DTS step (5/6 → 7)",
|
|
"Step_Advance": "Advancing DTS step (5/6/7 → 8)",
|
|
"DTS_Start": "DTS process beginning (34 → 5)",
|
|
"Mode_Change": "Other mode transitions"
|
|
},
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting R1000 monitor status: {e}")
|
|
return create_error_response(
|
|
"Internal Error",
|
|
f"Failed to get R1000 monitor status: {str(e)}",
|
|
500
|
|
) |