From 3fb5074ccf776e42f4388ab7edf3e7fe2fef0ccd Mon Sep 17 00:00:00 2001 From: haoliang <821644@qq.com> Date: Fri, 1 May 2026 04:17:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ECncSimulator=E6=A8=A1?= =?UTF-8?q?=E6=8B=9F=E9=87=87=E9=9B=86=E6=9C=8D=E5=8A=A1=EF=BC=88=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=8A=B6=E6=80=81=E6=9C=BA+8=E7=A7=8D=E5=9C=BA?= =?UTF-8?q?=E6=99=AF+FANUC=2019=20Tag+=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2?= =?UTF-8?q?+=E7=BD=91=E7=BB=9C=E5=BC=82=E5=B8=B8=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CncSimulator/Admin/AdminHandler.cs | 274 +++++++++ src/CncSimulator/App.config | 29 + src/CncSimulator/CncSimulator.csproj | 28 + src/CncSimulator/Config/SimulatorConfig.cs | 65 +++ src/CncSimulator/Core/LogRecorder.cs | 102 ++++ src/CncSimulator/Core/SimulatorEngine.cs | 181 ++++++ src/CncSimulator/Core/SimulatorServer.cs | 521 ++++++++++++++++++ src/CncSimulator/Device/DeviceSimulator.cs | 220 ++++++++ src/CncSimulator/Device/DeviceState.cs | 82 +++ src/CncSimulator/Device/ScenarioPlayer.cs | 97 ++++ .../Generator/FanucDataGenerator.cs | 94 ++++ src/CncSimulator/Generator/IBrandGenerator.cs | 15 + src/CncSimulator/Program.cs | 64 +++ src/CncSimulator/simulator.json | 53 ++ 14 files changed, 1825 insertions(+) create mode 100644 src/CncSimulator/Admin/AdminHandler.cs create mode 100644 src/CncSimulator/App.config create mode 100644 src/CncSimulator/CncSimulator.csproj create mode 100644 src/CncSimulator/Config/SimulatorConfig.cs create mode 100644 src/CncSimulator/Core/LogRecorder.cs create mode 100644 src/CncSimulator/Core/SimulatorEngine.cs create mode 100644 src/CncSimulator/Core/SimulatorServer.cs create mode 100644 src/CncSimulator/Device/DeviceSimulator.cs create mode 100644 src/CncSimulator/Device/DeviceState.cs create mode 100644 src/CncSimulator/Device/ScenarioPlayer.cs create mode 100644 src/CncSimulator/Generator/FanucDataGenerator.cs create mode 100644 src/CncSimulator/Generator/IBrandGenerator.cs create mode 100644 src/CncSimulator/Program.cs create mode 100644 src/CncSimulator/simulator.json diff --git a/src/CncSimulator/Admin/AdminHandler.cs b/src/CncSimulator/Admin/AdminHandler.cs new file mode 100644 index 0000000..27cf515 --- /dev/null +++ b/src/CncSimulator/Admin/AdminHandler.cs @@ -0,0 +1,274 @@ +using System.Text; +using CncSimulator.Core; + +namespace CncSimulator.Admin +{ + /// + /// 管理页面HTML生成器。 + /// 生成总管理页面和单地址管理页面的完整HTML+CSS+JS。 + /// + public class AdminHandler + { + /// 生成总管理页面(网关页面) + public string BuildGatewayPage(SimulatorEngine engine) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("CNC 模拟采集服务 - 总管理"); + sb.AppendLine(""); + sb.AppendLine("
"); + sb.AppendLine("

CNC 模拟采集服务

"); + sb.AppendLine("
"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("

地址列表

"); + sb.AppendLine(" "); + sb.AppendLine("
名称端口状态设备数数据频率请求次数操作
"); + sb.AppendLine("
"); + sb.AppendLine("

控制台日志

"); + sb.AppendLine("
加载中...
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine(""); + return sb.ToString(); + } + + /// 生成单地址管理页面 + public string BuildSingleAddressPage(SimulatorServer server) + { + var sb = new StringBuilder(); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine("" + server.Name + " - 模拟管理"); + sb.AppendLine(""); + sb.AppendLine("
"); + sb.AppendLine("

" + server.Name + " (端口 " + server.Port + ")

"); + sb.AppendLine("
"); + sb.AppendLine(" 数据接口"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + // 全局设置 + sb.AppendLine("

全局设置

"); + sb.AppendLine("
"); + sb.AppendLine(" "); + sb.AppendLine(" 秒"); + sb.AppendLine(" "); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine("
"); + sb.AppendLine("
"); + // 设备状态卡片 + sb.AppendLine("

设备状态卡片

"); + sb.AppendLine("
加载中...
"); + sb.AppendLine("
"); + // JSON预览 + sb.AppendLine("

当前返回JSON预览

"); + sb.AppendLine(" "); + sb.AppendLine("
加载中...
"); + sb.AppendLine("
"); + // 日志 + sb.AppendLine("

返回数据日志(最近100条)

"); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine(" "); + sb.AppendLine("
#时间设备数关键数据耗时操作
"); + sb.AppendLine("
"); + // 统计 + sb.AppendLine("

统计

