模拟器改为手动启动+从数据库读取机床+零件统计+设备增减

main
haoliang 6 days ago
parent 3fb5074ccf
commit 9890daf9aa

@ -5,26 +5,27 @@ namespace CncSimulator.Admin
{
/// <summary>
/// 管理页面HTML生成器。
/// 生成总管理页面和单地址管理页面的完整HTML+CSS+JS。
/// 总管理页面:显示数据库地址+手动启停。
/// 单地址页面:设备卡片+统计+设备增减。
/// </summary>
public class AdminHandler
{
/// <summary>生成总管理页面(网关页面)</summary>
/// <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("<!DOCTYPE html><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 h1 { font-size:20px; }");
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(".btn-blue { background:#1890ff; 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; }");
@ -34,39 +35,64 @@ namespace CncSimulator.Admin
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("button { cursor:pointer; }");
sb.AppendLine(".btn-sm { padding:4px 12px; border:1px solid #d9d9d9; border-radius:4px; background:#fff; font-size:13px; }");
sb.AppendLine(".btn-sm:hover { border-color:#1890ff; color:#1890ff; }");
sb.AppendLine(".btn-sm-primary { padding:4px 12px; border:1px solid #1890ff; border-radius:4px; background:#1890ff; color:#fff; font-size:13px; }");
sb.AppendLine(".btn-sm-danger { padding:4px 12px; border:1px solid #ff4d4f; border-radius:4px; background:#fff; color:#ff4d4f; font-size:13px; }");
sb.AppendLine(".interval-input { width:60px; padding:2px 6px; border:1px solid #d9d9d9; border-radius:4px; text-align:center; }");
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(" <button class='btn-blue' onclick='reloadDb()'>刷新数据库</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 class='section'><h2>数据库采集地址</h2>");
sb.AppendLine(" <table><thead><tr><th>ID</th><th>名称</th><th>机床数</th><th>状态</th><th>端口</th><th>频率(秒)</th><th>总零件</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='addrTable'></tbody></table>");
sb.AppendLine(" </div>");
sb.AppendLine(" <div class='section'><h2>控制台日志</h2>");
sb.AppendLine(" <div class='log-area' id='logArea'>加载中...</div>");
sb.AppendLine(" <div class='section'><h2>运行中模拟端口</h2>");
sb.AppendLine(" <table><thead><tr><th>名称</th><th>端口</th><th>设备</th><th>请求次数</th><th>总零件</th><th>操作</th></tr></thead>");
sb.AppendLine(" <tbody id='runningTable'></tbody></table>");
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/db-addresses').then(r=>r.json()).then(addrs=>{");
sb.AppendLine(" var tb=document.getElementById('addrTable');tb.innerHTML='';");
sb.AppendLine(" addrs.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(" var portVal=a.runningPort||'';");
sb.AppendLine(" var h='<tr><td>'+a.dbId+'</td><td>'+a.name+'</td><td>'+a.machineCount+'台</td><td>'+st+'</td>';");
sb.AppendLine(" h+='<td>'+(a.isRunning?a.runningPort:'-')+'</td>';");
sb.AppendLine(" h+='<td>'+(a.isRunning?'<input class=\"interval-input\" id=\"int_'+a.dbId+'\" value=\"10\" disabled>':'<input class=\"interval-input\" id=\"int_'+a.dbId+'\" value=\"10\">')+'</td>';");
sb.AppendLine(" h+='<td id=\"parts_'+a.dbId+'\">-</td>';");
sb.AppendLine(" h+='<td>';");
sb.AppendLine(" if(a.isRunning){h+='<button class=\"btn-sm-danger\" onclick=\"stopAddr('+a.dbId+')\">停止</button> ';");
sb.AppendLine(" h+='<a class=\"btn-sm\" href=\"http://localhost:'+a.runningPort+'/admin\" target=\"_blank\">管理</a> ';}");
sb.AppendLine(" else{h+='<button class=\"btn-sm-primary\" onclick=\"startAddr('+a.dbId+')\">启动</button>'}");
sb.AppendLine(" h+='</td></tr>';");
sb.AppendLine(" tb.innerHTML+=h;");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
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(" var rt=document.getElementById('runningTable');rt.innerHTML='';");
sb.AppendLine(" data.forEach(function(s){");
sb.AppendLine(" rt.innerHTML+='<tr><td>'+s.name+'</td><td>'+s.port+'</td><td>'+s.onlineDevices+'/'+s.totalDevices+'</td><td>'+s.requestCount+'</td><td>'+(s.totalParts||0)+'</td><td><a class=\"btn-sm\" href=\"http://localhost:'+s.port+'/admin\" target=\"_blank\">管理</a> <button class=\"btn-sm-danger\" onclick=\"stopAddr('+s.dbAddressId+')\">停止</button></td></tr>';");
sb.AppendLine(" });");
sb.AppendLine(" // 更新零件数");
sb.AppendLine(" data.forEach(function(s){ var el=document.getElementById('parts_'+s.dbAddressId); if(el) el.textContent=s.totalParts||0; });");
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("function startAddr(dbId){var intEl=document.getElementById('int_'+dbId);var interval=intEl?parseInt(intEl.value):10;fetch('/admin/api/start-address',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dbAddressId:dbId,interval:interval})}).then(r=>r.json()).then(function(){refresh();});}");
sb.AppendLine("function stopAddr(dbId){fetch('/admin/api/stop-address',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dbAddressId:dbId})}).then(function(){refresh();});}");
sb.AppendLine("function startAll(){fetch('/admin/api/start-all',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function stopAll(){fetch('/admin/api/stop-all',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("function reloadDb(){fetch('/admin/api/reload-db',{method:'POST'}).then(function(){refresh();});}");
sb.AppendLine("setInterval(refresh,2000);refresh();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
@ -76,15 +102,14 @@ namespace CncSimulator.Admin
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("<!DOCTYPE html><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 h1 { font-size:18px; }");
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; }");
@ -93,26 +118,31 @@ namespace CncSimulator.Admin
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(".settings-row input,.settings-row select { padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; }");
sb.AppendLine(".btn-apply { padding:4px 12px; border:1px solid #1890ff; border-radius:4px; background:#fff; color:#1890ff; cursor:pointer; }");
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(".device-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,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 h3 { font-size:14px; margin-bottom:6px; }");
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(".device-card .actions button.btn-remove { border-color:#ff4d4f; color:#ff4d4f; }");
sb.AppendLine(".parts-table { width:100%; border-collapse:collapse; font-size:13px; }");
sb.AppendLine(".parts-table th,.parts-table td { padding:6px 8px; text-align:left; border-bottom:1px solid #f0f0f0; }");
sb.AppendLine(".parts-table th { background:#fafafa; color:#666; }");
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(".log-table th { background:#fafafa; color:#666; }");
sb.AppendLine(".add-device-row { display:flex; gap:8px; align-items:center; margin-top:12px; }");
sb.AppendLine(".add-device-row input { padding:4px 8px; border:1px solid #d9d9d9; border-radius:4px; font-size:13px; }");
sb.AppendLine(".add-device-row button { padding:4px 12px; border:1px solid #52c41a; border-radius:4px; background:#52c41a; color:#fff; font-size:13px; cursor:pointer; }");
sb.AppendLine("</style></head><body>");
sb.AppendLine("<div class='header'>");
sb.AppendLine(" <h1>" + server.Name + " (端口 " + server.Port + ")</h1>");
@ -123,6 +153,20 @@ namespace CncSimulator.Admin
sb.AppendLine(" </div>");
sb.AppendLine("</div>");
sb.AppendLine("<div class='container'>");
// 统计概览
sb.AppendLine(" <div class='section'><h2>统计概览</h2>");
sb.AppendLine(" <div class='stats-grid' id='statsGrid'>加载中...</div>");
sb.AppendLine(" </div>");
// 零件统计表(按设备+NC程序
sb.AppendLine(" <div class='section'><h2>零件统计(按设备+NC程序</h2>");
sb.AppendLine(" <table class='parts-table'>");
sb.AppendLine(" <thead><tr><th>设备编码</th><th>描述</th><th>NC程序</th><th>零件数</th><th>当前零件</th></tr></thead>");
sb.AppendLine(" <tbody id='partsTable'></tbody>");
sb.AppendLine(" </table>");
sb.AppendLine(" </div>");
// 全局设置
sb.AppendLine(" <div class='section'><h2>全局设置</h2>");
sb.AppendLine(" <div class='settings-row'>");
@ -138,40 +182,65 @@ namespace CncSimulator.Admin
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(" <option value='normal'>正常</option><option value='http500'>HTTP 500</option>");
sb.AppendLine(" <option value='timeout'>连接超时</option><option value='refuse'>拒绝连接</option>");
sb.AppendLine(" <option value='empty'>返回空数组</option><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='section'><h2>设备状态</h2>");
sb.AppendLine(" <div class='device-cards' id='deviceCards'>加载中...</div>");
sb.AppendLine(" <div class='add-device-row'>");
sb.AppendLine(" <input id='addDeviceCode' placeholder='设备编码(如CNC-NEW)'>");
sb.AppendLine(" <input id='addDeviceDesc' placeholder='设备描述'>");
sb.AppendLine(" <button onclick='addDevice()'>添加设备</button>");
sb.AppendLine(" </div>");
sb.AppendLine(" </div>");
// JSON预览
sb.AppendLine(" <div class='section'><h2>当前返回JSON预览</h2>");
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(" <div class='section'><h2>日志</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(" <thead><tr><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 scenarioNames={'machining':'正常加工','same_part':'同一零件','idle':'待机','program_change':'换零件','manual_reset':'手动清零','power_off':'断电','power_on':'恢复开机','pause':'暂停'};");
sb.AppendLine("var runNames={0:'待机',1:'运行',3:'加工中'};");
// 刷新统计
sb.AppendLine("function refreshStats(){");
sb.AppendLine(" fetch('/admin/api/stats').then(r=>r.json()).then(st=>{");
sb.AppendLine(" document.getElementById('statsGrid').innerHTML=");
sb.AppendLine(" '<div class=\"stat-item\"><div class=\"value\">'+st.totalDevices+'</div><div class=\"label\">总设备数</div></div>';");
sb.AppendLine(" var sg=document.getElementById('statsGrid');");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+st.onlineDevices+'</div><div class=\"label\">在线设备</div></div>';");
sb.AppendLine(" sg.innerHTML+='<div class=\"stat-item\"><div class=\"value\">'+st.totalParts+'</div><div class=\"label\">总加工零件</div></div>';");
// 零件统计表
sb.AppendLine(" var pt=document.getElementById('partsTable');pt.innerHTML='';");
sb.AppendLine(" var devs=st.partsByDevice;");
sb.AppendLine(" Object.keys(devs).forEach(function(code){");
sb.AppendLine(" var d=devs[code];");
sb.AppendLine(" var progs=d.programs||{};");
sb.AppendLine(" Object.keys(progs).forEach(function(prog){");
sb.AppendLine(" pt.innerHTML+='<tr><td>'+code+'</td><td>'+d.desc+'</td><td>'+prog+'</td><td>'+progs[prog]+'</td><td>'+(prog===d.currentProgram?d.currentPartCount:'-')+'</td></tr>';");
sb.AppendLine(" });");
sb.AppendLine(" });");
sb.AppendLine(" }).catch(function(){});");
sb.AppendLine("}");
// 刷新设备卡片
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='';");
@ -181,92 +250,40 @@ namespace CncSimulator.Admin
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+='场景:'+scName+' ('+d.scenarioTick+'/'+d.scenarioDuration+') 程序:'+d.programName+' 零件:'+d.partCount+' 状态:'+runNames[d.runStatus];");
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=\"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+='<button class=\"btn-remove\" onclick=\"removeDevice(\\''+d.deviceCode+'\\')\">移除</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(" document.querySelectorAll('.device-card .actions button[data-event]').forEach(function(btn){");
sb.AppendLine(" btn.onclick=function(){var dev=this.parentElement.getAttribute('data-device');var evt=this.getAttribute('data-event');triggerEvent(dev,evt);};");
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 refreshJson(){fetch('/data').then(r=>r.text()).then(json=>{try{document.getElementById('jsonPreview').textContent=JSON.stringify(JSON.parse(json),null,2);}catch(e){document.getElementById('jsonPreview').textContent=json;}}).catch(function(e){document.getElementById('jsonPreview').textContent='请求失败:'+e.message;});}");
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 refreshLogs(){fetch('/admin/api/logs').then(r=>r.json()).then(logs=>{var tb=document.getElementById('logTable');tb.innerHTML='';logs.forEach(function(l){tb.innerHTML+='<tr><td>'+l.index+'</td><td>'+l.timestamp+'</td><td>'+l.deviceCount+'</td><td>'+l.keyData+'</td><td>'+l.duration+'ms</td></tr>';});}).catch(function(){});}");
sb.AppendLine("function triggerEvent(dev,evt){fetch('/admin/api/event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceId:dev,eventType:evt})}).then(function(){setTimeout(function(){refresh();refreshStats();},500);});}");
sb.AppendLine("function addDevice(){var code=document.getElementById('addDeviceCode').value;var desc=document.getElementById('addDeviceDesc').value;if(!code)return;fetch('/admin/api/add-device',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceCode:code,desc:desc})}).then(function(){refresh();refreshStats();});}");
sb.AppendLine("function removeDevice(code){if(!confirm('确定移除设备'+code+''))return;fetch('/admin/api/remove-device',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({deviceCode:code})}).then(function(){refresh();refreshStats();});}");
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 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(){});}");
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("document.querySelectorAll('input[name=mode]').forEach(function(r){r.addEventListener('change',function(){fetch('/admin/api/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:this.value})}).then(function(){});});});");
sb.AppendLine("setInterval(function(){refresh();refreshJson();refreshLogs();},2000);");
sb.AppendLine("refresh();refreshJson();refreshLogs();");
sb.AppendLine("setInterval(function(){refresh();refreshStats();refreshLogs();refreshJson();},2000);");
sb.AppendLine("refresh();refreshStats();refreshLogs();refreshJson();");
sb.AppendLine("</script></body></html>");
return sb.ToString();
}

@ -17,6 +17,7 @@
<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" />
<PackageReference Include="MySqlConnector" Version="2.3.7" />
</ItemGroup>
<ItemGroup>

@ -10,7 +10,15 @@ namespace CncSimulator.Config
[JsonProperty("gatewayPort")]
public int GatewayPort { get; set; } = 9000;
/// <summary>采集地址列表</summary>
/// <summary>数据库连接字符串</summary>
[JsonProperty("dbConnection")]
public string DbConnection { get; set; } = "Server=localhost;Database=cnc_business;Uid=root;Pwd=root;Charset=utf8mb4;SslMode=None;";
/// <summary>数据变化频率(秒)- 全局默认值</summary>
[JsonProperty("defaultDataChangeInterval")]
public int DefaultDataChangeInterval { get; set; } = 10;
/// <summary>采集地址列表(运行时动态创建)</summary>
[JsonProperty("addresses")]
public List<AddressConfig> Addresses { get; set; } = new List<AddressConfig>();
}
@ -18,6 +26,10 @@ namespace CncSimulator.Config
/// <summary>单个采集地址配置</summary>
public class AddressConfig
{
/// <summary>数据库中的采集地址ID</summary>
[JsonProperty("dbAddressId")]
public int DbAddressId { get; set; }
/// <summary>显示名称</summary>
[JsonProperty("name")]
public string Name { get; set; }

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using MySqlConnector;
using CncSimulator.Config;
namespace CncSimulator.Core
{
/// <summary>
/// 数据库读取器。从MariaDB读取采集地址和机床信息。
/// </summary>
public class DatabaseReader
{
private readonly string _connectionString;
public DatabaseReader(string connectionString)
{
_connectionString = connectionString;
}
/// <summary>测试数据库连接</summary>
public bool TestConnection(out string error)
{
error = "";
try
{
using (var conn = new MySqlConnection(_connectionString))
{
conn.Open();
return true;
}
}
catch (Exception ex)
{
error = ex.Message;
return false;
}
}
/// <summary>读取所有采集地址及其机床</summary>
public List<AddressInfo> ReadAddresses()
{
var addresses = new List<AddressInfo>();
using (var conn = new MySqlConnection(_connectionString))
{
conn.Open();
// 读取采集地址
string addrSql = "SELECT id, name, url FROM cnc_collect_address ORDER BY id";
using (var cmd = new MySqlCommand(addrSql, conn))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
addresses.Add(new AddressInfo
{
DbId = reader.GetInt32("id"),
Name = reader.GetString("name"),
Url = reader.IsDBNull(reader.GetOrdinal("url")) ? "" : reader.GetString("url"),
Machines = new List<MachineInfo>()
});
}
}
// 为每个地址读取机床
foreach (var addr in addresses)
{
string machineSql = "SELECT id, device_code, name FROM cnc_machine WHERE collect_address_id = @addrId ORDER BY id";
using (var cmd = new MySqlCommand(machineSql, conn))
{
cmd.Parameters.AddWithValue("@addrId", addr.DbId);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
addr.Machines.Add(new MachineInfo
{
Id = reader.GetInt32("id"),
DeviceCode = reader.GetString("device_code"),
Name = reader.IsDBNull(reader.GetOrdinal("name")) ? "" : reader.GetString("name")
});
}
}
}
}
}
return addresses;
}
}
/// <summary>采集地址信息(从数据库读取)</summary>
public class AddressInfo
{
public int DbId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<MachineInfo> Machines { get; set; }
}
/// <summary>机床信息(从数据库读取)</summary>
public class MachineInfo
{
public int Id { get; set; }
public string DeviceCode { get; set; }
public string Name { get; set; }
}
}

