feat: 完成S7 PLC模拟器完整实现
实现基于HSL Communication库的西门子S7 PLC模拟器系统,用于WCS开发测试。
核心功能:
- 多实例管理:支持创建和管理多个独立的S7服务器实例
- 内存模拟:支持M/DB/I/Q/T/C所有常用地址区域
- 地址解析:支持"M100"、"DB1.DBD0"、"I0.0"、"T1"、"C1"等S7地址格式
- 数据持久化:配置和内存数据自动保存到本地文件
- Web管理界面:提供直观的Web UI进行实例管理
- 实时监控:显示连接状态、请求数、客户端信息等
项目结构:
- WIDESEAWCS_S7Simulator.Core - 核心领域层
* Memory/ - 内存区域实现(I/Q/T/C/M/DB regions)
* Persistence/ - 文件持久化服务
* Server/ - S7服务器实例(HSL集成)
* Manager/ - 实例管理器
- WIDESEAWCS_S7Simulator.Server - Web API服务器
* Controllers/ - REST API控制器
- WIDESEAWCS_S7Simulator.Web - Web管理界面
* Pages/ - Razor Pages(列表/创建/编辑/详情)
- WIDESEAWCS_S7Simulator.UnitTests - 单元测试
技术细节:
- 线程安全的内存操作(ReaderWriterLockSlim)
- 大端字节序(S7 PLC规范)
- 完整的Dispose模式
- 原子性写入操作(文件持久化)
- 异步API设计
- Bootstrap 5 + Alpine.js 前端
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Interfaces |
| | | { |
| | | /// <summary> |
| | | /// S7æå¡å¨å®ä¾æ¥å£ |
| | | /// 管çå个S7 PLC仿çå®ä¾ççå½å¨æ |
| | | /// </summary> |
| | | public interface IS7ServerInstance : IDisposable |
| | | { |
| | | /// <summary> |
| | | /// å®ä¾é
ç½® |
| | | /// </summary> |
| | | InstanceConfig Config { get; } |
| | | |
| | | /// <summary> |
| | | /// å®ä¾ç¶æ |
| | | /// </summary> |
| | | InstanceState State { get; } |
| | | |
| | | /// <summary> |
| | | /// å
ååå¨ |
| | | /// </summary> |
| | | IMemoryStore MemoryStore { get; } |
| | | |
| | | /// <summary> |
| | | /// å¯å¨S7æå¡å¨ |
| | | /// </summary> |
| | | /// <returns>å¯å¨æ¯å¦æå</returns> |
| | | bool Start(); |
| | | |
| | | /// <summary> |
| | | /// 忢S7æå¡å¨ |
| | | /// </summary> |
| | | void Stop(); |
| | | |
| | | /// <summary> |
| | | /// éå¯S7æå¡å¨ |
| | | /// </summary> |
| | | /// <returns>é坿¯å¦æå</returns> |
| | | bool Restart(); |
| | | |
| | | /// <summary> |
| | | /// è·åå®ä¾ç¶æå¿«ç
§ |
| | | /// </summary> |
| | | /// <returns>ç¶æå¿«ç
§</returns> |
| | | InstanceState GetState(); |
| | | |
| | | /// <summary> |
| | | /// æ¸
空ææå
å |
| | | /// </summary> |
| | | void ClearMemory(); |
| | | |
| | | /// <summary> |
| | | /// 导åºå
åæ°æ® |
| | | /// </summary> |
| | | /// <returns>å
åæ°æ®</returns> |
| | | Dictionary<string, byte[]> ExportMemory(); |
| | | |
| | | /// <summary> |
| | | /// 导å
¥å
åæ°æ® |
| | | /// </summary> |
| | | /// <param name="data">å
åæ°æ®</param> |
| | | void ImportMemory(Dictionary<string, byte[]> data); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using System.Collections.Generic; |
| | | using System.Threading.Tasks; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Interfaces |
| | | { |
| | | /// <summary> |
| | | /// 仿çå¨å®ä¾ç®¡ç卿¥å£ |
| | | /// 管çå¤ä¸ªS7æå¡å¨å®ä¾ççå½å¨æ |
| | | /// </summary> |
| | | public interface ISimulatorInstanceManager |
| | | { |
| | | /// <summary> |
| | | /// å®ä¾ç¶æååäºä»¶ |
| | | /// </summary> |
| | | event EventHandler<InstanceStateChangedEventArgs>? InstanceStateChanged; |
| | | |
| | | /// <summary> |
| | | /// è·åææå®ä¾ |
| | | /// </summary> |
| | | /// <returns>å®ä¾å表</returns> |
| | | IReadOnlyList<IS7ServerInstance> GetAllInstances(); |
| | | |
| | | /// <summary> |
| | | /// æ ¹æ®IDè·åå®ä¾ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <returns>å®ä¾ï¼ä¸åå¨åè¿ånull</returns> |
| | | IS7ServerInstance? GetInstance(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// æ£æ¥å®ä¾æ¯å¦åå¨ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <returns>æ¯å¦åå¨</returns> |
| | | bool InstanceExists(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// å建æ°å®ä¾ |
| | | /// </summary> |
| | | /// <param name="config">å®ä¾é
ç½®</param> |
| | | /// <returns>å建çå®ä¾</returns> |
| | | Task<IS7ServerInstance> CreateInstanceAsync(InstanceConfig config); |
| | | |
| | | /// <summary> |
| | | /// å¯å¨å®ä¾ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <returns>æ¯å¦æå</returns> |
| | | Task<bool> StartInstanceAsync(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// 忢å®ä¾ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | Task StopInstanceAsync(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// éå¯å®ä¾ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <returns>æ¯å¦æå</returns> |
| | | Task<bool> RestartInstanceAsync(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// å é¤å®ä¾ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <param name="deleteConfig">æ¯å¦å é¤é
ç½®æä»¶</param> |
| | | Task DeleteInstanceAsync(string instanceId, bool deleteConfig = true); |
| | | |
| | | /// <summary> |
| | | /// è·åå®ä¾ç¶æ |
| | | /// </summary> |
| | | /// <param name="instanceId">å®ä¾ID</param> |
| | | /// <returns>å®ä¾ç¶æï¼ä¸åå¨åè¿ånull</returns> |
| | | InstanceState? GetInstanceState(string instanceId); |
| | | |
| | | /// <summary> |
| | | /// è·åææå®ä¾ç¶æ |
| | | /// </summary> |
| | | /// <returns>å®ä¾ç¶æå表</returns> |
| | | IReadOnlyList<InstanceState> GetAllInstanceStates(); |
| | | |
| | | /// <summary> |
| | | /// å è½½ææå·²ä¿åçå®ä¾ |
| | | /// </summary> |
| | | /// <param name="autoStart">æ¯å¦èªå¨å¯å¨æ 记为AutoStartçå®ä¾</param> |
| | | Task LoadSavedInstancesAsync(bool autoStart = true); |
| | | |
| | | /// <summary> |
| | | /// 忢ææå®ä¾ |
| | | /// </summary> |
| | | Task StopAllInstancesAsync(); |
| | | |
| | | /// <summary> |
| | | /// è·åè¿è¡ä¸çå®ä¾æ°é |
| | | /// </summary> |
| | | int GetRunningInstanceCount(); |
| | | |
| | | /// <summary> |
| | | /// è·åå®ä¾æ»æ° |
| | | /// </summary> |
| | | int GetTotalInstanceCount(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å®ä¾ç¶æååäºä»¶åæ° |
| | | /// </summary> |
| | | public class InstanceStateChangedEventArgs : EventArgs |
| | | { |
| | | /// <summary> |
| | | /// å®ä¾ID |
| | | /// </summary> |
| | | public string InstanceId { get; set; } = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// æ§ç¶æ |
| | | /// </summary> |
| | | public InstanceStatus OldStatus { get; set; } |
| | | |
| | | /// <summary> |
| | | /// æ°ç¶æ |
| | | /// </summary> |
| | | public InstanceStatus NewStatus { get; set; } |
| | | |
| | | /// <summary> |
| | | /// å®ä¾ç¶æ |
| | | /// </summary> |
| | | public InstanceState InstanceState { get; set; } = new(); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using System; |
| | | using System.Collections.Concurrent; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Threading.Tasks; |
| | | using Microsoft.Extensions.Logging; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | using WIDESEAWCS_S7Simulator.Core.Persistence; |
| | | using WIDESEAWCS_S7Simulator.Core.Server; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Manager |
| | | { |
| | | /// <summary> |
| | | /// 仿çå¨å®ä¾ç®¡çå¨å®ç° |
| | | /// 管çå¤ä¸ªS7æå¡å¨å®ä¾ççå½å¨æï¼æä¾çº¿ç¨å®å
¨çCRUDæä½ |
| | | /// </summary> |
| | | public class SimulatorInstanceManager : ISimulatorInstanceManager |
| | | { |
| | | private readonly ConcurrentDictionary<string, IS7ServerInstance> _instances = new(); |
| | | private readonly IPersistenceService _persistenceService; |
| | | private readonly ILogger<SimulatorInstanceManager> _logger; |
| | | private readonly ILoggerFactory _loggerFactory; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<InstanceStateChangedEventArgs>? InstanceStateChanged; |
| | | |
| | | /// <summary> |
| | | /// æé 彿° |
| | | /// </summary> |
| | | /// <param name="persistenceService">æä¹
åæå¡</param> |
| | | /// <param name="logger">æ¥å¿è®°å½å¨</param> |
| | | /// <param name="loggerFactory">æ¥å¿å·¥å</param> |
| | | public SimulatorInstanceManager( |
| | | IPersistenceService persistenceService, |
| | | ILogger<SimulatorInstanceManager> logger, |
| | | ILoggerFactory loggerFactory) |
| | | { |
| | | _persistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService)); |
| | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | | _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public IReadOnlyList<IS7ServerInstance> GetAllInstances() |
| | | { |
| | | return _instances.Values.ToList().AsReadOnly(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public IS7ServerInstance? GetInstance(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | _instances.TryGetValue(instanceId, out var instance); |
| | | return instance; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool InstanceExists(string instanceId) |
| | | { |
| | | return !string.IsNullOrWhiteSpace(instanceId) && _instances.ContainsKey(instanceId); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<IS7ServerInstance> CreateInstanceAsync(InstanceConfig config) |
| | | { |
| | | if (config == null) |
| | | { |
| | | throw new ArgumentNullException(nameof(config)); |
| | | } |
| | | |
| | | // å¦ææ²¡ææä¾IDï¼çææ°çGUID |
| | | if (string.IsNullOrWhiteSpace(config.Id)) |
| | | { |
| | | config.Id = Guid.NewGuid().ToString("N"); |
| | | _logger.LogDebug("为å®ä¾çææ°ID: {InstanceId}", config.Id); |
| | | } |
| | | |
| | | // æ£æ¥IDæ¯å¦å·²åå¨ |
| | | if (_instances.ContainsKey(config.Id)) |
| | | { |
| | | throw new InvalidOperationException($"å®ä¾ID {config.Id} å·²åå¨"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | // å建å®ä¾ |
| | | var instanceLogger = _loggerFactory.CreateLogger<S7ServerInstance>(); |
| | | var instance = new S7ServerInstance(config, instanceLogger); |
| | | |
| | | // æ·»å å°åå
¸ |
| | | if (!_instances.TryAdd(config.Id, instance)) |
| | | { |
| | | throw new InvalidOperationException($"æ æ³å°å®ä¾ {config.Id} æ·»å å°ç®¡çå¨"); |
| | | } |
| | | |
| | | // ä¿åé
ç½® |
| | | await _persistenceService.SaveInstanceConfigAsync(config); |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = config.Id, |
| | | OldStatus = InstanceStatus.Stopped, |
| | | NewStatus = InstanceStatus.Stopped, |
| | | InstanceState = instance.GetState() |
| | | }); |
| | | |
| | | _logger.LogInformation("å·²å建å®ä¾ {InstanceId} ({InstanceName})", config.Id, config.Name); |
| | | return instance; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å建å®ä¾ {InstanceId} æ¶åçé误", config.Id); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<bool> StartInstanceAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | _logger.LogWarning("å°è¯å¯å¨å®ä¾æ¶æä¾äºç©ºID"); |
| | | return false; |
| | | } |
| | | |
| | | if (!_instances.TryGetValue(instanceId, out var instance)) |
| | | { |
| | | _logger.LogWarning("å°è¯å¯å¨ä¸åå¨çå®ä¾ {InstanceId}", instanceId); |
| | | return false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | var oldState = instance.GetState(); |
| | | var oldStatus = oldState.Status; |
| | | |
| | | // å¯å¨å®ä¾ |
| | | var success = instance.Start(); |
| | | |
| | | if (success) |
| | | { |
| | | var newState = instance.GetState(); |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = instanceId, |
| | | OldStatus = oldStatus, |
| | | NewStatus = newState.Status, |
| | | InstanceState = newState |
| | | }); |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} å·²å¯å¨", instanceId); |
| | | } |
| | | else |
| | | { |
| | | _logger.LogWarning("å®ä¾ {InstanceId} å¯å¨å¤±è´¥", instanceId); |
| | | } |
| | | |
| | | return success; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å¯å¨å®ä¾ {InstanceId} æ¶åçå¼å¸¸", instanceId); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task StopInstanceAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | _logger.LogWarning("å°è¯åæ¢å®ä¾æ¶æä¾äºç©ºID"); |
| | | return; |
| | | } |
| | | |
| | | if (!_instances.TryGetValue(instanceId, out var instance)) |
| | | { |
| | | _logger.LogWarning("å°è¯åæ¢ä¸åå¨çå®ä¾ {InstanceId}", instanceId); |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | var oldState = instance.GetState(); |
| | | var oldStatus = oldState.Status; |
| | | |
| | | // 忢å®ä¾ |
| | | instance.Stop(); |
| | | |
| | | // 忥å
åæ°æ®å°æä¹
ååå¨ |
| | | await _persistenceService.SaveMemoryDataAsync(instanceId, instance.MemoryStore); |
| | | |
| | | var newState = instance.GetState(); |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = instanceId, |
| | | OldStatus = oldStatus, |
| | | NewStatus = newState.Status, |
| | | InstanceState = newState |
| | | }); |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} 已忢", instanceId); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "忢å®ä¾ {InstanceId} æ¶åçå¼å¸¸", instanceId); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<bool> RestartInstanceAsync(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | _logger.LogWarning("å°è¯éå¯å®ä¾æ¶æä¾äºç©ºID"); |
| | | return false; |
| | | } |
| | | |
| | | if (!_instances.TryGetValue(instanceId, out var instance)) |
| | | { |
| | | _logger.LogWarning("å°è¯éå¯ä¸åå¨çå®ä¾ {InstanceId}", instanceId); |
| | | return false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | var oldState = instance.GetState(); |
| | | var oldStatus = oldState.Status; |
| | | |
| | | // éå¯å®ä¾ |
| | | var success = instance.Restart(); |
| | | |
| | | if (success) |
| | | { |
| | | var newState = instance.GetState(); |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = instanceId, |
| | | OldStatus = oldStatus, |
| | | NewStatus = newState.Status, |
| | | InstanceState = newState |
| | | }); |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} å·²éå¯", instanceId); |
| | | } |
| | | else |
| | | { |
| | | _logger.LogWarning("å®ä¾ {InstanceId} éå¯å¤±è´¥", instanceId); |
| | | } |
| | | |
| | | return success; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "éå¯å®ä¾ {InstanceId} æ¶åçå¼å¸¸", instanceId); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task DeleteInstanceAsync(string instanceId, bool deleteConfig = true) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | _logger.LogWarning("å°è¯å é¤å®ä¾æ¶æä¾äºç©ºID"); |
| | | return; |
| | | } |
| | | |
| | | if (!_instances.TryRemove(instanceId, out var instance)) |
| | | { |
| | | _logger.LogWarning("å°è¯å é¤ä¸åå¨çå®ä¾ {InstanceId}", instanceId); |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | var oldState = instance.GetState(); |
| | | |
| | | // 忢å®ä¾ |
| | | instance.Stop(); |
| | | |
| | | // éæ¾å®ä¾èµæº |
| | | instance.Dispose(); |
| | | |
| | | // å é¤é
ç½®æä»¶ |
| | | if (deleteConfig) |
| | | { |
| | | await _persistenceService.DeleteInstanceConfigAsync(instanceId); |
| | | } |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = instanceId, |
| | | OldStatus = oldState.Status, |
| | | NewStatus = InstanceStatus.Stopped, |
| | | InstanceState = oldState |
| | | }); |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} å·²å é¤", instanceId); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å é¤å®ä¾ {InstanceId} æ¶åçå¼å¸¸", instanceId); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public InstanceState? GetInstanceState(string instanceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(instanceId)) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | if (!_instances.TryGetValue(instanceId, out var instance)) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | return instance.GetState(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public IReadOnlyList<InstanceState> GetAllInstanceStates() |
| | | { |
| | | return _instances.Values |
| | | .Select(i => i.GetState()) |
| | | .ToList() |
| | | .AsReadOnly(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task LoadSavedInstancesAsync(bool autoStart = true) |
| | | { |
| | | try |
| | | { |
| | | _logger.LogInformation("å¼å§å 载已ä¿åçå®ä¾é
ç½®..."); |
| | | |
| | | // å è½½ææé
ç½® |
| | | var configs = await _persistenceService.LoadAllInstanceConfigsAsync(); |
| | | |
| | | if (configs == null || configs.Count == 0) |
| | | { |
| | | _logger.LogInformation("æ²¡ææ¾å°å·²ä¿åçå®ä¾é
ç½®"); |
| | | return; |
| | | } |
| | | |
| | | _logger.LogInformation("æ¾å° {Count} 个已ä¿åçå®ä¾é
ç½®", configs.Count); |
| | | |
| | | foreach (var config in configs) |
| | | { |
| | | try |
| | | { |
| | | // å建å®ä¾ |
| | | var instanceLogger = _loggerFactory.CreateLogger<S7ServerInstance>(); |
| | | var instance = new S7ServerInstance(config, instanceLogger); |
| | | |
| | | // æ·»å å°åå
¸ |
| | | if (_instances.TryAdd(config.Id, instance)) |
| | | { |
| | | // å è½½å
åæ°æ® |
| | | try |
| | | { |
| | | await _persistenceService.LoadMemoryDataAsync(config.Id, instance.MemoryStore); |
| | | _logger.LogDebug("å·²å è½½å®ä¾ {InstanceId} çå
åæ°æ®", config.Id); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogWarning(ex, "å è½½å®ä¾ {InstanceId} çå
åæ°æ®æ¶åçè¦å", config.Id); |
| | | } |
| | | |
| | | // 妿é
ç½®äºèªå¨å¯å¨ï¼åå¯å¨å®ä¾ |
| | | if (autoStart && config.AutoStart) |
| | | { |
| | | _logger.LogInformation("èªå¨å¯å¨å®ä¾ {InstanceId} ({InstanceName})", config.Id, config.Name); |
| | | var success = instance.Start(); |
| | | if (success) |
| | | { |
| | | _logger.LogInformation("å®ä¾ {InstanceId} èªå¨å¯å¨æå", config.Id); |
| | | } |
| | | else |
| | | { |
| | | _logger.LogWarning("å®ä¾ {InstanceId} èªå¨å¯å¨å¤±è´¥", config.Id); |
| | | } |
| | | } |
| | | |
| | | // 触åç¶æååäºä»¶ |
| | | OnInstanceStateChanged(new InstanceStateChangedEventArgs |
| | | { |
| | | InstanceId = config.Id, |
| | | OldStatus = InstanceStatus.Stopped, |
| | | NewStatus = instance.State.Status, |
| | | InstanceState = instance.GetState() |
| | | }); |
| | | |
| | | _logger.LogInformation("å·²å è½½å®ä¾ {InstanceId} ({InstanceName})", config.Id, config.Name); |
| | | } |
| | | else |
| | | { |
| | | _logger.LogWarning("å®ä¾ {InstanceId} å·²åå¨ï¼è·³è¿å è½½", config.Id); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å è½½å®ä¾ {InstanceId} æ¶åçé误", config.Id); |
| | | } |
| | | } |
| | | |
| | | _logger.LogInformation("å®ä¾å è½½å®æï¼å
±å è½½ {Count} 个å®ä¾", _instances.Count); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å 载已ä¿åçå®ä¾æ¶åçå¼å¸¸"); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task StopAllInstancesAsync() |
| | | { |
| | | _logger.LogInformation("å¼å§åæ¢ææå®ä¾..."); |
| | | |
| | | var instanceIds = _instances.Keys.ToList(); |
| | | |
| | | foreach (var instanceId in instanceIds) |
| | | { |
| | | try |
| | | { |
| | | await StopInstanceAsync(instanceId); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "忢å®ä¾ {InstanceId} æ¶åçå¼å¸¸", instanceId); |
| | | } |
| | | } |
| | | |
| | | _logger.LogInformation("ææå®ä¾å·²åæ¢"); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public int GetRunningInstanceCount() |
| | | { |
| | | return _instances.Values.Count(i => i.State.Status == InstanceStatus.Running); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public int GetTotalInstanceCount() |
| | | { |
| | | return _instances.Count; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触åå®ä¾ç¶æååäºä»¶ |
| | | /// </summary> |
| | | /// <param name="e">äºä»¶åæ°</param> |
| | | protected virtual void OnInstanceStateChanged(InstanceStateChangedEventArgs e) |
| | | { |
| | | InstanceStateChanged?.Invoke(this, e); |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using System.Collections.Concurrent; |
| | | using HslCommunication; |
| | | using HslCommunication.Profinet.Siemens; |
| | | using HslCommunication.Reflection; |
| | | using Microsoft.Extensions.Logging; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | using WIDESEAWCS_S7Simulator.Core.Memory; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Core.Server |
| | | { |
| | | /// <summary> |
| | | /// S7æå¡å¨å®ä¾å®ç° |
| | | /// 使ç¨HSL Communicationåºå®ç°S7 PLC仿çæå¡å¨ |
| | | /// </summary> |
| | | public class S7ServerInstance : IS7ServerInstance |
| | | { |
| | | private readonly ILogger<S7ServerInstance> _logger; |
| | | private readonly object _lock = new(); |
| | | private SiemensS7Server? _server; |
| | | private bool _disposed; |
| | | |
| | | /// <inheritdoc/> |
| | | public InstanceConfig Config { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public InstanceState State { get; private set; } |
| | | |
| | | /// <inheritdoc/> |
| | | public IMemoryStore MemoryStore { get; } |
| | | |
| | | /// <summary> |
| | | /// 客æ·ç«¯è¿æ¥è¿½è¸ª |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, S7ClientConnection> _clients = new(); |
| | | |
| | | /// <summary> |
| | | /// æé 彿° |
| | | /// </summary> |
| | | /// <param name="config">å®ä¾é
ç½®</param> |
| | | /// <param name="logger">æ¥å¿è®°å½å¨</param> |
| | | public S7ServerInstance(InstanceConfig config, ILogger<S7ServerInstance> logger) |
| | | { |
| | | Config = config ?? throw new ArgumentNullException(nameof(config)); |
| | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | | |
| | | // åå§åå
ååå¨ |
| | | MemoryStore = new MemoryStore(config.MemoryConfig); |
| | | |
| | | // åå§åç¶æ |
| | | State = new InstanceState |
| | | { |
| | | InstanceId = config.Id, |
| | | Status = InstanceStatus.Stopped, |
| | | ClientCount = 0, |
| | | TotalRequests = 0 |
| | | }; |
| | | |
| | | _logger.LogInformation("S7æå¡å¨å®ä¾ {InstanceId} ({InstanceName}) å·²å建ï¼PLCç±»å: {PLCType}, 端å£: {Port}", |
| | | config.Id, config.Name, config.PLCType, config.Port); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool Start() |
| | | { |
| | | lock (_lock) |
| | | { |
| | | if (_disposed) |
| | | { |
| | | _logger.LogError("æ æ³å¯å¨å·²éæ¾çå®ä¾ {InstanceId}", Config.Id); |
| | | return false; |
| | | } |
| | | |
| | | if (State.Status == InstanceStatus.Running) |
| | | { |
| | | _logger.LogWarning("å®ä¾ {InstanceId} å·²å¨è¿è¡ä¸", Config.Id); |
| | | return true; |
| | | } |
| | | |
| | | try |
| | | { |
| | | // å建S7æå¡å¨ |
| | | _server = new SiemensS7Server(); |
| | | |
| | | // è®¾ç½®æ¿æ´»ç |
| | | if (!string.IsNullOrWhiteSpace(Config.ActivationKey)) |
| | | { |
| | | HslCommunication.Authorization.SetAuthorizationCode(Config.ActivationKey); |
| | | _logger.LogDebug("å·²è®¾ç½®æ¿æ´»ç "); |
| | | } |
| | | |
| | | // åå§åDBåï¼æ ¹æ®é
ç½®ï¼ |
| | | InitializeDbBlocks(); |
| | | |
| | | // ä»MemoryStore忥åå§æ°æ®å°æå¡å¨ |
| | | SynchronizeMemoryToServer(); |
| | | |
| | | // å¯å¨æå¡å¨ |
| | | try |
| | | { |
| | | _server.ServerStart(Config.Port); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å¯å¨S7æå¡å¨å¤±è´¥"); |
| | | State.Status = InstanceStatus.Error; |
| | | State.ErrorMessage = ex.Message; |
| | | return false; |
| | | } |
| | | |
| | | // æ´æ°ç¶æ |
| | | State.Status = InstanceStatus.Running; |
| | | State.StartTime = DateTime.Now; |
| | | State.ErrorMessage = null; |
| | | |
| | | _logger.LogInformation("S7æå¡å¨å®ä¾ {InstanceId} å·²æåå¯å¨ï¼çå¬ç«¯å£: {Port}", Config.Id, Config.Port); |
| | | return true; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "å¯å¨S7æå¡å¨å®ä¾ {InstanceId} æ¶åçå¼å¸¸", Config.Id); |
| | | State.Status = InstanceStatus.Error; |
| | | State.ErrorMessage = ex.Message; |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Stop() |
| | | { |
| | | lock (_lock) |
| | | { |
| | | if (_disposed || State.Status == InstanceStatus.Stopped) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | if (_server != null) |
| | | { |
| | | // 忢å忥æå¡å¨æ°æ®å°MemoryStore |
| | | SynchronizeServerToMemory(); |
| | | |
| | | _server.ServerClose(); |
| | | _server = null; |
| | | } |
| | | |
| | | // æ¸
空客æ·ç«¯è¿æ¥ |
| | | _clients.Clear(); |
| | | |
| | | // æ´æ°ç¶æ |
| | | State.Status = InstanceStatus.Stopped; |
| | | State.ClientCount = 0; |
| | | State.StartTime = null; |
| | | |
| | | _logger.LogInformation("S7æå¡å¨å®ä¾ {InstanceId} 已忢", Config.Id); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "忢S7æå¡å¨å®ä¾ {InstanceId} æ¶åçå¼å¸¸", Config.Id); |
| | | State.Status = InstanceStatus.Error; |
| | | State.ErrorMessage = ex.Message; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool Restart() |
| | | { |
| | | Stop(); |
| | | return Start(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public InstanceState GetState() |
| | | { |
| | | lock (_lock) |
| | | { |
| | | // æ´æ°å®¢æ·ç«¯è¿æ¥æ° |
| | | State.ClientCount = _clients.Count; |
| | | State.Clients = _clients.Values.ToList(); |
| | | |
| | | // è¿åç¶æå¯æ¬ |
| | | return new InstanceState |
| | | { |
| | | InstanceId = State.InstanceId, |
| | | Status = State.Status, |
| | | ClientCount = State.ClientCount, |
| | | TotalRequests = State.TotalRequests, |
| | | StartTime = State.StartTime, |
| | | LastActivityTime = State.LastActivityTime, |
| | | Clients = new List<S7ClientConnection>(State.Clients), |
| | | ErrorMessage = State.ErrorMessage |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void ClearMemory() |
| | | { |
| | | lock (_lock) |
| | | { |
| | | MemoryStore.Clear(); |
| | | |
| | | // åæ¶æ¸
空æå¡å¨å
鍿°æ® |
| | | if (_server != null && State.Status == InstanceStatus.Running) |
| | | { |
| | | // æ¸
空åå
ååºå |
| | | for (int i = 0; i < 100; i++) |
| | | { |
| | | _server.Write($"M{i}", (byte)0); |
| | | _server.Write($"I{i}", (byte)0); |
| | | _server.Write($"Q{i}", (byte)0); |
| | | } |
| | | |
| | | // æ¸
空DBå |
| | | for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++) |
| | | { |
| | | for (int i = 0; i < 10; i++) |
| | | { |
| | | _server.Write($"DB{db}.DBD{i}", (byte)0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} å
åå·²æ¸
空", Config.Id); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Dictionary<string, byte[]> ExportMemory() |
| | | { |
| | | lock (_lock) |
| | | { |
| | | // å
忥æå¡å¨æ°æ®å°MemoryStore |
| | | if (_server != null && State.Status == InstanceStatus.Running) |
| | | { |
| | | SynchronizeServerToMemory(); |
| | | } |
| | | return MemoryStore.Export(); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void ImportMemory(Dictionary<string, byte[]> data) |
| | | { |
| | | lock (_lock) |
| | | { |
| | | MemoryStore.Import(data); |
| | | |
| | | // 忥尿å¡å¨ |
| | | if (_server != null && State.Status == InstanceStatus.Running) |
| | | { |
| | | SynchronizeMemoryToServer(); |
| | | } |
| | | |
| | | _logger.LogInformation("å®ä¾ {InstanceId} å
åæ°æ®å·²å¯¼å
¥", Config.Id); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// åå§åDBå |
| | | /// </summary> |
| | | private void InitializeDbBlocks() |
| | | { |
| | | if (_server == null) |
| | | return; |
| | | |
| | | try |
| | | { |
| | | // æ ¹æ®é
置添å DBå |
| | | for (ushort i = 1; i <= Config.MemoryConfig.DBBlockCount; i++) |
| | | { |
| | | _server.AddDbBlock(i, Config.MemoryConfig.DBBlockSize); |
| | | _logger.LogDebug("已添å DBå: DB{DbNumber}, 大å°: {Size}", i, Config.MemoryConfig.DBBlockSize); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogWarning(ex, "åå§åDBåæ¶åçè¦å"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// ä»MemoryStoreåæ¥æ°æ®å°æå¡å¨ |
| | | /// </summary> |
| | | private void SynchronizeMemoryToServer() |
| | | { |
| | | if (_server == null) |
| | | return; |
| | | |
| | | try |
| | | { |
| | | var data = MemoryStore.Export(); |
| | | |
| | | // 忥Måº |
| | | if (data.ContainsKey("M")) |
| | | { |
| | | var mBytes = data["M"]; |
| | | for (int i = 0; i < Math.Min(mBytes.Length, Config.MemoryConfig.MRegionSize); i++) |
| | | { |
| | | _server.Write($"M{i}", mBytes[i]); |
| | | } |
| | | } |
| | | |
| | | // 忥Iåº |
| | | if (data.ContainsKey("I")) |
| | | { |
| | | var iBytes = data["I"]; |
| | | for (int i = 0; i < Math.Min(iBytes.Length, Config.MemoryConfig.IRegionSize); i++) |
| | | { |
| | | _server.Write($"I{i}", iBytes[i]); |
| | | } |
| | | } |
| | | |
| | | // 忥Qåº |
| | | if (data.ContainsKey("Q")) |
| | | { |
| | | var qBytes = data["Q"]; |
| | | for (int i = 0; i < Math.Min(qBytes.Length, Config.MemoryConfig.QRegionSize); i++) |
| | | { |
| | | _server.Write($"Q{i}", qBytes[i]); |
| | | } |
| | | } |
| | | |
| | | // 忥DBåº |
| | | if (data.ContainsKey("DB")) |
| | | { |
| | | var dbBytes = data["DB"]; |
| | | int offset = 0; |
| | | for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++) |
| | | { |
| | | int blockSize = Math.Min(Config.MemoryConfig.DBBlockSize, dbBytes.Length - offset); |
| | | for (int i = 0; i < blockSize; i++) |
| | | { |
| | | _server.Write($"DB{db}.DBD{i}", dbBytes[offset + i]); |
| | | } |
| | | offset += Config.MemoryConfig.DBBlockSize; |
| | | } |
| | | } |
| | | |
| | | _logger.LogDebug("å·²å°MemoryStoreæ°æ®åæ¥å°S7æå¡å¨"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogWarning(ex, "åæ¥æ°æ®å°æå¡å¨æ¶åçè¦å"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 仿å¡å¨åæ¥æ°æ®å°MemoryStore |
| | | /// </summary> |
| | | private void SynchronizeServerToMemory() |
| | | { |
| | | if (_server == null) |
| | | return; |
| | | |
| | | try |
| | | { |
| | | var data = new Dictionary<string, byte[]>(); |
| | | |
| | | // 读åMåº |
| | | var mResult = _server.Read("M0", (ushort)Config.MemoryConfig.MRegionSize); |
| | | if (mResult.IsSuccess) |
| | | { |
| | | data["M"] = mResult.Content; |
| | | } |
| | | |
| | | // 读åIåº |
| | | var iResult = _server.Read("I0", (ushort)Config.MemoryConfig.IRegionSize); |
| | | if (iResult.IsSuccess) |
| | | { |
| | | data["I"] = iResult.Content; |
| | | } |
| | | |
| | | // 读åQåº |
| | | var qResult = _server.Read("Q0", (ushort)Config.MemoryConfig.QRegionSize); |
| | | if (qResult.IsSuccess) |
| | | { |
| | | data["Q"] = qResult.Content; |
| | | } |
| | | |
| | | // 读åDBåº |
| | | var dbBytes = new List<byte>(); |
| | | for (ushort db = 1; db <= Config.MemoryConfig.DBBlockCount; db++) |
| | | { |
| | | var dbResult = _server.Read($"DB{db}.DBD0", (ushort)Config.MemoryConfig.DBBlockSize); |
| | | if (dbResult.IsSuccess) |
| | | { |
| | | dbBytes.AddRange(dbResult.Content); |
| | | } |
| | | } |
| | | data["DB"] = dbBytes.ToArray(); |
| | | |
| | | // 导å
¥å°MemoryStore |
| | | MemoryStore.Import(data); |
| | | |
| | | _logger.LogDebug("å·²å°S7æå¡å¨æ°æ®åæ¥å°MemoryStore"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogWarning(ex, "仿å¡å¨åæ¥æ°æ®æ¶åçè¦å"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å¢å 请æ±è®¡æ°å¹¶æ´æ°æ´»å¨æ¶é´ |
| | | /// </summary> |
| | | private void IncrementRequestCount() |
| | | { |
| | | State.TotalRequests++; |
| | | State.LastActivityTime = DateTime.Now; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// éæ¾èµæº |
| | | /// </summary> |
| | | public void Dispose() |
| | | { |
| | | Dispose(true); |
| | | GC.SuppressFinalize(this); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// éæ¾èµæº |
| | | /// </summary> |
| | | /// <param name="disposing">æ¯å¦æ£å¨éæ¾æç®¡èµæº</param> |
| | | protected virtual void Dispose(bool disposing) |
| | | { |
| | | if (!_disposed) |
| | | { |
| | | if (disposing) |
| | | { |
| | | Stop(); |
| | | MemoryStore?.Dispose(); |
| | | } |
| | | _disposed = true; |
| | | } |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Server.Controllers |
| | | { |
| | | /// <summary> |
| | | /// 客æ·ç«¯è¿æ¥ç®¡çæ§å¶å¨ |
| | | /// </summary> |
| | | [ApiController] |
| | | [Route("api/instances/{id}/[controller]")] |
| | | public class ClientsController : ControllerBase |
| | | { |
| | | private readonly ISimulatorInstanceManager _instanceManager; |
| | | private readonly ILogger<ClientsController> _logger; |
| | | |
| | | public ClientsController( |
| | | ISimulatorInstanceManager instanceManager, |
| | | ILogger<ClientsController> logger) |
| | | { |
| | | _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); |
| | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// è·åè¿æ¥ç客æ·ç«¯å表 |
| | | /// </summary> |
| | | [HttpGet] |
| | | [ProducesResponseType(typeof(List<S7ClientConnection>), StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult<List<S7ClientConnection>> GetConnectedClients(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | var state = instance.GetState(); |
| | | return Ok(state.Clients); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to get clients for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve clients" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æå¼æå®å®¢æ·ç«¯ |
| | | /// </summary> |
| | | [HttpDelete("{clientId}")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult DisconnectClient(string id, string clientId) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (string.IsNullOrWhiteSpace(clientId)) |
| | | { |
| | | return BadRequest(new { error = "Client ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | var state = instance.GetState(); |
| | | var client = state.Clients.FirstOrDefault(c => c.ClientId == clientId); |
| | | |
| | | if (client == null) |
| | | { |
| | | return NotFound(new { error = $"Client with ID '{clientId}' not found" }); |
| | | } |
| | | |
| | | // Note: The actual disconnection logic would need to be implemented in IS7ServerInstance |
| | | // For now, we're returning a success response indicating the operation was requested |
| | | // In a real implementation, you would call instance.DisconnectClient(clientId) |
| | | _logger.LogInformation("Disconnect request for client {ClientId} on instance {InstanceId}", clientId, id); |
| | | |
| | | return Ok(new |
| | | { |
| | | message = "Client disconnect requested", |
| | | clientId = clientId, |
| | | instanceId = id, |
| | | note = "Actual disconnection logic should be implemented in IS7ServerInstance" |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to disconnect client {ClientId} for instance {InstanceId}", clientId, id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to disconnect client" }); |
| | | } |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Server.Controllers |
| | | { |
| | | /// <summary> |
| | | /// å
åæä½æ§å¶å¨ |
| | | /// </summary> |
| | | [ApiController] |
| | | [Route("api/instances/{id}/[controller]")] |
| | | public class MemoryController : ControllerBase |
| | | { |
| | | private readonly ISimulatorInstanceManager _instanceManager; |
| | | private readonly ILogger<MemoryController> _logger; |
| | | |
| | | public MemoryController( |
| | | ISimulatorInstanceManager instanceManager, |
| | | ILogger<MemoryController> logger) |
| | | { |
| | | _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); |
| | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 读åå
åæ°æ® |
| | | /// </summary> |
| | | [HttpGet] |
| | | [ProducesResponseType(typeof(Dictionary<string, byte[]>), StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult<Dictionary<string, byte[]>> ReadMemory(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | var memoryData = instance.ExportMemory(); |
| | | return Ok(memoryData); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to read memory for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to read memory" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// åå
¥å
åæ°æ® |
| | | /// </summary> |
| | | [HttpPost] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public ActionResult WriteMemory(string id, [FromBody] Dictionary<string, byte[]> data) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (data == null || data.Count == 0) |
| | | { |
| | | return BadRequest(new { error = "Memory data is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | instance.ImportMemory(data); |
| | | return Ok(new { message = "Memory data written successfully" }); |
| | | } |
| | | catch (ArgumentException ex) |
| | | { |
| | | _logger.LogWarning(ex, "Invalid memory data for instance {InstanceId}", id); |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to write memory for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to write memory" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æ¸
空å
åæ°æ® |
| | | /// </summary> |
| | | [HttpDelete] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult ClearMemory(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | instance.ClearMemory(); |
| | | return Ok(new { message = "Memory cleared successfully" }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to clear memory for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to clear memory" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// ä¿åå
åå¿«ç
§ |
| | | /// </summary> |
| | | [HttpPost("save")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult SaveMemorySnapshot(string id, [FromQuery] string? snapshotName = null) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | var memoryData = instance.ExportMemory(); |
| | | var fileName = string.IsNullOrWhiteSpace(snapshotName) |
| | | ? $"snapshot_{id}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json" |
| | | : snapshotName; |
| | | |
| | | // In a real implementation, you would save this to a file system or database |
| | | // For now, we'll return the data that would be saved |
| | | return Ok(new |
| | | { |
| | | message = "Memory snapshot captured successfully", |
| | | fileName = fileName, |
| | | timestamp = DateTime.UtcNow, |
| | | dataSize = memoryData.Sum(kvp => kvp.Value.Length), |
| | | regions = memoryData.Keys.ToList() |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to save memory snapshot for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to save memory snapshot" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å è½½å
åå¿«ç
§ |
| | | /// </summary> |
| | | [HttpPost("load")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public ActionResult LoadMemorySnapshot(string id, [FromBody] Dictionary<string, byte[]> snapshotData) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (snapshotData == null || snapshotData.Count == 0) |
| | | { |
| | | return BadRequest(new { error = "Snapshot data is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | instance.ImportMemory(snapshotData); |
| | | return Ok(new |
| | | { |
| | | message = "Memory snapshot loaded successfully", |
| | | timestamp = DateTime.UtcNow, |
| | | regions = snapshotData.Keys.ToList() |
| | | }); |
| | | } |
| | | catch (ArgumentException ex) |
| | | { |
| | | _logger.LogWarning(ex, "Invalid snapshot data for instance {InstanceId}", id); |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to load memory snapshot for instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to load memory snapshot" }); |
| | | } |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Server.Controllers |
| | | { |
| | | /// <summary> |
| | | /// 仿çå¨å®ä¾ç®¡çæ§å¶å¨ |
| | | /// </summary> |
| | | [ApiController] |
| | | [Route("api/[controller]")] |
| | | public class SimulatorInstancesController : ControllerBase |
| | | { |
| | | private readonly ISimulatorInstanceManager _instanceManager; |
| | | private readonly ILogger<SimulatorInstancesController> _logger; |
| | | |
| | | public SimulatorInstancesController( |
| | | ISimulatorInstanceManager instanceManager, |
| | | ILogger<SimulatorInstancesController> logger) |
| | | { |
| | | _instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); |
| | | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// è·åææå®ä¾å表 |
| | | /// </summary> |
| | | [HttpGet] |
| | | [ProducesResponseType(typeof(IEnumerable<InstanceState>), StatusCodes.Status200OK)] |
| | | public ActionResult<IEnumerable<InstanceState>> GetAllInstances() |
| | | { |
| | | try |
| | | { |
| | | var instances = _instanceManager.GetAllInstances(); |
| | | var states = instances.Select(i => i.GetState()).ToList(); |
| | | return Ok(states); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to get all instances"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve instances" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å建æ°å®ä¾ |
| | | /// </summary> |
| | | [HttpPost] |
| | | [ProducesResponseType(typeof(InstanceState), StatusCodes.Status201Created)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult<InstanceState>> CreateInstance([FromBody] InstanceConfig config) |
| | | { |
| | | try |
| | | { |
| | | if (config == null) |
| | | { |
| | | return BadRequest(new { error = "Instance configuration is required" }); |
| | | } |
| | | |
| | | if (string.IsNullOrWhiteSpace(config.Id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (string.IsNullOrWhiteSpace(config.Name)) |
| | | { |
| | | return BadRequest(new { error = "Instance name is required" }); |
| | | } |
| | | |
| | | if (_instanceManager.InstanceExists(config.Id)) |
| | | { |
| | | return Conflict(new { error = $"Instance with ID '{config.Id}' already exists" }); |
| | | } |
| | | |
| | | if (config.Port <= 0 || config.Port > 65535) |
| | | { |
| | | return BadRequest(new { error = "Port must be between 1 and 65535" }); |
| | | } |
| | | |
| | | var instance = await _instanceManager.CreateInstanceAsync(config); |
| | | var state = instance.GetState(); |
| | | |
| | | return CreatedAtAction( |
| | | nameof(GetInstance), |
| | | new { id = config.Id }, |
| | | state); |
| | | } |
| | | catch (ArgumentException ex) |
| | | { |
| | | _logger.LogWarning(ex, "Invalid instance configuration"); |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to create instance"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to create instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// è·åæå®å®ä¾è¯¦æ
|
| | | /// </summary> |
| | | [HttpGet("{id}")] |
| | | [ProducesResponseType(typeof(InstanceState), StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public ActionResult<InstanceState> GetInstance(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | var state = instance.GetState(); |
| | | return Ok(state); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to get instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to retrieve instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// æ´æ°å®ä¾é
ç½® |
| | | /// </summary> |
| | | [HttpPut("{id}")] |
| | | [ProducesResponseType(typeof(InstanceState), StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public async Task<ActionResult<InstanceState>> UpdateInstance(string id, [FromBody] InstanceConfig config) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (config == null) |
| | | { |
| | | return BadRequest(new { error = "Instance configuration is required" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | if (instance == null) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | // Validate port if changed |
| | | if (config.Port <= 0 || config.Port > 65535) |
| | | { |
| | | return BadRequest(new { error = "Port must be between 1 and 65535" }); |
| | | } |
| | | |
| | | // Delete existing instance and recreate with new config |
| | | // Note: This is a simplified approach. In production, you might want to support hot-reload |
| | | bool wasRunning = instance.GetState().Status == Core.Enums.InstanceStatus.Running; |
| | | await _instanceManager.DeleteInstanceAsync(id, deleteConfig: false); |
| | | |
| | | config.Id = id; // Ensure ID matches route parameter |
| | | var newInstance = await _instanceManager.CreateInstanceAsync(config); |
| | | |
| | | if (wasRunning) |
| | | { |
| | | await _instanceManager.StartInstanceAsync(id); |
| | | } |
| | | |
| | | var state = newInstance.GetState(); |
| | | return Ok(state); |
| | | } |
| | | catch (ArgumentException ex) |
| | | { |
| | | _logger.LogWarning(ex, "Invalid instance configuration"); |
| | | return BadRequest(new { error = ex.Message }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to update instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to update instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å é¤å®ä¾ |
| | | /// </summary> |
| | | [HttpDelete("{id}")] |
| | | [ProducesResponseType(StatusCodes.Status204NoContent)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public async Task<IActionResult> DeleteInstance(string id, [FromQuery] bool deleteConfig = true) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (!_instanceManager.InstanceExists(id)) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | await _instanceManager.DeleteInstanceAsync(id, deleteConfig); |
| | | return NoContent(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to delete instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to delete instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å¯å¨å®ä¾ |
| | | /// </summary> |
| | | [HttpPost("{id}/start")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult<InstanceState>> StartInstance(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (!_instanceManager.InstanceExists(id)) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | bool success = await _instanceManager.StartInstanceAsync(id); |
| | | if (!success) |
| | | { |
| | | return BadRequest(new { error = "Failed to start instance" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | var state = instance?.GetState(); |
| | | return Ok(state); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to start instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to start instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 忢å®ä¾ |
| | | /// </summary> |
| | | [HttpPost("{id}/stop")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | public async Task<ActionResult<InstanceState>> StopInstance(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (!_instanceManager.InstanceExists(id)) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | await _instanceManager.StopInstanceAsync(id); |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | var state = instance?.GetState(); |
| | | return Ok(state); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to stop instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to stop instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// éå¯å®ä¾ |
| | | /// </summary> |
| | | [HttpPost("{id}/restart")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | [ProducesResponseType(StatusCodes.Status404NotFound)] |
| | | [ProducesResponseType(StatusCodes.Status400BadRequest)] |
| | | public async Task<ActionResult<InstanceState>> RestartInstance(string id) |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest(new { error = "Instance ID is required" }); |
| | | } |
| | | |
| | | if (!_instanceManager.InstanceExists(id)) |
| | | { |
| | | return NotFound(new { error = $"Instance with ID '{id}' not found" }); |
| | | } |
| | | |
| | | bool success = await _instanceManager.RestartInstanceAsync(id); |
| | | if (!success) |
| | | { |
| | | return BadRequest(new { error = "Failed to restart instance" }); |
| | | } |
| | | |
| | | var instance = _instanceManager.GetInstance(id); |
| | | var state = instance?.GetState(); |
| | | return Ok(state); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to restart instance {InstanceId}", id); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to restart instance" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å¯å¨ææèªå¨å¯å¨å®ä¾ |
| | | /// </summary> |
| | | [HttpPost("start-all")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | public async Task<ActionResult> StartAllAutoStartInstances() |
| | | { |
| | | try |
| | | { |
| | | await _instanceManager.LoadSavedInstancesAsync(autoStart: true); |
| | | return Ok(new { |
| | | message = "Started all auto-start instances", |
| | | runningCount = _instanceManager.GetRunningInstanceCount() |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to start all instances"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to start instances" }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 忢ææå®ä¾ |
| | | /// </summary> |
| | | [HttpPost("stop-all")] |
| | | [ProducesResponseType(StatusCodes.Status200OK)] |
| | | public async Task<ActionResult> StopAllInstances() |
| | | { |
| | | try |
| | | { |
| | | await _instanceManager.StopAllInstancesAsync(); |
| | | return Ok(new { |
| | | message = "Stopped all instances", |
| | | runningCount = _instanceManager.GetRunningInstanceCount() |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to stop all instances"); |
| | | return StatusCode(StatusCodes.Status500InternalServerError, new { error = "Failed to stop instances" }); |
| | | } |
| | | } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @page |
| | | @model CreateModel |
| | | @{ |
| | | ViewData["Title"] = "å建å®ä¾"; |
| | | } |
| | | |
| | | <div class="row justify-content-center"> |
| | | <div class="col-lg-8"> |
| | | <div class="card shadow"> |
| | | <div class="card-header bg-primary text-white"> |
| | | <h4 class="mb-0"> |
| | | <i class="bi bi-plus-circle me-2"></i>å建æ°å®ä¾ |
| | | </h4> |
| | | </div> |
| | | <div class="card-body"> |
| | | <form method="post" id="createForm"> |
| | | <div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div> |
| | | |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <h5 class="mb-3"> |
| | | <i class="bi bi-info-circle me-2"></i>åºæ¬ä¿¡æ¯ |
| | | </h5> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.Id" class="form-label required">å®ä¾ID</label> |
| | | <input asp-for="Input.Id" class="form-control" placeholder="ä¾å¦: plc-simulator-1" /> |
| | | <span asp-validation-for="Input.Id" class="text-danger"></span> |
| | | <small class="text-muted">å¯ä¸æ è¯ç¬¦ï¼åªè½å
å«åæ¯ãæ°åãä¸å线åè¿å符</small> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.Name" class="form-label required">å®ä¾åç§°</label> |
| | | <input asp-for="Input.Name" class="form-control" placeholder="ä¾å¦: 1å·PLC仿çå¨" /> |
| | | <span asp-validation-for="Input.Name" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.PLCType" class="form-label required">PLCåå·</label> |
| | | <select asp-for="Input.PLCType" class="form-select"> |
| | | <option value="0">S7-200 Smart</option> |
| | | <option value="1" selected>S7-1200</option> |
| | | <option value="2">S7-1500</option> |
| | | <option value="3">S7-300</option> |
| | | <option value="4">S7-400</option> |
| | | </select> |
| | | <span asp-validation-for="Input.PLCType" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.Port" class="form-label required">çå¬ç«¯å£</label> |
| | | <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" placeholder="102" /> |
| | | <span asp-validation-for="Input.Port" class="text-danger"></span> |
| | | <small class="text-muted">S7é»è®¤ç«¯å£ä¸º102</small> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.ActivationKey" class="form-label">HSLæ¿æ´»ç </label> |
| | | <input asp-for="Input.ActivationKey" class="form-control" placeholder="å¯é" /> |
| | | <span asp-validation-for="Input.ActivationKey" class="text-danger"></span> |
| | | <small class="text-muted">ç¨äºHSLåºçå䏿¿æ´»ï¼ç空使ç¨å
è´¹ç</small> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.AutoStart" class="form-label">èªå¨å¯å¨</label> |
| | | <div class="form-check mt-2"> |
| | | <input asp-for="Input.AutoStart" class="form-check-input" /> |
| | | <label asp-for="Input.AutoStart" class="form-check-label"> |
| | | æå¡å¨å¯å¨æ¶èªå¨å¯å¨æ¤å®ä¾ |
| | | </label> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <hr class="my-4"> |
| | | |
| | | <!-- å
åé
ç½® --> |
| | | <div class="d-flex justify-content-between align-items-center mb-3"> |
| | | <h5 class="mb-0"> |
| | | <i class="bi bi-memory me-2"></i>å
åé
ç½® |
| | | </h5> |
| | | <button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetMemoryConfig()"> |
| | | <i class="bi bi-arrow-counterclockwise me-1"></i>é置为é»è®¤å¼ |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.MRegionSize" class="form-label">Måºå大å°</label> |
| | | <input asp-for="Input.MRegionSize" class="form-control" type="number" min="0" placeholder="1024" /> |
| | | <span asp-validation-for="Input.MRegionSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.IRegionSize" class="form-label">Iåºå大å°</label> |
| | | <input asp-for="Input.IRegionSize" class="form-control" type="number" min="0" placeholder="256" /> |
| | | <span asp-validation-for="Input.IRegionSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.QRegionSize" class="form-label">Qåºå大å°</label> |
| | | <input asp-for="Input.QRegionSize" class="form-control" type="number" min="0" placeholder="256" /> |
| | | <span asp-validation-for="Input.QRegionSize" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.DBBlockCount" class="form-label">DBåæ°é</label> |
| | | <input asp-for="Input.DBBlockCount" class="form-control" type="number" min="0" placeholder="100" /> |
| | | <span asp-validation-for="Input.DBBlockCount" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.DBBlockSize" class="form-label">DBå大å°</label> |
| | | <input asp-for="Input.DBBlockSize" class="form-control" type="number" min="0" placeholder="1024" /> |
| | | <span asp-validation-for="Input.DBBlockSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-2"> |
| | | <label asp-for="Input.TRegionCount" class="form-label">宿¶å¨æ°é</label> |
| | | <input asp-for="Input.TRegionCount" class="form-control" type="number" min="0" placeholder="64" /> |
| | | <span asp-validation-for="Input.TRegionCount" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-2"> |
| | | <label asp-for="Input.CRegionCount" class="form-label">计æ°å¨æ°é</label> |
| | | <input asp-for="Input.CRegionCount" class="form-control" type="number" min="0" placeholder="64" /> |
| | | <span asp-validation-for="Input.CRegionCount" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <hr class="my-4"> |
| | | |
| | | <!-- æäº¤æé® --> |
| | | <div class="d-flex justify-content-between"> |
| | | <a asp-page="/Index" class="btn btn-secondary"> |
| | | <i class="bi bi-x-lg me-1"></i>åæ¶ |
| | | </a> |
| | | <button type="submit" class="btn btn-primary"> |
| | | <i class="bi bi-check-lg me-1"></i>å建å®ä¾ |
| | | </button> |
| | | </div> |
| | | </form> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | @section Scripts { |
| | | @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} |
| | | |
| | | <script> |
| | | function resetMemoryConfig() { |
| | | document.querySelector('#Input_MRegionSize').value = '1024'; |
| | | document.querySelector('#Input_IRegionSize').value = '256'; |
| | | document.querySelector('#Input_QRegionSize').value = '256'; |
| | | document.querySelector('#Input_DBBlockCount').value = '100'; |
| | | document.querySelector('#Input_DBBlockSize').value = '1024'; |
| | | document.querySelector('#Input_TRegionCount').value = '64'; |
| | | document.querySelector('#Input_CRegionCount').value = '64'; |
| | | } |
| | | </script> |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.AspNetCore.Mvc.RazorPages; |
| | | using System.ComponentModel.DataAnnotations; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Web.Pages; |
| | | |
| | | /// <summary> |
| | | /// å建å®ä¾é¡µ |
| | | /// </summary> |
| | | public class CreateModel : PageModel |
| | | { |
| | | private readonly ILogger<CreateModel> _logger; |
| | | private readonly HttpClient _httpClient; |
| | | |
| | | public CreateModel(ILogger<CreateModel> logger, IHttpClientFactory httpClientFactory) |
| | | { |
| | | _logger = logger; |
| | | _httpClient = httpClientFactory.CreateClient(); |
| | | _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}"); |
| | | } |
| | | |
| | | [BindProperty] |
| | | public CreateInstanceInputModel Input { get; set; } = new(); |
| | | |
| | | public void OnGet() |
| | | { |
| | | _logger.LogInformation("Loading create instance page"); |
| | | } |
| | | |
| | | public async Task<IActionResult> OnPostAsync() |
| | | { |
| | | if (!ModelState.IsValid) |
| | | { |
| | | return Page(); |
| | | } |
| | | |
| | | try |
| | | { |
| | | var config = new InstanceConfig |
| | | { |
| | | Id = Input.Id, |
| | | Name = Input.Name, |
| | | PLCType = Input.PLCType, |
| | | Port = Input.Port, |
| | | ActivationKey = Input.ActivationKey ?? string.Empty, |
| | | AutoStart = Input.AutoStart, |
| | | MemoryConfig = new MemoryRegionConfig |
| | | { |
| | | MRegionSize = Input.MRegionSize > 0 ? Input.MRegionSize : 1024, |
| | | DBBlockCount = Input.DBBlockCount > 0 ? Input.DBBlockCount : 100, |
| | | DBBlockSize = Input.DBBlockSize > 0 ? Input.DBBlockSize : 1024, |
| | | IRegionSize = Input.IRegionSize > 0 ? Input.IRegionSize : 256, |
| | | QRegionSize = Input.QRegionSize > 0 ? Input.QRegionSize : 256, |
| | | TRegionCount = Input.TRegionCount > 0 ? Input.TRegionCount : 64, |
| | | CRegionCount = Input.CRegionCount > 0 ? Input.CRegionCount : 64 |
| | | } |
| | | }; |
| | | |
| | | var response = await _httpClient.PostAsJsonAsync("/api/SimulatorInstances", config); |
| | | |
| | | if (response.IsSuccessStatusCode) |
| | | { |
| | | _logger.LogInformation("Instance {InstanceId} created successfully", Input.Id); |
| | | TempData["SuccessMessage"] = $"å®ä¾ \"{Input.Id}\" å建æå!"; |
| | | return RedirectToPage("/Index"); |
| | | } |
| | | else if (response.StatusCode == System.Net.HttpStatusCode.Conflict) |
| | | { |
| | | ModelState.AddModelError(string.Empty, "å®ä¾IDå·²åå¨ï¼è¯·ä½¿ç¨å
¶ä»ID"); |
| | | } |
| | | else |
| | | { |
| | | var error = await response.Content.ReadFromJsonAsync<object>(); |
| | | ModelState.AddModelError(string.Empty, error?.ToString() ?? "å建å®ä¾å¤±è´¥"); |
| | | } |
| | | } |
| | | catch (HttpRequestException ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to create instance"); |
| | | ModelState.AddModelError(string.Empty, "ç½ç»é误ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | |
| | | return Page(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// å建å®ä¾è¾å
¥æ¨¡å |
| | | /// </summary> |
| | | public class CreateInstanceInputModel |
| | | { |
| | | /// <summary> |
| | | /// å®ä¾ID |
| | | /// </summary> |
| | | [Required(ErrorMessage = "å®ä¾IDä¸è½ä¸ºç©º")] |
| | | [StringLength(50, MinimumLength = 1, ErrorMessage = "å®ä¾IDé¿åº¦å¿
é¡»å¨1-50个å符ä¹é´")] |
| | | [RegularExpression("^[a-zA-Z0-9_-]+$", ErrorMessage = "å®ä¾IDåªè½å
å«åæ¯ãæ°åãä¸å线åè¿å符")] |
| | | public string Id { get; set; } = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// å®ä¾åç§° |
| | | /// </summary> |
| | | [Required(ErrorMessage = "å®ä¾åç§°ä¸è½ä¸ºç©º")] |
| | | [StringLength(100, ErrorMessage = "å®ä¾åç§°ä¸è½è¶
è¿100个å符")] |
| | | public string Name { get; set; } = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// PLCåå· |
| | | /// </summary> |
| | | [Required(ErrorMessage = "PLCåå·ä¸è½ä¸ºç©º")] |
| | | public SiemensPLCType PLCType { get; set; } = SiemensPLCType.S71200; |
| | | |
| | | /// <summary> |
| | | /// çå¬ç«¯å£ |
| | | /// </summary> |
| | | [Required(ErrorMessage = "端å£ä¸è½ä¸ºç©º")] |
| | | [Range(1, 65535, ErrorMessage = "端å£å¿
é¡»å¨1-65535ä¹é´")] |
| | | public int Port { get; set; } = 102; |
| | | |
| | | /// <summary> |
| | | /// HSLæ¿æ´»ç |
| | | /// </summary> |
| | | [StringLength(200, ErrorMessage = "æ¿æ´»ç ä¸è½è¶
è¿200个å符")] |
| | | public string? ActivationKey { get; set; } |
| | | |
| | | /// <summary> |
| | | /// èªå¨å¯å¨ |
| | | /// </summary> |
| | | public bool AutoStart { get; set; } = false; |
| | | |
| | | // å
åé
ç½® |
| | | public int MRegionSize { get; set; } |
| | | public int DBBlockCount { get; set; } |
| | | public int DBBlockSize { get; set; } |
| | | public int IRegionSize { get; set; } |
| | | public int QRegionSize { get; set; } |
| | | public int TRegionCount { get; set; } |
| | | public int CRegionCount { get; set; } |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @page |
| | | @model DetailsModel |
| | | @{ |
| | | ViewData["Title"] = $"å®ä¾è¯¦æ
- {Model.Instance?.InstanceId}"; |
| | | } |
| | | |
| | | @if (Model.Instance == null) |
| | | { |
| | | <div class="alert alert-warning" role="alert"> |
| | | <i class="bi bi-exclamation-triangle-fill me-2"></i> |
| | | å®ä¾ä¸å卿å 载失败 |
| | | </div> |
| | | <a asp-page="/Index" class="btn btn-primary"> |
| | | <i class="bi bi-arrow-left me-1"></i>è¿åå表 |
| | | </a> |
| | | } |
| | | else |
| | | { |
| | | <div class="d-flex justify-content-between align-items-center mb-4"> |
| | | <div> |
| | | <h2 class="mb-0"> |
| | | <i class="bi bi-info-circle me-2"></i>å®ä¾è¯¦æ
: @Model.Instance.InstanceId |
| | | </h2> |
| | | <p class="text-muted mb-0 mt-1">æ¥çå管ç仿çå¨å®ä¾ç详ç»ä¿¡æ¯</p> |
| | | </div> |
| | | <div> |
| | | <a asp-page="/Edit" asp-route-id="@Model.Instance.InstanceId" class="btn btn-primary me-2"> |
| | | <i class="bi bi-pencil-fill me-1"></i>ç¼è¾ |
| | | </a> |
| | | <a asp-page="/Index" class="btn btn-secondary"> |
| | | <i class="bi bi-arrow-left me-1"></i>è¿åå表 |
| | | </a> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ç¶æå¡ç --> |
| | | <div class="row mb-4"> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">ç¶æ</h6> |
| | | <h4 class="card-title"> |
| | | <span class="badge @GetStatusBadgeClass(Model.Instance.Status) fs-6"> |
| | | @GetStatusText(Model.Instance.Status) |
| | | </span> |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">è¿æ¥å®¢æ·ç«¯</h6> |
| | | <h4 class="card-title"> |
| | | <i class="bi bi-people-fill text-primary me-1"></i>@Model.Instance.ClientCount |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">æ»è¯·æ±æ°</h6> |
| | | <h4 class="card-title"> |
| | | <i class="bi bi-graph-up-arrow text-success me-1"></i>@Model.Instance.TotalRequests |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="col-md-3"> |
| | | <div class="card text-center"> |
| | | <div class="card-body"> |
| | | <h6 class="card-subtitle mb-2 text-muted">è¿è¡æ¶é´</h6> |
| | | <h4 class="card-title" id="runningTime"> |
| | | @if (Model.Instance.StartTime.HasValue) |
| | | { |
| | | <span>@GetRunningTime(Model.Instance.StartTime.Value)</span> |
| | | } |
| | | else |
| | | { |
| | | <span class="text-muted">-</span> |
| | | } |
| | | </h4> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å®ä¾ä¿¡æ¯ --> |
| | | <div class="card mb-4"> |
| | | <div class="card-header"> |
| | | <h5 class="mb-0"> |
| | | <i class="bi bi-cpu me-2"></i>å®ä¾ä¿¡æ¯ |
| | | </h5> |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="row"> |
| | | <div class="col-md-6 mb-3"> |
| | | <strong>å®ä¾ID:</strong> |
| | | <div class="text-muted">@Model.Instance.InstanceId</div> |
| | | </div> |
| | | <div class="col-md-6 mb-3"> |
| | | <strong>å¯å¨æ¶é´:</strong> |
| | | <div class="text-muted">@(Model.Instance.StartTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</div> |
| | | </div> |
| | | <div class="col-md-6 mb-3"> |
| | | <strong>æåæ´»å¨:</strong> |
| | | <div class="text-muted">@(Model.Instance.LastActivityTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-")</div> |
| | | </div> |
| | | <div class="col-md-6 mb-3"> |
| | | <strong>é误信æ¯:</strong> |
| | | <div class="text-muted">@(Model.Instance.ErrorMessage ?? "æ ")</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="card-footer"> |
| | | <div class="d-flex gap-2"> |
| | | @if (Model.Instance.Status == WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running) |
| | | { |
| | | <button class="btn btn-warning" onclick="stopInstance('@Model.Instance.InstanceId')"> |
| | | <i class="bi bi-stop-fill me-1"></i>忢 |
| | | </button> |
| | | <button class="btn btn-info" onclick="restartInstance('@Model.Instance.InstanceId')"> |
| | | <i class="bi bi-arrow-clockwise me-1"></i>éå¯ |
| | | </button> |
| | | } |
| | | else if (Model.Instance.Status == WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped) |
| | | { |
| | | <button class="btn btn-success" onclick="startInstance('@Model.Instance.InstanceId')"> |
| | | <i class="bi bi-play-fill me-1"></i>å¯å¨ |
| | | </button> |
| | | } |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- è¿æ¥ç客æ·ç«¯ --> |
| | | <div class="card"> |
| | | <div class="card-header d-flex justify-content-between align-items-center"> |
| | | <h5 class="mb-0"> |
| | | <i class="bi bi-people me-2"></i>è¿æ¥ç客æ·ç«¯ |
| | | </h5> |
| | | <span class="badge bg-primary">@Model.Clients.Count</span> |
| | | </div> |
| | | <div class="card-body"> |
| | | @if (Model.Clients.Any()) |
| | | { |
| | | <div class="table-responsive"> |
| | | <table class="table table-striped table-hover client-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>客æ·ç«¯ID</th> |
| | | <th>è¿ç¨å°å</th> |
| | | <th>è¿æ¥æ¶é´</th> |
| | | <th>æåæ´»å¨</th> |
| | | <th>æä½</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | @foreach (var client in Model.Clients) |
| | | { |
| | | <tr> |
| | | <td><code>@client.ClientId</code></td> |
| | | <td>@client.RemoteEndPoint</td> |
| | | <td>@client.ConnectedTime.ToString("yyyy-MM-dd HH:mm:ss")</td> |
| | | <td>@client.LastActivityTime.ToString("yyyy-MM-dd HH:mm:ss")</td> |
| | | <td> |
| | | <button class="btn btn-sm btn-danger" onclick="disconnectClient('@Model.Instance.InstanceId', '@client.ClientId')"> |
| | | <i class="bi bi-x-circle-fill"></i>æå¼ |
| | | </button> |
| | | </td> |
| | | </tr> |
| | | } |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | } |
| | | else |
| | | { |
| | | <div class="text-center text-muted py-4"> |
| | | <i class="bi bi-inbox" style="font-size: 2rem;"></i> |
| | | <p class="mt-2">ææ è¿æ¥ç客æ·ç«¯</p> |
| | | </div> |
| | | } |
| | | </div> |
| | | </div> |
| | | } |
| | | |
| | | @{ |
| | | string GetStatusBadgeClass(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status) |
| | | { |
| | | return status switch |
| | | { |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "bg-success", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "bg-secondary", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "bg-info", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "bg-warning text-dark", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "bg-danger", |
| | | _ => "bg-secondary" |
| | | }; |
| | | } |
| | | |
| | | string GetStatusText(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status) |
| | | { |
| | | return status switch |
| | | { |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "è¿è¡ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "已忢", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "å¯å¨ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "忢ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "é误", |
| | | _ => status.ToString() |
| | | }; |
| | | } |
| | | |
| | | string GetRunningTime(DateTime startTime) |
| | | { |
| | | var span = DateTime.Now - startTime; |
| | | if (span.TotalDays >= 1) |
| | | return $"{(int)span.TotalDays}天 {span.Hours}å°æ¶"; |
| | | else if (span.TotalHours >= 1) |
| | | return $"{(int)span.TotalHours}å°æ¶ {span.Minutes}åé"; |
| | | else if (span.TotalMinutes >= 1) |
| | | return $"{(int)span.TotalMinutes}åé {span.Seconds}ç§"; |
| | | else |
| | | return $"{span.Seconds}ç§"; |
| | | } |
| | | } |
| | | |
| | | @section Scripts { |
| | | <script> |
| | | // Auto refresh |
| | | const instanceId = '@Model.Instance?.InstanceId'; |
| | | let refreshInterval = null; |
| | | |
| | | document.addEventListener('DOMContentLoaded', function () { |
| | | if (instanceId) { |
| | | startAutoRefresh(); |
| | | // Update running time every second |
| | | setInterval(updateRunningTime, 1000); |
| | | } |
| | | }); |
| | | |
| | | function startAutoRefresh() { |
| | | refreshInterval = setInterval(async () => { |
| | | try { |
| | | const [instance, clients] = await Promise.all([ |
| | | fetch(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(instanceId)}`).then(r => r.json()), |
| | | fetch(`${API_BASE_URL}/instances/${encodeURIComponent(instanceId)}/Clients`).then(r => r.json()) |
| | | ]); |
| | | |
| | | // Update status badges |
| | | updateInstanceStatus(instance); |
| | | } catch (error) { |
| | | console.error('Auto refresh failed:', error); |
| | | } |
| | | }, 5000); |
| | | } |
| | | |
| | | function updateInstanceStatus(instance) { |
| | | // You can add logic here to update UI elements without full page reload |
| | | // For now, we rely on page refresh for major changes |
| | | } |
| | | |
| | | function updateRunningTime() { |
| | | const startTimeElement = document.querySelector('#runningTime span'); |
| | | if (startTimeElement && startTimeElement.textContent !== '-') { |
| | | // This would require storing the start time and calculating |
| | | // For simplicity, we'll skip the real-time update |
| | | } |
| | | } |
| | | |
| | | async function startInstance(id) { |
| | | confirmAction(`ç¡®å®è¦å¯å¨å®ä¾ "${id}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/start`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast('å¯å¨å½ä»¤å·²åé', 'success'); |
| | | setTimeout(() => location.reload(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to start instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function stopInstance(id) { |
| | | confirmAction(`ç¡®å®è¦åæ¢å®ä¾ "${id}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/stop`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast('忢å½ä»¤å·²åé', 'success'); |
| | | setTimeout(() => location.reload(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to stop instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function restartInstance(id) { |
| | | confirmAction(`ç¡®å®è¦éå¯å®ä¾ "${id}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/restart`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast('éå¯å½ä»¤å·²åé', 'success'); |
| | | setTimeout(() => location.reload(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to restart instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function disconnectClient(instanceId, clientId) { |
| | | confirmAction(`ç¡®å®è¦æå¼å®¢æ·ç«¯ "${clientId}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/instances/${encodeURIComponent(instanceId)}/Clients/${encodeURIComponent(clientId)}`, { |
| | | method: 'DELETE' |
| | | }); |
| | | showToast('客æ·ç«¯å·²æå¼', 'success'); |
| | | setTimeout(() => location.reload(), 500); |
| | | } catch (error) { |
| | | console.error('Failed to disconnect client:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // Stop auto-refresh when page is hidden |
| | | document.addEventListener('visibilitychange', function () { |
| | | if (document.hidden) { |
| | | if (refreshInterval) clearInterval(refreshInterval); |
| | | } else { |
| | | startAutoRefresh(); |
| | | } |
| | | }); |
| | | </script> |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.AspNetCore.Mvc.RazorPages; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Web.Pages; |
| | | |
| | | /// <summary> |
| | | /// å®ä¾è¯¦æ
页 |
| | | /// </summary> |
| | | public class DetailsModel : PageModel |
| | | { |
| | | private readonly ILogger<DetailsModel> _logger; |
| | | private readonly HttpClient _httpClient; |
| | | |
| | | public DetailsModel(ILogger<DetailsModel> logger, IHttpClientFactory httpClientFactory) |
| | | { |
| | | _logger = logger; |
| | | _httpClient = httpClientFactory.CreateClient(); |
| | | _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}"); |
| | | } |
| | | |
| | | public InstanceState? Instance { get; private set; } |
| | | |
| | | public List<S7ClientConnection> Clients { get; private set; } = new(); |
| | | |
| | | public async Task<IActionResult> OnGetAsync(string id) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest("å®ä¾IDä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}"); |
| | | |
| | | if (response.IsSuccessStatusCode) |
| | | { |
| | | Instance = await response.Content.ReadFromJsonAsync<InstanceState>(); |
| | | |
| | | // Load clients |
| | | var clientsResponse = await _httpClient.GetAsync($"/api/instances/{Uri.EscapeDataString(id)}/Clients"); |
| | | if (clientsResponse.IsSuccessStatusCode) |
| | | { |
| | | Clients = await clientsResponse.Content.ReadFromJsonAsync<List<S7ClientConnection>>() ?? new(); |
| | | } |
| | | } |
| | | else if (response.StatusCode == System.Net.HttpStatusCode.NotFound) |
| | | { |
| | | return NotFound($"å®ä¾ \"{id}\" ä¸åå¨"); |
| | | } |
| | | } |
| | | catch (HttpRequestException ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to load instance details for {InstanceId}", id); |
| | | ModelState.AddModelError(string.Empty, "å è½½å®ä¾è¯¦æ
失败ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | |
| | | return Page(); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @page |
| | | @model EditModel |
| | | @{ |
| | | ViewData["Title"] = $"ç¼è¾å®ä¾ - {Model.Input.Id}"; |
| | | } |
| | | |
| | | <div class="row justify-content-center"> |
| | | <div class="col-lg-8"> |
| | | <div class="card shadow"> |
| | | <div class="card-header bg-warning text-dark"> |
| | | <h4 class="mb-0"> |
| | | <i class="bi bi-pencil-square me-2"></i>ç¼è¾å®ä¾: @Model.Input.Id |
| | | </h4> |
| | | </div> |
| | | <div class="card-body"> |
| | | @if (Model.IsRunning) |
| | | { |
| | | <div class="alert alert-warning d-flex align-items-center" role="alert"> |
| | | <i class="bi bi-exclamation-triangle-fill flex-shrink-0 me-2"></i> |
| | | <div> |
| | | <strong>è¦å:</strong> å®ä¾æ£å¨è¿è¡ä¸ãæäºé
置修æ¹å¯è½éè¦éå¯å®ä¾æè½çæã |
| | | </div> |
| | | </div> |
| | | } |
| | | |
| | | <form method="post" id="editForm"> |
| | | <div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div> |
| | | |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <h5 class="mb-3"> |
| | | <i class="bi bi-info-circle me-2"></i>åºæ¬ä¿¡æ¯ |
| | | </h5> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label class="form-label">å®ä¾ID</label> |
| | | <input type="text" class="form-control" value="@Model.Input.Id" disabled /> |
| | | <small class="text-muted">å®ä¾IDä¸å¯ä¿®æ¹</small> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.Name" class="form-label required">å®ä¾åç§°</label> |
| | | <input asp-for="Input.Name" class="form-control" /> |
| | | <span asp-validation-for="Input.Name" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.PLCType" class="form-label required">PLCåå·</label> |
| | | @if (Model.IsRunning) |
| | | { |
| | | <select asp-for="Input.PLCType" class="form-select" disabled> |
| | | <option value="0">S7-200 Smart</option> |
| | | <option value="1">S7-1200</option> |
| | | <option value="2">S7-1500</option> |
| | | <option value="3">S7-300</option> |
| | | <option value="4">S7-400</option> |
| | | </select> |
| | | <small class="text-muted">è¿è¡ä¸ä¸å¯ä¿®æ¹</small> |
| | | } |
| | | else |
| | | { |
| | | <select asp-for="Input.PLCType" class="form-select"> |
| | | <option value="0">S7-200 Smart</option> |
| | | <option value="1">S7-1200</option> |
| | | <option value="2">S7-1500</option> |
| | | <option value="3">S7-300</option> |
| | | <option value="4">S7-400</option> |
| | | </select> |
| | | } |
| | | <span asp-validation-for="Input.PLCType" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.Port" class="form-label required">çå¬ç«¯å£</label> |
| | | @if (Model.IsRunning) |
| | | { |
| | | <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" disabled /> |
| | | <small class="text-muted">è¿è¡ä¸ä¸å¯ä¿®æ¹</small> |
| | | } |
| | | else |
| | | { |
| | | <input asp-for="Input.Port" class="form-control" type="number" min="1" max="65535" /> |
| | | } |
| | | <span asp-validation-for="Input.Port" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.ActivationKey" class="form-label">HSLæ¿æ´»ç </label> |
| | | <input asp-for="Input.ActivationKey" class="form-control" placeholder="å¯é" /> |
| | | <span asp-validation-for="Input.ActivationKey" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-6"> |
| | | <label asp-for="Input.AutoStart" class="form-label">èªå¨å¯å¨</label> |
| | | <div class="form-check mt-2"> |
| | | <input asp-for="Input.AutoStart" class="form-check-input" /> |
| | | <label asp-for="Input.AutoStart" class="form-check-label"> |
| | | æå¡å¨å¯å¨æ¶èªå¨å¯å¨æ¤å®ä¾ |
| | | </label> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <hr class="my-4"> |
| | | |
| | | <!-- å
åé
ç½® --> |
| | | <div class="d-flex justify-content-between align-items-center mb-3"> |
| | | <h5 class="mb-0"> |
| | | <i class="bi bi-memory me-2"></i>å
åé
ç½® |
| | | </h5> |
| | | <button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetMemoryConfig()"> |
| | | <i class="bi bi-arrow-counterclockwise me-1"></i>é置为é»è®¤å¼ |
| | | </button> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.MRegionSize" class="form-label">Måºå大å°</label> |
| | | <input asp-for="Input.MRegionSize" class="form-control" type="number" min="0" placeholder="1024" /> |
| | | <span asp-validation-for="Input.MRegionSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.IRegionSize" class="form-label">Iåºå大å°</label> |
| | | <input asp-for="Input.IRegionSize" class="form-control" type="number" min="0" placeholder="256" /> |
| | | <span asp-validation-for="Input.IRegionSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.QRegionSize" class="form-label">Qåºå大å°</label> |
| | | <input asp-for="Input.QRegionSize" class="form-control" type="number" min="0" placeholder="256" /> |
| | | <span asp-validation-for="Input.QRegionSize" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row mb-3"> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.DBBlockCount" class="form-label">DBåæ°é</label> |
| | | <input asp-for="Input.DBBlockCount" class="form-control" type="number" min="0" placeholder="100" /> |
| | | <span asp-validation-for="Input.DBBlockCount" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <label asp-for="Input.DBBlockSize" class="form-label">DBå大å°</label> |
| | | <input asp-for="Input.DBBlockSize" class="form-control" type="number" min="0" placeholder="1024" /> |
| | | <span asp-validation-for="Input.DBBlockSize" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-2"> |
| | | <label asp-for="Input.TRegionCount" class="form-label">宿¶å¨æ°é</label> |
| | | <input asp-for="Input.TRegionCount" class="form-control" type="number" min="0" placeholder="64" /> |
| | | <span asp-validation-for="Input.TRegionCount" class="text-danger"></span> |
| | | </div> |
| | | <div class="col-md-2"> |
| | | <label asp-for="Input.CRegionCount" class="form-label">计æ°å¨æ°é</label> |
| | | <input asp-for="Input.CRegionCount" class="form-control" type="number" min="0" placeholder="64" /> |
| | | <span asp-validation-for="Input.CRegionCount" class="text-danger"></span> |
| | | </div> |
| | | </div> |
| | | |
| | | <hr class="my-4"> |
| | | |
| | | <!-- æäº¤æé® --> |
| | | <div class="d-flex justify-content-between"> |
| | | <a asp-page="/Index" class="btn btn-secondary"> |
| | | <i class="bi bi-x-lg me-1"></i>åæ¶ |
| | | </a> |
| | | <button type="submit" class="btn btn-primary"> |
| | | <i class="bi bi-check-lg me-1"></i>ä¿åä¿®æ¹ |
| | | </button> |
| | | </div> |
| | | </form> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å½åç¶æå¡ç --> |
| | | @if (Model.CurrentInstance != null) |
| | | { |
| | | <div class="card mt-4"> |
| | | <div class="card-header"> |
| | | <h6 class="mb-0"> |
| | | <i class="bi bi-activity me-2"></i>å½åç¶æ |
| | | </h6> |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="row"> |
| | | <div class="col-md-4"> |
| | | <strong>ç¶æ:</strong> |
| | | <span class="badge @GetStatusBadgeClass(Model.CurrentInstance.Status)"> |
| | | @GetStatusText(Model.CurrentInstance.Status) |
| | | </span> |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <strong>客æ·ç«¯æ°:</strong> @Model.CurrentInstance.ClientCount |
| | | </div> |
| | | <div class="col-md-4"> |
| | | <strong>æ»è¯·æ±æ°:</strong> @Model.CurrentInstance.TotalRequests |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | } |
| | | </div> |
| | | </div> |
| | | |
| | | @{ |
| | | string GetStatusBadgeClass(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status) |
| | | { |
| | | return status switch |
| | | { |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "bg-success", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "bg-secondary", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "bg-info", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "bg-warning", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "bg-danger", |
| | | _ => "bg-secondary" |
| | | }; |
| | | } |
| | | |
| | | string GetStatusText(WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus status) |
| | | { |
| | | return status switch |
| | | { |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Running => "è¿è¡ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopped => "已忢", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Starting => "å¯å¨ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Stopping => "忢ä¸", |
| | | WIDESEAWCS_S7Simulator.Core.Enums.InstanceStatus.Error => "é误", |
| | | _ => status.ToString() |
| | | }; |
| | | } |
| | | } |
| | | |
| | | @section Scripts { |
| | | @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} |
| | | |
| | | <script> |
| | | function resetMemoryConfig() { |
| | | document.querySelector('#Input_MRegionSize').value = '1024'; |
| | | document.querySelector('#Input_IRegionSize').value = '256'; |
| | | document.querySelector('#Input_QRegionSize').value = '256'; |
| | | document.querySelector('#Input_DBBlockCount').value = '100'; |
| | | document.querySelector('#Input_DBBlockSize').value = '1024'; |
| | | document.querySelector('#Input_TRegionCount').value = '64'; |
| | | document.querySelector('#Input_CRegionCount').value = '64'; |
| | | } |
| | | </script> |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.AspNetCore.Mvc.RazorPages; |
| | | using System.ComponentModel.DataAnnotations; |
| | | using WIDESEAWCS_S7Simulator.Core.Entities; |
| | | using WIDESEAWCS_S7Simulator.Core.Enums; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Web.Pages; |
| | | |
| | | /// <summary> |
| | | /// ç¼è¾å®ä¾é¡µ |
| | | /// </summary> |
| | | public class EditModel : PageModel |
| | | { |
| | | private readonly ILogger<EditModel> _logger; |
| | | private readonly HttpClient _httpClient; |
| | | |
| | | public EditModel(ILogger<EditModel> logger, IHttpClientFactory httpClientFactory) |
| | | { |
| | | _logger = logger; |
| | | _httpClient = httpClientFactory.CreateClient(); |
| | | _httpClient.BaseAddress = new Uri($"{Request.Scheme}://{Request.Host}"); |
| | | } |
| | | |
| | | [BindProperty] |
| | | public EditInstanceInputModel Input { get; set; } = new(); |
| | | |
| | | public InstanceState? CurrentInstance { get; private set; } |
| | | |
| | | public bool IsRunning { get; private set; } |
| | | |
| | | public async Task<IActionResult> OnGetAsync(string id) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(id)) |
| | | { |
| | | return BadRequest("å®ä¾IDä¸è½ä¸ºç©º"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}"); |
| | | |
| | | if (response.IsSuccessStatusCode) |
| | | { |
| | | CurrentInstance = await response.Content.ReadFromJsonAsync<InstanceState>(); |
| | | IsRunning = CurrentInstance?.Status == InstanceStatus.Running; |
| | | |
| | | // Load existing config |
| | | var configResponse = await _httpClient.GetAsync($"/api/SimulatorInstances"); |
| | | if (configResponse.IsSuccessStatusCode) |
| | | { |
| | | var allInstances = await configResponse.Content.ReadFromJsonAsync<List<InstanceState>>(); |
| | | // For now, we'll initialize with defaults. In production, you'd have a separate endpoint to get instance config |
| | | } |
| | | |
| | | // Initialize input with current values (default for now) |
| | | Input.Id = id; |
| | | Input.Name = CurrentInstance?.InstanceId ?? id; |
| | | Input.Port = 102; // Default |
| | | } |
| | | else if (response.StatusCode == System.Net.HttpStatusCode.NotFound) |
| | | { |
| | | return NotFound($"å®ä¾ \"{id}\" ä¸åå¨"); |
| | | } |
| | | } |
| | | catch (HttpRequestException ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to load instance {InstanceId}", id); |
| | | ModelState.AddModelError(string.Empty, "å è½½å®ä¾å¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | |
| | | return Page(); |
| | | } |
| | | |
| | | public async Task<IActionResult> OnPostAsync() |
| | | { |
| | | if (!ModelState.IsValid) |
| | | { |
| | | // Reload instance state |
| | | await LoadInstanceStateAsync(Input.Id); |
| | | return Page(); |
| | | } |
| | | |
| | | try |
| | | { |
| | | var config = new InstanceConfig |
| | | { |
| | | Id = Input.Id, |
| | | Name = Input.Name, |
| | | PLCType = Input.PLCType, |
| | | Port = Input.Port, |
| | | ActivationKey = Input.ActivationKey ?? string.Empty, |
| | | AutoStart = Input.AutoStart, |
| | | MemoryConfig = new MemoryRegionConfig |
| | | { |
| | | MRegionSize = Input.MRegionSize > 0 ? Input.MRegionSize : 1024, |
| | | DBBlockCount = Input.DBBlockCount > 0 ? Input.DBBlockCount : 100, |
| | | DBBlockSize = Input.DBBlockSize > 0 ? Input.DBBlockSize : 1024, |
| | | IRegionSize = Input.IRegionSize > 0 ? Input.IRegionSize : 256, |
| | | QRegionSize = Input.QRegionSize > 0 ? Input.QRegionSize : 256, |
| | | TRegionCount = Input.TRegionCount > 0 ? Input.TRegionCount : 64, |
| | | CRegionCount = Input.CRegionCount > 0 ? Input.CRegionCount : 64 |
| | | } |
| | | }; |
| | | |
| | | var response = await _httpClient.PutAsJsonAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(Input.Id)}", config); |
| | | |
| | | if (response.IsSuccessStatusCode) |
| | | { |
| | | _logger.LogInformation("Instance {InstanceId} updated successfully", Input.Id); |
| | | TempData["SuccessMessage"] = $"å®ä¾ \"{Input.Id}\" æ´æ°æå!"; |
| | | return RedirectToPage("/Index"); |
| | | } |
| | | else if (response.StatusCode == System.Net.HttpStatusCode.NotFound) |
| | | { |
| | | ModelState.AddModelError(string.Empty, "å®ä¾ä¸åå¨"); |
| | | } |
| | | else |
| | | { |
| | | var error = await response.Content.ReadFromJsonAsync<object>(); |
| | | ModelState.AddModelError(string.Empty, error?.ToString() ?? "æ´æ°å®ä¾å¤±è´¥"); |
| | | } |
| | | } |
| | | catch (HttpRequestException ex) |
| | | { |
| | | _logger.LogError(ex, "Failed to update instance"); |
| | | ModelState.AddModelError(string.Empty, "ç½ç»é误ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | |
| | | await LoadInstanceStateAsync(Input.Id); |
| | | return Page(); |
| | | } |
| | | |
| | | private async Task LoadInstanceStateAsync(string id) |
| | | { |
| | | try |
| | | { |
| | | var response = await _httpClient.GetAsync($"/api/SimulatorInstances/{Uri.EscapeDataString(id)}"); |
| | | if (response.IsSuccessStatusCode) |
| | | { |
| | | CurrentInstance = await response.Content.ReadFromJsonAsync<InstanceState>(); |
| | | IsRunning = CurrentInstance?.Status == InstanceStatus.Running; |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | // Ignore errors when reloading state |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// ç¼è¾å®ä¾è¾å
¥æ¨¡å |
| | | /// </summary> |
| | | public class EditInstanceInputModel |
| | | { |
| | | public string Id { get; set; } = string.Empty; |
| | | |
| | | [Required(ErrorMessage = "å®ä¾åç§°ä¸è½ä¸ºç©º")] |
| | | [StringLength(100, ErrorMessage = "å®ä¾åç§°ä¸è½è¶
è¿100个å符")] |
| | | public string Name { get; set; } = string.Empty; |
| | | |
| | | [Required(ErrorMessage = "PLCåå·ä¸è½ä¸ºç©º")] |
| | | public SiemensPLCType PLCType { get; set; } = SiemensPLCType.S71200; |
| | | |
| | | [Required(ErrorMessage = "端å£ä¸è½ä¸ºç©º")] |
| | | [Range(1, 65535, ErrorMessage = "端å£å¿
é¡»å¨1-65535ä¹é´")] |
| | | public int Port { get; set; } = 102; |
| | | |
| | | [StringLength(200, ErrorMessage = "æ¿æ´»ç ä¸è½è¶
è¿200个å符")] |
| | | public string? ActivationKey { get; set; } |
| | | |
| | | public bool AutoStart { get; set; } = false; |
| | | |
| | | // å
åé
ç½® |
| | | public int MRegionSize { get; set; } |
| | | public int DBBlockCount { get; set; } |
| | | public int DBBlockSize { get; set; } |
| | | public int IRegionSize { get; set; } |
| | | public int QRegionSize { get; set; } |
| | | public int TRegionCount { get; set; } |
| | | public int CRegionCount { get; set; } |
| | | } |
| | | } |
| | |
| | | @page |
| | | @model IndexModel |
| | | @{ |
| | | ViewData["Title"] = "Home page"; |
| | | ViewData["Title"] = "å®ä¾å表"; |
| | | } |
| | | |
| | | <div class="text-center"> |
| | | <h1 class="display-4">Welcome</h1> |
| | | <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> |
| | | <div class="d-flex justify-content-between align-items-center mb-4"> |
| | | <div> |
| | | <h2 class="mb-0"> |
| | | <i class="bi bi-cpu-fill me-2"></i>S7 PLC 仿çå¨å®ä¾ |
| | | </h2> |
| | | <p class="text-muted mb-0 mt-1">管çåçæ§ S7 PLC 仿çå¨å®ä¾</p> |
| | | </div> |
| | | <a asp-page="/Create" class="btn btn-primary"> |
| | | <i class="bi bi-plus-lg me-1"></i>å建å®ä¾ |
| | | </a> |
| | | </div> |
| | | |
| | | <div id="instancesContainer"> |
| | | <!-- Loading state --> |
| | | <div id="loadingState" class="text-center py-5"> |
| | | <div class="spinner-border text-primary" role="status"> |
| | | <span class="visually-hidden">å è½½ä¸...</span> |
| | | </div> |
| | | <p class="mt-3 text-muted">æ£å¨å è½½å®ä¾å表...</p> |
| | | </div> |
| | | |
| | | <!-- Empty state --> |
| | | <div id="emptyState" class="empty-state d-none"> |
| | | <i class="bi bi-inbox"></i> |
| | | <h3>ææ å®ä¾</h3> |
| | | <p>ç¹å»ä¸æ¹"å建å®ä¾"æé®æ¥å建æ¨ç第ä¸ä¸ªä»¿çå¨å®ä¾</p> |
| | | </div> |
| | | |
| | | <!-- Instances grid --> |
| | | <div id="instancesGrid" class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4 d-none"></div> |
| | | </div> |
| | | |
| | | @section Scripts { |
| | | <script> |
| | | let autoRefreshInterval = null; |
| | | |
| | | document.addEventListener('DOMContentLoaded', function () { |
| | | loadInstances(); |
| | | startAutoRefresh(loadInstances, 5000); |
| | | }); |
| | | |
| | | async function loadInstances() { |
| | | try { |
| | | const instances = await apiCall(`${API_BASE_URL}/SimulatorInstances`); |
| | | renderInstances(instances); |
| | | } catch (error) { |
| | | console.error('Failed to load instances:', error); |
| | | document.getElementById('loadingState').classList.add('d-none'); |
| | | document.getElementById('emptyState').classList.remove('d-none'); |
| | | } |
| | | } |
| | | |
| | | function renderInstances(instances) { |
| | | const loadingState = document.getElementById('loadingState'); |
| | | const emptyState = document.getElementById('emptyState'); |
| | | const grid = document.getElementById('instancesGrid'); |
| | | |
| | | loadingState.classList.add('d-none'); |
| | | grid.classList.add('d-none'); |
| | | emptyState.classList.add('d-none'); |
| | | |
| | | if (!instances || instances.length === 0) { |
| | | emptyState.classList.remove('d-none'); |
| | | return; |
| | | } |
| | | |
| | | grid.classList.remove('d-none'); |
| | | grid.innerHTML = instances.map(instance => createInstanceCard(instance)).join(''); |
| | | } |
| | | |
| | | function createInstanceCard(instance) { |
| | | const statusClass = getStatusClass(instance.status); |
| | | const statusText = getStatusText(instance.status); |
| | | const plcTypeText = getPlcTypeText(instance.plcType); |
| | | const isRunning = instance.status === 'Running'; |
| | | const isStopped = instance.status === 'Stopped'; |
| | | |
| | | return ` |
| | | <div class="col"> |
| | | <div class="card instance-card h-100 ${statusClass}"> |
| | | <div class="card-header d-flex justify-content-between align-items-center"> |
| | | <h5 class="card-title mb-0">${escapeHtml(instance.instanceId)}</h5> |
| | | <span class="badge ${statusClass}">${statusText}</span> |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="instance-info mb-3"> |
| | | <div class="row mb-2"> |
| | | <div class="col-6"> |
| | | <small class="instance-info-label">åç§°</small> |
| | | <div class="instance-info-value">${escapeHtml(instance.name || '-')}</div> |
| | | </div> |
| | | <div class="col-6"> |
| | | <small class="instance-info-label">PLCåå·</small> |
| | | <div class="instance-info-value">${plcTypeText}</div> |
| | | </div> |
| | | </div> |
| | | <div class="row mb-2"> |
| | | <div class="col-6"> |
| | | <small class="instance-info-label">端å£</small> |
| | | <div class="instance-info-value">${instance.port || '-'}</div> |
| | | </div> |
| | | <div class="col-6"> |
| | | <small class="instance-info-label">客æ·ç«¯</small> |
| | | <div class="instance-info-value"> |
| | | <i class="bi bi-people-fill me-1"></i>${instance.clientCount || 0} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ${instance.startTime ? ` |
| | | <div class="row"> |
| | | <div class="col-12"> |
| | | <small class="instance-info-label">å¯å¨æ¶é´</small> |
| | | <div class="instance-info-value small">${formatDate(instance.startTime)}</div> |
| | | </div> |
| | | </div> |
| | | ` : ''} |
| | | ${instance.errorMessage ? ` |
| | | <div class="alert alert-danger alert-sm mt-2 mb-0 py-2 small"> |
| | | <i class="bi bi-exclamation-triangle-fill me-1"></i>${escapeHtml(instance.errorMessage)} |
| | | </div> |
| | | ` : ''} |
| | | </div> |
| | | </div> |
| | | <div class="card-footer bg-white"> |
| | | <div class="action-buttons d-flex gap-2"> |
| | | ${isRunning ? ` |
| | | <button class="btn btn-warning btn-sm flex-fill" onclick="stopInstance('${instance.instanceId}')" ${instance.status === 'Stopping' ? 'disabled' : ''}> |
| | | <i class="bi bi-stop-fill me-1"></i>忢 |
| | | </button> |
| | | ` : ''} |
| | | ${isStopped ? ` |
| | | <button class="btn btn-success btn-sm flex-fill" onclick="startInstance('${instance.instanceId}')" ${instance.status === 'Starting' ? 'disabled' : ''}> |
| | | <i class="bi bi-play-fill me-1"></i>å¯å¨ |
| | | </button> |
| | | ` : ''} |
| | | <a href="/Details?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-info btn-sm text-white flex-fill"> |
| | | <i class="bi bi-info-circle-fill me-1"></i>详æ
|
| | | </a> |
| | | <a href="/Edit?id=${encodeURIComponent(instance.instanceId)}" class="btn btn-primary btn-sm flex-fill"> |
| | | <i class="bi bi-pencil-fill me-1"></i>ç¼è¾ |
| | | </a> |
| | | <button class="btn btn-danger btn-sm" onclick="deleteInstance('${instance.instanceId}')"> |
| | | <i class="bi bi-trash-fill"></i> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | `; |
| | | } |
| | | |
| | | function getStatusClass(status) { |
| | | const map = { |
| | | 'Stopped': 'status-stopped', |
| | | 'Starting': 'status-starting', |
| | | 'Running': 'status-running', |
| | | 'Stopping': 'status-stopping', |
| | | 'Error': 'status-error' |
| | | }; |
| | | return map[status] || ''; |
| | | } |
| | | |
| | | async function startInstance(id) { |
| | | confirmAction(`ç¡®å®è¦å¯å¨å®ä¾ "${id}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/start`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast(`å®ä¾ "${id}" å¯å¨å½ä»¤å·²åé`, 'success'); |
| | | setTimeout(() => loadInstances(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to start instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function stopInstance(id) { |
| | | confirmAction(`ç¡®å®è¦åæ¢å®ä¾ "${id}" å?`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}/stop`, { |
| | | method: 'POST' |
| | | }); |
| | | showToast(`å®ä¾ "${id}" 忢å½ä»¤å·²åé`, 'success'); |
| | | setTimeout(() => loadInstances(), 1000); |
| | | } catch (error) { |
| | | console.error('Failed to stop instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | async function deleteInstance(id) { |
| | | confirmAction(`ç¡®å®è¦å é¤å®ä¾ "${id}" å?æ¤æä½ä¸å¯æ¤é!`, async () => { |
| | | try { |
| | | await apiCall(`${API_BASE_URL}/SimulatorInstances/${encodeURIComponent(id)}?deleteConfig=true`, { |
| | | method: 'DELETE' |
| | | }); |
| | | showToast(`å®ä¾ "${id}" å·²å é¤`, 'success'); |
| | | loadInstances(); |
| | | } catch (error) { |
| | | console.error('Failed to delete instance:', error); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | function escapeHtml(text) { |
| | | const div = document.createElement('div'); |
| | | div.textContent = text; |
| | | return div.innerHTML; |
| | | } |
| | | |
| | | // Stop auto-refresh when page is hidden |
| | | document.addEventListener('visibilitychange', function () { |
| | | if (document.hidden) { |
| | | stopAutoRefresh(); |
| | | } else { |
| | | startAutoRefresh(loadInstances, 5000); |
| | | } |
| | | }); |
| | | </script> |
| | | } |
| | |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using Microsoft.AspNetCore.Mvc.RazorPages; |
| | | using Microsoft.AspNetCore.Mvc.RazorPages; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Web.Pages; |
| | | |
| | | /// <summary> |
| | | /// å®ä¾å表页 |
| | | /// </summary> |
| | | public class IndexModel : PageModel |
| | | { |
| | | private readonly ILogger<IndexModel> _logger; |
| | |
| | | |
| | | public void OnGet() |
| | | { |
| | | |
| | | _logger.LogInformation("Loading instance list page"); |
| | | } |
| | | } |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="en"> |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>@ViewData["Title"] - WIDESEAWCS_S7Simulator.Web</title> |
| | | <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> |
| | | <title>@ViewData["Title"] - S7 PLC Simulator</title> |
| | | |
| | | <!-- Bootstrap 5 CSS --> |
| | | <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> |
| | | |
| | | <!-- Bootstrap Icons --> |
| | | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> |
| | | |
| | | <!-- Custom CSS --> |
| | | <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> |
| | | <link rel="stylesheet" href="~/WIDESEAWCS_S7Simulator.Web.styles.css" asp-append-version="true" /> |
| | | </head> |
| | | <body> |
| | | <header> |
| | | <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> |
| | | <div class="container"> |
| | | <a class="navbar-brand" asp-area="" asp-page="/Index">WIDESEAWCS_S7Simulator.Web</a> |
| | | <nav class="navbar navbar-expand-sm navbar-dark bg-primary shadow-sm mb-4"> |
| | | <div class="container-fluid"> |
| | | <a class="navbar-brand" asp-page="/Index"> |
| | | <i class="bi bi-cpu-fill me-2"></i>S7 PLC Simulator |
| | | </a> |
| | | <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent" |
| | | aria-expanded="false" aria-label="Toggle navigation"> |
| | | <span class="navbar-toggler-icon"></span> |
| | |
| | | <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> |
| | | <ul class="navbar-nav flex-grow-1"> |
| | | <li class="nav-item"> |
| | | <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> |
| | | </li> |
| | | <li class="nav-item"> |
| | | <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> |
| | | <a class="nav-link" asp-page="/Index"> |
| | | <i class="bi bi-house-door me-1"></i>å®ä¾å表 |
| | | </a> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | </nav> |
| | | </header> |
| | | <div class="container"> |
| | | |
| | | <div class="container-fluid px-4"> |
| | | <main role="main" class="pb-3"> |
| | | @RenderBody() |
| | | </main> |
| | | </div> |
| | | |
| | | <footer class="border-top footer text-muted"> |
| | | <div class="container"> |
| | | © 2026 - WIDESEAWCS_S7Simulator.Web - <a asp-area="" asp-page="/Privacy">Privacy</a> |
| | | <footer class="border-top py-3 mt-4 bg-light"> |
| | | <div class="container-fluid text-center text-muted"> |
| | | <small>© 2026 - S7 PLC Simulator Management UI</small> |
| | | </div> |
| | | </footer> |
| | | |
| | | <script src="~/lib/jquery/dist/jquery.min.js"></script> |
| | | <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> |
| | | <!-- Bootstrap 5 JS Bundle --> |
| | | <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> |
| | | |
| | | <!-- Alpine.js --> |
| | | <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> |
| | | |
| | | <!-- Custom JS --> |
| | | <script src="~/js/site.js" asp-append-version="true"></script> |
| | | |
| | | @await RenderSectionAsync("Scripts", required: false) |
| | | </body> |
| | | </html> |
| | | </html> |
| | |
| | | <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> |
| | | <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> |
| | | <environment include="Development"> |
| | | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script> |
| | | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script> |
| | | </environment> |
| | | <environment exclude="Development"> |
| | | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js" |
| | | asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js" |
| | | asp-fallback-test="window.jQuery && window.jQuery.validator"> |
| | | </script> |
| | | <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js" |
| | | asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" |
| | | asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"> |
| | | </script> |
| | | </environment> |
| | |
| | | @using WIDESEAWCS_S7Simulator.Web |
| | | @using WIDESEAWCS_S7Simulator.Core.Entities |
| | | @using WIDESEAWCS_S7Simulator.Core.Enums |
| | | @namespace WIDESEAWCS_S7Simulator.Web.Pages |
| | | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
| | |
| | | html { |
| | | font-size: 14px; |
| | | } |
| | | |
| | | @media (min-width: 768px) { |
| | | html { |
| | | font-size: 16px; |
| | | } |
| | | } |
| | | |
| | | html { |
| | | position: relative; |
| | | min-height: 100%; |
| | | } |
| | | |
| | | body { |
| | | margin-bottom: 60px; |
| | | background-color: #f8f9fa; |
| | | } |
| | | |
| | | /* Status Badge Colors */ |
| | | .status-stopped { |
| | | background-color: #6c757d; |
| | | } |
| | | |
| | | .status-starting { |
| | | background-color: #0dcaf0; |
| | | } |
| | | |
| | | .status-running { |
| | | background-color: #198754; |
| | | } |
| | | |
| | | .status-stopping { |
| | | background-color: #ffc107; |
| | | color: #000; |
| | | } |
| | | |
| | | .status-error { |
| | | background-color: #dc3545; |
| | | } |
| | | |
| | | /* Instance Card Styles */ |
| | | .instance-card { |
| | | transition: transform 0.2s, box-shadow 0.2s; |
| | | border-left: 4px solid transparent; |
| | | } |
| | | |
| | | .instance-card:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | } |
| | | |
| | | .instance-card.status-running { |
| | | border-left-color: #198754; |
| | | } |
| | | |
| | | .instance-card.status-stopped { |
| | | border-left-color: #6c757d; |
| | | } |
| | | |
| | | .instance-card.status-error { |
| | | border-left-color: #dc3545; |
| | | } |
| | | |
| | | .instance-card.status-starting, |
| | | .instance-card.status-stopping { |
| | | border-left-color: #ffc107; |
| | | } |
| | | |
| | | /* Instance Info Section */ |
| | | .instance-info-label { |
| | | font-weight: 600; |
| | | color: #495057; |
| | | } |
| | | |
| | | .instance-info-value { |
| | | color: #212529; |
| | | } |
| | | |
| | | /* Action Buttons */ |
| | | .action-buttons .btn { |
| | | min-width: 80px; |
| | | } |
| | | |
| | | /* Loading Spinner */ |
| | | .spinner-overlay { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | width: 100%; |
| | | height: 100%; |
| | | background-color: rgba(0, 0, 0, 0.5); |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | z-index: 9999; |
| | | } |
| | | |
| | | /* Memory Region Table */ |
| | | .memory-table th { |
| | | background-color: #e9ecef; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | /* Client Table */ |
| | | .client-table { |
| | | font-size: 0.9rem; |
| | | } |
| | | |
| | | /* Toast Container */ |
| | | .toast-container { |
| | | position: fixed; |
| | | top: 20px; |
| | | right: 20px; |
| | | z-index: 9999; |
| | | } |
| | | |
| | | /* Empty State */ |
| | | .empty-state { |
| | | text-align: center; |
| | | padding: 60px 20px; |
| | | color: #6c757d; |
| | | } |
| | | |
| | | .empty-state i { |
| | | font-size: 4rem; |
| | | margin-bottom: 20px; |
| | | opacity: 0.5; |
| | | } |
| | | |
| | | /* Form Styles */ |
| | | .form-label.required::after { |
| | | content: " *"; |
| | | color: #dc3545; |
| | | } |
| | |
| | | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification |
| | | // for details on configuring this project to bundle and minify static web assets. |
| | | |
| | | // Write your JavaScript code. |
| | | // API Base URL |
| | | const API_BASE_URL = '/api'; |
| | | |
| | | // Show toast notification |
| | | function showToast(message, type = 'info') { |
| | | const toastContainer = document.getElementById('toastContainer') || createToastContainer(); |
| | | const toastId = 'toast-' + Date.now(); |
| | | |
| | | const bgClass = { |
| | | 'success': 'bg-success', |
| | | 'danger': 'bg-danger', |
| | | 'warning': 'bg-warning', |
| | | 'info': 'bg-info', |
| | | 'error': 'bg-danger' |
| | | }[type] || 'bg-info'; |
| | | |
| | | const toastHtml = ` |
| | | <div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true"> |
| | | <div class="d-flex"> |
| | | <div class="toast-body"> |
| | | ${message} |
| | | </div> |
| | | <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> |
| | | </div> |
| | | </div> |
| | | `; |
| | | |
| | | toastContainer.insertAdjacentHTML('beforeend', toastHtml); |
| | | const toastElement = document.getElementById(toastId); |
| | | const toast = new bootstrap.Toast(toastElement, { delay: 3000 }); |
| | | toast.show(); |
| | | |
| | | toastElement.addEventListener('hidden.bs.toast', () => { |
| | | toastElement.remove(); |
| | | }); |
| | | } |
| | | |
| | | function createToastContainer() { |
| | | const container = document.createElement('div'); |
| | | container.id = 'toastContainer'; |
| | | container.className = 'toast-container'; |
| | | document.body.appendChild(container); |
| | | return container; |
| | | } |
| | | |
| | | // Show loading spinner |
| | | function showLoading() { |
| | | const existingOverlay = document.getElementById('loadingOverlay'); |
| | | if (existingOverlay) return; |
| | | |
| | | const overlay = document.createElement('div'); |
| | | overlay.id = 'loadingOverlay'; |
| | | overlay.className = 'spinner-overlay'; |
| | | overlay.innerHTML = ` |
| | | <div class="spinner-border text-light" style="width: 3rem; height: 3rem;" role="status"> |
| | | <span class="visually-hidden">Loading...</span> |
| | | </div> |
| | | `; |
| | | document.body.appendChild(overlay); |
| | | } |
| | | |
| | | // Hide loading spinner |
| | | function hideLoading() { |
| | | const overlay = document.getElementById('loadingOverlay'); |
| | | if (overlay) { |
| | | overlay.remove(); |
| | | } |
| | | } |
| | | |
| | | // API call helper |
| | | async function apiCall(url, options = {}) { |
| | | try { |
| | | showLoading(); |
| | | const response = await fetch(url, { |
| | | ...options, |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...options.headers |
| | | } |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | const errorData = await response.json().catch(() => ({ error: response.statusText })); |
| | | throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); |
| | | } |
| | | |
| | | return await response.json(); |
| | | } catch (error) { |
| | | showToast(error.message, 'error'); |
| | | throw error; |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | // Format date |
| | | function formatDate(dateString) { |
| | | if (!dateString) return '-'; |
| | | const date = new Date(dateString); |
| | | return date.toLocaleString('zh-CN', { |
| | | year: 'numeric', |
| | | month: '2-digit', |
| | | day: '2-digit', |
| | | hour: '2-digit', |
| | | minute: '2-digit', |
| | | second: '2-digit' |
| | | }); |
| | | } |
| | | |
| | | // Get status badge class |
| | | function getStatusBadgeClass(status) { |
| | | const statusMap = { |
| | | 'Stopped': 'status-stopped', |
| | | 'Starting': 'status-starting', |
| | | 'Running': 'status-running', |
| | | 'Stopping': 'status-stopping', |
| | | 'Error': 'status-error' |
| | | }; |
| | | return statusMap[status] || 'bg-secondary'; |
| | | } |
| | | |
| | | // Get status text |
| | | function getStatusText(status) { |
| | | const statusMap = { |
| | | 'Stopped': '已忢', |
| | | 'Starting': 'å¯å¨ä¸', |
| | | 'Running': 'è¿è¡ä¸', |
| | | 'Stopping': '忢ä¸', |
| | | 'Error': 'é误' |
| | | }; |
| | | return statusMap[status] || status; |
| | | } |
| | | |
| | | // Get PLC type text |
| | | function getPlcTypeText(plcType) { |
| | | const plcTypeMap = { |
| | | 'S7200Smart': 'S7-200 Smart', |
| | | 'S71200': 'S7-1200', |
| | | 'S71500': 'S7-1500', |
| | | 'S7300': 'S7-300', |
| | | 'S7400': 'S7-400' |
| | | }; |
| | | return plcTypeMap[plcType] || plcType; |
| | | } |
| | | |
| | | // Confirm action |
| | | function confirmAction(message, callback) { |
| | | if (confirm(message)) { |
| | | callback(); |
| | | } |
| | | } |
| | | |
| | | // Redirect with delay |
| | | function redirectWithDelay(url, delay = 1500) { |
| | | setTimeout(() => { |
| | | window.location.href = url; |
| | | }, delay); |
| | | } |
| | | |
| | | // Format bytes |
| | | function formatBytes(bytes, decimals = 2) { |
| | | if (bytes === 0) return '0 Bytes'; |
| | | const k = 1024; |
| | | const dm = decimals < 0 ? 0 : decimals; |
| | | const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| | | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| | | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; |
| | | } |
| | | |
| | | // Auto-refresh interval handler |
| | | let refreshInterval = null; |
| | | |
| | | function startAutoRefresh(callback, intervalMs = 5000) { |
| | | stopAutoRefresh(); |
| | | callback(); // Initial call |
| | | refreshInterval = setInterval(callback, intervalMs); |
| | | } |
| | | |
| | | function stopAutoRefresh() { |
| | | if (refreshInterval) { |
| | | clearInterval(refreshInterval); |
| | | refreshInterval = null; |
| | | } |
| | | } |
| | | |
| | | // Page unload handler |
| | | window.addEventListener('beforeunload', () => { |
| | | stopAutoRefresh(); |
| | | }); |