"); + sb.AppendLine("
加载中...
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + // JavaScript + sb.AppendLine(""); + return sb.ToString(); + } + } +} diff --git a/src/CncSimulator/App.config b/src/CncSimulator/App.config new file mode 100644 index 0000000..766ef2d --- /dev/null +++ b/src/CncSimulator/App.config @@ -0,0 +1,29 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CncSimulator/CncSimulator.csproj b/src/CncSimulator/CncSimulator.csproj new file mode 100644 index 0000000..06b24d3 --- /dev/null +++ b/src/CncSimulator/CncSimulator.csproj @@ -0,0 +1,28 @@ + + + + net472 + x64 + CncSimulator + CncSimulator + false + false + Exe + bin\ + false + false + + + + + + + + + + + Always + + + + diff --git a/src/CncSimulator/Config/SimulatorConfig.cs b/src/CncSimulator/Config/SimulatorConfig.cs new file mode 100644 index 0000000..f85fbed --- /dev/null +++ b/src/CncSimulator/Config/SimulatorConfig.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CncSimulator.Config +{ + /// 模拟器顶层配置 + public class SimulatorConfig + { + /// 总管理页面端口 + [JsonProperty("gatewayPort")] + public int GatewayPort { get; set; } = 9000; + + /// 采集地址列表 + [JsonProperty("addresses")] + public List Addresses { get; set; } = new List(); + } + + /// 单个采集地址配置 + public class AddressConfig + { + /// 显示名称 + [JsonProperty("name")] + public string Name { get; set; } + + /// 监听端口 + [JsonProperty("port")] + public int Port { get; set; } + + /// 品牌标识(预留扩展) + [JsonProperty("brand")] + public string Brand { get; set; } = "fanuc"; + + /// 数据变化频率(秒) + [JsonProperty("dataChangeInterval")] + public int DataChangeInterval { get; set; } = 10; + + /// 剧本模式:auto=自动循环 / manual=手动触发 + [JsonProperty("scenarioMode")] + public string ScenarioMode { get; set; } = "auto"; + + /// 模拟设备列表 + [JsonProperty("devices")] + public List Devices { get; set; } = new List(); + } + + /// 单台模拟设备配置 + public class DeviceConfig + { + /// 设备编码(需与cnc_machine.device_code一致) + [JsonProperty("deviceCode")] + public string DeviceCode { get; set; } + + /// 设备描述 + [JsonProperty("desc")] + public string Desc { get; set; } + + /// 初始NC程序名 + [JsonProperty("initialProgram")] + public string InitialProgram { get; set; } = "O0001"; + + /// 初始零件数 + [JsonProperty("initialPartCount")] + public int InitialPartCount { get; set; } = 0; + } +} diff --git a/src/CncSimulator/Core/LogRecorder.cs b/src/CncSimulator/Core/LogRecorder.cs new file mode 100644 index 0000000..0766804 --- /dev/null +++ b/src/CncSimulator/Core/LogRecorder.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; + +namespace CncSimulator.Core +{ + /// 日志条目 + public class LogEntry + { + /// 时间戳 + public DateTime Timestamp { get; set; } + + /// 地址端口 + public int AddressPort { get; set; } + + /// 设备数量 + public int DeviceCount { get; set; } + + /// 关键数据摘要 + public string KeyData { get; set; } + + /// 完整JSON + public string FullJson { get; set; } + + /// 响应耗时(毫秒) + public long Duration { get; set; } + } + + /// + /// 日志记录器。同时写入内存环形缓冲和log4net文件。 + /// + public class LogRecorder + { + private readonly int _capacity; + private readonly LogEntry[] _buffer; + private int _writeIndex; + private int _count; + private readonly object _lock = new object(); + private readonly log4net.ILog _log; + + public LogRecorder(int capacity = 200) + { + _capacity = capacity; + _buffer = new LogEntry[capacity]; + _writeIndex = 0; + _count = 0; + _log = log4net.LogManager.GetLogger(typeof(LogRecorder)); + } + + /// 记录一次返回 + public void Record(int addressPort, int deviceCount, string keyData, string fullJson, long durationMs) + { + var entry = new LogEntry + { + Timestamp = DateTime.Now, + AddressPort = addressPort, + DeviceCount = deviceCount, + KeyData = keyData, + FullJson = fullJson, + Duration = durationMs + }; + + lock (_lock) + { + _buffer[_writeIndex] = entry; + _writeIndex = (_writeIndex + 1) % _capacity; + if (_count < _capacity) _count++; + } + + // 写文件日志 + _log.Info($"[{addressPort}] 返回{deviceCount}台设备, 耗时{durationMs}ms"); + _log.Info($"[{addressPort}] 关键数据: {keyData}"); + } + + /// 获取最近的日志 + public List GetRecentLogs(int count) + { + var result = new List(); + lock (_lock) + { + int toRead = Math.Min(count, _count); + // 从最新到最旧 + for (int i = 0; i < toRead; i++) + { + int idx = (_writeIndex - 1 - i + _capacity) % _capacity; + result.Add(_buffer[idx]); + } + } + return result; + } + + /// 获取最新一条日志 + public LogEntry GetLatest() + { + lock (_lock) + { + if (_count == 0) return null; + int idx = (_writeIndex - 1 + _capacity) % _capacity; + return _buffer[idx]; + } + } + } +} diff --git a/src/CncSimulator/Core/SimulatorEngine.cs b/src/CncSimulator/Core/SimulatorEngine.cs new file mode 100644 index 0000000..d7391d1 --- /dev/null +++ b/src/CncSimulator/Core/SimulatorEngine.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using CncSimulator.Admin; +using CncSimulator.Config; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CncSimulator.Core +{ + /// + /// 引擎主控。管理多个SimulatorServer实例和总管理页面。 + /// + public class SimulatorEngine + { + private readonly List _servers = new List(); + private SimulatorConfig _config; + private HttpListener _gatewayListener; + private readonly AdminHandler _adminHandler = new AdminHandler(); + private bool _running; + + /// 所有地址服务 + public List Servers => _servers; + + /// 加载配置并创建所有SimulatorServer + public void LoadConfig(SimulatorConfig config) + { + _config = config; + _servers.Clear(); + + foreach (var addr in config.Addresses) + { + var server = new SimulatorServer(addr); + _servers.Add(server); + } + } + + /// 启动所有服务 + public void StartAll() + { + _running = true; + + // 启动所有地址服务 + foreach (var server in _servers) + { + server.Start(); + } + + // 启动总管理页面 + StartGateway(); + } + + /// 停止所有服务 + public void StopAll() + { + _running = false; + foreach (var server in _servers) + { + server.Shutdown(); + } + + try { _gatewayListener?.Stop(); } catch { } + } + + /// 按端口查找SimulatorServer + public SimulatorServer GetServerByPort(int port) + { + foreach (var s in _servers) + { + if (s.Port == port) return s; + } + return null; + } + + /// 获取所有地址的状态汇总 + public JArray GetStatusSummary() + { + var arr = new JArray(); + foreach (var server in _servers) + { + arr.Add(new JObject + { + ["name"] = server.Name, + ["port"] = server.Port, + ["isRunning"] = server.IsRunning, + ["totalDevices"] = server.TotalDeviceCount, + ["onlineDevices"] = server.OnlineDeviceCount, + ["requestCount"] = server.RequestCount, + ["dataChangeInterval"] = server.Config.DataChangeInterval + }); + } + return arr; + } + + // ===== 总管理页面 ===== + + private void StartGateway() + { + try + { + _gatewayListener = new HttpListener(); + _gatewayListener.Prefixes.Add($"http://+:{_config.GatewayPort}/"); + _gatewayListener.Start(); + _gatewayListener.BeginGetContext(OnGatewayRequest, null); + Console.WriteLine($" [✓] 总管理页面: http://localhost:{_config.GatewayPort}/admin"); + } + catch (Exception ex) + { + Console.WriteLine($" [✗] 总管理页面启动失败(端口 {_config.GatewayPort}): {ex.Message}"); + } + } + + private void OnGatewayRequest(IAsyncResult ar) + { + HttpListenerContext ctx; + try + { + if (_gatewayListener == null || !_gatewayListener.IsListening) return; + ctx = _gatewayListener.EndGetContext(ar); + } + catch { return; } + + try + { + if (_gatewayListener.IsListening) + _gatewayListener.BeginGetContext(OnGatewayRequest, null); + } + catch { } + + string path = ctx.Request.Url.AbsolutePath.TrimEnd('/'); + + try + { + if (path == "/admin") + { + string html = _adminHandler.BuildGatewayPage(this); + SendResponse(ctx, 200, html, "text/html; charset=utf-8"); + } + else if (path == "/admin/api/status") + { + var summary = GetStatusSummary(); + SendResponse(ctx, 200, summary.ToString(), "application/json"); + } + else if (path == "/admin/api/start") + { + foreach (var s in _servers) if (!s.IsRunning) s.Start(); + SendResponse(ctx, 200, "{\"ok\":true}", "application/json"); + } + else if (path == "/admin/api/stop") + { + foreach (var s in _servers) s.Stop(); + SendResponse(ctx, 200, "{\"ok\":true}", "application/json"); + } + else + { + SendResponse(ctx, 200, "CNC模拟采集服务网关。请访问 /admin 管理页面。", "text/plain"); + } + } + catch (Exception ex) + { + try { SendResponse(ctx, 500, ex.Message, "text/plain"); } catch { } + } + } + + private void SendResponse(HttpListenerContext ctx, int status, string body, string contentType) + { + try + { + ctx.Response.StatusCode = status; + ctx.Response.ContentType = contentType; + byte[] bytes = Encoding.UTF8.GetBytes(body); + ctx.Response.ContentLength64 = bytes.Length; + ctx.Response.OutputStream.Write(bytes, 0, bytes.Length); + ctx.Response.OutputStream.Close(); + } + catch { } + } + } +} diff --git a/src/CncSimulator/Core/SimulatorServer.cs b/src/CncSimulator/Core/SimulatorServer.cs new file mode 100644 index 0000000..ef03b64 --- /dev/null +++ b/src/CncSimulator/Core/SimulatorServer.cs @@ -0,0 +1,521 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Timers; +using CncSimulator.Admin; +using CncSimulator.Config; +using CncSimulator.Device; +using CncSimulator.Generator; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CncSimulator.Core +{ + /// + /// 单个模拟地址的HTTP服务。 + /// 一个端口同时提供:数据API + 管理界面。 + /// + public class SimulatorServer + { + private readonly AddressConfig _config; + private readonly List _devices; + private readonly List _players; + private readonly IBrandGenerator _generator; + private readonly LogRecorder _logRecorder; + private readonly AdminHandler _adminHandler; + private HttpListener _listener; + private Timer _tickTimer; + private bool _isRunning; + private string _networkError = "normal"; + private DateTime _startTime; + private long _requestCount; + private long _successCount; + private long _failCount; + private bool _stopped; + + /// 地址名称 + public string Name => _config.Name; + + /// 端口 + public int Port => _config.Port; + + /// 是否运行中 + public bool IsRunning => _isRunning; + + /// 总请求次数 + public long RequestCount => _requestCount; + + /// 设备总数 + public int TotalDeviceCount => _devices.Count; + + /// 在线设备数 + public int OnlineDeviceCount + { + get + { + int count = 0; + foreach (var d in _devices) if (d.State.IsOnline) count++; + return count; + } + } + + /// 启动时间 + public DateTime StartTime => _startTime; + + /// 配置引用 + public AddressConfig Config => _config; + + /// 设备列表引用 + public List Devices => _devices; + + /// 日志记录器引用 + public LogRecorder LogRecorder => _logRecorder; + + public SimulatorServer(AddressConfig config) + { + _config = config; + _devices = new List(); + _players = new List(); + _logRecorder = new LogRecorder(200); + _generator = new FanucDataGenerator(); + _adminHandler = new AdminHandler(); + + // 初始化设备和剧本播放器 + for (int i = 0; i < config.Devices.Count; i++) + { + var devCfg = config.Devices[i]; + var sim = new DeviceSimulator(devCfg, config.DataChangeInterval); + _devices.Add(sim); + + var player = new ScenarioPlayer( + sim, + config.ScenarioMode == "auto", + Environment.TickCount + i * 1000 + devCfg.DeviceCode.GetHashCode() + ); + _players.Add(player); + } + } + + /// 启动HTTP服务和定时器 + public void Start() + { + if (_isRunning) return; + + _startTime = DateTime.Now; + _isRunning = true; + _stopped = false; + + // 启动tick定时器 + _tickTimer = new Timer(_config.DataChangeInterval * 1000); + _tickTimer.Elapsed += OnTick; + _tickTimer.Start(); + + // 启动HttpListener + _listener = new HttpListener(); + _listener.Prefixes.Add($"http://+:{_config.Port}/"); + _listener.Start(); + + // 异步接收请求 + _listener.BeginGetContext(OnRequest, null); + + Console.WriteLine($" [✓] {_config.Name}: http://localhost:{_config.Port}/ (管理: http://localhost:{_config.Port}/admin)"); + } + + /// 停止模拟(停止Timer,HttpListener继续运行以接受管理请求) + public void Stop() + { + _isRunning = false; + _tickTimer?.Stop(); + _tickTimer?.Dispose(); + _tickTimer = null; + } + + /// 完全关闭(包括HttpListener) + public void Shutdown() + { + Stop(); + _stopped = true; + try { _listener?.Stop(); } catch { } + } + + /// 手动触发设备事件 + public void TriggerDeviceEvent(string deviceCode, string eventType) + { + for (int i = 0; i < _devices.Count; i++) + { + if (_devices[i].State.DeviceCode == deviceCode) + { + _players[i].TriggerEvent(eventType); + return; + } + } + } + + /// 设置网络异常类型 + public void SetNetworkError(string type) + { + _networkError = type; + + if (type == "refuse") + { + // 停止HttpListener模拟拒绝连接 + try { _listener?.Stop(); } catch { } + } + else if (type == "normal") + { + // 恢复HttpListener + if (_listener != null && !_stopped) + { + try + { + if (!_listener.IsListening) + { + _listener.Start(); + _listener.BeginGetContext(OnRequest, null); + } + } + catch { } + } + } + } + + /// 修改数据变化频率 + public void SetInterval(int seconds) + { + _config.DataChangeInterval = seconds; + if (_tickTimer != null) + { + _tickTimer.Interval = seconds * 1000; + } + } + + /// 切换剧本模式 + public void SetMode(string mode) + { + _config.ScenarioMode = mode; + foreach (var player in _players) + { + player.SetMode(mode); + } + } + + /// 获取所有设备状态(用于管理API) + public JArray GetDeviceStatusArray() + { + var arr = new JArray(); + foreach (var dev in _devices) + { + var s = dev.State; + arr.Add(new JObject + { + ["deviceCode"] = s.DeviceCode, + ["desc"] = s.Desc, + ["scenario"] = s.CurrentScenario, + ["isOnline"] = s.IsOnline, + ["programName"] = s.ProgramName, + ["partCount"] = s.PartCount, + ["runStatus"] = s.RunStatus, + ["operateMode"] = s.OperateMode, + ["spindleSpeedSet"] = s.SpindleSpeedSet, + ["spindleSpeedActual"] = s.SpindleSpeedActual, + ["feedSpeedSet"] = s.FeedSpeedSet, + ["feedSpeedActual"] = s.FeedSpeedActual, + ["spindleLoad"] = s.SpindleLoad, + ["machiningStatus"] = s.MachiningStatus, + ["scenarioTick"] = s.ScenarioTick, + ["scenarioDuration"] = s.ScenarioDuration + }); + } + return arr; + } + + // ===== 私有方法 ===== + + /// 定时器回调:推进每台设备的剧本和状态 + private void OnTick(object sender, ElapsedEventArgs e) + { + if (!_isRunning) return; + + foreach (var player in _players) + { + player.Tick(); + // 每台设备tick后更新状态 + } + foreach (var dev in _devices) + { + dev.Tick(); + } + } + + /// 生成当前JSON响应 + private string GenerateCurrentJson() + { + var devices = new JArray(); + foreach (var dev in _devices) + { + if (dev.State.IsOnline) + { + devices.Add(_generator.GenerateDevice(dev.State)); + } + } + return devices.ToString(Formatting.None); + } + + /// 生成关键数据摘要 + private string GenerateKeyData() + { + var parts = new List(); + foreach (var dev in _devices) + { + var s = dev.State; + if (s.IsOnline) + { + parts.Add($"{s.DeviceCode}(P={s.PartCount},Prog={s.ProgramName},Run={s.RunStatus})"); + } + else + { + parts.Add($"{s.DeviceCode}(OFFLINE)"); + } + } + return string.Join(" ", parts); + } + + /// HttpListener请求回调 + private void OnRequest(IAsyncResult ar) + { + HttpListenerContext ctx; + try + { + if (_listener == null || !_listener.IsListening) return; + ctx = _listener.EndGetContext(ar); + } + catch + { + return; + } + + // 继续接收下一个请求 + try + { + if (_listener.IsListening) + _listener.BeginGetContext(OnRequest, null); + } + catch { } + + ProcessRequest(ctx); + } + + /// 处理单个HTTP请求 + private void ProcessRequest(HttpListenerContext ctx) + { + string path = ctx.Request.Url.AbsolutePath.TrimEnd('/'); + string method = ctx.Request.HttpMethod; + + try + { + // ===== 管理页面路由 ===== + if (path == "/admin") + { + ServeAdminPage(ctx); + return; + } + + // ===== 管理API路由 ===== + if (path.StartsWith("/admin/api/")) + { + HandleAdminApi(ctx, path, method); + return; + } + + // ===== 数据接口 ===== + if (path == "" || path == "/data") + { + ServeData(ctx); + return; + } + + // 404 + SendJsonResponse(ctx, 404, new JObject { ["error"] = "Not Found" }.ToString()); + } + catch (Exception ex) + { + try + { + SendJsonResponse(ctx, 500, new JObject { ["error"] = ex.Message }.ToString()); + } + catch { } + } + } + + /// 提供数据接口 + private void ServeData(HttpListenerContext ctx) + { + _requestCount++; + + // 网络异常模拟 + switch (_networkError) + { + case "http500": + _failCount++; + SendTextResponse(ctx, 500, "Internal Server Error (模拟)"); + return; + case "timeout": + _failCount++; + System.Threading.Thread.Sleep(60000); + SendTextResponse(ctx, 200, "delayed response"); + return; + case "empty": + _successCount++; + SendTextResponse(ctx, 200, "[]", "application/json"); + return; + case "malformed": + _successCount++; + SendTextResponse(ctx, 200, "{broken", "application/json"); + return; + } + + // 正常生成数据 + var sw = System.Diagnostics.Stopwatch.StartNew(); + string json = GenerateCurrentJson(); + sw.Stop(); + + string keyData = GenerateKeyData(); + _logRecorder.Record(_config.Port, OnlineDeviceCount, keyData, json, sw.ElapsedMilliseconds); + _successCount++; + + SendTextResponse(ctx, 200, json, "application/json"); + + Console.WriteLine($"{DateTime.Now:HH:mm:ss} [{_config.Port}] GET / → {OnlineDeviceCount}台设备, {sw.ElapsedMilliseconds}ms"); + } + + /// 提供管理页面 + private void ServeAdminPage(HttpListenerContext ctx) + { + string html = _adminHandler.BuildSingleAddressPage(this); + SendTextResponse(ctx, 200, html, "text/html; charset=utf-8"); + } + + /// 处理管理API + private void HandleAdminApi(HttpListenerContext ctx, string path, string method) + { + switch (path) + { + case "/admin/api/status": + var status = new JObject + { + ["name"] = _config.Name, + ["port"] = _config.Port, + ["isRunning"] = _isRunning, + ["requestCount"] = _requestCount, + ["successCount"] = _successCount, + ["failCount"] = _failCount, + ["totalDevices"] = _devices.Count, + ["onlineDevices"] = OnlineDeviceCount, + ["dataChangeInterval"] = _config.DataChangeInterval, + ["scenarioMode"] = _config.ScenarioMode, + ["networkError"] = _networkError, + ["startTime"] = _startTime.ToString("yyyy-MM-dd HH:mm:ss"), + ["uptime"] = (DateTime.Now - _startTime).ToString(@"hh\:mm\:ss"), + ["devices"] = GetDeviceStatusArray() + }; + SendTextResponse(ctx, 200, status.ToString(), "application/json"); + break; + + case "/admin/api/start": + Start(); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/stop": + Stop(); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/event": + string eventBody = ReadRequestBody(ctx); + var eventObj = JObject.Parse(eventBody); + TriggerDeviceEvent( + eventObj["deviceId"]?.ToString(), + eventObj["eventType"]?.ToString() + ); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/interval": + string intervalBody = ReadRequestBody(ctx); + var intervalObj = JObject.Parse(intervalBody); + SetInterval(intervalObj["value"]?.Value() ?? 10); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/network": + string netBody = ReadRequestBody(ctx); + var netObj = JObject.Parse(netBody); + SetNetworkError(netObj["type"]?.ToString() ?? "normal"); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/mode": + string modeBody = ReadRequestBody(ctx); + var modeObj = JObject.Parse(modeBody); + SetMode(modeObj["mode"]?.ToString() ?? "auto"); + SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json"); + break; + + case "/admin/api/logs": + var logs = _logRecorder.GetRecentLogs(100); + var logsArr = new JArray(); + for (int i = 0; i < logs.Count; i++) + { + var l = logs[i]; + logsArr.Add(new JObject + { + ["index"] = logs.Count - i, + ["timestamp"] = l.Timestamp.ToString("HH:mm:ss"), + ["deviceCount"] = l.DeviceCount, + ["keyData"] = l.KeyData, + ["duration"] = l.Duration, + ["fullJson"] = l.FullJson + }); + } + SendTextResponse(ctx, 200, logsArr.ToString(), "application/json"); + break; + + default: + SendJsonResponse(ctx, 404, "{\"error\":\"Unknown API\"}"); + break; + } + } + + // ===== HTTP辅助方法 ===== + + private string ReadRequestBody(HttpListenerContext ctx) + { + using (var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + + private void SendTextResponse(HttpListenerContext ctx, int statusCode, string body, string contentType = "text/plain") + { + try + { + ctx.Response.StatusCode = statusCode; + ctx.Response.ContentType = contentType; + byte[] bytes = Encoding.UTF8.GetBytes(body); + ctx.Response.ContentLength64 = bytes.Length; + ctx.Response.OutputStream.Write(bytes, 0, bytes.Length); + ctx.Response.OutputStream.Close(); + } + catch { } + } + + private void SendJsonResponse(HttpListenerContext ctx, int statusCode, string json) + { + SendTextResponse(ctx, statusCode, json, "application/json"); + } + } +} diff --git a/src/CncSimulator/Device/DeviceSimulator.cs b/src/CncSimulator/Device/DeviceSimulator.cs new file mode 100644 index 0000000..d0fd5ce --- /dev/null +++ b/src/CncSimulator/Device/DeviceSimulator.cs @@ -0,0 +1,220 @@ +using System; + +namespace CncSimulator.Device +{ + /// + /// 单台设备的状态机。 + /// 维护设备当前状态,根据场景规则更新各字段。 + /// + public class DeviceSimulator + { + private readonly DeviceState _state; + private readonly Random _rng; + private static readonly string[] MachiningStatusOptions = { "G01", "G01", "G01", "G02", "G00" }; + private static readonly string[] ProgramPool = { "O0001", "O0002", "1566.NC", "PART-A", "TEST-03" }; + private int _programPoolIndex; + + /// 设备当前状态(只读引用) + public DeviceState State => _state; + + public DeviceSimulator(Config.DeviceConfig config, int dataChangeInterval) + { + _rng = new Random(Environment.TickCount + config.DeviceCode.GetHashCode()); + _state = new DeviceState + { + DeviceCode = config.DeviceCode, + Desc = config.Desc, + ProgramName = config.InitialProgram, + PartCount = config.InitialPartCount, + // LastPartCount removed - not in DeviceState + DataChangeInterval = dataChangeInterval, + IsOnline = true, + DeviceStatus = 1, + RunStatus = 0, + OperateMode = 10, + SpindleSpeedSet = 450, + FeedSpeedSet = 60, + SpindleSpeedActual = 0, + FeedSpeedActual = 0, + SpindleLoad = 0, + SpindleOverride = 100, + PowerOnTime = _rng.Next(20000000, 24000000), + RunTime = _rng.Next(15000, 20000), + CuttingTime = _rng.Next(6000000, 7000000), + CycleTime = _rng.Next(500, 800), + MachiningStatus = "", + ProgramContent = "", + CurrentScenario = "idle", + ScenarioTick = 0, + ScenarioDuration = 10 + }; + + // 找到初始程序在池中的位置 + _programPoolIndex = Array.IndexOf(ProgramPool, config.InitialProgram); + if (_programPoolIndex < 0) _programPoolIndex = 0; + } + + /// 每次tick调用,根据当前场景更新字段 + public void Tick() + { + if (!_state.IsOnline) return; + + _state.ScenarioTick++; + ApplyScenarioUpdate(_state.CurrentScenario); + } + + /// 切换场景 + public void SetScenario(string scenarioName, int duration) + { + _state.CurrentScenario = scenarioName; + _state.ScenarioTick = 0; + _state.ScenarioDuration = duration; + + // 瞬时场景立即执行 + if (scenarioName == "program_change") ApplyProgramChange(); + else if (scenarioName == "manual_reset") ApplyManualReset(); + else if (scenarioName == "power_on") ApplyPowerOn(); + } + + /// 手动触发事件(忽略剧本) + public void TriggerEvent(string eventType) + { + switch (eventType) + { + case "power_off": + ApplyPowerOff(); + break; + case "power_on": + ApplyPowerOn(); + break; + case "program_change": + ApplyProgramChange(); + break; + case "manual_reset": + ApplyManualReset(); + break; + case "pause": + SetScenario("pause", 999); + break; + case "idle": + SetScenario("idle", 999); + break; + case "machining": + SetScenario("machining", 999); + break; + case "same_part": + SetScenario("same_part", 999); + break; + } + } + + /// 根据场景名更新字段 + private void ApplyScenarioUpdate(string scenario) + { + int interval = _state.DataChangeInterval; + + switch (scenario) + { + case "machining": + _state.PartCount++; + _state.RunStatus = 3; + _state.DeviceStatus = 1; + _state.OperateMode = 1; + // 主轴实际速度 = 设定 ± 10%随机波动 + _state.SpindleSpeedActual = _state.SpindleSpeedSet * (1m + (decimal)(_rng.NextDouble() * 0.2 - 0.1)); + _state.FeedSpeedActual = _state.FeedSpeedSet * (1m + (decimal)(_rng.NextDouble() * 0.1 - 0.05)); + _state.SpindleLoad = _rng.Next(15, 46); + _state.SpindleOverride = 100; + _state.MachiningStatus = MachiningStatusOptions[_rng.Next(MachiningStatusOptions.Length)]; + _state.CuttingTime += interval; + _state.RunTime += interval; + _state.PowerOnTime += interval; + _state.CycleTime += interval; + _state.ProgramContent = "<" + _state.ProgramName + ">\nG40G49G80\n( SIMULATOR )"; + break; + + case "same_part": + // 所有值不变,只有time会在生成JSON时更新 + _state.PowerOnTime += interval; + _state.RunTime += interval; + break; + + case "idle": + _state.RunStatus = 0; + _state.OperateMode = 10; + _state.SpindleSpeedActual = 0; + _state.FeedSpeedActual = 0; + _state.SpindleLoad = 0; + _state.FeedSpeedSet = 0; + _state.MachiningStatus = ""; + _state.PowerOnTime += interval; + _state.RunTime += interval; + break; + + case "pause": + _state.RunStatus = 1; + _state.SpindleSpeedActual = 0; + _state.FeedSpeedActual = 0; + _state.SpindleLoad = 0; + _state.MachiningStatus = ""; + _state.PowerOnTime += interval; + _state.RunTime += interval; + break; + + case "program_change": + case "manual_reset": + case "power_on": + // 瞬时场景,已在SetScenario中处理 + break; + } + } + + /// 换零件 + private void ApplyProgramChange() + { + string oldProgram = _state.ProgramName; + _programPoolIndex = (_programPoolIndex + 1) % ProgramPool.Length; + _state.ProgramName = ProgramPool[_programPoolIndex]; + _state.PartCount = 0; + _state.CycleTime = 0; + _state.RunStatus = 3; + _state.DeviceStatus = 1; + _state.OperateMode = 1; + _state.MachiningStatus = "G01"; + _state.SpindleSpeedSet = _rng.Next(200, 801); + _state.FeedSpeedSet = _rng.Next(30, 151); + _state.SpindleOverride = 100; + _state.ProgramContent = "<" + _state.ProgramName + ">\nG40G49G80\n( SIMULATOR )"; + } + + /// 手动清零 + private void ApplyManualReset() + { + _state.PartCount = 0; + _state.RunStatus = 3; + _state.DeviceStatus = 1; + } + + /// 断电 + private void ApplyPowerOff() + { + _state.IsOnline = false; + _state.DeviceStatus = 0; + } + + /// 恢复开机 + private void ApplyPowerOn() + { + _state.IsOnline = true; + _state.DeviceStatus = 1; + _state.PartCount = 0; + _state.CycleTime = 0; + _state.RunStatus = 0; + _state.OperateMode = 10; + _state.SpindleSpeedActual = 0; + _state.FeedSpeedActual = 0; + _state.SpindleLoad = 0; + _state.MachiningStatus = ""; + } + } +} diff --git a/src/CncSimulator/Device/DeviceState.cs b/src/CncSimulator/Device/DeviceState.cs new file mode 100644 index 0000000..11ad840 --- /dev/null +++ b/src/CncSimulator/Device/DeviceState.cs @@ -0,0 +1,82 @@ +namespace CncSimulator.Device +{ + /// 单台模拟设备的完整状态 + public class DeviceState + { + // ===== 固定信息(来自配置) ===== + /// 设备编码 + public string DeviceCode { get; set; } + + /// 设备描述 + public string Desc { get; set; } + + // ===== 动态状态 ===== + /// 当前场景名 + public string CurrentScenario { get; set; } = "idle"; + + /// 是否在线(断电=false,不参与JSON生成) + public bool IsOnline { get; set; } = true; + + /// 当前NC程序名 + public string ProgramName { get; set; } = "O0001"; + + /// 当前零件数 + public int PartCount { get; set; } = 0; + + /// 设备状态 _io_status: 0=离线, 1=在线 + public int DeviceStatus { get; set; } = 1; + + /// 运行状态: 0=待机, 1=运行, 3=加工中 + public int RunStatus { get; set; } = 0; + + /// 操作模式: 1=MEM, 10=JOG + public int OperateMode { get; set; } = 1; + + /// 主轴设定速度 + public decimal SpindleSpeedSet { get; set; } = 450; + + /// 进给设定速度 + public decimal FeedSpeedSet { get; set; } = 60; + + /// 主轴实际速度 + public decimal SpindleSpeedActual { get; set; } = 0; + + /// 进给实际速度 + public decimal FeedSpeedActual { get; set; } = 0; + + /// 主轴负载 + public decimal SpindleLoad { get; set; } = 0; + + /// 主轴倍率 + public decimal SpindleOverride { get; set; } = 100; + + /// 开机累计时间(秒) + public decimal PowerOnTime { get; set; } = 0; + + /// 运行累计时间(秒) + public decimal RunTime { get; set; } = 0; + + /// 切削累计时间(秒) + public decimal CuttingTime { get; set; } = 0; + + /// 循环时间(秒) + public decimal CycleTime { get; set; } = 0; + + /// 加工状态: G01/G00/G02/等 + public string MachiningStatus { get; set; } = ""; + + /// 加工程序内容片段 + public string ProgramContent { get; set; } = ""; + + // ===== 剧本控制 ===== + /// 当前场景已持续的tick数 + public int ScenarioTick { get; set; } = 0; + + /// 当前场景总tick数 + public int ScenarioDuration { get; set; } = 10; + + // ===== 内部辅助 ===== + /// 数据变化间隔(秒) + public int DataChangeInterval { get; set; } = 10; + } +} diff --git a/src/CncSimulator/Device/ScenarioPlayer.cs b/src/CncSimulator/Device/ScenarioPlayer.cs new file mode 100644 index 0000000..37aa43a --- /dev/null +++ b/src/CncSimulator/Device/ScenarioPlayer.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace CncSimulator.Device +{ + /// + /// 剧本播放器。按预设顺序循环场景,每台设备独立运行。 + /// + public class ScenarioPlayer + { + private readonly DeviceSimulator _simulator; + private readonly Random _rng; + private int _stepIndex; + private bool _autoMode; + + /// 剧本步骤定义:{ 场景名, 最小tick, 最大tick } + private static readonly string[,] ScriptSteps = new string[,] + { + { "machining", "20", "40" }, + { "same_part", "5", "10" }, + { "machining", "10", "20" }, + { "program_change", "1", "1" }, + { "machining", "15", "30" }, + { "pause", "5", "10" }, + { "machining", "10", "15" }, + { "same_part", "3", "5" }, + { "manual_reset", "1", "1" }, + { "machining", "20", "30" }, + { "idle", "10", "20" }, + { "machining", "15", "25" }, + { "power_off", "5", "15" }, + { "power_on", "1", "1" } + }; + + public ScenarioPlayer(DeviceSimulator simulator, bool autoMode, int seed) + { + _simulator = simulator; + _rng = new Random(seed); + _autoMode = autoMode; + + // 随机偏移起始步骤,避免所有设备同步 + _stepIndex = _rng.Next(ScriptSteps.GetLength(0)); + + // 初始化第一步 + StartCurrentStep(); + } + + /// 切换模式 + public void SetMode(string mode) + { + _autoMode = (mode == "auto"); + } + + /// 每个数据变化间隔调用一次 + public void Tick() + { + if (!_autoMode) return; + + var state = _simulator.State; + + // 断电状态下不推进剧本 + if (!state.IsOnline) return; + + state.ScenarioTick++; + + // 检查是否达到当前场景的持续时间 + if (state.ScenarioTick >= state.ScenarioDuration) + { + AdvanceToNextStep(); + } + } + + /// 手动触发特定事件 + public void TriggerEvent(string eventType) + { + _simulator.TriggerEvent(eventType); + } + + /// 推进到下一个剧本步骤 + private void AdvanceToNextStep() + { + _stepIndex = (_stepIndex + 1) % ScriptSteps.GetLength(0); + StartCurrentStep(); + } + + /// 启动当前步骤 + private void StartCurrentStep() + { + string scenario = ScriptSteps[_stepIndex, 0]; + int minTicks = int.Parse(ScriptSteps[_stepIndex, 1]); + int maxTicks = int.Parse(ScriptSteps[_stepIndex, 2]); + int duration = (minTicks == maxTicks) ? minTicks : _rng.Next(minTicks, maxTicks + 1); + + _simulator.SetScenario(scenario, duration); + } + } +} diff --git a/src/CncSimulator/Generator/FanucDataGenerator.cs b/src/CncSimulator/Generator/FanucDataGenerator.cs new file mode 100644 index 0000000..7fc04ca --- /dev/null +++ b/src/CncSimulator/Generator/FanucDataGenerator.cs @@ -0,0 +1,94 @@ +using System; +using Newtonsoft.Json.Linq; +using CncSimulator.Device; + +namespace CncSimulator.Generator +{ + /// + /// FANUC品牌JSON数据生成器。 + /// 根据设备状态生成18个Tag的FANUC格式JSON。 + /// + public class FanucDataGenerator : IBrandGenerator + { + private readonly Random _rng = new Random(); + + /// 品牌标识 + public string BrandKey => "fanuc"; + + /// 生成单个设备的完整JSON对象 + public JObject GenerateDevice(DeviceState state) + { + var device = new JObject + { + ["device"] = state.DeviceCode, + ["desc"] = state.Desc, + ["tags"] = GenerateTags(state) + }; + return device; + } + + /// 生成18个Tag的JArray + private JArray GenerateTags(DeviceState state) + { + var tags = new JArray(); + DateTime baseTime = DateTime.Now; + + // 每个tag的时间基准上随机偏移 -5~0 秒 + DateTime TagTime() + { + return baseTime.AddSeconds(-_rng.Next(0, 6)); + } + + // 数值型tag + void AddNumericTag(string id, string desc, decimal value) + { + tags.Add(new JObject + { + ["id"] = id, + ["desc"] = desc, + ["quality"] = "0", + ["value"] = value.ToString("0.00000"), + ["time"] = TagTime().ToString("yyyy-MM-dd HH:mm:ss") + }); + } + + // 字符串型tag + void AddStringTag(string id, string desc, string value) + { + tags.Add(new JObject + { + ["id"] = id, + ["desc"] = desc, + ["quality"] = "0", + ["value"] = value, + ["time"] = TagTime().ToString("yyyy-MM-dd HH:mm:ss") + }); + } + + AddNumericTag("_io_status", "设备状态", state.DeviceStatus); + AddNumericTag("Tag2", "当前轴数", 4); + AddStringTag("Tag5", "执行的NC主程序名", state.ProgramName); + AddStringTag("Tag6", "执行的NC主程序号", "N0"); + AddStringTag("Tag7", "当前加工程序内容", + string.IsNullOrEmpty(state.ProgramContent) + ? $"<{state.ProgramName}>\nG40G49G80\n( SIMULATOR )" + : state.ProgramContent); + AddNumericTag("Tag8", "当前加工零件数", state.PartCount); + AddNumericTag("Tag9", "运行状态", state.RunStatus); + AddNumericTag("Tag11", "操作模式", state.OperateMode); + AddNumericTag("Tag14", "当前主轴倍率", state.SpindleOverride); + AddNumericTag("Tag17", "主轴设定速度", state.SpindleSpeedSet); + AddNumericTag("Tag18", "进给设定速度", state.FeedSpeedSet); + AddNumericTag("Tag19", "主轴实际速度", state.SpindleSpeedActual); + AddNumericTag("Tag20", "进给实际转速", state.FeedSpeedActual); + AddNumericTag("Tag21", "主轴负载", state.SpindleLoad); + AddNumericTag("Tag22", "开机时间", state.PowerOnTime); + AddNumericTag("Tag23", "运行时间", state.RunTime); + AddNumericTag("Tag24", "切削时间", state.CuttingTime); + AddNumericTag("Tag25", "循环时间", state.CycleTime); + AddStringTag("Tag26", "加工状态", state.MachiningStatus); + + return tags; + } + } +} diff --git a/src/CncSimulator/Generator/IBrandGenerator.cs b/src/CncSimulator/Generator/IBrandGenerator.cs new file mode 100644 index 0000000..7dac345 --- /dev/null +++ b/src/CncSimulator/Generator/IBrandGenerator.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json.Linq; +using CncSimulator.Device; + +namespace CncSimulator.Generator +{ + /// 品牌数据生成器接口(预留扩展) + public interface IBrandGenerator + { + /// 品牌标识(配置文件用) + string BrandKey { get; } + + /// 根据设备状态生成一个设备的JSON对象 + JObject GenerateDevice(DeviceState state); + } +} diff --git a/src/CncSimulator/Program.cs b/src/CncSimulator/Program.cs new file mode 100644 index 0000000..7cf7ca2 --- /dev/null +++ b/src/CncSimulator/Program.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Threading; +using CncSimulator.Core; +using CncSimulator.Config; +using Newtonsoft.Json; + +namespace CncSimulator +{ + /// + /// CNC模拟采集服务主入口。 + /// 读取配置→启动引擎→等待退出。 + /// + class Program + { + static void Main(string[] args) + { + // 初始化log4net + log4net.Config.XmlConfigurator.Configure(); + + Console.WriteLine("CNC 模拟采集服务 v1.0"); + Console.WriteLine("================================================"); + + // 读取配置 + string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "simulator.json"); + if (!File.Exists(configPath)) + { + // 尝试从项目根目录读取(开发模式) + configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "simulator.json"); + } + + if (!File.Exists(configPath)) + { + Console.WriteLine("错误: 找不到配置文件 simulator.json"); + Console.WriteLine("按任意键退出..."); + Console.ReadKey(); + return; + } + + string json = File.ReadAllText(configPath); + var config = JsonConvert.DeserializeObject(json); + + // 显示配置 + Console.WriteLine("加载配置: simulator.json"); + foreach (var addr in config.Addresses) + { + Console.WriteLine($" - {addr.Name} (:{addr.Port}) {addr.Devices.Count}台设备"); + } + + // 创建并启动引擎 + var engine = new SimulatorEngine(); + engine.LoadConfig(config); + + Console.WriteLine("\n启动服务..."); + engine.StartAll(); + + Console.WriteLine("\n按任意键退出..."); + Console.ReadKey(); + + engine.StopAll(); + Console.WriteLine("已退出。"); + } + } +} diff --git a/src/CncSimulator/simulator.json b/src/CncSimulator/simulator.json new file mode 100644 index 0000000..ad6963f --- /dev/null +++ b/src/CncSimulator/simulator.json @@ -0,0 +1,53 @@ +{ + "gatewayPort": 9000, + "addresses": [ + { + "name": "FANUC-1号模拟", + "port": 9001, + "brand": "fanuc", + "dataChangeInterval": 10, + "scenarioMode": "auto", + "devices": [ + { + "deviceCode": "CNC-A001", + "desc": "西栋1号", + "initialProgram": "O0001", + "initialPartCount": 50 + }, + { + "deviceCode": "CNC-006", + "desc": "6号机床", + "initialProgram": "O0002", + "initialPartCount": 120 + }, + { + "deviceCode": "CNC-008", + "desc": "8号机床", + "initialProgram": "O0003", + "initialPartCount": 0 + } + ] + }, + { + "name": "FANUC-2号模拟", + "port": 9002, + "brand": "fanuc", + "dataChangeInterval": 15, + "scenarioMode": "auto", + "devices": [ + { + "deviceCode": "CNC-B002", + "desc": "B栋2号", + "initialProgram": "1566.NC", + "initialPartCount": 80 + }, + { + "deviceCode": "CNC-005", + "desc": "验证机床", + "initialProgram": "TEST001", + "initialPartCount": 10 + } + ] + } + ] +}