");
@@ -138,40 +182,65 @@ namespace CncSimulator.Admin
sb.AppendLine("
");
sb.AppendLine(" ");
sb.AppendLine(" ");
sb.AppendLine("
");
sb.AppendLine("
设备状态卡片
");
+
+ // 设备状态卡片(含添加/移除)
+ sb.AppendLine("
");
+
// JSON预览
- sb.AppendLine("
当前返回JSON预览
");
+ sb.AppendLine("
当前返回JSON
");
sb.AppendLine("
");
sb.AppendLine("
加载中...
");
sb.AppendLine("
");
+
// 日志
- sb.AppendLine("
返回数据日志(最近100条)
");
+ sb.AppendLine("
日志
");
sb.AppendLine("
");
- sb.AppendLine(" | # | 时间 | 设备数 | 关键数据 | 耗时 | 操作 |
");
+ sb.AppendLine(" | # | 时间 | 设备数 | 关键数据 | 耗时 |
");
sb.AppendLine(" ");
sb.AppendLine("
");
sb.AppendLine("
");
- // 统计
- sb.AppendLine("
统计
");
- sb.AppendLine("
加载中...
");
- sb.AppendLine("
");
sb.AppendLine("
");
+
// JavaScript
sb.AppendLine("");
return sb.ToString();
}
diff --git a/src/CncSimulator/CncSimulator.csproj b/src/CncSimulator/CncSimulator.csproj
index 06b24d3..c9f1dd4 100644
--- a/src/CncSimulator/CncSimulator.csproj
+++ b/src/CncSimulator/CncSimulator.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/CncSimulator/Config/SimulatorConfig.cs b/src/CncSimulator/Config/SimulatorConfig.cs
index f85fbed..36b3549 100644
--- a/src/CncSimulator/Config/SimulatorConfig.cs
+++ b/src/CncSimulator/Config/SimulatorConfig.cs
@@ -10,7 +10,15 @@ namespace CncSimulator.Config
[JsonProperty("gatewayPort")]
public int GatewayPort { get; set; } = 9000;
- /// 采集地址列表
+ /// 数据库连接字符串
+ [JsonProperty("dbConnection")]
+ public string DbConnection { get; set; } = "Server=localhost;Database=cnc_business;Uid=root;Pwd=root;Charset=utf8mb4;SslMode=None;";
+
+ /// 数据变化频率(秒)- 全局默认值
+ [JsonProperty("defaultDataChangeInterval")]
+ public int DefaultDataChangeInterval { get; set; } = 10;
+
+ /// 采集地址列表(运行时动态创建)
[JsonProperty("addresses")]
public List Addresses { get; set; } = new List();
}
@@ -18,6 +26,10 @@ namespace CncSimulator.Config
/// 单个采集地址配置
public class AddressConfig
{
+ /// 数据库中的采集地址ID
+ [JsonProperty("dbAddressId")]
+ public int DbAddressId { get; set; }
+
/// 显示名称
[JsonProperty("name")]
public string Name { get; set; }
diff --git a/src/CncSimulator/Core/DatabaseReader.cs b/src/CncSimulator/Core/DatabaseReader.cs
new file mode 100644
index 0000000..de1833e
--- /dev/null
+++ b/src/CncSimulator/Core/DatabaseReader.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using MySqlConnector;
+using CncSimulator.Config;
+
+namespace CncSimulator.Core
+{
+ ///
+ /// 数据库读取器。从MariaDB读取采集地址和机床信息。
+ ///
+ public class DatabaseReader
+ {
+ private readonly string _connectionString;
+
+ public DatabaseReader(string connectionString)
+ {
+ _connectionString = connectionString;
+ }
+
+ /// 测试数据库连接
+ 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;
+ }
+ }
+
+ /// 读取所有采集地址及其机床
+ public List ReadAddresses()
+ {
+ var addresses = new List();
+
+ 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()
+ });
+ }
+ }
+
+ // 为每个地址读取机床
+ 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;
+ }
+ }
+
+ /// 采集地址信息(从数据库读取)
+ public class AddressInfo
+ {
+ public int DbId { get; set; }
+ public string Name { get; set; }
+ public string Url { get; set; }
+ public List Machines { get; set; }
+ }
+
+ /// 机床信息(从数据库读取)
+ public class MachineInfo
+ {
+ public int Id { get; set; }
+ public string DeviceCode { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/src/CncSimulator/Core/SimulatorEngine.cs b/src/CncSimulator/Core/SimulatorEngine.cs
index d7391d1..ca5c68e 100644
--- a/src/CncSimulator/Core/SimulatorEngine.cs
+++ b/src/CncSimulator/Core/SimulatorEngine.cs
@@ -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
{
///
- /// 引擎主控。管理多个SimulatorServer实例和总管理页面。
+ /// 引擎主控。启动时只开启9000管理页面,从数据库读取地址信息,
+ /// 用户手动启动各端口模拟。
///
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 _dbAddresses = new List();
+ private int _nextPort;
- /// 所有地址服务
+ /// 所有已创建的地址服务
public List Servers => _servers;
- /// 加载配置并创建所有SimulatorServer
+ /// 数据库中的采集地址列表
+ public List DbAddresses => _dbAddresses;
+
+ /// 配置
+ public SimulatorConfig Config => _config;
+
+ /// 加载数据库配置
public void LoadConfig(SimulatorConfig config)
{
_config = config;
- _servers.Clear();
+ _nextPort = config.GatewayPort + 1;
+ }
- foreach (var addr in config.Addresses)
+ /// 连接数据库并读取采集地址
+ 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;
}
- /// 启动所有服务
- public void StartAll()
+ /// 只启动总管理页面(不启动模拟端口)
+ public void StartGateway()
{
- _running = true;
+ StartGatewayListener();
+ }
- // 启动所有地址服务
- foreach (var server in _servers)
+ /// 为指定采集地址创建并启动模拟端口
+ 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;
+ }
+
+ /// 停止指定地址的模拟
+ 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);
+ }
+ }
}
/// 停止所有服务
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() ?? 0;
+ int? port = obj["port"]?.Value();
+ int? interval = obj["interval"]?.Value();
+ 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() ?? 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();
}
}
diff --git a/src/CncSimulator/Core/SimulatorServer.cs b/src/CncSimulator/Core/SimulatorServer.cs
index ef03b64..83ddeea 100644
--- a/src/CncSimulator/Core/SimulatorServer.cs
+++ b/src/CncSimulator/Core/SimulatorServer.cs
@@ -73,6 +73,74 @@ namespace CncSimulator.Core
/// 日志记录器引用
public LogRecorder LogRecorder => _logRecorder;
+ /// 获取本次启动后所有设备的总加工零件数
+ public int GetTotalParts()
+ {
+ int total = 0;
+ foreach (var d in _devices) total += d.State.TotalPartsSinceStart;
+ return total;
+ }
+
+ /// 获取按设备+NC程序名统计的零件数
+ 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;
+ }
+
+ /// 添加一台模拟设备
+ 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);
+ }
+
+ /// 移除一台模拟设备
+ 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;
diff --git a/src/CncSimulator/Device/DeviceSimulator.cs b/src/CncSimulator/Device/DeviceSimulator.cs
index d0fd5ce..14ee454 100644
--- a/src/CncSimulator/Device/DeviceSimulator.cs
+++ b/src/CncSimulator/Device/DeviceSimulator.cs
@@ -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;
diff --git a/src/CncSimulator/Device/DeviceState.cs b/src/CncSimulator/Device/DeviceState.cs
index 11ad840..713fc87 100644
--- a/src/CncSimulator/Device/DeviceState.cs
+++ b/src/CncSimulator/Device/DeviceState.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+
namespace CncSimulator.Device
{
/// 单台模拟设备的完整状态
@@ -78,5 +80,12 @@ namespace CncSimulator.Device
// ===== 内部辅助 =====
/// 数据变化间隔(秒)
public int DataChangeInterval { get; set; } = 10;
+
+ // ===== 统计信息 =====
+ /// 本次启动后累计总加工零件数
+ public int TotalPartsSinceStart { get; set; } = 0;
+
+ /// 按NC程序名统计的零件数(程序名 → 零件数)
+ public Dictionary PartsByProgram { get; set; } = new Dictionary();
}
}
diff --git a/src/CncSimulator/Program.cs b/src/CncSimulator/Program.cs
index 7cf7ca2..a677acc 100644
--- a/src/CncSimulator/Program.cs
+++ b/src/CncSimulator/Program.cs
@@ -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();
diff --git a/src/CncSimulator/simulator.json b/src/CncSimulator/simulator.json
index ad6963f..2ed5ac4 100644
--- a/src/CncSimulator/simulator.json
+++ b/src/CncSimulator/simulator.json
@@ -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": []
}