新增CncSimulator模拟采集服务(设备状态机+8种场景+FANUC 19 Tag+管理界面+网络异常模拟)

main
haoliang 6 days ago
parent 1fd32b3082
commit 3fb5074ccf

@ -0,0 +1,274 @@
using System.Text;
using CncSimulator.Core;
namespace CncSimulator.Admin
{
/// <summary>
/// 管理页面HTML生成器。
/// 生成总管理页面和单地址管理页面的完整HTML+CSS+JS。
/// </summary>
public class AdminHandler
{
/// <summary>生成总管理页面(网关页面)</summary>
public string BuildGatewayPage(SimulatorEngine engine)
{
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang='zh-CN'><head><meta charset='utf-8'>");
sb.AppendLine("<meta name='viewport' content='width=device-width, initial-scale=1'>");
sb.AppendLine("<title>CNC 模拟采集服务 - 总管理</title>");
sb.AppendLine("<style>");
sb.AppendLine("* { margin:0; padding:0; box-sizing:border-box; }");
sb.AppendLine("body { font-family:'Microsoft YaHei',sans-serif; background:#f0f2f5; color:#333; }");
sb.AppendLine(".header { background:#1890ff; color:#fff; padding:16px 24px; display:flex; justify-content:space-between; align-items:center; }");
sb.AppendLine(".header h1 { font-size:20px; font-weight:600; }");
sb.AppendLine(".header-actions button { margin-left:8px; padding:6px 16px; border:none; border-radius:4px; cursor:pointer; font-size:14px; }");
sb.AppendLine(".btn-start { background:#52c41a; color:#fff; }");
sb.AppendLine(".btn-stop { background:#ff4d4f; color:#fff; }");
sb.AppendLine(".container { max-width:1200px; margin:20px auto; padding:0 20px; }");
sb.AppendLine(".section { background:#fff; border-radius:8px; padding:20px; margin-bottom:16px; box-shadow:0 1px 4px rgba(0,0,0,0.1); }");
sb.AppendLine(".section h2 { font-size:16px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid #e8e8e8; }");
sb.AppendLine("table { width:100%; border-collapse:collapse; }");
sb.AppendLine("th,td { padding:10px 12px; text-align:left; border-bottom:1px solid #f0f0f0; font-size:14px; }");
sb.AppendLine("th { background:#fafafa; font-weight:600; color:#666; }");
sb.AppendLine(".status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }");
sb.AppendLine(".status-running { background:#52c41a; }");
sb.AppendLine(".status-stopped { background:#d9d9d9; }");
sb.AppendLine(".btn-link { color:#1890ff; cursor:pointer; text-decoration:none; border:none; background:none; font-size:14px; }");
sb.AppendLine(".log-area { max-height:300px; overflow-y:auto; font-family:Consolas,monospace; font-size:13px; line-height:1.8; color:#666; }");
sb.AppendLine("</style></head><body>");
sb.AppendLine("<div class='header'>");
sb.AppendLine(" <h1>CNC 模拟采集服务</h1>");
sb.AppendLine(" <div class='header-actions'>");
sb.AppendLine(" <button class='btn-start' onclick='startAll()'>全部启动</button>");
sb.AppendLine(" <button class='btn-stop' onclick='stopAll()'>全部停止</button>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<div class='container'>");
sb.AppendLine(" <div class='section'><h2>地址列表</h2>");
sb.AppendLine(" <table><thead><tr><th>名称</th><th>端口</th><th>状态</th><th>设备数</th><th>数据频率</th><th>请求次数</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='addressTable'></tbody></table>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='section'><h2>控制台日志</h2>");
sb.AppendLine(" <div class='log-area' id='logArea'>加载中...</div>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<script>");
sb.AppendLine("var logLines=[];var maxLogs=50;");
sb.AppendLine("function refresh(){");
sb.AppendLine(" fetch('/admin/api/status').then(r=>r.json()).then(data=>{");
sb.AppendLine(" var tb=document.getElementById('addressTable');tb.innerHTML='';");
sb.AppendLine(" data.forEach(function(a){");
sb.AppendLine(" var st=a.isRunning?'<span class=\"status-dot status-running\"></span>运行中':'<span class=\"status-dot status-stopped\"></span>已停止';");
sb.AppendLine(" tb.innerHTML+='<tr><td>'+a.name+'</td><td>'+a.port+'</td><td>'+st+'</td><td>'+a.onlineDevices+'/'+a.totalDevices+'台</td><td>'+a.dataChangeInterval+'秒</td><td>'+a.requestCount+'</td><td><a class=\"btn-link\" href=\"http://localhost:'+a.port+'/admin\" target=\"_blank\">管理</a></td></tr>';");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function addLog(msg){var now=new Date();var ts=now.toTimeString().substr(0,8);logLines.unshift(ts+' '+msg);if(logLines.length>maxLogs)logLines.length=maxLogs;document.getElementById('logArea').innerHTML=logLines.join('<br>');}");
sb.AppendLine("function startAll(){fetch('/admin/api/start',{method:'POST'}).then(function(){addLog('全部启动');refresh()});}");
sb.AppendLine("function stopAll(){fetch('/admin/api/stop',{method:'POST'}).then(function(){addLog('全部停止');refresh()});}");
sb.AppendLine("setInterval(refresh,2000);refresh();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
}
/// <summary>生成单地址管理页面</summary>
public string BuildSingleAddressPage(SimulatorServer server)
{
var sb = new StringBuilder();
sb.AppendLine("<!DOCTYPE html>");
sb.AppendLine("<html lang='zh-CN'><head><meta charset='utf-8'>");
sb.AppendLine("<meta name='viewport' content='width=device-width, initial-scale=1'>");
sb.AppendLine("<title>" + server.Name + " - 模拟管理</title>");
sb.AppendLine("<style>");
sb.AppendLine("* { margin:0; padding:0; box-sizing:border-box; }");
sb.AppendLine("body { font-family:'Microsoft YaHei',sans-serif; background:#f0f2f5; color:#333; }");
sb.AppendLine(".header { background:#1890ff; color:#fff; padding:16px 24px; display:flex; justify-content:space-between; align-items:center; }");
sb.AppendLine(".header h1 { font-size:18px; font-weight:600; }");
sb.AppendLine(".header-actions button { margin-left:8px; padding:6px 16px; border:none; border-radius:4px; cursor:pointer; font-size:14px; }");
sb.AppendLine(".btn-start { background:#52c41a; color:#fff; }");
sb.AppendLine(".btn-stop { background:#ff4d4f; color:#fff; }");
sb.AppendLine(".container { max-width:1200px; margin:20px auto; padding:0 20px; }");
sb.AppendLine(".section { background:#fff; border-radius:8px; padding:20px; margin-bottom:16px; box-shadow:0 1px 4px rgba(0,0,0,0.1); }");
sb.AppendLine(".section h2 { font-size:16px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid #e8e8e8; }");
sb.AppendLine(".settings-row { display:flex; gap:24px; align-items:center; flex-wrap:wrap; margin-bottom:8px; }");
sb.AppendLine(".settings-row label { font-size:14px; color:#666; min-width:100px; }");
sb.AppendLine(".settings-row input[type=number] { width:80px; padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; }");
sb.AppendLine(".settings-row select { padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; }");
sb.AppendLine(".settings-row input[type=radio] { margin-right:4px; }");
sb.AppendLine(".device-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:12px; }");
sb.AppendLine(".device-card { border:1px solid #e8e8e8; border-radius:6px; padding:12px; }");
sb.AppendLine(".device-card.offline { opacity:0.5; background:#fafafa; }");
sb.AppendLine(".device-card h3 { font-size:14px; margin-bottom:8px; }");
sb.AppendLine(".device-card .info { font-size:13px; color:#666; line-height:1.8; }");
sb.AppendLine(".device-card .actions { margin-top:8px; display:flex; flex-wrap:wrap; gap:4px; }");
sb.AppendLine(".device-card .actions button { padding:2px 8px; font-size:12px; border:1px solid #d9d9d9; border-radius:3px; background:#fff; cursor:pointer; }");
sb.AppendLine(".device-card .actions button:hover { border-color:#1890ff; color:#1890ff; }");
sb.AppendLine(".json-preview { max-height:200px; overflow:auto; background:#fafafa; padding:12px; border-radius:4px; font-family:Consolas,monospace; font-size:12px; white-space:pre-wrap; word-break:break-all; }");
sb.AppendLine(".log-table { width:100%; border-collapse:collapse; font-size:13px; }");
sb.AppendLine(".log-table th,.log-table td { padding:6px 8px; text-align:left; border-bottom:1px solid #f0f0f0; }");
sb.AppendLine(".log-table th { background:#fafafa; color:#666; font-weight:600; }");
sb.AppendLine(".stats-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:12px; }");
sb.AppendLine(".stat-item { text-align:center; padding:12px; background:#fafafa; border-radius:4px; }");
sb.AppendLine(".stat-item .value { font-size:24px; font-weight:600; color:#1890ff; }");
sb.AppendLine(".stat-item .label { font-size:13px; color:#999; margin-top:4px; }");
sb.AppendLine(".btn-apply { padding:4px 12px; border:1px solid #1890ff; border-radius:4px; background:#fff; color:#1890ff; cursor:pointer; }");
sb.AppendLine("</style></head><body>");
sb.AppendLine("<div class='header'>");
sb.AppendLine(" <h1>" + server.Name + " (端口 " + server.Port + ")</h1>");
sb.AppendLine(" <div class='header-actions'>");
sb.AppendLine(" <a href='http://localhost:" + server.Port + "/' target='_blank' style='color:#fff;margin-right:8px;font-size:14px;'>数据接口</a>");
sb.AppendLine(" <button class='btn-start' onclick='startSim()'>启动</button>");
sb.AppendLine(" <button class='btn-stop' onclick='stopSim()'>停止</button>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<div class='container'>");
// 全局设置
sb.AppendLine(" <div class='section'><h2>全局设置</h2>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>数据变化频率:</label>");
sb.AppendLine(" <input type='number' id='intervalInput' min='1' max='300' value='" + server.Config.DataChangeInterval + "'> 秒");
sb.AppendLine(" <button class='btn-apply' onclick='setInterval2()'>应用</button>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>剧本模式:</label>");
sb.AppendLine(" <label><input type='radio' name='mode' value='auto'" + (server.Config.ScenarioMode == "auto" ? " checked" : "") + "> 自动循环</label>");
sb.AppendLine(" <label><input type='radio' name='mode' value='manual'" + (server.Config.ScenarioMode == "manual" ? " checked" : "") + "> 手动触发</label>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='settings-row'>");
sb.AppendLine(" <label>网络异常模拟:</label>");
sb.AppendLine(" <select id='networkSelect' onchange='setNetwork(this.value)'>");
sb.AppendLine(" <option value='normal'>正常</option>");
sb.AppendLine(" <option value='http500'>HTTP 500</option>");
sb.AppendLine(" <option value='timeout'>连接超时(60s)</option>");
sb.AppendLine(" <option value='refuse'>拒绝连接</option>");
sb.AppendLine(" <option value='empty'>返回空数组</option>");
sb.AppendLine(" <option value='malformed'>畸形JSON</option>");
sb.AppendLine(" </select>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
// 设备状态卡片
sb.AppendLine(" <div class='section'><h2>设备状态卡片</h2>");
sb.AppendLine(" <div class='device-cards' id='deviceCards'>加载中...</div>");
sb.AppendLine(" </div>");
// JSON预览
sb.AppendLine(" <div class='section'><h2>当前返回JSON预览</h2>");
sb.AppendLine(" <button class='btn-apply' onclick='refreshJson()' style='margin-bottom:8px;'>刷新</button>");
sb.AppendLine(" <div class='json-preview' id='jsonPreview'>加载中...</div>");
sb.AppendLine(" </div>");
// 日志
sb.AppendLine(" <div class='section'><h2>返回数据日志最近100条</h2>");
sb.AppendLine(" <table class='log-table'>");
sb.AppendLine(" <thead><tr><th>#</th><th>时间</th><th>设备数</th><th>关键数据</th><th>耗时</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='logTable'></tbody>");
sb.AppendLine(" </table>");
sb.AppendLine(" </div>");
// 统计
sb.AppendLine(" <div class='section'><h2>统计</h2>");
sb.AppendLine(" <div class='stats-grid' id='statsGrid'>加载中...</div>");
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
// JavaScript
sb.AppendLine("<script>");
sb.AppendLine("var scenarioNames={'machining':'正常加工','same_part':'同一零件','idle':'待机空闲','program_change':'换零件','manual_reset':'手动清零','power_off':'断电','power_on':'恢复开机','pause':'暂停加工'};");
sb.AppendLine("var runNames={0:'待机',1:'运行',3:'加工中'};");
sb.AppendLine("function refresh(){");
sb.AppendLine(" fetch('/admin/api/status').then(r=>r.json()).then(data=>{");
sb.AppendLine(" var cards=document.getElementById('deviceCards');cards.innerHTML='';");
sb.AppendLine(" data.devices.forEach(function(d){");
sb.AppendLine(" var cls=d.isOnline?'':'offline';");
sb.AppendLine(" var scName=scenarioNames[d.scenario]||d.scenario;");
sb.AppendLine(" var h='<div class=\"device-card '+cls+'\">';");
sb.AppendLine(" h+='<h3>'+d.deviceCode+' '+d.desc+(d.isOnline?'':' (离线)')+'</h3>';");
sb.AppendLine(" h+='<div class=\"info\">';");
sb.AppendLine(" h+='场景: '+scName+' ('+d.scenarioTick+'/'+d.scenarioDuration+')<br>';");
sb.AppendLine(" if(d.isOnline){");
sb.AppendLine(" h+='程序: '+d.programName+'<br>';");
sb.AppendLine(" h+='零件数: '+d.partCount+'<br>';");
sb.AppendLine(" h+='运行状态: '+d.runStatus+' ('+runNames[d.runStatus]+')<br>';");
sb.AppendLine(" h+='主轴: '+d.spindleSpeedActual+'/'+d.spindleSpeedSet+' 负载'+d.spindleLoad+'%<br>';");
sb.AppendLine(" h+='进给: '+d.feedSpeedActual+'/'+d.feedSpeedSet+'<br>';");
sb.AppendLine(" h+='加工: '+d.machiningStatus;");
sb.AppendLine(" } else { h+='(设备离线,不返回数据)'; }");
sb.AppendLine(" h+='</div>';");
// 按钮使用data属性避免转义问题
sb.AppendLine(" h+='<div class=\"actions\" data-device=\"'+d.deviceCode+'\">';");
sb.AppendLine(" h+='<button data-event=\"program_change\">换零件</button>';");
sb.AppendLine(" h+='<button data-event=\"manual_reset\">手动清零</button>';");
sb.AppendLine(" h+='<button data-event=\"power_off\">断电</button>';");
sb.AppendLine(" h+='<button data-event=\"pause\">暂停</button>';");
sb.AppendLine(" h+='<button data-event=\"power_on\">恢复</button>';");
sb.AppendLine(" h+='<button data-event=\"machining\">加工</button>';");
sb.AppendLine(" h+='<button data-event=\"idle\">待机</button>';");
sb.AppendLine(" h+='</div></div>';");
sb.AppendLine(" cards.innerHTML+=h;");
sb.AppendLine(" });");
// 事件委托处理按钮点击
sb.AppendLine(" document.querySelectorAll('.device-card .actions button').forEach(function(btn){");
sb.AppendLine(" btn.onclick=function(){");
sb.AppendLine(" var dev=this.parentElement.getAttribute('data-device');");
sb.AppendLine(" var evt=this.getAttribute('data-event');");
sb.AppendLine(" triggerEvent(dev,evt);");
sb.AppendLine(" };");
sb.AppendLine(" });");
// 统计
sb.AppendLine(" var sg=document.getElementById('statsGrid');");
sb.AppendLine(" sg.innerHTML='<div class=\"stat-item\"><div class=\"value\">'+data.requestCount+'</div><div class=\"label\">总请求次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.successCount+'</div><div class=\"label\">成功次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.failCount+'</div><div class=\"label\">失败次数</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.onlineDevices+'/'+data.totalDevices+'</div><div class=\"label\">在线/总设备</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.uptime+'</div><div class=\"label\">运行时长</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+data.dataChangeInterval+'s</div><div class=\"label\">数据频率</div></div>';");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function refreshJson(){");
sb.AppendLine(" fetch('/data').then(r=>r.text()).then(json=>{");
sb.AppendLine(" try{document.getElementById('jsonPreview').textContent=JSON.stringify(JSON.parse(json),null,2);}");
sb.AppendLine(" catch(e){document.getElementById('jsonPreview').textContent=json;}");
sb.AppendLine(" }).catch(function(e){document.getElementById('jsonPreview').textContent='请求失败: '+e.message;});");
sb.AppendLine("}");
sb.AppendLine("var allLogs=[];");
sb.AppendLine("function refreshLogs(){");
sb.AppendLine(" fetch('/admin/api/logs').then(r=>r.json()).then(logs=>{");
sb.AppendLine(" allLogs=logs;var tb=document.getElementById('logTable');tb.innerHTML='';");
sb.AppendLine(" logs.forEach(function(l){");
sb.AppendLine(" var tr=document.createElement('tr');");
sb.AppendLine(" tr.innerHTML='<td>'+l.index+'</td><td>'+l.timestamp+'</td><td>'+l.deviceCount+'</td><td>'+l.keyData+'</td><td>'+l.duration+'ms</td><td></td>';");
sb.AppendLine(" var btn=document.createElement('button');");
sb.AppendLine(" btn.textContent='完整JSON';btn.style.cssText='font-size:12px;padding:2px 6px;cursor:pointer;';");
sb.AppendLine(" btn.onclick=(function(idx){return function(){showJson(idx);};})(l.index);");
sb.AppendLine(" tr.cells[5].appendChild(btn);");
sb.AppendLine(" tb.appendChild(tr);");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
sb.AppendLine("function showJson(idx){");
sb.AppendLine(" var l=allLogs.find(function(x){return x.index===idx;});");
sb.AppendLine(" if(l){try{document.getElementById('jsonPreview').textContent=JSON.stringify(JSON.parse(l.fullJson),null,2);}catch(e){document.getElementById('jsonPreview').textContent=l.fullJson;}}");
sb.AppendLine("}");
sb.AppendLine("function startSim(){fetch('/admin/api/start',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function stopSim(){fetch('/admin/api/stop',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function setInterval2(){var v=document.getElementById('intervalInput').value;fetch('/admin/api/interval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({value:parseInt(v)})}).then(function(){refresh();});}");
sb.AppendLine("function setNetwork(type){fetch('/admin/api/network',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type:type})}).then(function(){});}");
sb.AppendLine("function triggerEvent(deviceId,eventType){fetch('/admin/api/event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceId:deviceId,eventType:eventType})}).then(function(){setTimeout(refresh,500);});}");
sb.AppendLine("document.querySelectorAll('input[name=mode]').forEach(function(r){");
sb.AppendLine(" r.addEventListener('change',function(){");
sb.AppendLine(" fetch('/admin/api/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:this.value})}).then(function(){});");
sb.AppendLine(" });");
sb.AppendLine("});");
sb.AppendLine("setInterval(function(){refresh();refreshJson();refreshLogs();},2000);");
sb.AppendLine("refresh();refreshJson();refreshLogs();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
}
}
}

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="logs\simulator" />
<datePattern value="-yyyy-MM-dd'.log'" />
<staticLogFileName value="false" />
<appendToFile value="true" />
<maximumFileSize value="50MB" />
<maxSizeRollBackups value="10" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%date %-5level] %message%newline" />
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="RollingFile" />
</root>
</log4net>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>CncSimulator</RootNamespace>
<AssemblyName>CncSimulator</AssemblyName>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<OutputType>Exe</OutputType>
<OutputPath>bin\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net472" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="log4net" Version="2.0.15" />
</ItemGroup>
<ItemGroup>
<None Update="simulator.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

@ -0,0 +1,65 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CncSimulator.Config
{
/// <summary>模拟器顶层配置</summary>
public class SimulatorConfig
{
/// <summary>总管理页面端口</summary>
[JsonProperty("gatewayPort")]
public int GatewayPort { get; set; } = 9000;
/// <summary>采集地址列表</summary>
[JsonProperty("addresses")]
public List<AddressConfig> Addresses { get; set; } = new List<AddressConfig>();
}
/// <summary>单个采集地址配置</summary>
public class AddressConfig
{
/// <summary>显示名称</summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>监听端口</summary>
[JsonProperty("port")]
public int Port { get; set; }
/// <summary>品牌标识(预留扩展)</summary>
[JsonProperty("brand")]
public string Brand { get; set; } = "fanuc";
/// <summary>数据变化频率(秒)</summary>
[JsonProperty("dataChangeInterval")]
public int DataChangeInterval { get; set; } = 10;
/// <summary>剧本模式auto=自动循环 / manual=手动触发</summary>
[JsonProperty("scenarioMode")]
public string ScenarioMode { get; set; } = "auto";
/// <summary>模拟设备列表</summary>
[JsonProperty("devices")]
public List<DeviceConfig> Devices { get; set; } = new List<DeviceConfig>();
}
/// <summary>单台模拟设备配置</summary>
public class DeviceConfig
{
/// <summary>设备编码需与cnc_machine.device_code一致</summary>
[JsonProperty("deviceCode")]
public string DeviceCode { get; set; }
/// <summary>设备描述</summary>
[JsonProperty("desc")]
public string Desc { get; set; }
/// <summary>初始NC程序名</summary>
[JsonProperty("initialProgram")]
public string InitialProgram { get; set; } = "O0001";
/// <summary>初始零件数</summary>
[JsonProperty("initialPartCount")]
public int InitialPartCount { get; set; } = 0;
}
}

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
namespace CncSimulator.Core
{
/// <summary>日志条目</summary>
public class LogEntry
{
/// <summary>时间戳</summary>
public DateTime Timestamp { get; set; }
/// <summary>地址端口</summary>
public int AddressPort { get; set; }
/// <summary>设备数量</summary>
public int DeviceCount { get; set; }
/// <summary>关键数据摘要</summary>
public string KeyData { get; set; }
/// <summary>完整JSON</summary>
public string FullJson { get; set; }
/// <summary>响应耗时(毫秒)</summary>
public long Duration { get; set; }
}
/// <summary>
/// 日志记录器。同时写入内存环形缓冲和log4net文件。
/// </summary>
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));
}
/// <summary>记录一次返回</summary>
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}");
}
/// <summary>获取最近的日志</summary>
public List<LogEntry> GetRecentLogs(int count)
{
var result = new List<LogEntry>();
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;
}
/// <summary>获取最新一条日志</summary>
public LogEntry GetLatest()
{
lock (_lock)
{
if (_count == 0) return null;
int idx = (_writeIndex - 1 + _capacity) % _capacity;
return _buffer[idx];
}
}
}
}

