Files
FCI_WaterMaker_API/watermaker_plc_api/controllers/dts_controller.py
2025-06-12 17:31:36 +00:00

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
)