You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/src/CncSimulator/Core/SimulatorServer.cs

783 lines
28 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
/// <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;
_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>启动数据模拟定时器HttpListener由构造函数或Shutdown后由外部重建</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未运行则启动
if (_listener == null || !_listener.IsListening)
{
_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,
["totalPartCount"] = s.TotalPartCount,
["runStatus"] = s.RunStatus,
["operateMode"] = s.OperateMode,
["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));
}
else
{
// 离线设备生成带有quality=1和1970时间的tag
devices.Add(GenerateOfflineDevice(dev.State));
}
}
return devices.ToString(Formatting.None);
}
/// <summary>生成离线设备的JSON_io_status有效其余tag quality=1</summary>
private JObject GenerateOfflineDevice(DeviceState state)
{
var tags = new JArray();
DateTime now = DateTime.Now;
// _io_status 始终有效
tags.Add(new JObject
{
["id"] = "_io_status",
["desc"] = "设备状态",
["quality"] = "0",
["value"] = "0.00000",
["time"] = now.ToString("yyyy-MM-dd HH:mm:ss")
});
// 其余tag全部标记为无效quality=1, time=1970
string epoch = "1970-01-01 08:00:00";
var offlineTags = new[]
{
new { id = "Tag1", desc = "加工零件总数", value = "0.00000" },
new { id = "Tag5", desc = "执行的NC主程序名", value = "" },
new { id = "Tag6", desc = "执行的NC主程序号", value = "" },
new { id = "Tag7", desc = "当前加工程序内容", value = "" },
new { id = "Tag8", desc = "当前加工零件数", value = "0.00000" },
new { id = "Tag9", desc = "运行状态", value = "0.00000" },
new { id = "Tag22", desc = "开机时间", value = "0.00000" },
new { id = "Tag23", desc = "运行时间", value = "0.00000" },
new { id = "Tag24", desc = "切削时间", value = "0.00000" },
new { id = "Tag25", desc = "循环时间", value = "0.00000" },
new { id = "Tag26", desc = "加工状态", value = "" }
};
foreach (var t in offlineTags)
{
tags.Add(new JObject
{
["id"] = t.id,
["desc"] = t.desc,
["quality"] = "1",
["value"] = t.value,
["time"] = epoch
});
}
return new JObject
{
["device"] = state.DeviceCode,
["desc"] = state.Desc,
["tags"] = tags
};
}
/// <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++;
_logRecorder.RecordError("http500", "模拟HTTP 500错误", OnlineDeviceCount);
SendTextResponse(ctx, 500, "Internal Server Error (模拟)");
return;
case "timeout":
_failCount++;
_logRecorder.RecordError("timeout", "模拟超时响应(60秒延迟)", OnlineDeviceCount);
System.Threading.Thread.Sleep(60000);
SendTextResponse(ctx, 200, "delayed response");
return;
case "empty":
_successCount++;
_logRecorder.RecordError("empty", "模拟空数据返回([])", OnlineDeviceCount);
SendTextResponse(ctx, 200, "[]", "application/json");
return;
case "malformed":
_successCount++;
_logRecorder.RecordError("malformed", "模拟畸形JSON返回({broken)", OnlineDeviceCount);
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;
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;
case "/admin/api/event-history":
HandleEventHistoryApi(ctx);
break;
case "/admin/api/full-summary":
HandleFullSummaryApi(ctx);
break;
case "/admin/api/error-log":
HandleErrorLogApi(ctx);
break;
default:
SendJsonResponse(ctx, 404, "{\"error\":\"Unknown API\"}");
break;
}
}
// ===== 新增API处理方法 =====
/// <summary>事件历史API返回所有设备的事件变更记录</summary>
private void HandleEventHistoryApi(HttpListenerContext ctx)
{
var allEvents = new JArray();
foreach (var dev in _devices)
{
foreach (var evt in dev.State.EventHistory)
{
allEvents.Add(new JObject
{
["timestamp"] = evt.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
["deviceCode"] = evt.DeviceCode,
["eventType"] = evt.EventType,
["oldProgram"] = evt.OldProgram,
["newProgram"] = evt.NewProgram,
["partCountBefore"] = evt.PartCountBefore,
["partCountAfter"] = evt.PartCountAfter,
["detail"] = evt.Detail
});
}
}
SendTextResponse(ctx, 200, allEvents.ToString(), "application/json");
}
/// <summary>完整汇总导出API用于测试结束后数据对比</summary>
private void HandleFullSummaryApi(HttpListenerContext ctx)
{
var devices = new JArray();
foreach (var dev in _devices)
{
var s = dev.State;
var programs = new JObject();
foreach (var kvp in s.PartsByProgram)
{
programs[kvp.Key] = kvp.Value;
}
devices.Add(new JObject
{
["deviceCode"] = s.DeviceCode,
["desc"] = s.Desc,
["isOnline"] = s.IsOnline,
["currentProgram"] = s.ProgramName,
["currentPartCount"] = s.PartCount,
["totalParts"] = s.TotalPartsSinceStart,
["programs"] = programs,
["eventCount"] = s.EventHistory.Count,
["lastEvent"] = s.EventHistory.Count > 0
? new JObject
{
["type"] = s.EventHistory[s.EventHistory.Count - 1].EventType,
["time"] = s.EventHistory[s.EventHistory.Count - 1].Timestamp.ToString("yyyy-MM-dd HH:mm:ss")
}
: null
});
}
var summary = new JObject
{
["exportTime"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
["addressName"] = _config.Name,
["port"] = _config.Port,
["startTime"] = _startTime.ToString("yyyy-MM-dd HH:mm:ss"),
["uptime"] = (DateTime.Now - _startTime).ToString(@"hh\:mm\:ss"),
["totalDevices"] = _devices.Count,
["onlineDevices"] = OnlineDeviceCount,
["totalParts"] = GetTotalParts(),
["totalRequests"] = _requestCount,
["successRequests"] = _successCount,
["failRequests"] = _failCount,
["errorCount"] = _logRecorder.GetErrorCount(),
["devices"] = devices
};
SendTextResponse(ctx, 200, summary.ToString(), "application/json");
}
/// <summary>异常日志API返回所有异常记录</summary>
private void HandleErrorLogApi(HttpListenerContext ctx)
{
var errors = _logRecorder.GetErrors();
var arr = new JArray();
foreach (var err in errors)
{
arr.Add(new JObject
{
["timestamp"] = err.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
["errorType"] = err.ErrorType,
["description"] = err.Description,
["affectedDevices"] = err.AffectedDevices
});
}
SendTextResponse(ctx, 200, arr.ToString(), "application/json");
}
// ===== 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");
}
}
}