@ -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
{
/// <summary>
/// 引擎主控。管理多个SimulatorServer实例和总管理页面。
/// </summary>
public class SimulatorEngine
{
private readonly List<SimulatorServer> _servers = new List<SimulatorServer>();
private SimulatorConfig _config;
private HttpListener _gatewayListener;
private readonly AdminHandler _adminHandler = new AdminHandler();
private bool _running;
/// <summary>所有地址服务</summary>
public List<SimulatorServer> Servers => _servers;
/// <summary>加载配置并创建所有SimulatorServer</summary>
public void LoadConfig(SimulatorConfig config)
{
_config = config;
_servers.Clear();
foreach (var addr in config.Addresses)
{
var server = new SimulatorServer(addr);
_servers.Add(server);
}
}
/// <summary>启动所有服务</summary>
public void StartAll()
{
_running = true;
// 启动所有地址服务
foreach (var server in _servers)
{
server.Start();
}
// 启动总管理页面
StartGateway();
}
/// <summary>停止所有服务</summary>
public void StopAll()
{
_running = false;
foreach (var server in _servers)
{
server.Shutdown();
}
try { _gatewayListener?.Stop(); } catch { }
}
/// <summary>按端口查找SimulatorServer</summary>
public SimulatorServer GetServerByPort(int port)
{
foreach (var s in _servers)
{
if (s.Port == port) return s;
}
return null;
}
/// <summary>获取所有地址的状态汇总</summary>
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 { }
}
}
}