@ -1,17 +1,16 @@
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实例和总管理页面。
/// 引擎主控。启动时只开启9000管理页面从数据库读取地址信息
/// 用户手动启动各端口模拟。
/// </summary>
public class SimulatorEngine
{
@ -19,48 +18,120 @@ namespace CncSimulator.Core
private SimulatorConfig _config;
private HttpListener _gatewayListener;
private readonly AdminHandler _adminHandler = new AdminHandler();
private bool _running;
private DatabaseReader _dbReader;
private List<AddressInfo> _dbAddresses = new List<AddressInfo>();
private int _nextPort;
/// <summary>所有地址服务</summary>
/// <summary>所有已创建的地址服务</summary>
public List<SimulatorServer> Servers => _servers;
/// <summary>加载配置并创建所有SimulatorServer</summary>
/// <summary>数据库中的采集地址列表</summary>
public List<AddressInfo> DbAddresses => _dbAddresses;
/// <summary>配置</summary>
public SimulatorConfig Config => _config;
/// <summary>加载数据库配置</summary>
public void LoadConfig(SimulatorConfig config)
{
_config = config;
_servers.Clear();
_nextPort = config.GatewayPort + 1;
}
foreach (var addr in config.Addresses)
/// <summary>连接数据库并读取采集地址</summary>
public bool LoadFromDatabase(out string error)
{
_dbReader = new DatabaseReader(_config.DbConnection);
if (!_dbReader.TestConnection(out error))
{
var server = new SimulatorServer(addr);
_servers.Add(server);
return false;
}
_dbAddresses = _dbReader.ReadAddresses();
return true;
}
/// <summary>启动所有服务</summary>
public void StartAll()
/// <summary>只启动总管理页面(不启动模拟端口)</summary>
public void StartGateway()
{
_running = true;
StartGatewayListener();
}
// 启动所有地址服务
foreach (var server in _servers)
/// <summary>为指定采集地址创建并启动模拟端口</summary>
public SimulatorServer StartAddress(int dbAddressId, int? port = null, int? interval = null)
{
// 查找数据库中的地址信息
AddressInfo addrInfo = null;
foreach (var a in _dbAddresses)
{
server.Start();
if (a.DbId == dbAddressId) { addrInfo = a; break; }
}
if (addrInfo == null) return null;
// 启动总管理页面
StartGateway();
// 检查是否已存在
foreach (var s in _servers)
{
if (s.Config.DbAddressId == dbAddressId) return s;
}
// 分配端口
int usePort = port ?? _nextPort;
if (usePort <= _config.GatewayPort) usePort = _config.GatewayPort + 1;
_nextPort = usePort + 1;
// 创建配置
var addrConfig = new AddressConfig
{
DbAddressId = dbAddressId,
Name = addrInfo.Name + "模拟",
Port = usePort,
Brand = "fanuc",
DataChangeInterval = interval ?? _config.DefaultDataChangeInterval,
ScenarioMode = "auto"
};
// 为每台机床创建设备配置
foreach (var m in addrInfo.Machines)
{
addrConfig.Devices.Add(new DeviceConfig
{
DeviceCode = m.DeviceCode,
Desc = m.Name,
InitialProgram = "O0001",
InitialPartCount = 0
});
}
// 创建并启动服务
var server = new SimulatorServer(addrConfig);
server.Start(); // 启动HttpListener + Timer数据模拟
_servers.Add(server);
Console.WriteLine($" [✓] 启动模拟: {addrConfig.Name} → http://localhost:{usePort}/ ({addrInfo.Machines.Count}台设备)");
return server;
}
/// <summary>停止指定地址的模拟</summary>
public void StopAddress(int dbAddressId)
{
for (int i = _servers.Count - 1; i >= 0; i--)
{
if (_servers[i].Config.DbAddressId == dbAddressId)
{
_servers[i].Shutdown();
_servers.RemoveAt(i);
}
}
}
/// <summary>停止所有服务</summary>
public void StopAll()
{
_running = false;
foreach (var server in _servers)
{
server.Shutdown();
}
_servers.Clear();
try { _gatewayListener?.Stop(); } catch { }
}
@ -82,13 +153,16 @@ namespace CncSimulator.Core
{
arr.Add(new JObject
{
["dbAddressId"] = server.Config.DbAddressId,
["name"] = server.Name,
["port"] = server.Port,
["isRunning"] = server.IsRunning,
["totalDevices"] = server.TotalDeviceCount,
["onlineDevices"] = server.OnlineDeviceCount,
["requestCount"] = server.RequestCount,
["dataChangeInterval"] = server.Config.DataChangeInterval
["dataChangeInterval"] = server.Config.DataChangeInterval,
["totalParts"] = server.GetTotalParts(),
["partsByDevice"] = server.GetPartsByDeviceAndProgram()
});
}
return arr;
@ -96,7 +170,7 @@ namespace CncSimulator.Core
// ===== 总管理页面 =====
private void StartGateway()
private void StartGatewayListener()
{
try
{
@ -138,21 +212,87 @@ namespace CncSimulator.Core
string html = _adminHandler.BuildGatewayPage(this);
SendResponse(ctx, 200, html, "text/html; charset=utf-8");
}
else if (path == "/admin/api/db-addresses")
{
// 返回数据库中的采集地址列表
var arr = new JArray();
foreach (var a in _dbAddresses)
{
// 检查是否已在运行
bool running = false;
int runningPort = 0;
foreach (var s in _servers)
{
if (s.Config.DbAddressId == a.DbId)
{
running = true;
runningPort = s.Port;
break;
}
}
arr.Add(new JObject
{
["dbId"] = a.DbId,
["name"] = a.Name,
["url"] = a.Url,
["machineCount"] = a.Machines.Count,
["machines"] = JArray.FromObject(a.Machines),
["isRunning"] = running,
["runningPort"] = runningPort
});
}
SendResponse(ctx, 200, arr.ToString(), "application/json");
}
else if (path == "/admin/api/status")
{
var summary = GetStatusSummary();
SendResponse(ctx, 200, summary.ToString(), "application/json");
}
else if (path == "/admin/api/start")
else if (path == "/admin/api/start-address")
{
string body = ReadRequestBody(ctx);
var obj = JObject.Parse(body);
int dbId = obj["dbAddressId"]?.Value<int>() ?? 0;
int? port = obj["port"]?.Value<int>();
int? interval = obj["interval"]?.Value<int>();
var server = StartAddress(dbId, port, interval);
SendResponse(ctx, 200, server != null ? "{\"ok\":true,\"port\":" + server.Port + "}" : "{\"ok\":false}", "application/json");
}
else if (path == "/admin/api/stop-address")
{
string body = ReadRequestBody(ctx);
var obj = JObject.Parse(body);
int dbId = obj["dbAddressId"]?.Value<int>() ?? 0;
StopAddress(dbId);
SendResponse(ctx, 200, "{\"ok\":true}", "application/json");
}
else if (path == "/admin/api/start-all")
{
foreach (var s in _servers) if (!s.IsRunning) s.Start();
foreach (var a in _dbAddresses)
{
bool alreadyRunning = false;
foreach (var s in _servers)
{
if (s.Config.DbAddressId == a.DbId) { alreadyRunning = true; break; }
}
if (!alreadyRunning) StartAddress(a.DbId);
}
SendResponse(ctx, 200, "{\"ok\":true}", "application/json");
}
else if (path == "/admin/api/stop")
else if (path == "/admin/api/stop-all")
{
foreach (var s in _servers) s.Stop();
foreach (var server in _servers.ToArray())
{
server.Shutdown();
}
_servers.Clear();
SendResponse(ctx, 200, "{\"ok\":true}", "application/json");
}
else if (path == "/admin/api/reload-db")
{
_dbAddresses = _dbReader.ReadAddresses();
SendResponse(ctx, 200, "{\"ok\":true,\"count\":" + _dbAddresses.Count + "}", "application/json");
}
else
{
SendResponse(ctx, 200, "CNC模拟采集服务网关。请访问 /admin 管理页面。", "text/plain");
@ -160,7 +300,15 @@ namespace CncSimulator.Core
}
catch (Exception ex)
{
try { SendResponse(ctx, 500, ex.Message, "text/plain"); } catch { }
try { SendResponse(ctx, 500, "{\"error\":\"" + ex.Message.Replace("\"", "'") + "\"}", "application/json"); } catch { }
}
}
private string ReadRequestBody(HttpListenerContext ctx)
{
using (var reader = new System.IO.StreamReader(ctx.Request.InputStream, Encoding.UTF8))
{
return reader.ReadToEnd();
}
}

@ -73,6 +73,74 @@ namespace CncSimulator.Core
/// <summary>日志记录器引用</summary>
public LogRecorder LogRecorder => _logRecorder;
/// <summary>获取本次启动后所有设备的总加工零件数</summary>
public int GetTotalParts()
{
int total = 0;
foreach (var d in _devices) total += d.State.TotalPartsSinceStart;
return total;
}
/// <summary>获取按设备+NC程序名统计的零件数</summary>
public JObject GetPartsByDeviceAndProgram()
{
var result = new JObject();
foreach (var dev in _devices)
{
var s = dev.State;
var programs = new JObject();
foreach (var kvp in s.PartsByProgram)
{
programs[kvp.Key] = kvp.Value;
}
result[s.DeviceCode] = new JObject
{
["desc"] = s.Desc,
["totalParts"] = s.TotalPartsSinceStart,
["currentProgram"] = s.ProgramName,
["currentPartCount"] = s.PartCount,
["programs"] = programs
};
}
return result;
}
/// <summary>添加一台模拟设备</summary>
public void AddDevice(string deviceCode, string desc)
{
var devCfg = new Config.DeviceConfig
{
DeviceCode = deviceCode,
Desc = desc,
InitialProgram = "O0001",
InitialPartCount = 0
};
var sim = new DeviceSimulator(devCfg, _config.DataChangeInterval);
_devices.Add(sim);
var player = new ScenarioPlayer(
sim,
_config.ScenarioMode == "auto",
Environment.TickCount + _devices.Count * 1000 + deviceCode.GetHashCode()
);
_players.Add(player);
}
/// <summary>移除一台模拟设备</summary>
public bool RemoveDevice(string deviceCode)
{
for (int i = 0; i < _devices.Count; i++)
{
if (_devices[i].State.DeviceCode == deviceCode)
{
_devices.RemoveAt(i);
_players.RemoveAt(i);
return true;
}
}
return false;
}
public SimulatorServer(AddressConfig config)
{
_config = config;
@ -483,6 +551,34 @@ namespace CncSimulator.Core
SendTextResponse(ctx, 200, logsArr.ToString(), "application/json");
break;
case "/admin/api/stats":
var stats = new JObject
{
["totalDevices"] = _devices.Count,
["onlineDevices"] = OnlineDeviceCount,
["totalParts"] = GetTotalParts(),
["partsByDevice"] = GetPartsByDeviceAndProgram()
};
SendTextResponse(ctx, 200, stats.ToString(), "application/json");
break;
case "/admin/api/add-device":
string addBody = ReadRequestBody(ctx);
var addObj = JObject.Parse(addBody);
AddDevice(
addObj["deviceCode"]?.ToString(),
addObj["desc"]?.ToString()
);
SendTextResponse(ctx, 200, "{\"ok\":true}", "application/json");
break;
case "/admin/api/remove-device":
string remBody = ReadRequestBody(ctx);
var remObj = JObject.Parse(remBody);
bool removed = RemoveDevice(remObj["deviceCode"]?.ToString());
SendTextResponse(ctx, 200, removed ? "{\"ok\":true}" : "{\"ok\":false}", "application/json");
break;
default:
SendJsonResponse(ctx, 404, "{\"error\":\"Unknown API\"}");
break;

@ -117,6 +117,11 @@ namespace CncSimulator.Device
{
case "machining":
_state.PartCount++;
_state.TotalPartsSinceStart++;
// 按NC程序名累计零件数
if (!_state.PartsByProgram.ContainsKey(_state.ProgramName))
_state.PartsByProgram[_state.ProgramName] = 0;
_state.PartsByProgram[_state.ProgramName]++;
_state.RunStatus = 3;
_state.DeviceStatus = 1;
_state.OperateMode = 1;

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace CncSimulator.Device
{
/// <summary>单台模拟设备的完整状态</summary>
@ -78,5 +80,12 @@ namespace CncSimulator.Device
// ===== 内部辅助 =====
/// <summary>数据变化间隔(秒)</summary>
public int DataChangeInterval { get; set; } = 10;
// ===== 统计信息 =====
/// <summary>本次启动后累计总加工零件数</summary>
public int TotalPartsSinceStart { get; set; } = 0;
/// <summary>按NC程序名统计的零件数程序名 → 零件数)</summary>
public Dictionary<string, int> PartsByProgram { get; set; } = new Dictionary<string, int>();
}
}

@ -47,14 +47,32 @@ namespace CncSimulator
Console.WriteLine($" - {addr.Name} (:{addr.Port}) {addr.Devices.Count}台设备");
}
// 创建并启动引擎
// 创建引擎
var engine = new SimulatorEngine();
engine.LoadConfig(config);
Console.WriteLine("\n启动服务...");
engine.StartAll();
// 从数据库读取采集地址和机床
Console.WriteLine("\n连接数据库...");
if (engine.LoadFromDatabase(out string dbError))
{
Console.WriteLine($" [✓] 数据库连接成功,读取到 {engine.DbAddresses.Count} 个采集地址");
foreach (var a in engine.DbAddresses)
{
Console.WriteLine($" - {a.Name} ({a.Machines.Count}台机床)");
}
}
else
{
Console.WriteLine($" [✗] 数据库连接失败: {dbError}");
Console.WriteLine(" 将以空配置启动,可在管理界面手动配置");
}
// 只启动管理页面(不自动启动模拟端口)
Console.WriteLine("\n启动管理页面...");
engine.StartGateway();
Console.WriteLine("\n按任意键退出...");
Console.WriteLine("\n请在浏览器中打开管理页面配置并启动模拟。");
Console.WriteLine("按任意键退出...");
Console.ReadKey();
engine.StopAll();

@ -1,53 +1,6 @@
{
"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
}
]
}
]
"dbConnection": "Server=localhost;Database=cnc_business;Uid=root;Pwd=root;Charset=utf8mb4;SslMode=None;",
"defaultDataChangeInterval": 10,
"addresses": []
}

Loading…
Cancel
Save