|
|
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>停止模拟(停止Timer,HttpListener继续运行以接受管理请求)</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");
|
|
|
}
|
|
|
}
|
|
|
}
|