@ -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
{
/// <summary>
/// 单个模拟地址的HTTP服务。
/// 一个端口同时提供数据API + 管理界面。
/// </summary>
public class SimulatorServer
{
private readonly AddressConfig _config;
private readonly List<DeviceSimulator> _devices;
private readonly List<ScenarioPlayer> _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;
/// <summary>地址名称</summary>
public string Name => _config.Name;
/// <summary>端口</summary>
public int Port => _config.Port;
/// <summary>是否运行中</summary>
public bool IsRunning => _isRunning;
/// <summary>总请求次数</summary>
public long RequestCount => _requestCount;
/// <summary>设备总数</summary>
public int TotalDeviceCount => _devices.Count;
/// <summary>在线设备数</summary>
public int OnlineDeviceCount
{
get
{
int count = 0;
foreach (var d in _devices) if (d.State.IsOnline) count++;
return count;
}
}
/// <summary>启动时间</summary>
public DateTime StartTime => _startTime;
/// <summary>配置引用</summary>
public AddressConfig Config => _config;
/// <summary>设备列表引用</summary>
public List<DeviceSimulator> Devices => _devices;
/// <summary>日志记录器引用</summary>
public LogRecorder LogRecorder => _logRecorder;
public SimulatorServer(AddressConfig config)
{
_config = config;
_devices = new List<DeviceSimulator>();
_players = new List<ScenarioPlayer>();
_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);
}
}
/// <summary>启动HTTP服务和定时器</summary>
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)");
}
/// <summary>停止模拟停止TimerHttpListener继续运行以接受管理请求</summary>
public void Stop()
{
_isRunning = false;
_tickTimer?.Stop();
_tickTimer?.Dispose();
_tickTimer = null;
}
/// <summary>完全关闭包括HttpListener</summary>
public void Shutdown()
{
Stop();
_stopped = true;
try { _listener?.Stop(); } catch { }
}
/// <summary>手动触发设备事件</summary>
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;
}
}
}
/// <summary>设置网络异常类型</summary>
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 { }
}
}
}
/// <summary>修改数据变化频率</summary>
public void SetInterval(int seconds)
{
_config.DataChangeInterval = seconds;
if (_tickTimer != null)
{
_tickTimer.Interval = seconds * 1000;
}
}
/// <summary>切换剧本模式</summary>
public void SetMode(string mode)
{
_config.ScenarioMode = mode;
foreach (var player in _players)
{
player.SetMode(mode);
}
}
/// <summary>获取所有设备状态用于管理API</summary>
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;
}
// ===== 私有方法 =====
/// <summary>定时器回调:推进每台设备的剧本和状态</summary>
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();
}
}
/// <summary>生成当前JSON响应</summary>
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);
}
/// <summary>生成关键数据摘要</summary>
private string GenerateKeyData()
{
var parts = new List<string>();
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);
}
/// <summary>HttpListener请求回调</summary>
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);
}
/// <summary>处理单个HTTP请求</summary>
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 { }
}
}
/// <summary>提供数据接口</summary>
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");
}
/// <summary>提供管理页面</summary>
private void ServeAdminPage(HttpListenerContext ctx)
{
string html = _adminHandler.BuildSingleAddressPage(this);
SendTextResponse(ctx, 200, html, "text/html; charset=utf-8");
}
/// <summary>处理管理API</summary>
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<int>() ?? 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");
}
}
}

