You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
997 lines
38 KiB
C#
997 lines
38 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Haoliang.Core.Services;
|
|
using Haoliang.Models.Device;
|
|
using Haoliang.Models.System;
|
|
using Haoliang.Models.Common;
|
|
|
|
namespace Haoliang.Core.Services
|
|
{
|
|
public interface IDeviceStateMachine
|
|
{
|
|
/// <summary>
|
|
/// Get current state of device
|
|
/// </summary>
|
|
DeviceState GetCurrentState(int deviceId);
|
|
|
|
/// <summary>
|
|
/// Check if device can transition to target state
|
|
/// </summary>
|
|
Task<bool> CanTransitionAsync(int deviceId, DeviceState targetState);
|
|
|
|
/// <summary>
|
|
/// Transition device to new state
|
|
/// </summary>
|
|
Task<DeviceStateTransitionResult> TransitionToStateAsync(int deviceId, DeviceState targetState, object context = null);
|
|
|
|
/// <summary>
|
|
/// Trigger device event
|
|
/// </summary>
|
|
Task<DeviceStateTransitionResult> TriggerEventAsync(int deviceId, DeviceEvent deviceEvent, object context = null);
|
|
|
|
/// <summary>
|
|
/// Get device state history
|
|
/// </summary>
|
|
Task<List<DeviceStateHistory>> GetStateHistoryAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null);
|
|
|
|
/// <summary>
|
|
/// Get device state statistics
|
|
/// </summary>
|
|
Task<DeviceStateStatistics> GetStateStatisticsAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null);
|
|
|
|
/// <summary>
|
|
/// Force device to specific state
|
|
/// </summary>
|
|
Task<DeviceStateTransitionResult> ForceStateAsync(int deviceId, DeviceState targetState, string reason);
|
|
|
|
/// <summary>
|
|
/// Validate device state
|
|
/// </summary>
|
|
Task<DeviceValidationResult> ValidateStateAsync(int deviceId);
|
|
|
|
/// <summary>
|
|
/// Register state change handler
|
|
/// </summary>
|
|
void RegisterStateHandler(StateChangeHandler handler);
|
|
|
|
/// <summary>
|
|
/// Unregister state change handler
|
|
/// </summary>
|
|
void UnregisterStateHandler(StateChangeHandler handler);
|
|
}
|
|
|
|
public class DeviceStateMachine : IDeviceStateMachine, IHostedService
|
|
{
|
|
private readonly IDeviceRepository _deviceRepository;
|
|
private readonly IDeviceCollectionService _collectionService;
|
|
private readonly IAlarmService _alarmService;
|
|
private readonly ICacheService _cacheService;
|
|
private readonly Timer _stateCheckTimer;
|
|
private readonly ConcurrentDictionary<int, DeviceStateContext> _deviceStates = new ConcurrentDictionary<int, DeviceStateContext>();
|
|
private readonly List<StateChangeHandler> _stateHandlers = new List<StateChangeHandler>();
|
|
private readonly object _lock = new object();
|
|
|
|
public DeviceStateMachine(
|
|
IDeviceRepository deviceRepository,
|
|
IDeviceCollectionService collectionService,
|
|
IAlarmService alarmService,
|
|
ICacheService cacheService)
|
|
{
|
|
_deviceRepository = deviceRepository;
|
|
_collectionService = collectionService;
|
|
_alarmService = alarmService;
|
|
_cacheService = cacheService;
|
|
|
|
// Start timer for periodic state checks
|
|
_stateCheckTimer = new Timer(CheckDeviceStates, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
|
}
|
|
|
|
public DeviceState GetCurrentState(int deviceId)
|
|
{
|
|
if (_deviceStates.TryGetValue(deviceId, out var context))
|
|
{
|
|
return context.CurrentState;
|
|
}
|
|
return DeviceState.Unknown;
|
|
}
|
|
|
|
public async Task<bool> CanTransitionAsync(int deviceId, DeviceState targetState)
|
|
{
|
|
var currentState = GetCurrentState(deviceId);
|
|
return await CanTransitionFromAsync(currentState, targetState, deviceId);
|
|
}
|
|
|
|
public async Task<DeviceStateTransitionResult> TransitionToStateAsync(int deviceId, DeviceState targetState, object context = null)
|
|
{
|
|
var result = new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = GetCurrentState(deviceId),
|
|
ToState = targetState,
|
|
Success = false,
|
|
Message = "",
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
try
|
|
{
|
|
// Validate transition
|
|
if (!await CanTransitionAsync(deviceId, targetState))
|
|
{
|
|
result.Message = $"Cannot transition from {result.FromState} to {targetState}";
|
|
return result;
|
|
}
|
|
|
|
// Get device
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
if (device == null)
|
|
{
|
|
result.Message = "Device not found";
|
|
return result;
|
|
}
|
|
|
|
// Create transition context
|
|
var transitionContext = new DeviceStateTransitionContext
|
|
{
|
|
DeviceId = deviceId,
|
|
Device = device,
|
|
FromState = result.FromState,
|
|
ToState = targetState,
|
|
Context = context,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
// Execute exit actions for current state
|
|
var exitResult = await ExecuteStateActionsAsync(deviceId, result.FromState, transitionContext, true);
|
|
if (!exitResult.Success)
|
|
{
|
|
result.Message = $"Failed to exit {result.FromState}: {exitResult.Message}";
|
|
return result;
|
|
}
|
|
|
|
// Execute enter actions for target state
|
|
var enterResult = await ExecuteStateActionsAsync(deviceId, targetState, transitionContext, false);
|
|
if (!enterResult.Success)
|
|
{
|
|
result.Message = $"Failed to enter {targetState}: {enterResult.Message}";
|
|
// Attempt to revert to original state
|
|
await ExecuteStateActionsAsync(deviceId, result.FromState, transitionContext, true);
|
|
return result;
|
|
}
|
|
|
|
// Update device state
|
|
lock (_lock)
|
|
{
|
|
var deviceContext = _deviceStates.GetOrAdd(deviceId, new DeviceStateContext
|
|
{
|
|
CurrentState = targetState,
|
|
PreviousState = result.FromState,
|
|
StateChangedAt = DateTime.UtcNow,
|
|
Context = context
|
|
});
|
|
|
|
deviceContext.CurrentState = targetState;
|
|
deviceContext.PreviousState = result.FromState;
|
|
deviceContext.StateChangedAt = DateTime.UtcNow;
|
|
deviceContext.Context = context;
|
|
}
|
|
|
|
// Record state change in history
|
|
await RecordStateChangeAsync(deviceId, result.FromState, targetState, context);
|
|
|
|
// Notify handlers
|
|
await NotifyStateHandlersAsync(deviceId, result.FromState, targetState, transitionContext);
|
|
|
|
// Update device status
|
|
await UpdateDeviceStatusAsync(device, targetState);
|
|
|
|
result.Success = true;
|
|
result.Message = $"Successfully transitioned from {result.FromState} to {targetState}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.Message = $"Error during state transition: {ex.Message}";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<DeviceStateTransitionResult> TriggerEventAsync(int deviceId, DeviceEvent deviceEvent, object context = null)
|
|
{
|
|
var currentState = GetCurrentState(deviceId);
|
|
var eventConfig = GetEventConfiguration(deviceEvent, currentState);
|
|
|
|
if (eventConfig == null)
|
|
{
|
|
return new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = currentState,
|
|
ToState = currentState,
|
|
Success = false,
|
|
Message = $"No event handler configured for {deviceEvent} in state {currentState}",
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
// Check event conditions
|
|
if (!await EvaluateEventConditionsAsync(deviceId, deviceEvent, eventConfig.Conditions))
|
|
{
|
|
return new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = currentState,
|
|
ToState = currentState,
|
|
Success = false,
|
|
Message = $"Event conditions not met for {deviceEvent}",
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
// Transition to target state
|
|
return await TransitionToStateAsync(deviceId, eventConfig.TargetState, context);
|
|
}
|
|
|
|
public async Task<List<DeviceStateHistory>> GetStateHistoryAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null)
|
|
{
|
|
var history = await _deviceRepository.GetDeviceStateHistoryAsync(deviceId, startDate, endDate);
|
|
|
|
// Add current state if not in history
|
|
if (!history.Any(h => h.State == GetCurrentState(deviceId)))
|
|
{
|
|
history.Add(new DeviceStateHistory
|
|
{
|
|
DeviceId = deviceId,
|
|
State = GetCurrentState(deviceId),
|
|
ChangedAt = DateTime.UtcNow,
|
|
ChangedBy = "System",
|
|
Notes = "Current state"
|
|
});
|
|
}
|
|
|
|
return history.OrderBy(h => h.ChangedAt).ToList();
|
|
}
|
|
|
|
public async Task<DeviceStateStatistics> GetStateStatisticsAsync(int deviceId, DateTime? startDate = null, DateTime? endDate = null)
|
|
{
|
|
var history = await GetStateHistoryAsync(deviceId, startDate, endDate);
|
|
var statistics = new DeviceStateStatistics
|
|
{
|
|
DeviceId = deviceId,
|
|
PeriodStart = startDate ?? DateTime.UtcNow.AddDays(-7),
|
|
PeriodEnd = endDate ?? DateTime.UtcNow,
|
|
StateTransitions = history.Count - 1, // Excluding initial state
|
|
StateDurations = new Dictionary<DeviceState, TimeSpan>(),
|
|
StateCounts = new Dictionary<DeviceState, int>(),
|
|
LastStateChange = history.LastOrDefault()?.ChangedAt
|
|
};
|
|
|
|
// Calculate state durations
|
|
for (int i = 0; i < history.Count - 1; i++)
|
|
{
|
|
var fromState = history[i].State;
|
|
var toState = history[i + 1].State;
|
|
var duration = history[i + 1].ChangedAt - history[i].ChangedAt;
|
|
|
|
if (!statistics.StateDurations.ContainsKey(fromState))
|
|
{
|
|
statistics.StateDurations[fromState] = TimeSpan.Zero;
|
|
}
|
|
statistics.StateDurations[fromState] += duration;
|
|
|
|
if (!statistics.StateCounts.ContainsKey(fromState))
|
|
{
|
|
statistics.StateCounts[fromState] = 0;
|
|
}
|
|
statistics.StateCounts[fromState]++;
|
|
}
|
|
|
|
// Add current state duration
|
|
if (history.Any())
|
|
{
|
|
var currentState = GetCurrentState(deviceId);
|
|
var lastState = history.Last();
|
|
var currentDuration = DateTime.UtcNow - lastState.ChangedAt;
|
|
|
|
if (!statistics.StateDurations.ContainsKey(currentState))
|
|
{
|
|
statistics.StateDurations[currentState] = TimeSpan.Zero;
|
|
}
|
|
statistics.StateDurations[currentState] += currentDuration;
|
|
|
|
if (!statistics.StateCounts.ContainsKey(currentState))
|
|
{
|
|
statistics.StateCounts[currentState] = 0;
|
|
}
|
|
statistics.StateCounts[currentState]++;
|
|
}
|
|
|
|
return statistics;
|
|
}
|
|
|
|
public async Task<DeviceStateTransitionResult> ForceStateAsync(int deviceId, DeviceState targetState, string reason)
|
|
{
|
|
// Create force transition context
|
|
var forceContext = new { Forced = true, Reason = reason };
|
|
|
|
var result = await TransitionToStateAsync(deviceId, targetState, forceContext);
|
|
|
|
if (!result.Success)
|
|
{
|
|
// If normal transition fails, create a force entry in history
|
|
await RecordStateChangeAsync(deviceId, GetCurrentState(deviceId), targetState, forceContext, true);
|
|
|
|
// Update in-memory state
|
|
lock (_lock)
|
|
{
|
|
var deviceContext = _deviceStates.GetOrAdd(deviceId, new DeviceStateContext());
|
|
deviceContext.CurrentState = targetState;
|
|
deviceContext.PreviousState = GetCurrentState(deviceId);
|
|
deviceContext.StateChangedAt = DateTime.UtcNow;
|
|
deviceContext.Context = forceContext;
|
|
}
|
|
|
|
result.Success = true;
|
|
result.Message = $"Forced state transition to {targetState}: {reason}";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<DeviceValidationResult> ValidateStateAsync(int deviceId)
|
|
{
|
|
var result = new DeviceValidationResult
|
|
{
|
|
DeviceId = deviceId,
|
|
IsValid = true,
|
|
Issues = new List<string>(),
|
|
CurrentState = GetCurrentState(deviceId),
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
try
|
|
{
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
if (device == null)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add("Device not found");
|
|
return result;
|
|
}
|
|
|
|
// Check if device state matches actual status
|
|
var actualStatus = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
var expectedState = TranslateStatusToDeviceState(actualStatus);
|
|
|
|
if (expectedState != result.CurrentState)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add($"State mismatch: expected {expectedState}, actual {result.CurrentState}");
|
|
}
|
|
|
|
// Validate state-specific rules
|
|
var stateValidation = await ValidateStateRulesAsync(deviceId, result.CurrentState);
|
|
if (!stateValidation.IsValid)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.AddRange(stateValidation.Issues);
|
|
}
|
|
|
|
// Check for state timeouts
|
|
var stateTimeout = await CheckStateTimeoutAsync(deviceId, result.CurrentState);
|
|
if (stateTimeout.IsTimeout)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add($"State timeout: {result.CurrentState} exceeded maximum duration");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add($"Validation error: {ex.Message}");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public void RegisterStateHandler(StateChangeHandler handler)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_stateHandlers.Add(handler);
|
|
}
|
|
}
|
|
|
|
public void UnregisterStateHandler(StateChangeHandler handler)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_stateHandlers.Remove(handler);
|
|
}
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
// Initialize device states
|
|
return InitializeDeviceStatesAsync();
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_stateCheckTimer?.Dispose();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
#region Private Methods
|
|
|
|
private async Task InitializeDeviceStatesAsync()
|
|
{
|
|
var devices = await _deviceRepository.GetAllDevicesAsync();
|
|
|
|
foreach (var device in devices)
|
|
{
|
|
var stateContext = new DeviceStateContext
|
|
{
|
|
CurrentState = DeviceState.Unknown,
|
|
PreviousState = DeviceState.Unknown,
|
|
StateChangedAt = DateTime.UtcNow,
|
|
Context = null
|
|
};
|
|
|
|
// Get current device status to determine state
|
|
var status = await _collectionService.GetDeviceCurrentStatusAsync(device.Id);
|
|
stateContext.CurrentState = TranslateStatusToDeviceState(status);
|
|
|
|
_deviceStates.AddOrUpdate(device.Id, stateContext, (key, existing) => stateContext);
|
|
}
|
|
}
|
|
|
|
private async Task CheckDeviceStates(object state)
|
|
{
|
|
try
|
|
{
|
|
var devices = await _deviceRepository.GetAllActiveDevicesAsync();
|
|
|
|
foreach (var device in devices)
|
|
{
|
|
var validationResult = await ValidateStateAsync(device.Id);
|
|
if (!validationResult.IsValid)
|
|
{
|
|
// Handle invalid state
|
|
await HandleInvalidStateAsync(device.Id, validationResult);
|
|
}
|
|
|
|
// Check for state timeouts
|
|
var stateTimeout = await CheckStateTimeoutAsync(device.Id, GetCurrentState(device.Id));
|
|
if (stateTimeout.IsTimeout)
|
|
{
|
|
await HandleStateTimeoutAsync(device.Id, stateTimeout);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error
|
|
Console.WriteLine($"Error checking device states: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task<DeviceStateTransitionResult> ExecuteStateActionsAsync(
|
|
int deviceId,
|
|
DeviceState state,
|
|
DeviceStateTransitionContext context,
|
|
bool isExit)
|
|
{
|
|
var actions = isExit ? GetExitActions(state) : GetEnterActions(state);
|
|
|
|
foreach (var action in actions)
|
|
{
|
|
try
|
|
{
|
|
var result = await action.ExecuteAsync(deviceId, context);
|
|
if (!result.Success)
|
|
{
|
|
return new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = context.FromState,
|
|
ToState = context.ToState,
|
|
Success = false,
|
|
Message = result.Message,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = context.FromState,
|
|
ToState = context.ToState,
|
|
Success = false,
|
|
Message = $"Error executing action: {ex.Message}",
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
}
|
|
|
|
return new DeviceStateTransitionResult
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = context.FromState,
|
|
ToState = context.ToState,
|
|
Success = true,
|
|
Message = "Actions executed successfully",
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
private List<StateAction> GetExitActions(DeviceState state)
|
|
{
|
|
return GetStateActions(state, "exit");
|
|
}
|
|
|
|
private List<StateAction> GetEnterActions(DeviceState state)
|
|
{
|
|
return GetStateActions(state, "enter");
|
|
}
|
|
|
|
private List<StateAction> GetStateActions(DeviceState state, string actionType)
|
|
{
|
|
// This would typically come from configuration or database
|
|
// For now, return basic actions
|
|
var actions = new List<StateAction>();
|
|
|
|
switch (state)
|
|
{
|
|
case DeviceState.Running:
|
|
if (actionType == "exit")
|
|
{
|
|
actions.Add(new StopProductionAction());
|
|
}
|
|
else
|
|
{
|
|
actions.Add(new StartProductionAction());
|
|
}
|
|
break;
|
|
|
|
case DeviceState.Idle:
|
|
if (actionType == "enter")
|
|
{
|
|
actions.Add(new LogIdleAction());
|
|
}
|
|
break;
|
|
|
|
case DeviceState.Error:
|
|
if (actionType == "enter")
|
|
{
|
|
actions.Add(new NotifyErrorAction());
|
|
}
|
|
break;
|
|
|
|
default:
|
|
actions.Add(new LogStateAction(actionType, state));
|
|
break;
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
private async Task<bool> CanTransitionFromAsync(DeviceState fromState, DeviceState targetState, int deviceId)
|
|
{
|
|
var allowedTransitions = GetAllowedTransitions(fromState);
|
|
return allowedTransitions.Contains(targetState);
|
|
}
|
|
|
|
private List<DeviceState> GetAllowedTransitions(DeviceState fromState)
|
|
{
|
|
// Define state transition rules
|
|
var transitions = new Dictionary<DeviceState, List<DeviceState>>
|
|
{
|
|
[DeviceState.Unknown] = new List<DeviceState> { DeviceState.Offline, DeviceState.Idle },
|
|
[DeviceState.Offline] = new List<DeviceState> { DeviceState.Online, DeviceState.Unknown },
|
|
[DeviceState.Online] = new List<DeviceState> { DeviceState.Idle, DeviceState.Running, DeviceState.Error, DeviceState.Maintenance },
|
|
[DeviceState.Idle] = new List<DeviceState> { DeviceState.Running, DeviceState.Offline, DeviceState.Maintenance },
|
|
[DeviceState.Running] = new List<DeviceState> { DeviceState.Idle, DeviceState.Error, DeviceState.Stopped, DeviceState.Maintenance },
|
|
[DeviceState.Error] = new List<DeviceState> { DeviceState.Idle, DeviceState.Maintenance, DeviceState.Unknown },
|
|
[DeviceState.Maintenance] = new List<DeviceState> { DeviceState.Idle, DeviceState.Offline, DeviceState.Unknown },
|
|
[DeviceState.Stopped] = new List<DeviceState> { DeviceState.Idle, DeviceState.Offline }
|
|
};
|
|
|
|
return transitions.GetValueOrDefault(fromState, new List<DeviceState>());
|
|
}
|
|
|
|
private EventConfig GetEventConfiguration(DeviceEvent deviceEvent, DeviceState currentState)
|
|
{
|
|
// This would typically come from configuration
|
|
var eventConfigs = new Dictionary<(DeviceEvent, DeviceState), EventConfig>
|
|
{
|
|
[(DeviceEvent.Start, DeviceState.Idle)] = new EventConfig
|
|
{
|
|
TargetState = DeviceState.Running,
|
|
Conditions = new List<Func<int, Task<bool>>>
|
|
{
|
|
async deviceId => await IsDeviceReadyForProduction(deviceId)
|
|
}
|
|
},
|
|
|
|
[(DeviceEvent.Stop, DeviceState.Running)] = new EventConfig
|
|
{
|
|
TargetState = DeviceState.Idle,
|
|
Conditions = new List<Func<int, Task<bool>>>
|
|
{
|
|
async deviceId => await IsProductionComplete(deviceId)
|
|
}
|
|
},
|
|
|
|
[(DeviceEvent.Error, DeviceState.Running)] = new EventConfig
|
|
{
|
|
TargetState = DeviceState.Error,
|
|
Conditions = new List<Func<int, Task<bool>>>
|
|
{
|
|
async deviceId => await HasDeviceError(deviceId)
|
|
}
|
|
},
|
|
|
|
[(DeviceEvent.Resume, DeviceState.Maintenance)] = new EventConfig
|
|
{
|
|
TargetState = DeviceState.Idle,
|
|
Conditions = new List<Func<int, Task<bool>>>
|
|
{
|
|
async deviceId => await IsMaintenanceComplete(deviceId)
|
|
}
|
|
}
|
|
};
|
|
|
|
return eventConfigs.GetValueOrDefault((deviceEvent, currentState));
|
|
}
|
|
|
|
private async Task<bool> EvaluateEventConditionsAsync(int deviceId, DeviceEvent deviceEvent, List<Func<int, Task<bool>>> conditions)
|
|
{
|
|
if (conditions == null || !conditions.Any())
|
|
return true;
|
|
|
|
foreach (var condition in conditions)
|
|
{
|
|
var conditionResult = await condition(deviceId);
|
|
if (!conditionResult)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task RecordStateChangeAsync(int deviceId, DeviceState fromState, DeviceState toState, object context, bool isForced = false)
|
|
{
|
|
var history = new DeviceStateHistory
|
|
{
|
|
DeviceId = deviceId,
|
|
FromState = fromState,
|
|
ToState = toState,
|
|
ChangedAt = DateTime.UtcNow,
|
|
ChangedBy = isForced ? "System (Forced)" : "System",
|
|
Notes = isForced ? $"Forced transition: {JsonSerializer(context)}" : JsonSerializer(context)
|
|
};
|
|
|
|
await _deviceRepository.AddDeviceStateHistoryAsync(history);
|
|
}
|
|
|
|
private async Task NotifyStateHandlersAsync(int deviceId, DeviceState fromState, DeviceState toState, DeviceStateTransitionContext context)
|
|
{
|
|
var handlersCopy = _stateHandlers.ToList(); // Copy to avoid modification during iteration
|
|
|
|
foreach (var handler in handlersCopy)
|
|
{
|
|
try
|
|
{
|
|
await handler(deviceId, fromState, toState, context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error but continue with other handlers
|
|
Console.WriteLine($"Error in state handler: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task UpdateDeviceStatusAsync(CNCDevice device, DeviceState state)
|
|
{
|
|
// Update device status in database
|
|
device.Status = TranslateStateToDeviceStatus(state);
|
|
await _deviceRepository.UpdateDeviceAsync(device);
|
|
|
|
// Update cache
|
|
_cacheService.InvalidateDeviceCache(device.Id);
|
|
}
|
|
|
|
private DeviceState TranslateStatusToDeviceState(DeviceCurrentStatus status)
|
|
{
|
|
if (status == null || status.Status == DeviceStatus.Unknown)
|
|
return DeviceState.Unknown;
|
|
|
|
return status.Status switch
|
|
{
|
|
DeviceStatus.Offline => DeviceState.Offline,
|
|
DeviceStatus.Online => DeviceState.Online,
|
|
DeviceStatus.Idle => DeviceState.Idle,
|
|
DeviceStatus.Running => DeviceState.Running,
|
|
DeviceStatus.Error => DeviceState.Error,
|
|
DeviceStatus.Maintenance => DeviceState.Maintenance,
|
|
DeviceStatus.Stopped => DeviceState.Stopped,
|
|
_ => DeviceState.Unknown
|
|
};
|
|
}
|
|
|
|
private DeviceStatus TranslateStateToDeviceStatus(DeviceState state)
|
|
{
|
|
return state switch
|
|
{
|
|
DeviceState.Offline => DeviceStatus.Offline,
|
|
DeviceState.Online => DeviceStatus.Online,
|
|
DeviceState.Idle => DeviceStatus.Idle,
|
|
DeviceState.Running => DeviceStatus.Running,
|
|
DeviceState.Error => DeviceStatus.Error,
|
|
DeviceState.Maintenance => DeviceStatus.Maintenance,
|
|
DeviceState.Stopped => DeviceStatus.Stopped,
|
|
_ => DeviceStatus.Unknown
|
|
};
|
|
}
|
|
|
|
private async Task<DeviceValidationResult> ValidateStateRulesAsync(int deviceId, DeviceState state)
|
|
{
|
|
var result = new DeviceValidationResult { IsValid = true, Issues = new List<string>() };
|
|
|
|
switch (state)
|
|
{
|
|
case DeviceState.Running:
|
|
// Check if device should actually be running
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
if (device != null && device.EnableProduction)
|
|
{
|
|
var status = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
if (status.Status != DeviceStatus.Running)
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add("Device is in Running state but actual status is not Running");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case DeviceState.Error:
|
|
// Check if device has active errors
|
|
var activeAlarms = await _alarmService.GetActiveAlertsByDeviceAsync(deviceId);
|
|
if (!activeAlarms.Any(a => a.AlertType == "DeviceError"))
|
|
{
|
|
result.IsValid = false;
|
|
result.Issues.Add("Device is in Error state but has no active error alerts");
|
|
}
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task<StateTimeoutInfo> CheckStateTimeoutAsync(int deviceId, DeviceState state)
|
|
{
|
|
var stateContext = _deviceStates.GetValueOrDefault(deviceId);
|
|
if (stateContext == null)
|
|
return new StateTimeoutInfo { IsTimeout = false };
|
|
|
|
var stateDurations = GetStateTimeoutDurations();
|
|
var maxDuration = stateDurations.GetValueOrDefault(state, TimeSpan.FromMinutes(30));
|
|
|
|
var currentDuration = DateTime.UtcNow - stateContext.StateChangedAt;
|
|
|
|
return new StateTimeoutInfo
|
|
{
|
|
IsTimeout = currentDuration > maxDuration,
|
|
State = state,
|
|
CurrentDuration = currentDuration,
|
|
MaxDuration = maxDuration,
|
|
ExceededBy = currentDuration - maxDuration
|
|
};
|
|
}
|
|
|
|
private Dictionary<DeviceState, TimeSpan> GetStateTimeoutDurations()
|
|
{
|
|
return new Dictionary<DeviceState, TimeSpan>
|
|
{
|
|
[DeviceState.Unknown] = TimeSpan.FromMinutes(5),
|
|
[DeviceState.Offline] = TimeSpan.FromHours(1),
|
|
[DeviceState.Online] = TimeSpan.FromMinutes(15),
|
|
[DeviceState.Idle] = TimeSpan.FromMinutes(10),
|
|
[DeviceState.Running] = TimeSpan.FromHours(8),
|
|
[DeviceState.Error] = TimeSpan.FromMinutes(60),
|
|
[DeviceState.Maintenance] = TimeSpan.FromHours(4),
|
|
[DeviceState.Stopped] = TimeSpan.FromMinutes(30)
|
|
};
|
|
}
|
|
|
|
private async Task HandleInvalidStateAsync(int deviceId, DeviceValidationResult validationResult)
|
|
{
|
|
// Log warning
|
|
Console.WriteLine($"Device {deviceId} state validation failed: {string.Join(", ", validationResult.Issues)}");
|
|
|
|
// Attempt to transition to a safe state
|
|
var safeState = DetermineSafeState(deviceId, validationResult.CurrentState);
|
|
await ForceStateAsync(deviceId, safeState, $"State validation failed: {string.Join(", ", validationResult.Issues)}");
|
|
}
|
|
|
|
private async Task HandleStateTimeoutAsync(int deviceId, StateTimeoutInfo timeoutInfo)
|
|
{
|
|
// Log warning
|
|
Console.WriteLine($"Device {deviceId} state {timeoutInfo.State} exceeded maximum duration by {timeoutInfo.ExceededBy}");
|
|
|
|
// Trigger timeout event
|
|
await TriggerEventAsync(deviceId, DeviceEvent.Timeout, timeoutInfo);
|
|
}
|
|
|
|
private DeviceState DetermineSafeState(int deviceId, DeviceState currentState)
|
|
{
|
|
return DeviceState.Idle; // Default safe state
|
|
}
|
|
|
|
private async Task<bool> IsDeviceReadyForProduction(int deviceId)
|
|
{
|
|
// Check if device is ready for production
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
var status = await _collectionService.GetDeviceCurrentStatusAsync(deviceId);
|
|
|
|
return device != null && device.EnableProduction &&
|
|
status.Status == DeviceStatus.Online &&
|
|
!device.IsUnderMaintenance;
|
|
}
|
|
|
|
private async Task<bool> IsProductionComplete(int deviceId)
|
|
{
|
|
// Check if production is complete
|
|
var records = await _deviceRepository.GetProductionRecordsByDeviceAsync(deviceId);
|
|
return records.Any() && records.Last().IsComplete;
|
|
}
|
|
|
|
private async Task<bool> HasDeviceError(int deviceId)
|
|
{
|
|
// Check if device has errors
|
|
var alerts = await _alarmService.GetActiveAlertsByDeviceAsync(deviceId);
|
|
return alerts.Any(a => a.AlertType == "DeviceError");
|
|
}
|
|
|
|
private async Task<bool> IsMaintenanceComplete(int deviceId)
|
|
{
|
|
// Check if maintenance is complete
|
|
var device = await _deviceRepository.GetByIdAsync(deviceId);
|
|
return device != null && !device.IsUnderMaintenance;
|
|
}
|
|
|
|
private string JsonSerializer(object obj)
|
|
{
|
|
// Simplified JSON serialization
|
|
return obj?.ToString() ?? "{}";
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Supporting Classes and Interfaces
|
|
|
|
public delegate Task StateChangeHandler(int deviceId, DeviceState fromState, DeviceState toState, DeviceStateTransitionContext context);
|
|
|
|
public interface IStateAction
|
|
{
|
|
Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context);
|
|
}
|
|
|
|
public class StateActionResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; }
|
|
}
|
|
|
|
public class DeviceStateContext
|
|
{
|
|
public DeviceState CurrentState { get; set; }
|
|
public DeviceState PreviousState { get; set; }
|
|
public DateTime StateChangedAt { get; set; }
|
|
public object Context { get; set; }
|
|
}
|
|
|
|
public class DeviceStateTransitionContext
|
|
{
|
|
public int DeviceId { get; set; }
|
|
public CNCDevice Device { get; set; }
|
|
public DeviceState FromState { get; set; }
|
|
public DeviceState ToState { get; set; }
|
|
public object Context { get; set; }
|
|
public DateTime Timestamp { get; set; }
|
|
}
|
|
|
|
public class EventConfig
|
|
{
|
|
public DeviceState TargetState { get; set; }
|
|
public List<Func<int, Task<bool>>> Conditions { get; set; }
|
|
}
|
|
|
|
public class StateTimeoutInfo
|
|
{
|
|
public bool IsTimeout { get; set; }
|
|
public DeviceState State { get; set; }
|
|
public TimeSpan CurrentDuration { get; set; }
|
|
public TimeSpan MaxDuration { get; set; }
|
|
public TimeSpan ExceededBy { get; set; }
|
|
}
|
|
|
|
public class DeviceValidationResult
|
|
{
|
|
public int DeviceId { get; set; }
|
|
public bool IsValid { get; set; }
|
|
public List<string> Issues { get; set; }
|
|
public DeviceState CurrentState { get; set; }
|
|
public DateTime Timestamp { get; set; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State Action Implementations
|
|
|
|
public class StopProductionAction : IStateAction
|
|
{
|
|
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
{
|
|
// Implementation to stop production
|
|
return new StateActionResult { Success = true, Message = "Production stopped" };
|
|
}
|
|
}
|
|
|
|
public class StartProductionAction : IStateAction
|
|
{
|
|
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
{
|
|
// Implementation to start production
|
|
return new StateActionResult { Success = true, Message = "Production started" };
|
|
}
|
|
}
|
|
|
|
public class LogIdleAction : IStateAction
|
|
{
|
|
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
{
|
|
// Implementation to log idle state
|
|
return new StateActionResult { Success = true, Message = "Idle state logged" };
|
|
}
|
|
}
|
|
|
|
public class NotifyErrorAction : IStateAction
|
|
{
|
|
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
{
|
|
// Implementation to notify about error
|
|
return new StateActionResult { Success = true, Message = "Error notification sent" };
|
|
}
|
|
}
|
|
|
|
public class LogStateAction : IStateAction
|
|
{
|
|
private readonly string _actionType;
|
|
private readonly DeviceState _state;
|
|
|
|
public LogStateAction(string actionType, DeviceState state)
|
|
{
|
|
_actionType = actionType;
|
|
_state = state;
|
|
}
|
|
|
|
public async Task<StateActionResult> ExecuteAsync(int deviceId, DeviceStateTransitionContext context)
|
|
{
|
|
// Implementation to log state changes
|
|
return new StateActionResult { Success = true, Message = $"{_actionType} action for {_state} logged" };
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
} |