@ -0,0 +1,220 @@
using System;
namespace CncSimulator.Device
{
/// <summary>
/// 单台设备的状态机。
/// 维护设备当前状态,根据场景规则更新各字段。
/// </summary>
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;
/// <summary>设备当前状态(只读引用)</summary>
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;
}
/// <summary>每次tick调用根据当前场景更新字段</summary>
public void Tick()
{
if (!_state.IsOnline) return;
_state.ScenarioTick++;
ApplyScenarioUpdate(_state.CurrentScenario);
}
/// <summary>切换场景</summary>
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();
}
/// <summary>手动触发事件(忽略剧本)</summary>
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;
}
}
/// <summary>根据场景名更新字段</summary>
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;
}
}
/// <summary>换零件</summary>
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 )";
}
/// <summary>手动清零</summary>
private void ApplyManualReset()
{
_state.PartCount = 0;
_state.RunStatus = 3;
_state.DeviceStatus = 1;
}
/// <summary>断电</summary>
private void ApplyPowerOff()
{
_state.IsOnline = false;
_state.DeviceStatus = 0;
}
/// <summary>恢复开机</summary>
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 = "";
}
}
}

@ -0,0 +1,82 @@
namespace CncSimulator.Device
{
/// <summary>单台模拟设备的完整状态</summary>
public class DeviceState
{
// ===== 固定信息(来自配置) =====
/// <summary>设备编码</summary>
public string DeviceCode { get; set; }
/// <summary>设备描述</summary>
public string Desc { get; set; }
// ===== 动态状态 =====
/// <summary>当前场景名</summary>
public string CurrentScenario { get; set; } = "idle";
/// <summary>是否在线(断电=false不参与JSON生成</summary>
public bool IsOnline { get; set; } = true;
/// <summary>当前NC程序名</summary>
public string ProgramName { get; set; } = "O0001";
/// <summary>当前零件数</summary>
public int PartCount { get; set; } = 0;
/// <summary>设备状态 _io_status: 0=离线, 1=在线</summary>
public int DeviceStatus { get; set; } = 1;
/// <summary>运行状态: 0=待机, 1=运行, 3=加工中</summary>
public int RunStatus { get; set; } = 0;
/// <summary>操作模式: 1=MEM, 10=JOG</summary>
public int OperateMode { get; set; } = 1;
/// <summary>主轴设定速度</summary>
public decimal SpindleSpeedSet { get; set; } = 450;
/// <summary>进给设定速度</summary>
public decimal FeedSpeedSet { get; set; } = 60;
/// <summary>主轴实际速度</summary>
public decimal SpindleSpeedActual { get; set; } = 0;
/// <summary>进给实际速度</summary>
public decimal FeedSpeedActual { get; set; } = 0;
/// <summary>主轴负载</summary>
public decimal SpindleLoad { get; set; } = 0;
/// <summary>主轴倍率</summary>
public decimal SpindleOverride { get; set; } = 100;
/// <summary>开机累计时间(秒)</summary>
public decimal PowerOnTime { get; set; } = 0;
/// <summary>运行累计时间(秒)</summary>
public decimal RunTime { get; set; } = 0;
/// <summary>切削累计时间(秒)</summary>
public decimal CuttingTime { get; set; } = 0;
/// <summary>循环时间(秒)</summary>
public decimal CycleTime { get; set; } = 0;
/// <summary>加工状态: G01/G00/G02/等</summary>
public string MachiningStatus { get; set; } = "";
/// <summary>加工程序内容片段</summary>
public string ProgramContent { get; set; } = "";
// ===== 剧本控制 =====
/// <summary>当前场景已持续的tick数</summary>
public int ScenarioTick { get; set; } = 0;
/// <summary>当前场景总tick数</summary>
public int ScenarioDuration { get; set; } = 10;
// ===== 内部辅助 =====
/// <summary>数据变化间隔(秒)</summary>
public int DataChangeInterval { get; set; } = 10;
}
}

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
namespace CncSimulator.Device
{
/// <summary>
/// 剧本播放器。按预设顺序循环场景,每台设备独立运行。
/// </summary>
public class ScenarioPlayer
{
private readonly DeviceSimulator _simulator;
private readonly Random _rng;
private int _stepIndex;
private bool _autoMode;
/// <summary>剧本步骤定义:{ 场景名, 最小tick, 最大tick }</summary>
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();
}
/// <summary>切换模式</summary>
public void SetMode(string mode)
{
_autoMode = (mode == "auto");
}
/// <summary>每个数据变化间隔调用一次</summary>
public void Tick()
{
if (!_autoMode) return;
var state = _simulator.State;
// 断电状态下不推进剧本
if (!state.IsOnline) return;
state.ScenarioTick++;
// 检查是否达到当前场景的持续时间
if (state.ScenarioTick >= state.ScenarioDuration)
{
AdvanceToNextStep();
}
}
/// <summary>手动触发特定事件</summary>
public void TriggerEvent(string eventType)
{
_simulator.TriggerEvent(eventType);
}
/// <summary>推进到下一个剧本步骤</summary>
private void AdvanceToNextStep()
{
_stepIndex = (_stepIndex + 1) % ScriptSteps.GetLength(0);
StartCurrentStep();
}
/// <summary>启动当前步骤</summary>
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);
}
}
}

@ -0,0 +1,94 @@
using System;
using Newtonsoft.Json.Linq;
using CncSimulator.Device;
namespace CncSimulator.Generator
{
/// <summary>
/// FANUC品牌JSON数据生成器。
/// 根据设备状态生成18个Tag的FANUC格式JSON。
/// </summary>
public class FanucDataGenerator : IBrandGenerator
{
private readonly Random _rng = new Random();
/// <summary>品牌标识</summary>
public string BrandKey => "fanuc";
/// <summary>生成单个设备的完整JSON对象</summary>
public JObject GenerateDevice(DeviceState state)
{
var device = new JObject
{
["device"] = state.DeviceCode,
["desc"] = state.Desc,
["tags"] = GenerateTags(state)
};
return device;
}
/// <summary>生成18个Tag的JArray</summary>
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;
}
}
}

@ -0,0 +1,15 @@
using Newtonsoft.Json.Linq;
using CncSimulator.Device;
namespace CncSimulator.Generator
{
/// <summary>品牌数据生成器接口(预留扩展)</summary>
public interface IBrandGenerator
{
/// <summary>品牌标识(配置文件用)</summary>
string BrandKey { get; }
/// <summary>根据设备状态生成一个设备的JSON对象</summary>
JObject GenerateDevice(DeviceState state);
}
}

@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Threading;
using CncSimulator.Core;
using CncSimulator.Config;
using Newtonsoft.Json;
namespace CncSimulator
{
/// <summary>
/// CNC模拟采集服务主入口。
/// 读取配置→启动引擎→等待退出。
/// </summary>
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<SimulatorConfig>(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("已退出。");
}
}
}

@ -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
}
]
}
]
}
Loading…
Cancel
Save