init commit

This commit is contained in:
zhaowei.huang
2026-06-14 11:19:15 +08:00
parent e79a3815a5
commit c8e540d272
174 changed files with 60830 additions and 39 deletions
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.CarTypes
{
internal class BasicCarFields
{
public float MagSlowSpeed = 0;
public float MagFullSpeed = 0;
}
internal class BasicSiteFields
{
public bool Shelf = false;
public float CarLength = -1;
public float CarWidth = -1;
public float CarCenterX = 0;
public float CarCenterY = 0;
public int tag = -1;
public int TagValue = -1;//磁导航,二维码值,或者rfid 值
}
internal class BasicTrackFields
{
public int IOArea = -1;
public int LidarArea = -2;
public float BiasAlarmThresh = -1;
public float DthAlarmThresh = -1;
public float Speed = 0.2f;
public bool Reverse = false;
public int ReverseDst = -1;
public bool SwitchBarrier = false;
public bool CalibrateWheelEncoder = false;
public float CarDirectionBias = 0;
public bool EnableCarAbsoluteDirection = false;
public float CarAbsoluteDirection = 0;
public float SlowDistance = -1;
public float StopDistance = -1;
}
internal class BasicPlanFields
{
public string action = "/";
public float CarLength = -1;
public float CarWidth = -1;
}
}
+740
View File
@@ -0,0 +1,740 @@
using AMRScene1;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleCore;
using SimpleCore.BasicProps;
using SimpleCore.Compiler;
using SimpleCore.Extras;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using StandardScene.CarTypes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.ComTypes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AMRScene1
{
class DummyCarTrackField
{
public float Speed = -1;
public bool Reverse = false;
public int ReverseDst = -1;
}
class DummyCarSiteField
{
public bool Shelf = false;
}
class DummyCarPlanField
{
public string action = "/";
}
[TemplateTrackCoderSettings(
priority = 0,
templateString = "agv.Go(${src.x},${src.y},${src.id},${dst.x},${dst.y},${dst.id},${track.id}," +
"${track.Speed},${track.Reverse || track.ReverseDst == dst.id},${track.typeInfo});",
trackFields = typeof(DummyCarTrackField),
siteFields = typeof(DummyCarSiteField))]
[TemplateTrackCoderSettings(
priority = 5,
useVerb = "plan.action=='put'&& dst.Shelf ",
templateString = "agv.Put(${src.x},${src.y},${src.id},${dst.x},${dst.y},${dst.id},${track.id}," +
"${track.Speed},${track.Reverse || track.ReverseDst == dst.id},${track.typeInfo});",
blockVerb = "true",
trackFields = typeof(DummyCarTrackField),
siteFields = typeof(DummyCarSiteField),
planFields = typeof(DummyCarPlanField))]
[TemplateTrackCoderSettings(
priority = 5,
useVerb = "plan.action=='fetch'&& dst.Shelf ",
templateString = "agv.Fetch(${src.x},${src.y},${src.id},${dst.x},${dst.y},${dst.id},${track.id}," +
"${track.Speed},${track.Reverse || track.ReverseDst == dst.id},${track.typeInfo});",
blockVerb = "true",
trackFields = typeof(DummyCarTrackField),
siteFields = typeof(DummyCarSiteField),
planFields = typeof(DummyCarPlanField))]
[CarType(Name = "模拟车-包络")]
[EnvelopConfig(centerX = 0, centerY = 0, lengthX = 1000, lengthY = 700)]
public class DummyCar:Car
{
[FieldMember] public string conf = "realistic";
public class RotateSiteEnvelope : SiteEnvelopeDefinition<DummyCar>
{
public override bool Use() => true;
public override void Prompt()
{
if (curSeg == 0|| plan.codeArr==null)
{
return;
}
var code = plan.codeArr[curSeg - 1];
if (code.Contains("Fetch"))
{
reshape(1550, 1150, 0, 0);
}
else if (code.Contains("Put"))
{
reshape(1000,700, 0, 0);
}
}
public override bool Block() => false;
public override int priority => -1;
}
public virtual AGV getAGV(int id) => new AGV(id);
public class DummyCarStatus : CarStatus
{
public string simStat = "/";
public double simulatedDistance = 0;
}
public override CarStatus status { get; set; } = new DummyCarStatus();
public static async Task<DummyCar> Create() // boilerplate
{
return new()
{
lstatus = "正常",
name = $"模拟车",
haveCoordination = true
};
}
public override void rightClickAction(float mouseX, float mouseY)
{
x = mouseX;
y = mouseY;
}
public class AGV: AGVInterface
{
public DummyCar car;
public AGV(int id)
{
car = (DummyCar) SimpleLib.GetCar(id);
}
public void Sleep(int millis)
{
Thread.Sleep(millis);
}
protected class Segment
{
public int trackID, srcID, dstID;
public double srcX, srcY, dstX, dstY;
}
protected List<Segment> routeCache = []; // use circular list.
protected virtual Task following(double dstX, double dstY, double speed, bool reverse, params float[] typeInfo)
{
var promise = new TaskCompletionSource<int>();
int i = 0;
IEnumerable<bool> iterActions()
{
if (typeInfo.Length == 0 || (int)typeInfo[0] == 0) // line path
{
// car always run to completion.
while ((Math.Abs(car.x - dstX) > 10 || Math.Abs(car.y - dstY) > 10))// && car.running)
{
if (car.fields.TryGetValue("speed", out var sv))
speed = float.Parse(sv);
var dx = dstX - car.x;
var dy = dstY - car.y;
var d = Math.Sqrt(dx * dx + dy * dy);
var ed = (DateTime.Now - car.lastRefresh).TotalSeconds * speed;
if (ed > d)
{
car.th = (float)(Math.Atan2(dstY - car.y, dstX - car.x) / Math.PI * 180 + (reverse ? 180 : 0));
car.x = (float)dstX;
car.y = (float)dstY;
break;
}
dx = (float)(dx / d * ed);
dy = (float)(dy / d * ed);
car.th = (float)(Math.Atan2(dstY - car.y, dstX - car.x) / Math.PI * 180 + (reverse ? 180 : 0));
car.x += (float)dx;
car.y += (float)dy;
((DummyCarStatus)car.status).simStat = $"following-iter-{i++}";
//Console.WriteLine($"move one frame:{car.x},{car.y},{car.speed}");
yield return true;
}
}
else if ((int)typeInfo[0] == 1) // circularArc path
{
var center = new Vector2(typeInfo[1], typeInfo[2]);
var radius = typeInfo[3];
var angleStart = typeInfo[4];
var angleEnd = typeInfo[5];
var dstAngle = (float)(Math.Atan2(dstY - center.Y, dstX - center.X) / Math.PI * 180);
var dir = 1; // counter-clockwise
if (Math.Abs(LessMath.thDiff(angleStart, dstAngle)) <
Math.Abs(LessMath.thDiff(angleEnd, dstAngle))) dir = -1;
while (true)//(car.running)
{
var pTh = (float)(Math.Atan2(car.y - center.Y, car.x - center.X) / Math.PI * 180);
var ed = (DateTime.Now - car.lastRefresh).TotalSeconds * speed;
var dRadius = Math.Abs(Vector2.Distance(center, new Vector2(car.x, car.y)) - radius);
if (dRadius > 10)
{
// first go to arc
var targetX = (float)(center.X + radius * Math.Cos(pTh / 180 * Math.PI));
var targetY = (float)(center.Y + radius * Math.Sin(pTh / 180 * Math.PI));
car.th = (float)(Math.Atan2(targetY - car.y, targetX - car.x) / Math.PI * 180 + (reverse ? 180 : 0));
if (ed > dRadius)
{
car.x = targetX;
car.y = targetY;
}
car.x += (float)((targetX - car.x) / dRadius * ed);
car.y += (float)((targetY - car.y) / dRadius * ed);
}
else
{
// then go along arc
if (Math.Abs(LessMath.thDiff(dstAngle, pTh)) < 0.01) break;
var eth = (float)(ed / radius / Math.PI * 180);
var dth = LessMath.thDiff(dstAngle, pTh);
if (Math.Abs(eth) > Math.Abs(dth))
{
car.th = dstAngle + 90 * dir + (reverse ? 180 : 0);
car.x = (float)(center.X + radius * Math.Cos(dstAngle / 180 * Math.PI));
car.y = (float)(center.Y + radius * Math.Sin(dstAngle / 180 * Math.PI));
break;
}
var newTh = pTh + eth * dir;
car.th = newTh + 90 * dir + (reverse ? 180 : 0);
car.x = (float)(center.X + radius * Math.Cos(newTh / 180 * Math.PI));
car.y = (float)(center.Y + radius * Math.Sin(newTh / 180 * Math.PI));
}
((DummyCarStatus)car.status).simStat = $"following-iter-{i++}";
yield return true;
}
}
((DummyCarStatus)car.status).simStat = $"following-done";
car.moveAction = null;
// Console.WriteLine($"** following to {dstX},{dstY} done");
promise.SetResult(1);
// Task.Run(()=>promise.SetResult(1));
}
car.moveAction = iterActions().GetEnumerator();
return promise.Task;
}
/// <summary>
/// 模拟车有TryLock的agv函数,进入函数需先调用AddRoute()
/// </summary>
/// <param name="srcid"></param>
/// <param name="dstid"></param>
/// <param name="trackid"></param>
public void AddRoute(int srcid, int dstid, int trackid)
{
var route_id = routeCache.Count;
lock (routeCache)
routeCache.Add(new Segment() { trackID = trackid, srcID = srcid, dstID = dstid });
if (car.route.Length == 0)
car.route = [srcid];
car.route = car.route.Append(dstid).ToArray();
}
private static ConcurrentDictionary<AbstractCar, Task> _taskmap = new();
public Task mvmtTsk
{
get
{
if (_taskmap.TryGetValue(car, out var tsk)) return tsk;
return _taskmap[car] = Task.CompletedTask;
}
set
{
_taskmap[car] = value;
}
}
// trackType: 0 line, 1 circularArc
public void Go(double srcX, double srcY, int srcid, double dstX, double dstY, int dstid, int trackid,
int speed = -1, bool reverse = false, params float[] trackTypeInfo)
{
// car.AppendDebug($"Go() track typeInfo:{string.Join(",", trackTypeInfo)}");
AddRoute(srcid, dstid, trackid);
// ReSharper disable once PossiblyMistakenUseOfParamsMethod
var promise = new TaskCompletionSource<int>();
Queue(async () =>
{
while (!TryLock(dstid))
await Task.Delay(100);
// Console.WriteLine($"** ready to go to {dstid}({dstX},{dstY})");
mvmtTsk = mvmtTsk.ContinueWith(async _ =>
{
//Console.WriteLine($"** begin to go to {dstid}({dstX},{dstY})");
await following(dstX, dstY, speed > 0 ? speed : car.speed, reverse, trackTypeInfo);
((DummyCarStatus)car.status).simulatedDistance += LessMath.dist(srcX, srcY, dstX, dstY);
promise.SetResult(1);
// Task.Run(() => promise.SetResult(1)); // following finished.
}).Unwrap();
//Console.WriteLine($"** issued to go to {dstid}({dstX},{dstY})");
}, async () =>
{
await promise.Task;
//Console.WriteLine($"** done go to {dstid}({dstX},{dstY})");
Leave(srcid);
});
}
public void Fetch(double srcX, double srcY, int srcid, double dstX, double dstY, int dstid, int trackid,
int speed = -1, bool reverse = false, params float[] trackTypeInfo)
{
// car.AppendDebug($"Go() track typeInfo:{string.Join(",", trackTypeInfo)}");
AddRoute(srcid, dstid, trackid);
// ReSharper disable once PossiblyMistakenUseOfParamsMethod
var promise = new TaskCompletionSource<int>();
Queue(async () =>
{
while (!TryLock(dstid))
await Task.Delay(100);
// Console.WriteLine($"** ready to go to {dstid}({dstX},{dstY})");
mvmtTsk = mvmtTsk.ContinueWith(async _ =>
{
//Console.WriteLine($"** begin to go to {dstid}({dstX},{dstY})");
await following(dstX, dstY, speed > 0 ? speed : car.speed, reverse, trackTypeInfo);
((DummyCarStatus)car.status).simulatedDistance += LessMath.dist(srcX, srcY, dstX, dstY);
promise.SetResult(1);
// Task.Run(() => promise.SetResult(1)); // following finished.
}).Unwrap();
//Console.WriteLine($"** issued to go to {dstid}({dstX},{dstY})");
}, async () =>
{
await promise.Task;
//Console.WriteLine($"** done go to {dstid}({dstX},{dstY})");
Leave(srcid);
});
}
public void Put(double srcX, double srcY, int srcid, double dstX, double dstY, int dstid, int trackid,
int speed = -1, bool reverse = false, params float[] trackTypeInfo)
{
// car.AppendDebug($"Go() track typeInfo:{string.Join(",", trackTypeInfo)}");
AddRoute(srcid, dstid, trackid);
// ReSharper disable once PossiblyMistakenUseOfParamsMethod
var promise = new TaskCompletionSource<int>();
Queue(async () =>
{
while (!TryLock(dstid))
await Task.Delay(100);
// Console.WriteLine($"** ready to go to {dstid}({dstX},{dstY})");
mvmtTsk = mvmtTsk.ContinueWith(async _ =>
{
//Console.WriteLine($"** begin to go to {dstid}({dstX},{dstY})");
await following(dstX, dstY, speed > 0 ? speed : car.speed, reverse, trackTypeInfo);
((DummyCarStatus)car.status).simulatedDistance += LessMath.dist(srcX, srcY, dstX, dstY);
promise.SetResult(1);
// Task.Run(() => promise.SetResult(1)); // following finished.
}).Unwrap();
//Console.WriteLine($"** issued to go to {dstid}({dstX},{dstY})");
}, async () =>
{
await promise.Task;
//Console.WriteLine($"** done go to {dstid}({dstX},{dstY})");
Leave(srcid);
});
}
public void Nop(int srcid, int dstid, int trackid)
{
var route_id = routeCache.Count;
lock (routeCache)
routeCache.Add(new Segment() { trackID = trackid, srcID = srcid, dstID = dstid });
if (car.route.Length == 0)
car.route = [srcid];
car.route = car.route.Append(dstid).ToArray();
Queue(async () =>
{
while (!TryLock(dstid))
await Task.Delay(5);
}, async () =>
{
Leave(srcid);
});
}
public override bool TryLock(int siteId)
{
if (car.status.holdingLocks.Last() == siteId) return true;
if (!car.status.usage.Get().scheduling)
throw new Exception("abandoned");
if (!car.status.usage.Get().scheduling)
throw new Exception("Stopped");
lock (routeCache)
return TrafficControl.TryLock(car, siteId);
}
public override void Leave(int siteID)
{
TrafficControl.Leave(car, siteID);
}
}
private bool running = false;
public override async Task actualSendScript(string script)
{
if (running)
throw new Exception($"dummy car {id} already running script");
running = true;
try
{
route = new int[0];
AppendDebug($"dummy car {id} use jint for simulation");
currentAgv = getAGV(id);
var tcs = new TaskCompletionSource<int>();
new Thread(() => {
try
{
SelfEvaluating(currentAgv, script);
tcs.SetResult(1);
}
catch (Exception ex)
{
tcs.SetException(ex);
}}
) { Name = $"eva_{name}({id}):{status.programs.now.name}" }.Start();
await tcs.Task;
await currentAgv.WaitAsync();
Console.WriteLine($"{name}({id}) self evaluating script completed");
}
catch (Exception ex)
{
AppendDebug($"simulation error:{ExceptionFormatter.FormatEx(ex)}");
Console.WriteLine($"* {id} evaluating script failed, ex:{ExceptionFormatter.FormatEx(ex)}");
running = false;
throw;
}
running = false;
}
[MethodMember(Name = "设定路线", Description = "从当前位置出发,不停地走路线")]
public async void Path()
{
Site starting = null;
float dist = float.MaxValue;
foreach (var site in SimpleLib.GetAllSites())
{
var d = LessMath.dist(site.x, site.y, x, y);
if (d < dist)
{
dist = (float)d;
starting = site;
}
}
var p = new Pen(Color.Red, 3);
var lineCap =
new AdjustableArrowCap(6, 6, true);
p.CustomEndCap = lineCap;
p.StartCap = LineCap.RoundAnchor;
var segments = new List<Prop>();
var painter = SimpleMonitor.getPainter("flat-goto");
painter.clear();
Dictionary<int, Action> triggers = new();
CarProgram program = null;
try
{
while (true)
{
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var dstSite = (UISite)SimpleLib.GetSite(pt.site);
var plan = new SegmentPlan() { usingCar = this };
plan.fields["allow_destination_on_route"] = "true";
if (segments.Count > 0)
plan.fields["useMustCanGo"] = "false";
plan.FindRoute((UISite)starting, dstSite,false);
segments = segments.Concat(plan.segments.Skip(segments.Count > 0 ? 1 : 0)).ToList();
triggers[dstSite.id]= () => { Console.WriteLine($"VDASegment end {dstSite.id} reached"); };
Console.WriteLine($"goto site {pt.site}");
starting = dstSite;
painter.clear();
for (int i = 0; i + 2 < segments.Count; i += 2)
{
var st = (UISite)segments[i];
var ed = (UISite)segments[i + 2];
painter.drawLine(p, st.x, st.y, ed.x, ed.y);
}
if (program == null) program = plan.Compile("walk", false);
else
program.Append(plan, (go) =>
{
Task.Run(() =>
{
MessageBox.Show("go?");
go();
});
});
}
}
catch (TaskCanceledException ex)
{
Console.WriteLine("end");
}
Console.WriteLine($"issue command");
_ = Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
painter.clear();
});
program.Forecast();
// plan.segments = segments;
// if (plan.segments.Count == 0)
// {
// MessageBox.Show("未生成路径!");
// return;
// }
//
// var program = plan.Compile("walk");
foreach (var kvp in triggers)
{
program.TriggerOnSite(kvp.Key, kvp.Value);
// program.AwaitOnSite(kvp.Key, go =>
// {
// kvp.Value();
//
// Task.Run(() =>
// {
// MessageBox.Show("go?");
// go();
// });
// });
}
Console.WriteLine(program.script);
G.pushStatus($"向AGV:{name}({id})下发行走任务");
var tsk = program.Queue();
_ = Task.Factory.StartNew(() =>
{
G.pushStatus($"AGV:{name}({id})开始执行任务");
tsk.Wait();
G.pushStatus($"AGV:{name}({id})执行任务完毕");
});
siteID = segments.Last().id;
}
[MethodMember(Name = "去某地",Description="从当前位置找一条路径去某站点")]
public void Goto()
{
async void goto_fun(){
if (GetLastSite() == -1)
{
G.pushStatus("Car not initialized to any site!");
return;
}
try
{
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
Console.WriteLine($"goto site {pt.site}");
Site site1 = SimpleLib.GetSite(GetLastSite());
var plan = new SegmentPlan() { usingCar = this };
plan.FindRoute(site1, SimpleLib.GetSite(pt.site), findLoop:false);
// var code = $"{plan.Code()};agv.Wait();";
// Console.WriteLine(code);
await plan.Compile($"goto_{pt.site}").Queue();
Console.WriteLine("Done");
}
catch (Exception ex)
{
Console.WriteLine(ExceptionFormatter.FormatEx(ex));
}
}
Task.Run(goto_fun);
}
[MethodMember(Name = "增加小车enums", Description = "双击添加标签")]
public void AddEnums()
{
if (InputBox.ShowDialog("请输入小车enums\" enums:value") != SimpleLite.DialogResult.OK) return;
string tag = InputBox.ResultValue;
if (tag.Contains(""))
{
MessageBox.Show("需要切换英文输入法输入:");
return;
}
if (!string.IsNullOrEmpty(tag) && tag.Contains(":"))
{
string[] tagValue = tag.Split(':');
this.status.enums[tagValue[0]] = tagValue[1];
}
}
[MethodMember(Name = "劫持", Description = "指定一个起点和终点,当小车到达起点后劫持小车至终点")]
public void Hijack()
{
async void hijiack_fun()
{
G.pushStatus("Select starting point");
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
G.pushStatus("Select ending point");
var pt2 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
if (GetLastSite() == -1)
{
G.pushStatus("Car not initialized to any site!");
return;
}
try
{
var plan = new SegmentPlan() { usingCar = this };
plan.FindRoute(SimpleLib.GetSite(pt1.site), SimpleLib.GetSite(pt2.site), findLoop: false);
await plan.Compile($"hijack", forecast:false).TryHijack().Queue();
Console.WriteLine("Done");
}
catch (Exception ex)
{
Console.WriteLine(ExceptionFormatter.FormatEx(ex));
}
}
Task.Run(hijiack_fun);
}
[MethodMember(Name = "设置位姿", Description = "拖拽以设置位姿")]
public void SetPosition()
{
SimpleMonitor.registerDownevent((sender, args) =>
{
x = SimpleMonitor.mouseX;
y = SimpleMonitor.mouseY;
},null, (sender, args) =>
{
th = (float)(Math.Atan2(SimpleMonitor.mouseY - y, SimpleMonitor.mouseX - x) / Math.PI * 180);
}, (sender, args) => SimpleMonitor.clearDownevent());
}
[MethodMember(Name = "走到指定位置并设置Escape", Description = "点一个位置,再点一个位置")]
public void GoEscaped()
{
async void goto_fun()
{
if (GetLastSite() == -1)
{
G.pushStatus("Car not initialized to any site!");
return;
}
try
{
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var ptesc = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
Console.WriteLine($"goto site {pt.site}, ptesc={ptesc.site}");
var planRoute = new SegmentPlan() { usingCar = this };
planRoute.fields["forbid_cross"] = "false";
planRoute.FindRoute(SimpleLib.GetSite(GetLastSite()), SimpleLib.GetSite(pt.site), findLoop: false);
var planEsc = new SegmentPlan() { usingCar = this };
planEsc.fields["forbid_cross"] = "false";
planEsc.FindRoute(SimpleLib.GetSite(pt.site), SimpleLib.GetSite(ptesc.site), findLoop: false);
await planRoute.Compile($"goto_{pt.site}", false).Forecast(planEsc).Queue();
}
catch (Exception ex)
{
Console.WriteLine(ExceptionFormatter.FormatEx(ex));
}
}
Task.Run(goto_fun);
}
[MethodMember(Name = "设置角度", Description = "指定模拟车的朝向角度")]
public void SetAngle()
{
if (InputBox.ShowDialog("输入角度", "模拟车设置角度", "0", InputBox.Buttons.OkCancel) == SimpleLite.DialogResult.Cancel) return;
if (!float.TryParse(InputBox.ResultValue, out var angle)) return;
th = angle;
}
protected override void draw(Graphics eGraphics)
{
eGraphics.FillRectangle(Brushes.Gray, -320, -240 , 640 , 480 );
eGraphics.DrawRectangle(Pens.White, -320, -240 , 640 , 480 );
eGraphics.DrawLine(Pens.White, 0, -240, 320, 0);
eGraphics.DrawLine(Pens.White, 0, 240 , 320 , 0);
}
public DateTime lastRefresh =DateTime.MinValue;
protected IEnumerator<bool> moveAction;
private AGV currentAgv;
public override void keepAlive()
{
if (lastRefresh == DateTime.MinValue)
{
lastRefresh = DateTime.Now;
return;
}
if (moveAction != null)
moveAction.MoveNext();
haveCoordination = true;
lastRefresh = DateTime.Now;
}
}
}
@@ -0,0 +1,15 @@
namespace StandardScene.CarTypes
{
/// <summary>
/// 脚本异常自恢复能力(opt-in)。
/// <para>背景:AbstractLoopMission 原以 <c>car is Kiva</c> 硬编码筛选参与「脚本 Error/Bad 状态
/// 自动下线 + 就地重置」的车型;车型按平台拆分为插件后,Core 不能反向依赖插件内的具体车型,
/// 改为由车型实现本接口声明该能力(当前仅 Kiva 实现,行为与拆分前一致)。</para>
/// </summary>
public interface IScriptErrorRecoverable
{
/// <summary>脚本异常下线后的就地重置(原 Kiva.newReset)。</summary>
/// <param name="resetSiteId">重置目标站点 id;0 表示按车辆当前位置自动找最近站点。</param>
void RecoverReset(int resetSiteId = 0);
}
}
+80
View File
@@ -0,0 +1,80 @@
namespace StandardScene.CarTypes
{
// Kiva 系字段袋。原内联于 Kiva.cs;车型按平台拆分(Kiva→MagneticArmCar→QrLidar)后,
// ArmCarXxxFields 仍继承这些类,故下沉基座供两侧插件共用(internal + InternalsVisibleTo)。
class KivaCarFields : BasicCarFields
{
}
class KivaSiteFields : BasicSiteFields
{
public int AngleTarget = 0;
public bool Turn = false;
public float FetchSpeed = 0;
public int FetchLidarArea = -2;
public int FetchIOArea = -1;
public bool FetchReverse = false;
public float FetchBlindMoveDist = 0;
public float FetchLiftDownTarget = -1;
public float FetchLiftUpTarget = -1;
public bool FetchUseQr = false;
public int FetchQrMode = -1;
public bool FetchIsUpQr = false;
public bool FetchUseDetector = false;
public int FetchDetector = 0;
public float FetchDetectWidth = -1;
public float FetchDetectDepth = -1;
public bool FetchLeaveSrcEarly = false;
public float FetchShieldObstacleDist = -1;
public float PutSpeed = 0;
public int PutLidarArea = -1;
public int PutIOArea = -1;
public bool PutReverse = false;
public bool PutSyncRotate = false;
public float PutBlindMoveDist = 0;
public float PutLiftDownTarget = -1;
public float PutLiftUpTarget = -1;
public bool PutUseQr = false;
public int PutQrMode = -1;
public bool PutIsUpQr = false;
public bool PutUseDetector = false;
public int PutDetector = 0;
public float PutDetectWidth = -1;
public float PutDetectDepth = -1;
public bool PutLeaveSrcEarly = false;
public float PutShieldObstacleDist = -1;
public float LeaveShelfSpeed = 0;
public int LeaveShelfLidarArea = -1;
public int LeaveShelfIOArea = -1;
public bool LeaveShelfReverse = false;
public bool LeaveShelfSyncRotate = false;
public float LeaveShelfBlindMoveDist = 0;
public float LeaveShelfLiftDownTarget = -1;
public float LeaveShelfRecoveryObstacleDist = -1;
}
class KivaTrackFields : BasicTrackFields
{
public int ManeuverDir = 0;
public int ForwardDst = 0;
public int ForwardObChooseDst = -2;
public float BlindMoveDist = 0;
public bool UseDetector = false;
public int DetectorMode = -1;
public float DetectWidth = -1;
public float DetectDepth = -1;
public bool LeaveSrcEarly = false;
public float ShieldObstacleDist = -1;
}
class KivaPlanFields : BasicPlanFields
{
public bool reverse = false;
public int level = 0;
}
}
@@ -0,0 +1,35 @@
namespace StandardScene.CarTypes
{
// MultiWheelLifter 系字段袋。原内联于 MultiWheelLifterCar.cs;车型按平台拆分
// MultiWheelLifterCar→MagneticMultiVehicleCar→QrLidar)后两侧共用,故下沉基座。
class MultiWheelLifterCarFields : BasicCarFields
{
}
class MultiWheelLifterSiteFields : BasicSiteFields
{
public bool ChangeAvoidanceParam = false;
public bool ClampClose = false;
public bool NeedRotate = false;
}
class MultiWheelLifterTrackFields : BasicTrackFields
{
public int SleepTime = 0;
public float TrayTarget = 0;
public int MagnetChoose = 0;
public bool MultiVehicleSync = false;
}
class MultiWheelLifterPlanFields : BasicPlanFields
{
public float AngleTarget = 0;
public float TireNum = 0;
public bool FrontLidarDetect = false;
public bool FirstTire = false;
public bool Reverse = false;
}
}
+46
View File
@@ -0,0 +1,46 @@
namespace StandardScene
{
partial class VehicleMonitor
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// VehicleMonitor
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1800, 900);
this.Name = "VehicleMonitor";
this.Text = "车辆状态监控系统";
this.ResumeLayout(false);
}
#endregion
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
using System;
using System.Linq;
using SimpleCore.Library;
namespace StandardScene.Chained
{
/// <summary>
/// 负责根据 Delivery 上记录的回调 key 列表,通过回调注册表统一挂载所有事件回调。
/// </summary>
public static class DeliveryCallbackAttacher
{
public static void AttachAll(ChainedDeliveryMission.Delivery d)
{
if (d == null) return;
// OnStart
foreach (var key in d.OnStartCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveOnStart(key);
if (cb != null) d.OnStart += cb;
else Diagnosis.Log($"Unknown OnStart callback key: {key}", "DeliveryCallbackAttacher");
}
// DoneFetch
foreach (var key in d.DoneFetchCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDoneFetch(key);
if (cb != null) d.DoneFetch += cb;
else Diagnosis.Log($"Unknown DoneFetch callback key: {key}", "DeliveryCallbackAttacher");
}
// DonePut
foreach (var key in d.DonePutCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDonePut(key);
if (cb != null) d.DonePut += cb;
else Diagnosis.Log($"Unknown DonePut callback key: {key}", "DeliveryCallbackAttacher");
}
// DoneMission
foreach (var key in d.DoneMissionCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveDoneMission(key);
if (cb != null) d.DoneMission += cb;
else Diagnosis.Log($"Unknown DoneMission callback key: {key}", "DeliveryCallbackAttacher");
}
// Failed
foreach (var key in d.FailedCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveFailed(key);
if (cb != null) d.Failed += cb;
else Diagnosis.Log($"Unknown Failed callback key: {key}", "DeliveryCallbackAttacher");
}
// OnTerminated
foreach (var key in d.OnTerminatedCallbackKeys.Distinct())
{
var cb = DeliveryCallbackRegistry.ResolveOnTerminated(key);
if (cb != null) d.OnTerminated += cb;
else Diagnosis.Log($"Unknown OnTerminated callback key: {key}", "DeliveryCallbackAttacher");
}
}
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StandardScene.Chained
{
/// <summary>
/// 可用于区分 Delivery 各生命周期事件的枚举。
/// </summary>
public enum DeliveryEventType
{
OnStart,
DoneFetch,
DonePut,
DoneMission,
Failed,
OnTerminated
}
/// <summary>
/// 全局任务回调注册表:通过 (事件类型, key) 注册/解析各阶段回调,便于持久化和恢复。
/// </summary>
public static class DeliveryCallbackRegistry
{
private static readonly Dictionary<(DeliveryEventType, string), Delegate> _callbacks = new();
private static void RegisterInternal(DeliveryEventType type, string key, Delegate callback)
{
if (string.IsNullOrWhiteSpace(key) || callback == null) return;
_callbacks[(type, key)] = callback;
}
private static T ResolveInternal<T>(DeliveryEventType type, string key) where T : class
{
if (key == null) return null;
return _callbacks.TryGetValue((type, key), out var d) ? d as T : null;
}
public static void RegisterOnStart(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.OnStart, key, callback);
public static void RegisterDoneFetch(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DoneFetch, key, callback);
public static void RegisterDonePut(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DonePut, key, callback);
public static void RegisterDoneMission(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.DoneMission, key, callback);
public static void RegisterFailed(string key, Action<ChainedDeliveryMission.Delivery> callback) =>
RegisterInternal(DeliveryEventType.Failed, key, callback);
public static void RegisterOnTerminated(string key, Func<ChainedDeliveryMission.Delivery, string, Task<int>> callback) =>
RegisterInternal(DeliveryEventType.OnTerminated, key, callback);
public static Action<ChainedDeliveryMission.Delivery> ResolveOnStart(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.OnStart, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDoneFetch(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DoneFetch, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDonePut(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DonePut, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveDoneMission(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.DoneMission, key);
public static Action<ChainedDeliveryMission.Delivery> ResolveFailed(string key) =>
ResolveInternal<Action<ChainedDeliveryMission.Delivery>>(DeliveryEventType.Failed, key);
public static Func<ChainedDeliveryMission.Delivery, string, Task<int>> ResolveOnTerminated(string key) =>
ResolveInternal<Func<ChainedDeliveryMission.Delivery, string, Task<int>>>(DeliveryEventType.OnTerminated, key);
}
}
@@ -0,0 +1,316 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using StandardScene.Model;
using SimpleLite;
using SimpleCore;
using SimpleCore.Library;
using StandardScene.Utils;
using static StandardScene.Chained.ChainedDeliveryMission;
namespace StandardScene.Chained
{
public partial class DeliveryViewer : Form
{
private const int OverdueMinutesThreshold = 10000; // 约7天视为超时
private const int DisplayColumnIndexOverdueFlag = 9;
private static readonly HttpClient SharedHttpClient = new HttpClient();
/// <summary>选中行的背景色</summary>
private static readonly Color SelectedRowBackColor = Color.FromArgb(220, 230, 250);
/// <summary>缓存选中行索引,避免在 RetrieveVirtualItem 中访问 SelectedIndices 引发递归</summary>
private readonly HashSet<int> _selectedIndicesCache = new HashSet<int>();
private ListViewItem _item = null;
public DeliveryViewer()
{
InitializeComponent();
}
private readonly ContextMenuStrip strip = new ContextMenuStrip();
private void DeliveryViewer_Load(object sender, EventArgs e)
{
strip.Items.Clear();
strip.Items.Add("取消任务", null, CancelClick);
strip.Items.Add("重发任务", null, ResendClick);
strip.Items.Add("换车重发任务", null, ChangeCarResendClick);
currentTaskList.ContextMenuStrip = strip;
}
private List<string[]> _listDeliveries = new List<string[]>();
/// <summary>将任务标记为已取消(Canceled)。</summary>
private static void MarkDeliveryCanceled(Delivery d)
{
if (d == null) return;
lock (d.SyncStatus)
{
d.Canceled = true;
d.Active = false;
}
}
/// <summary>
/// 将任务状态重置为 Waiting。
/// 当 clearCarForChange=true 时,仅当状态为 Suspended 或 Waiting 且未处于放货阶段时,
/// 才会清空 UsingCar 并返回 true;否则返回 false。
/// </summary>
private static bool MarkDeliveryWaiting(Delivery d, bool clearCarForChange)
{
if (d == null) return false;
lock (d.SyncStatus)
{
var status = d.GetStatus();
if (clearCarForChange)
{
if ((status is not DeliveryStatus.Suspended and not DeliveryStatus.Waiting) || d.Putting)
{
return false;
}
d.UsingCar = null;
}
else
{
d.SkipFetch = d.Putting;
}
d.Active = false;
d.Finished = false;
d.Error = false;
d.Canceled = false;
d.Terminated = false;
d.Suspended = false;
return true;
}
}
protected virtual string[] GetDisplayContent(Delivery dd)
{
var srcName =SimpleLib.GetSite(dd.Src).name;
var dstName =SimpleLib.GetSite(dd.Dst).name;
var now = DateTime.Now;
var usingCar = dd.UsingCar == null ? string.Empty : dd.UsingCar.name;
return
[
$"{dd.Id}",
$"{usingCar}",
$"{dd.Src}-{srcName}",
$"{dd.Dst}-{dstName}",
$"{dd.GetStatus()}",
$"{dd.CreateTime:yyyy-mm-dd HH:mm:ss:fff}",
$"{dd.StartTime:yyyy-mm-dd HH:mm:ss:fff}",
$"{dd.FinishTime:yyyy-mm-dd HH:mm:ss:fff}",
$"{dd.Priority}",
$"{((now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold ? 1 : 0)}",
$"{dd.Id}"
];
}
private void TaskFlush()
{
_listDeliveries.Clear();
try
{
foreach (var cdm in SimpleProject.proj.Missions.OfType<ChainedDeliveryMission>())
foreach (var dd in cdm.GetDeliveries(checkBox1.Checked, checkBox2.Checked,checkBox2.Checked,checkBox2.Checked))
_listDeliveries.Add(GetDisplayContent(dd));
if (_listDeliveries.Count > 0)
{
var len = _listDeliveries[0].Length;
if (len > 0) _listDeliveries = _listDeliveries.OrderByDescending(p => int.Parse(p[len - 2])).ToList();
}
}
catch (Exception ex)
{
Diagnosis.Post($"TaskFlush 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void timer1_Tick(object sender, EventArgs e)
{
try
{
TaskFlush();
currentTaskList.VirtualListSize = _listDeliveries.Count;
currentTaskList.Invalidate();
}
catch (Exception ex)
{
Diagnosis.Post($"timer1_Tick 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void currentTaskList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
try
{
var n = e.ItemIndex;
e.Item = new ListViewItem(_listDeliveries[n]);
if (_listDeliveries[n].Length > DisplayColumnIndexOverdueFlag && _listDeliveries[n][DisplayColumnIndexOverdueFlag] == "1")
e.Item.ForeColor = Color.Red;
if (_selectedIndicesCache.Contains(n))
e.Item.BackColor = SelectedRowBackColor;
}
catch (Exception)
{
e.Item = new ListViewItem(["", "", "", "", "", "", "", "", ""]);
}
}
private void currentTaskList_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Right) return;
_item = currentTaskList.GetItemAt(e.X, e.Y);
}
private void ResendClick(object sender, EventArgs e)
{
if (_item == null) return;
string taskCode = _item.Text;
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var ms = MessageBox.Show($"是否重发任务--{taskCode}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
if (ms != System.Windows.Forms.DialogResult.OK) return;
if (!MarkDeliveryWaiting(d, clearCarForChange: false))
{
MessageBox.Show("重发任务失败:当前状态不允许重发", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 状态已改为 Waiting,持久化
cdm.PersistDelivery(d);
}
catch (Exception)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void CancelClick(object sender, EventArgs e)
{
if (_item == null) return;
string str = _item.Text;
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == str);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var ms = MessageBox.Show($"是否结束任务--{str}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
if (ms != System.Windows.Forms.DialogResult.OK || d.IsFinished()) return;
// 1) 状态上将任务标记为已取消
MarkDeliveryCanceled(d);
// 2) 若小车当前正在执行该任务,则下发 reset 指令
bool excutingTask = d.UsingCar != null && d.UsingCar.tags.IsEqual("taskCode", d.TaskId);
if (excutingTask && d.UsingCar != null
&& (d.UsingCar.tags?.Contains("occupied") == true || (d.UsingCar.status?.pendingLocks?.Length ?? 0) != 0))
{
_ = SharedHttpClient.GetStringAsync($"http://{d.UsingCar.address}:8008/reset");
Diagnosis.Log($"手动结束任务;{d.TaskId}", "task", true);
}
// 3) 持久化已取消状态
cdm.PersistDelivery(d);
}
catch (Exception ex)
{
Diagnosis.Post($"结束任务 {str} 异常: {ExceptionFormatter.FormatEx(ex)}");
}
}
private void ChangeCarResendClick(object sender, EventArgs e)
{
if (_item == null) return;
string taskCode = _item.Text;
try
{
var cdm = SimpleProject.proj.Missions.OfType<TransportMission>().FirstOrDefault();
if (cdm == null) return;
var d = cdm.GetDeliveries(true, true, true, true)
.OfType<TransportDelivery>()
.FirstOrDefault(s => s.Id == taskCode);
if (d == null)
{
MessageBox.Show("列表中不存在目标任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var ms = MessageBox.Show($"是否换车重发任务--{taskCode}", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
if (ms != System.Windows.Forms.DialogResult.OK) return;
if (!MarkDeliveryWaiting(d, clearCarForChange: true))
{
MessageBox.Show("换车重发失败:仅当任务状态为 Suspended 或 Waiting 且未处于放货阶段时才允许换车重发", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 状态已改为 Waiting 且 UsingCar 已清空,持久化
cdm.PersistDelivery(d);
}
catch (Exception ex)
{
Diagnosis.Post($"换车重发任务 {taskCode} 异常: {ExceptionFormatter.FormatEx(ex)}");
MessageBox.Show("换车重发任务异常,请查看日志", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
private void DeliveryViewer_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
e.Cancel = true;
this.Visible = false;
timer1.Stop();
}
}
protected override void SetVisibleCore(bool value)
{
if (!IsHandleCreated && value)
CreateHandle();
bool wasVisible = Visible;
base.SetVisibleCore(value);
if (value && !wasVisible)
timer1.Start();
}
private void currentTaskList_SelectedIndexChanged(object sender, EventArgs e)
{
_selectedIndicesCache.Clear();
foreach (int i in currentTaskList.SelectedIndices)
_selectedIndicesCache.Add(i);
this.BeginInvoke(() => currentTaskList.Invalidate());
}
}
}
+206
View File
@@ -0,0 +1,206 @@
using System.Windows.Forms;
namespace StandardScene.Chained
{
partial class DeliveryViewer
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
if (disposing)
{
strip?.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.currentTaskList = new System.Windows.Forms.ListView();
this.columnHeader8 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader4 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader5 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader9 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader6 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader7 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader3 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.label2 = new System.Windows.Forms.Label();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this.checkBox1 = new System.Windows.Forms.CheckBox();
this.checkBox2 = new System.Windows.Forms.CheckBox();
this.SuspendLayout();
//
// currentTaskList
//
this.currentTaskList.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.currentTaskList.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader8,
this.columnHeader1,
this.columnHeader4,
this.columnHeader5,
this.columnHeader9,
this.columnHeader6,
this.columnHeader2,
this.columnHeader7,
this.columnHeader3});
this.currentTaskList.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.currentTaskList.FullRowSelect = true;
this.currentTaskList.GridLines = true;
this.currentTaskList.HideSelection = false;
this.currentTaskList.Location = new System.Drawing.Point(38, 62);
this.currentTaskList.Name = "currentTaskList";
this.currentTaskList.Size = new System.Drawing.Size(1146, 429);
this.currentTaskList.TabIndex = 2;
this.currentTaskList.UseCompatibleStateImageBehavior = false;
this.currentTaskList.View = System.Windows.Forms.View.Details;
this.currentTaskList.VirtualMode = true;
this.currentTaskList.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.currentTaskList_RetrieveVirtualItem);
this.currentTaskList.SelectedIndexChanged += new System.EventHandler(this.currentTaskList_SelectedIndexChanged);
this.currentTaskList.MouseClick += new System.Windows.Forms.MouseEventHandler(this.currentTaskList_MouseClick);
//
// columnHeader8
//
this.columnHeader8.Text = "任务号";
this.columnHeader8.Width = 130;
//
// columnHeader1
//
this.columnHeader1.Text = "小车";
this.columnHeader1.Width = 100;
//
// columnHeader4
//
this.columnHeader4.Text = "取货点";
this.columnHeader4.Width = 130;
//
// columnHeader5
//
this.columnHeader5.Text = "放货点";
this.columnHeader5.Width = 130;
//
// columnHeader9
//
this.columnHeader9.Text = "任务状态";
this.columnHeader9.Width = 100;
//
// columnHeader6
//
this.columnHeader6.Text = "下发时间";
this.columnHeader6.Width = 130;
//
// columnHeader2
//
this.columnHeader2.Text = "执行时间";
this.columnHeader2.Width = 130;
//
// columnHeader7
//
this.columnHeader7.Text = "结束时间";
this.columnHeader7.Width = 130;
//
// columnHeader3
//
this.columnHeader3.Text = "优先级";
this.columnHeader3.Width = 83;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("微软雅黑", 14.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.label2.Location = new System.Drawing.Point(33, 7);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(88, 26);
this.label2.TabIndex = 3;
this.label2.Text = "任务列表";
//
// timer1
//
this.timer1.Enabled = true;
this.timer1.Interval = 1000;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// checkBox1
//
this.checkBox1.AutoSize = true;
this.checkBox1.Checked = true;
this.checkBox1.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBox1.Location = new System.Drawing.Point(127, 15);
this.checkBox1.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
this.checkBox1.Name = "checkBox1";
this.checkBox1.Size = new System.Drawing.Size(108, 16);
this.checkBox1.TabIndex = 4;
this.checkBox1.Text = "显示已完成任务";
this.checkBox1.UseVisualStyleBackColor = true;
//
// checkBox2
//
this.checkBox2.AutoSize = true;
this.checkBox2.Checked = true;
this.checkBox2.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBox2.Location = new System.Drawing.Point(239, 14);
this.checkBox2.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2);
this.checkBox2.Name = "checkBox2";
this.checkBox2.Size = new System.Drawing.Size(318, 16);
this.checkBox2.TabIndex = 5;
this.checkBox2.Text = "显示废止的任务(包括Error、Canceled、Terminated";
this.checkBox2.UseVisualStyleBackColor = true;
//
// DeliveryViewer
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1199, 551);
this.Controls.Add(this.checkBox2);
this.Controls.Add(this.checkBox1);
this.Controls.Add(this.label2);
this.Controls.Add(this.currentTaskList);
this.Name = "DeliveryViewer";
this.Text = "DeliveryViewer";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DeliveryViewer_FormClosing);
this.Load += new System.EventHandler(this.DeliveryViewer_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ColumnHeader columnHeader4;
private System.Windows.Forms.ColumnHeader columnHeader5;
private System.Windows.Forms.ColumnHeader columnHeader6;
private System.Windows.Forms.ColumnHeader columnHeader7;
private System.Windows.Forms.ColumnHeader columnHeader8;
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.ColumnHeader columnHeader9;
private System.Windows.Forms.CheckBox checkBox1;
private System.Windows.Forms.CheckBox checkBox2;
public System.Windows.Forms.ListView currentTaskList;
private System.Windows.Forms.ColumnHeader columnHeader3;
}
}
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
@@ -0,0 +1,58 @@
using StandardScene.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static StandardScene.Chained.AbstractLoopMission;
namespace StandardScene.Chained.Loop
{
/// <summary>
/// 进入点规则:返回是否允许进入。
/// </summary>
public interface IEnterRule
{
bool CanEnter(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 离开点规则:返回是否允许离开。
/// </summary>
public interface IExitRule
{
bool CanExit(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 合流规则:从候选任务中选择一个
/// </summary>
public interface IJoinRule
{
LoopTask SelectJoin(IEnumerable<LoopTask> candidates, LoopPoint point);
}
/// <summary>
/// 分流规则:为任务选取下一分支标识
/// </summary>
public interface IBranchRule
{
string SelectBranch(LoopPoint point, LoopTask task, object context = null);
}
/// <summary>
/// 任务策略接口:提供任务数据来源与基础策略决策(由 LoopViewer 或其它组件实现/替换)。
/// </summary>
public interface ITaskStrategy
{
/// <summary>
/// 返回当前策略下的任务集合(策略负责数据来源,例如读取 LoopViewer 的 JSON)。
/// </summary>
IEnumerable<LoopTask> GetTasks();
/// <summary>
/// 刷新策略数据(例如重新加载 JSON)。
/// </summary>
void Refresh();
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Chained.Loop
{
/// <summary>
/// 触发器适配器统一接口:外部设备(PLC、按钮盒、API 网关等)实现此接口并触发事件。
/// </summary>
public interface ITriggerAdapter : IDisposable
{
/// <summary>
/// 外部触发事件:Source 用于区分来源("PLC","ButtonBox","API"等),Key/Value 为业务自定义负载。
/// </summary>
event EventHandler<TriggerEventArgs> TriggerRaised;
/// <summary>
/// 可选:启动适配器(开启轮询、建立连接等)。
/// </summary>
void Start();
/// <summary>
/// 可选:停止适配器。
/// </summary>
void Stop();
}
public class TriggerEventArgs : EventArgs
{
public string Source { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty;
public object Value { get; set; }
}
}
+163
View File
@@ -0,0 +1,163 @@
using IoTClient.Clients.PLC;
using IoTClient.Common.Enums;
using LessokajiWeaverUtilities.Utilities;
using Newtonsoft.Json;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore.Library;
using StandardScene.Chained;
using StandardScene.Model;
using System.Collections.Concurrent;
using System.Threading;
using static StandardScene.Chained.AbstractLoopMission;
namespace StandardScene
{
public class LoopMissionStatus : AbstractLoopMissionStatus
{
}
[MissionType(Name = "环线进程", editor = typeof(LoopMission))]
[I18N.DocumentTranslation(Name = "Loop Mission", locale = "en")]
public class LoopMission : AbstractLoopMission
{
[JsonIgnore] public override MissionStatus status { get; set; } = new LoopMissionStatus();
[JsonIgnore] public Thread myThread;
[JsonIgnore] public bool started = false;
[JsonIgnore] private readonly ConcurrentDictionary<int, byte> _buttonTriggeredSiteIds = new ConcurrentDictionary<int, byte>();
public float ChargesocStandard = 30;
public float NoChargesocStandard = 80;
/// <summary>
/// API 触发:检查站点物料状态
/// </summary>
protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car)
{
/* var site = SimpleLib.GetSite(currentSiteId);
if (site != null && site.fields.TryGetValue("hasMaterial", out var val) && val == "true")
{
// 物料存在,使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
return ExternalTriggerResult.Fail();*/
if (car.status.enums.TryGetValue("Soc", out var soc))
{
if (float.Parse(soc) <= ChargesocStandard)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
}
/* var plcTarget = 17;
if (plcTarget > 0)
{
// PLC 指定了目标站点
return ExternalTriggerResult.UseTarget(plcTarget);
}*/
return ExternalTriggerResult.Fail();
}
/// <summary>
/// PLC 触发:根据 PLC 信号决定目标
/// </summary>
protected override ExternalTriggerResult OnPlcTrigger(int currentSiteId, LoopTask task, Car car)
{
//var plcTarget = ReadPlcTargetSite(currentSiteId);
SiemensClient client = new SiemensClient(SiemensVersion.S7_200Smart, "127.0.0.1", 103);
var M100 = client.ReadBoolean("M100").Value;
if (M100)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
/* var plcTarget = 17;
if (plcTarget > 0)
{
// PLC 指定了目标站点
return ExternalTriggerResult.UseTarget(plcTarget);
}*/
return ExternalTriggerResult.Fail();
}
protected override ExternalTriggerResult OnChargeTrigger(int currentSiteId, LoopTask task, Car car)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
if (car.status.enums.TryGetValue("Soc", out var soc))
{
if (float.Parse(soc) >= NoChargesocStandard)
{
//使用配置目标
return ExternalTriggerResult.UseConfigTarget();
}
}
}
/// <summary>
/// 供 ButtonMission 反射调用:登记一个需要按钮放行的站点。
/// 如果站点已经存在于字典中则忽略,避免重复放行信号堆积。
/// </summary>
/// <param name="siteId">需要放行的站点 ID,通常对应 LoopTask.CurrentStationId</param>
/// <returns>始终返回 true,表示本次按钮触发已被接受</returns>
public bool EnqueueButtonTriggerSite(int siteId)
{
if (siteId <= 0)
{
Diagnosis.Log($"按钮放行登记失败:无效站点 {siteId}", "LoopMission", true);
return false;
}
var car = FindCarArrivedAtSite(siteId);
if (car == null)
{
Diagnosis.Post($"按钮放行登记忽略:站点 {siteId} 当前没有到站车辆", "LoopMission", false);
return false;
}
if (_buttonTriggeredSiteIds.TryAdd(siteId, 0))
{
Diagnosis.Post($"按钮放行登记成功:站点 {siteId},车辆 {car.name}", "LoopMission", false);
}
else
{
Diagnosis.Post($"按钮放行已存在,忽略重复登记:站点 {siteId}", "LoopMission", false);
}
return true;
}
/// <summary>
/// 按钮盒触发:只有当前站点已被按钮登记过,才允许使用配置目标放行。
/// 命中后立即消费并删除,保证一次按钮只放行一次。
/// </summary>
protected override ExternalTriggerResult OnButtonTrigger(int currentSiteId, LoopTask task, Car car)
{
if (_buttonTriggeredSiteIds.TryRemove(currentSiteId, out _))
{
Diagnosis.Post($"按钮放行消费成功:站点 {currentSiteId},车辆 {car?.name}", "LoopMission", false);
return ExternalTriggerResult.UseConfigTarget();
}
return ExternalTriggerResult.Fail();
}
/* private int ReadPlcTargetSite(int siteId)
{
// 从 PLC 读取目标站点的业务逻辑
// 返回 0 表示未获取到有效目标
return 0;
}
*/
}
}
+570
View File
@@ -0,0 +1,570 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LoopViewerApp
{
partial class LoopViewer
{
private System.ComponentModel.IContainer components = null;
private ComboBox cmbTaskKind;
private NumericUpDown numCurrent;
private NumericUpDown numTarget;
private NumericUpDown numTraffic;
private CheckBox chkViaPoint;
private ComboBox cmbStartType;
private NumericUpDown numPriority;
private Button btnEdit; // 保留字段以供代码逻辑/样式使用(在界面上隐藏)
private Button btnDelete; // 保留字段以供代码逻辑/样式使用(在界面上隐藏)
private Button btnSave;
private Button btnCancel;
private ListView lstTasks;
private GroupBox grpEdit;
// 布局控件
private SplitContainer splitContainer;
private TableLayoutPanel tlpEdit;
private FlowLayoutPanel flpButtons;
// 中间竖向按钮(列表与编辑区之间)
private Panel pnlMiddle;
private FlowLayoutPanel flpMiddle;
private Button btnMiddleEdit;
private Button btnMiddleDelete;
// 列头
private ColumnHeader colId;
private ColumnHeader colTaskType;
private ColumnHeader colCurrent;
private ColumnHeader colTarget;
private ColumnHeader colTraffic;
private ColumnHeader colPriority;
private ColumnHeader colViaPoint;
private ColumnHeader colStartType;
// 标签字段(编辑区)
private Label lblKind;
private Label lblCurrent;
private Label lblTarget;
private Label lblTraffic;
private Label lblPriority;
private Label lblVia;
private Label lblStartType;
private Label lblEditingId; // 显示当前编辑的任务ID
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.splitContainer = new System.Windows.Forms.SplitContainer();
this.pnlMiddle = new System.Windows.Forms.Panel();
this.flpMiddle = new System.Windows.Forms.FlowLayoutPanel();
this.btnMiddleEdit = new System.Windows.Forms.Button();
this.btnMiddleDelete = new System.Windows.Forms.Button();
this.lstTasks = new System.Windows.Forms.ListView();
this.colId = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTaskType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colCurrent = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTarget = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colTraffic = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colPriority = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colViaPoint = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colStartType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.grpEdit = new System.Windows.Forms.GroupBox();
this.tlpEdit = new System.Windows.Forms.TableLayoutPanel();
this.lblEditingId = new System.Windows.Forms.Label();
this.lblKind = new System.Windows.Forms.Label();
this.cmbTaskKind = new System.Windows.Forms.ComboBox();
this.lblCurrent = new System.Windows.Forms.Label();
this.numCurrent = new System.Windows.Forms.NumericUpDown();
this.lblTarget = new System.Windows.Forms.Label();
this.numTarget = new System.Windows.Forms.NumericUpDown();
this.lblTraffic = new System.Windows.Forms.Label();
this.numTraffic = new System.Windows.Forms.NumericUpDown();
this.lblPriority = new System.Windows.Forms.Label();
this.numPriority = new System.Windows.Forms.NumericUpDown();
this.lblVia = new System.Windows.Forms.Label();
this.chkViaPoint = new System.Windows.Forms.CheckBox();
this.lblStartType = new System.Windows.Forms.Label();
this.cmbStartType = new System.Windows.Forms.ComboBox();
this.flpButtons = new System.Windows.Forms.FlowLayoutPanel();
this.btnSave = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.btnEdit = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.Panel2.SuspendLayout();
this.splitContainer.SuspendLayout();
this.pnlMiddle.SuspendLayout();
this.flpMiddle.SuspendLayout();
this.grpEdit.SuspendLayout();
this.tlpEdit.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numCurrent)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numTarget)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numTraffic)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numPriority)).BeginInit();
this.flpButtons.SuspendLayout();
this.SuspendLayout();
//
// splitContainer
//
this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer.Location = new System.Drawing.Point(0, 0);
this.splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
this.splitContainer.Panel1.Controls.Add(this.pnlMiddle);
this.splitContainer.Panel1.Controls.Add(this.lstTasks);
//
// splitContainer.Panel2
//
this.splitContainer.Panel2.Controls.Add(this.grpEdit);
this.splitContainer.Size = new System.Drawing.Size(1200, 600);
this.splitContainer.SplitterDistance = 680;
this.splitContainer.SplitterWidth = 6;
this.splitContainer.TabIndex = 0;
//
// pnlMiddle
//
this.pnlMiddle.Controls.Add(this.flpMiddle);
this.pnlMiddle.Dock = System.Windows.Forms.DockStyle.Right;
this.pnlMiddle.Location = new System.Drawing.Point(614, 0);
this.pnlMiddle.Name = "pnlMiddle";
this.pnlMiddle.Padding = new System.Windows.Forms.Padding(6);
this.pnlMiddle.Size = new System.Drawing.Size(66, 600);
this.pnlMiddle.TabIndex = 0;
//
// flpMiddle
//
this.flpMiddle.Anchor = System.Windows.Forms.AnchorStyles.None;
this.flpMiddle.Controls.Add(this.btnMiddleEdit);
this.flpMiddle.Controls.Add(this.btnMiddleDelete);
this.flpMiddle.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
this.flpMiddle.Location = new System.Drawing.Point(0, 220);
this.flpMiddle.Name = "flpMiddle";
this.flpMiddle.Padding = new System.Windows.Forms.Padding(2);
this.flpMiddle.Size = new System.Drawing.Size(63, 160);
this.flpMiddle.TabIndex = 0;
this.flpMiddle.WrapContents = false;
//
// btnMiddleEdit
//
this.btnMiddleEdit.BackColor = System.Drawing.SystemColors.Control;
this.btnMiddleEdit.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnMiddleEdit.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnMiddleEdit.Location = new System.Drawing.Point(6, 12);
this.btnMiddleEdit.Margin = new System.Windows.Forms.Padding(4, 10, 4, 4);
this.btnMiddleEdit.Name = "btnMiddleEdit";
this.btnMiddleEdit.Size = new System.Drawing.Size(50, 40);
this.btnMiddleEdit.TabIndex = 0;
this.btnMiddleEdit.Text = "编辑";
this.btnMiddleEdit.UseVisualStyleBackColor = false;
this.btnMiddleEdit.Click += new System.EventHandler(this.btnEdit_Click);
//
// btnMiddleDelete
//
this.btnMiddleDelete.BackColor = System.Drawing.Color.LightCoral;
this.btnMiddleDelete.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnMiddleDelete.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnMiddleDelete.Location = new System.Drawing.Point(6, 62);
this.btnMiddleDelete.Margin = new System.Windows.Forms.Padding(4, 6, 4, 4);
this.btnMiddleDelete.Name = "btnMiddleDelete";
this.btnMiddleDelete.Size = new System.Drawing.Size(50, 40);
this.btnMiddleDelete.TabIndex = 1;
this.btnMiddleDelete.Text = "删除";
this.btnMiddleDelete.UseVisualStyleBackColor = false;
this.btnMiddleDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// lstTasks
//
this.lstTasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.colId,
this.colTaskType,
this.colCurrent,
this.colTarget,
this.colTraffic,
this.colPriority,
this.colViaPoint,
this.colStartType});
this.lstTasks.Dock = System.Windows.Forms.DockStyle.Fill;
this.lstTasks.FullRowSelect = true;
this.lstTasks.HideSelection = false;
this.lstTasks.Location = new System.Drawing.Point(0, 0);
this.lstTasks.Name = "lstTasks";
this.lstTasks.OwnerDraw = true;
this.lstTasks.Size = new System.Drawing.Size(680, 600);
this.lstTasks.TabIndex = 0;
this.lstTasks.UseCompatibleStateImageBehavior = false;
this.lstTasks.View = System.Windows.Forms.View.Details;
this.lstTasks.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.lstTasks_DrawColumnHeader);
this.lstTasks.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.lstTasks_DrawItem);
this.lstTasks.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.lstTasks_DrawSubItem);
this.lstTasks.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.lstTasks_MouseDoubleClick);
//
// colId
//
this.colId.Text = "ID";
this.colId.Width = 40;
//
// colTaskType
//
this.colTaskType.Text = "任务类别";
this.colTaskType.Width = 110;
//
// colCurrent
//
this.colCurrent.Text = "当前站点";
this.colCurrent.Width = 90;
//
// colTarget
//
this.colTarget.Text = "目标站点";
this.colTarget.Width = 90;
//
// colTraffic
//
this.colTraffic.Text = "流量控制";
this.colTraffic.Width = 90;
//
// colPriority
//
this.colPriority.Text = "优先级";
this.colPriority.Width = 80;
//
// colViaPoint
//
this.colViaPoint.Text = "途径点";
this.colViaPoint.Width = 70;
//
// colStartType
//
this.colStartType.Text = "启动类型";
this.colStartType.Width = 100;
//
// grpEdit
//
this.grpEdit.Controls.Add(this.tlpEdit);
this.grpEdit.Dock = System.Windows.Forms.DockStyle.Fill;
this.grpEdit.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.grpEdit.Location = new System.Drawing.Point(0, 0);
this.grpEdit.Name = "grpEdit";
this.grpEdit.Size = new System.Drawing.Size(514, 600);
this.grpEdit.TabIndex = 1;
this.grpEdit.TabStop = false;
this.grpEdit.Text = "任务信息(选中列表项后可编辑)";
//
// tlpEdit
//
this.tlpEdit.ColumnCount = 2;
this.tlpEdit.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 120F));
this.tlpEdit.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tlpEdit.Controls.Add(this.lblEditingId, 0, 0);
this.tlpEdit.Controls.Add(this.lblKind, 0, 1);
this.tlpEdit.Controls.Add(this.cmbTaskKind, 1, 1);
this.tlpEdit.Controls.Add(this.lblCurrent, 0, 2);
this.tlpEdit.Controls.Add(this.numCurrent, 1, 2);
this.tlpEdit.Controls.Add(this.lblTarget, 0, 3);
this.tlpEdit.Controls.Add(this.numTarget, 1, 3);
this.tlpEdit.Controls.Add(this.lblTraffic, 0, 4);
this.tlpEdit.Controls.Add(this.numTraffic, 1, 4);
this.tlpEdit.Controls.Add(this.lblPriority, 0, 5);
this.tlpEdit.Controls.Add(this.numPriority, 1, 5);
this.tlpEdit.Controls.Add(this.lblVia, 0, 6);
this.tlpEdit.Controls.Add(this.chkViaPoint, 1, 6);
this.tlpEdit.Controls.Add(this.lblStartType, 0, 7);
this.tlpEdit.Controls.Add(this.cmbStartType, 1, 7);
this.tlpEdit.Controls.Add(this.flpButtons, 1, 8);
this.tlpEdit.Dock = System.Windows.Forms.DockStyle.Fill;
this.tlpEdit.Location = new System.Drawing.Point(3, 25);
this.tlpEdit.Name = "tlpEdit";
this.tlpEdit.Padding = new System.Windows.Forms.Padding(8);
this.tlpEdit.RowCount = 9;
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 36F));
this.tlpEdit.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tlpEdit.Size = new System.Drawing.Size(508, 572);
this.tlpEdit.TabIndex = 0;
//
// lblEditingId
//
this.lblEditingId.Anchor = System.Windows.Forms.AnchorStyles.Left;
this.lblEditingId.AutoSize = true;
this.tlpEdit.SetColumnSpan(this.lblEditingId, 2);
this.lblEditingId.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.lblEditingId.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(215)))));
this.lblEditingId.Location = new System.Drawing.Point(11, 14);
this.lblEditingId.Name = "lblEditingId";
this.lblEditingId.Size = new System.Drawing.Size(78, 24);
this.lblEditingId.TabIndex = 0;
this.lblEditingId.Text = "新增任务";
//
// lblKind
//
this.lblKind.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblKind.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblKind.Location = new System.Drawing.Point(11, 44);
this.lblKind.Name = "lblKind";
this.lblKind.Size = new System.Drawing.Size(114, 36);
this.lblKind.TabIndex = 1;
this.lblKind.Text = "任务类别:";
this.lblKind.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmbTaskKind
//
this.cmbTaskKind.Dock = System.Windows.Forms.DockStyle.Fill;
this.cmbTaskKind.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbTaskKind.Font = new System.Drawing.Font("微软雅黑", 10F);
this.cmbTaskKind.Items.AddRange(new object[] {
"Loop",
"BranchPoint",
"JoinPoint"});
this.cmbTaskKind.Location = new System.Drawing.Point(131, 47);
this.cmbTaskKind.Name = "cmbTaskKind";
this.cmbTaskKind.Size = new System.Drawing.Size(366, 31);
this.cmbTaskKind.TabIndex = 2;
//
// lblCurrent
//
this.lblCurrent.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblCurrent.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblCurrent.Location = new System.Drawing.Point(11, 80);
this.lblCurrent.Name = "lblCurrent";
this.lblCurrent.Size = new System.Drawing.Size(114, 36);
this.lblCurrent.TabIndex = 3;
this.lblCurrent.Text = "当前站点:";
this.lblCurrent.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numCurrent
//
this.numCurrent.Dock = System.Windows.Forms.DockStyle.Left;
this.numCurrent.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numCurrent.Location = new System.Drawing.Point(131, 83);
this.numCurrent.Maximum = new decimal(new int[] {
1000000,
0,
0,
0});
this.numCurrent.Name = "numCurrent";
this.numCurrent.Size = new System.Drawing.Size(120, 29);
this.numCurrent.TabIndex = 4;
//
// lblTarget
//
this.lblTarget.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblTarget.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblTarget.Location = new System.Drawing.Point(11, 116);
this.lblTarget.Name = "lblTarget";
this.lblTarget.Size = new System.Drawing.Size(114, 36);
this.lblTarget.TabIndex = 5;
this.lblTarget.Text = "目标站点:";
this.lblTarget.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numTarget
//
this.numTarget.Dock = System.Windows.Forms.DockStyle.Left;
this.numTarget.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numTarget.Location = new System.Drawing.Point(131, 119);
this.numTarget.Maximum = new decimal(new int[] {
1000000,
0,
0,
0});
this.numTarget.Name = "numTarget";
this.numTarget.Size = new System.Drawing.Size(120, 29);
this.numTarget.TabIndex = 6;
//
// lblTraffic
//
this.lblTraffic.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblTraffic.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblTraffic.Location = new System.Drawing.Point(11, 152);
this.lblTraffic.Name = "lblTraffic";
this.lblTraffic.Size = new System.Drawing.Size(114, 36);
this.lblTraffic.TabIndex = 7;
this.lblTraffic.Text = "流量控制:";
this.lblTraffic.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numTraffic
//
this.numTraffic.Dock = System.Windows.Forms.DockStyle.Left;
this.numTraffic.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numTraffic.Location = new System.Drawing.Point(131, 155);
this.numTraffic.Maximum = new decimal(new int[] {
1000,
0,
0,
0});
this.numTraffic.Name = "numTraffic";
this.numTraffic.Size = new System.Drawing.Size(120, 29);
this.numTraffic.TabIndex = 8;
//
// lblPriority
//
this.lblPriority.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblPriority.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblPriority.Location = new System.Drawing.Point(11, 188);
this.lblPriority.Name = "lblPriority";
this.lblPriority.Size = new System.Drawing.Size(114, 36);
this.lblPriority.TabIndex = 9;
this.lblPriority.Text = "优先级:";
this.lblPriority.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numPriority
//
this.numPriority.Dock = System.Windows.Forms.DockStyle.Left;
this.numPriority.Font = new System.Drawing.Font("微软雅黑", 10F);
this.numPriority.Location = new System.Drawing.Point(131, 191);
this.numPriority.Name = "numPriority";
this.numPriority.Size = new System.Drawing.Size(120, 29);
this.numPriority.TabIndex = 10;
this.numPriority.Value = new decimal(new int[] {
1,
0,
0,
0});
//
// lblVia
//
this.lblVia.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblVia.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblVia.Location = new System.Drawing.Point(11, 224);
this.lblVia.Name = "lblVia";
this.lblVia.Size = new System.Drawing.Size(114, 36);
this.lblVia.TabIndex = 11;
this.lblVia.Text = "途径点:";
this.lblVia.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// chkViaPoint
//
this.chkViaPoint.Dock = System.Windows.Forms.DockStyle.Left;
this.chkViaPoint.Font = new System.Drawing.Font("微软雅黑", 10F);
this.chkViaPoint.Location = new System.Drawing.Point(131, 227);
this.chkViaPoint.Name = "chkViaPoint";
this.chkViaPoint.Size = new System.Drawing.Size(104, 30);
this.chkViaPoint.TabIndex = 12;
this.chkViaPoint.Text = "是";
//
// lblStartType
//
this.lblStartType.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblStartType.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblStartType.Location = new System.Drawing.Point(11, 260);
this.lblStartType.Name = "lblStartType";
this.lblStartType.Size = new System.Drawing.Size(114, 36);
this.lblStartType.TabIndex = 13;
this.lblStartType.Text = "启动类型:";
this.lblStartType.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmbStartType
//
this.cmbStartType.Dock = System.Windows.Forms.DockStyle.Fill;
this.cmbStartType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbStartType.Font = new System.Drawing.Font("微软雅黑", 10F);
this.cmbStartType.Items.AddRange(new object[] {
"Api",
"Plc",
"ButtonBox",
"AutoLoop",
"Charge"});
this.cmbStartType.Location = new System.Drawing.Point(131, 263);
this.cmbStartType.Name = "cmbStartType";
this.cmbStartType.Size = new System.Drawing.Size(366, 31);
this.cmbStartType.TabIndex = 14;
//
// flpButtons
//
this.flpButtons.AutoSize = true;
this.flpButtons.Controls.Add(this.btnSave);
this.flpButtons.Controls.Add(this.btnCancel);
this.flpButtons.Dock = System.Windows.Forms.DockStyle.Left;
this.flpButtons.Location = new System.Drawing.Point(131, 299);
this.flpButtons.Name = "flpButtons";
this.flpButtons.Size = new System.Drawing.Size(292, 262);
this.flpButtons.TabIndex = 15;
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold);
this.btnSave.Location = new System.Drawing.Point(3, 3);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(140, 40);
this.btnSave.TabIndex = 0;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnCancel
//
this.btnCancel.BackColor = System.Drawing.SystemColors.Control;
this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnCancel.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnCancel.Location = new System.Drawing.Point(149, 3);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(140, 40);
this.btnCancel.TabIndex = 1;
this.btnCancel.Text = "取消";
this.btnCancel.UseVisualStyleBackColor = false;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnEdit
//
this.btnEdit.Location = new System.Drawing.Point(0, 0);
this.btnEdit.Name = "btnEdit";
this.btnEdit.Size = new System.Drawing.Size(75, 23);
this.btnEdit.TabIndex = 0;
this.btnEdit.Visible = false;
//
// btnDelete
//
this.btnDelete.Location = new System.Drawing.Point(0, 0);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(75, 23);
this.btnDelete.TabIndex = 0;
this.btnDelete.Visible = false;
//
// LoopViewer
//
this.ClientSize = new System.Drawing.Size(1200, 600);
this.Controls.Add(this.splitContainer);
this.Font = new System.Drawing.Font("微软雅黑", 9F);
this.MinimumSize = new System.Drawing.Size(1000, 420);
this.Name = "LoopViewer";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "任务列表管理器";
this.splitContainer.Panel1.ResumeLayout(false);
this.splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
this.splitContainer.ResumeLayout(false);
this.pnlMiddle.ResumeLayout(false);
this.flpMiddle.ResumeLayout(false);
this.grpEdit.ResumeLayout(false);
this.tlpEdit.ResumeLayout(false);
this.tlpEdit.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numCurrent)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numTarget)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numTraffic)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numPriority)).EndInit();
this.flpButtons.ResumeLayout(false);
this.ResumeLayout(false);
}
}
}
+595
View File
@@ -0,0 +1,595 @@
using Newtonsoft.Json;
using StandardScene.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LoopViewerApp
{
public partial class LoopViewer : Form
{
private readonly string jsonPath =
Path.Combine(Application.StartupPath, "tasklist.json");
private List<LoopTask> tasks = new List<LoopTask>();
// -1 表示新增模式;>=0 表示正在编辑对应索引
private int editingIndex = -1;
public LoopViewer()
{
InitializeComponent();
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
return;
// 应用 ChargeStationManagementForm 风格的运行时样式调整
ApplyChargeStyle();
EnsureComboItems();
// 启用多选并绑定右键菜单与 Delete 键删除功能
try
{
if (lstTasks != null)
{
lstTasks.MultiSelect = true;
// 右键菜单:删除
var ctx = new ContextMenuStrip();
ctx.Items.Add("删除", null, (s, e) => OnDeleteSelectedTasks());
lstTasks.ContextMenuStrip = ctx;
// 键盘删除键绑定
lstTasks.KeyDown += lstTasks_KeyDown;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer context menu init error: {ex}");
}
try
{
InitOrLoadJson();
RenderListView();
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoopViewer initialization error: {ex}");
}
}
private void lstTasks_KeyDown(object sender, KeyEventArgs e)
{
try
{
if (e.KeyCode == Keys.Delete)
{
OnDeleteSelectedTasks();
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_KeyDown error: {ex}");
}
}
/// <summary>
/// 删除 ListView 中选中的任务(支持多选)
/// </summary>
private void OnDeleteSelectedTasks()
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
return;
// 收集被选中的索引并按降序删除,避免索引移动问题
var selectedIndices = lstTasks.SelectedIndices.Cast<int>().OrderByDescending(i => i).ToList();
// 构造确认提示
string prompt;
if (selectedIndices.Count == 1)
{
int idx = selectedIndices[0];
if (idx >= 0 && idx < tasks.Count)
prompt = $"确认删除任务 ID={tasks[idx].Id}";
else
prompt = "确认删除选中任务?";
}
else
{
prompt = $"确认删除所选 {selectedIndices.Count} 个任务?";
}
if (MessageBox.Show(prompt, "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
// 删除任务
foreach (var idx in selectedIndices)
{
if (idx >= 0 && idx < tasks.Count)
{
tasks.RemoveAt(idx);
}
}
// 如果被删除项包含当前正在编辑的项,退出编辑状态
if (editingIndex >= 0)
{
if (editingIndex >= tasks.Count || selectedIndices.Any(i => i == editingIndex))
{
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
else
{
// 重新计算编辑索引在删除后的新位置
int removedBefore = selectedIndices.Count(i => i < editingIndex);
editingIndex -= removedBefore;
}
}
// 持久化并刷新列表视图
Save();
RenderListView();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"OnDeleteSelectedTasks error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
}
}
/// <summary>
/// 将 LoopViewer 的运行时样式调整为与 ChargeStationManagementForm 接近的视觉风格:
/// - 全局字体设为微软雅黑
/// - 表头暖色替换为蓝色沉稳风格(和充电界面一致)
/// - 按钮字号、背景色与充电界面保持一致(保存/删除/取消)
/// - 列表视图设置为整行选择、无边框、交替背景等
/// 注意:不修改 Designer 文件,仅在运行时统一控件表现,避免破坏设计器生成代码。
/// </summary>
private void ApplyChargeStyle()
{
try
{
// 窗体级设置
this.StartPosition = FormStartPosition.CenterScreen;
this.MinimumSize = new System.Drawing.Size(1327, 738);
this.Font = new Font("微软雅黑", 9F, FontStyle.Regular);
// 调整 ListView(如果存在)
if (lstTasks != null)
{
lstTasks.View = View.Details;
lstTasks.FullRowSelect = true;
lstTasks.GridLines = false;
lstTasks.HeaderStyle = ColumnHeaderStyle.Nonclickable;
lstTasks.OwnerDraw = true; // 已有自定义绘制
lstTasks.BackColor = Color.White;
lstTasks.ForeColor = Color.FromArgb(33, 33, 33);
// 多选由初始化时控制(这里不强制)
}
// 下拉框统一字体
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 数值输入框统一字体
if (numCurrent != null) numCurrent.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTarget != null) numTarget.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numTraffic != null) numTraffic.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (numPriority != null) numPriority.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
// 标签字体统一
if (lblEditingId != null) lblEditingId.Font = new Font("微软雅黑", 10F, FontStyle.Bold);
// 按钮风格:与 ChargeStationManagementForm 保持一致的视觉优先级
if (btnSave != null)
{
btnSave.BackColor = Color.LightBlue;
btnSave.ForeColor = Color.Black;
btnSave.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnSave.FlatStyle = FlatStyle.Flat;
}
if (btnDelete != null)
{
btnDelete.BackColor = Color.LightCoral;
btnDelete.ForeColor = Color.Black;
btnDelete.Font = new Font("微软雅黑", 11F, FontStyle.Bold);
btnDelete.FlatStyle = FlatStyle.Flat;
}
if (btnCancel != null)
{
btnCancel.BackColor = SystemColors.Control;
btnCancel.ForeColor = Color.Black;
btnCancel.Font = new Font("微软雅黑", 11F, FontStyle.Regular);
btnCancel.FlatStyle = FlatStyle.Flat;
}
// 如果存在额外的操作按钮(例如在面板上),尝试统一风格(容错)
foreach (Control ctrl in this.Controls)
{
if (ctrl is Panel pnl)
{
pnl.Padding = new Padding(12);
}
else if (ctrl is Button btn)
{
// 已设置主要按钮,其他按钮使用中性风格
if (btn == btnSave || btn == btnDelete || btn == btnCancel) continue;
btn.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ApplyChargeStyle error: {ex}");
}
}
private void EnsureComboItems()
{
try
{
if (cmbTaskKind != null && cmbTaskKind.Items.Count == 0)
{
cmbTaskKind.Items.AddRange(new object[] { "Loop", "BranchPoint", "JoinPoint" });
cmbTaskKind.SelectedIndex = 0;
}
if (cmbStartType != null && cmbStartType.Items.Count == 0)
{
cmbStartType.Items.AddRange(new object[] { "Api", "Plc", "ButtonBox", "AutoLoop" });
cmbStartType.SelectedIndex = 3;
}
// 确保下拉框字体一致(防止 Designer 未设置)
if (cmbTaskKind != null) cmbTaskKind.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
if (cmbStartType != null) cmbStartType.Font = new Font("微软雅黑", 10F, FontStyle.Regular);
}
catch { }
}
#region /
private void InitOrLoadJson()
{
try
{
if (!File.Exists(jsonPath))
File.WriteAllText(jsonPath, "[]");
var text = File.ReadAllText(jsonPath);
tasks = JsonConvert.DeserializeObject<List<LoopTask>>(text) ?? new List<LoopTask>();
}
catch (Exception ex)
{
tasks = new List<LoopTask>();
System.Diagnostics.Debug.WriteLine($"Load tasks failed: {ex}");
}
}
#endregion
#region ID
/// <summary>
/// 获取下一个可用的任务ID(当前最大ID + 1)
/// </summary>
/// <returns>新的任务ID</returns>
private int GetNextTaskId()
{
if (tasks == null || tasks.Count == 0)
return 1;
int maxId = tasks.Max(t => t.Id);
return maxId + 1;
}
#endregion
#region OwnerDraw +
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 ChargeStationManagementForm 表头保持一致的深蓝背景与白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181))) // 深蓝(与 Charge 界面一致)
using (var textBrush = new SolidBrush(Color.White)) // 白色文字
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
var sf = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
var rect = e.Bounds;
rect.Inflate(-8, 0);
e.Graphics.DrawString(e.Header.Text, font, textBrush, rect, sf);
// 分隔线
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
{
e.Graphics.DrawLine(pen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
}
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
{
// 由 DrawSubItem 绘制全部内容以保证每列对齐
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
try
{
var item = e.Item;
bool selected = item.Selected;
Rectangle bounds = e.Bounds;
// 选中行颜色:与 ChargeStationManagementForm 保持一致的蓝色强调
Color selectedBack = Color.FromArgb(0, 120, 215);
Color selectedFore = Color.White;
// 非选中行交替背景
Color evenBack = Color.White;
Color oddBack = Color.FromArgb(250, 251, 253);
Color normalFore = Color.FromArgb(33, 33, 33);
// 填充背景
if (selected)
{
using (var selBrush = new SolidBrush(selectedBack))
{
e.Graphics.FillRectangle(selBrush, bounds);
}
}
else
{
using (var back = new SolidBrush(e.ItemIndex % 2 == 0 ? evenBack : oddBack))
{
e.Graphics.FillRectangle(back, bounds);
}
}
// 绘制文本(加一点内边距)
string text = e.SubItem.Text ?? string.Empty;
Color fore = selected ? selectedFore : normalFore;
TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
Rectangle textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
{
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, flags);
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region /
private void RenderListView()
{
try
{
if (lstTasks == null) return;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var t in tasks)
{
var lvi = new ListViewItem(new[]
{
t.Id.ToString(), // ID 列
t.Kind.ToString(),
t.CurrentStationId.ToString(),
t.TargetStationId.ToString(),
t.TrafficControl.ToString(),
t.Priority.ToString(),
t.IsViaPoint ? "是" : "否",
t.StartType.ToString()
});
lstTasks.Items.Add(lvi);
}
lstTasks.EndUpdate();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
}
}
private void Save()
{
try
{
File.WriteAllText(jsonPath, JsonConvert.SerializeObject(tasks, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region /
private void UpdateSaveButtonText()
{
if (btnSave != null)
{
// 文案固定为"保存"
btnSave.Text = "保存";
}
}
private void btnSave_Click(object sender, EventArgs e)
{
try
{
// 从面板读取值,直接在界面内编辑/新增
Enum.TryParse<TaskKind>(cmbTaskKind?.SelectedItem?.ToString() ?? "Loop", out var kind);
Enum.TryParse<TaskStartType>(cmbStartType?.SelectedItem?.ToString() ?? "AutoLoop", out var st);
if (editingIndex >= 0 && editingIndex < tasks.Count)
{
// 更新模式:保留原有ID
var existingTask = tasks[editingIndex];
existingTask.Kind = kind;
existingTask.CurrentStationId = (int)(numCurrent?.Value ?? 0);
existingTask.TargetStationId = (int)(numTarget?.Value ?? 0);
existingTask.TrafficControl = (int)(numTraffic?.Value ?? 0);
existingTask.Priority = (int)(numPriority?.Value ?? 1);
existingTask.IsViaPoint = chkViaPoint?.Checked ?? false;
existingTask.StartType = st;
}
else
{
// 新增模式:自动分配新ID
var t = new LoopTask
{
Id = GetNextTaskId(), // 自增ID
Kind = kind,
CurrentStationId = (int)(numCurrent?.Value ?? 0),
TargetStationId = (int)(numTarget?.Value ?? 0),
TrafficControl = (int)(numTraffic?.Value ?? 0),
Priority = (int)(numPriority?.Value ?? 1),
IsViaPoint = chkViaPoint?.Checked ?? false,
StartType = st
};
tasks.Add(t);
}
Save();
RenderListView();
// 恢复新增状态
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
// 取消编辑,清空面板并回到"添加"模式
editingIndex = -1;
UpdateSaveButtonText();
ClearPanelInputs();
}
private void btnEdit_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnEdit_Click error: {ex}");
}
}
private void btnDelete_Click(object sender, EventArgs e)
{
// 兼容旧的删除按钮:复用统一删除逻辑
OnDeleteSelectedTasks();
}
#endregion
#region
private void lstTasks_MouseDoubleClick(object sender, MouseEventArgs e)
{
try
{
if (lstTasks == null) return;
var item = lstTasks.GetItemAt(e.X, e.Y);
if (item == null) return;
int idx = item.Index;
if (idx < 0 || idx >= tasks.Count) return;
editingIndex = idx;
LoadTaskToPanel(tasks[idx]);
UpdateSaveButtonText();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"lstTasks_MouseDoubleClick error: {ex}");
}
}
#endregion
#region
private void LoadTaskToPanel(LoopTask t)
{
if (t == null) return;
try
{
// 显示当前编辑的任务ID(只读显示)
if (lblEditingId != null) lblEditingId.Text = $"编辑任务 ID: {t.Id}";
if (cmbTaskKind != null) cmbTaskKind.SelectedItem = t.Kind.ToString();
if (numCurrent != null) numCurrent.Value = Math.Max(numCurrent.Minimum, Math.Min(numCurrent.Maximum, t.CurrentStationId));
if (numTarget != null) numTarget.Value = Math.Max(numTarget.Minimum, Math.Min(numTarget.Maximum, t.TargetStationId));
if (numTraffic != null) numTraffic.Value = Math.Max(numTraffic.Minimum, Math.Min(numTraffic.Maximum, t.TrafficControl));
if (numPriority != null) numPriority.Value = Math.Max(numPriority.Minimum, Math.Min(numPriority.Maximum, t.Priority));
if (chkViaPoint != null) chkViaPoint.Checked = t.IsViaPoint;
if (cmbStartType != null) cmbStartType.SelectedItem = t.StartType.ToString();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadTaskToPanel error: {ex}");
}
}
private void ClearPanelInputs()
{
try
{
// 清除编辑ID显示
if (lblEditingId != null) lblEditingId.Text = "新增任务";
if (cmbTaskKind != null) cmbTaskKind.SelectedIndex = 0;
if (numCurrent != null) numCurrent.Value = 0;
if (numTarget != null) numTarget.Value = 0;
if (numTraffic != null) numTraffic.Value = 0;
if (numPriority != null) numPriority.Value = 1;
if (chkViaPoint != null) chkViaPoint.Checked = false;
if (cmbStartType != null) cmbStartType.SelectedIndex = 3;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
}
}
#endregion
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,206 @@
using SimpleCore;
using SimpleCore.Library;
using StandardScene.Utils;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using StandardScene.Model;
namespace StandardScene.Chained
{
/// <summary>
/// 统一管理 TransportDelivery 相关的任务回调方法,并通过回调注册表提供可恢复性。
/// </summary>
public static class TransportDeliveryCallbacks
{
public const string KeyOnStarted = "Transport.OnMissionStarted";
public const string KeyOnFetched = "Transport.OnFetched";
public const string KeyOnPut = "Transport.OnPut";
public const string KeyOnFinished = "Transport.OnMissionFinished";
public const string KeyOnFailed = "Transport.OnMissionFailed";
public const string KeyOnTerminated = "Transport.OnMissionTerminated";
private static bool _initialized;
private static readonly HttpClient _httpClient = new HttpClient();
private static string _callbackUrl = "http://127.0.0.1:20101/api/v1/MDCS/State";
static TransportDeliveryCallbacks()
{
if (_initialized) return;
_initialized = true;
DeliveryCallbackRegistry.RegisterOnStart (KeyOnStarted, OnMissionStarted);
DeliveryCallbackRegistry.RegisterDoneFetch (KeyOnFetched, OnFetched);
DeliveryCallbackRegistry.RegisterDonePut (KeyOnPut, OnPut);
DeliveryCallbackRegistry.RegisterDoneMission(KeyOnFinished, OnMissionFinished);
DeliveryCallbackRegistry.RegisterFailed (KeyOnFailed, OnMissionFailed);
DeliveryCallbackRegistry.RegisterOnTerminated(KeyOnTerminated, OnMissionTerminated);
}
/// <summary>
/// 显式调用以确保静态构造函数已执行(注册所有默认回调)。
/// </summary>
public static void EnsureInitialized()
{
// 访问本类,确保静态构造已执行
if (!_initialized)
{
// 触发 static ctorCLR 保证线程安全)
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(TransportDeliveryCallbacks).TypeHandle);
}
}
/// <summary>
/// 配置任务状态回调的完整 URL(例如 http://ip:port/api/v1/MDCS/State)。
/// 建议在 TransportMission 启动或构造时调用一次。
/// </summary>
public static void ConfigureCallbackUrl(string url)
{
if (!string.IsNullOrWhiteSpace(url))
{
_callbackUrl = url;
}
}
private static void DispatchMissionState(TransportDelivery d, MissionState.MissionStateEnum state)
{
var ms = new MissionState
{
MissionId = d.TaskId,
CarCode = d.UsingCar?.id.ToString() ?? "0",
TriggerTime = DateTime.Now,
State = state
};
MissionStatePost(ms);
}
private static async void MissionStatePost(MissionState ms)
{
var retryCount = 10;
var retryDelay = TimeSpan.FromSeconds(1);
var content = new StringContent(ms.ToJson());
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
for (int i = 0; i < retryCount; i++)
{
try
{
var resp = await _httpClient.PostAsync(_callbackUrl, content);
Diagnosis.Post($"{ms.State}回调结果 => {resp.Content.ReadAsStringAsync().Result}");
var body = resp.Content.ReadAsStringAsync().Result.JsonTo<BaseRespose>();
var result = body.Code == 200 ? "成功" : "失败";
Diagnosis.Post($"车辆编号:{ms.CarCode} 任务id{ms.MissionId} 状态回调执行{result}");
break;
}
catch (Exception ex)
{
Diagnosis.Log($"{ms.State}状态回调失败 => ex:{ex}");
if (i == retryCount - 1)
{
Diagnosis.Log($"StringContent:{content}", "apiError");
}
await Task.Delay(retryDelay);
}
}
}
public static async void OnMissionStarted(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) started");
if (!string.IsNullOrWhiteSpace(d.TaskIdsString))
{
Commons.AddOrUpdateTag(d.UsingCar.tags, "taskIdsString", d.TaskIdsString);
Diagnosis.Log(
$"holdCarTagAddOrUpdate--TaskId:{d.TaskId},task:[{d.ToJson()}].taskIdsString:{d.TaskIdsString}",
"holdCarTag",
true);
}
Commons.DeleteTag(d.UsingCar.tags, "holdCar");
if (!string.IsNullOrEmpty(d.HoldCarSite))
{
Commons.AddOrUpdateTag(d.UsingCar.tags, "holdCar", d.HoldCarSite);
Diagnosis.Log(
$"holdCarTagAddOrUpdate--TaskId:{d.TaskId},task:[{d.ToJson()}].holdCar:{d.HoldCarSite}",
"holdCarTag",
true);
}
DispatchMissionState(d, MissionState.MissionStateEnum.Started);
}
public static async void OnFetched(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
if (d.Src == d.Dst) return;
Diagnosis.Log($"task(#{d.TaskId}) fetched");
DispatchMissionState(d, MissionState.MissionStateEnum.Fetched);
}
public static async void OnPut(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
if (d.Src == d.Dst) return;
Diagnosis.Log($"task(#{d.TaskId}) put");
DispatchMissionState(d, MissionState.MissionStateEnum.Put);
}
public static async void OnMissionFailed(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) failed");
DispatchMissionState(d, MissionState.MissionStateEnum.Failed);
}
public static async void OnMissionFinished(ChainedDeliveryMission.Delivery delivery)
{
var d = (TransportDelivery)delivery;
Diagnosis.Log($"task(#{d.TaskId}) finished");
if (!string.IsNullOrWhiteSpace(d.MGTaskCode!) && d.MGTaskCode.Contains("-"))
{
var phase = d.MGTaskCode.Split('-')[1];
var last = int.Parse(phase) - 1;
if (last > 0 && string.IsNullOrWhiteSpace(d.TaskIdsString))
{
var holdCar = SimpleLib.GetAllCars()
.FirstOrDefault(e => e.tags.TryGetValue("taskIdsString", out var v) && v.Contains(d.TaskId));
if (holdCar != null)
{
Commons.DeleteTag(holdCar.tags, "taskIdsString");
Diagnosis.Log(
$"holdCarTagDelete-2OnMissionFinished--TaskId:{d.TaskId},task:[{d.ToJson()}].taskIdsString:{d.TaskIdsString}",
"holdCarTag",
true);
}
}
}
DispatchMissionState(d, MissionState.MissionStateEnum.Finished);
}
public static async Task<int> OnMissionTerminated(ChainedDeliveryMission.Delivery delivery, string ISRelease)
{
try
{
using var hc = new HttpClient();
var task = await hc.GetAsync($"http://{delivery.UsingCar.address}:8008/car/startOrPause?ISRelease={ISRelease}");
var resp = task.Content.ReadAsStringAsync().Result.JsonTo<BaseRespose>();
var result = resp.Code == 200 ? "成功" : "失败";
return 1;
}
catch (Exception ex)
{
Diagnosis.Log($"暂停恢复失败 => ex:{ExceptionFormatter.FormatEx(ex)}");
return 0;
}
}
}
}
@@ -0,0 +1,599 @@
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.RCS.Signal;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.Library;
using SimpleCore.PropType;
using StandardScene.Model;
using StandardScene;
using StandardScene.Utils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32.SafeHandles;
namespace StandardScene.Chained
{
/// <summary>
/// 任务类型枚举
/// </summary>
public enum TaskType
{
= 1, // 移动任务(当前未使用)
= 2, // 搬运任务
= 3, // 装车任务
= 4, // 卸车任务
}
/// <summary>
/// 运输任务类
/// 继承自AbstractDelivery,实现具体的运输任务功能
/// </summary>
/// public class TransportDelivery : AbstractChainedDeliveryMission.AbstractDelivery
public class TransportDelivery : ChainedDeliveryMission.Delivery
{
/// <summary>任务类型(枚举值)</summary>
public TaskType Type;
/// <summary>任务类型(字符串形式)</summary>
public string TaskType;
/// <summary>使用的小车名称</summary>
public string UsingCarName = "/";
/// <summary>物料信息</summary>
public string Material = "";
/// <summary>占车站点</summary>
public string HoldCarSite = "";
/// <summary>阶段任务代码(用于标记阶段任务)</summary>
public string MGTaskCode = "";
/// <summary>任务ID字符串(用于标记阶段任务,可包含多个任务ID,用逗号分隔)</summary>
public string TaskIdsString = "";
/// <summary>
/// 构造函数
/// 注册任务生命周期回调函数
/// </summary>
public TransportDelivery()
{
// 注册任务开始回调
OnStart += d => { TransportOnStart((TransportDelivery)d); };
// 注册取货完成回调
DoneFetch += d => { TransportDoneFetch((TransportDelivery)d); };
// 注册放货完成回调
DonePut += d => { TransportDonePut((TransportDelivery)d); };
// 注册任务完成回调
DoneMission += d => { TransportDoneMission((TransportDelivery)d); };
}
/// <summary>
/// 任务开始时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportOnStart(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-onStart");
}
/// <summary>
/// 取货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneFetch(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneFetch");
}
/// <summary>
/// 放货完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDonePut(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-donePut");
}
/// <summary>
/// 任务完成时的回调处理
/// </summary>
/// <param name="d">运输任务对象</param>
private void TransportDoneMission(TransportDelivery d)
{
Diagnosis.Post($"{d.TaskId}___{d.Src}->{d.Dst}: step-doneMission");
}
}
/// <summary>
/// 搬运任务进程类
/// 继承自AbstractChainedDeliveryMission,实现具体的搬运任务调度
/// </summary>
[MissionType(Name = "搬运任务进程", editor = typeof(TransportMission))]
internal class TransportMission : ChainedDeliveryMission
{
/// <summary>是否启用就近任务策略("1"=启用,"0"=禁用)</summary>
private static readonly string NearestTask = "1";
/// <summary>任务查看器窗口对象</summary>
private DeliveryViewer dv;
public TransportMission()
{
// 确保回调静态类已完成注册
TransportDeliveryCallbacks.EnsureInitialized();
// 根据配置的 MissionCallbackURL 设置回调地址(若有)
var p = Param;
if (p != null && !string.IsNullOrWhiteSpace(p.MissionCallbackURL))
{
if (Commons.IsValidHttpUrl(p.MissionCallbackURL))
{
_lastCallbackUrl = p.MissionCallbackURL;
TransportDeliveryCallbacks.ConfigureCallbackUrl(_lastCallbackUrl);
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {p.MissionCallbackURL}", "TransportMission");
}
}
}
public class TransportMissionParam
{
/// <summary>
/// 递送任务状态回调地址。
/// </summary>
public string MissionCallbackURL;
}
public TransportMissionParam Param => StringDictConvert<TransportMissionParam>.Convert(fields);
private readonly bool _onDisplay = true;
private string _lastCallbackUrl;
protected override Delivery CreateDeliveryFromSnapshot(DeliveryStateSnapshot snap)
{
var usingCar = SimpleLib.GetCar(snap.UsingCarId);
if (snap.UsingCarId != -1 && usingCar == null)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] failed:usingCar[{snap.UsingCarId}] not found");
return null;
}
if (usingCar != null && usingCar is not Car)
{
Diagnosis.Post($"CreateDeliveryFromSnapshot[{snap.Id}] skipped:usingCar[{snap.UsingCarId}] is not Car type");
return null;
}
var d = new TransportDelivery
{
Id = snap.Id,
TaskId = snap.TaskId ?? string.Empty,
Src = snap.Src,
Dst = snap.Dst,
SkipFetch = snap.SkipFetch,
SkipPut = snap.SkipPut,
Putting = snap.Putting,
CreateTime = snap.CreateTime,
StartTime = snap.StartTime,
FinishTime = snap.FinishTime,
CarType = snap.CarType,
UsingCar = (Car)usingCar
};
if (usingCar != null && string.Equals(snap.Status, "putting", StringComparison.OrdinalIgnoreCase) && !CheckCarLoaded(usingCar))
{
d.Error = true;
}
// 恢复回调配置与事件绑定
d.ReportOnStarted = snap.ReportOnStarted;
d.ReportOnFetched = snap.ReportOnFetched;
d.ReportOnPut = snap.ReportOnPut;
d.ReportOnFinished = snap.ReportOnFinished;
d.ReportOnFailed = snap.ReportOnFailed;
d.ReportOnTerminated = snap.ReportOnTerminated;
d.OnStartCallbackKeys = snap.OnStartCallbackKeys ?? new List<string>();
d.DoneFetchCallbackKeys = snap.DoneFetchCallbackKeys ?? new List<string>();
d.DonePutCallbackKeys = snap.DonePutCallbackKeys ?? new List<string>();
d.DoneMissionCallbackKeys = snap.DoneMissionCallbackKeys ?? new List<string>();
d.FailedCallbackKeys = snap.FailedCallbackKeys ?? new List<string>();
d.OnTerminatedCallbackKeys = snap.OnTerminatedCallbackKeys ?? new List<string>();
// 统一通过 Attacher 挂载所有回调
DeliveryCallbackAttacher.AttachAll(d);
return d;
}
protected override bool CheckCarLoaded(AbstractCar car)
{
if(car is DummyCar)
return true;
var carLoaded = Commons.GetCarStatus((Car)car, "Loaded");
if (!string.IsNullOrEmpty(carLoaded) && bool.TryParse(carLoaded, out var result))
return result;
return false;
}
/// <summary>
/// 执行方法(启动任务调度进程)
/// 启动基类的任务调度循环,并启动状态显示线程
/// </summary>
public override void Execute()
{
base.Execute(); // 调用基类Execute方法,启动任务调度循环
//HideSimpleConsole(); // 隐藏控制台窗口
// 启动状态显示线程,实时显示小车状态和任务执行情况
new Thread(() =>
{
while (true)
{
Thread.Sleep(100); // 每100ms更新一次显示
var painter = SimpleMonitor.getPainter("TransportMissionPainter");
painter.clear();
if (!_onDisplay) continue; // 如果未启用显示,跳过
// 检查并更新任务状态回调 URL(如果配置发生变化)
var p = Param;
var currentUrl = p?.MissionCallbackURL;
if (!string.IsNullOrWhiteSpace(currentUrl) && currentUrl != _lastCallbackUrl)
{
// 使用 Commons 中的统一 URL 校验逻辑
if (Commons.IsValidHttpUrl(currentUrl))
{
TransportDeliveryCallbacks.ConfigureCallbackUrl(currentUrl);
_lastCallbackUrl = currentUrl;
}
else
{
Diagnosis.Log($"无效的 MissionCallbackURL 配置: {currentUrl}", "TransportMission");
}
}
var onMissionCnt = 0; // 正在执行任务的小车数量
var carStr = $"{string.Join("\n", SimpleLib.GetAllCars().Select(cc =>
{
var status = "";
if (cc.tags.TryGetValue("occupied", out var occupiedStr))
{
onMissionCnt++;
status = occupiedStr;
if (cc.tags.TryGetValue("idle", out var _))
{
Diagnosis.Post($"strange idle tag, {cc.name}({cc.id})");
cc.tags.Remove("idle");
}
}
else if (cc.tags.TryGetValue("idle", out var idleStr)) status = DateTime.TryParse(idleStr, out var idleTime) ? $"idle:{idleTime}" : $"idle:{idleStr}";
return $"{cc.name}({cc.id})\t{status}";
}))}";
//var waitingMissions = GetDeliveries()
// .Where(dd => dd.GetStatus() == DeliveryStatus.Waiting).ToList();
painter.drawTextFixed(
$"小车:\t开动率:{onMissionCnt}/{SimpleLib.GetAllCars().Length}\n{carStr}\n\n",
new SolidBrush(Color.Black),
VirtualPainter.DrawPosition.LeftTop, Color.AliceBlue);
painter = null;
}
}).Start();
}
/// <summary>
/// 变更任务优先级(重写基类方法)
/// 根据配置决定是否执行就近任务策略
/// </summary>
public override void ChangePriority()
{
try
{
if (NearestTask == "1") // NearestTask 等于1 执行就近原则
{
// 就近任务执行(当前被注释,未启用)
// NearestTaskExecute();
}
else
{
// 如果未启用就近原则,移除所有小车的就近选车标记
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
Commons.DeleteTag(car.tags, "changePriority");
}
}
}
catch (Exception ex)
{
Console.WriteLine("同一工序就近执行就近任务执行:" + ex.ToString());
}
}
/// <summary>
/// 一键初始化所有小车
/// 将状态为"正常"或"上线"且未初始化的小车进行重置
/// </summary>
[MethodMember(Name = "一键初始化")]
public void ResetAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车未初始化(GetLastSite返回-1),执行重置
if (car.GetLastSite() == -1)
((Car)car).Reset();
}
}
/// <summary>
/// 一键上线所有小车
/// 将状态为"正常"或"上线"且已初始化的小车标记为在线状态
/// </summary>
[MethodMember(Name = "一键上线")]
public void OnlineAll()
{
foreach (var car in SimpleLib.GetAllCars().OfType<Car>().Where(c => c.lstatus.Contains("正常") || c.lstatus.Contains("上线")))
{
// 如果小车已初始化,标记为在线
if (car.GetLastSite() != -1)
{
Commons.AddOrUpdateTag(car.tags, "Online", "true");
}
}
}
/// <summary>
/// 查看任务列表界面
/// 打开任务查看器窗口,显示所有任务的状态
/// </summary>
[MethodMember(Name = "查看任务", Description = "显示界面")]
public void Print()
{
dv = new DeliveryViewer();
dv.Show();
}
/// <summary>
/// 手动创建单取货任务
/// 通过UI交互选择小车和取货点,创建仅取货任务(跳过取货步骤,小车从当前位置移动到取货点)
/// </summary>
[MethodMember(Name = "单取货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueueFetch()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
// 创建运输任务:从小车当前位置到选择的取货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的取货点
SkipFetch = true, // 跳过取货步骤(因为小车已经在起始点)
UsingCar = car, // 指定使用的小车
PutPlanInfo = new() { { "action", "fetch" } }, // 路径动作设置为fetch
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {car.GetLastSite()} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
//写一个冒泡排序的算法,按照距离排序
[MethodMember(Name = "切换后台显示")]
public void SwitchBackgroundDisplay()
{
ShowSimpleConsole();
}
/// <summary>
/// 手动创建单放货任务
/// 通过UI交互选择小车和放货点,创建仅放货任务(小车从当前位置移动到放货点并放货)
/// </summary>
[MethodMember(Name = "单放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueuePut()
{
try
{
G.pushStatus("选择小车");
var selected = SimpleMonitor.selected.ToArray();
if (selected.Length == 0) { MessageBox.Show("请先选择需要控制的小车!"); return; }
var obj = selected[0];
if (obj is Car car)
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择放货点(注释写的是"取货位"但实际是放货点)
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建运输任务:从小车当前位置到选择的放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = car.GetLastSite(), // 起始点为小车当前位置
Dst = pt1.site, // 目标点为选择的放货点
UsingCar = car, // 指定使用的小车
SkipFetch = true, // 跳过取货步骤
PutPlanInfo = new() { { "action", "put" } }, // 路径动作设置为put
};
G.pushStatus($"排序了一个{car.name}搬运任务{d.Id}: {srcSite.id} -> {d.Dst}");
Enqueue(d);
}
else
{
MessageBox.Show("请选择需要控制的小车!");
}
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建完整的取放货任务
/// 通过UI交互选择取货点和放货点,创建完整的搬运任务(取货->放货)
/// </summary>
[MethodMember(Name = "取放货", Description = "增加并排队搬运任务链")]
public async void ManualEnqueue()
{
try
{
G.pushStatus("选择取货位");
// 等待用户在UI中选择取货点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
G.pushStatus("选择放货位");
// 等待用户在UI中选择放货点
var pt2 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var dstSite = SimpleLib.GetSite(pt2.site);
// 创建完整的运输任务:从取货点到放货点
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSite.id, // 取货点
Dst = dstSite.id, // 放货点
TaskId = $"ManualTask-{DateTime.Now}" // 生成任务ID
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {dstSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 手动创建移动任务(去某地)
/// 通过UI交互选择目标站点,创建移动任务(起点和终点相同,只移动不放货)
/// </summary>
[MethodMember(Name = "去某地", Description = "去某地")]
public async void ManualEnqueueGo()
{
try
{
G.pushStatus("选择前往的站点");
// 等待用户在UI中选择目标站点
var pt1 = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var srcSite = SimpleLib.GetSite(pt1.site);
// 创建移动任务:起点和终点相同(只移动到目标点,不执行取放货操作)
var d = new TransportDelivery
{
CarType = "Car",
Src = pt1.site, // 起点(实际不会取货)
Dst = pt1.site, // 终点(实际不会放货)
TaskId = $"ManualTask-{DateTime.Now}",
SrcFindLoop = false, // 起点不查找回路
DstFindLoop = false // 终点不查找回路
};
G.pushStatus($"排序了一个叉车搬运任务{d.Id}: {srcSite.id} -> {srcSite.id}");
Enqueue(d);
}
catch
{
G.pushStatus($"结束任务链");
}
}
/// <summary>
/// 供按钮盒进程反射调用:ButtonMission.ExecuteButtonActionInternal 反射调用本方法后,
/// 由 HandleMethodResult 解析返回值,驱动 OnActionExecuted 成功/失败反馈。
/// 返回 true 表示已入队;false 表示站点无效。若 Enqueue 抛错,由按钮进程捕获后同样视为失败。
/// 按钮配置示例:TriggerMission=TransportMissionTriggerMethod=EnqueueTransportByButtonTriggerMethodParams=取货站点ID,放货站点ID
/// </summary>
/// <param name="srcSiteId">取货站点 ID</param>
/// <param name="dstSiteId">放货站点 ID</param>
public bool EnqueueTransportByButton(int srcSiteId, int dstSiteId)
{
if (SimpleLib.GetSite(srcSiteId) == null || SimpleLib.GetSite(dstSiteId) == null)
{
Diagnosis.Log($"按钮排队搬运失败:站点不存在 src={srcSiteId}, dst={dstSiteId}", "TransportMission", true);
return false;
}
var d = new TransportDelivery
{
CarType = "Car",
Src = srcSiteId,
Dst = dstSiteId,
TaskId = $"Button-{DateTime.Now:yyyyMMddHHmmssfff}"
};
Enqueue(d);
Diagnosis.Post($"按钮排队搬运 {d.Id}: {srcSiteId} -> {dstSiteId}");
return true;
}
// ========== 控制台窗口显示控制 ==========
/// <summary>Windows API:获取控制台窗口句柄</summary>
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
/// <summary>Windows API:显示/隐藏窗口</summary>
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
/// <summary>控制台显示状态标志</summary>
private static bool ShowConsole = false;
/// <summary>
/// 切换控制台窗口显示/隐藏
/// </summary>
public static void ShowSimpleConsole()
{
ShowConsole = !ShowConsole; // 切换显示状态
var handle = GetConsoleWindow();
int n = ShowConsole ? 0 : 5; // 0=显示,5=隐藏
Console.WriteLine(n);
ShowWindow(handle, n);
}
/// <summary>
/// 隐藏控制台窗口
/// </summary>
public static void HideSimpleConsole()
{
var handle = GetConsoleWindow();
ShowWindow(handle, 0); // 0表示隐藏窗口
}
}
}
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
using System;
using System.ComponentModel;
namespace StandardScene.Charge
{
/// <summary>
/// 报警配置数据模型
/// </summary>
public class AlarmConfig
{
/// <summary>
/// 报警编号(自动生成)
/// </summary>
[DisplayName("编号")]
public string AlarmId { get; set; }
/// <summary>
/// 报警编码值
/// </summary>
[DisplayName("报警编码")]
public int AlarmCode { get; set; }
/// <summary>
/// 报警内容描述
/// </summary>
[DisplayName("报警内容")]
public string AlarmContent { get; set; }
/// <summary>
/// 报警级别
/// </summary>
[DisplayName("报警级别")]
public AlarmLevel Level { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[DisplayName("启用")]
public bool Enabled { get; set; }
/// <summary>
/// 备注
/// </summary>
[DisplayName("备注")]
public string Remarks { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[DisplayName("创建时间")]
public DateTime CreatedTime { get; set; }
/// <summary>
/// 最后修改时间
/// </summary>
[DisplayName("修改时间")]
public DateTime ModifiedTime { get; set; }
public AlarmConfig()
{
AlarmId = GenerateAlarmId();
Level = AlarmLevel.Medium;
Enabled = true;
CreatedTime = DateTime.Now;
ModifiedTime = DateTime.Now;
}
/// <summary>
/// 生成报警编号
/// </summary>
private static string GenerateAlarmId()
{
return $"ALM{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(100, 999)}";
}
/// <summary>
/// 验证数据有效性
/// </summary>
public bool IsValid(out string errorMessage)
{
if (string.IsNullOrWhiteSpace(AlarmId))
{
errorMessage = "报警编号不能为空";
return false;
}
if (AlarmCode < 0)
{
errorMessage = "报警编码不能为负数";
return false;
}
if (string.IsNullOrWhiteSpace(AlarmContent))
{
errorMessage = "报警内容不能为空";
return false;
}
errorMessage = string.Empty;
return true;
}
public override string ToString()
{
return $"[{AlarmCode}] {AlarmContent}";
}
}
}
@@ -0,0 +1,310 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace StandardScene.Charge
{
/// <summary>
/// 报警配置数据服务(单例模式)
/// </summary>
public class AlarmConfigDataService
{
private static AlarmConfigDataService _instance;
private static readonly object _lock = new object();
private List<AlarmConfig> _alarmConfigs;
private readonly string _dataFilePath;
private AlarmConfigDataService()
{
_dataFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config", "AlarmConfigs.json");
LoadData();
}
/// <summary>
/// 获取单例实例
/// </summary>
public static AlarmConfigDataService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new AlarmConfigDataService();
}
}
}
return _instance;
}
}
/// <summary>
/// 从文件加载数据
/// </summary>
private void LoadData()
{
try
{
// 确保数据目录存在
var directory = Path.GetDirectoryName(_dataFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
if (File.Exists(_dataFilePath))
{
var json = File.ReadAllText(_dataFilePath);
if (!string.IsNullOrWhiteSpace(json))
{
_alarmConfigs = JsonConvert.DeserializeObject<List<AlarmConfig>>(json);
}
// 如果反序列化失败或为null,创建新列表
if (_alarmConfigs == null)
{
_alarmConfigs = new List<AlarmConfig>();
}
}
else
{
_alarmConfigs = new List<AlarmConfig>();
InitializeDefaultAlarms();
SaveData();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"加载报警配置数据失败: {ex.Message}");
_alarmConfigs = new List<AlarmConfig>();
InitializeDefaultAlarms();
}
}
/// <summary>
/// 初始化默认报警配置
/// </summary>
private void InitializeDefaultAlarms()
{
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 1001,
// AlarmContent = "电压过高",
// Level = AlarmLevel.High,
// Enabled = true,
// Remarks = "电压超过额定值10%"
//});
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 1002,
// AlarmContent = "电压过低",
// Level = AlarmLevel.High,
// Enabled = true,
// Remarks = "电压低于额定值10%"
//});
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 1003,
// AlarmContent = "电流过大",
// Level = AlarmLevel.Critical,
// Enabled = true,
// Remarks = "电流超过额定值"
//});
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 2001,
// AlarmContent = "温度异常",
// Level = AlarmLevel.High,
// Enabled = true,
// Remarks = "温度超过安全范围"
//});
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 3001,
// AlarmContent = "通讯超时",
// Level = AlarmLevel.Medium,
// Enabled = true,
// Remarks = "通讯响应时间超过阈值"
//});
//_alarmConfigs.Add(new AlarmConfig
//{
// AlarmCode = 3002,
// AlarmContent = "连接断开",
// Level = AlarmLevel.Critical,
// Enabled = true,
// Remarks = "网络连接中断"
//});
}
/// <summary>
/// 保存数据到文件
/// </summary>
private void SaveData()
{
try
{
var directory = Path.GetDirectoryName(_dataFilePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonConvert.SerializeObject(_alarmConfigs, Formatting.Indented);
File.WriteAllText(_dataFilePath, json);
}
catch (Exception ex)
{
throw new Exception($"保存数据失败: {ex.Message}");
}
}
/// <summary>
/// 获取所有报警配置
/// </summary>
public List<AlarmConfig> GetAllAlarmConfigs()
{
lock (_lock)
{
if (_alarmConfigs == null)
{
_alarmConfigs = new List<AlarmConfig>();
}
return new List<AlarmConfig>(_alarmConfigs);
}
}
/// <summary>
/// 根据编号获取报警配置
/// </summary>
public AlarmConfig GetAlarmConfig(string alarmId)
{
lock (_lock)
{
return _alarmConfigs.FirstOrDefault(a => a.AlarmId == alarmId);
}
}
/// <summary>
/// 根据编号获取报警配置
/// </summary>
public AlarmConfig GetAlarmConfigAlarmCode(int alarmCode)
{
lock (_lock)
{
return _alarmConfigs.FirstOrDefault(a => a.AlarmCode == alarmCode);
}
}
/// <summary>
/// 根据报警编码获取报警配置
/// </summary>
public AlarmConfig GetAlarmConfigByCode(int alarmCode)
{
lock (_lock)
{
return _alarmConfigs.FirstOrDefault(a => a.AlarmCode == alarmCode);
}
}
/// <summary>
/// 添加报警配置
/// </summary>
public bool AddAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)
{
lock (_lock)
{
if (!alarmConfig.IsValid(out errorMessage))
{
return false;
}
// 检查编码是否已存在
if (_alarmConfigs.Any(a => a.AlarmCode == alarmConfig.AlarmCode))
{
errorMessage = $"报警编码 {alarmConfig.AlarmCode} 已存在";
return false;
}
_alarmConfigs.Add(alarmConfig);
SaveData();
errorMessage = string.Empty;
return true;
}
}
/// <summary>
/// 更新报警配置
/// </summary>
public bool UpdateAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)
{
lock (_lock)
{
if (!alarmConfig.IsValid(out errorMessage))
{
return false;
}
var index = _alarmConfigs.FindIndex(a => a.AlarmId == alarmConfig.AlarmId);
if (index == -1)
{
errorMessage = "报警配置不存在";
return false;
}
// 检查编码是否与其他配置冲突
if (_alarmConfigs.Any(a => a.AlarmId != alarmConfig.AlarmId && a.AlarmCode == alarmConfig.AlarmCode))
{
errorMessage = $"报警编码 {alarmConfig.AlarmCode} 已被其他配置使用";
return false;
}
alarmConfig.ModifiedTime = DateTime.Now;
_alarmConfigs[index] = alarmConfig;
SaveData();
errorMessage = string.Empty;
return true;
}
}
/// <summary>
/// 删除报警配置
/// </summary>
public bool DeleteAlarmConfig(string alarmId, out string errorMessage)
{
lock (_lock)
{
var alarmConfig = _alarmConfigs.FirstOrDefault(a => a.AlarmId == alarmId);
if (alarmConfig == null)
{
errorMessage = "报警配置不存在";
return false;
}
_alarmConfigs.Remove(alarmConfig);
SaveData();
errorMessage = string.Empty;
return true;
}
}
/// <summary>
/// 重新加载数据
/// </summary>
public void Reload()
{
lock (_lock)
{
LoadData();
}
}
}
}
@@ -0,0 +1,596 @@
namespace StandardScene.Charge
{
partial class AlarmConfigManagementForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle3 = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle4 = new System.Windows.Forms.DataGridViewCellStyle();
this.splitContainer = new System.Windows.Forms.SplitContainer();
this.pnlList = new System.Windows.Forms.Panel();
this.dgvAlarmConfigs = new System.Windows.Forms.DataGridView();
this.pnlListButtons = new System.Windows.Forms.Panel();
this.lblStatistics = new System.Windows.Forms.Label();
this.btnClose = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.pnlSearch = new System.Windows.Forms.Panel();
this.cmbLevelFilter = new System.Windows.Forms.ComboBox();
this.lblLevelFilter = new System.Windows.Forms.Label();
this.txtSearch = new System.Windows.Forms.TextBox();
this.lblSearch = new System.Windows.Forms.Label();
this.pnlEdit = new System.Windows.Forms.Panel();
this.grpEditInfo = new System.Windows.Forms.GroupBox();
this.txtRemarks = new System.Windows.Forms.TextBox();
this.lblRemarks = new System.Windows.Forms.Label();
this.chkEnabled = new System.Windows.Forms.CheckBox();
this.cmbLevel = new System.Windows.Forms.ComboBox();
this.lblLevel = new System.Windows.Forms.Label();
this.txtAlarmContent = new System.Windows.Forms.TextBox();
this.lblAlarmContent = new System.Windows.Forms.Label();
this.numAlarmCode = new System.Windows.Forms.NumericUpDown();
this.lblAlarmCode = new System.Windows.Forms.Label();
this.txtAlarmId = new System.Windows.Forms.TextBox();
this.lblAlarmId = new System.Windows.Forms.Label();
this.pnlEditButtons = new System.Windows.Forms.Panel();
this.btnCancel = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
this.btnSave = new System.Windows.Forms.Button();
this.colAlarmId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAlarmCode = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAlarmContent = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colLevel = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colEnabled = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colRemarks = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.Panel2.SuspendLayout();
this.splitContainer.SuspendLayout();
this.pnlList.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvAlarmConfigs)).BeginInit();
this.pnlListButtons.SuspendLayout();
this.pnlSearch.SuspendLayout();
this.pnlEdit.SuspendLayout();
this.grpEditInfo.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numAlarmCode)).BeginInit();
this.pnlEditButtons.SuspendLayout();
this.SuspendLayout();
//
// splitContainer
//
this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer.Location = new System.Drawing.Point(0, 0);
this.splitContainer.Margin = new System.Windows.Forms.Padding(4);
this.splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
this.splitContainer.Panel1.Controls.Add(this.pnlList);
//
// splitContainer.Panel2
//
this.splitContainer.Panel2.Controls.Add(this.pnlEdit);
this.splitContainer.Size = new System.Drawing.Size(1400, 750);
this.splitContainer.SplitterDistance = 900;
this.splitContainer.SplitterWidth = 5;
this.splitContainer.TabIndex = 0;
//
// pnlList
//
this.pnlList.Controls.Add(this.dgvAlarmConfigs);
this.pnlList.Controls.Add(this.pnlListButtons);
this.pnlList.Controls.Add(this.pnlSearch);
this.pnlList.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlList.Location = new System.Drawing.Point(0, 0);
this.pnlList.Margin = new System.Windows.Forms.Padding(4);
this.pnlList.Name = "pnlList";
this.pnlList.Size = new System.Drawing.Size(900, 750);
this.pnlList.TabIndex = 0;
//
// dgvAlarmConfigs
//
this.dgvAlarmConfigs.AllowUserToAddRows = false;
this.dgvAlarmConfigs.AllowUserToDeleteRows = false;
this.dgvAlarmConfigs.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dgvAlarmConfigs.BackgroundColor = System.Drawing.Color.White;
this.dgvAlarmConfigs.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dgvAlarmConfigs.CellBorderStyle = System.Windows.Forms.DataGridViewCellBorderStyle.SingleHorizontal;
dataGridViewCellStyle3.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle3.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(81)))), ((int)(((byte)(181)))));
dataGridViewCellStyle3.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
dataGridViewCellStyle3.ForeColor = System.Drawing.Color.White;
dataGridViewCellStyle3.SelectionBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(81)))), ((int)(((byte)(181)))));
dataGridViewCellStyle3.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle3.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.dgvAlarmConfigs.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle3;
this.dgvAlarmConfigs.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvAlarmConfigs.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colAlarmId,
this.colAlarmCode,
this.colAlarmContent,
this.colLevel,
this.colEnabled,
this.colRemarks});
dataGridViewCellStyle4.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle4.BackColor = System.Drawing.Color.White;
dataGridViewCellStyle4.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
dataGridViewCellStyle4.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64)))));
dataGridViewCellStyle4.SelectionBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(197)))), ((int)(((byte)(202)))), ((int)(((byte)(233)))));
dataGridViewCellStyle4.SelectionForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(33)))), ((int)(((byte)(33)))));
dataGridViewCellStyle4.WrapMode = System.Windows.Forms.DataGridViewTriState.False;
this.dgvAlarmConfigs.DefaultCellStyle = dataGridViewCellStyle4;
this.dgvAlarmConfigs.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvAlarmConfigs.EnableHeadersVisualStyles = false;
this.dgvAlarmConfigs.GridColor = System.Drawing.Color.FromArgb(((int)(((byte)(224)))), ((int)(((byte)(224)))), ((int)(((byte)(224)))));
this.dgvAlarmConfigs.Location = new System.Drawing.Point(0, 62);
this.dgvAlarmConfigs.Margin = new System.Windows.Forms.Padding(4);
this.dgvAlarmConfigs.MultiSelect = false;
this.dgvAlarmConfigs.Name = "dgvAlarmConfigs";
this.dgvAlarmConfigs.ReadOnly = true;
this.dgvAlarmConfigs.RowHeadersVisible = false;
this.dgvAlarmConfigs.RowHeadersWidth = 30;
this.dgvAlarmConfigs.RowTemplate.Height = 35;
this.dgvAlarmConfigs.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvAlarmConfigs.Size = new System.Drawing.Size(900, 600);
this.dgvAlarmConfigs.TabIndex = 2;
this.dgvAlarmConfigs.CellDoubleClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.dgvAlarmConfigs_CellDoubleClick);
//
// pnlListButtons
//
this.pnlListButtons.Controls.Add(this.lblStatistics);
this.pnlListButtons.Controls.Add(this.btnClose);
this.pnlListButtons.Controls.Add(this.btnRefresh);
this.pnlListButtons.Dock = System.Windows.Forms.DockStyle.Bottom;
this.pnlListButtons.Location = new System.Drawing.Point(0, 662);
this.pnlListButtons.Margin = new System.Windows.Forms.Padding(4);
this.pnlListButtons.Name = "pnlListButtons";
this.pnlListButtons.Padding = new System.Windows.Forms.Padding(13, 12, 13, 12);
this.pnlListButtons.Size = new System.Drawing.Size(900, 88);
this.pnlListButtons.TabIndex = 1;
//
// lblStatistics
//
this.lblStatistics.AutoSize = true;
this.lblStatistics.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblStatistics.Location = new System.Drawing.Point(20, 31);
this.lblStatistics.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblStatistics.Name = "lblStatistics";
this.lblStatistics.Size = new System.Drawing.Size(204, 24);
this.lblStatistics.TabIndex = 2;
this.lblStatistics.Text = "总数: 0 | 启用: 0 | 禁用: 0";
//
// btnClose
//
this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnClose.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnClose.Location = new System.Drawing.Point(753, 19);
this.btnClose.Margin = new System.Windows.Forms.Padding(4);
this.btnClose.Name = "btnClose";
this.btnClose.Size = new System.Drawing.Size(120, 50);
this.btnClose.TabIndex = 1;
this.btnClose.Text = "关闭";
this.btnClose.UseVisualStyleBackColor = true;
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// btnRefresh
//
this.btnRefresh.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnRefresh.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnRefresh.Location = new System.Drawing.Point(620, 19);
this.btnRefresh.Margin = new System.Windows.Forms.Padding(4);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(120, 50);
this.btnRefresh.TabIndex = 0;
this.btnRefresh.Text = "刷新";
this.btnRefresh.UseVisualStyleBackColor = true;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// pnlSearch
//
this.pnlSearch.Controls.Add(this.cmbLevelFilter);
this.pnlSearch.Controls.Add(this.lblLevelFilter);
this.pnlSearch.Controls.Add(this.txtSearch);
this.pnlSearch.Controls.Add(this.lblSearch);
this.pnlSearch.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlSearch.Location = new System.Drawing.Point(0, 0);
this.pnlSearch.Margin = new System.Windows.Forms.Padding(4);
this.pnlSearch.Name = "pnlSearch";
this.pnlSearch.Padding = new System.Windows.Forms.Padding(13, 12, 13, 12);
this.pnlSearch.Size = new System.Drawing.Size(900, 62);
this.pnlSearch.TabIndex = 0;
//
// cmbLevelFilter
//
this.cmbLevelFilter.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbLevelFilter.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.cmbLevelFilter.FormattingEnabled = true;
this.cmbLevelFilter.Location = new System.Drawing.Point(550, 16);
this.cmbLevelFilter.Margin = new System.Windows.Forms.Padding(4);
this.cmbLevelFilter.Name = "cmbLevelFilter";
this.cmbLevelFilter.Size = new System.Drawing.Size(150, 31);
this.cmbLevelFilter.TabIndex = 3;
this.cmbLevelFilter.SelectedIndexChanged += new System.EventHandler(this.cmbLevelFilter_SelectedIndexChanged);
//
// lblLevelFilter
//
this.lblLevelFilter.AutoSize = true;
this.lblLevelFilter.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblLevelFilter.Location = new System.Drawing.Point(463, 21);
this.lblLevelFilter.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblLevelFilter.Name = "lblLevelFilter";
this.lblLevelFilter.Size = new System.Drawing.Size(61, 23);
this.lblLevelFilter.TabIndex = 2;
this.lblLevelFilter.Text = "级别:";
//
// txtSearch
//
this.txtSearch.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.txtSearch.Location = new System.Drawing.Point(100, 16);
this.txtSearch.Margin = new System.Windows.Forms.Padding(4);
this.txtSearch.Name = "txtSearch";
this.txtSearch.Size = new System.Drawing.Size(300, 29);
this.txtSearch.TabIndex = 1;
this.txtSearch.TextChanged += new System.EventHandler(this.txtSearch_TextChanged);
//
// lblSearch
//
this.lblSearch.AutoSize = true;
this.lblSearch.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblSearch.Location = new System.Drawing.Point(13, 21);
this.lblSearch.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblSearch.Name = "lblSearch";
this.lblSearch.Size = new System.Drawing.Size(61, 23);
this.lblSearch.TabIndex = 0;
this.lblSearch.Text = "搜索:";
//
// pnlEdit
//
this.pnlEdit.Controls.Add(this.grpEditInfo);
this.pnlEdit.Controls.Add(this.pnlEditButtons);
this.pnlEdit.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlEdit.Location = new System.Drawing.Point(0, 0);
this.pnlEdit.Margin = new System.Windows.Forms.Padding(4);
this.pnlEdit.Name = "pnlEdit";
this.pnlEdit.Size = new System.Drawing.Size(495, 750);
this.pnlEdit.TabIndex = 0;
//
// grpEditInfo
//
this.grpEditInfo.Controls.Add(this.txtRemarks);
this.grpEditInfo.Controls.Add(this.lblRemarks);
this.grpEditInfo.Controls.Add(this.chkEnabled);
this.grpEditInfo.Controls.Add(this.cmbLevel);
this.grpEditInfo.Controls.Add(this.lblLevel);
this.grpEditInfo.Controls.Add(this.txtAlarmContent);
this.grpEditInfo.Controls.Add(this.lblAlarmContent);
this.grpEditInfo.Controls.Add(this.numAlarmCode);
this.grpEditInfo.Controls.Add(this.lblAlarmCode);
this.grpEditInfo.Controls.Add(this.txtAlarmId);
this.grpEditInfo.Controls.Add(this.lblAlarmId);
this.grpEditInfo.Dock = System.Windows.Forms.DockStyle.Fill;
this.grpEditInfo.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.grpEditInfo.Location = new System.Drawing.Point(0, 0);
this.grpEditInfo.Margin = new System.Windows.Forms.Padding(4);
this.grpEditInfo.Name = "grpEditInfo";
this.grpEditInfo.Padding = new System.Windows.Forms.Padding(20, 19, 20, 19);
this.grpEditInfo.Size = new System.Drawing.Size(495, 625);
this.grpEditInfo.TabIndex = 1;
this.grpEditInfo.TabStop = false;
this.grpEditInfo.Text = "报警配置信息";
//
// txtRemarks
//
this.txtRemarks.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.txtRemarks.Location = new System.Drawing.Point(130, 350);
this.txtRemarks.Margin = new System.Windows.Forms.Padding(4);
this.txtRemarks.Multiline = true;
this.txtRemarks.Name = "txtRemarks";
this.txtRemarks.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtRemarks.Size = new System.Drawing.Size(330, 80);
this.txtRemarks.TabIndex = 10;
//
// lblRemarks
//
this.lblRemarks.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblRemarks.Location = new System.Drawing.Point(27, 350);
this.lblRemarks.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblRemarks.Name = "lblRemarks";
this.lblRemarks.Size = new System.Drawing.Size(100, 31);
this.lblRemarks.TabIndex = 9;
this.lblRemarks.Text = "备注:";
this.lblRemarks.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// chkEnabled
//
this.chkEnabled.AutoSize = true;
this.chkEnabled.Checked = true;
this.chkEnabled.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkEnabled.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.chkEnabled.Location = new System.Drawing.Point(130, 300);
this.chkEnabled.Margin = new System.Windows.Forms.Padding(4);
this.chkEnabled.Name = "chkEnabled";
this.chkEnabled.Size = new System.Drawing.Size(83, 27);
this.chkEnabled.TabIndex = 8;
this.chkEnabled.Text = "启用中";
this.chkEnabled.UseVisualStyleBackColor = true;
this.chkEnabled.Visible = false;
//
// cmbLevel
//
this.cmbLevel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbLevel.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.cmbLevel.FormattingEnabled = true;
this.cmbLevel.Location = new System.Drawing.Point(130, 244);
this.cmbLevel.Margin = new System.Windows.Forms.Padding(4);
this.cmbLevel.Name = "cmbLevel";
this.cmbLevel.Size = new System.Drawing.Size(330, 31);
this.cmbLevel.TabIndex = 7;
//
// lblLevel
//
this.lblLevel.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblLevel.Location = new System.Drawing.Point(27, 244);
this.lblLevel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblLevel.Name = "lblLevel";
this.lblLevel.Size = new System.Drawing.Size(100, 31);
this.lblLevel.TabIndex = 6;
this.lblLevel.Text = "报警级别:";
this.lblLevel.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txtAlarmContent
//
this.txtAlarmContent.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.txtAlarmContent.Location = new System.Drawing.Point(130, 181);
this.txtAlarmContent.Margin = new System.Windows.Forms.Padding(4);
this.txtAlarmContent.Multiline = true;
this.txtAlarmContent.Name = "txtAlarmContent";
this.txtAlarmContent.Size = new System.Drawing.Size(330, 50);
this.txtAlarmContent.TabIndex = 5;
//
// lblAlarmContent
//
this.lblAlarmContent.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblAlarmContent.Location = new System.Drawing.Point(27, 181);
this.lblAlarmContent.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblAlarmContent.Name = "lblAlarmContent";
this.lblAlarmContent.Size = new System.Drawing.Size(100, 31);
this.lblAlarmContent.TabIndex = 4;
this.lblAlarmContent.Text = "报警内容:";
this.lblAlarmContent.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// numAlarmCode
//
this.numAlarmCode.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.numAlarmCode.Location = new System.Drawing.Point(130, 119);
this.numAlarmCode.Margin = new System.Windows.Forms.Padding(4);
this.numAlarmCode.Maximum = new decimal(new int[] {
99999,
0,
0,
0});
this.numAlarmCode.Name = "numAlarmCode";
this.numAlarmCode.Size = new System.Drawing.Size(330, 29);
this.numAlarmCode.TabIndex = 3;
//
// lblAlarmCode
//
this.lblAlarmCode.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblAlarmCode.Location = new System.Drawing.Point(27, 119);
this.lblAlarmCode.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblAlarmCode.Name = "lblAlarmCode";
this.lblAlarmCode.Size = new System.Drawing.Size(100, 31);
this.lblAlarmCode.TabIndex = 2;
this.lblAlarmCode.Text = "报警编码:";
this.lblAlarmCode.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txtAlarmId
//
this.txtAlarmId.BackColor = System.Drawing.Color.LightGray;
this.txtAlarmId.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.txtAlarmId.Location = new System.Drawing.Point(130, 56);
this.txtAlarmId.Margin = new System.Windows.Forms.Padding(4);
this.txtAlarmId.Name = "txtAlarmId";
this.txtAlarmId.ReadOnly = true;
this.txtAlarmId.Size = new System.Drawing.Size(330, 27);
this.txtAlarmId.TabIndex = 1;
this.txtAlarmId.Visible = false;
//
// lblAlarmId
//
this.lblAlarmId.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblAlarmId.Location = new System.Drawing.Point(27, 56);
this.lblAlarmId.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.lblAlarmId.Name = "lblAlarmId";
this.lblAlarmId.Size = new System.Drawing.Size(100, 31);
this.lblAlarmId.TabIndex = 0;
this.lblAlarmId.Text = "编号:";
this.lblAlarmId.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
this.lblAlarmId.Visible = false;
//
// pnlEditButtons
//
this.pnlEditButtons.Controls.Add(this.btnCancel);
this.pnlEditButtons.Controls.Add(this.btnDelete);
this.pnlEditButtons.Controls.Add(this.btnSave);
this.pnlEditButtons.Dock = System.Windows.Forms.DockStyle.Bottom;
this.pnlEditButtons.Location = new System.Drawing.Point(0, 625);
this.pnlEditButtons.Margin = new System.Windows.Forms.Padding(4);
this.pnlEditButtons.Name = "pnlEditButtons";
this.pnlEditButtons.Padding = new System.Windows.Forms.Padding(13, 12, 13, 12);
this.pnlEditButtons.Size = new System.Drawing.Size(495, 125);
this.pnlEditButtons.TabIndex = 0;
//
// btnCancel
//
this.btnCancel.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnCancel.Location = new System.Drawing.Point(333, 25);
this.btnCancel.Margin = new System.Windows.Forms.Padding(4);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(133, 62);
this.btnCancel.TabIndex = 2;
this.btnCancel.Text = "取消";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnDelete
//
this.btnDelete.BackColor = System.Drawing.Color.LightCoral;
this.btnDelete.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnDelete.Location = new System.Drawing.Point(180, 25);
this.btnDelete.Margin = new System.Windows.Forms.Padding(4);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(133, 62);
this.btnDelete.TabIndex = 1;
this.btnDelete.Text = "删除";
this.btnDelete.UseVisualStyleBackColor = false;
this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnSave.Location = new System.Drawing.Point(27, 25);
this.btnSave.Margin = new System.Windows.Forms.Padding(4);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(133, 62);
this.btnSave.TabIndex = 0;
this.btnSave.Text = "新增";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// colAlarmId
//
this.colAlarmId.HeaderText = "编号";
this.colAlarmId.MinimumWidth = 6;
this.colAlarmId.Name = "colAlarmId";
this.colAlarmId.ReadOnly = true;
this.colAlarmId.Visible = false;
//
// colAlarmCode
//
this.colAlarmCode.HeaderText = "报警编码";
this.colAlarmCode.MinimumWidth = 6;
this.colAlarmCode.Name = "colAlarmCode";
this.colAlarmCode.ReadOnly = true;
//
// colAlarmContent
//
this.colAlarmContent.HeaderText = "报警内容";
this.colAlarmContent.MinimumWidth = 6;
this.colAlarmContent.Name = "colAlarmContent";
this.colAlarmContent.ReadOnly = true;
//
// colLevel
//
this.colLevel.HeaderText = "级别";
this.colLevel.MinimumWidth = 6;
this.colLevel.Name = "colLevel";
this.colLevel.ReadOnly = true;
//
// colEnabled
//
this.colEnabled.HeaderText = "启用";
this.colEnabled.MinimumWidth = 6;
this.colEnabled.Name = "colEnabled";
this.colEnabled.ReadOnly = true;
//
// colRemarks
//
this.colRemarks.HeaderText = "备注";
this.colRemarks.MinimumWidth = 6;
this.colRemarks.Name = "colRemarks";
this.colRemarks.ReadOnly = true;
//
// AlarmConfigManagementForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1400, 750);
this.Controls.Add(this.splitContainer);
this.Margin = new System.Windows.Forms.Padding(4);
this.MinimumSize = new System.Drawing.Size(1200, 600);
this.Name = "AlarmConfigManagementForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "报警配置管理";
this.splitContainer.Panel1.ResumeLayout(false);
this.splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
this.splitContainer.ResumeLayout(false);
this.pnlList.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dgvAlarmConfigs)).EndInit();
this.pnlListButtons.ResumeLayout(false);
this.pnlListButtons.PerformLayout();
this.pnlSearch.ResumeLayout(false);
this.pnlSearch.PerformLayout();
this.pnlEdit.ResumeLayout(false);
this.grpEditInfo.ResumeLayout(false);
this.grpEditInfo.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numAlarmCode)).EndInit();
this.pnlEditButtons.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.Panel pnlList;
private System.Windows.Forms.DataGridView dgvAlarmConfigs;
private System.Windows.Forms.Panel pnlListButtons;
private System.Windows.Forms.Label lblStatistics;
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Panel pnlSearch;
private System.Windows.Forms.ComboBox cmbLevelFilter;
private System.Windows.Forms.Label lblLevelFilter;
private System.Windows.Forms.TextBox txtSearch;
private System.Windows.Forms.Label lblSearch;
private System.Windows.Forms.Panel pnlEdit;
private System.Windows.Forms.GroupBox grpEditInfo;
private System.Windows.Forms.TextBox txtRemarks;
private System.Windows.Forms.Label lblRemarks;
private System.Windows.Forms.CheckBox chkEnabled;
private System.Windows.Forms.ComboBox cmbLevel;
private System.Windows.Forms.Label lblLevel;
private System.Windows.Forms.TextBox txtAlarmContent;
private System.Windows.Forms.Label lblAlarmContent;
private System.Windows.Forms.NumericUpDown numAlarmCode;
private System.Windows.Forms.Label lblAlarmCode;
private System.Windows.Forms.TextBox txtAlarmId;
private System.Windows.Forms.Label lblAlarmId;
private System.Windows.Forms.Panel pnlEditButtons;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Button btnDelete;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.DataGridViewTextBoxColumn colAlarmId;
private System.Windows.Forms.DataGridViewTextBoxColumn colAlarmCode;
private System.Windows.Forms.DataGridViewTextBoxColumn colAlarmContent;
private System.Windows.Forms.DataGridViewTextBoxColumn colLevel;
private System.Windows.Forms.DataGridViewTextBoxColumn colEnabled;
private System.Windows.Forms.DataGridViewTextBoxColumn colRemarks;
}
}
@@ -0,0 +1,547 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace StandardScene.Charge
{
/// <summary>
/// 报警配置管理窗体
/// </summary>
public partial class AlarmConfigManagementForm : Form
{
private readonly AlarmConfigDataService dataService;
private AlarmConfig selectedAlarmConfig;
public AlarmConfigManagementForm()
{
try
{
InitializeComponent();
dataService = AlarmConfigDataService.Instance;
// 订阅Load事件,确保所有控件都已初始化后再加载数据
this.Load += AlarmConfigManagementForm_Load;
}
catch (Exception ex)
{
MessageBox.Show($"初始化报警配置管理窗体失败: {ex.Message}\n\n详细信息:\n{ex.StackTrace}",
"错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 窗体加载事件
/// </summary>
private void AlarmConfigManagementForm_Load(object sender, EventArgs e)
{
InitializeForm();
}
/// <summary>
/// 初始化窗体
/// </summary>
private void InitializeForm()
{
try
{
// 初始化报警级别下拉框
if (cmbLevel != null)
{
cmbLevel.Items.Clear();
cmbLevel.Items.Add("无");
cmbLevel.Items.Add("低");
cmbLevel.Items.Add("中");
cmbLevel.Items.Add("高");
cmbLevel.Items.Add("严重");
cmbLevel.SelectedIndex = 2; // 默认选择"中"
}
// 初始化级别筛选下拉框
if (cmbLevelFilter != null)
{
cmbLevelFilter.Items.Clear();
cmbLevelFilter.Items.Add("全部");
cmbLevelFilter.Items.Add("无");
cmbLevelFilter.Items.Add("低");
cmbLevelFilter.Items.Add("中");
cmbLevelFilter.Items.Add("高");
cmbLevelFilter.Items.Add("严重");
cmbLevelFilter.SelectedIndex = 0;
}
LoadAlarmConfigs();
ClearEditFields();
}
catch (Exception ex)
{
MessageBox.Show($"初始化窗体失败: {ex.Message}\n\n{ex.StackTrace}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 加载报警配置列表
/// </summary>
private void LoadAlarmConfigs()
{
try
{
if (dgvAlarmConfigs == null)
{
return; // 控件还未初始化,直接返回
}
var alarmConfigs = dataService.GetAllAlarmConfigs();
if (alarmConfigs == null)
{
alarmConfigs = new System.Collections.Generic.List<AlarmConfig>();
}
// 根据级别筛选
if (cmbLevelFilter != null && cmbLevelFilter.SelectedIndex > 0)
{
var filterLevel = (AlarmLevel)(cmbLevelFilter.SelectedIndex - 1);
alarmConfigs = alarmConfigs.Where(a => a.Level == filterLevel).ToList();
}
// 根据搜索文本筛选
if (txtSearch != null && !string.IsNullOrWhiteSpace(txtSearch.Text))
{
var searchText = txtSearch.Text.Trim().ToLower();
alarmConfigs = alarmConfigs.Where(a =>
a.AlarmId.ToLower().Contains(searchText) ||
a.AlarmCode.ToString().Contains(searchText) ||
a.AlarmContent.ToLower().Contains(searchText)
).ToList();
}
dgvAlarmConfigs.Rows.Clear();
foreach (var alarm in alarmConfigs)
{
var index = dgvAlarmConfigs.Rows.Add(
alarm.AlarmId,
alarm.AlarmCode,
alarm.AlarmContent,
GetLevelText(alarm.Level),
alarm.Enabled ? "是" : "否",
alarm.Remarks
);
// 根据级别设置行颜色
var row = dgvAlarmConfigs.Rows[index];
switch (alarm.Level)
{
case AlarmLevel.Critical:
row.DefaultCellStyle.BackColor = Color.FromArgb(255, 235, 238); // 浅红色
row.DefaultCellStyle.ForeColor = Color.FromArgb(183, 28, 28);
// 安全地创建粗体字体
var baseFont = row.DefaultCellStyle.Font ?? dgvAlarmConfigs.DefaultCellStyle.Font ?? new Font("微软雅黑", 9F);
row.DefaultCellStyle.Font = new Font(baseFont, FontStyle.Bold);
break;
case AlarmLevel.High:
row.DefaultCellStyle.BackColor = Color.FromArgb(255, 243, 224); // 浅橙色
row.DefaultCellStyle.ForeColor = Color.FromArgb(230, 81, 0);
break;
case AlarmLevel.Medium:
row.DefaultCellStyle.BackColor = Color.FromArgb(255, 249, 196); // 浅黄色
row.DefaultCellStyle.ForeColor = Color.FromArgb(245, 127, 23);
break;
case AlarmLevel.Low:
row.DefaultCellStyle.BackColor = Color.FromArgb(232, 245, 233); // 浅绿色
row.DefaultCellStyle.ForeColor = Color.FromArgb(46, 125, 50);
break;
}
// 如果未启用,显示为灰色
if (!alarm.Enabled)
{
row.DefaultCellStyle.BackColor = Color.FromArgb(238, 238, 238);
row.DefaultCellStyle.ForeColor = Color.FromArgb(158, 158, 158);
}
}
UpdateStatistics();
UpdateTitleWithFilter(alarmConfigs.Count);
}
catch (Exception ex)
{
MessageBox.Show($"加载数据失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 更新统计信息
/// </summary>
private void UpdateStatistics()
{
try
{
if (lblStatistics == null)
{
return;
}
var alarmConfigs = dataService.GetAllAlarmConfigs();
if (alarmConfigs == null)
{
alarmConfigs = new System.Collections.Generic.List<AlarmConfig>();
}
var total = alarmConfigs.Count;
var enabled = alarmConfigs.Count(a => a.Enabled);
var disabled = total - enabled;
var critical = alarmConfigs.Count(a => a.Level == AlarmLevel.Critical);
var high = alarmConfigs.Count(a => a.Level == AlarmLevel.High);
lblStatistics.Text = $"总数: {total} | 启用: {enabled} | 禁用: {disabled} | 严重: {critical} | 高级: {high}";
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新统计信息失败: {ex.Message}");
}
}
/// <summary>
/// 更新标题显示筛选信息
/// </summary>
private void UpdateTitleWithFilter(int displayCount)
{
try
{
var allConfigs = dataService.GetAllAlarmConfigs();
var totalCount = allConfigs != null ? allConfigs.Count : 0;
if (cmbLevelFilter != null && cmbLevelFilter.SelectedIndex > 0)
{
this.Text = $"报警配置管理 - 显示: {displayCount}/{totalCount} ({cmbLevelFilter.Text})";
}
else
{
this.Text = $"报警配置管理 - 总数: {totalCount}";
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新标题失败: {ex.Message}");
this.Text = "报警配置管理";
}
}
/// <summary>
/// 获取级别文本
/// </summary>
private string GetLevelText(AlarmLevel level)
{
switch (level)
{
case AlarmLevel.None: return "无";
case AlarmLevel.Low: return "低";
case AlarmLevel.Medium: return "中";
case AlarmLevel.High: return "高";
case AlarmLevel.Critical: return "严重";
default: return "未知";
}
}
/// <summary>
/// 清空编辑字段
/// </summary>
private void ClearEditFields()
{
try
{
selectedAlarmConfig = null;
if (txtAlarmId != null)
{
txtAlarmId.Text = "";
txtAlarmId.Enabled = false; // 新增时编号自动生成
}
if (numAlarmCode != null)
{
numAlarmCode.Value = 0;
numAlarmCode.Enabled = true;
numAlarmCode.ReadOnly = false;
}
if (txtAlarmContent != null)
{
txtAlarmContent.Text = "";
txtAlarmContent.Enabled = true;
txtAlarmContent.ReadOnly = false;
}
if (cmbLevel != null)
{
cmbLevel.SelectedIndex = 2; // 中
cmbLevel.Enabled = true;
}
if (chkEnabled != null)
{
chkEnabled.Checked = true;
chkEnabled.Enabled = true;
}
if (txtRemarks != null)
{
txtRemarks.Text = "";
txtRemarks.Enabled = true;
txtRemarks.ReadOnly = false;
}
if (btnSave != null)
{
btnSave.Text = "新增";
btnSave.Enabled = true;
}
if (btnDelete != null)
{
btnDelete.Enabled = false;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"清空编辑字段失败: {ex.Message}");
}
}
/// <summary>
/// 从字段创建报警配置
/// </summary>
private AlarmConfig CreateAlarmConfigFromFields()
{
var alarmConfig = selectedAlarmConfig ?? new AlarmConfig();
alarmConfig.AlarmCode = (int)numAlarmCode.Value;
alarmConfig.AlarmContent = txtAlarmContent.Text.Trim();
alarmConfig.Level = (AlarmLevel)cmbLevel.SelectedIndex;
alarmConfig.Enabled = chkEnabled.Checked;
alarmConfig.Remarks = txtRemarks.Text.Trim();
return alarmConfig;
}
/// <summary>
/// 加载报警配置到编辑区
/// </summary>
private void LoadAlarmConfigToFields(AlarmConfig alarmConfig)
{
try
{
selectedAlarmConfig = alarmConfig;
// 填充数据
if (txtAlarmId != null)
{
txtAlarmId.Text = alarmConfig.AlarmId;
txtAlarmId.Enabled = false; // 编号不可修改
}
if (numAlarmCode != null)
{
numAlarmCode.Value = alarmConfig.AlarmCode;
numAlarmCode.Enabled = true;
numAlarmCode.ReadOnly = false;
}
if (txtAlarmContent != null)
{
txtAlarmContent.Text = alarmConfig.AlarmContent;
txtAlarmContent.Enabled = true;
txtAlarmContent.ReadOnly = false;
}
if (cmbLevel != null)
{
cmbLevel.SelectedIndex = (int)alarmConfig.Level;
cmbLevel.Enabled = true;
}
if (chkEnabled != null)
{
chkEnabled.Checked = alarmConfig.Enabled;
chkEnabled.Enabled = true;
}
if (txtRemarks != null)
{
txtRemarks.Text = alarmConfig.Remarks ?? "";
txtRemarks.Enabled = true;
txtRemarks.ReadOnly = false;
}
// 设置按钮状态
if (btnSave != null)
{
btnSave.Text = "保存";
btnSave.Enabled = true;
}
if (btnDelete != null)
{
btnDelete.Enabled = true;
}
}
catch (Exception ex)
{
MessageBox.Show($"加载数据到编辑区失败: {ex.Message}\n\n{ex.StackTrace}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ==================== 事件处理 ====================
private void btnSave_Click(object sender, EventArgs e)
{
try
{
// 验证报警编码
if (numAlarmCode.Value < 0)
{
MessageBox.Show("报警编码不能为负数", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
numAlarmCode.Focus();
return;
}
// 验证报警内容
if (string.IsNullOrWhiteSpace(txtAlarmContent.Text))
{
MessageBox.Show("报警内容不能为空", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
txtAlarmContent.Focus();
return;
}
var alarmConfig = CreateAlarmConfigFromFields();
string errorMessage;
bool success;
if (selectedAlarmConfig == null)
{
// 新增
success = dataService.AddAlarmConfig(alarmConfig, out errorMessage);
}
else
{
// 更新
success = dataService.UpdateAlarmConfig(alarmConfig, out errorMessage);
}
if (success)
{
MessageBox.Show("保存成功!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
LoadAlarmConfigs();
ClearEditFields();
}
else
{
MessageBox.Show($"保存失败: {errorMessage}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnDelete_Click(object sender, EventArgs e)
{
if (selectedAlarmConfig == null)
{
MessageBox.Show("请先选择要删除的报警配置", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var result = MessageBox.Show(
$"确定要删除报警配置 [{selectedAlarmConfig.AlarmCode}] {selectedAlarmConfig.AlarmContent} 吗?",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
if (dataService.DeleteAlarmConfig(selectedAlarmConfig.AlarmId, out string errorMessage))
{
MessageBox.Show("删除成功!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
LoadAlarmConfigs();
ClearEditFields();
}
else
{
MessageBox.Show($"删除失败: {errorMessage}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
ClearEditFields();
}
private void btnRefresh_Click(object sender, EventArgs e)
{
dataService.Reload();
LoadAlarmConfigs();
}
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
private void dgvAlarmConfigs_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
try
{
if (e.RowIndex >= 0 && e.RowIndex < dgvAlarmConfigs.Rows.Count)
{
var row = dgvAlarmConfigs.Rows[e.RowIndex];
if (row.Cells[0].Value != null)
{
var alarmId = row.Cells[1].Value.ToString();
var alarmConfig = dataService.GetAlarmConfigAlarmCode(int.Parse(alarmId));
if (alarmConfig != null)
{
LoadAlarmConfigToFields(alarmConfig);
}
else
{
MessageBox.Show($"未找到报警配置: {alarmId}", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
}
catch (Exception ex)
{
MessageBox.Show($"加载报警配置失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void txtSearch_TextChanged(object sender, EventArgs e)
{
LoadAlarmConfigs();
}
private void cmbLevelFilter_SelectedIndexChanged(object sender, EventArgs e)
{
LoadAlarmConfigs();
}
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+429
View File
@@ -0,0 +1,429 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json;
namespace StandardScene.Charge
{
/// <summary>
/// 充电桩数据模型
/// </summary>
public class ChargeStation
{
/// <summary>
/// 充电桩编号(唯一标识)
/// </summary>
[DisplayName("编号")]
public string StationId { get; set; } = "1";
/// <summary>
/// 充电桩名称
/// </summary>
[DisplayName("名称")]
public string Name { get; set; }
/// <summary>
/// 充电桩类型
/// </summary>
[DisplayName("类型")]
public ChargeStationType Type { get; set; }
/// <summary>
/// 充电方式
/// </summary>
[DisplayName("充电方式")]
public ChargeMethodType ChargeMethod { get; set; }
/// <summary>
/// IP地址
/// </summary>
[DisplayName("IP地址")]
public string IpAddress { get; set; }
/// <summary>
/// 端口号
/// </summary>
[DisplayName("端口")]
public int Port { get; set; }
/// <summary>
/// 通讯类型 (UDP/TCP)
/// </summary>
[DisplayName("通讯类型")]
public string CommunicationType { get; set; } = "TCP";
/// <summary>
/// 额定电压 (V)
/// </summary>
[DisplayName("电压(V)")]
public double SetVoltage { get; set; }
/// <summary>
/// 额定电流 (A)
/// </summary>
[DisplayName("电流(A)")]
public double SetElectricCurrent { get; set; }
/// <summary>
/// 实时电压 (V) - 当前充电时的实际电压
/// </summary>
[DisplayName("实时电压(V)")]
[JsonIgnore]
public double RealTimeVoltage { get; set; }
/// <summary>
/// 实时电流 (A) - 当前充电时的实际电流
/// </summary>
[DisplayName("实时电流(A)")]
[JsonIgnore]
public double RealTimeCurrent { get; set; }
/// <summary>
/// 最后发送数据时间
/// </summary>
[DisplayName("发送时间")]
[JsonIgnore]
public DateTime? LastSendTime { get; set; }
/// <summary>
/// 最后接收数据时间
/// </summary>
[DisplayName("接收时间")]
[JsonIgnore]
public DateTime? LastReceiveTime { get; set; }
/// <summary>
/// 是否有报警
/// </summary>
[DisplayName("报警")]
[JsonIgnore]
public bool HasAlarm { get; set; }
/// <summary>
/// 报警信息
/// </summary>
[DisplayName("报警信息")]
[JsonIgnore]
public string AlarmMessage { get; set; }
/// <summary>
/// 报警级别
/// </summary>
[DisplayName("报警级别")]
public AlarmLevel AlarmLevel { get; set; }
/// <summary>
/// 网络通讯状态
/// </summary>
[DisplayName("通讯状态")]
[JsonIgnore]
public CommunicationStatus CommStatus { get; set; }
/// <summary>
/// 最后通讯成功时间
/// </summary>
[JsonIgnore]
[DisplayName("最后通讯时间")]
public DateTime? LastCommunicationTime { get; set; }
/// <summary>
/// 机构伸缩状态
/// </summary>
[JsonIgnore]
[DisplayName("机构状态")]
public MechanismStatus MechanismStatus { get; set; }
[DisplayName("屏蔽机构状态交互")]
public bool ShieldSiteMechanismStatus { get; set; }
/// <summary>
/// 当前充电车辆编号
/// </summary>
[DisplayName("当前车辆")]
[JsonIgnore]
public string CurrentVehicle { get; set; }
/// <summary>
/// 当前电量百分比 (0-100)
/// </summary>
[DisplayName("电量")]
[JsonIgnore]
public double BatteryLevel { get; set; }
/// <summary>
/// 发送充电的状态
/// </summary>
[DisplayName("充电指令状态")]
[JsonIgnore]
public ChargeCommandStatus ChargeCommandStatus { get; set; }
/// <summary>
/// 充电桩状态
/// </summary>
[DisplayName("状态")]
[JsonIgnore]
public ChargeStationStatus Status { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[DisplayName("启用")]
public bool Enabled { get; set; }
[DisplayName("停靠车辆类型")]
public ChargeStationCarType GroupCarType { get; set; }
/// <summary>
/// 关联的站点ID(可选)
/// </summary>
[DisplayName("站点ID")]
public int? SiteId { get; set; }
/// <summary>
/// 备注
/// </summary>
[DisplayName("备注")]
public string Remarks { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[DisplayName("创建时间")]
public DateTime CreatedTime { get; set; }
/// <summary>
/// 最后修改时间
/// </summary>
[DisplayName("修改时间")]
public DateTime ModifiedTime { get; set; }
/// <summary>
/// 计算功率 (W)
/// </summary>
[JsonIgnore]
[DisplayName("功率(W)")]
public double Power => SetVoltage * SetElectricCurrent;
public ChargeStation()
{
StationId = GenerateStationId();
Type = ChargeStationType.FRLDTall; // 默认FRLD高款充电桩
ChargeMethod = ChargeMethodType.Ground; // 默认地充
Status = ChargeStationStatus.Idle;
Enabled = true;
CreatedTime = DateTime.Now;
ModifiedTime = DateTime.Now;
Port = 502; // 默认Modbus TCP端口
CommunicationType = "UDP"; // 默认UDP通讯
SetVoltage = 29.2;
SetElectricCurrent = 45.0;
}
/// <summary>
/// 生成充电桩编号
/// </summary>
private static string GenerateStationId()
{
return "1";
//return $"CS{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}";
}
/// <summary>
/// 验证数据有效性
/// </summary>
public bool IsValid(out string errorMessage)
{
if (string.IsNullOrWhiteSpace(StationId))
{
errorMessage = "充电桩编号不能为空";
return false;
}
if (string.IsNullOrWhiteSpace(Name))
{
errorMessage = "充电桩名称不能为空";
return false;
}
if (string.IsNullOrWhiteSpace(IpAddress))
{
errorMessage = "IP地址不能为空";
return false;
}
// 验证IP格式
if (!System.Net.IPAddress.TryParse(IpAddress, out _))
{
errorMessage = "IP地址格式不正确";
return false;
}
// 验证端口范围
if (Port < 1 || Port > 65535)
{
errorMessage = "端口号必须在 1-65535 之间";
return false;
}
// 验证电压范围
if (SetVoltage <= 0 || SetVoltage > 64)
{
errorMessage = "电压必须在 0-64V 之间";
return false;
}
// 验证电流范围
if (SetElectricCurrent <= 0 || SetElectricCurrent > 101)
{
errorMessage = "电流必须在 0-101A 之间";
return false;
}
errorMessage = string.Empty;
return true;
}
public override string ToString()
{
return $"[{StationId}] {Name} ({IpAddress}:{Port}) - {Status}";
}
}
/// <summary>
/// 充电桩类型枚举
/// </summary>
public enum ChargeStationType
{
[Description("FRLD高款充电桩")]
FRLDTall = 0,
[Description("FRLD矮款充电桩")]
FRLDShort = 1,
[Description("牧星充电桩")]
MuXing = 2
// 后续可在此处添加其他充电桩类型
}
public enum ChargeStationCarType
{
[Description("FRLD充电")]
FRLD = 0,
[Description("牧星充电桩充电")]
MuXing = 1
// 后续可在此处添加其他充电桩类型
}
/// <summary>
/// 充电桩状态枚举
/// </summary>
public enum ChargeStationStatus
{
[Description("空闲")]
Idle = 0,
[Description("充电中")]
Charging = 1,
[Description("报警中")]
Fault = 2,
[Description("AGV电池已接入")]
Battery = 3
}
/// <summary>
/// 报警级别枚举
/// </summary>
public enum AlarmLevel
{
[Description("无")]
None = 0,
[Description("低")]
Low = 1,
[Description("中")]
Medium = 2,
[Description("高")]
High = 3,
[Description("严重")]
Critical = 4
}
/// <summary>
/// 机构伸缩状态枚举
/// </summary>
public enum MechanismStatus
{
[Description("伸出")]
Extended = 1,
[Description("缩回")]
Retracted = 2,
[Description("运动中")]
Extending = 3,
}
/// <summary>
/// 网络通讯状态枚举
/// </summary>
public enum CommunicationStatus
{
[Description("未知")]
Unknown = 0,
[Description("正常")]
Normal = 1,
[Description("延迟")]
Delayed = 2,
[Description("超时")]
Timeout = 3,
[Description("断开")]
Disconnected = 4,
[Description("错误")]
Error = 5
}
/// <summary>
/// 充电指令状态枚举
/// </summary>
public enum ChargeCommandStatus
{
[Description("停止")]
Stopped = 0,
[Description("启动")]
Started = 1
}
/// <summary>
/// 充电方式枚举
/// </summary>
public enum ChargeMethodType
{
[Description("地充")]
Ground = 0,
[Description("尾充")]
Rear = 1,
[Description("侧充")]
Side = 2
}
}
@@ -0,0 +1,386 @@
using DocumentFormat.OpenXml.Bibliography;
using Newtonsoft.Json;
using SimpleCore;
using SimpleCore.Library;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace StandardScene.Charge
{
/// <summary>
/// 充电桩数据服务 - 负责数据的持久化和管理
/// </summary>
public class ChargeStationDataService
{
private static ChargeStationDataService _instance;
private static readonly object lockObj = new object();
private List<ChargeStation> chargeStations;
private readonly string dataFilePath;
// 单例模式
public static ChargeStationDataService Instance
{
get
{
if (_instance == null)
{
lock (lockObj)
{
if (_instance == null)
{
_instance = new ChargeStationDataService();
}
}
}
return _instance;
}
}
private ChargeStationDataService()
{
// 数据文件路径:项目根目录/Config/ChargeStations.json
var dataDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config");
if (!Directory.Exists(dataDir))
{
Directory.CreateDirectory(dataDir);
}
dataFilePath = Path.Combine(dataDir, "ChargeStations.json");
chargeStations = new List<ChargeStation>();
LoadData();
}
/// <summary>
/// 加载数据
/// </summary>
private void LoadData()
{
try
{
if (File.Exists(dataFilePath))
{
var json = File.ReadAllText(dataFilePath);
chargeStations = JsonConvert.DeserializeObject<List<ChargeStation>>(json)
?? new List<ChargeStation>();
Diagnosis.Log($"加载充电桩数据成功,共 {chargeStations.Count} 条记录", "ChargeStation");
}
else
{
chargeStations = new List<ChargeStation>();
Diagnosis.Log("充电桩数据文件不存在,已创建新列表", "ChargeStation");
}
}
catch (Exception ex)
{
Diagnosis.Log($"加载充电桩数据失败: {ExceptionFormatter.FormatEx(ex)}", "ChargeStation", true);
chargeStations = new List<ChargeStation>();
}
}
/// <summary>
/// 保存数据
/// </summary>
private bool SaveData()
{
try
{
lock (lockObj)
{
var json = JsonConvert.SerializeObject(chargeStations, Formatting.Indented);
File.WriteAllText(dataFilePath, json);
//Diagnosis.Log($"保存充电桩数据成功,共 {chargeStations.Count} 条记录", "ChargeStation");
return true;
}
}
catch (Exception ex)
{
Diagnosis.Log($"保存充电桩数据失败: {ExceptionFormatter.FormatEx(ex)}", "ChargeStation", true);
return false;
}
}
/// <summary>
/// 获取所有充电桩
/// </summary>
public List<ChargeStation> GetAllStations()
{
lock (lockObj)
{
return new List<ChargeStation>(chargeStations);
}
}
/// <summary>
/// 根据编号获取充电桩
/// </summary>
public ChargeStation GetStationById(string stationId)
{
lock (lockObj)
{
return chargeStations.FirstOrDefault(s => s.StationId == stationId);
}
}
/// <summary>
/// 根据IP地址获取充电桩
/// </summary>
public ChargeStation GetStationByIp(string ipAddress, int port)
{
lock (lockObj)
{
return chargeStations.FirstOrDefault(s => s.IpAddress == ipAddress && s.Port == port);
}
}
public ChargeStation GetStationByIp(string ipAddress)
{
lock (lockObj)
{
return chargeStations.FirstOrDefault(s => s.IpAddress == ipAddress);
}
}
/// <summary>
/// 添加充电桩
/// </summary>
public bool AddStation(ChargeStation station, out string errorMessage)
{
if (station == null)
{
errorMessage = "充电桩数据不能为空";
return false;
}
// 验证数据
if (!station.IsValid(out errorMessage))
{
return false;
}
lock (lockObj)
{
// 检查编号是否已存在
if (chargeStations.Any(s => s.StationId == station.StationId))
{
errorMessage = $"充电桩编号 {station.StationId} 已存在";
return false;
}
// 检查IP和端口是否已被使用
if (chargeStations.Any(s => s.IpAddress == station.IpAddress && s.Port == station.Port))
{
errorMessage = $"IP地址 {station.IpAddress}:{station.Port} 已被使用";
return false;
}
if (chargeStations.Any(s => s.SiteId == station.SiteId))
{
errorMessage = $"SiteID {station.SiteId} 已被使用";
return false;
}
station.CreatedTime = DateTime.Now;
station.ModifiedTime = DateTime.Now;
chargeStations.Add(station);
if (SaveData())
{
Diagnosis.Log($"添加充电桩成功: {station}", "ChargeStation", true);
errorMessage = string.Empty;
return true;
}
else
{
chargeStations.Remove(station);
errorMessage = "保存数据失败";
return false;
}
}
}
/// <summary>
/// 更新充电桩
/// </summary>
public bool UpdateStation(ChargeStation station, out string errorMessage, bool isSave = false)
{
if (station == null)
{
errorMessage = "充电桩数据不能为空";
return false;
}
// 验证数据
if (!station.IsValid(out errorMessage))
{
return false;
}
lock (lockObj)
{
var existingStation = chargeStations.FirstOrDefault(s => s.StationId == station.StationId);
if (existingStation == null)
{
errorMessage = $"充电桩编号 {station.StationId} 不存在";
return false;
}
// 检查IP和端口是否与其他充电桩冲突
if (chargeStations.Any(s => s.StationId != station.StationId &&
s.IpAddress == station.IpAddress &&
s.Port == station.Port))
{
errorMessage = $"IP地址 {station.IpAddress}:{station.Port} 已被其他充电桩使用";
return false;
}
if (chargeStations.Any(s => s.StationId != station.StationId && s.SiteId == station.SiteId))
{
errorMessage = $"SiteID {station.SiteId} 已被使用";
return false;
}
// 保留创建时间
station.CreatedTime = existingStation.CreatedTime;
station.ModifiedTime = DateTime.Now;
var index = chargeStations.IndexOf(existingStation);
//进行赋值
if (isSave)
{
existingStation.StationId = station.StationId;
existingStation.Name = station.Name;
existingStation.Type = station.Type;
existingStation.ChargeMethod = station.ChargeMethod;
existingStation.IpAddress = station.IpAddress;
existingStation.Port = station.Port;
existingStation.SetVoltage = station.SetVoltage;
existingStation.SetElectricCurrent = station.SetElectricCurrent;
existingStation.Enabled = station.Enabled;
existingStation.ShieldSiteMechanismStatus = station.ShieldSiteMechanismStatus;
existingStation.GroupCarType = station.GroupCarType;
existingStation.SiteId = station.SiteId;
existingStation.Remarks = station.Remarks;
station = existingStation;
station.ModifiedTime = DateTime.Now;
}
chargeStations[index] = station;
if (SaveData())
{
//Diagnosis.Log($"更新充电桩成功: {station}", "ChargeStation", true);
errorMessage = string.Empty;
return true;
}
else
{
chargeStations[index] = existingStation;
errorMessage = "保存数据失败";
return false;
}
}
}
/// <summary>
/// 删除充电桩
/// </summary>
public bool DeleteStation(string stationId, out string errorMessage)
{
lock (lockObj)
{
var station = chargeStations.FirstOrDefault(s => s.StationId == stationId);
if (station == null)
{
errorMessage = $"充电桩编号 {stationId} 不存在";
return false;
}
// 检查是否正在充电
//if (station.Status == ChargeStationStatus.Charging)
//{
// errorMessage = $"充电桩 {station.Name} 正在充电中,无法删除";
// return false;
//}
chargeStations.Remove(station);
if (SaveData())
{
Diagnosis.Log($"删除充电桩成功: {station}", "ChargeStation", true);
errorMessage = string.Empty;
return true;
}
else
{
chargeStations.Add(station);
errorMessage = "保存数据失败";
return false;
}
}
}
/// <summary>
/// 更新充电桩状态
/// </summary>
public bool UpdateStationStatus(string stationId, ChargeStationStatus status)
{
lock (lockObj)
{
var station = chargeStations.FirstOrDefault(s => s.StationId == stationId);
if (station == null)
{
return false;
}
station.Status = status;
station.ModifiedTime = DateTime.Now;
return SaveData();
}
}
/// <summary>
/// 获取空闲的充电桩
/// </summary>
public List<ChargeStation> GetIdleStations()
{
lock (lockObj)
{
return chargeStations
.Where(s => s.Enabled && s.Status == ChargeStationStatus.Idle)
.ToList();
}
}
/// <summary>
/// 获取充电中的充电桩数量
/// </summary>
public int GetChargingCount()
{
lock (lockObj)
{
return chargeStations.Count(s => s.Status == ChargeStationStatus.Charging);
}
}
/// <summary>
/// 重新加载数据
/// </summary>
public void Reload()
{
LoadData();
}
}
}
@@ -0,0 +1,448 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using SimpleCore;
using SimpleCore.Library;
namespace StandardScene.Charge
{
/// <summary>
/// 充电桩管理辅助类
/// 提供简化的静态方法用于快速访问充电桩功能
/// </summary>
public static class ChargeStationHelper
{
private static ChargeStationManagementForm _managementForm;
/// <summary>
/// 打开充电桩管理窗口(单例模式)
/// </summary>
public static void OpenManagementWindow()
{
if (_managementForm == null || _managementForm.IsDisposed)
{
_managementForm = new ChargeStationManagementForm();
_managementForm.FormClosed += (s, e) => _managementForm = null;
_managementForm.Show();
}
else
{
_managementForm.BringToFront();
_managementForm.Activate();
}
}
/// <summary>
/// 打开充电桩管理窗口(对话框模式)
/// </summary>
public static DialogResult OpenManagementDialog()
{
using (var form = new ChargeStationManagementForm())
{
return form.ShowDialog();
}
}
/// <summary>
/// 获取指定站点的充电桩
/// </summary>
/// <param name="siteId">站点ID</param>
/// <returns>充电桩对象,如果不存在则返回null</returns>
public static ChargeStation GetStationBySiteId(int siteId)
{
var dataService = ChargeStationDataService.Instance;
return dataService.GetAllStations()
.FirstOrDefault(s => s.SiteId == siteId);
}
/// <summary>
/// 获取指定IP的充电桩
/// </summary>
/// <param name="ipAddress">IP地址</param>
/// <returns>充电桩对象,如果不存在则返回null</returns>
public static ChargeStation GetStationByIp(string ipAddress)
{
var dataService = ChargeStationDataService.Instance;
return dataService.GetAllStations()
.FirstOrDefault(s => s.IpAddress == ipAddress);
}
/// <summary>
/// 获取所有充电桩配置
/// </summary>
/// <returns>所有充电桩配置列表</returns>
public static List<ChargeStation> GetAllStationConfigs()
{
var dataService = ChargeStationDataService.Instance;
return dataService.GetAllStations();
}
/// <summary>
/// 获取当前充电策略配置
/// </summary>
/// <returns>充电策略配置对象</returns>
public static ChargeStrategyConfig GetChargeStrategyConfig()
{
var configService = ChargeStrategyConfigService.Instance;
return configService.LoadConfig();
}
/// <summary>
/// 保存充电策略配置
/// </summary>
/// <param name="config">充电策略配置对象</param>
public static void SaveChargeStrategyConfig(ChargeStrategyConfig config)
{
var configService = ChargeStrategyConfigService.Instance;
configService.SaveConfig(config);
}
/// <summary>
/// 检查指定站点是否有可用的充电桩
/// </summary>
/// <param name="siteId">站点ID</param>
/// <returns>true表示有可用充电桩,false表示没有</returns>
public static bool IsSiteHasAvailableChargeStation(int siteId)
{
var station = GetStationBySiteId(siteId);
return station != null &&
station.Enabled &&
station.Status == ChargeStationStatus.Idle;
}
/// <summary>
/// 标记充电桩开始充电
/// </summary>
/// <param name="stationId">充电桩编号</param>
/// <param name="carId">车辆ID</param>
/// <returns>成功返回true,失败返回false</returns>
public static bool StartCharging(string stationId, int carId)
{
try
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station == null)
{
Diagnosis.Log($"充电桩 {stationId} 不存在", "ChargeStation", true);
return false;
}
if (station.Status == ChargeStationStatus.Charging)
{
Diagnosis.Log($"充电桩 {station.Name} 已经在充电中", "ChargeStation", true);
return false;
}
bool success = dataService.UpdateStationStatus(stationId, ChargeStationStatus.Charging);
if (success)
{
Diagnosis.Log($"车辆 {carId} 开始在充电桩 {station.Name} 充电", "ChargeStation", true);
}
return success;
}
catch (Exception ex)
{
Diagnosis.Log($"启动充电失败: {ExceptionFormatter.FormatEx(ex)}", "ChargeStation", true);
return false;
}
}
/// <summary>
/// 标记充电桩停止充电
/// </summary>
/// <param name="stationId">充电桩编号</param>
/// <param name="carId">车辆ID</param>
/// <returns>成功返回true,失败返回false</returns>
public static bool StopCharging(string stationId, int carId)
{
try
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station == null)
{
Diagnosis.Log($"充电桩 {stationId} 不存在", "ChargeStation", true);
return false;
}
bool success = dataService.UpdateStationStatus(stationId, ChargeStationStatus.Idle);
if (success)
{
Diagnosis.Log($"车辆 {carId} 在充电桩 {station.Name} 充电完成", "ChargeStation", true);
}
return success;
}
catch (Exception ex)
{
Diagnosis.Log($"停止充电失败: {ExceptionFormatter.FormatEx(ex)}", "ChargeStation", true);
return false;
}
}
/// <summary>
/// 标记充电桩为故障状态
/// </summary>
/// <param name="stationId">充电桩编号</param>
/// <param name="reason">故障原因</param>
/// <returns>成功返回true,失败返回false</returns>
public static bool MarkAsFault(string stationId, string reason = "")
{
try
{
var dataService = ChargeStationDataService.Instance;
bool success = dataService.UpdateStationStatus(stationId, ChargeStationStatus.Fault);
if (success)
{
var station = dataService.GetStationById(stationId);
var message = string.IsNullOrEmpty(reason)
? $"充电桩 {station.Name} 标记为故障"
: $"充电桩 {station.Name} 标记为故障: {reason}";
Diagnosis.Log(message, "ChargeStation", true);
}
return success;
}
catch (Exception ex)
{
Diagnosis.Log($"标记故障失败: {ExceptionFormatter.FormatEx(ex)}", "ChargeStation", true);
return false;
}
}
/// <summary>
/// 获取充电桩状态摘要信息
/// </summary>
/// <returns>格式化的状态字符串</returns>
public static string GetStatusSummary()
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations();
var total = stations.Count;
var idle = stations.Count(s => s.Status == ChargeStationStatus.Idle && s.Enabled);
var charging = stations.Count(s => s.Status == ChargeStationStatus.Charging);
var fault = stations.Count(s => s.Status == ChargeStationStatus.Fault);
var offline = stations.Count(s => s.Status == ChargeStationStatus.Battery);
return $"总数:{total} | 空闲:{idle} | 充电中:{charging} | 故障:{fault} | 离线:{offline}";
}
/// <summary>
/// 获取最近的空闲充电桩(基于站点ID)
/// </summary>
/// <param name="currentSiteId">当前站点ID</param>
/// <returns>最近的充电桩,如果没有则返回null</returns>
public static ChargeStation FindNearestIdleStation(int currentSiteId)
{
var dataService = ChargeStationDataService.Instance;
var idleStations = dataService.GetIdleStations();
if (idleStations.Count == 0)
return null;
// 优先选择同站点的充电桩
var sameStation = idleStations.FirstOrDefault(s => s.SiteId == currentSiteId);
if (sameStation != null)
return sameStation;
// 否则选择第一个可用的
return idleStations[0];
}
/// <summary>
/// 快速创建测试充电桩(用于测试)
/// </summary>
/// <param name="name">名称</param>
/// <param name="ip">IP地址</param>
/// <param name="siteId">站点ID</param>
/// <returns>创建成功返回true</returns>
public static bool QuickAddStation(string name, string ip, int? siteId = null)
{
var dataService = ChargeStationDataService.Instance;
var station = new ChargeStation
{
Name = name,
IpAddress = ip,
Port = 502,
SetVoltage = 220.0,
SetElectricCurrent = 32.0,
Status = ChargeStationStatus.Idle,
Enabled = true,
SiteId = siteId,
Remarks = $"快速创建于 {DateTime.Now}"
};
bool success = dataService.AddStation(station, out string errorMsg);
if (success)
{
Diagnosis.Log($"快速创建充电桩: {name}", "ChargeStation", true);
}
else
{
Diagnosis.Log($"快速创建充电桩失败: {errorMsg}", "ChargeStation", true);
}
return success;
}
/// <summary>
/// 显示充电桩选择对话框
/// </summary>
/// <param name="filterByStatus">按状态过滤(null表示显示全部)</param>
/// <returns>选中的充电桩,取消则返回null</returns>
public static ChargeStation ShowStationSelectionDialog(ChargeStationStatus? filterByStatus = null)
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations();
if (filterByStatus.HasValue)
{
stations = stations.Where(s => s.Status == filterByStatus.Value).ToList();
}
if (stations.Count == 0)
{
MessageBox.Show("没有符合条件的充电桩", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return null;
}
// 创建简单的选择对话框
using (var dialog = new Form())
{
dialog.Text = "选择充电桩";
dialog.Size = new System.Drawing.Size(500, 400);
dialog.StartPosition = FormStartPosition.CenterParent;
var listBox = new ListBox
{
Dock = DockStyle.Fill,
Font = new System.Drawing.Font("微软雅黑", 10F)
};
foreach (var station in stations)
{
listBox.Items.Add($"[{station.StationId}] {station.Name} - {station.IpAddress}:{station.Port} - {GetStatusText(station.Status)}");
}
var btnOK = new Button
{
Text = "确定",
DialogResult = DialogResult.OK,
Dock = DockStyle.Bottom,
Height = 40
};
dialog.Controls.Add(listBox);
dialog.Controls.Add(btnOK);
dialog.AcceptButton = btnOK;
if (dialog.ShowDialog() == DialogResult.OK && listBox.SelectedIndex >= 0)
{
return stations[listBox.SelectedIndex];
}
return null;
}
}
/// <summary>
/// 获取状态文本
/// </summary>
private static string GetStatusText(ChargeStationStatus status)
{
switch (status)
{
case ChargeStationStatus.Idle: return "空闲";
case ChargeStationStatus.Charging: return "充电中";
case ChargeStationStatus.Fault: return "故障";
case ChargeStationStatus.Battery: return "离线";
default: return "未知";
}
}
/// <summary>
/// 批量更新充电桩在线状态(用于定期监控)
/// </summary>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>更新的充电桩数量</returns>
public static int UpdateOnlineStatus(int timeout = 3000)
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations().Where(s => s.Enabled).ToList();
int updatedCount = 0;
foreach (var station in stations)
{
try
{
// 这里应该实际ping充电桩,此处仅演示
bool isOnline = PingStation(station.IpAddress, station.Port, timeout);
var expectedStatus = isOnline
? (station.Status == ChargeStationStatus.Battery ? ChargeStationStatus.Idle : station.Status)
: ChargeStationStatus.Battery;
if (station.Status != expectedStatus &&
(station.Status == ChargeStationStatus.Battery || expectedStatus == ChargeStationStatus.Battery))
{
if (dataService.UpdateStationStatus(station.StationId, expectedStatus))
{
updatedCount++;
Diagnosis.Log($"充电桩 {station.Name} 状态更新为: {GetStatusText(expectedStatus)}",
"ChargeStation", true);
}
}
}
catch (Exception ex)
{
Diagnosis.Log($"检查充电桩 {station.Name} 在线状态失败: {ex.Message}",
"ChargeStation");
}
}
return updatedCount;
}
/// <summary>
/// Ping 充电桩(检查连通性)
/// </summary>
private static bool PingStation(string ip, int port, int timeout)
{
try
{
using (var client = new System.Net.Sockets.TcpClient())
{
var result = client.BeginConnect(ip, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(timeout));
if (success)
{
client.EndConnect(result);
return true;
}
return false;
}
}
catch
{
return false;
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,309 @@
using System;
using System.ComponentModel;
namespace StandardScene.Charge
{
/// <summary>
/// 充电策略配置
/// </summary>
public class ChargeStrategyConfig
{
#region SOC
/// <summary>
/// 必充电量 (%)
/// </summary>
[Description("必充电量")]
[DisplayName("必充电量(%)")]
public double MustChargeSoc { get; set; }
/// <summary>
/// 空闲充电电量 (%)
/// </summary>
[Description("空闲充电电量")]
[DisplayName("空闲充电电量(%)")]
public double IdleChargeSoc { get; set; }
/// <summary>
/// 任务可用电量 (%)
/// </summary>
[Description("任务可用电量")]
[DisplayName("任务可用电量(%)")]
public double TaskAvailableSoc { get; set; }
/// <summary>
/// 满电电量 (%)
/// </summary>
[Description("满电电量")]
[DisplayName("满电电量(%)")]
public double FullChargeSoc { get; set; }
/// <summary>
/// 允许中断电量 (%)
/// </summary>
[Description("允许中断电量")]
[DisplayName("允许中断电量(%)")]
public double AllowInterruptSoc { get; set; }
#endregion
#region
/// <summary>
/// 空闲充电时间 (秒)
/// </summary>
[Description("空闲充电时间")]
[DisplayName("空闲充电时间(秒)")]
public double IdleChargeSeconds { get; set; }
/// <summary>
/// 空闲时间 (秒)
/// </summary>
[Description("空闲时间")]
[DisplayName("空闲时间(秒)")]
public double IdleSeconds { get; set; }
/// <summary>
/// 必充时间 (秒)
/// </summary>
[Description("必充时间")]
[DisplayName("必充时间(秒)")]
public double MustChargeSeconds { get; set; }
/// <summary>
/// 补电时间 (分钟)
/// </summary>
[Description("补电时间")]
[DisplayName("补电时间(分钟)")]
public double TopUpMinutes { get; set; }
#endregion
#region
/// <summary>
/// 允许空闲车充电的最小任务数
/// </summary>
[Description("允许空闲车充电的最小任务数")]
[DisplayName("最小任务数")]
public int MinAllowFreeCarToChargeTaskCnt { get; set; }
#endregion
#region
/// <summary>
/// 允许中断充电任务
/// </summary>
[Description("允许中断充电任务")]
[DisplayName("允许中断任务")]
public bool AllowInterruptTask { get; set; }
/// <summary>
/// 优先使用低电量车辆充电
/// </summary>
[Description("优先使用低电量车辆充电")]
[DisplayName("优先低电量充电")]
public bool UseLowerSocForCharge { get; set; }
/// <summary>
/// 启用充电错误检测
/// </summary>
[Description("启用充电错误检测")]
[DisplayName("错误检测")]
public bool EnableErrorChargeDetection { get; set; }
/// <summary>
/// 使用充电站点筛选
/// </summary>
[Description("使用充电站点筛选")]
[DisplayName("站点筛选")]
public bool UseChargeSiteFilter { get; set; }
#endregion
#region
public ChargeStrategyConfig()
{
// 使用默认值初始化
SetDefaults();
}
/// <summary>
/// 设置默认值
/// </summary>
private void SetDefaults()
{
// SOC 参数默认值
MustChargeSoc = 20;
IdleChargeSoc = 90;
TaskAvailableSoc = 60;
FullChargeSoc = 90;
AllowInterruptSoc = 45;
// 时间参数默认值
IdleChargeSeconds = 30;
IdleSeconds = 5;
MustChargeSeconds = 60;
TopUpMinutes = 5;
// 任务参数默认值
MinAllowFreeCarToChargeTaskCnt = 0;
// 开关参数默认值
AllowInterruptTask = false;
UseLowerSocForCharge = true;
EnableErrorChargeDetection = false;
UseChargeSiteFilter = false;
}
/// <summary>
/// 创建默认配置
/// </summary>
public static ChargeStrategyConfig CreateDefault()
{
return new ChargeStrategyConfig();
}
#endregion
#region
/// <summary>
/// 验证配置是否有效
/// </summary>
public bool Validate(out string errorMessage)
{
// 验证 SOC 范围
if (MustChargeSoc < 0 || MustChargeSoc > 100)
{
errorMessage = "必充电量必须在 0-100 之间";
return false;
}
if (IdleChargeSoc < 0 || IdleChargeSoc > 100)
{
errorMessage = "空闲充电电量必须在 0-100 之间";
return false;
}
if (TaskAvailableSoc < 0 || TaskAvailableSoc > 100)
{
errorMessage = "任务可用电量必须在 0-100 之间";
return false;
}
if (FullChargeSoc < 0 || FullChargeSoc > 100)
{
errorMessage = "满电电量必须在 0-100 之间";
return false;
}
if (AllowInterruptSoc < 0 || AllowInterruptSoc > 100)
{
errorMessage = "允许中断电量必须在 0-100 之间";
return false;
}
// 验证 SOC 逻辑关系
if (MustChargeSoc >= IdleChargeSoc)
{
errorMessage = "必充电量必须小于空闲充电电量";
return false;
}
if (TaskAvailableSoc <= MustChargeSoc)
{
errorMessage = "任务可用电量必须大于必充电量";
return false;
}
if (FullChargeSoc < IdleChargeSoc)
{
errorMessage = "满电电量必须大于等于空闲充电电量";
return false;
}
if (AllowInterruptSoc <= MustChargeSoc)
{
errorMessage = "允许中断电量必须大于必充电量";
return false;
}
// 验证时间参数
if (IdleChargeSeconds < 0)
{
errorMessage = "空闲充电时间不能为负数";
return false;
}
if (IdleSeconds < 0)
{
errorMessage = "空闲时间不能为负数";
return false;
}
if (MustChargeSeconds < 0)
{
errorMessage = "必充时间不能为负数";
return false;
}
if (TopUpMinutes < 0)
{
errorMessage = "补电时间不能为负数";
return false;
}
// 验证任务参数
if (MinAllowFreeCarToChargeTaskCnt < 0)
{
errorMessage = "最小任务数不能为负数";
return false;
}
errorMessage = string.Empty;
return true;
}
#endregion
#region
/// <summary>
/// 克隆配置
/// </summary>
public ChargeStrategyConfig Clone()
{
return new ChargeStrategyConfig
{
MustChargeSoc = this.MustChargeSoc,
IdleChargeSoc = this.IdleChargeSoc,
TaskAvailableSoc = this.TaskAvailableSoc,
FullChargeSoc = this.FullChargeSoc,
AllowInterruptSoc = this.AllowInterruptSoc,
IdleChargeSeconds = this.IdleChargeSeconds,
IdleSeconds = this.IdleSeconds,
MustChargeSeconds = this.MustChargeSeconds,
TopUpMinutes = this.TopUpMinutes,
MinAllowFreeCarToChargeTaskCnt = this.MinAllowFreeCarToChargeTaskCnt,
AllowInterruptTask = this.AllowInterruptTask,
UseLowerSocForCharge = this.UseLowerSocForCharge,
EnableErrorChargeDetection = this.EnableErrorChargeDetection,
UseChargeSiteFilter = this.UseChargeSiteFilter
};
}
/// <summary>
/// 转换为字符串
/// </summary>
public override string ToString()
{
return $"充电策略配置 [必充:{MustChargeSoc}%, 空闲充:{IdleChargeSoc}%, 任务可用:{TaskAvailableSoc}%]";
}
#endregion
}
}
@@ -0,0 +1,572 @@
namespace StandardScene.Charge
{
partial class ChargeStrategyConfigForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
// 创建所有控件实例
this.pnlMain = new System.Windows.Forms.Panel();
this.pnlBottom = new System.Windows.Forms.Panel();
this.grpSocParams = new System.Windows.Forms.GroupBox();
this.grpTimeParams = new System.Windows.Forms.GroupBox();
this.grpTaskParams = new System.Windows.Forms.GroupBox();
this.grpSwitchParams = new System.Windows.Forms.GroupBox();
// SOC 参数控件
this.lblMustChargeSoc = new System.Windows.Forms.Label();
this.numMustChargeSoc = new System.Windows.Forms.NumericUpDown();
this.lblIdleChargeSoc = new System.Windows.Forms.Label();
this.numIdleChargeSoc = new System.Windows.Forms.NumericUpDown();
this.lblTaskAvailableSoc = new System.Windows.Forms.Label();
this.numTaskAvailableSoc = new System.Windows.Forms.NumericUpDown();
this.lblFullChargeSoc = new System.Windows.Forms.Label();
this.numFullChargeSoc = new System.Windows.Forms.NumericUpDown();
this.lblAllowInterruptSoc = new System.Windows.Forms.Label();
this.numAllowInterruptSoc = new System.Windows.Forms.NumericUpDown();
// 时间参数控件
this.lblIdleChargeSeconds = new System.Windows.Forms.Label();
this.numIdleChargeSeconds = new System.Windows.Forms.NumericUpDown();
this.lblIdleSeconds = new System.Windows.Forms.Label();
this.numIdleSeconds = new System.Windows.Forms.NumericUpDown();
this.lblMustChargeSeconds = new System.Windows.Forms.Label();
this.numMustChargeSeconds = new System.Windows.Forms.NumericUpDown();
this.lblTopUpMinutes = new System.Windows.Forms.Label();
this.numTopUpMinutes = new System.Windows.Forms.NumericUpDown();
// 任务参数控件
this.lblMinAllowFreeCarToChargeTaskCnt = new System.Windows.Forms.Label();
this.numMinAllowFreeCarToChargeTaskCnt = new System.Windows.Forms.NumericUpDown();
// 开关参数控件
this.chkAllowInterruptTask = new System.Windows.Forms.CheckBox();
this.chkUseLowerSocForCharge = new System.Windows.Forms.CheckBox();
this.chkEnableErrorChargeDetection = new System.Windows.Forms.CheckBox();
this.chkUseChargeSiteFilter = new System.Windows.Forms.CheckBox();
// 底部控件
this.lblStatus = new System.Windows.Forms.Label();
this.btnSave = new System.Windows.Forms.Button();
this.btnApply = new System.Windows.Forms.Button();
this.btnRestoreDefaults = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.pnlMain.SuspendLayout();
this.grpSwitchParams.SuspendLayout();
this.grpTaskParams.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numMinAllowFreeCarToChargeTaskCnt)).BeginInit();
this.grpTimeParams.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numTopUpMinutes)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numMustChargeSeconds)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleSeconds)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleChargeSeconds)).BeginInit();
this.grpSocParams.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.numAllowInterruptSoc)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numFullChargeSoc)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numTaskAvailableSoc)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleChargeSoc)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.numMustChargeSoc)).BeginInit();
this.pnlBottom.SuspendLayout();
this.SuspendLayout();
//
// pnlMain
//
this.pnlMain.AutoScroll = true;
this.pnlMain.Controls.Add(this.grpSwitchParams);
this.pnlMain.Controls.Add(this.grpTaskParams);
this.pnlMain.Controls.Add(this.grpTimeParams);
this.pnlMain.Controls.Add(this.grpSocParams);
this.pnlMain.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlMain.Location = new System.Drawing.Point(0, 0);
this.pnlMain.Name = "pnlMain";
this.pnlMain.Padding = new System.Windows.Forms.Padding(10);
this.pnlMain.Size = new System.Drawing.Size(784, 631);
this.pnlMain.TabIndex = 0;
//
// grpSwitchParams
//
this.grpSwitchParams.Controls.Add(this.chkUseChargeSiteFilter);
this.grpSwitchParams.Controls.Add(this.chkEnableErrorChargeDetection);
this.grpSwitchParams.Controls.Add(this.chkUseLowerSocForCharge);
this.grpSwitchParams.Controls.Add(this.chkAllowInterruptTask);
this.grpSwitchParams.Dock = System.Windows.Forms.DockStyle.Top;
this.grpSwitchParams.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold);
this.grpSwitchParams.Location = new System.Drawing.Point(10, 460);
this.grpSwitchParams.Name = "grpSwitchParams";
this.grpSwitchParams.Padding = new System.Windows.Forms.Padding(10);
this.grpSwitchParams.Size = new System.Drawing.Size(764, 150);
this.grpSwitchParams.TabIndex = 3;
this.grpSwitchParams.TabStop = false;
this.grpSwitchParams.Text = "开关参数";
//
// chkUseChargeSiteFilter
//
this.chkUseChargeSiteFilter.AutoSize = true;
this.chkUseChargeSiteFilter.Font = new System.Drawing.Font("微软雅黑", 9F);
this.chkUseChargeSiteFilter.Location = new System.Drawing.Point(400, 80);
this.chkUseChargeSiteFilter.Name = "chkUseChargeSiteFilter";
this.chkUseChargeSiteFilter.Size = new System.Drawing.Size(147, 24);
this.chkUseChargeSiteFilter.TabIndex = 3;
this.chkUseChargeSiteFilter.Text = "使用充电站点筛选";
this.chkUseChargeSiteFilter.UseVisualStyleBackColor = true;
//
// chkEnableErrorChargeDetection
//
this.chkEnableErrorChargeDetection.AutoSize = true;
this.chkEnableErrorChargeDetection.Font = new System.Drawing.Font("微软雅黑", 9F);
this.chkEnableErrorChargeDetection.Location = new System.Drawing.Point(30, 80);
this.chkEnableErrorChargeDetection.Name = "chkEnableErrorChargeDetection";
this.chkEnableErrorChargeDetection.Size = new System.Drawing.Size(147, 24);
this.chkEnableErrorChargeDetection.TabIndex = 2;
this.chkEnableErrorChargeDetection.Text = "启用充电错误检测";
this.chkEnableErrorChargeDetection.UseVisualStyleBackColor = true;
//
// chkUseLowerSocForCharge
//
this.chkUseLowerSocForCharge.AutoSize = true;
this.chkUseLowerSocForCharge.Font = new System.Drawing.Font("微软雅黑", 9F);
this.chkUseLowerSocForCharge.Location = new System.Drawing.Point(400, 40);
this.chkUseLowerSocForCharge.Name = "chkUseLowerSocForCharge";
this.chkUseLowerSocForCharge.Size = new System.Drawing.Size(195, 24);
this.chkUseLowerSocForCharge.TabIndex = 1;
this.chkUseLowerSocForCharge.Text = "优先使用低电量车辆充电";
this.chkUseLowerSocForCharge.UseVisualStyleBackColor = true;
//
// chkAllowInterruptTask
//
this.chkAllowInterruptTask.AutoSize = true;
this.chkAllowInterruptTask.Font = new System.Drawing.Font("微软雅黑", 9F);
this.chkAllowInterruptTask.Location = new System.Drawing.Point(30, 40);
this.chkAllowInterruptTask.Name = "chkAllowInterruptTask";
this.chkAllowInterruptTask.Size = new System.Drawing.Size(147, 24);
this.chkAllowInterruptTask.TabIndex = 0;
this.chkAllowInterruptTask.Text = "允许中断充电任务";
this.chkAllowInterruptTask.UseVisualStyleBackColor = true;
//
// grpTaskParams
//
this.grpTaskParams.Controls.Add(this.numMinAllowFreeCarToChargeTaskCnt);
this.grpTaskParams.Controls.Add(this.lblMinAllowFreeCarToChargeTaskCnt);
this.grpTaskParams.Dock = System.Windows.Forms.DockStyle.Top;
this.grpTaskParams.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold);
this.grpTaskParams.Location = new System.Drawing.Point(10, 370);
this.grpTaskParams.Name = "grpTaskParams";
this.grpTaskParams.Padding = new System.Windows.Forms.Padding(10);
this.grpTaskParams.Size = new System.Drawing.Size(764, 90);
this.grpTaskParams.TabIndex = 2;
this.grpTaskParams.TabStop = false;
this.grpTaskParams.Text = "任务参数";
//
// numMinAllowFreeCarToChargeTaskCnt
//
this.numMinAllowFreeCarToChargeTaskCnt.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numMinAllowFreeCarToChargeTaskCnt.Location = new System.Drawing.Point(250, 40);
this.numMinAllowFreeCarToChargeTaskCnt.Maximum = new decimal(new int[] {
100,
0,
0,
0});
this.numMinAllowFreeCarToChargeTaskCnt.Name = "numMinAllowFreeCarToChargeTaskCnt";
this.numMinAllowFreeCarToChargeTaskCnt.Size = new System.Drawing.Size(120, 27);
this.numMinAllowFreeCarToChargeTaskCnt.TabIndex = 1;
//
// lblMinAllowFreeCarToChargeTaskCnt
//
this.lblMinAllowFreeCarToChargeTaskCnt.AutoSize = true;
this.lblMinAllowFreeCarToChargeTaskCnt.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblMinAllowFreeCarToChargeTaskCnt.Location = new System.Drawing.Point(30, 42);
this.lblMinAllowFreeCarToChargeTaskCnt.Name = "lblMinAllowFreeCarToChargeTaskCnt";
this.lblMinAllowFreeCarToChargeTaskCnt.Size = new System.Drawing.Size(207, 20);
this.lblMinAllowFreeCarToChargeTaskCnt.TabIndex = 0;
this.lblMinAllowFreeCarToChargeTaskCnt.Text = "允许空闲车充电的最小任务数:";
//
// grpTimeParams
//
this.grpTimeParams.Controls.Add(this.numTopUpMinutes);
this.grpTimeParams.Controls.Add(this.lblTopUpMinutes);
this.grpTimeParams.Controls.Add(this.numMustChargeSeconds);
this.grpTimeParams.Controls.Add(this.lblMustChargeSeconds);
this.grpTimeParams.Controls.Add(this.numIdleSeconds);
this.grpTimeParams.Controls.Add(this.lblIdleSeconds);
this.grpTimeParams.Controls.Add(this.numIdleChargeSeconds);
this.grpTimeParams.Controls.Add(this.lblIdleChargeSeconds);
this.grpTimeParams.Dock = System.Windows.Forms.DockStyle.Top;
this.grpTimeParams.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold);
this.grpTimeParams.Location = new System.Drawing.Point(10, 210);
this.grpTimeParams.Name = "grpTimeParams";
this.grpTimeParams.Padding = new System.Windows.Forms.Padding(10);
this.grpTimeParams.Size = new System.Drawing.Size(764, 160);
this.grpTimeParams.TabIndex = 1;
this.grpTimeParams.TabStop = false;
this.grpTimeParams.Text = "时间参数";
//
// numTopUpMinutes
//
this.numTopUpMinutes.DecimalPlaces = 1;
this.numTopUpMinutes.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numTopUpMinutes.Location = new System.Drawing.Point(580, 100);
this.numTopUpMinutes.Maximum = new decimal(new int[] {
1000,
0,
0,
0});
this.numTopUpMinutes.Name = "numTopUpMinutes";
this.numTopUpMinutes.Size = new System.Drawing.Size(120, 27);
this.numTopUpMinutes.TabIndex = 7;
//
// lblTopUpMinutes
//
this.lblTopUpMinutes.AutoSize = true;
this.lblTopUpMinutes.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblTopUpMinutes.Location = new System.Drawing.Point(400, 102);
this.lblTopUpMinutes.Name = "lblTopUpMinutes";
this.lblTopUpMinutes.Size = new System.Drawing.Size(159, 20);
this.lblTopUpMinutes.TabIndex = 6;
this.lblTopUpMinutes.Text = "补电时间 (分钟,min):";
//
// numMustChargeSeconds
//
this.numMustChargeSeconds.DecimalPlaces = 1;
this.numMustChargeSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numMustChargeSeconds.Location = new System.Drawing.Point(250, 100);
this.numMustChargeSeconds.Maximum = new decimal(new int[] {
10000,
0,
0,
0});
this.numMustChargeSeconds.Name = "numMustChargeSeconds";
this.numMustChargeSeconds.Size = new System.Drawing.Size(120, 27);
this.numMustChargeSeconds.TabIndex = 5;
//
// lblMustChargeSeconds
//
this.lblMustChargeSeconds.AutoSize = true;
this.lblMustChargeSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblMustChargeSeconds.Location = new System.Drawing.Point(30, 102);
this.lblMustChargeSeconds.Name = "lblMustChargeSeconds";
this.lblMustChargeSeconds.Size = new System.Drawing.Size(147, 20);
this.lblMustChargeSeconds.TabIndex = 4;
this.lblMustChargeSeconds.Text = "必充时间 (秒,sec):";
//
// numIdleSeconds
//
this.numIdleSeconds.DecimalPlaces = 1;
this.numIdleSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numIdleSeconds.Location = new System.Drawing.Point(580, 40);
this.numIdleSeconds.Maximum = new decimal(new int[] {
10000,
0,
0,
0});
this.numIdleSeconds.Name = "numIdleSeconds";
this.numIdleSeconds.Size = new System.Drawing.Size(120, 27);
this.numIdleSeconds.TabIndex = 3;
//
// lblIdleSeconds
//
this.lblIdleSeconds.AutoSize = true;
this.lblIdleSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblIdleSeconds.Location = new System.Drawing.Point(400, 42);
this.lblIdleSeconds.Name = "lblIdleSeconds";
this.lblIdleSeconds.Size = new System.Drawing.Size(147, 20);
this.lblIdleSeconds.TabIndex = 2;
this.lblIdleSeconds.Text = "空闲时间 (秒,sec):";
//
// numIdleChargeSeconds
//
this.numIdleChargeSeconds.DecimalPlaces = 1;
this.numIdleChargeSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numIdleChargeSeconds.Location = new System.Drawing.Point(250, 40);
this.numIdleChargeSeconds.Maximum = new decimal(new int[] {
10000,
0,
0,
0});
this.numIdleChargeSeconds.Name = "numIdleChargeSeconds";
this.numIdleChargeSeconds.Size = new System.Drawing.Size(120, 27);
this.numIdleChargeSeconds.TabIndex = 1;
//
// lblIdleChargeSeconds
//
this.lblIdleChargeSeconds.AutoSize = true;
this.lblIdleChargeSeconds.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblIdleChargeSeconds.Location = new System.Drawing.Point(30, 42);
this.lblIdleChargeSeconds.Name = "lblIdleChargeSeconds";
this.lblIdleChargeSeconds.Size = new System.Drawing.Size(171, 20);
this.lblIdleChargeSeconds.TabIndex = 0;
this.lblIdleChargeSeconds.Text = "空闲充电时间 (秒,sec):";
//
// grpSocParams
//
this.grpSocParams.Controls.Add(this.numAllowInterruptSoc);
this.grpSocParams.Controls.Add(this.lblAllowInterruptSoc);
this.grpSocParams.Controls.Add(this.numFullChargeSoc);
this.grpSocParams.Controls.Add(this.lblFullChargeSoc);
this.grpSocParams.Controls.Add(this.numTaskAvailableSoc);
this.grpSocParams.Controls.Add(this.lblTaskAvailableSoc);
this.grpSocParams.Controls.Add(this.numIdleChargeSoc);
this.grpSocParams.Controls.Add(this.lblIdleChargeSoc);
this.grpSocParams.Controls.Add(this.numMustChargeSoc);
this.grpSocParams.Controls.Add(this.lblMustChargeSoc);
this.grpSocParams.Dock = System.Windows.Forms.DockStyle.Top;
this.grpSocParams.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold);
this.grpSocParams.Location = new System.Drawing.Point(10, 10);
this.grpSocParams.Name = "grpSocParams";
this.grpSocParams.Padding = new System.Windows.Forms.Padding(10);
this.grpSocParams.Size = new System.Drawing.Size(764, 200);
this.grpSocParams.TabIndex = 0;
this.grpSocParams.TabStop = false;
this.grpSocParams.Text = "SOC 参数 (电量百分比)";
//
// numAllowInterruptSoc
//
this.numAllowInterruptSoc.DecimalPlaces = 1;
this.numAllowInterruptSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numAllowInterruptSoc.Location = new System.Drawing.Point(250, 150);
this.numAllowInterruptSoc.Name = "numAllowInterruptSoc";
this.numAllowInterruptSoc.Size = new System.Drawing.Size(120, 27);
this.numAllowInterruptSoc.TabIndex = 9;
//
// lblAllowInterruptSoc
//
this.lblAllowInterruptSoc.AutoSize = true;
this.lblAllowInterruptSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblAllowInterruptSoc.Location = new System.Drawing.Point(30, 152);
this.lblAllowInterruptSoc.Name = "lblAllowInterruptSoc";
this.lblAllowInterruptSoc.Size = new System.Drawing.Size(135, 20);
this.lblAllowInterruptSoc.TabIndex = 8;
this.lblAllowInterruptSoc.Text = "允许中断电量 (%):";
//
// numFullChargeSoc
//
this.numFullChargeSoc.DecimalPlaces = 1;
this.numFullChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numFullChargeSoc.Location = new System.Drawing.Point(580, 95);
this.numFullChargeSoc.Name = "numFullChargeSoc";
this.numFullChargeSoc.Size = new System.Drawing.Size(120, 27);
this.numFullChargeSoc.TabIndex = 7;
//
// lblFullChargeSoc
//
this.lblFullChargeSoc.AutoSize = true;
this.lblFullChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblFullChargeSoc.Location = new System.Drawing.Point(400, 97);
this.lblFullChargeSoc.Name = "lblFullChargeSoc";
this.lblFullChargeSoc.Size = new System.Drawing.Size(99, 20);
this.lblFullChargeSoc.TabIndex = 6;
this.lblFullChargeSoc.Text = "满电电量 (%):";
//
// numTaskAvailableSoc
//
this.numTaskAvailableSoc.DecimalPlaces = 1;
this.numTaskAvailableSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numTaskAvailableSoc.Location = new System.Drawing.Point(250, 95);
this.numTaskAvailableSoc.Name = "numTaskAvailableSoc";
this.numTaskAvailableSoc.Size = new System.Drawing.Size(120, 27);
this.numTaskAvailableSoc.TabIndex = 5;
//
// lblTaskAvailableSoc
//
this.lblTaskAvailableSoc.AutoSize = true;
this.lblTaskAvailableSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblTaskAvailableSoc.Location = new System.Drawing.Point(30, 97);
this.lblTaskAvailableSoc.Name = "lblTaskAvailableSoc";
this.lblTaskAvailableSoc.Size = new System.Drawing.Size(135, 20);
this.lblTaskAvailableSoc.TabIndex = 4;
this.lblTaskAvailableSoc.Text = "任务可用电量 (%):";
//
// numIdleChargeSoc
//
this.numIdleChargeSoc.DecimalPlaces = 1;
this.numIdleChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numIdleChargeSoc.Location = new System.Drawing.Point(580, 40);
this.numIdleChargeSoc.Name = "numIdleChargeSoc";
this.numIdleChargeSoc.Size = new System.Drawing.Size(120, 27);
this.numIdleChargeSoc.TabIndex = 3;
//
// lblIdleChargeSoc
//
this.lblIdleChargeSoc.AutoSize = true;
this.lblIdleChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblIdleChargeSoc.Location = new System.Drawing.Point(400, 42);
this.lblIdleChargeSoc.Name = "lblIdleChargeSoc";
this.lblIdleChargeSoc.Size = new System.Drawing.Size(135, 20);
this.lblIdleChargeSoc.TabIndex = 2;
this.lblIdleChargeSoc.Text = "空闲充电电量 (%):";
//
// numMustChargeSoc
//
this.numMustChargeSoc.DecimalPlaces = 1;
this.numMustChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.numMustChargeSoc.Location = new System.Drawing.Point(250, 40);
this.numMustChargeSoc.Name = "numMustChargeSoc";
this.numMustChargeSoc.Size = new System.Drawing.Size(120, 27);
this.numMustChargeSoc.TabIndex = 1;
//
// lblMustChargeSoc
//
this.lblMustChargeSoc.AutoSize = true;
this.lblMustChargeSoc.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblMustChargeSoc.Location = new System.Drawing.Point(30, 42);
this.lblMustChargeSoc.Name = "lblMustChargeSoc";
this.lblMustChargeSoc.Size = new System.Drawing.Size(99, 20);
this.lblMustChargeSoc.TabIndex = 0;
this.lblMustChargeSoc.Text = "必充电量 (%):";
//
// pnlBottom
//
this.pnlBottom.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(250)))), ((int)(((byte)(250)))), ((int)(((byte)(250)))));
this.pnlBottom.Controls.Add(this.lblStatus);
this.pnlBottom.Controls.Add(this.btnApply);
this.pnlBottom.Controls.Add(this.btnRestoreDefaults);
this.pnlBottom.Controls.Add(this.btnCancel);
this.pnlBottom.Controls.Add(this.btnSave);
this.pnlBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
this.pnlBottom.Location = new System.Drawing.Point(0, 631);
this.pnlBottom.Name = "pnlBottom";
this.pnlBottom.Size = new System.Drawing.Size(784, 70);
this.pnlBottom.TabIndex = 1;
//
// lblStatus
//
this.lblStatus.AutoSize = true;
this.lblStatus.Font = new System.Drawing.Font("微软雅黑", 9F);
this.lblStatus.Location = new System.Drawing.Point(20, 25);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Size = new System.Drawing.Size(54, 20);
this.lblStatus.TabIndex = 4;
this.lblStatus.Text = "就绪...";
//
// btnApply
//
this.btnApply.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnApply.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnApply.Location = new System.Drawing.Point(564, 18);
this.btnApply.Name = "btnApply";
this.btnApply.Size = new System.Drawing.Size(100, 35);
this.btnApply.TabIndex = 3;
this.btnApply.Text = "应用";
this.btnApply.UseVisualStyleBackColor = true;
this.btnApply.Click += new System.EventHandler(this.btnApply_Click);
//
// btnRestoreDefaults
//
this.btnRestoreDefaults.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnRestoreDefaults.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnRestoreDefaults.Location = new System.Drawing.Point(344, 18);
this.btnRestoreDefaults.Name = "btnRestoreDefaults";
this.btnRestoreDefaults.Size = new System.Drawing.Size(100, 35);
this.btnRestoreDefaults.TabIndex = 2;
this.btnRestoreDefaults.Text = "恢复默认";
this.btnRestoreDefaults.UseVisualStyleBackColor = true;
this.btnRestoreDefaults.Click += new System.EventHandler(this.btnRestoreDefaults_Click);
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnCancel.Location = new System.Drawing.Point(674, 18);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(100, 35);
this.btnCancel.TabIndex = 1;
this.btnCancel.Text = "取消";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnSave
//
this.btnSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 9F);
this.btnSave.Location = new System.Drawing.Point(454, 18);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(100, 35);
this.btnSave.TabIndex = 0;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = true;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// ChargeStrategyConfigForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(784, 701);
this.Controls.Add(this.pnlMain);
this.Controls.Add(this.pnlBottom);
this.Name = "ChargeStrategyConfigForm";
this.Text = "充电策略配置";
this.pnlMain.ResumeLayout(false);
this.grpSwitchParams.ResumeLayout(false);
this.grpSwitchParams.PerformLayout();
this.grpTaskParams.ResumeLayout(false);
this.grpTaskParams.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numMinAllowFreeCarToChargeTaskCnt)).EndInit();
this.grpTimeParams.ResumeLayout(false);
this.grpTimeParams.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numTopUpMinutes)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numMustChargeSeconds)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleSeconds)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleChargeSeconds)).EndInit();
this.grpSocParams.ResumeLayout(false);
this.grpSocParams.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.numAllowInterruptSoc)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numFullChargeSoc)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numTaskAvailableSoc)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numIdleChargeSoc)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.numMustChargeSoc)).EndInit();
this.pnlBottom.ResumeLayout(false);
this.pnlBottom.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel pnlMain;
private System.Windows.Forms.GroupBox grpSocParams;
private System.Windows.Forms.NumericUpDown numMustChargeSoc;
private System.Windows.Forms.Label lblMustChargeSoc;
private System.Windows.Forms.NumericUpDown numIdleChargeSoc;
private System.Windows.Forms.Label lblIdleChargeSoc;
private System.Windows.Forms.NumericUpDown numTaskAvailableSoc;
private System.Windows.Forms.Label lblTaskAvailableSoc;
private System.Windows.Forms.NumericUpDown numFullChargeSoc;
private System.Windows.Forms.Label lblFullChargeSoc;
private System.Windows.Forms.NumericUpDown numAllowInterruptSoc;
private System.Windows.Forms.Label lblAllowInterruptSoc;
private System.Windows.Forms.GroupBox grpTimeParams;
private System.Windows.Forms.NumericUpDown numIdleChargeSeconds;
private System.Windows.Forms.Label lblIdleChargeSeconds;
private System.Windows.Forms.NumericUpDown numIdleSeconds;
private System.Windows.Forms.Label lblIdleSeconds;
private System.Windows.Forms.NumericUpDown numMustChargeSeconds;
private System.Windows.Forms.Label lblMustChargeSeconds;
private System.Windows.Forms.NumericUpDown numTopUpMinutes;
private System.Windows.Forms.Label lblTopUpMinutes;
private System.Windows.Forms.GroupBox grpTaskParams;
private System.Windows.Forms.NumericUpDown numMinAllowFreeCarToChargeTaskCnt;
private System.Windows.Forms.Label lblMinAllowFreeCarToChargeTaskCnt;
private System.Windows.Forms.GroupBox grpSwitchParams;
private System.Windows.Forms.CheckBox chkAllowInterruptTask;
private System.Windows.Forms.CheckBox chkUseLowerSocForCharge;
private System.Windows.Forms.CheckBox chkEnableErrorChargeDetection;
private System.Windows.Forms.CheckBox chkUseChargeSiteFilter;
private System.Windows.Forms.Panel pnlBottom;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Button btnRestoreDefaults;
private System.Windows.Forms.Button btnApply;
private System.Windows.Forms.Label lblStatus;
}
}
@@ -0,0 +1,215 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace StandardScene.Charge
{
/// <summary>
/// 充电策略配置窗体
/// </summary>
public partial class ChargeStrategyConfigForm : Form
{
private ChargeStrategyConfig config;
private ChargeStrategyConfigService configService;
public ChargeStrategyConfigForm()
{
InitializeComponent();
configService = ChargeStrategyConfigService.Instance;
InitializeForm();
}
private void InitializeForm()
{
this.Text = "充电策略配置";
this.Size = new Size(800, 700);
this.StartPosition = FormStartPosition.CenterScreen;
this.MinimumSize = new Size(700, 600);
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
// 加载配置
LoadConfig();
}
/// <summary>
/// 加载配置到界面
/// </summary>
private void LoadConfig(bool isDef = false)
{
try
{
if (!isDef)
{
config = configService.LoadConfig();
}
// SOC 相关参数
numMustChargeSoc.Value = (decimal)config.MustChargeSoc;
numIdleChargeSoc.Value = (decimal)config.IdleChargeSoc;
numTaskAvailableSoc.Value = (decimal)config.TaskAvailableSoc;
numFullChargeSoc.Value = (decimal)config.FullChargeSoc;
numAllowInterruptSoc.Value = (decimal)config.AllowInterruptSoc;
// 时间相关参数
numIdleChargeSeconds.Value = (decimal)config.IdleChargeSeconds;
numIdleSeconds.Value = (decimal)config.IdleSeconds;
numMustChargeSeconds.Value = (decimal)config.MustChargeSeconds;
numTopUpMinutes.Value = (decimal)config.TopUpMinutes;
// 任务相关参数
numMinAllowFreeCarToChargeTaskCnt.Value = config.MinAllowFreeCarToChargeTaskCnt;
// 开关参数
chkAllowInterruptTask.Checked = config.AllowInterruptTask;
chkUseLowerSocForCharge.Checked = config.UseLowerSocForCharge;
chkEnableErrorChargeDetection.Checked = config.EnableErrorChargeDetection;
chkUseChargeSiteFilter.Checked = config.UseChargeSiteFilter;
lblStatus.Text = "配置加载成功";
lblStatus.ForeColor = Color.Green;
}
catch (Exception ex)
{
MessageBox.Show($"加载配置失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "配置加载失败";
lblStatus.ForeColor = Color.Red;
}
}
/// <summary>
/// 从界面保存配置
/// </summary>
private void SaveConfig()
{
try
{
// SOC 相关参数
config.MustChargeSoc = (double)numMustChargeSoc.Value;
config.IdleChargeSoc = (double)numIdleChargeSoc.Value;
config.TaskAvailableSoc = (double)numTaskAvailableSoc.Value;
config.FullChargeSoc = (double)numFullChargeSoc.Value;
config.AllowInterruptSoc = (double)numAllowInterruptSoc.Value;
// 时间相关参数
config.IdleChargeSeconds = (double)numIdleChargeSeconds.Value;
config.IdleSeconds = (double)numIdleSeconds.Value;
config.MustChargeSeconds = (double)numMustChargeSeconds.Value;
config.TopUpMinutes = (double)numTopUpMinutes.Value;
// 任务相关参数
config.MinAllowFreeCarToChargeTaskCnt = (int)numMinAllowFreeCarToChargeTaskCnt.Value;
// 开关参数
config.AllowInterruptTask = chkAllowInterruptTask.Checked;
config.UseLowerSocForCharge = chkUseLowerSocForCharge.Checked;
config.EnableErrorChargeDetection = chkEnableErrorChargeDetection.Checked;
config.UseChargeSiteFilter = chkUseChargeSiteFilter.Checked;
// 保存到文件
configService.SaveConfig(config);
lblStatus.Text = "配置保存成功";
lblStatus.ForeColor = Color.Green;
MessageBox.Show("充电策略配置保存成功!", "成功",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"保存配置失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "配置保存失败";
lblStatus.ForeColor = Color.Red;
}
}
/// <summary>
/// 恢复默认配置
/// </summary>
private void RestoreDefaults()
{
var result = MessageBox.Show(
"确定要恢复默认配置吗?当前配置将被覆盖。",
"确认恢复",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
config = ChargeStrategyConfig.CreateDefault();
LoadConfig(true);
lblStatus.Text = "已恢复默认配置(未保存)";
lblStatus.ForeColor = Color.Blue;
}
}
/// <summary>
/// 验证配置参数
/// </summary>
private bool ValidateConfig()
{
// 验证 SOC 范围
if (numMustChargeSoc.Value >= numIdleChargeSoc.Value)
{
MessageBox.Show("必充电量必须小于空闲充电电量", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
if (numTaskAvailableSoc.Value <= numMustChargeSoc.Value)
{
MessageBox.Show("任务可用电量必须大于必充电量", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
if (numFullChargeSoc.Value < numIdleChargeSoc.Value)
{
MessageBox.Show("满电电量必须大于等于空闲充电电量", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
if (numAllowInterruptSoc.Value <= numMustChargeSoc.Value)
{
MessageBox.Show("允许中断电量必须大于必充电量", "验证失败",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
// ==================== 事件处理 ====================
private void btnSave_Click(object sender, EventArgs e)
{
if (ValidateConfig())
{
SaveConfig();
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
this.Close();
}
private void btnRestoreDefaults_Click(object sender, EventArgs e)
{
RestoreDefaults();
}
private void btnApply_Click(object sender, EventArgs e)
{
if (ValidateConfig())
{
SaveConfig();
}
}
}
}
@@ -0,0 +1,203 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace StandardScene.Charge
{
/// <summary>
/// 充电策略配置服务(单例模式)
/// </summary>
public class ChargeStrategyConfigService
{
private static ChargeStrategyConfigService _instance;
private static readonly object _lock = new object();
private readonly string configFilePath;
private const string ConfigFileName = "ChargeStrategyConfig.json";
/// <summary>
/// 获取单例实例
/// </summary>
public static ChargeStrategyConfigService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ChargeStrategyConfigService();
}
}
}
return _instance;
}
}
private ChargeStrategyConfigService()
{
// 配置文件保存在应用程序目录下的 Config 文件夹
string configDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config");
// 确保目录存在
if (!Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
configFilePath = Path.Combine(configDir, ConfigFileName);
}
/// <summary>
/// 加载配置
/// </summary>
public ChargeStrategyConfig LoadConfig()
{
try
{
if (File.Exists(configFilePath))
{
string json = File.ReadAllText(configFilePath);
var config = JsonConvert.DeserializeObject<ChargeStrategyConfig>(json);
// 验证配置
if (config.Validate(out string errorMessage))
{
return config;
}
else
{
// 配置无效,返回默认配置
System.Diagnostics.Debug.WriteLine($"配置验证失败: {errorMessage},使用默认配置");
return ChargeStrategyConfig.CreateDefault();
}
}
else
{
// 文件不存在,创建默认配置并保存
var defaultConfig = ChargeStrategyConfig.CreateDefault();
SaveConfig(defaultConfig);
return defaultConfig;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"加载配置失败: {ex.Message}");
// 加载失败,返回默认配置
return ChargeStrategyConfig.CreateDefault();
}
}
/// <summary>
/// 保存配置
/// </summary>
public void SaveConfig(ChargeStrategyConfig config)
{
try
{
// 验证配置
if (!config.Validate(out string errorMessage))
{
throw new InvalidOperationException($"配置验证失败: {errorMessage}");
}
// 序列化为 JSON
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
// 保存到文件
File.WriteAllText(configFilePath, json);
System.Diagnostics.Debug.WriteLine($"配置保存成功: {configFilePath}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"保存配置失败: {ex.Message}");
throw new Exception($"保存配置失败: {ex.Message}", ex);
}
}
/// <summary>
/// 获取配置文件路径
/// </summary>
public string GetConfigFilePath()
{
return configFilePath;
}
/// <summary>
/// 检查配置文件是否存在
/// </summary>
public bool ConfigFileExists()
{
return File.Exists(configFilePath);
}
/// <summary>
/// 删除配置文件
/// </summary>
public void DeleteConfig()
{
try
{
if (File.Exists(configFilePath))
{
File.Delete(configFilePath);
System.Diagnostics.Debug.WriteLine($"配置文件已删除: {configFilePath}");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"删除配置文件失败: {ex.Message}");
throw new Exception($"删除配置文件失败: {ex.Message}", ex);
}
}
/// <summary>
/// 导出配置到指定路径
/// </summary>
public void ExportConfig(string exportPath, ChargeStrategyConfig config)
{
try
{
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(exportPath, json);
System.Diagnostics.Debug.WriteLine($"配置导出成功: {exportPath}");
}
catch (Exception ex)
{
throw new Exception($"导出配置失败: {ex.Message}", ex);
}
}
/// <summary>
/// 从指定路径导入配置
/// </summary>
public ChargeStrategyConfig ImportConfig(string importPath)
{
try
{
if (!File.Exists(importPath))
{
throw new FileNotFoundException($"配置文件不存在: {importPath}");
}
string json = File.ReadAllText(importPath);
var config = JsonConvert.DeserializeObject<ChargeStrategyConfig>(json);
// 验证配置
if (!config.Validate(out string errorMessage))
{
throw new InvalidOperationException($"配置验证失败: {errorMessage}");
}
return config;
}
catch (Exception ex)
{
throw new Exception($"导入配置失败: {ex.Message}", ex);
}
}
}
}
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SimpleLite;
using SimpleCore;
using SimpleCore.Library;
using StandardScene.ChargeStationType;
namespace StandardScene.Charge
{
/// <summary>
/// 充电桩udp监听
/// </summary>
public class ChargeUdpService
{
public Thread ListenerThread;
public ChargeUdpService()
{
ListenerThread = new Thread(ListenerProcess);
ListenerThread.Start();
}
private static async void ListenerProcess()
{
var messageService = CommunicationMessageService.Instance;
using (UdpClient udpListener = new UdpClient(40001))
{
Diagnosis.Log($"Listening for UDP messages on port {40001}");
while (true)
{
try
{
var result = await udpListener.ReceiveAsync();
var remoteEndPoint = result.RemoteEndPoint;
var message = result.Buffer;
messageService.AddReceiveMessage(remoteEndPoint.Address.ToString(), 40001, BitConverter.ToString(message).Replace("-", " "), "FRLDShort");
Diagnosis.Log($"ChargeStation ADD:[{BitConverter.ToString(message).Replace("-", " ")}]","UDP返回报文信息",true);
var chargeMission = SimpleProject.proj.Missions.OfType<StandardChargeMission>().FirstOrDefault();
if (chargeMission == null) continue;
var chargeStation =
chargeMission.ChargeStations.FirstOrDefault(c =>
c.Value.Ip == remoteEndPoint.Address.ToString()).Value;
if (chargeStation == null) continue;
if (message.Length > 28)
chargeStation.IsSafe = message[28] == 1;
chargeStation.OnUdpMessage(message);
}
catch (Exception e)
{
// 单帧异常(含越界/半包)不得中断 UDP 监听线程
Diagnosis.Log($"充电UDP接收处理异常: {e.Message}", "UDP", true);
}
}
}
}
}
}
@@ -0,0 +1,89 @@
using System;
namespace StandardScene.Charge
{
/// <summary>
/// 通讯报文数据模型
/// </summary>
public class CommunicationMessage
{
/// <summary>
/// 报文ID(自动生成)
/// </summary>
public string MessageId { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// 方向(发送/接收)
/// </summary>
public MessageDirection Direction { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// 端口号
/// </summary>
public int Port { get; set; }
/// <summary>
/// 原始报文数据(十六进制字符串)
/// </summary>
public string RawData { get; set; }
/// <summary>
/// 报文长度(字节)
/// </summary>
public int Length { get; set; }
/// <summary>
/// 协议类型
/// </summary>
public string Type { get; set; }
/// <summary>
/// 关联的充电桩ID(可选)
/// </summary>
public string StationId { get; set; }
public CommunicationMessage()
{
MessageId = GenerateMessageId();
Timestamp = DateTime.Now;
}
private static string GenerateMessageId()
{
return $"MSG{DateTime.Now:yyyyMMddHHmmssfff}{new Random().Next(100, 999)}";
}
public override string ToString()
{
return $"[{Timestamp:HH:mm:ss.fff}] {Direction} {IpAddress}:{Port} - {Length}字节";
}
}
//解析后的数据
/// <summary>
/// 报文方向枚举
/// </summary>
public enum MessageDirection
{
/// <summary>
/// 发送
/// </summary>
Send = 0,
/// <summary>
/// 接收
/// </summary>
Receive = 1
}
}
@@ -0,0 +1,587 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace StandardScene.Charge
{
/// <summary>
/// 通讯报文数据服务(单例模式)
/// </summary>
public class CommunicationMessageService
{
private static CommunicationMessageService _instance;
private static readonly object _lock = new object();
private readonly object _dataLock = new object();
private readonly AlarmConfigDataService dataService;
private readonly LinkedList<CommunicationMessage> _messages;
private const int MaxMessages = 100; // 最多保留100条
/// <summary>
/// 报文添加事件
/// </summary>
public event EventHandler<CommunicationMessage> MessageAdded;
/// <summary>
/// 获取单例实例
/// </summary>
public static CommunicationMessageService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new CommunicationMessageService();
}
}
}
return _instance;
}
}
private CommunicationMessageService()
{
_messages = new LinkedList<CommunicationMessage>();
dataService = AlarmConfigDataService.Instance;
}
/// <summary>
/// 添加报文
/// </summary>
public void AddMessage(CommunicationMessage message)
{
if (message == null)
return;
lock (_dataLock)
{
// 添加到链表头部(最新的在前面)
_messages.AddFirst(message);
// 如果超过最大数量,移除最旧的
while (_messages.Count > MaxMessages)
{
_messages.RemoveLast();
}
}
// 触发事件
MessageAdded?.Invoke(this, message);
}
/// <summary>
/// 添加发送报文
/// </summary>
public void AddSendMessage(string ipAddress, int port, string rawData, string type, string stationId = null)
{
var message = new CommunicationMessage
{
Direction = MessageDirection.Send,
IpAddress = ipAddress,
Port = port,
RawData = rawData,
Length = rawData.Split(' ')?.Length ?? 0, // 假设是十六进制字符串
StationId = stationId,
Type = type
};
AddMessage(message);
// 发送报文后,解析并更新充电桩数据(发送方向)
ParseSendDataAndUpdateStation(ipAddress, port, rawData, type);
}
/// <summary>
/// 添加接收报文
/// </summary>
public void AddReceiveMessage(string ipAddress, int port, string rawData, string type, string stationId = null)
{
var message = new CommunicationMessage
{
Direction = MessageDirection.Receive,
IpAddress = ipAddress,
Port = port,
RawData = rawData,
Length = rawData.Split(' ')?.Length ?? 0,
StationId = stationId,
Type = type
};
AddMessage(message);
// 接收到报文后,解析并更新充电桩数据(接收方向)
ParseReceiveDataAndUpdateStation(ipAddress, port, rawData, type);
}
/// <summary>
/// 解析发送报文并更新充电桩数据
/// </summary>
private void ParseSendDataAndUpdateStation(string ipAddress, int port, string rawData, string type)
{
try
{
var dataService = ChargeStationDataService.Instance;
// 根据IP地址查找充电桩
var station = dataService.GetStationByIp(ipAddress, port);
if (station == null)
{
return; // 未找到对应充电桩,不处理
}
// 解析发送报文数据
var parsedData = ParseSendRawData(rawData, type);
if (parsedData == null)
{
return; // 解析失败,不处理
}
// 更新充电桩数据(发送方向)
UpdateStationFromSendData(station, parsedData);
// 更新到数据服务
dataService.UpdateStation(station, out string errorMessage);
}
catch
{
// 静默处理异常,不影响报文记录
}
}
/// <summary>
/// 解析接收报文并更新充电桩数据
/// </summary>
public void ParseReceiveDataAndUpdateStation(string ipAddress, int port, string rawData, string type)
{
try
{
var dataService = ChargeStationDataService.Instance;
// 根据IP地址查找充电桩
var station = dataService.GetStationByIp(ipAddress);
if (station == null)
{
return; // 未找到对应充电桩,不处理
}
// 解析接收报文数据
var parsedData = ParseReceiveRawData(rawData, type);
if (parsedData == null)
{
return; // 解析失败,不处理
}
// 更新充电桩数据(接收方向)
UpdateStationFromReceiveData(station, parsedData);
// 合并发送和接收数据,更新到数据服务
dataService.UpdateStation(station, out string errorMessage);
}
catch
{
// 静默处理异常,不影响报文记录
}
}
/// <summary>
/// 解析发送报文数据
/// </summary>
public ParsedSendData ParseSendRawData(string rawData, string type)
{
try
{
// 将逗号分隔的字符串转换为字节数组
var parts = rawData.Split(' ');
if (parts.Length < 10) // 发送报文至少10字节
{
return null;
}
var bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!byte.TryParse(parts[i], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out bytes[i]))
{
return null;
}
}
byte chargeCommand = 0;
double setVoltage = 0;
double setCurrent = 0;
short carId = 0;
int carSoc = 0;
double carVoltage = 0;
double carCurrent = 0;
if (type == "FRLDShort")
{
chargeCommand = bytes[2];
setCurrent = BitConverter.ToInt32(new byte[] { bytes[6], bytes[5], bytes[4], bytes[3] }, 0) / 10f;
setVoltage = BitConverter.ToInt32(new byte[] { bytes[10], bytes[9], bytes[8], bytes[7] }, 0) / 10f;
carId = BitConverter.ToInt16(new byte[] { bytes[14], bytes[13] }, 0);
carSoc = bytes[15];
carVoltage = BitConverter.ToInt32(new byte[] { bytes[19], bytes[18], bytes[17], bytes[16] }, 0);
carCurrent = BitConverter.ToInt32(new byte[] { bytes[23], bytes[22], bytes[21], bytes[20] }, 0);
}
else if (type == "FRLDTall")
{
chargeCommand = bytes[1];
setCurrent = BitConverter.ToSingle(new byte[] { bytes[5], bytes[4], bytes[3], bytes[2] }, 0);
setVoltage = BitConverter.ToSingle(new byte[] { bytes[9], bytes[8], bytes[7], bytes[6] }, 0);
carId = BitConverter.ToInt16(new byte[] { bytes[13], bytes[12] }, 0);
carSoc = BitConverter.ToInt16(new byte[] { bytes[15], bytes[14] }, 0);
carVoltage = BitConverter.ToSingle(new byte[] { bytes[19], bytes[18], bytes[17], bytes[16] }, 0);
carCurrent = BitConverter.ToSingle(new byte[] { bytes[23], bytes[22], bytes[21], bytes[20] }, 0);
}
// 解析发送报文(根据实际协议)
var parsed = new ParsedSendData
{
ChargeCommand = chargeCommand,
SetVoltage = setVoltage,
SetCurrent = setCurrent,
CurrentVehicleId = carId,
BatteryLevel = carSoc,
CarVoltage = carVoltage,
CarCurrent = carCurrent,
// 发送时间
SendTime = DateTime.Now
};
return parsed;
}
catch
{
return null;
}
}
/// <summary>
/// 解析接收报文数据
/// </summary>
public ParsedReceiveData ParseReceiveRawData(string rawData, string type)
{
try
{
// 将逗号分隔的字符串转换为字节数组
var parts = rawData.Split(' ');
if (parts.Length < 30) // 假设报文至少30字节
{
return null;
}
var bytes = new byte[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!byte.TryParse(parts[i], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out bytes[i]))
{
return null;
}
}
double realTimeVoltage = 0;
double realTimeCurrent = 0;
byte chargeStationStatus = 0;
byte chargeId = 0;
short batteryAH = 0;
byte mechanismStatus = 0;
byte alarmValue = 0;
if (type == "FRLDShort")
{
realTimeCurrent = BitConverter.ToInt32(new byte[] { bytes[5], bytes[4], bytes[3], bytes[2] }, 0) / 10f;
realTimeVoltage = BitConverter.ToInt32(new byte[] { bytes[9], bytes[8], bytes[7], bytes[6] }, 0) / 10f;
chargeStationStatus = bytes[14];
chargeId = bytes[15];
batteryAH = BitConverter.ToInt16(new byte[] { bytes[17], bytes[16] }, 0);
mechanismStatus = bytes[28];
}
else if (type == "FRLDTall")
{
chargeStationStatus = bytes[14];
mechanismStatus = bytes[28];
alarmValue = bytes[15];
}
// 根据实际协议解析接收数据
var parsed = new ParsedReceiveData
{
RealTimeCurrent = realTimeCurrent,
RealTimeVoltage = realTimeVoltage,
Status = type == "FRLDTall" ? ParseStationStatusFRLDTall(chargeStationStatus) : ParseStationStatus(chargeStationStatus),
ChargeID = chargeId,
BatteryAH = batteryAH,
MechanismStatus = type == "FRLDTall" ? ParseMechanismStatusFRLDTall(mechanismStatus) : ParseMechanismStatus(mechanismStatus),
HasAlarm = chargeStationStatus == 2,
AlarmCode = alarmValue,
// AlarmLevel = ParseAlarmLevel(bytes[20])
ReceiveTime = DateTime.Now
};
return parsed;
}
catch
{
return null;
}
}
/// <summary>
/// 从发送报文更新充电桩数据
/// </summary>
private void UpdateStationFromSendData(ChargeStation station, ParsedSendData parsedData)
{
// 更新发送的设定值
//station.SetVoltage = parsedData.SetVoltage;
//station.SetElectricCurrent = parsedData.SetCurrent;
// 更新最后发送时间
station.LastSendTime = parsedData.SendTime;
// 根据发送的充电指令更新状态
if (parsedData.ChargeCommand == 1)
{
// 发送了启动充电指令
station.ChargeCommandStatus = ChargeCommandStatus.Started;
}
else if (parsedData.ChargeCommand == 0)
{
// 发送了停止充电指令
station.ChargeCommandStatus = ChargeCommandStatus.Stopped;
}
station.BatteryLevel = parsedData.BatteryLevel;
station.CurrentVehicle = parsedData.CurrentVehicleId.ToString();
}
/// <summary>
/// 从接收报文更新充电桩数据
/// </summary>
private void UpdateStationFromReceiveData(ChargeStation station, ParsedReceiveData parsedData)
{
station.LastReceiveTime = parsedData.ReceiveTime;
station.MechanismStatus = parsedData.MechanismStatus;
station.RealTimeVoltage = parsedData.RealTimeVoltage;
station.RealTimeCurrent = parsedData.RealTimeCurrent;
station.Status = parsedData.Status;
station.HasAlarm = parsedData.HasAlarm;
station.AlarmLevel = parsedData.AlarmLevel;
if (parsedData.HasAlarm)
{
var alarmInfo = dataService.GetAlarmConfigAlarmCode(parsedData.AlarmCode);
if (alarmInfo != null)
{
station.AlarmMessage = $"报警级别: {GetAlarmLevelText(alarmInfo.Level)}:{alarmInfo.AlarmContent}";
}
}
else
{
station.AlarmMessage = string.Empty;
}
}
/// <summary>
/// 解析机构状态
/// </summary>
private MechanismStatus ParseMechanismStatus(byte statusByte)
{
switch (statusByte)
{
case 1: return MechanismStatus.Retracted;
case 2: return MechanismStatus.Extended;
default: return MechanismStatus.Extending;
}
}
/// <summary>
/// 解析机构状态
/// </summary>
private MechanismStatus ParseMechanismStatusFRLDTall(byte statusByte)
{
switch (statusByte)
{
case 1: return MechanismStatus.Extended;
case 2: return MechanismStatus.Retracted;
default: return MechanismStatus.Extending;
}
}
/// <summary>
/// 解析报警级别
/// </summary>
private AlarmLevel ParseAlarmLevel(byte alarmByte)
{
if (alarmByte == 0) return AlarmLevel.None;
if (alarmByte <= 2) return AlarmLevel.Low;
if (alarmByte <= 5) return AlarmLevel.Medium;
if (alarmByte <= 8) return AlarmLevel.High;
return AlarmLevel.Critical;
}
/// <summary>
/// 解析充电桩状态
/// </summary>
private ChargeStationStatus ParseStationStatus(byte statusByte)
{
switch (statusByte)
{
case 0: return ChargeStationStatus.Idle;
case 1: return ChargeStationStatus.Charging;
case 2: return ChargeStationStatus.Fault;
case 3: return ChargeStationStatus.Battery;
default: return ChargeStationStatus.Idle;
}
}
private ChargeStationStatus ParseStationStatusFRLDTall(byte statusByte)
{
switch (statusByte)
{
case 0: return ChargeStationStatus.Idle;
case 2: return ChargeStationStatus.Idle;
case 3: return ChargeStationStatus.Charging;
case 4: return ChargeStationStatus.Fault;
default: return ChargeStationStatus.Idle;
}
}
/// <summary>
/// 获取报警级别文本
/// </summary>
private string GetAlarmLevelText(AlarmLevel level)
{
switch (level)
{
case AlarmLevel.None: return "无";
case AlarmLevel.Low: return "低";
case AlarmLevel.Medium: return "中";
case AlarmLevel.High: return "高";
case AlarmLevel.Critical: return "严重";
default: return "未知";
}
}
/// <summary>
/// 解析后的发送报文数据(内部类)
/// </summary>
public class ParsedSendData
{
public byte ChargeCommand { get; set; }
public double SetVoltage { get; set; }
public double SetCurrent { get; set; }
public double BatteryLevel { get; set; }
public int CurrentVehicleId { get; set; }
public double CarVoltage { get; set; }
public double CarCurrent { get; set; }
public DateTime SendTime { get; set; }
}
/// <summary>
/// 解析后的接收报文数据(内部类)
/// </summary>
public class ParsedReceiveData
{
public CommunicationStatus CommStatus { get; set; }
public ChargeCommandStatus ChargeCommandStatus { get; set; }
public MechanismStatus MechanismStatus { get; set; }
public double RealTimeVoltage { get; set; }
public double RealTimeCurrent { get; set; }
public int ChargeID { get; set; }
public float BatteryAH { get; set; }
public bool HasAlarm { get; set; }
public AlarmLevel AlarmLevel { get; set; }
public int AlarmCode { get; set; }
public ChargeStationStatus Status { get; set; }
public DateTime ReceiveTime { get; set; }
}
/// <summary>
/// 获取所有报文
/// </summary>
public List<CommunicationMessage> GetAllMessages()
{
lock (_dataLock)
{
return _messages.ToList();
}
}
/// <summary>
/// 根据IP筛选报文
/// </summary>
public List<CommunicationMessage> GetMessagesByIp(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
return GetAllMessages();
lock (_dataLock)
{
return _messages.Where(m => m.IpAddress == ipAddress).ToList();
}
}
/// <summary>
/// 根据充电桩ID筛选报文
/// </summary>
public List<CommunicationMessage> GetMessagesByStationId(string stationId)
{
if (string.IsNullOrWhiteSpace(stationId))
return GetAllMessages();
lock (_dataLock)
{
return _messages.Where(m => m.StationId == stationId).ToList();
}
}
/// <summary>
/// 清空所有报文
/// </summary>
public void Clear()
{
lock (_dataLock)
{
_messages.Clear();
}
}
/// <summary>
/// 获取所有唯一IP地址列表
/// </summary>
public List<string> GetUniqueIpAddresses()
{
lock (_dataLock)
{
return _messages
.Select(m => m.IpAddress)
.Distinct()
.OrderBy(ip => ip)
.ToList();
}
}
}
}
@@ -0,0 +1,376 @@
namespace StandardScene.Charge
{
partial class CommunicationMonitorForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.splitContainer = new System.Windows.Forms.SplitContainer();
this.pnlLeft = new System.Windows.Forms.Panel();
this.dgvMessages = new System.Windows.Forms.DataGridView();
this.colTime = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colDirection = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colIpAddress = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPort = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colLength = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colRawData = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colStationId = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.type = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.pnlLeftTop = new System.Windows.Forms.Panel();
this.button1 = new System.Windows.Forms.Button();
this.btnClear = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.lblStatistics = new System.Windows.Forms.Label();
this.cmbIpFilter = new System.Windows.Forms.ComboBox();
this.lblIpFilter = new System.Windows.Forms.Label();
this.pnlRight = new System.Windows.Forms.Panel();
this.txtParsedData = new System.Windows.Forms.TextBox();
this.pnlRightTop = new System.Windows.Forms.Panel();
this.btnClose = new System.Windows.Forms.Button();
this.lblParsedTitle = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit();
this.splitContainer.Panel1.SuspendLayout();
this.splitContainer.Panel2.SuspendLayout();
this.splitContainer.SuspendLayout();
this.pnlLeft.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvMessages)).BeginInit();
this.pnlLeftTop.SuspendLayout();
this.pnlRight.SuspendLayout();
this.pnlRightTop.SuspendLayout();
this.SuspendLayout();
//
// splitContainer
//
this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer.Location = new System.Drawing.Point(0, 0);
this.splitContainer.Name = "splitContainer";
//
// splitContainer.Panel1
//
this.splitContainer.Panel1.Controls.Add(this.pnlLeft);
//
// splitContainer.Panel2
//
this.splitContainer.Panel2.Controls.Add(this.pnlRight);
this.splitContainer.Size = new System.Drawing.Size(1400, 800);
this.splitContainer.SplitterDistance = 850;
this.splitContainer.TabIndex = 0;
//
// pnlLeft
//
this.pnlLeft.Controls.Add(this.dgvMessages);
this.pnlLeft.Controls.Add(this.pnlLeftTop);
this.pnlLeft.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlLeft.Location = new System.Drawing.Point(0, 0);
this.pnlLeft.Name = "pnlLeft";
this.pnlLeft.Size = new System.Drawing.Size(850, 800);
this.pnlLeft.TabIndex = 0;
//
// dgvMessages
//
this.dgvMessages.AllowUserToAddRows = false;
this.dgvMessages.AllowUserToDeleteRows = false;
this.dgvMessages.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dgvMessages.BackgroundColor = System.Drawing.Color.White;
this.dgvMessages.BorderStyle = System.Windows.Forms.BorderStyle.None;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(81)))), ((int)(((byte)(181)))));
dataGridViewCellStyle1.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
dataGridViewCellStyle1.ForeColor = System.Drawing.Color.White;
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.dgvMessages.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
this.dgvMessages.ColumnHeadersHeight = 35;
this.dgvMessages.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colTime,
this.colDirection,
this.colIpAddress,
this.colPort,
this.colLength,
this.colRawData,
this.colStationId,
this.type});
this.dgvMessages.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvMessages.EnableHeadersVisualStyles = false;
this.dgvMessages.GridColor = System.Drawing.Color.LightGray;
this.dgvMessages.Location = new System.Drawing.Point(0, 80);
this.dgvMessages.MultiSelect = false;
this.dgvMessages.Name = "dgvMessages";
this.dgvMessages.ReadOnly = true;
this.dgvMessages.RowHeadersVisible = false;
this.dgvMessages.RowHeadersWidth = 51;
this.dgvMessages.RowTemplate.Height = 30;
this.dgvMessages.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dgvMessages.Size = new System.Drawing.Size(850, 720);
this.dgvMessages.TabIndex = 1;
this.dgvMessages.SelectionChanged += new System.EventHandler(this.dgvMessages_SelectionChanged);
//
// colTime
//
this.colTime.FillWeight = 80F;
this.colTime.HeaderText = "时间";
this.colTime.MinimumWidth = 6;
this.colTime.Name = "colTime";
this.colTime.ReadOnly = true;
//
// colDirection
//
this.colDirection.FillWeight = 50F;
this.colDirection.HeaderText = "方向";
this.colDirection.MinimumWidth = 6;
this.colDirection.Name = "colDirection";
this.colDirection.ReadOnly = true;
//
// colIpAddress
//
this.colIpAddress.FillWeight = 80F;
this.colIpAddress.HeaderText = "IP地址";
this.colIpAddress.MinimumWidth = 6;
this.colIpAddress.Name = "colIpAddress";
this.colIpAddress.ReadOnly = true;
//
// colPort
//
this.colPort.FillWeight = 50F;
this.colPort.HeaderText = "端口";
this.colPort.MinimumWidth = 6;
this.colPort.Name = "colPort";
this.colPort.ReadOnly = true;
//
// colLength
//
this.colLength.FillWeight = 50F;
this.colLength.HeaderText = "长度";
this.colLength.MinimumWidth = 6;
this.colLength.Name = "colLength";
this.colLength.ReadOnly = true;
//
// colRawData
//
this.colRawData.FillWeight = 200F;
this.colRawData.HeaderText = "原始数据";
this.colRawData.MinimumWidth = 6;
this.colRawData.Name = "colRawData";
this.colRawData.ReadOnly = true;
//
// colStationId
//
this.colStationId.FillWeight = 80F;
this.colStationId.HeaderText = "充电桩";
this.colStationId.MinimumWidth = 6;
this.colStationId.Name = "colStationId";
this.colStationId.ReadOnly = true;
//
// type
//
this.type.HeaderText = "协议类型";
this.type.MinimumWidth = 6;
this.type.Name = "type";
this.type.ReadOnly = true;
//
// pnlLeftTop
//
this.pnlLeftTop.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(250)))), ((int)(((byte)(250)))), ((int)(((byte)(250)))));
this.pnlLeftTop.Controls.Add(this.button1);
this.pnlLeftTop.Controls.Add(this.btnClear);
this.pnlLeftTop.Controls.Add(this.btnRefresh);
this.pnlLeftTop.Controls.Add(this.lblStatistics);
this.pnlLeftTop.Controls.Add(this.cmbIpFilter);
this.pnlLeftTop.Controls.Add(this.lblIpFilter);
this.pnlLeftTop.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlLeftTop.Location = new System.Drawing.Point(0, 0);
this.pnlLeftTop.Name = "pnlLeftTop";
this.pnlLeftTop.Padding = new System.Windows.Forms.Padding(10);
this.pnlLeftTop.Size = new System.Drawing.Size(850, 80);
this.pnlLeftTop.TabIndex = 0;
//
// button1
//
this.button1.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.button1.Location = new System.Drawing.Point(546, 16);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(80, 32);
this.button1.TabIndex = 5;
this.button1.Text = "暂停";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// btnClear
//
this.btnClear.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnClear.Location = new System.Drawing.Point(460, 15);
this.btnClear.Name = "btnClear";
this.btnClear.Size = new System.Drawing.Size(80, 32);
this.btnClear.TabIndex = 4;
this.btnClear.Text = "清空";
this.btnClear.UseVisualStyleBackColor = true;
this.btnClear.Click += new System.EventHandler(this.btnClear_Click);
//
// btnRefresh
//
this.btnRefresh.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnRefresh.Location = new System.Drawing.Point(370, 15);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(80, 32);
this.btnRefresh.TabIndex = 3;
this.btnRefresh.Text = "刷新";
this.btnRefresh.UseVisualStyleBackColor = true;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// lblStatistics
//
this.lblStatistics.AutoSize = true;
this.lblStatistics.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblStatistics.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(100)))), ((int)(((byte)(100)))), ((int)(((byte)(100)))));
this.lblStatistics.Location = new System.Drawing.Point(13, 52);
this.lblStatistics.Name = "lblStatistics";
this.lblStatistics.Size = new System.Drawing.Size(115, 20);
this.lblStatistics.TabIndex = 2;
this.lblStatistics.Text = "显示: 0 | 总数: 0";
//
// cmbIpFilter
//
this.cmbIpFilter.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbIpFilter.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.cmbIpFilter.FormattingEnabled = true;
this.cmbIpFilter.Location = new System.Drawing.Point(100, 17);
this.cmbIpFilter.Name = "cmbIpFilter";
this.cmbIpFilter.Size = new System.Drawing.Size(250, 28);
this.cmbIpFilter.TabIndex = 1;
this.cmbIpFilter.SelectedIndexChanged += new System.EventHandler(this.cmbIpFilter_SelectedIndexChanged);
//
// lblIpFilter
//
this.lblIpFilter.AutoSize = true;
this.lblIpFilter.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblIpFilter.Location = new System.Drawing.Point(13, 21);
this.lblIpFilter.Name = "lblIpFilter";
this.lblIpFilter.Size = new System.Drawing.Size(67, 20);
this.lblIpFilter.TabIndex = 0;
this.lblIpFilter.Text = "IP筛选:";
//
// pnlRight
//
this.pnlRight.Controls.Add(this.txtParsedData);
this.pnlRight.Controls.Add(this.pnlRightTop);
this.pnlRight.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlRight.Location = new System.Drawing.Point(0, 0);
this.pnlRight.Name = "pnlRight";
this.pnlRight.Size = new System.Drawing.Size(546, 800);
this.pnlRight.TabIndex = 0;
//
// txtParsedData
//
this.txtParsedData.BackColor = System.Drawing.Color.White;
this.txtParsedData.Dock = System.Windows.Forms.DockStyle.Fill;
this.txtParsedData.Font = new System.Drawing.Font("Consolas", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.txtParsedData.Location = new System.Drawing.Point(0, 60);
this.txtParsedData.Multiline = true;
this.txtParsedData.Name = "txtParsedData";
this.txtParsedData.ReadOnly = true;
this.txtParsedData.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.txtParsedData.Size = new System.Drawing.Size(546, 740);
this.txtParsedData.TabIndex = 1;
this.txtParsedData.WordWrap = false;
//
// pnlRightTop
//
this.pnlRightTop.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(250)))), ((int)(((byte)(250)))), ((int)(((byte)(250)))));
this.pnlRightTop.Controls.Add(this.btnClose);
this.pnlRightTop.Controls.Add(this.lblParsedTitle);
this.pnlRightTop.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlRightTop.Location = new System.Drawing.Point(0, 0);
this.pnlRightTop.Name = "pnlRightTop";
this.pnlRightTop.Padding = new System.Windows.Forms.Padding(10);
this.pnlRightTop.Size = new System.Drawing.Size(546, 60);
this.pnlRightTop.TabIndex = 0;
//
// btnClose
//
this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.btnClose.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnClose.Location = new System.Drawing.Point(446, 15);
this.btnClose.Name = "btnClose";
this.btnClose.Size = new System.Drawing.Size(80, 32);
this.btnClose.TabIndex = 1;
this.btnClose.Text = "关闭";
this.btnClose.UseVisualStyleBackColor = true;
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// lblParsedTitle
//
this.lblParsedTitle.AutoSize = true;
this.lblParsedTitle.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.lblParsedTitle.Location = new System.Drawing.Point(13, 20);
this.lblParsedTitle.Name = "lblParsedTitle";
this.lblParsedTitle.Size = new System.Drawing.Size(112, 24);
this.lblParsedTitle.TabIndex = 0;
this.lblParsedTitle.Text = "报文数据解析";
//
// CommunicationMonitorForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1400, 800);
this.Controls.Add(this.splitContainer);
this.Name = "CommunicationMonitorForm";
this.Text = "通讯监控";
this.Load += new System.EventHandler(this.CommunicationMonitorForm_Load);
this.splitContainer.Panel1.ResumeLayout(false);
this.splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit();
this.splitContainer.ResumeLayout(false);
this.pnlLeft.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.dgvMessages)).EndInit();
this.pnlLeftTop.ResumeLayout(false);
this.pnlLeftTop.PerformLayout();
this.pnlRight.ResumeLayout(false);
this.pnlRight.PerformLayout();
this.pnlRightTop.ResumeLayout(false);
this.pnlRightTop.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.Panel pnlLeft;
private System.Windows.Forms.DataGridView dgvMessages;
private System.Windows.Forms.Panel pnlLeftTop;
private System.Windows.Forms.ComboBox cmbIpFilter;
private System.Windows.Forms.Label lblIpFilter;
private System.Windows.Forms.Panel pnlRight;
private System.Windows.Forms.TextBox txtParsedData;
private System.Windows.Forms.Panel pnlRightTop;
private System.Windows.Forms.Label lblParsedTitle;
private System.Windows.Forms.Label lblStatistics;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Button btnClear;
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.DataGridViewTextBoxColumn colTime;
private System.Windows.Forms.DataGridViewTextBoxColumn colDirection;
private System.Windows.Forms.DataGridViewTextBoxColumn colIpAddress;
private System.Windows.Forms.DataGridViewTextBoxColumn colPort;
private System.Windows.Forms.DataGridViewTextBoxColumn colLength;
private System.Windows.Forms.DataGridViewTextBoxColumn colRawData;
private System.Windows.Forms.DataGridViewTextBoxColumn colStationId;
private System.Windows.Forms.DataGridViewTextBoxColumn type;
private System.Windows.Forms.Button button1;
}
}
@@ -0,0 +1,561 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace StandardScene.Charge
{
/// <summary>
/// 通讯监控窗体
/// </summary>
public partial class CommunicationMonitorForm : Form
{
private readonly CommunicationMessageService messageService;
private bool isFormLoaded = false;
private bool isFormMessageStop = false;
private const int MaxDisplayRows = 100;
private const int UiBatchSize = 20;
private const int StatsRefreshMs = 500;
private readonly Queue<CommunicationMessage> pendingMessages = new Queue<CommunicationMessage>();
private readonly object pendingMessagesLock = new object();
private readonly Timer uiFlushTimer;
private readonly Timer statsRefreshTimer;
private bool pendingStatsRefresh = false;
private int lastDisplayCountForStats = 0;
public CommunicationMonitorForm()
{
InitializeComponent();
messageService = CommunicationMessageService.Instance;
uiFlushTimer = new Timer { Interval = 500 };
uiFlushTimer.Tick += UiFlushTimer_Tick;
statsRefreshTimer = new Timer { Interval = StatsRefreshMs };
statsRefreshTimer.Tick += StatsRefreshTimer_Tick;
// 订阅窗体关闭事件
this.FormClosing += CommunicationMonitorForm_FormClosing;
}
private void CommunicationMonitorForm_Load(object sender, EventArgs e)
{
try
{
InitializeForm();
LoadMessages();
// 标记窗体已加载完成
isFormLoaded = true;
uiFlushTimer.Start();
statsRefreshTimer.Start();
// 在窗体加载完成后再订阅报文添加事件(避免在初始化期间触发)
messageService.MessageAdded += OnMessageAdded;
}
catch (Exception ex)
{
MessageBox.Show($"窗体加载失败: {ex.Message}\r\n{ex.StackTrace}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void InitializeForm()
{
this.Text = "通讯监控";
this.Size = new Size(1400, 800);
this.StartPosition = FormStartPosition.CenterScreen;
this.MinimumSize = new Size(1200, 600);
// 初始化IP筛选下拉框
RefreshIpFilter();
}
/// <summary>
/// 刷新IP筛选下拉框
/// </summary>
private void RefreshIpFilter()
{
try
{
if (cmbIpFilter == null || messageService == null)
return;
var selectedIp = cmbIpFilter.SelectedItem?.ToString();
cmbIpFilter.Items.Clear();
cmbIpFilter.Items.Add("全部");
var ipAddresses = messageService.GetUniqueIpAddresses();
if (ipAddresses != null)
{
foreach (var ip in ipAddresses)
{
if (!string.IsNullOrEmpty(ip))
{
cmbIpFilter.Items.Add(ip);
}
}
}
// 恢复选中项
if (!string.IsNullOrEmpty(selectedIp) && cmbIpFilter.Items.Contains(selectedIp))
{
cmbIpFilter.SelectedItem = selectedIp;
}
else if (cmbIpFilter.Items.Count > 0)
{
cmbIpFilter.SelectedIndex = 0;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"刷新IP筛选失败: {ex.Message}");
}
}
/// <summary>
/// 加载报文列表
/// </summary>
private void LoadMessages()
{
var layoutSuspended = false;
try
{
if (dgvMessages == null|| isFormMessageStop)
return;
var selectedIp = cmbIpFilter?.SelectedItem?.ToString();
var messages = string.IsNullOrEmpty(selectedIp) || selectedIp == "全部"
? messageService.GetAllMessages()
: messageService.GetMessagesByIp(selectedIp);
dgvMessages.SuspendLayout();
layoutSuspended = true;
dgvMessages.Rows.Clear();
foreach (var msg in messages)
{
AddMessageRow(msg, false);
}
UpdateStatistics(messages.Count);
pendingStatsRefresh = false;
}
catch (Exception ex)
{
MessageBox.Show($"加载报文失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
if (layoutSuspended && dgvMessages != null)
{
dgvMessages.ResumeLayout();
}
}
}
/// <summary>
/// 定时批量刷新UI,避免每条报文都抢占UI线程
/// </summary>
private void UiFlushTimer_Tick(object sender, EventArgs e)
{
if (!isFormLoaded || isFormMessageStop)
return;
List<CommunicationMessage> batch = null;
lock (pendingMessagesLock)
{
if (pendingMessages.Count == 0)
return;
int count = Math.Min(UiBatchSize, pendingMessages.Count);
batch = new List<CommunicationMessage>(count);
for (int i = 0; i < count; i++)
{
batch.Add(pendingMessages.Dequeue());
}
}
if (batch == null || batch.Count == 0)
return;
dgvMessages.SuspendLayout();
try
{
var selectedIp = cmbIpFilter?.SelectedItem?.ToString();
bool displayChanged = false;
foreach (var message in batch)
{
EnsureIpInFilter(message.IpAddress);
if (string.IsNullOrEmpty(selectedIp) || selectedIp == "全部" || selectedIp == message.IpAddress)
{
AddMessageRow(message, true);
displayChanged = true;
}
}
if (displayChanged)
{
RequestStatisticsRefresh(dgvMessages.Rows.Count);
}
}
finally
{
dgvMessages.ResumeLayout();
}
}
/// <summary>
/// 统计信息低频刷新(500ms
/// </summary>
private void StatsRefreshTimer_Tick(object sender, EventArgs e)
{
if (!isFormLoaded || isFormMessageStop || !pendingStatsRefresh)
return;
pendingStatsRefresh = false;
UpdateStatistics(lastDisplayCountForStats);
}
private void RequestStatisticsRefresh(int displayCount)
{
lastDisplayCountForStats = displayCount;
pendingStatsRefresh = true;
}
private void EnsureIpInFilter(string ipAddress)
{
if (cmbIpFilter == null || string.IsNullOrWhiteSpace(ipAddress))
return;
if (!cmbIpFilter.Items.Contains(ipAddress))
{
cmbIpFilter.Items.Add(ipAddress);
}
}
/// <summary>
/// 向表格新增一条报文行(支持头部插入)
/// </summary>
private void AddMessageRow(CommunicationMessage msg, bool insertAtTop = true)
{
if (msg == null || dgvMessages == null)
return;
DataGridViewRow row;
if (insertAtTop)
{
dgvMessages.Rows.Insert(0,
msg.Timestamp.ToString("HH:mm:ss.fff"),
msg.Direction == MessageDirection.Send ? "发送" : "接收",
msg.IpAddress,
msg.Port,
msg.Length,
msg.RawData,
msg.StationId ?? "-",
msg.Type);
row = dgvMessages.Rows[0];
}
else
{
var index = dgvMessages.Rows.Add(
msg.Timestamp.ToString("HH:mm:ss.fff"),
msg.Direction == MessageDirection.Send ? "发送" : "接收",
msg.IpAddress,
msg.Port,
msg.Length,
msg.RawData,
msg.StationId ?? "-",
msg.Type);
row = dgvMessages.Rows[index];
}
if (msg.Direction == MessageDirection.Send)
{
row.DefaultCellStyle.BackColor = Color.FromArgb(232, 245, 233);
row.DefaultCellStyle.ForeColor = Color.FromArgb(46, 125, 50);
}
else
{
row.DefaultCellStyle.BackColor = Color.FromArgb(227, 242, 253);
row.DefaultCellStyle.ForeColor = Color.FromArgb(13, 71, 161);
}
while (dgvMessages.Rows.Count > MaxDisplayRows)
{
dgvMessages.Rows.RemoveAt(dgvMessages.Rows.Count - 1);
}
}
/// <summary>
/// 更新统计信息
/// </summary>
private void UpdateStatistics(int displayCount)
{
try
{
if (lblStatistics == null || messageService == null)
return;
var allMessages = messageService.GetAllMessages();
if (allMessages == null)
return;
var sendCount = allMessages.Count(m => m.Direction == MessageDirection.Send);
var receiveCount = allMessages.Count(m => m.Direction == MessageDirection.Receive);
lblStatistics.Text = $"显示: {displayCount} | 总数: {allMessages.Count} | 发送: {sendCount} | 接收: {receiveCount}";
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"更新统计信息失败: {ex.Message}");
if (lblStatistics != null)
{
lblStatistics.Text = "统计信息加载失败";
}
}
}
/// <summary>
/// 新报文添加事件处理(线程安全)
/// </summary>
private void OnMessageAdded(object sender, CommunicationMessage message)
{
// 如果窗体还未加载完成,忽略此事件
if (!isFormLoaded || isFormMessageStop)
return;
try
{
if (message == null)
{
return;
}
lock (pendingMessagesLock)
{
pendingMessages.Enqueue(message);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"处理新报文失败: {ex.Message}");
}
}
/// <summary>
/// 解析报文数据
/// </summary>
private void ParseMessage(CommunicationMessage message)
{
if (message == null || txtParsedData == null)
return;
try
{
var parsed = new System.Text.StringBuilder();
parsed.AppendLine("=== 报文解析 ===");
parsed.AppendLine($"时间: {message.Timestamp:yyyy-MM-dd HH:mm:ss.fff}");
parsed.AppendLine($"方向: {(message.Direction == MessageDirection.Send ? "" : "")}");
parsed.AppendLine($"地址: {message.IpAddress}:{message.Port}");
parsed.AppendLine($"站点: {message.StationId ?? ""}");
parsed.AppendLine($"长度: {message.Length} 字节");
parsed.AppendLine();
parsed.AppendLine("=== 原始数据 (HEX) ===");
parsed.AppendLine(message.RawData);
// parsed.AppendLine(FormatHexString(message.RawData));
parsed.AppendLine();
parsed.AppendLine("=== 数据解析 ===");
// TODO: 根据实际协议进行解析
parsed.AppendLine();
if (message.Direction== MessageDirection.Send)
{
var sendDate = messageService.ParseSendRawData(message.RawData, message.Type);
parsed.AppendLine("示例解析:");
parsed.AppendLine($"充电指令:{sendDate.ChargeCommand}");
parsed.AppendLine($"发送电压:{sendDate.SetVoltage}");
parsed.AppendLine($"发送电流:{sendDate.SetCurrent}");
parsed.AppendLine($"车辆ID{sendDate.CurrentVehicleId}");
parsed.AppendLine($"车辆电量:{sendDate.BatteryLevel}");
parsed.AppendLine($"车辆电压:{sendDate.CarVoltage}");
parsed.AppendLine($"车辆电流:{sendDate.CarCurrent}");
}
else
{
var recDate = messageService.ParseReceiveRawData(message.RawData, message.Type);
string mechanismStatus = (int)recDate.MechanismStatus == 1 ? "伸出" : (int)recDate.MechanismStatus == 2 ? "缩回" : (int)recDate.MechanismStatus == 3 ? "运动中" : recDate.MechanismStatus.ToString();
parsed.AppendLine("示例解析:");
parsed.AppendLine($"机构状态:{mechanismStatus}");
parsed.AppendLine($"实时电压:{recDate.RealTimeVoltage}");
parsed.AppendLine($"实时电流:{recDate.RealTimeCurrent}");
parsed.AppendLine($"充电量: {recDate.BatteryAH}");
parsed.AppendLine($"是否报警:{recDate.HasAlarm}");
parsed.AppendLine($"充电状态:{recDate.Status.ToString()}");
}
txtParsedData.Text = parsed.ToString();
}
catch (Exception ex)
{
txtParsedData.Text = $"解析失败: {ex.Message}";
}
}
/// <summary>
/// 格式化十六进制字符串
/// </summary>
private string FormatHexString(string hexData)
{
if (string.IsNullOrEmpty(hexData))
return string.Empty;
var formatted = new System.Text.StringBuilder();
for (int i = 0; i < hexData.Length; i += 2)
{
if (i > 0 && i % 32 == 0)
formatted.AppendLine();
else if (i > 0)
formatted.Append(" ");
if (i + 1 < hexData.Length)
formatted.Append(hexData.Substring(i, 2));
else
formatted.Append(hexData[i]);
}
return formatted.ToString();
}
// ==================== 事件处理 ====================
private void cmbIpFilter_SelectedIndexChanged(object sender, EventArgs e)
{
try
{
LoadMessages();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"筛选改变失败: {ex.Message}");
}
}
private void dgvMessages_SelectionChanged(object sender, EventArgs e)
{
try
{
if (dgvMessages.SelectedRows.Count > 0)
{
var row = dgvMessages.SelectedRows[0];
var rawData = row.Cells[5].Value?.ToString();
var ipAddress = row.Cells[2].Value?.ToString();
var port = int.Parse(row.Cells[3].Value?.ToString() ?? "0");
var timeStr = row.Cells[0].Value?.ToString();
var directionStr = row.Cells[1].Value?.ToString();
var stationId = row.Cells[6].Value?.ToString();
var type = row.Cells[7].Value?.ToString();
// 构造消息对象用于解析
var message = new CommunicationMessage
{
RawData = rawData,
IpAddress = ipAddress,
Port = port,
Direction = directionStr == "发送" ? MessageDirection.Send : MessageDirection.Receive,
StationId = stationId == "-" ? null : stationId,
Length = rawData.Split(' ')?.Length ?? 0,
Type=type,
};
if (DateTime.TryParse(timeStr, out DateTime timestamp))
{
message.Timestamp = timestamp;
}
ParseMessage(message);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"选择报文失败: {ex.Message}");
}
}
private void btnRefresh_Click(object sender, EventArgs e)
{
try
{
RefreshIpFilter();
LoadMessages();
RequestStatisticsRefresh(dgvMessages?.Rows.Count ?? 0);
}
catch (Exception ex)
{
MessageBox.Show($"刷新失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnClear_Click(object sender, EventArgs e)
{
try
{
var result = MessageBox.Show(
"确定要清空所有报文记录吗?",
"确认清空",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
messageService.Clear();
RefreshIpFilter();
LoadMessages();
if (txtParsedData != null)
{
txtParsedData.Clear();
}
dgvMessages.Rows.Clear();
RequestStatisticsRefresh(0);
}
}
catch (Exception ex)
{
MessageBox.Show($"清空报文失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
private void CommunicationMonitorForm_FormClosing(object sender, FormClosingEventArgs e)
{
// 取消订阅事件
messageService.MessageAdded -= OnMessageAdded;
uiFlushTimer.Stop();
uiFlushTimer.Dispose();
statsRefreshTimer.Stop();
statsRefreshTimer.Dispose();
}
private void button1_Click(object sender, EventArgs e)
{
isFormMessageStop = !isFormMessageStop;
if (sender is Button pauseButton)
{
pauseButton.Text = isFormMessageStop ? "继续" : "暂停";
}
}
}
}
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="type.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>
+413
View File
@@ -0,0 +1,413 @@
# 🚀 充电桩管理系统 - 快速启动指南
## 📦 文件清单
已创建的文件:
```
Charge/
├── ChargeStation.cs # 充电桩数据模型
├── ChargeStationDataService.cs # 数据服务(单例)
├── ChargeStationManagementForm.cs # 管理窗口主类
├── ChargeStationManagementForm.Designer.cs # 窗口UI设计
├── ChargeStationManagementExample.cs # 示例代码
├── README_ChargeStationManagement.md # 详细使用说明
└── QUICKSTART.md # 本文件
```
## ⚡ 5分钟快速上手
### 步骤1:在主窗口添加菜单(推荐方式)
如果您的主窗口有菜单栏,添加一个菜单项:
```csharp
// 在主窗口的 InitializeComponent() 或构造函数中添加
// 方法1: 如果有工具栏
var btnChargeManagement = new ToolStripButton("充电桩管理");
btnChargeManagement.Click += (s, e) => {
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
};
toolStrip.Items.Add(btnChargeManagement);
// 方法2: 如果有菜单栏
var menuItemCharge = new ToolStripMenuItem("充电桩管理(&C)");
menuItemCharge.Click += (s, e) => {
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
};
menuStrip.Items.Add(menuItemCharge);
// 方法3: 如果有按钮面板
var btnChargeManagement = new Button
{
Text = "充电桩管理",
Size = new Size(120, 40),
Location = new Point(10, 10)
};
btnChargeManagement.Click += (s, e) => {
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
};
this.Controls.Add(btnChargeManagement);
```
### 步骤2:初始化测试数据(首次运行)
在程序启动时或通过菜单调用:
```csharp
// 在主窗口的 Load 事件或启动代码中
StandardScene.Charge.ChargeStationManagementExample.InitializeTestData();
```
### 步骤3:打开管理窗口
点击您添加的菜单项或按钮,即可打开充电桩管理窗口。
---
## 🎯 集成到 AbstractChargeMission
如果您想将充电桩数据与充电任务关联,在 `AbstractChargeMission.cs` 中添加:
### 1. 引用命名空间
```csharp
using StandardScene.Charge;
```
### 2. 在选择充电站点时使用充电桩数据
```csharp
// 在 Execute() 方法的充电决策部分
var dataService = ChargeStationDataService.Instance;
// 获取空闲的充电桩
var idleStations = dataService.GetIdleStations();
// 根据充电桩的站点ID筛选
targetPlan = Commons.GetNearestPlan((Car)car, site =>
site.fields.ContainsKey("group") &&
site.fields.ContainsKey("Charge") &&
GetChargeType(car).Contains(site.fields["group"]) &&
idleStations.Any(s => s.SiteId == site.id) // 确保站点有空闲充电桩
);
```
### 3. 在开始充电时更新充电桩状态
```csharp
// 在车辆到达充电站时
public override void ArriveAction(Car car, Site site)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetAllStations()
.FirstOrDefault(s => s.SiteId == site.id && s.Status == ChargeStationStatus.Idle);
if (station != null)
{
dataService.UpdateStationStatus(station.StationId, ChargeStationStatus.Charging);
car.tags.Add("chargingStationId", station.StationId);
Diagnosis.Log($"车辆 {car.name} 开始在充电桩 {station.Name} 充电",
"Charge", true);
}
}
```
### 4. 在离开充电站时更新充电桩状态
```csharp
// 在车辆离开充电站时
public override void LeaveAction(Car car, Site site)
{
if (car.tags.TryGetValue("chargingStationId", out var stationId))
{
var dataService = ChargeStationDataService.Instance;
dataService.UpdateStationStatus(stationId, ChargeStationStatus.Idle);
car.tags.Remove("chargingStationId");
Diagnosis.Log($"车辆 {car.name} 充电完成,充电桩 {stationId} 恢复空闲",
"Charge", true);
}
}
```
---
## 📊 在主界面显示充电桩统计
在主窗口添加实时统计显示:
```csharp
// 添加一个 Timer 定时更新统计信息
private Timer chargeStationStatusTimer;
private Label lblChargeStationStatus;
private void InitializeChargeStationMonitor()
{
// 创建状态标签
lblChargeStationStatus = new Label
{
Text = "充电桩: 加载中...",
AutoSize = true,
Location = new Point(10, 10),
Font = new Font("微软雅黑", 10F, FontStyle.Bold)
};
this.Controls.Add(lblChargeStationStatus);
// 创建定时器(每3秒更新一次)
chargeStationStatusTimer = new Timer
{
Interval = 3000,
Enabled = true
};
chargeStationStatusTimer.Tick += UpdateChargeStationStatus;
chargeStationStatusTimer.Start();
}
private void UpdateChargeStationStatus(object sender, EventArgs e)
{
try
{
var dataService = StandardScene.Charge.ChargeStationDataService.Instance;
var stations = dataService.GetAllStations();
var idle = stations.Count(s => s.Status == StandardScene.Charge.ChargeStationStatus.Idle);
var charging = stations.Count(s => s.Status == StandardScene.Charge.ChargeStationStatus.Charging);
var fault = stations.Count(s => s.Status == StandardScene.Charge.ChargeStationStatus.Fault);
lblChargeStationStatus.Text = $"充电桩: 总数 {stations.Count} | " +
$"空闲 {idle} | 充电中 {charging} | 故障 {fault}";
// 根据状态设置颜色
if (fault > 0)
lblChargeStationStatus.ForeColor = Color.Red;
else if (idle == 0 && charging > 0)
lblChargeStationStatus.ForeColor = Color.Orange;
else
lblChargeStationStatus.ForeColor = Color.Green;
}
catch (Exception ex)
{
lblChargeStationStatus.Text = $"充电桩: 获取状态失败 - {ex.Message}";
lblChargeStationStatus.ForeColor = Color.Gray;
}
}
```
---
## 🔧 配置充电桩与站点的映射
### 方式1: 在站点属性中添加充电桩编号
修改地图站点的 `fields`
```csharp
// 为站点添加充电桩编号
site.fields.Add("ChargeStationId", "CS20240115123456");
```
### 方式2: 在充电桩管理界面直接设置站点ID
在充电桩管理窗口中,编辑充电桩时填写"站点ID"字段。
### 方式3: 自动关联(代码实现)
```csharp
// 自动将充电桩与最近的充电站点关联
public void AutoAssignStationsToSites()
{
var dataService = ChargeStationDataService.Instance;
var allStations = dataService.GetAllStations();
var chargeSites = SimpleLib.GetAllSites()
.Where(s => s.fields.ContainsKey("Charge"))
.ToList();
foreach (var station in allStations)
{
if (station.SiteId == null || station.SiteId == 0)
{
// 根据名称或其他规则自动匹配站点
var matchedSite = chargeSites.FirstOrDefault(s =>
s.fields.ContainsKey("name") &&
s.fields["name"].Contains(station.Name)
);
if (matchedSite != null)
{
station.SiteId = matchedSite.id;
dataService.UpdateStation(station, out _);
Diagnosis.Log($"自动关联充电桩 {station.Name} 到站点 {matchedSite.id}",
"ChargeStation", true);
}
}
}
}
```
---
## 📱 添加快捷键
为管理窗口添加快捷键(在主窗口):
```csharp
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// Ctrl+C 打开充电桩管理
if (keyData == (Keys.Control | Keys.C))
{
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
```
---
## 🎨 自定义界面样式
如果需要调整窗口样式,修改 `ChargeStationManagementForm.Designer.cs`
```csharp
// 修改窗口大小
this.Size = new Size(1400, 800);
// 修改按钮颜色
btnAdd.BackColor = Color.FromArgb(144, 238, 144); // 浅绿色
btnSave.BackColor = Color.FromArgb(135, 206, 250); // 浅蓝色
btnDelete.BackColor = Color.FromArgb(255, 182, 193); // 浅红色
// 修改字体
this.Font = new Font("微软雅黑", 9F);
```
---
## 🐛 常见问题
### Q1: 窗口打不开
**A**: 检查是否正确引用了命名空间:
```csharp
using StandardScene.Charge;
```
### Q2: 数据保存失败
**A**: 确保 `Data` 文件夹有写入权限:
```bash
# Windows
右键 Data 文件夹 -> 属性 -> 安全 -> 确保当前用户有"写入"权限
```
### Q3: 找不到充电桩数据
**A**: 首次运行时需要初始化数据:
```csharp
ChargeStationManagementExample.InitializeTestData();
```
### Q4: 充电桩状态不更新
**A**: 手动刷新数据:
```csharp
ChargeStationDataService.Instance.Reload();
```
---
## 📚 进阶功能
### 实时监控充电桩通信状态
```csharp
// 定期 Ping 充电桩 IP
private async Task<bool> PingChargeStation(string ip, int port)
{
try
{
using (var client = new System.Net.Sockets.TcpClient())
{
var result = client.BeginConnect(ip, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3));
if (success)
{
client.EndConnect(result);
return true;
}
return false;
}
}
catch
{
return false;
}
}
```
### 充电桩数据可视化
```csharp
// 在主界面添加充电桩状态图表
private void DrawChargeStationChart(Graphics g)
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations();
int x = 10, y = 10, size = 40;
foreach (var station in stations)
{
Color color = station.Status switch
{
ChargeStationStatus.Idle => Color.Green,
ChargeStationStatus.Charging => Color.Yellow,
ChargeStationStatus.Fault => Color.Red,
ChargeStationStatus.Offline => Color.Gray,
_ => Color.White
};
g.FillRectangle(new SolidBrush(color), x, y, size, size);
g.DrawString(station.Name, this.Font, Brushes.Black, x, y + size + 5);
x += size + 10;
if (x > this.Width - 100)
{
x = 10;
y += size + 30;
}
}
}
```
---
## 🎉 完成
现在您已经完成了充电桩管理系统的集成!
**下一步**
1. ✅ 添加菜单项或按钮
2. ✅ 初始化测试数据
3. ✅ 打开管理窗口测试
4. ✅ 将充电桩数据集成到充电任务
5. ✅ 添加实时监控和统计
**需要帮助?**
- 查看 `README_ChargeStationManagement.md` 获取详细文档
- 参考 `ChargeStationManagementExample.cs` 查看示例代码
- 检查日志中的 `ChargeStation` 标签
---
**版本**: 1.0.0
**最后更新**: 2024-01-15
@@ -0,0 +1,310 @@
# 充电桩管理系统使用说明
## 📋 概述
充电桩管理系统是一个基于 WinForms 的可视化管理工具,用于管理 AGV 系统中的充电桩设备。
## 🚀 快速开始
### 打开管理窗口
```csharp
// 在代码中打开充电桩管理窗口
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.ShowDialog();
// 或者在按钮点击事件中
private void btnOpenChargeManagement_Click(object sender, EventArgs e)
{
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
}
```
### 添加菜单项(推荐)
在主窗口的菜单栏中添加:
```csharp
// 在主窗口的初始化代码中
var menuItem = new ToolStripMenuItem("充电桩管理");
menuItem.Click += (s, e) => {
var form = new StandardScene.Charge.ChargeStationManagementForm();
form.Show();
};
// 将 menuItem 添加到主菜单
```
## 📖 功能说明
### 1. 充电桩列表(左侧面板)
#### 功能特性
- **实时显示**:显示所有充电桩的详细信息
- **颜色标识**
- 🟢 **绿色**:充电中
- 🔴 **红色**:故障
-**灰色**:离线
-**白色**:空闲/其他状态
- **搜索功能**:支持按编号、名称、IP地址搜索
- **双击编辑**:双击列表项可快速编辑
#### 列表字段
| 字段 | 说明 | 示例 |
|-----|------|------|
| 编号 | 充电桩唯一标识 | CS20240115123456 |
| 名称 | 充电桩名称 | 1号充电桩 |
| IP地址 | 设备IP | 192.168.1.100 |
| 端口 | 通信端口 | 502 |
| 电压(V) | 额定电压 | 220.0 |
| 电流(A) | 额定电流 | 32.0 |
| 功率(W) | 计算功率 | 7040.0 |
| 状态 | 当前状态 | 空闲/充电中/故障 |
| 启用 | 是否启用 | 是/否 |
| 站点ID | 关联站点 | 1001 |
| 备注 | 备注信息 | 南区1号充电桩 |
### 2. 充电桩编辑(右侧面板)
#### 必填字段
-**编号**:自动生成(格式:CS+时间戳+随机数)
-**名称**:充电桩名称,便于识别
-**IP地址**:设备IP,必须为有效IP格式
-**端口**:通信端口(1-65535
-**电压**:额定电压(0-1000V
-**电流**:额定电流(0-500A
#### 选填字段
- 📌 **状态**:空闲/充电中/故障/离线/维护中/预约中
- 📌 **启用**:是否启用该充电桩
- 📌 **站点ID**:关联的站点编号(与地图站点关联)
- 📌 **备注**:额外说明信息
#### 自动计算
-**功率**:自动计算(电压 × 电流)
### 3. 操作按钮
#### 右侧编辑区
- 🆕 **新增**:清空表单,准备添加新充电桩
- 💾 **保存**:保存当前充电桩信息(新增或更新)
- 🗑️ **删除**:删除当前选中的充电桩
-**取消**:清空表单
#### 左侧列表区
- 🔄 **刷新**:重新加载数据
- 📤 **导出**:导出充电桩数据为 JSON 或 CSV 文件
## 🔒 数据验证规则
### IP地址验证
```
✅ 有效:192.168.1.100, 10.0.0.1, 172.16.0.1
❌ 无效:192.168.1, 256.1.1.1, abc.def.ghi.jkl
```
### 端口验证
```
✅ 有效:502, 8080, 1234
❌ 无效:0, 70000, -1
```
### 电压验证
```
✅ 有效:220V, 380V, 110V
❌ 无效:-10V, 1500V, 0V
```
### 电流验证
```
✅ 有效:32A, 16A, 63A
❌ 无效:-5A, 600A, 0A
```
### 唯一性验证
- ❌ 编号不能重复
- ❌ IP地址+端口组合不能重复
## 💾 数据存储
### 存储位置
```
项目根目录/Data/ChargeStations.json
```
### 数据格式
```json
[
{
"StationId": "CS20240115123456",
"Name": "1号充电桩",
"IpAddress": "192.168.1.100",
"Port": 502,
"Voltage": 220.0,
"Current": 32.0,
"Status": 0,
"Enabled": true,
"SiteId": 1001,
"Remarks": "南区1号充电桩",
"CreatedTime": "2024-01-15T12:34:56",
"ModifiedTime": "2024-01-15T14:20:30"
}
]
```
## 📊 代码集成
### 获取充电桩数据
```csharp
using StandardScene.Charge;
// 获取数据服务实例
var dataService = ChargeStationDataService.Instance;
// 获取所有充电桩
var allStations = dataService.GetAllStations();
// 获取空闲充电桩
var idleStations = dataService.GetIdleStations();
// 根据编号获取充电桩
var station = dataService.GetStationById("CS20240115123456");
// 获取充电中的充电桩数量
int chargingCount = dataService.GetChargingCount();
```
### 添加/更新充电桩
```csharp
// 创建新充电桩
var newStation = new ChargeStation
{
Name = "2号充电桩",
IpAddress = "192.168.1.101",
Port = 502,
Voltage = 220.0,
Current = 32.0
};
// 添加
if (dataService.AddStation(newStation, out string errorMsg))
{
Console.WriteLine("添加成功");
}
else
{
Console.WriteLine($"添加失败: {errorMsg}");
}
// 更新状态
dataService.UpdateStationStatus("CS20240115123456", ChargeStationStatus.Charging);
```
### 删除充电桩
```csharp
// 删除充电桩
if (dataService.DeleteStation("CS20240115123456", out string errorMsg))
{
Console.WriteLine("删除成功");
}
else
{
Console.WriteLine($"删除失败: {errorMsg}");
}
```
## ⚠️ 注意事项
1. **数据持久化**:所有数据自动保存到 JSON 文件,重启后数据不会丢失
2. **线程安全**:数据服务使用单例模式和锁机制,支持多线程访问
3. **状态管理**:充电中的充电桩无法删除,需先停止充电
4. **IP冲突检测**:系统会自动检测IP和端口的冲突
5. **数据备份**:建议定期备份 `Data/ChargeStations.json` 文件
## 🔧 扩展功能建议
### 与充电任务集成
`AbstractChargeMission.cs` 中集成充电桩数据:
```csharp
// 在充电任务中获取充电桩信息
private void SelectChargeStation(Car car)
{
var dataService = ChargeStationDataService.Instance;
var idleStations = dataService.GetIdleStations();
if (idleStations.Count > 0)
{
var nearestStation = FindNearestStation(car, idleStations);
// 更新充电桩状态
dataService.UpdateStationStatus(
nearestStation.StationId,
ChargeStationStatus.Reserved
);
// 分配车辆到充电桩
AssignCarToStation(car, nearestStation);
}
}
// 充电完成后
private void OnChargeComplete(Car car, ChargeStation station)
{
var dataService = ChargeStationDataService.Instance;
dataService.UpdateStationStatus(
station.StationId,
ChargeStationStatus.Idle
);
}
```
### 监控充电桩状态
```csharp
// 定期检查充电桩在线状态
private void MonitorChargeStations()
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations();
foreach (var station in stations)
{
if (station.Enabled)
{
bool isOnline = PingStation(station.IpAddress, station.Port);
var newStatus = isOnline
? ChargeStationStatus.Idle
: ChargeStationStatus.Offline;
if (station.Status != newStatus)
{
dataService.UpdateStationStatus(station.StationId, newStatus);
Diagnosis.Log($"充电桩 {station.Name} 状态变更: {newStatus}",
"ChargeStation", true);
}
}
}
}
```
## 📞 技术支持
如有问题,请检查:
1. `Data` 文件夹是否有写入权限
2. JSON 文件格式是否正确
3. 日志中的错误信息(标签:`ChargeStation`
---
**版本**: 1.0.0
**最后更新**: 2024-01-15
**作者**: MDCS System
@@ -0,0 +1,785 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using CommonUsage;
using LessokajiWeaverUtilities.Utilities;
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore;
using SimpleCore.Library;
using SimpleCore.PropType;
using StandardScene.Chained;
using StandardScene.ChargeStationType;
using StandardScene.Model;
namespace StandardScene.Charge
{
/// <summary>
/// 标准充电进程状态
/// </summary>
public class StandardChargeMissionStatus : AbstractChargeMissionStatus
{
/// <summary>
/// 是否屏蔽充电桩交互(true=屏蔽,false=允许)
/// </summary>
public bool ShieldInterLock = false;
}
/// <summary>
/// 标准充电进程
/// 负责充电桩的初始化、通讯管理和充电业务逻辑处理
/// </summary>
[MissionType(Name = "充电进程", editor = typeof(StandardChargeMission))]
[I18N.DocumentTranslation(Name = "Charge Mission", locale = "en")]
public class StandardChargeMission : AbstractChargeLogiceMission
{
#region
/// <summary>
/// 充电站字典 Key=站点ID, Value=充电站对象
/// </summary>
[JsonIgnore]
public Dictionary<int, AbstractChargeStation> ChargeStations;
/// <summary>
/// 进程状态
/// </summary>
public override MissionStatus status { get; set; } = new StandardChargeMissionStatus();
/// <summary>
/// 进程是否已启动
/// </summary>
[JsonIgnore]
public bool myStarted = false;
/// <summary>
/// 时间同步服务是否已启动
/// </summary>
[JsonIgnore]
public bool tsStarted = false;
/// <summary>
/// 充电处理线程
/// </summary>
[JsonIgnore]
private Thread ChargeThread;
/// <summary>
/// 当前正在充电的车辆 Key=车辆ID, Value=充电次数
/// </summary>
[JsonIgnore]
public Dictionary<string, int> inChargeCar = new();
/// <summary>
/// 上一次充电的车辆记录
/// </summary>
[JsonIgnore]
public Dictionary<string, int> lastinChargeCar = new();
/// <summary>
/// 充电开始时间 Key=车辆ID, Value=开始时间
/// </summary>
[JsonIgnore]
public Dictionary<string, DateTime> BeginTime = new();
/// <summary>
/// 充电条件状态
/// </summary>
[JsonIgnore]
public Dictionary<string, bool> Condition = new();
/// <summary>
/// 上一次充电条件状态
/// </summary>
[JsonIgnore]
public Dictionary<string, bool> lastCondition = new();
/// <summary>
/// 充电超时时间(小时)
/// </summary>
[JsonIgnore]
public double outtimeOfCharge = 0.5;
/// <summary>
/// UDP通讯服务
/// </summary>
[JsonIgnore]
public ChargeUdpService UdpService;
#endregion
#region
/// <summary>
/// 获取最低电量车辆的SOC值
/// 用于充电策略判断,找到系统中电量最低的空闲车辆
/// </summary>
/// <param name="car">当前车辆</param>
/// <param name="allChargeSite">所有充电站点列表</param>
/// <returns>最低电量值</returns>
public override float LowerCarSoc(AbstractCar car, List<Site> allChargeSite)
{
try
{
// 查找符合条件的最低电量车辆:
// 1. 车辆在有效站点上(GetLastSite != -1
// 2. 车辆有坐标信息(haveCoordination
// 3. 车辆未被占用(!occupied
// 4. 车辆未在充电(!charging
// 5. 车辆不在充电站点上
// 6. 车辆在线
var lowCar = SimpleLib.GetAllCars()
.OfType<Car>()
.ToList()
.FindAll(p =>
p.GetLastSite() != -1 &&
p.haveCoordination &&
!p.tags.Contains("occupied") &&
!p.tags.Contains("charging") &&
!allChargeSite.Contains(SimpleLib.GetSite(p.GetLastSite())) &&
IsOnlineCar((Car)p))
.OrderBy(p => Commons.CarValue(p, "Soc"))
.FirstOrDefault();
// 如果没有找到符合条件的车辆,返回当前车辆的电量
if (lowCar == null)
return (float)Commons.CarValue((Car)car, "Soc");
return (float)Commons.CarValue(lowCar, "Soc");
}
catch (Exception e)
{
Diagnosis.Post($"获取最低电量车辆失败: {e.Message}", "error");
return (float)0;
}
}
/// <summary>
/// 获取车辆的充电类型
/// 根据车辆字段判断是FRLD还是MuXing类型
/// </summary>
/// <param name="car">车辆对象</param>
/// <returns>充电类型字符串</returns>
public override string GetChargeType(AbstractCar car)
{
if (car is null)
{
return "";
}
// 优先判断FRLD类型
if (car.fields.ContainsKey("FRLD"))
return "FRLD";
// 其次判断MuXing类型
if (car.fields.ContainsKey("MuXing"))
return "MuXing";
// 默认返回MuXing
return "MuXing";
}
/// <summary>
/// 判断车辆是否在线
/// 通过车辆是否在有效站点上来判断
/// </summary>
/// <param name="car">车辆对象</param>
/// <returns>true=在线, false=离线</returns>
public override bool IsOnlineCar(Car car)
{
return car.GetLastSite() != -1;
}
/// <summary>
/// 车辆到达站点时的处理
/// 记录车辆到达充电站点的日志
/// </summary>
/// <param name="car">车辆对象</param>
/// <param name="site">站点对象</param>
public override void ArriveAction(Car car, Site site)
{
// 判断是否为充电站点
if (site.fields.TryGetValue("Charge", out var strStationId))
{
// 记录到达日志(模拟车辆除外)
if (!car.name.Contains("模拟"))
{
Diagnosis.Post($"{car.name}({car.id})到达充电站点{site.id}");
}
}
else
{
Diagnosis.Post($"arrived, site{site.id} is not charge site");
}
}
/// <summary>
/// 车辆离开站点时的处理
/// 记录车辆离开充电站点的日志
/// </summary>
/// <param name="car">车辆对象</param>
/// <param name="site">站点对象</param>
public override void LeaveAction(Car car, Site site)
{
// 判断是否为充电站点
if (site.fields.TryGetValue("Charge", out var strStationId))
{
// 记录离开日志(模拟车辆除外)
if (!car.name.Contains("模拟"))
{
Diagnosis.Post($"{car.name}({car.id})离开充电站点{site.id}");
}
}
else
{
Diagnosis.Post($"left, site{site.id} is not charge site");
}
}
/// <summary>
/// 站点筛选器
/// 判断站点是否为充电站点
/// </summary>
/// <param name="siteId">站点ID</param>
/// <returns>true=充电站点, false=非充电站点</returns>
public override bool SiteFilter(int siteId)
{
// 如果未屏蔽充电桩交互,返回false
if (((StandardChargeMissionStatus)status).ShieldInterLock)
{
return false;
}
// 检查站点字段中是否包含"Charge"关键字
var site = SimpleLib.GetSite(siteId);
return site.fields.Keys.ToList().Any(p => p.Contains("Charge"));
}
#endregion
#region
/// <summary>
/// 启动充电进程
/// 初始化充电站、创建通讯连接、启动充电业务循环
/// </summary>
[MethodMember(Name = "启动进程", Description = "开始处理充电维护进程")]
public override void Execute()
{
// 防止重复启动
if (myStarted)
{
MessageBox.Show("充电进程已启动,不可重复启动");
return;
}
status.status = "已启动";
myStarted = true;
int iteration = 0;
ChargeStations = new Dictionary<int, AbstractChargeStation>();
// 创建充电处理线程
ChargeThread = new Thread(() =>
{
ChargeStations = new Dictionary<int, AbstractChargeStation>();
while (true)
{
try
{
if (status.status.Contains("已停止"))
{
break;
}
var shieldInterLock = ((StandardChargeMissionStatus)status).ShieldInterLock;
// ==================== 步骤1: 初始化充电站 ====================
// 从充电桩管理配置中获取所有充电桩配置
var allStationConfigs = ChargeStationHelper.GetAllStationConfigs();
// 遍历所有充电桩配置,创建充电站实例
foreach (var stationConfig in allStationConfigs)
{
// 1.1 检查充电桩是否启用
if (!stationConfig.Enabled)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] is disabled, skip");
continue;
}
// 1.2 验证站点ID
if (!stationConfig.SiteId.HasValue || stationConfig.SiteId.Value <= 0)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] has invalid SiteId, skip");
continue;
}
int siteId = stationConfig.SiteId.Value;
// 1.4 验证IP地址格式
if (string.IsNullOrWhiteSpace(stationConfig.IpAddress) ||
!IPAddress.TryParse(stationConfig.IpAddress, out var ipAddress))
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] IP[{stationConfig.IpAddress}] is invalid");
continue;
}
// 1.5 验证端口范围
int port = stationConfig.Port;
if (port <= 0 || port > 65535)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] Port[{port}] is invalid");
continue;
}
// 1.3 检查站点是否已存在,以及IP/端口是否变更
if (ChargeStations.ContainsKey(siteId))
{
var existingStation = ChargeStations[siteId];
// 检查IP或端口是否变更
if (existingStation.Ip != ipAddress.ToString() ||
existingStation.Port != port)
{
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] IP/Port changed from [{existingStation.Ip}:{existingStation.Port}] to [{ipAddress}:{port}], recreating connection...");
// 先关闭旧连接
try
{
existingStation.CloseCommunication();
}
catch (Exception ex)
{
Diagnosis.Log($"WARN:ChargeStation[{stationConfig.StationId}] failed to close old connection: {ex.Message}");
}
// 更新IP和端口
existingStation.Ip = ipAddress.ToString();
existingStation.Port = port;
// 重新创建通讯连接
try
{
existingStation.CreateCommunication(ipAddress, port);
Diagnosis.Log($"ChargeStation[{stationConfig.StationId}] connection recreated successfully");
}
catch (Exception ex)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] failed to recreate connection: {ex.Message}");
}
}
// 站点已存在且IP/端口未变更,跳过
continue;
}
// 1.6 根据充电桩类型创建对应的充电站对象
string chargeTypeString = GetChargeTypeString(stationConfig.Type);
// 跨程序集解析:充电桩具体类型可能位于卫星插件 dllStandardScene.Devices.Charge),
// 不能再用 Type.GetType(简单名,仅当前程序集)。改用内核同款全域类型发现。
string chargeTypeFullName = "StandardScene.ChargeStationType." + chargeTypeString;
Type type = SimpleLite.Utils.UiTypeDiscovery.AllTypes()
.FirstOrDefault(t => t.FullName == chargeTypeFullName);
if (type == null)
{
Diagnosis.Log($"ERR:ChargeStation[{stationConfig.StationId}] Type[{chargeTypeString}] not found");
continue;
}
// 1.7 创建充电站实例并配置基本信息
object chargeStation = Activator.CreateInstance(type);
((AbstractChargeStation)chargeStation).SiteId = siteId;
((AbstractChargeStation)chargeStation).Ip = ipAddress.ToString();
((AbstractChargeStation)chargeStation).Port = port;
// 1.8 设置通讯类型(从配置读取,默认TCP)
if (stationConfig.Type == ChargeStationType.FRLDShort)
{
((AbstractChargeStation)chargeStation).CommunicationType = "UDP";
}
else
{
((AbstractChargeStation)chargeStation).CommunicationType = stationConfig.CommunicationType;
}
//((AbstractChargeStation)chargeStation).CommunicationType =
// string.IsNullOrWhiteSpace(stationConfig.CommunicationType)
// ? "UDP"
// : stationConfig.CommunicationType.ToUpper();
// 1.9 创建通讯连接
((AbstractChargeStation)chargeStation).CreateCommunication(ipAddress, port);
// 1.10 添加到充电站字典
ChargeStations.Add(siteId, (AbstractChargeStation)chargeStation);
Diagnosis.Log($"ChargeStation ADD: StationId[{stationConfig.StationId}] SiteId[{siteId}] IP[{ipAddress}:{port}] Type[{chargeTypeString}] Comm[{((AbstractChargeStation)chargeStation).CommunicationType}]");
}
// ==================== 步骤2: 初始化UDP服务 ====================
// 如果有任意充电桩使用UDP通讯,则创建UDP服务
if (ChargeStations.Any(c => c.Value.CommunicationType == "UDP"))
{
UdpService ??= new ChargeUdpService();
}
status.status = $"已启动-循环{iteration++}";
// ==================== 步骤3: 充电业务处理循环 ====================
foreach (var item in ChargeStations.Keys.ToArray()) //移除配置
{
var chargeStationSetting = ChargeStationHelper.GetStationBySiteId(item);
if (chargeStationSetting == null)
{
try
{
ChargeStations[item].CloseCommunication();
}
catch (Exception ex)
{
Diagnosis.Log($"WARN:ChargeStationHelper [{item}] failed to close old connection: {ex.Message}");
}
ChargeStations.Remove(item);
}
}
//S站点存在配置里没有的,需要移除配置
var sites = SimpleLib.GetAllSites().Where(s => s.fields.ContainsKey("Charge"));
foreach (var item in sites)
{
if (!ChargeStations.Keys.Contains(item.id))
{
//移除站点的配置
item.fields.Remove("Charge");
item.fields.Remove("setVoltage");
item.fields.Remove("setElectricCurrent");
item.fields.Remove("group");
Diagnosis.Post($"Charge {item.name}-{item.id} 未在充电管理配置移除参数");
item.name = "NoName";
//并关闭对应的连接
}
}
// 遍历所有已添加的充电站,处理充电业务逻辑
foreach (var chargeStationEntry in ChargeStations)
{
var openCharge = 0;
int siteId = chargeStationEntry.Key;
var chargeStation = chargeStationEntry.Value;
var site = SimpleLib.GetSite(siteId);
if (site == null)
{
Console.WriteLine($"从地图中未获取到站点的信息 siteId {siteId}");
continue;
}
var chargeStationSetting = ChargeStationHelper.GetStationBySiteId(siteId);
if (!chargeStationSetting.Enabled)
{
continue;
}
//绑定group 添加charge
if (site != null)
{
site.name = chargeStationSetting.Name;
site.fields["Charge"] = "True";
site.fields["setVoltage"] = chargeStationSetting.SetVoltage.ToString("0.0");
site.fields["setElectricCurrent"] = chargeStationSetting.SetElectricCurrent.ToString("0.0");
if (chargeStationSetting.Enabled)
{
site.fields["group"] = chargeStationSetting.GroupCarType.ToString();
}
else
{
site.fields["group"] = "禁停";
}
}
// 3.1 设置站点访问权限(默认允许进入和离开)
if (chargeStationSetting.ChargeMethod == ChargeMethodType.Side)
{
SetAllowEnter(siteId, chargeStationSetting.ShieldSiteMechanismStatus || chargeStationSetting.MechanismStatus == MechanismStatus.Retracted);
SetAllowExit(siteId, chargeStationSetting.ShieldSiteMechanismStatus || chargeStationSetting.MechanismStatus == MechanismStatus.Retracted);
SetAcknowledgeLeave(siteId, true);
}
else
{
SetAllowEnter(siteId, true);
SetAllowExit(siteId, true);
SetAcknowledgeLeave(siteId, true);
}
// 3.2 查找当前在充电站点的车辆
// 条件:车辆在站点上 或 正在获取站点锁 或 持有站点锁
var car = SimpleLib
.GetAllCars()
.FirstOrDefault(c =>
c.GetLastSite() == siteId ||
c.status.aquiringLock == siteId ||
c.status.holdingLocks.Contains(siteId)
);
// 3.3 如果找到车辆,处理充电逻辑
if (car != null)
{
// 3.3.1 检查车辆状态是否正常
if (Commons.GetVehicleStatus((Car)car) != VehicleStatus.Normal&&car.fields.ContainsKey("SkipStatus"))
{
continue;
}
// 3.3.2 判断车辆是否正在充电
var charging = car.tags.Contains("charging");
// 3.3.3 检查充电条件
// 条件:未被占用 && 未获取其他锁 && 正在充电标记
if (!car.tags.Contains("occupied")
&& car.status.holdingLocks.Length == 1 && car.status.pendingLocks.Length == 0 &&
charging)
{
openCharge = 1; // 允许充电
//// 3.3.4 安全检查:验证叉齿是否升起
//var actualLiftPillar = 1; // 默认已升起
//if (car.status.enums.TryGetValue("actualLiftPillar", out var liftPillar))
//{
// actualLiftPillar = Convert.ToInt32(liftPillar);
//}
//// 如果叉齿未升起,禁止充电
//if (actualLiftPillar != 1)
//{
// openCharge = 0;
// Console.WriteLine($"{car.id} 小车不满足充电的安全条件 叉齿未抬升");
// // TODO: 触发报警,通知人员处理
//}
}
}
// 3.3.5 发送充电指令(如果未屏蔽充电桩交互)
if (!shieldInterLock)
{
Diagnosis.Log($"向充电站[{siteId}]发送充电指令, 车辆[{car?.id}], 指令[{openCharge}]","Charge",true);
chargeStation.SendToChargeStation(openCharge, (Car)car, site);
}
}
}
catch (Exception ex)
{
Diagnosis.Post(
$"StandardChargeMission Error: {ExceptionFormatter.FormatEx(ex)}",
"error"
);
}
// 等待500ms后进行下一次循环
Thread.Sleep(500);
}
})
{
Name = "StandardChargeMission",
IsBackground = true
};
// 启动充电处理线程
ChargeThread.Start();
// ==================== 步骤4: 启动站点禁用状态上传任务 ====================
// 定期将禁用站点信息上传到迷毂系统
Task.Factory.StartNew(() =>
{
HttpPostData httpPostData = new HttpPostData();
while (true)
{
try
{
if (status.status.Contains("已停止"))
{
break;
}
// 4.1 获取所有标记为"unavailable"的站点
var sites = SimpleLib
.GetAllSites()
.Where(site => site.tags.Contains("unavailable"))
.ToList();
// 4.2 收集所有需要禁用的站点ID
HashSet<int> disabledSites = new HashSet<int>();
foreach (var site in sites)
{
// 添加当前站点
disabledSites.Add(site.id);
// 4.3 检查并添加关联的必须释放站点(mustFree)
if (site.fields.ContainsKey("mustFree") &&
site.mustFree != null &&
site.mustFree.Length > 0)
{
for (int i = 0; i < site.mustFree.Length; i++)
{
disabledSites.Add(site.mustFree[i]);
}
}
}
// 4.4 上传禁用站点列表到迷毂系统
Diagnosis.Log($"向迷毂提供禁用站点,共 {disabledSites.Count} 个", "siteIsEnable", true);
httpPostData.UploadListNode(disabledSites);
// 等待500ms后进行下一次上传
Thread.Sleep(500);
}
catch (Exception e)
{
Diagnosis.Post($"上传禁用站点失败: {ExceptionFormatter.FormatEx(e)}", "禁用站点");
}
}
}, TaskCreationOptions.LongRunning);
// 调用基类Execute方法
base.Execute();
}
/// <summary>
/// 关闭充电进程
/// 停止所有相关线程
/// </summary>
[MethodMember(Name = "关闭进程", Description = "关闭充电进程")]
public void Stop()
{
try
{
started = false;
myStarted = false;
// 先置停止状态,循环体检测到“已停止”后会自行 break
status.status = "已停止";
// 协作式停止:等待工作线程在下一次循环检测标志后退出(不再使用 .NET8 已不支持的 Thread.Abort
myThread?.Join(2000);
ChargeThread?.Join(2000);
Diagnosis.Log("充电进程已停止");
foreach (var item in ChargeStations.Values)
{
item.CloseCommunication();
Diagnosis.Post($"充电进程已停止,关闭充电通讯连接{item.Ip}-{item.Port}");
}
}
catch (Exception ex)
{
Diagnosis.Post($"充电进程已停止 {ex.ToString()}");
}
}
/// <summary>
/// 切换充电桩交互屏蔽状态
/// true=屏蔽交互,false=允许交互
/// </summary>
[MethodMember(Name = "切换充电桩交互状态", Description = "屏蔽/允许充电桩交互")]
public void ShieldInterLock()
{
var currentStatus = ((StandardChargeMissionStatus)status).ShieldInterLock;
((StandardChargeMissionStatus)status).ShieldInterLock = !currentStatus;
string statusText = ((StandardChargeMissionStatus)status).ShieldInterLock ? "已屏蔽" : "已允许";
Console.WriteLine($"充电桩交互状态: {statusText}");
Diagnosis.Log($"充电桩交互状态切换为: {statusText}");
}
/// <summary>
/// 打开充电桩管理界面
/// 用于配置和监控充电桩
/// </summary>
[MethodMember(Name = "打开充电桩管理界面", Description = "打开充电桩管理界面")]
public void OpenManagementWindow()
{
ChargeStationHelper.OpenManagementWindow();
}
#endregion
#region
/// <summary>
/// 将充电桩类型枚举转换为类型字符串
/// 用于通过反射创建对应的充电站对象
/// </summary>
/// <param name="type">充电桩类型枚举</param>
/// <returns>充电站类名</returns>
private string GetChargeTypeString(ChargeStationType type)
{
switch (type)
{
case ChargeStationType.FRLDTall:
return "FLChargeStation";
case ChargeStationType.FRLDShort:
return "PCBChargeStation";
case ChargeStationType.MuXing:
return "MuXingChargeStation";
default:
// 默认使用FRLD矮款充电桩
return "PCBChargeStation";
}
}
/// <summary>
/// 给车辆下发充电任务
/// 为测试或调试用途,手动给车辆添加shouldCharge标签
/// </summary>
[MethodMember(Name = "小车下发充电任务", Description = "给小车下发充电任务")]
public void addtagshuldcharge()
{
// 查找第一个在有效站点上的车辆
var car = (Car)SimpleLib.GetAllCars()
.ToList()
.Find(c => c.GetLastSite() != -1);
if (car != null)
{
// 添加shouldCharge标签
Commons.AddOrUpdateTag(car.tags, "shouldCharge", "true");
Diagnosis.Log($"已为车辆 {car.id} 添加充电任务标签");
}
else
{
Diagnosis.Log("未找到在有效站点上的车辆");
}
}
#endregion
}
}
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore.PropType;
namespace StandardScene.ChargeStationType
{
public class AbstractChargeStation
{
public bool IsSafe = true;
public int SiteId { get; set; }
public string Ip { get; set; }
public int Port { get; set; }
public string CommunicationType { get; set; }
/// <summary>
/// 关闭当前通讯连接
/// </summary>
public virtual void CloseCommunication()
{
// 默认实现为空,子类可重写
}
/// <summary>
/// 创建通讯连接
/// </summary>
public virtual void CreateCommunication(IPAddress ip, int port)
{
}
public virtual void SendToChargeStation(int isCharge,Car car,Site site)
{}
/// <summary>
/// UDP 报文回调钩子:默认空实现,具体桩型按需重写。
/// 用于解耦 ChargeUdpService 对具体充电桩类型的 is 判断,便于驱动外移至卫星 dll。
/// </summary>
public virtual void OnUdpMessage(byte[] message)
{
}
}
}
@@ -0,0 +1,111 @@
using SimpleCore.Compiler;
using SimpleCore.PropType;
using StandardScene.CarTypes;
namespace StandardScene.Coders
{
/// <summary>
/// 通用(导航无关)轨道 coder 基类。
///
/// 背景:避障/IO/纠偏等 coder 原以 [TemplateTrackCoderSettings] 内联重复在各车型上。
/// 这里把它们抽离为单一可复用实现,各车型改用 [ProgramTrackCoderSettings] 按各自 priority 引用。
///
/// 等价性:内部仍走内核 Template 机制(ProgramCoderHelper.PrepareTrackEngine + Topaz 求值),
/// useVerb / templateString 与原模板逐字一致;这些 coder 仅引用 Basic 字段,故统一用 BasicXxxFields
/// 与原先传车型 Fields 在“模板所引用字段”上行为一致。priority 由引用方车型特性指定,执行顺序不变。
/// </summary>
public abstract class CommonTemplateTrackCoder : ITrackCoder
{
protected abstract string UseVerb { get; }
protected abstract string TemplateString { get; }
// 默认仅用 Basic 字段袋;个别 coder(如避障含 ChangeAvoidanceParam 标志位)可覆写站点字段袋。
protected virtual System.Type SiteFieldsType => typeof(BasicSiteFields);
public virtual bool toBlock() => false;
public bool Code(SegmentPlan plan, Track track, Site src, Site dst, int i)
{
var engine = ProgramCoderHelper.PrepareTrackEngine(plan, track, src, dst, i,
carFields: typeof(BasicCarFields),
siteFields: SiteFieldsType,
trackFields: typeof(BasicTrackFields),
planFields: typeof(BasicPlanFields));
if (!(engine.ExecuteExpression(UseVerb) is bool ok) || !ok)
return false;
plan.codeArr[i] += (string)engine.ExecuteExpression("`" + TemplateString + "`");
return false;
}
}
// 原 useVerb: track.LidarArea != -2
public class LidarAreaSwitchCoder : CommonTemplateTrackCoder
{
protected override string UseVerb => "track.LidarArea != -2";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });";
}
// 原无 useVerb(等价 true,每段执行)
public class AvoidanceDistanceCoder : CommonTemplateTrackCoder
{
protected override string UseVerb => "true";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceDistance(${track.StopDistance},${track.SlowDistance}); });";
}
// 原 useVerb: track.IOArea != -1
public class IoAreaSwitchCoder : CommonTemplateTrackCoder
{
protected override string UseVerb => "track.IOArea != -1";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });";
}
// 原 useVerb: track.BiasAlarmThresh >0 || track.DthAlarmThresh > 0
public class TrackingErrThreshCoder : CommonTemplateTrackCoder
{
protected override string UseVerb => "track.BiasAlarmThresh >0 || track.DthAlarmThresh > 0";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(${track.BiasAlarmThresh},${track.DthAlarmThresh}); });";
}
// 避障区尺寸切换(4 参,含中心点)站点字段袋:在 Basic 基础上补 ChangeAvoidanceParam 标志位。
// 等价于 MWL/MultiVehicle 原用的 MultiWheelLifterSiteFields 中本 coder 实际引用到的字段。
internal class AvoidanceParamSiteFields : BasicSiteFields
{
public bool ChangeAvoidanceParam = false;
}
/// <summary>
/// 避障区尺寸切换 coder(4 参,含中心点)。
/// 合并自 MultiWheelLifterCar 与 MultiVehicleCar 两份**逐字相同**的内联模板(零行为变更):
/// useVerb=dst.ChangeAvoidanceParam==true;模板=ChangeAvoidanceParam(L,W,CenterX,CenterY)。
/// 注:Kiva / Forklift 的 2 参变体见同文件 AvoidanceParamLWCoder(方案 B4 参 flag 版 + 2 参 L,W 版)。
/// </summary>
public class AvoidanceParamCoder : CommonTemplateTrackCoder
{
protected override System.Type SiteFieldsType => typeof(AvoidanceParamSiteFields);
protected override string UseVerb => "dst.ChangeAvoidanceParam==true";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth},${dst.CarCenterX},${dst.CarCenterY}); });";
}
/// <summary>
/// 避障区尺寸切换 coder(2 参,无中心点)。
/// 合并自 Kiva 与 Forklift 两份**逐字相同**的 2 参内联模板。
/// 触发条件统一取 Forklift 的「已配置车体长宽」(dst.CarLength!=-1 &amp;&amp; dst.CarWidth!=-1)
/// - ForkliftuseVerb/模板逐字一致,行为不变;
/// - Kiva:原为无条件触发,统一后当站点未配置长宽(=-1)时不再下发 ChangeAvoidanceParam(-1,-1)
/// 属预期内的安全收敛(方案 B,见 StandardScene拆分计划.md §11.4-2)。
/// 仅引用 Basic 字段(CarLength/CarWidth 已在 BasicSiteFields),故用默认 BasicSiteFields。
/// </summary>
public class AvoidanceParamLWCoder : CommonTemplateTrackCoder
{
protected override string UseVerb => "dst.CarLength != -1 && dst.CarWidth != -1";
protected override string TemplateString =>
"agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth}); });";
}
}
@@ -0,0 +1,69 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using SimpleCore.Library;
namespace StandardScene.CommonTools
{
/// <summary>
/// 按文件路径串行化读写,保证高并发下对同一文件的更新原子、不覆盖其他记录。
/// 调用方在委托中完成“读当前内容 → 修改 → 返回新内容”,由本类负责加锁与写回。
/// </summary>
public static class AtomicFileUpdateHelper
{
private static readonly ConcurrentDictionary<string, object> PathLocks = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 对指定路径执行原子更新:在持锁下读取当前文件内容,调用 update 得到新内容并写回。
/// 不修改其他记录时,应在 update 中仅变更需要变更的条目后返回完整内容。
/// </summary>
/// <param name="filePath">文件完整路径</param>
/// <param name="update">接收当前文件文本(若文件不存在则为 null),返回要写回的新文本;返回 null 表示不写入</param>
public static void ExecuteAtomicUpdate(string filePath, Func<string, string> update)
{
if (string.IsNullOrEmpty(filePath))
throw new ArgumentNullException(nameof(filePath));
if (update == null)
throw new ArgumentNullException(nameof(update));
var lockObj = PathLocks.GetOrAdd(filePath, _ => new object());
lock (lockObj)
{
string current = null;
try
{
if (File.Exists(filePath))
current = File.ReadAllText(filePath);
}
catch (Exception ex)
{
Diagnosis.Log($"AtomicFileUpdate read error: {filePath}, {ex.Message}", "AtomicFileUpdate", true);
throw;
}
string newContent = update(current);
if (newContent == null)
return;
var dir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
try
{
var tempPath = filePath + ".tmp";
File.WriteAllText(tempPath, newContent);
if (File.Exists(filePath))
File.Replace(tempPath, filePath, null);
else
File.Move(tempPath, filePath);
}
catch (Exception ex)
{
Diagnosis.Log($"AtomicFileUpdate write error: {filePath}, {ex.Message}", "AtomicFileUpdate", true);
throw;
}
}
}
}
}
@@ -0,0 +1,128 @@
using System;
using System.Threading;
namespace StandardScene.CommonTools
{
public sealed class SnowflakeIdGenerator
{
// 默认起始时间戳:2026-01-01T00:00:00.000ZUnix 毫秒)
// 如果你希望生成的数字更短,可以在构造函数里传入更“近”的 _epochMs(建议全系统统一)。
private const long DefaultEpochMs = 1767225600000L;
private const int WorkerIdBits = 5; // 机器ID所占的位数
private const int DatacenterIdBits = 5; // 数据中心ID所占的位数
private const int MaxWorkerId = -1 ^ (-1 << WorkerIdBits); // 最大机器ID
private const int MaxDatacenterId = -1 ^ (-1 << DatacenterIdBits); // 最大数据中心ID
private const int SequenceBits = 12; // 序列号所占的位数
private const int WorkerIdShift = SequenceBits; // 机器ID左移的位数
private const int DatacenterIdShift = SequenceBits + WorkerIdBits; // 数据中心ID左移的位数
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; // 时间戳左移的位数
private const long SequenceMask = -1L ^ (-1L << SequenceBits); // 序列号的最大值
private const string Base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private readonly object _syncRoot = new object();
private readonly long _epochMs;
private readonly long _workerId; // 机器ID
private readonly long _datacenterId; // 数据中心ID
private long _sequence; // 序列号
private long _lastTimestamp = -1L; // 上次生成ID的时间戳
public SnowflakeIdGenerator(long workerId, long datacenterId, long? epochMs = null)
{
this._epochMs = epochMs ?? DefaultEpochMs;
if (this._epochMs > TimeGen())
{
throw new ArgumentException("_epochMs cannot be in the future.");
}
if (workerId is > MaxWorkerId or < 0)
{
throw new ArgumentException($"worker Id can't be greater than {MaxWorkerId} or less than 0");
}
if (datacenterId is > MaxDatacenterId or < 0)
{
throw new ArgumentException($"{datacenterId} can't be greater than {MaxDatacenterId} or less than 0");
}
this._workerId = workerId;
this._datacenterId = datacenterId;
}
public long NextId()
{
lock (_syncRoot)
{
long timestamp = TimeGen();
if (timestamp < _lastTimestamp)
{
// 容忍系统时钟回拨:等待到追上 _lastTimestamp,避免直接抛异常把业务打崩。
timestamp = TilNextMillis(_lastTimestamp);
}
if (_lastTimestamp == timestamp)
{
_sequence = (_sequence + 1) & SequenceMask;
if (_sequence == 0)
{
timestamp = TilNextMillis(_lastTimestamp);
}
}
else
{
_sequence = 0;
}
_lastTimestamp = timestamp;
long id = ((timestamp - _epochMs) << TimestampLeftShift)
| (_datacenterId << DatacenterIdShift)
| (_workerId << WorkerIdShift)
| _sequence;
return id;
}
}
/// <summary>
/// 生成更短的字符串形式 ID(Base62 编码),便于显示/存储。
/// </summary>
public string NextIdBase62()
{
ulong value = unchecked((ulong)NextId());
return ToBase62(value);
}
private static long TilNextMillis(long lastTimestamp)
{
var spin = new SpinWait();
long timestamp;
do
{
spin.SpinOnce();
timestamp = TimeGen();
}
while (timestamp <= lastTimestamp);
return timestamp;
}
private static long TimeGen()
{
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
private static string ToBase62(ulong value)
{
if (value == 0)
{
return "0";
}
// 2^64-1 的 base62 最大长度为 11(因为 62^11 > 2^64)。
char[] buffer = new char[11];
int pos = buffer.Length;
while (value > 0)
{
ulong rem = value % 62;
value /= 62;
buffer[--pos] = Base62Alphabet[(int)rem];
}
return new string(buffer, pos, buffer.Length - pos);
}
}
}
+717
View File
@@ -0,0 +1,717 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleCore;
using SimpleCore.Compiler;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using StandardScene.InterLock;
using StandardScene.Model;
using static StandardScene.Chained.ChainedDeliveryMission;
namespace StandardScene
{
/// <summary>
/// 插件本地化设置
/// </summary>
public class CustomOperationsBeforeLoading
{
public static void Set()
{
///死锁提醒
TrafficControl.OnDeadLock = (loopingCar) =>
{
try
{
var cars = string.Join(",", loopingCar.Select(p => $"{p.id}"));
var msg = $"小车:{cars}间发生死锁" +
$"请及时人工介入处理!!!!";
MessageBox.Show(msg);
}
catch { }
};
}
}
public class NoReflectionApi : Attribute
{
}
public class ReflectionApiWithParameter : Attribute
{
public string name;
public string desc;
public string Hint;
}
public static class Commons
{
public static VehicleStatus GetVehicleStatus(Car car)
{
if(car is DummyCar && car.lstatus.Contains("上线"))
return VehicleStatus.Normal;
if (car.lstatus.Contains("正常但未初始化")||car.lstatus.Contains("Normal but not initialized"))
return VehicleStatus.NeedInit;
if (car.lstatus.Contains("正常") || car.lstatus.Contains("Normal")||car.lstatus.Contains("上线"))
return VehicleStatus.Normal;
if (car.lstatus.Contains("自动驾驶系统失联") || car.lstatus.Contains("Autonomous driving system lost"))
return VehicleStatus.Offline;
return VehicleStatus.Unknown;
}
/// <summary>
/// 获取车辆状态
/// </summary>
/// <param name="car">车辆</param>
/// <param name="key">状态-键</param>
/// <returns>状态-值</returns>
public static string GetCarStatus(Car car, string key)
{
string value = "0";
if (car == null) return value;
if (car.status.enums.TryGetValue(key, out var valueStr))
value = valueStr;
return value;
}
/// <summary>
/// 添加标签
/// </summary>
/// <param name="item"></param>
/// <param name="tag"></param>
/// <param name="value"></param>
public static void AddOrUpdateTag<T>(T item, string tag, string value) where T : TagSet
{
if (item.Contains(tag))
item.Remove(tag);
item.Add(tag, value);
}
/// <summary>
/// 删除标签
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="item"></param>
/// <param name="tag"></param>
public static void DeleteTag<T>(T item, string tag) where T : TagSet
{
if (item.Contains(tag))
{
item.Remove(tag);
}
}
/// <summary>
/// 清空标签
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="item"></param>
public static void ClearTags<T>(T item) where T : TagSet
{
item.Clear();
}
/// <summary>
/// 添加或更新字段
/// </summary>
/// <param name="car"></param>
/// <param name="field"></param>
/// <param name="value"></param>
public static void AddOrUpdateCarField(Car car, string field, string value)
{
if (car.fields.ContainsKey(field))
{
car.fields.Remove(field);
car.fields.Add(field, value);
}
else
car.fields.Add(field, value);
}
/// <summary>
/// 删除字段
/// </summary>
/// <param name="car"></param>
/// <param name="field"></param>
public static void DeleteCarField(Car car, string field)
{
if (car.fields.ContainsKey(field))
{
car.fields.Remove(field);
}
}
/// <summary>
/// 清空字段
/// </summary>
/// <param name="car"></param>
public static void ClearCarFields(Car car)
{
car.fields.Clear();
}
/// <summary>
/// 添加或更新site的字段
/// </summary>
/// <param name="site"></param>
/// <param name="field"></param>
/// <param name="value"></param>
public static void AddOrUpdateSiteField(Site site, string field, string value)
{
if (site.fields.ContainsKey(field))
{
site.fields.Remove(field);
site.fields.Add(field, value);
}
else
site.fields.Add(field, value);
}
/// <summary>
/// 删除site的字段
/// </summary>
/// <param name="site"></param>
/// <param name="field"></param>
public static void DeleteSiteField(Site site, string field)
{
if (site.fields.ContainsKey(field))
{
site.fields.Remove(field);
}
}
/// <summary>
/// 清空site的字段
/// </summary>
/// <param name="site"></param>
public static void ClearSiteFields(Site site)
{
site.fields.Clear();
}
/// <summary>
/// 检查给定字符串是否为合法的 HTTP/HTTPS URL(至少包含协议与主机)。
/// </summary>
/// <param name="url">待校验的 URL 字符串</param>
/// <returns>true 表示 URL 合法</returns>
public static bool IsValidHttpUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) return false;
if (string.IsNullOrEmpty(uri.Host)) return false;
return true;
}
public static double CarValue(Car car, string key)
{
if (car.fields.TryGetValue("electricCurrent", out var strelectricCurrent))
return double.Parse(strelectricCurrent);
if (key == "Soc" && (car.name.Contains("模拟") || car.name.ToLower().Contains("sim")))
{
if (car.fields.TryGetValue("Soc", out var strSoc))
return double.Parse(strSoc);
return 100;
}
var value = "0";
if (car.status.enums.TryGetValue(key, out var valueStr))
value = valueStr;
return double.Parse(value);
}
/// <summary>
/// 添加或更新mission的字段
/// </summary>
/// <param name="mission"></param>
/// <param name="field"></param>
/// <param name="value"></param>
public static void AddOrUpdateMissionField(Mission mission, string field, string value)
{
if (mission.fields.ContainsKey(field))
{
mission.fields.Remove(field);
mission.fields.Add(field, value);
}
else
mission.fields.Add(field, value);
}
/// <summary>
/// 删除mission的字段
/// </summary>
/// <param name="mission"></param>
/// <param name="field"></param>
/// <returns></returns>
public static void DeleteMissionField(Mission mission, string field)
{
if (mission.fields.ContainsKey(field))
mission.fields.Remove(field);
}
public static List<AbstractCar> GetOnlineCars()
{
return SimpleLib.GetAllCars().Where(c => c.GetLastSite() != -1 && Commons.GetVehicleStatus((Car)c)== VehicleStatus.Normal).ToList();
}
public static bool IsSceneSiteByCode(int[] siteCodes)
{
try
{
foreach (var siteCode in siteCodes)
{
var site = SimpleLib.GetAllSites().FirstOrDefault(s => s.id == siteCode);
if (site == null) return false;
}
}
catch (Exception ex)
{
Console.WriteLine($@"IsSceneSite 校验报错=>{ex.Message}");
return false;
}
return true;
}
public static readonly object PlanSession = new object();
public static int SelectCar(AbstractCar car, bool enableGoingStandbyCar = false, bool needCharge = false, bool checkHoldCar = false)
{
var carHaveCoordination = (Car)car;
var conditions = car.GetLastSite() != -1 && GetVehicleStatus((Car)car)== VehicleStatus.Normal&&
!car.tags.Contains("changePriority") &&
!car.tags.Contains("priority") && !car.tags.Contains("occupied")
&& !car.tags.Contains("agvOffline")
&& car.status.pendingLocks.Length == 0 && !car.tags.Contains("deliver") &&//pendingLocks临时增加解决车接多任务问题
(!car.tags.Contains("shouldCharge") || needCharge)
&& !car.tags.Contains("currentWorkStep") && !car.fields.ContainsKey("StopAccept") && car.tags.Contains("Online"); //巡航任务未完成的车
if (car.name.Contains("模拟"))
{
conditions = car.GetLastSite() != -1 &&
(!car.tags.Contains("holdCar") || !checkHoldCar) && !car.tags.Contains("occupied")
&& car.status.pendingLocks.Length == 0 && !car.tags.Contains("deliver") &&//pendingLocks临时增加解决车接多任务问题
(!car.tags.Contains("shouldCharge") || needCharge)
&& !car.tags.Contains("currentWorkStep") && !car.fields.ContainsKey("StopAccept"); //巡航任务未完成的车
}
//((carHaveCoordination.haveCoordination || car.name.Contains("模拟")) && (!car.tags.Contains("shouldCharge") || needCharge));
if (conditions) return 1;
if (enableGoingStandbyCar && car.tags.TryGetValue("dest", out var dst) &&
int.TryParse(dst, out var dstId) &&
SimpleLib.GetSite(dstId).fields.ContainsKey(GetGiveWayType((Car)car))) return 1;
return -1;
}
private static string GetGiveWayType(Car car)
{
if (car.fields.ContainsKey("mover")) return "standbyMover";
if (car.fields.ContainsKey("loader")) return "standbyLoader";
return "giveWay";
}
public static bool SiteHaveTask(Site site)
{
var siteHaveTask = SimpleLib.GetAllCars().ToList().FindAll(o =>
{
if (o.status.pendingLocks.Length > 0)
{
if (o.status.pendingLocks.Contains(site.id))
{
return true;
}
}
if (o.status.holdingLocks.Length > 0)
{
if (o.status.holdingLocks.Contains(site.id))
{
return true;
}
}
return false;
}).Count != 0;
return siteHaveTask;
}
public static SegmentPlan GetNearestPlan(Car car, Func<Site, bool> test, int srcId = -1, bool disableConflict = false)
{
var curRouteLength = float.MaxValue;
SegmentPlan targetPlan = null;
foreach (var site in SimpleLib.GetAllSites())
{
if (!test(site)) continue;
bool occupied = false;
foreach (var cc in SimpleLib.GetAllCars())
{
if (cc != car)
{
if (cc.tags.Contains("dest") && cc.tags.IsEqual("dest", site.id.ToString()) ||
cc.status.holdingLocks.Contains(site.id))
occupied = true;
}
}
if (occupied) continue;
var mPlan = new SegmentPlan { usingCar = car };
if (disableConflict)
mPlan.fields["forbid_cross"] = "false";
try
{
var newRouteLength = mPlan.FindRoute(
SimpleLib.GetSite(srcId == -1 ? car.GetLastSite() : srcId),
SimpleLib.GetSite(site.id));
if (newRouteLength < curRouteLength)
{
curRouteLength = newRouteLength;
targetPlan = mPlan;
}
}
catch
{
// ignored
}
}
return targetPlan;
}
public static void NearestTask(List<Delivery> deliveries, List<Delivery> runningDeliveries = null)
{
try
{
var curW = float.MaxValue;
var priority = int.MinValue;
Delivery priorityNextDelivery = null;
Delivery deliveryTask = null;
//车不是空闲车,但这个任务结束的时候离另一个将要执行的任务很近。
//(通常来说是同一个工序的,除非单纯地返空托(或路线规划失败,跳别的任务了未按优先级执行)在仓库)
// 车空闲
foreach (var car in SimpleLib.GetAllCars().OfType<Car>()) //找到那个车 距离待执行任务最近
{
DeleteTag(car.tags, "changePriority");
//车的状态是OK的
if (car.GetLastSite() == -1 || car.tags.Contains("agvOffline") || car.tags.Contains("occupied")) continue;
//车已经被分配过任务了
if (deliveries.FirstOrDefault(t => t.UsingCar == car) != null) continue;
int dst = 0;
#region ObsoleteCode
//if (car.tags.ContainsKey("occupied")) //说明是正在执行任务的车
//{
// if (car.tags.ContainsKey("dest")
// && deliverys.Where(d => d.usingCar != null && d.usingCar == car) == null //并且这个车没有需要等待的任务执行的任务
// //还存在一种情况车被选了但中途有高优先级的任务进来
// )
// {
// //这个车正在执行的任务
// dst = int.Parse(car.tags["dest"]);
// }
// else
// {
// //这个车已经有需要等待的任务了。
// continue;
// }
//}
//else //车空闲
//{
// dst = car.GetLastSite();
//}
//无效
#endregion
dst = car.GetLastSite();
if (dst == -1)
{
//输出日志 变更dst;
dst = car.status.holdingLocks.First();
Diagnosis.Post($"调度小车{car.name}:{car.id}的GetLastSite 为 -1 。变更 holdingLocks.First() 为小车初始点{dst}");
}
//空闲的车 //如果有多个那个离的最近
foreach (var delivery in deliveries.Where(d => d.UsingCar == null))
{
//车是否能接到该任务。
if (car.fields.ContainsKey("group") && !string.IsNullOrEmpty(car.fields["group"]) && !car.fields["group"].Contains(delivery.Group))
continue;
var mPlan = new SegmentPlan { usingCar = car };
try
{
var myW = mPlan.FindRoute(
SimpleLib.GetSite(dst),
SimpleLib.GetSite(delivery.Src));
//找到最近的,
if (myW < curW)
{
curW = myW;
//找到优先级最高的。且距离最近
if (delivery.Priority >= priority)
{
priorityNextDelivery = delivery;
priority = delivery.Priority;
Diagnosis.Post($"{priorityNextDelivery.Id}/{priorityNextDelivery.Group}/{priority}", " delivery.priority");
}
//找到距离最近的。
Delivery nextDelivery = delivery;
if (nextDelivery != priorityNextDelivery) //并且这两个任务不同 优先级高的比较远
{
if (runningDeliveries != null)
{
//优先级高的先执行
Delivery deliveryRunning = null; // runningDeliveries.Where(d => d.group == priorityNextDelivery.group).FirstOrDefault();
//有车正在执行优先级高的任务。有的话就不让车过去
if (deliveryRunning != null) //true
{
Diagnosis.Post($"{deliveryRunning.Id}/{deliveryRunning.UsingCar?.id}/" +
$"{deliveryRunning.Group}/{deliveryRunning.Priority}", "deliveryRunning");
deliveryTask = nextDelivery;
}
else
{
deliveryTask = priorityNextDelivery;
Diagnosis.Post($"{priorityNextDelivery.Id}/{priorityNextDelivery.UsingCar?.id}/" +
$"{priorityNextDelivery.Group}/{priorityNextDelivery.Priority}", "priorityNextDelivery1");
}
}
else
{
deliveryTask = priorityNextDelivery;
Diagnosis.Post($"{priorityNextDelivery.Id}/{priorityNextDelivery.UsingCar?.id}/" +
$"{priorityNextDelivery.Group}/{priorityNextDelivery.Priority}", "priorityNextDelivery2");
}
}
else
{
deliveryTask = nextDelivery;
Diagnosis.Post($"{nextDelivery.Id}/{nextDelivery.UsingCar?.id}/" +
$"{nextDelivery.Group}/{nextDelivery.Priority}", "nextDelivery");
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.ToString());
}
}
if (deliveryTask != null)
{
deliveryTask.UsingCar = car;
deliveryTask.Priority = 50;
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + ex.ToString());
}
}
public static async Task GoSite(AbstractCar car, Site targetSite, int step, string action = "/", bool reverse = false)
{
try
{
var plan = new SegmentPlan
{
usingCar = car,
fields =
{
["reverse"] = reverse.ToString(),
["action"] = action,
["allow_destination_on_route"] = "true"
}
};
/* if (stations!=null && stations.Count()>=1)
{
var finishLeftPtlSites = SimpleLib.GetAllSites().Where(site => site.name.Contains("TaskStation")).ToList();
foreach (var site in finishLeftPtlSites)
{
var stationCode = site.name.Split('-')[1];
if (stations.Contains(stationCode))
{
site.fields.Add("askInfons1", "1");
}
}
}*/
plan.FindRoute(SimpleLib.GetSite(car.GetLastSite()), targetSite);
//plan.HintNotEnding();
var program = plan.Compile("move");
car.tags.Add("occupied", $"go{targetSite.id}");
Console.WriteLine($">>Script:{program.script}");
var tsk = program.Queue();
/* if (stations != null && stations.Count() >= 1)
{
var finishLeftPtlSites = SimpleLib.GetAllSites().Where(site => site.name.Contains("TaskStation")).ToList();
foreach (var site in finishLeftPtlSites)
{
var stationCode = site.name.Split('-')[1];
if (stations.Contains(stationCode))
{
site.fields.Remove("askInfons1");
}
}
}*/
await tsk;
car.tags.Remove("occupied");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message + "重新执行");
Thread.Sleep(3000);
}
}
public static SegmentPlan GenerateEscapePlan(SegmentPlan dstPlan, Func<Site, bool> escapeSiteCondition = null)
{
var curW = float.MaxValue;
var car = dstPlan.usingCar;
var escapedSites = SimpleLib.GetAllCars().Where(cc => cc.status.escape.Length > 0)
.Select(cc => cc.status.escape.Last()).ToHashSet();
SegmentPlan planEsc = null;
foreach (var site in SimpleLib.GetAllSites())
{
if (site.id == dstPlan.Destination.id || site.id == dstPlan.Source.id) continue;
if (escapedSites.Contains(site.id)) continue;
if (escapeSiteCondition != null && !escapeSiteCondition(site)) continue;
bool occupied = false;
foreach (var cc in SimpleLib.GetAllCars())
{
if (cc != car)
{
if (cc.status.holdingLocks.Contains(site.id) ||
(cc.tags.Contains("dest") && cc.tags["dest"] == site.id.ToString())) occupied = true;
}
}
if (occupied) continue;
TryGeneratePlan(dstPlan.usingCar, dstPlan.Destination, site, ref curW, ref planEsc);
}
return planEsc;
}
private static void TryGeneratePlan(AbstractCar car, Site srcSite, Site dstSite, ref float curW, ref SegmentPlan generatedPlan)
{
var mPlan = new SegmentPlan { usingCar = car, findLoop = false, fields = new Dictionary<string, string>() };
mPlan.fields["forbid_cross"] = "false";
try
{
var actuallyFindRoute = false;
var myw = TryGetWeight(srcSite, dstSite);
if (myw < 0)
{
myw = mPlan.FindRoute(srcSite, dstSite);
AddWeight(srcSite, dstSite, myw);
actuallyFindRoute = true;
}
if (myw < curW)
{
if (!actuallyFindRoute) mPlan.FindRoute(srcSite, dstSite);
curW = myw;
generatedPlan = mPlan;
}
}
catch
{
// ignored
}
}
public static Dictionary<(Site, Site), float> weightDictionary = new();
public static float TryGetWeight(Site s1, Site s2)
{
lock (weightDictionary)
{
if (weightDictionary.TryGetValue((s1, s2), out var w)) return w;
if (weightDictionary.TryGetValue((s2, s1), out var ww)) return ww;
}
return -1;
}
public static void AddWeight(Site s1, Site s2, float w)
{
lock (weightDictionary) weightDictionary[(s1, s2)] = w;
}
public static SegmentPlan SimpleToNearestPlan(AbstractCar car, Func<Site, bool> test, int srcID = -1)
{
var curw = float.MaxValue;
SegmentPlan targetPlan = null;
var srcSite = SimpleLib.GetSite(srcID == -1 ? car.GetLastSite() : srcID);
foreach (var site in SimpleLib.GetAllSites())
{
if (!test(site)) continue;
if (site.id == srcSite.id) continue;
bool occupied = false;
foreach (var cc in SimpleLib.GetAllCars())
{
// if (cc != car)//当前
{
if (cc.status.holdingLocks.Contains(site.id) ||
(cc.tags.Contains("dest") && cc.tags["dest"] == site.id.ToString())) occupied = true;
}
}
if (occupied) continue;
TryGeneratePlan(car, srcSite, site, ref curw, ref targetPlan);
}
return targetPlan;
}
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
private static bool ShowConsole = false;
public static void ShowConsoleFun()
{
ShowConsole = !ShowConsole;
var handle = GetConsoleWindow();
int n = ShowConsole ? 0 : 5;
Console.WriteLine(n);
ShowWindow(handle, n);
}
public static void HideConsoleFun()
{
var handle = GetConsoleWindow();
ShowWindow(handle, 0);
}
}
}
Binary file not shown.
+987
View File
@@ -0,0 +1,987 @@
# StandardScene - Charge 模块逻辑文档
本文档用于梳理 `StandardScene/Charge/` 充电桩管理与充电业务的整体逻辑,重点包含:
- 充电桩配置(数据模型与持久化)
- 通信报文解析与 `ChargeStation` 状态落库
- `StandardChargeMission` 的充电业务循环
- 相关 WinForms 界面如何展示与交互
---
## 1. 目录/模块职责速览(Charge/ 内)
### 数据与配置层
- `ChargeStation.cs`:充电桩数据模型(`ChargeStation`)及相关枚举(`ChargeStationStatus``CommunicationStatus``ChargeCommandStatus` 等)
- `ChargeStationDataService.cs``ChargeStation` 的持久化与查询/更新(JSON 文件存储)
- `AlarmConfig.cs`:报警配置模型(`AlarmConfig`
- `AlarmConfigDataService.cs`:报警配置的持久化与查询/更新(JSON 文件存储)
- `ChargeStrategyConfig.cs`:充电策略配置模型
- `ChargeStrategyConfigService.cs`:充电策略配置的持久化(JSON 文件存储)
### 运行时与通信层
- `CommunicationMessageService.cs`:通信报文“记录 + 解析 + 更新 ChargeStation”的核心服务
- `ChargeUdpService.cs`:UDP 监听入口,将 UDP 收到的数据转为 `CommunicationMessageService.AddReceiveMessage(...)`
- `StandardChargeMission.cs`:主业务进程(初始化充电站实例、500ms 循环下发充电指令)
### WinForms 界面层
- `ChargeStationManagementForm.cs`:充电桩管理窗口(列表、增删改、跳转其它窗口)
- `ChargeStrategyConfigForm.cs`:充电策略参数配置窗口
- `CommunicationMonitorForm.cs`:通信报文监控窗口(订阅 `MessageAdded` 并刷新表格)
- `AlarmConfigManagementForm.cs`:报警配置管理窗口(增删改、筛选与搜索)
---
## 2. 数据模型(ChargeStation / AlarmConfig / 策略)
### 2.1 `ChargeStation``Charge/ChargeStation.cs`
`ChargeStation` 是所有界面展示与通信落库的核心对象。与本模块强相关的字段包括:
- 身份与配置
- `StationId`:充电桩编号(用于唯一标识,UI 校验 1-99)
- `Name``Type`(充电桩类型:`FRLDTall` / `FRLDShort` / `MuXing`
- `ChargeMethod`(地充/尾充/侧充)
- `IpAddress``Port`:通信地址
- `SetVoltage``SetElectricCurrent`:设定值
- `Enabled`:是否启用
- `GroupCarType``SiteId`:与调度系统站点配置绑定
- 通信与状态(用于 UI 展示)
- `Status``ChargeStationStatus`):`Idle` / `Charging` / `Fault` / `Battery`
- `CommStatus``CommunicationStatus`):UI 中展示用的通讯状态(通常由 UI Ping 计算)
- `ChargeCommandStatus``ChargeCommandStatus`):最近一次“启动/停止充电指令”的状态
- `MechanismStatus`:机构伸缩状态
- `HasAlarm``AlarmLevel``AlarmMessage`:报警相关
- 实时数值
- `LastSendTime``LastReceiveTime`
- `RealTimeVoltage``RealTimeCurrent`
- `BatteryLevel``CurrentVehicle`
### 2.2 枚举含义(`ChargeStation.cs`
主要枚举:
- `ChargeStationStatus`:空闲/充电中/报警中/AGV电池已接入
- `CommunicationStatus`:未知/正常/延迟/超时/断开/错误
- `ChargeCommandStatus`:停止/启动
- `MechanismStatus`:伸出/缩回/运动中
- `AlarmLevel`:无/低/中/高/严重
- `ChargeMethodType`:地充/尾充/侧充
### 2.3 报警配置 `AlarmConfig``Charge/AlarmConfig.cs`
报警配置用于 UI 管理与展示(`AlarmConfigManagementForm` 管理)。字段包括:
- `AlarmId``AlarmCode``AlarmContent`
- `Level`(报警级别)、`Enabled`
- `Remarks`
### 2.4 策略配置 `ChargeStrategyConfig``Charge/ChargeStrategyConfig.cs`
策略配置包含 SOC 阈值、时间参数、以及开关项(例如 `AllowInterruptTask``UseLowerSocForCharge` 等),由 `ChargeStrategyConfigForm` 编辑、由 `ChargeStrategyConfigService` 持久化。
---
## 3. 持久化与服务层(DataService
### 3.1 充电桩数据持久化:`ChargeStationDataService`
入口与关键能力(来自实现):
- 获取:`GetAllStations()``GetStationById(...)``GetStationByIp(ip,port)`
- 增加:`AddStation(...)`
- 更新:`UpdateStation(...)`(可选 `isSave`)、`UpdateStationStatus(...)`
- 删除:`DeleteStation(...)`
- 刷新:`Reload()`
落库逻辑特点:
- 通信解析后会调用 `ChargeStationDataService.UpdateStation(station, out errorMessage)`,最终把 `ChargeStation` 新状态写回 JSON。
### 3.2 报警配置持久化:`AlarmConfigDataService`
入口与关键能力:
- 获取:`GetAllAlarmConfigs()``GetAlarmConfig(alarmId)``GetAlarmConfigByCode(...)`
- 增加:`AddAlarmConfig(...)`
- 更新:`UpdateAlarmConfig(...)`
- 删除:`DeleteAlarmConfig(...)`
- 刷新:`Reload()`(实现中一般会重新从文件加载)
---
## 4. 通信报文解析与落库:CommunicationMessageService
`Charge/CommunicationMessageService.cs` 是本模块最核心的“桥梁”:
1. 把发送/接收报文记录到内存队列(`LinkedList`
2. 根据报文原始 hex 字符串与协议类型 `type` 解析出结构化数据
3. 更新对应的 `ChargeStation` 字段
4. 调用 `ChargeStationDataService.UpdateStation(...)` 落库到 JSON,并触发 UI 展示更新
### 4.1 报文记录与订阅
- `MessageAdded` 事件:当新报文加入时触发
- UI 通信监控窗体(`CommunicationMonitorForm`)订阅该事件,并在 UI 线程刷新表格
### 4.2 发送报文路径(AddSendMessage -> 更新 ChargeCommandStatus 等)
- 外部调用:`AddSendMessage(ipAddress, port, rawData, type, stationId?)`
- 内部流程:
- `ParseSendRawData(rawData, type)` 解析
- `UpdateStationFromSendData(station, parsedData)` 更新:
- `LastSendTime = SendTime`
- 根据 `ChargeCommand`(启动/停止)更新 `ChargeCommandStatus`
- 更新 `BatteryLevel``CurrentVehicle`
- `ChargeStationDataService.UpdateStation(station, out errorMessage)` 落库
### 4.3 接收报文路径(AddReceiveMessage -> 更新状态/机构/告警)
- 外部调用:`AddReceiveMessage(ipAddress, port, rawData, type, stationId?)`
- 内部流程:
- `ParseReceiveRawData(rawData, type)` 解析
- `UpdateStationFromReceiveData(station, parsedData)` 更新:
- `LastReceiveTime`
- `MechanismStatus``RealTimeVoltage``RealTimeCurrent`
- `Status``Idle/Charging/Fault/Battery`
- `HasAlarm``AlarmLevel``AlarmMessage`
- `ChargeStationDataService.UpdateStation(...)` 落库
### 4.4 协议类型 `type`
解析分支中常见类型示例:
- `FRLDShort`
- `FRLDTall`
不同类型会使用不同索引位置从报文字节数组中解析字段。
---
## 5. 通信接入入口
### 5.1 UDP 接入:ChargeUdpService
`Charge/ChargeUdpService.cs`
- 创建线程监听 UDP`UdpClient(40001)`
- 循环接收并转发:
- `CommunicationMessageService.AddReceiveMessage(remoteIp, 40001, hexString, "FRLDShort")`
- 同时会通过 `SimpleProject.proj.Missions` 找到 `StandardChargeMission` 实例,并在 `chargeMission.ChargeStations` 中按 IP 找到对应站点
- 对特定站点类型(例如 `PCBChargeStation`)进一步更新站点字段(例如 `IsSafe``IndexReceive`
### 5.2 TCP 接入:以 FLChargeStation 为例(ChargeStationType
`ChargeStationType/FLChargeStation.cs` 为例:
- `OnPlaintextReceived(...)` 在收到 TCP 明文后:
- 提取报文字节(示例中 `Take(35)`
- 更新站点内的一些运行时字段(例如 `IsSafe`
- 调用 `CommunicationMessageService.AddReceiveMessage(...)`,并把 `type` 传为对应协议类型(例如 `"FRLDTall"`
> 说明:具体 TCP 断连/重连机制由底层 TCP 客户端与对应站点实现决定;无论 TCP/UDP,最终都会汇聚到 `CommunicationMessageService` 完成解析与落库。
---
## 6. 运行时充电业务循环:StandardChargeMission
`Charge/StandardChargeMission.cs` 负责把“调度系统中的车的状态 + 充电策略 + 站点配置”组合成周期性的充电指令下发。
### 6.1 初始化充电桩实例(创建 station 对象)
关键步骤(来自实现片段):
1. 遍历系统 `Site` 中带有 `fields["Charge"]` 的站点,构建 station 配置
2. 根据 `ChargeStationType` 使用反射创建 `AbstractChargeStation` 实例
3. 给站点对象赋值:
- `SiteId``Ip``Port`
- `CommunicationType`
- 示例:`FRLDShort` 时设置为 `"UDP"`;否则使用配置中的 `CommunicationType`(默认走 TCP
4. 调用 `chargeStation.CreateCommunication(ipAddress, port)` 建立通信通道
5. 把站点对象加入 `ChargeStations` 字典:`Dictionary<int, AbstractChargeStation>`
如果存在任何 UDP 站点,会创建 `UdpService ??= new ChargeUdpService()`
### 6.2 500ms 业务循环(选择车辆 -> 下发指令)
主循环(实现中包含 `Thread.Sleep(500)`)逻辑大致如下:
1. 对每个 `chargeStationEntry`(按站点遍历):
- 通过 `SimpleLib.GetAllCars()` 查找:
- 车辆当前所在站点 `c.GetLastSite() == siteId`
- 或车辆正在竞争锁/持有锁(`aquiringLock == siteId``holdingLocks.Contains(siteId)`
2. 若找到车辆:
- 判断车辆状态:`Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal`
- 判断是否正在“充电标记”(`car.tags.Contains("charging")`
- 结合锁状态与 tag 状态计算 `openCharge`0/1
3. 当未屏蔽交互(`shieldInterLock == false`)时下发指令:
- `chargeStation.SendToChargeStation(openCharge, (Car)car)`
### 6.3 Stop/ShieldInterLock/管理界面入口
- `Stop()`:中止 mission 线程,并对每个 station 调用 `CloseCommunication()`
- `ShieldInterLock()`:切换“是否屏蔽充电桩交互”
- `OpenManagementWindow()`:打开 `ChargeStationHelper.OpenManagementWindow()`
---
## 7. WinForms 界面与交互细节
### 7.1 充电桩管理:ChargeStationManagementForm
文件:`Charge/ChargeStationManagementForm.cs`
#### 核心展示数据来源
- 列表数据来源:`ChargeStationDataService.GetAllStations()`
- UI 侧通讯状态:
-`LoadStations()` 中对每个 station 执行 `Ping.Send(station.IpAddress, 1000)`
- Ping 成功则 `station.CommStatus = CommunicationStatus.Normal`,否则 `CommunicationStatus.Error`
- 电气/运行时信息来源:
- `ChargeCommandStatus``Status``MechanismStatus``HasAlarm/AlarmLevel/AlarmMessage``RealTimeVoltage/Current` 等都来自 `CommunicationMessageService` 解析并落库后的 `ChargeStation` 字段
#### 自动刷新
- `autoRefreshTimer.Interval = 3000`
- `AutoRefreshTimer_Tick`
- 保存当前选中行的 `StationId`
- 调用 `LoadStations()` 重绘
- 恢复选中行
#### 关键编辑与保存逻辑(btnSave)
- `btnSave.Text == "修改"`:先切换为编辑模式 `SetEditMode(true)`
- 新增/保存时校验:
- `StationId` 不能为空且必须是 1-99 范围整数
- 新增时禁止重复 `StationId`
- `SiteId` 必须存在于调度系统站点集合(`SimpleLib.GetSite((int)numSiteId.Value)`
- 保存调用:
- 新增:`dataService.AddStation(...)`
- 更新:`dataService.UpdateStation(..., isSave:true)`
- 同步到调度系统 `Site.fields`
- `setVoltage``setElectricCurrent`
- `group`:根据 `Enabled` 设置为 `"禁用"``GroupCarType`
#### 删除逻辑(btnDelete
- 调用 `dataService.DeleteStation(stationId, out errorMessage)`
- 同步清理 `Site.fields`
- 移除 `setVoltage``setElectricCurrent``Charge``group`
#### 列表交互
- `dgvStations_CellDoubleClick`
- 根据 `StationId` 查找 `ChargeStation`
- 调用 `LoadStationToFields(station)`
- 进入编辑模式 `SetEditMode(true, true)`
#### 其它窗口入口按钮
- `btnStrategyConfig_Click`:打开 `ChargeStrategyConfigForm`
- `btnCommMonitor_Click`:打开 `CommunicationMonitorForm`
- `btnAlarmConfig_Click`:打开 `AlarmConfigManagementForm`
- `btnExport_Click`:导出 JSON 或 CSV(从 `GetAllStations()` 读取)
### 7.2 策略配置:ChargeStrategyConfigForm
文件:`Charge/ChargeStrategyConfigForm.cs`
- 初始化:`config = configService.LoadConfig()`
- 保存:把 UI 控件值写入 `ChargeStrategyConfig` 后调用 `configService.SaveConfig(config)`
- 恢复默认:调用 `ChargeStrategyConfig.CreateDefault()` 并重新加载到界面
### 7.3 通信监控:CommunicationMonitorForm
文件:`Charge/CommunicationMonitorForm.cs`
- 初始化:
- `messageService = CommunicationMessageService.Instance`
- 窗体加载完成后订阅:`messageService.MessageAdded += OnMessageAdded`
- 新报文到达:`OnMessageAdded(...)`
-`InvokeRequired``BeginInvoke` 回 UI 线程
- 根据当前 IP 筛选条件刷新消息列表(调用 `LoadMessages()`
- 消息列表展示:
-`messageService.GetAllMessages()``GetMessagesByIp(ip)` 取出数据
- 根据 `Direction`(发送/接收)设置行颜色
- 统计信息:
- `lblStatistics.Text = $"显示: {displayCount} | 总数: ... | 发送: ... | 接收: ..."`
### 7.4 报警配置管理:AlarmConfigManagementForm
文件:`Charge/AlarmConfigManagementForm.cs`
- 界面加载:
- 初始化级别下拉框与筛选下拉框
- 调用 `LoadAlarmConfigs()`
- 列表加载逻辑:
-`AlarmConfigDataService.GetAllAlarmConfigs()` 获取全量
- 按筛选条件(等级 `cmbLevelFilter`、搜索框 `txtSearch`)过滤
- 填充 `dgvAlarmConfigs` 并根据 `AlarmLevel` 设置行颜色
- 保存:
- `selectedAlarmConfig == null` -> 新增 `dataService.AddAlarmConfig`
- 否则 -> 更新 `dataService.UpdateAlarmConfig`
- 删除:
- `dataService.DeleteAlarmConfig(selectedAlarmConfig.AlarmId, out ...)`
- 双击列表:
- `dgvAlarmConfigs_CellDoubleClick` 读取 `AlarmId` 并加载到编辑区
---
## 7(代码一致性修订):UI 窗体导航与更新流
### 7.1 `ChargeStationManagementForm`(充电桩管理)
入口/导航
- 通过 `ChargeStationHelper.OpenManagementWindow()`(单例 `Show()`)或 `ChargeStationHelper.OpenManagementDialog()``ShowDialog()`)打开。
- 窗体内通过按钮打开:
- `btnStrategyConfig_Click` -> `ChargeStrategyConfigForm.ShowDialog()`
- `btnCommMonitor_Click` -> `CommunicationMonitorForm.Show()`
- `btnAlarmConfig_Click` -> `AlarmConfigManagementForm.ShowDialog()`
更新/刷新
- 列表自动刷新:`autoRefreshTimer.Interval = 3000``AutoRefreshTimer_Tick` 会保存当前选中 `StationId`、重建 `dgvStations``LoadStations()`)、再恢复选中行。
- 关闭窗体:`OnFormClosing` 停止并释放 `autoRefreshTimer`
- `LoadStations()` 的状态刷新点:
- 数据:`ChargeStationDataService.GetAllStations()` + 按 `cmbStatusFilter` 过滤。
- 通讯状态:逐个对站点执行 `Ping.Send(station.IpAddress, 1000)`,成功/失败分别写入 `station.CommStatus`,再刷新行颜色。
- 搜索/筛选:`txtSearch_TextChanged``cmbStatusFilter_SelectedIndexChanged` 都会触发 `ApplyFilters()`,清空并重建 `dgvStations`(包含行颜色规则)。
- 手动刷新:`btnRefresh_Click` -> `dataService.Reload()` -> `LoadStations()`
编辑与保存
- 双击列表:`dgvStations_CellDoubleClick` -> `LoadStationToFields(station)` -> `SetEditMode(false)`(查看模式,`btnSave.Text="修改"`)。
- `btnSave_Click` 两段式:
- `btnSave.Text=="修改"`:仅切到编辑模式 `SetEditMode(true)`
- 否则执行保存:校验 `StationId`1-99)、新增时校验唯一性、校验 `SiteId` 存在,然后调用 `AddStation` / `UpdateStation(..., isSave:true)`
- 保存成功后同步调度系统 `Site.fields``setVoltage``setElectricCurrent``group`(启用写 `GroupCarType`,禁用写 `"禁用"`),再刷新列表并清空编辑区。
- 删除:`btnDelete_Click` 确认后 `DeleteStation`,并同步清理 `Site.fields``setVoltage``setElectricCurrent``Charge``group`)。
### 7.2 `ChargeStrategyConfigForm`(充电策略配置)
入口/导航
- 通常由管理窗体打开:`ChargeStationManagementForm``btnStrategyConfig_Click` 使用 `ShowDialog()`
更新/刷新
- 初始化:`configService = ChargeStrategyConfigService.Instance`,构造时 `LoadConfig()` 把文件配置加载到界面控件。
- 保存/应用:`btnSave_Click``btnApply_Click` 都会先 `ValidateConfig()` 校验阈值关系,再把控件值写回 `config` 并调用 `configService.SaveConfig(config)`
- 恢复默认:`btnRestoreDefaults_Click` 确认后 `config = ChargeStrategyConfig.CreateDefault()`,调用 `LoadConfig(true)` 刷新界面,但不自动保存(状态提示“未保存”)。
- 取消:`btnCancel_Click` -> `Close()`
### 7.3 `CommunicationMonitorForm`(通信监控)
入口/导航
- 由管理窗体 `btnCommMonitor_Click` 打开:`Show()`(非阻塞)。
更新/刷新(事件驱动)
- 构造中拿到 `messageService = CommunicationMessageService.Instance``FormClosing` 退订 `MessageAdded`
- `CommunicationMonitorForm_Load`
- `InitializeForm()` + `LoadMessages()` 后设置 `isFormLoaded=true`
- 再订阅 `messageService.MessageAdded += OnMessageAdded`
- `OnMessageAdded`
- `InvokeRequired``BeginInvoke` 回 UI 线程
- 新 IP 则刷新 `cmbIpFilter``RefreshIpFilter()`
- 若当前筛选匹配(“全部”或等于当前消息 IP)则调用 `LoadMessages()` 重建消息列表
- 手动操作:
- `cmbIpFilter_SelectedIndexChanged` -> `LoadMessages()`
- `btnRefresh_Click` -> `RefreshIpFilter()` + `LoadMessages()`
- `btnClear_Click`:确认 -> `messageService.Clear()` -> 刷新列表并清空 `txtParsedData`
- 列表选择与解析展示:
- `dgvMessages_SelectionChanged` 根据所选行构造临时 `CommunicationMessage`,再调用 `ParseMessage()`,并将解析结果写入 `txtParsedData`
### 7.4 `AlarmConfigManagementForm`(报警配置管理)
入口/导航
- 由管理窗体 `btnAlarmConfig_Click` 打开:`ShowDialog()`
更新/刷新(加载 + 筛选/搜索)
- 构造:`dataService = AlarmConfigDataService.Instance`,并订阅 `this.Load += AlarmConfigManagementForm_Load`
- `InitializeForm()`
- 初始化 `cmbLevel``cmbLevelFilter`
- 调用 `LoadAlarmConfigs()` 加载列表
- 调用 `ClearEditFields()` 初始化编辑区(默认新增态)
- `LoadAlarmConfigs()`
- 数据源:`dataService.GetAllAlarmConfigs()`
- 过滤:`cmbLevelFilter`(映射到 `AlarmLevel`)与 `txtSearch`(匹配 `AlarmId/AlarmCode/AlarmContent`
- 填充 `dgvAlarmConfigs` 并按 `AlarmLevel` + `Enabled` 设置行颜色/样式,同时更新统计与标题
- 实时刷新:`txtSearch_TextChanged``cmbLevelFilter_SelectedIndexChanged` 都直接调用 `LoadAlarmConfigs()``btnRefresh_Click``dataService.Reload()` 后重新加载。
编辑与保存
- 双击列表:`dgvAlarmConfigs_CellDoubleClick` 读取 `AlarmId` -> `dataService.GetAlarmConfig(alarmId)` -> `LoadAlarmConfigToFields()`(编号不可编辑,切为编辑态)。
- 保存:`btnSave_Click` 校验 `numAlarmCode >= 0``txtAlarmContent` 非空;根据是否选中项决定 `AddAlarmConfig``UpdateAlarmConfig`;成功后刷新列表并清空编辑区。
- 删除:`btnDelete_Click` 确认后 `DeleteAlarmConfig(selectedAlarmConfig.AlarmId)`,成功后刷新列表并清空编辑区。
- 取消/关闭:`btnCancel_Click` 清空编辑区,`btnClose_Click` 关闭窗体。
---
## 8. 关键调用链(建议排查/理解用)
### 8.1 周期循环下发充电指令 -> 发送报文记录 -> ChargeCommandStatus 更新
```mermaid
flowchart TD
A[StandardChargeMission 500ms循环] --> B[chargeStation.SendToChargeStation(openCharge, car)]
B --> C[chargeStation 内部构造发送报文]
C --> D[CommunicationMessageService.AddSendMessage(...)]
D --> E[ParseSendRawData(type)]
E --> F[UpdateStationFromSendData]
F --> G[ChargeStationDataService.UpdateStation]
G --> H[ChargeStation 字段落库]
H --> I[ChargeStationManagementForm(3s刷新) 展示]
```
### 8.2 TCP/UDP 接收报文 -> 解析 -> ChargeStation 状态与告警更新 -> UI 展示
```mermaid
flowchart TD
A[TCP 收到明文 或 UDP 收到报文] --> B[CommunicationMessageService.AddReceiveMessage(...)]
B --> C[ParseReceiveRawData(type)]
C --> D[UpdateStationFromReceiveData]
D --> E[ChargeStationDataService.UpdateStation]
E --> F[ChargeStation 字段落库]
F --> G[ChargeStationManagementForm(3s刷新) 展示 Status/告警/电压电流]
```
### 8.3 通信监控界面订阅报文事件
```mermaid
flowchart TD
A[CommunicationMessageService.AddMessage/MessageAdded] --> B[CommunicationMonitorForm.OnMessageAdded]
B --> C[BeginInvoke 切到UI线程]
C --> D[LoadMessages 刷新 dgvMessages]
```
---
## 9. 常用调试点(建议)
- 通信解析落库:
-`CommunicationMessageService``UpdateStationFromSendData/ReceiveData` 更新了哪些字段
- UI 展示:
- `ChargeStationManagementForm.LoadStations()` 中的 `Ping.Send(...)` 会影响 `CommStatus` 展示
- 如果“列表里状态不变”:
- 优先确认报文是否真的进入 `CommunicationMessageService.AddSendMessage/AddReceiveMessage`
- 再确认解析是否返回非 null(解析失败会直接 `return null`
# StandardScene/Charge:充电桩数据模型与持久化(仅数据层)
本页聚焦 `StandardScene/Charge/` 中与“数据模型 + DataService 持久化/更新 API”相关的部分,覆盖:
1. `ChargeStation`:充电桩配置/运行时状态字段含义与 `JsonIgnore` 持久化边界
2. `ChargeStationDataService``Config/ChargeStations.json` 的读取/保存、增删改与状态更新
3. `AlarmConfig``AlarmConfigDataService``Config/AlarmConfigs.json` 的读取/保存、增删改
---
## 1. 数据模型:`ChargeStation`
文件:`Charge/ChargeStation.cs`
### 1.1 配置/计算字段说明(按 `JsonIgnore` 区分)
`ChargeStation` 的下列字段用于“充电桩配置”,在 JSON 里会被序列化(即:未标注 `JsonIgnore`):
- `StationId`:充电桩编号(唯一标识)
- `Name`:充电桩名称
- `Type`:充电桩类型(`ChargeStationType`
- `ChargeMethod`:充电方式(`ChargeMethodType`
- `IpAddress`IP 地址
- `Port`:端口号
- `CommunicationType`:通讯类型(属性初始值为 `"TCP"`,但构造函数会覆盖为 `"UDP"`
- `SetVoltage`:额定电压(V
- `SetElectricCurrent`:额定电流(A
- `Enabled`:是否启用
- `GroupCarType`:停靠车辆类型(`ChargeStationCarType`
- `SiteId`:关联站点 ID(可选)
- `ShieldSiteMechanismStatus`:屏蔽机构状态交互
- `Remarks`:备注
- `CreatedTime`:创建时间
- `ModifiedTime`:最后修改时间
- `Power`:计算属性(`SetVoltage * SetElectricCurrent`),标注了 `[JsonIgnore]`,不会写入 JSON
### 1.2 运行时状态字段(不会被持久化到 JSON)
以下字段标注了 `[JsonIgnore]`,因此不会写入 `Config/ChargeStations.json`(重启后这些运行时状态通常会丢失):
- `RealTimeVoltage``RealTimeCurrent`:实时电压/电流
- `LastSendTime``LastReceiveTime`:最后发送/接收时间
- `HasAlarm``AlarmMessage``AlarmLevel`:报警标记/报警文本/报警级别
- `CommStatus``LastCommunicationTime`:通讯状态/最后通讯时间(注意:当前代码里通讯状态字段的更新路径不在本节展开)
- `MechanismStatus`:机构伸缩状态
- `CurrentVehicle`:当前充电车辆编号
- `BatteryLevel`:当前电量百分比
- `ChargeCommandStatus`:发送充电指令状态(停止/启动)
- `Status`:充电桩状态(空闲/充电中/报警中/AGV电池已接入)
### 1.3 校验:`IsValid(out errorMessage)`
`ChargeStation.IsValid()` 约束:
- `StationId``Name``IpAddress` 不能为空
- `IpAddress` 需为可解析的 IP
- `Port` 必须在 `1-65535`
- `SetVoltage` 必须在 `(0, 64]`
- `SetElectricCurrent` 必须在 `(0, 101]`
---
## 2. 数据服务:`ChargeStationDataService`
文件:`Charge/ChargeStationDataService.cs`
### 2.1 单例与持久化文件
- 单例:`ChargeStationDataService.Instance`
- 内部数据:`private List<ChargeStation> chargeStations`
- JSON 文件路径:基于运行目录写入
- `AppDomain.CurrentDomain.BaseDirectory/Config/ChargeStations.json`
- 构造函数会确保 `Config/` 目录存在,并执行 `LoadData()`
### 2.2 读取:`LoadData()`
行为:
- 若文件存在:读取文本并 `JsonConvert.DeserializeObject<List<ChargeStation>>(json)`
- 若文件不存在:初始化为空列表(并不会自动生成默认样例)
- 异常:记录诊断日志并回退到空列表
### 2.3 保存:`SaveData()`
行为:
- 在锁 `lockObj` 下序列化整个 `chargeStations` 列表
- 写入文件 `Config/ChargeStations.json``Formatting.Indented`
- 保存失败:返回 `false` 并由调用方回滚内存状态(部分方法会回滚)
### 2.4 查询 API
- `List<ChargeStation> GetAllStations()`:返回列表副本(拷贝)
- `ChargeStation GetStationById(string stationId)`:按 `StationId` 查找
- `ChargeStation GetStationByIp(string ipAddress, int port)`:按 `IpAddress + Port` 查找
- `List<ChargeStation> GetIdleStations()`:过滤 `Enabled && Status == Idle`
- `int GetChargingCount()`:统计 `Status == Charging`
- `void Reload()`:重新执行 `LoadData()`
### 2.5 新增:`AddStation(ChargeStation station, out string errorMessage)`
关键点:
- `station == null` 返回失败
- 先执行 `station.IsValid(out errorMessage)`
- 唯一性校验:
- `StationId` 不可重复
- `IpAddress + Port` 组合不可重复
- 写入字段:
- 设置 `CreatedTime` / `ModifiedTime` 为当前时间
- 成功后:`SaveData()`;失败则将新增对象从内存移除
### 2.6 更新:`UpdateStation(ChargeStation station, out string errorMessage, bool isSave = false)`
该方法同时被用作“配置更新”与“运行时状态合并后再落盘”的入口之一(不同调用方会用不同的 `isSave` 值)。
核心流程:
- 校验:`station.IsValid(out errorMessage)`
- 找到原对象:`existingStation = chargeStations.FirstOrDefault(s => s.StationId == station.StationId)`
- 冲突校验:`IpAddress + Port` 不能被其它站点占用
- 时间处理:
- 保留 `existingStation.CreatedTime`
- 更新 `station.ModifiedTime = DateTime.Now`
- 赋值策略取决于 `isSave`
- `isSave == true`:仅将“配置类字段”拷贝到 `existingStation`(并令 `station = existingStation`
- `isSave == false`:不进行字段级拷贝,直接用传入的 `station` 替换列表里的对应项
- 之后无论 `isSave` 为何都会执行 `SaveData()` 并落盘整个列表
- 保存失败:回滚为 `existingStation`
持久化边界提醒(结合 `ChargeStation``JsonIgnore`):
- 因为 `Status / Alarm / 实时电压电流 等运行时字段` 都是 `JsonIgnore`,即使 `UpdateStation` 被用于合并运行时字段,重启后这些运行时字段仍不会出现在 JSON 中
-`ModifiedTime`(未 `JsonIgnore`)会被写入,因此会出现“通信上报频繁导致 JSON 文件 `ModifiedTime` 刷新”的现象
### 2.7 删除:`DeleteStation(string stationId, out string errorMessage)`
-`stationId` 找到对象并移除
- 成功后保存;失败则将对象重新加入内存
- 代码中原本有“如果正在充电则禁止删除”的检查,但被注释掉了
### 2.8 状态更新(运行时):`UpdateStationStatus(string stationId, ChargeStationStatus status)`
- 修改内存对象的 `Status``ModifiedTime`
- 然后 `SaveData()`
- 由于 `Status` 标注了 `JsonIgnore`,因此重启后站点 `Status` 通常不会从 JSON 恢复(但 `ModifiedTime` 会更新)
---
## 3. 数据模型:`AlarmConfig`
文件:`Charge/AlarmConfig.cs`
### 3.1 字段含义(会被持久化)
`AlarmConfig` 没有 `JsonIgnore`,因此以下字段都能写入 `Config/AlarmConfigs.json`
- `AlarmId`:报警编号(构造函数自动生成,格式类似 `ALMyyyyMMddHHmmssxxx`
- `AlarmCode`:报警编码值(int
- `AlarmContent`:报警内容描述(文本)
- `Level`:报警级别(`AlarmLevel`None/Low/Medium/High/Critical
- `Enabled`:是否启用
- `Remarks`:备注
- `CreatedTime` / `ModifiedTime`:创建与修改时间
### 3.2 校验:`IsValid(out errorMessage)`
- `AlarmId` 不能为空
- `AlarmCode >= 0`
- `AlarmContent` 不能为空
---
## 4. 数据服务:`AlarmConfigDataService`
文件:`Charge/AlarmConfigDataService.cs`
### 4.1 单例与持久化文件
- 单例:`AlarmConfigDataService.Instance`
- 数据文件路径:`AppDomain.CurrentDomain.BaseDirectory/Config/AlarmConfigs.json`
- 构造函数调用 `LoadData()`;若目录不存在则创建
### 4.2 读取:`LoadData()`
行为:
- 文件存在:读取并反序列化为 `List<AlarmConfig>`
- 若反序列化结果为 `null`,回退为空列表
- 文件不存在:初始化默认报警配置 `InitializeDefaultAlarms()`,随后 `SaveData()`
- 异常:记录 `Debug.WriteLine`,回退到空列表并初始化默认报警配置
默认报警包含(示例):
- 1001:电压过高
- 1002:电压过低
- 1003:电流过大
- 2001:温度异常
- 3001:通讯超时
- 3002:连接断开
### 4.3 保存:`SaveData()`
- 序列化整个 `_alarmConfigs` 并写入 `AlarmConfigs.json`
- 保存失败会抛出异常(不只是返回 `false`
### 4.4 查询 API
- `List<AlarmConfig> GetAllAlarmConfigs()`:返回列表副本
- `AlarmConfig GetAlarmConfig(string alarmId)`:按 `AlarmId` 查找
- `AlarmConfig GetAlarmConfigByCode(int alarmCode)`:按 `AlarmCode` 查找
### 4.5 新增:`AddAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)`
- 先执行 `alarmConfig.IsValid(out errorMessage)`
- 校验 `AlarmCode` 唯一性(不允许重复)
- 添加到列表后 `SaveData()`
### 4.6 更新:`UpdateAlarmConfig(AlarmConfig alarmConfig, out string errorMessage)`
- 校验:`IsValid`
- 查找目标:按 `AlarmId` 找到索引;不存在则失败
- 冲突校验:`AlarmCode` 不能被其它报警配置占用
- 设置 `alarmConfig.ModifiedTime = DateTime.Now`
- 替换列表项并 `SaveData()`
### 4.7 删除:`DeleteAlarmConfig(string alarmId, out string errorMessage)`
-`AlarmId` 找到并移除
- 然后 `SaveData()`
### 4.8 重新加载:`Reload()`
- 在锁下重新执行 `LoadData()`
---
## 5. 与“更新路径”的关系(为何运行时变化也会触发落盘)
虽然本页主要讲 DataService,但为了说明“哪些字段会/不会出现在 JSON 里”,需要点到调用关系:
- 通讯层(`Charge/CommunicationMessageService.cs`)在解析发送/接收报文后,会:
- 更新 `ChargeStation` 的运行时字段(例如 `HasAlarm``Status``RealTimeVoltage/Current` 等)
- 然后调用 `ChargeStationDataService.UpdateStation(station, out errorMessage)`(使用默认 `isSave=false`
- UI 保存站点配置(`Charge/ChargeStationManagementForm.cs`)在“保存/修改配置”时会调用:
- `ChargeStationDataService.UpdateStation(station, out errorMessage, true)`
- 报警配置的 UI 增删改(`Charge/AlarmConfigManagementForm.cs`)直接调用:
- `AddAlarmConfig / UpdateAlarmConfig / DeleteAlarmConfig`
因此你会观察到:
- `ChargeStations.json` 中的“运行时字段”不会被写入(因为它们带 `JsonIgnore`
-`ModifiedTime` 这类未忽略字段会被写入,所以文件仍会频繁变化
---
## 6. 通讯报文解析:`CommunicationMessageService` 如何更新 `ChargeStation`
本节重点解释 `Charge/CommunicationMessageService.cs` 中“报文解析 -> 更新充电桩运行时字段”的完整链路(并说明当前实现里哪些字段没有被真正落到 `ChargeStation`)。
### 6.1 入口与站点匹配规则
`CommunicationMessageService` 通过两个入口接收外部报文,并在内部完成“解析 + 更新 + 落盘(通过 DataService)”:
- 发送报文入口:`AddSendMessage(ipAddress, port, rawData, type, stationId)`
- 接收报文入口:`AddReceiveMessage(ipAddress, port, rawData, type, stationId)`
两条链路在解析前都会做同样的站点匹配:
- 先拿到单例:`ChargeStationDataService.Instance`
- 通过 `GetStationByIp(ipAddress, port)` 找到对应 `ChargeStation`
- 找不到站点直接返回(此时只会记录报文,不会更新该站点运行时字段)
解析成功后才会调用:
- `ChargeStationDataService.UpdateStation(station, out errorMessage)`(该调用在当前代码里使用默认参数,最终会落盘整个 `ChargeStations.json`;但由于运行时字段多为 `JsonIgnore`,重启后这些运行时值不会恢复)
异常处理方面:
- `ParseSendDataAndUpdateStation` / `ParseReceiveDataAndUpdateStation` 都使用 `try/catch` 并“静默吞掉异常”,因此解析失败通常表现为:报文列表有记录,但充电桩字段没有变化。
### 6.2 发送报文解析与字段更新(`UpdateStationFromSendData`
发送报文完整调用链如下:
`AddSendMessage` -> `ParseSendDataAndUpdateStation`
-> `ParseSendRawData(rawData, type)`
-> `UpdateStationFromSendData(station, parsedData)`
-> `ChargeStationDataService.UpdateStation(...)`
#### 6.2.1 `ParseSendRawData` 输入格式与 `type` 支持
`ParseSendRawData` 的输入要求:
- `rawData` 以空格分隔字节 token(例如:`"BB 01 42 ..."`
- 每个 token 会按十六进制解析:`byte.TryParse(token, NumberStyles.HexNumber, ...)`
- 发送报文最少 token 数:`parts.Length >= 10`
当前实现里,`type` 仅对以下两种有明确字节位映射:
- `FRLDShort`
- `FRLDTall`
其他 `type`(例如 `MuXing`)不会命中映射分支,此时解析出来的数值保持默认值,然后仍可能触发 `UpdateStationFromSendData` 的“默认覆盖”逻辑(见下文“已知限制”)。
#### 6.2.2 从发送报文写入哪些 `ChargeStation` 字段
`UpdateStationFromSendData` 实际更新的字段如下(直接对应代码赋值):
- `station.LastSendTime = parsedData.SendTime`
- `station.ChargeCommandStatus`
- `parsedData.ChargeCommand == 1` -> `ChargeCommandStatus.Started`
- `parsedData.ChargeCommand == 0` -> `ChargeCommandStatus.Stopped`
- `station.BatteryLevel = parsedData.BatteryLevel`
- `station.CurrentVehicle = parsedData.CurrentVehicleId.ToString()`
注意:
- `UpdateStationFromSendData``SetVoltage` / `SetElectricCurrent` 的赋值被注释掉了(即:发送报文不会更新 `ChargeStation.SetVoltage` / `ChargeStation.SetElectricCurrent` 的配置目标值)。
### 6.3 接收报文解析与字段更新(`UpdateStationFromReceiveData`
接收报文完整调用链如下:
`AddReceiveMessage` -> `ParseReceiveDataAndUpdateStation`
-> `ParseReceiveRawData(rawData, type)`
-> `UpdateStationFromReceiveData(station, parsedData)`
-> `ChargeStationDataService.UpdateStation(...)`
#### 6.3.1 `ParseReceiveRawData` 输入格式与 `type` 支持
`ParseReceiveRawData` 的输入要求:
- `rawData` 以空格分隔字节 token`rawData.Split(' ')`
- 接收报文最少 token 数:`parts.Length >= 30`
- 每个 token 的解析使用的是 `byte.TryParse(parts[i], out bytes[i])`(没有显式 `NumberStyles.HexNumber`
因此当 `rawData` token 形如十六进制字节(例如 `0A``FF`)时,可能出现解析失败导致 `parsedData == null`(从而不会更新站点字段)的情况。
`type` 的字节位映射同样只实现了两种:
- `FRLDShort`
- `FRLDTall`
#### 6.3.2 从接收报文写入哪些 `ChargeStation` 字段
`UpdateStationFromReceiveData` 实际更新的字段如下:
- `station.LastReceiveTime = parsedData.ReceiveTime`
- `station.MechanismStatus = parsedData.MechanismStatus`
- `station.RealTimeVoltage = parsedData.RealTimeVoltage`
- `station.RealTimeCurrent = parsedData.RealTimeCurrent`
- `station.Status = parsedData.Status`
- `station.HasAlarm = parsedData.HasAlarm`
- `station.AlarmLevel = parsedData.AlarmLevel`
- `station.AlarmMessage`
- `parsedData.HasAlarm == true` -> `报警级别: {GetAlarmLevelText(parsedData.AlarmLevel)}`
- 否则 -> `string.Empty`
与报警相关的映射:
- `ParseReceiveRawData``HasAlarm = chargeStationStatus == 2`
- `ParseStationStatus``statusByte == 2` 映射为 `ChargeStationStatus.Fault`
当前实现里 `AlarmLevel` 的来源有一个明显限制:
- `ParseReceiveRawData``AlarmLevel = ParseAlarmLevel(bytes[20])` 被注释掉了
- 因此 `parsedData.AlarmLevel` 多半保持默认值(`AlarmLevel.None`),但只要 `HasAlarm == true``AlarmMessage` 仍会按默认 `AlarmLevel` 生成文本
同时,`ParsedReceiveData` 中的以下字段虽然会解析出来,但 `UpdateStationFromReceiveData` 没有把它们写入 `ChargeStation`
- `ParsedReceiveData.CommStatus`
- `ParsedReceiveData.ChargeCommandStatus`
- `ParsedReceiveData.ChargeID`
- `ParsedReceiveData.BatteryAH`
### 6.4 已知限制/行为总结(影响“字段是否更新”)
1. 解析失败只影响“字段更新”,不影响“报文记录与 UI 列表展示”
- 报文一定会先进入 `_messages`(并触发 `MessageAdded`
- 但解析函数返回 `null` / 站点找不到 / 异常时,字段更新不会发生
2. 站点匹配使用 `IP + Port`
- `GetStationByIp(ipAddress, port)` 找不到对应 `ChargeStation` 时,不会更新该站点运行时字段
3. `type` 只对 `FRLDShort` / `FRLDTall` 完成了映射
- 发送侧对未知 `type` 仍会返回默认 `ParsedSendData`,从而可能覆盖 `ChargeCommandStatus` / `BatteryLevel` / `CurrentVehicle` 为默认值
- 接收侧未知 `type` 也可能产生默认 `ParsedReceiveData`,但前提是 `rawData.Split(' ')` 后仍满足 `parts.Length >= 30`
4. 接收侧 token 解析方式可能与输入十六进制格式不一致
- `ParseReceiveRawData` 未使用 `NumberStyles.HexNumber`
- 如果 `rawData` token 是十六进制字节(如 `0A`),可能导致 `parsedData == null`,进而不更新实时字段
## 7. 运行时充电业务:StandardChargeMission
本节聚焦 `Charge/StandardChargeMission.cs` 中的“充电进程启动 + 500ms 充电业务循环”,并跟踪 `SendToChargeStation(...)` 的真实调用路径到具体充电桩实现类。
### 7.1 启动入口:`Execute()`
`StandardChargeMission.Execute()` 负责启动充电进程,核心流程:
- 设置进程状态:`status.status = "已启动"`
- 防重复启动:通过 `myStarted` 判断,避免重复创建线程
- 初始化运行时字典:`ChargeStations = new Dictionary<int, AbstractChargeStation>()`
- 创建后台线程:`ChargeThread = new Thread(() => { ... })`
- 在线程内部完成“充电站初始化 + 500ms 业务循环”
- 启动辅助任务:定期上传带 `unavailable` 标签的站点到迷毂系统(同样是 `Thread.Sleep(500)` 周期)
- 最后调用 `base.Execute()`,让基类调度/联锁逻辑继续工作
### 7.2 初始化:后台线程 Step1(创建/重建 `AbstractChargeStation`
`ChargeThread``while (true)` 内部,每一轮都会先执行“步骤1:初始化充电站”:
- 读取配置:`ChargeStationHelper.GetAllStationConfigs()`
- 底层来自 `ChargeStationDataService.Instance.GetAllStations()`
- 遍历每个充电桩配置项,执行校验与创建:
- `Enabled == false`:跳过
- 校验 `SiteId > 0``IpAddress` 可解析、`Port``1-65535`
- 若字典里已存在相同 `siteId` 的站点:
- 当 IP/Port 发生变化:`existingStation.CloseCommunication()` 后更新 `Ip/Port` 并重新 `CreateCommunication(...)`
- IP/Port 未变化:直接 `continue`(复用原连接)
- 若不存在:
- 使用 `GetChargeTypeString(stationConfig.Type)` 映射到具体站点类名:
- `FRLDTall` -> `FLChargeStation`
- `FRLDShort` -> `PCBChargeStation`
- `MuXing` -> `MuXingChargeStation`
- 默认回退 -> `PCBChargeStation`
- `Activator.CreateInstance(type)` 创建对象,设置:
- `SiteId / Ip / Port`
- `CommunicationType``FRLDShort` 强制 `UDP`,其它使用配置里的 `CommunicationType`
- 调用 `CreateCommunication(ipAddress, port)` 建立通信连接
- 放入字典:`ChargeStations.Add(siteId, stationInstance)`
同时,线程内部还会做 UDP 服务初始化:
- 若存在任意站点 `CommunicationType == "UDP"`
- `UdpService ??= new ChargeUdpService();`
### 7.3 500ms 业务循环:后台线程 Step3 + `SendToChargeStation(...)`
`ChargeThread` 的主循环结构(简化):
1. 读取互锁开关:`var shieldInterLock = ((StandardChargeMissionStatus)status).ShieldInterLock`
2. 更新/清理配置绑定:
-`ChargeStationHelper.GetStationBySiteId(siteId) == null`:从 `ChargeStations` 移除该站点
-`SimpleLib.GetAllSites()` 中仍带 `fields["Charge"]` 但不在 `ChargeStations` 配置里的站点:
- 移除 `Charge / setVoltage / setElectricCurrent / group` 等字段
3. 遍历每个站点,执行“车辆搜索 -> openCharge 计算 -> 下发”:
- 取站点配置:`chargeStationSetting = ChargeStationHelper.GetStationBySiteId(siteId)`
-`!chargeStationSetting.Enabled`:跳过
- 将站点配置绑定回 `site.fields`
- `site.fields["Charge"] = "True"`
- `site.fields["setVoltage"] = chargeStationSetting.SetVoltage.ToString("0.0")`
- `site.fields["setElectricCurrent"] = chargeStationSetting.SetElectricCurrent.ToString("0.0")`
- `site.fields["group"]`:启用时写 `GroupCarType`,禁用时写 `"禁用"`
- 设置站点进入/离开权限:
- `ChargeMethodType.Side` 分支:`SetAllowEnter / SetAllowExit``ShieldSiteMechanismStatus` / `MechanismStatus == Retracted` 联动
-`Side`:直接 `SetAllowEnter(true) / SetAllowExit(true) / SetAcknowledgeLeave(true)`
- 查找与该站点相关的车辆(在站/获取锁/持有锁):
- `GetLastSite() == siteId``aquiringLock == siteId``holdingLocks.Contains(siteId)`
- 计算 `openCharge`
- 默认 `0`
- 仅当车辆存在且 `Commons.GetVehicleStatus((Car)car) == VehicleStatus.Normal`
- 并且满足充电条件:
- `charging` 标记存在
- 未被占用:`!car.tags.Contains("occupied")`
- 锁状态匹配:`holdingLocks.Length == 1``pendingLocks.Length == 0`
-`openCharge = 1`
- 互锁门控后下发指令:
-`!shieldInterLock`
- `chargeStation.SendToChargeStation(openCharge, (Car)car);`
4. 循环尾部固定节拍:`Thread.Sleep(500)`
### 7.4 `SendToChargeStation` 调用链(下发路径)
在 500ms 循环中,下发的调用路径是:
`StandardChargeMission(ChargeThread 500ms loop)`
-> `AbstractChargeStation` 子类 `SendToChargeStation(int isCharge, Car car)`
-> 子类内部组包 + 记录发送报文:`CommunicationMessageService.Instance.AddSendMessage(...)`
-> 通过 TCP/UDP 通道真正发送报文
各站点实现类的“发送端”关键点:
- `FLChargeStation.SendToChargeStation`
- 依赖 `IsConnected && Client != null`,否则不发送
- 读取 `Car``Soc/Voltage/ElectricCurrent`,并可覆盖 `site.fields["setVoltage"]/["setElectricCurrent"]`
- `AddSendMessage(..., "FRLDTall", site?.name)``Client.Send(msg)`
- `MuXingChargeStation.SendToChargeStation`
- 计算 `openChargePort = (isCharge == 1 ? 2 : 3)` 并组包(包含时间戳与 CRC
- `AddSendMessage(..., "MuXing")` 后写入 TCP `stream`
- `PCBChargeStation.SendToChargeStation``FRLDShort`
- 使用 `UdpClient` 发送
- `AddSendMessage(..., "FRLDShort", site?.name)``udpClient.SendAsync(msg, msg.Length, _endPoint)`
```mermaid
flowchart TD
A[StandardChargeMission.Execute\n启动 ChargeThread] --> B[ChargeThread while(true)]
B --> C[Step1 初始化/重建 ChargeStations]
B --> D[Step3 遍历每个站点]
D --> E[计算 openCharge(0/1)]
E --> F{!ShieldInterLock}
F -->|false| Z[跳过下发]
F -->|true| G[chargeStation.SendToChargeStation(openCharge, car)]
G --> H[站点子类组包]
H --> I[CommunicationMessageService.AddSendMessage]
I --> J[TCP/UDP 发送报文]
```
@@ -0,0 +1,414 @@
# 🔌 充电桩实时数据功能说明
## ✅ 已完成的修改
### 1. 数据模型更新(ChargeStation.cs
添加了实时电压和电流字段:
```csharp
/// <summary>
/// 实时电压 (V) - 当前充电时的实际电压
/// </summary>
[DisplayName("实时电压(V)")]
public double RealTimeVoltage { get; set; }
/// <summary>
/// 实时电流 (A) - 当前充电时的实际电流
/// </summary>
[DisplayName("实时电流(A)")]
public double RealTimeCurrent { get; set; }
```
### 2. 列表显示更新
#### ❌ 移除的列:
- **功率(W)** - 功率列已移除
#### ✅ 新增的列:
- **实时电压(V)** - 显示充电桩当前实际电压
- **实时电流(A)** - 显示充电桩当前实际电流
#### 列表结构(更新后):
```
┌────────┬──────┬────────┬──────────┬────┬────────┬────────┬──────────┬──────────┬────┬────┬──────┬────┐
│ 编号 │ 名称 │ 类型 │ IP地址 │端口│ 电压(V)│ 电流(A)│实时电压(V)│实时电流(A)│状态│启用│站点ID│备注│
├────────┼──────┼────────┼──────────┼────┼────────┼────────┼──────────┼──────────┼────┼────┼──────┼────┤
│CS12345 │1号桩 │标准 │192.168..│502 │220.0 │32.0 │215.5 │28.3 │充电│是 │1001 │... │
│CS12346 │2号桩 │快速 │192.168..│502 │380.0 │63.0 │0.0 │0.0 │空闲│是 │1002 │... │
└────────┴──────┴────────┴──────────┴────┴────────┴────────┴──────────┴──────────┴────┴────┴──────┴────┘
```
### 3. 界面更新
#### ❌ 移除的按钮:
- **新增按钮** - 已从界面移除
#### ✅ 保留的按钮(重新排列):
- **保存** - 位置调整到最左侧(20, 20),尺寸 100×50
- **删除** - 位置调整到中间(150, 20),尺寸 100×50
- **取消** - 位置调整到右侧(280, 20),尺寸 100×50
```
┌──────────────────────────────────┐
│ │
│ [ 保存 ] [ 删除 ] [ 取消 ]│
│ (蓝色) (红色) (默认) │
│ │
└──────────────────────────────────┘
```
### 4. 编辑区保留功能
**设置电压和电流功能完全保留**
```
┌─────────────────────────────────┐
│ 充电桩信息 │
├─────────────────────────────────┤
│ │
│ 名称: [1号充电桩 ] │
│ 类型: [标准充电桩 ▼] │
│ IP地址:[192.168.1.100 ] │
│ 端口: [502 ▲▼] │
│ │
│ 电压(V)[220.0 ▲▼] │ ← 额定电压(设置值)
│ 电流(A)[32.0 ▲▼] │ ← 额定电流(设置值)
│ │
│ 状态: [空闲 ▼] │
│ ☑ 启用充电桩 │
│ ... │
└─────────────────────────────────┘
```
---
## 📊 字段说明
### 电压和电流的区别
| 字段 | 类型 | 说明 | 用途 |
|------|------|------|------|
| **Voltage** | 额定电压 | 充电桩的设计电压(固定值) | 充电桩参数配置 |
| **Current** | 额定电流 | 充电桩的设计电流(固定值) | 充电桩参数配置 |
| **RealTimeVoltage** | 实时电压 | 当前实际工作电压(动态值) | 实时监控显示 |
| **RealTimeCurrent** | 实时电流 | 当前实际工作电流(动态值) | 实时监控显示 |
### 典型场景示例
#### 场景1:充电桩空闲时
```
额定电压:220.0V
额定电流:32.0A
实时电压:0.0V ← 未在充电,实时值为0
实时电流:0.0A ← 未在充电,实时值为0
状态:空闲
```
#### 场景2:充电桩充电中
```
额定电压:220.0V
额定电流:32.0A
实时电压:215.5V ← 实际充电电压
实时电流:28.3A ← 实际充电电流
状态:充电中
实时功率:6098.65W (215.5V × 28.3A)
```
#### 场景3:充电桩故障
```
额定电压:220.0V
额定电流:32.0A
实时电压:180.2V ← 电压异常偏低
实时电流:5.1A ← 电流异常偏低
状态:故障
告警:电压低于额定值20%
```
---
## 💻 代码实现
### 1. 创建充电桩时初始化
```csharp
var station = new ChargeStation
{
Name = "1号充电桩",
Type = ChargeStationType.Standard,
IpAddress = "192.168.1.100",
Port = 502,
// 额定参数(固定)
Voltage = 220.0,
Current = 32.0,
// 实时参数(初始为0
RealTimeVoltage = 0.0,
RealTimeCurrent = 0.0,
Status = ChargeStationStatus.Idle
};
```
### 2. 更新实时数据(模拟PLC数据)
```csharp
/// <summary>
/// 更新充电桩实时数据
/// </summary>
public void UpdateRealTimeData(string stationId, double voltage, double current)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
station.RealTimeVoltage = voltage;
station.RealTimeCurrent = current;
dataService.UpdateStation(station, out string errorMsg);
// 检查异常
CheckVoltageCurrentAbnormal(station);
}
}
/// <summary>
/// 检查电压电流是否异常
/// </summary>
private void CheckVoltageCurrentAbnormal(ChargeStation station)
{
// 充电中才检查
if (station.Status == ChargeStationStatus.Charging)
{
// 电压偏差超过20%
double voltageDiff = Math.Abs(station.RealTimeVoltage - station.Voltage) / station.Voltage;
if (voltageDiff > 0.2)
{
Diagnosis.Log($"充电桩 {station.Name} 电压异常: " +
$"额定{station.Voltage}V, 实时{station.RealTimeVoltage}V",
"ChargeStation", true);
}
// 电流偏差超过20%
double currentDiff = Math.Abs(station.RealTimeCurrent - station.Current) / station.Current;
if (currentDiff > 0.2)
{
Diagnosis.Log($"充电桩 {station.Name} 电流异常: " +
$"额定{station.Current}A, 实时{station.RealTimeCurrent}A",
"ChargeStation", true);
}
}
}
```
### 3. 充电开始时设置实时数据
```csharp
/// <summary>
/// 开始充电
/// </summary>
public void StartCharging(string stationId, int carId)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
// 更新状态
station.Status = ChargeStationStatus.Charging;
// 初始化实时数据(初始值约为额定值的90%)
station.RealTimeVoltage = station.Voltage * 0.9;
station.RealTimeCurrent = station.Current * 0.9;
dataService.UpdateStation(station, out _);
Diagnosis.Log($"车辆 {carId} 开始充电: " +
$"充电桩 {station.Name}, " +
$"实时电压 {station.RealTimeVoltage:F1}V, " +
$"实时电流 {station.RealTimeCurrent:F1}A",
"ChargeStation", true);
}
}
```
### 4. 充电结束时清零实时数据
```csharp
/// <summary>
/// 停止充电
/// </summary>
public void StopCharging(string stationId, int carId)
{
var dataService = ChargeStationDataService.Instance;
var station = dataService.GetStationById(stationId);
if (station != null)
{
// 更新状态
station.Status = ChargeStationStatus.Idle;
// 清零实时数据
station.RealTimeVoltage = 0.0;
station.RealTimeCurrent = 0.0;
dataService.UpdateStation(station, out _);
Diagnosis.Log($"车辆 {carId} 充电完成: 充电桩 {station.Name}",
"ChargeStation", true);
}
}
```
### 5. 从PLC读取实时数据
```csharp
/// <summary>
/// 从PLC读取充电桩实时数据
/// </summary>
public void ReadRealTimeDataFromPLC()
{
var dataService = ChargeStationDataService.Instance;
var stations = dataService.GetAllStations()
.Where(s => s.Status == ChargeStationStatus.Charging)
.ToList();
foreach (var station in stations)
{
try
{
// 从PLC读取实时电压和电流
// 这里需要根据实际PLC通信协议实现
double voltage = ReadVoltageFromPLC(station.IpAddress, station.Port);
double current = ReadCurrentFromPLC(station.IpAddress, station.Port);
// 更新实时数据
station.RealTimeVoltage = voltage;
station.RealTimeCurrent = current;
dataService.UpdateStation(station, out _);
}
catch (Exception ex)
{
Diagnosis.Log($"读取充电桩 {station.Name} 实时数据失败: {ex.Message}",
"ChargeStation", true);
}
}
}
// 这些方法需要根据实际PLC协议实现
private double ReadVoltageFromPLC(string ip, int port)
{
// TODO: 实现PLC通信读取电压
return 0.0;
}
private double ReadCurrentFromPLC(string ip, int port)
{
// TODO: 实现PLC通信读取电流
return 0.0;
}
```
---
## 🔄 数据更新流程
### 完整充电流程
```
1. 车辆到达充电站
└─> 分配空闲充电桩
└─> 状态: Idle → Reserved
2. 开始充电
└─> 状态: Reserved → Charging
└─> 设置实时数据初始值
├─> RealTimeVoltage = Voltage * 0.9
└─> RealTimeCurrent = Current * 0.9
3. 充电中(定时更新)
└─> 每3-5秒从PLC读取实时数据
├─> 更新 RealTimeVoltage
├─> 更新 RealTimeCurrent
└─> 检查异常并告警
4. 充电完成
└─> 状态: Charging → Idle
└─> 清零实时数据
├─> RealTimeVoltage = 0.0
└─> RealTimeCurrent = 0.0
```
---
## 📋 JSON数据格式
保存到文件的数据包含实时字段:
```json
{
"StationId": "CS20240115123456",
"Name": "1号充电桩",
"Type": 0,
"IpAddress": "192.168.1.100",
"Port": 502,
"Voltage": 220.0,
"Current": 32.0,
"RealTimeVoltage": 215.5,
"RealTimeCurrent": 28.3,
"Status": 1,
"Enabled": true,
"SiteId": 1001,
"Remarks": "南区1号充电桩",
"CreatedTime": "2024-01-15T12:34:56",
"ModifiedTime": "2024-01-15T14:20:30"
}
```
---
## ✅ 使用检查清单
- [x] 数据模型添加实时电压和电流字段
- [x] 列表移除功率列
- [x] 列表添加实时电压和实时电流列
- [x] 界面移除新增按钮
- [x] 按钮重新排列
- [x] 保留设置电压和电流功能
- [x] 列表正确显示实时数据
- [x] 无编译错误
---
## 📝 总结
### ✅ 完成的功能
1. **数据模型** - 添加实时电压和电流字段
2. **列表显示** - 移除功率列,添加实时数据列
3. **界面优化** - 移除新增按钮,重新排列其他按钮
4. **功能保留** - 设置电压和电流功能完全保留
### 💡 后续集成建议
1. **与PLC通信集成**
- 实现从PLC读取实时电压和电流
- 定时更新实时数据(建议3-5秒)
2. **异常监控**
- 实时监控电压电流偏差
- 超过阈值时触发告警
3. **数据统计**
- 记录充电过程的电压电流曲线
- 分析充电效率和异常情况
4. **可视化展示**
- 实时数据图表显示
- 历史数据趋势分析
**现在您可以在充电桩管理界面中查看实时电压和电流数据了!** 🎉
@@ -0,0 +1,309 @@
# 充电桩报文自动更新功能说明
## 功能概述
系统现已支持发送和接收充电桩UDP报文,**分别解析后合并更新**充电桩管理列表中的实时数据。
## 工作流程
```
发送方向: 应用程序 → 发送报文 → CommunicationMessageService(存储+解析发送数据) → 更新设定值
接收方向: 充电桩设备 → UDP报文(40001端口) → ChargeUdpService → CommunicationMessageService(存储+解析接收数据) → 更新实时数据
合并结果: 发送数据 + 接收数据 → 数据服务 → 管理界面自动刷新
```
## 核心组件
### 1. CommunicationMessageService(通讯报文服务)
**文件位置**: `Charge/CommunicationMessageService.cs`
**主要功能**:
- 存储所有发送和接收的报文(最多保留100条)
- **分开解析发送和接收的报文数据**
- 根据IP地址匹配对应的充电桩并更新数据
- 提供报文查询和筛选功能
**发送报文解析的数据**:
- ✅ 充电指令(启动/停止)
- ✅ 设定电压(V
- ✅ 设定电流(A
- ✅ 发送时间
**接收报文解析的数据**:
- ✅ 通讯状态(正常/错误)
- ✅ 充电指令状态(启动/停止)
- ✅ 机构状态(伸出/缩回/伸出中/缩回中/故障)
- ✅ 实时电压(V
- ✅ 实时电流(A
- ✅ 电量百分比(%
- ✅ 报警状态和级别
- ✅ 充电桩状态(空闲/充电中/故障/离线)
- ✅ 当前充电车辆编号
- ✅ 接收时间
### 2. ChargeUdpServiceUDP监听服务)
**文件位置**: `Charge/ChargeUdpService.cs`
**监听端口**: 40001
**工作流程**:
1. 接收UDP报文
2. 调用 `CommunicationMessageService.AddReceiveMessage()` 记录报文
3. `CommunicationMessageService` 自动解析接收报文并更新充电桩数据
4. 保持原有任务处理逻辑的兼容性
### 3. 发送报文处理
**发送位置**:
- `ChargeStationType/MuXingChargeStation.cs`
- `ChargeStationType/FLChargeStation.cs`
- `ChargeStationType/PCBChargeStation.cs`
**工作流程**:
1. 发送UDP报文到充电桩
2. 调用 `CommunicationMessageService.AddSendMessage()` 记录报文
3. `CommunicationMessageService` 自动解析发送报文并更新充电桩设定值
### 4. ChargeStationDataService(数据服务)
**新增方法**: `GetStationByIp(string ipAddress)`
**功能**: 根据IP地址快速查找对应的充电桩记录
### 5. ChargeStationManagementForm(管理界面)
**新增功能**: 自动刷新
**刷新间隔**: 2秒
**特点**:
- 自动更新列表显示
- 不影响用户的编辑操作
- 窗体关闭时自动停止刷新
## 报文格式说明
### 当前支持的报文格式
报文采用逗号分隔的字节数组格式,例如:
```
1,2,3,4,5,...,28,29,30
```
### 发送报文字节位置定义(示例)
| 字节位置 | 数据内容 | 说明 |
|---------|---------|------|
| 5 | 充电指令 | 0=停止, 1=启动 |
| 6-7 | 设定电压 | 高低字节,单位0.1V |
| 8-9 | 设定电流 | 高低字节,单位0.1A |
### 接收报文字节位置定义(示例)
| 字节位置 | 数据内容 | 说明 |
|---------|---------|------|
| 10 | 充电指令状态 | 0=停止, 1=启动 |
| 11 | 机构状态 | 0=未知, 1=伸出, 2=缩回, 3=伸出中, 4=缩回中, 5=故障 |
| 12-13 | 实时电压 | 高低字节,单位0.1V |
| 14-15 | 实时电流 | 高低字节,单位0.1A |
| 16 | 电量百分比 | 0-100 |
| 20 | 报警级别 | 0=无, 1-2=低, 3-5=中, 6-8=高, 9+=严重 |
| 25 | 充电桩状态 | 0=空闲, 1=充电中, 2=故障, 3=离线 |
| 26-29 | 车辆编号 | 4字节整数 |
| 28 | 通讯状态 | 1=正常, 其他=错误 |
**⚠️ 注意**: 以上字节位置为示例,需要根据实际通讯协议进行调整。
## 如何调整报文解析规则
打开 `CommunicationMessageService.cs` 文件,分别修改发送和接收报文的解析方法:
### 调整发送报文解析
修改 `ParseSendRawData` 方法中的字节位置:
```csharp
private ParsedSendData ParseSendRawData(string rawData)
{
// ... 字节数组转换代码 ...
var parsed = new ParsedSendData
{
// 根据实际协议修改字节位置
ChargeCommand = bytes.Length > 5 ? bytes[5] : (byte)0,
SetVoltage = bytes.Length > 7 ? (bytes[6] << 8 | bytes[7]) / 10.0 : 0,
SetCurrent = bytes.Length > 9 ? (bytes[8] << 8 | bytes[9]) / 10.0 : 0,
SendTime = DateTime.Now
};
return parsed;
}
```
### 调整接收报文解析
修改 `ParseReceiveRawData` 方法中的字节位置:
```csharp
private ParsedReceiveData ParseReceiveRawData(string rawData)
{
// ... 字节数组转换代码 ...
var parsed = new ParsedReceiveData
{
// 根据实际协议修改字节位置
CommStatus = bytes[28] == 1 ? CommunicationStatus.Normal : CommunicationStatus.Error,
ChargeCommandStatus = bytes[10] == 1 ? ChargeCommandStatus.Started : ChargeCommandStatus.Stopped,
// ... 其他字段 ...
ReceiveTime = DateTime.Now
};
return parsed;
}
```
## 使用示例
### 1. 启动UDP监听
```csharp
// 在程序启动时创建UDP服务
var udpService = new ChargeUdpService();
```
### 2. 添加充电桩
在充电桩管理界面中添加充电桩,确保IP地址与实际设备一致:
```
充电桩编号: 1
名称: 1号充电桩
IP地址: 192.168.1.101 ← 必须与设备IP一致
端口: 502
```
### 3. 自动更新
**发送报文时**
1. 应用程序发送充电指令到充电桩
2. 系统记录发送报文
3. 解析发送报文数据(设定电压、电流等)
4. 根据IP地址匹配充电桩
5. 更新充电桩的设定值
**接收报文时**
1. 系统自动接收UDP报文(端口40001)
2. 记录接收报文
3. 解析接收报文数据(实时状态、电压、电流等)
4. 根据IP地址匹配充电桩
5. 更新充电桩的实时数据
**界面显示**
- 管理界面每2秒自动刷新显示
- 同时显示设定值(来自发送报文)和实时值(来自接收报文)
## 调试信息
系统会在日志中输出以下信息:
```
[UDP返回报文信息] ChargeStation ADD:[1,2,3,4,...]
[ChargeStation] 更新充电桩成功: [1] 1号充电桩 (192.168.1.101:502) - Charging
```
**查看报文记录**
- 打开通讯监控界面可以查看所有发送和接收的报文
- 报文按时间倒序排列(最新的在最前面)
- 最多保留100条报文记录
## 常见问题
### Q1: 报文接收了但数据没更新?
**检查项**:
1. 充电桩的IP地址是否在管理列表中
2. 报文格式是否正确(至少30字节)
3. 查看日志中是否有解析错误信息
### Q2: 如何修改刷新间隔?
`ChargeStationManagementForm.cs``InitializeAutoRefresh` 方法中修改:
```csharp
autoRefreshTimer.Interval = 2000; // 改为你需要的毫秒数
```
### Q3: 如何关闭自动刷新?
```csharp
// 在InitializeAutoRefresh方法中注释掉这行
// autoRefreshTimer.Start();
```
## 扩展功能
### 添加新的解析字段
**发送报文新字段**
1.`ParsedSendData` 类中添加新属性
2.`ParseSendRawData` 方法中解析新字段
3.`UpdateStationFromSendData` 方法中更新到充电桩对象
**接收报文新字段**
1.`ParsedReceiveData` 类中添加新属性
2.`ParseReceiveRawData` 方法中解析新字段
3.`UpdateStationFromReceiveData` 方法中更新到充电桩对象
### 支持其他通讯协议
`CommunicationMessageService.cs` 中可以根据端口号或其他特征判断协议类型:
```csharp
public void AddReceiveMessage(string ipAddress, int port, string rawData, string stationId = null)
{
// ... 添加报文记录 ...
// 根据端口判断协议类型
if (port == 40001)
{
ParseReceiveDataAndUpdateStation(ipAddress, rawData); // UDP协议
}
else if (port == 502)
{
ParseModbusReceiveAndUpdate(ipAddress, rawData); // Modbus协议
}
}
public void AddSendMessage(string ipAddress, int port, string rawData, string stationId = null)
{
// ... 添加报文记录 ...
// 根据端口判断协议类型
if (port == 40001)
{
ParseSendDataAndUpdateStation(ipAddress, rawData); // UDP协议
}
else if (port == 502)
{
ParseModbusSendAndUpdate(ipAddress, rawData); // Modbus协议
}
}
```
## 技术特点
**实时性**: UDP报文接收后立即解析更新
**自动化**: 无需手动刷新,数据自动同步
**分离解析**: 发送和接收报文分开解析,数据更准确
**合并更新**: 自动合并发送和接收数据到充电桩管理界面
**可扩展**: 支持自定义报文格式和解析规则
**兼容性**: 保留原有任务处理逻辑
**稳定性**: 异常处理完善,不影响系统运行
## 版本历史
- **v1.1** (2026-01-18): 发送和接收报文分开解析,合并更新到充电桩管理界面
- **v1.0** (2026-01-18): 初始版本,支持UDP报文自动解析和更新
@@ -0,0 +1,265 @@
# 充电策略配置说明
## 功能概述
充电策略配置界面用于管理和配置充电系统的各项参数,包括 SOC 阈值、时间参数、任务参数和开关参数。配置保存在 JSON 文件中,系统启动时自动加载。
## 打开配置界面
在**充电桩管理界面**点击 **"策略配置"** 按钮(绿色按钮)即可打开配置界面。
## 配置参数说明
### 1. SOC 参数(电量百分比)
| 参数名称 | 默认值 | 说明 | 取值范围 |
|---------|-------|------|---------|
| 必充电量 | 20% | 低于此电量必须充电 | 0-100% |
| 空闲充电电量 | 90% | 车辆空闲时开始充电的电量阈值 | 0-100% |
| 任务可用电量 | 60% | 可以执行任务的最低电量 | 0-100% |
| 满电电量 | 90% | 充电目标电量 | 0-100% |
| 允许中断电量 | 45% | 允许中断充电任务的最低电量 | 0-100% |
**逻辑关系**
- 必充电量 < 空闲充电电量
- 任务可用电量 > 必充电量
- 满电电量 ≥ 空闲充电电量
- 允许中断电量 > 必充电量
### 2. 时间参数
| 参数名称 | 默认值 | 说明 | 单位 |
|---------|-------|------|-----|
| 空闲充电时间 | 30 | 车辆空闲多久后开始充电 | 秒 (sec) |
| 空闲时间 | 5 | 判断车辆空闲的时间阈值 | 秒 (sec) |
| 必充时间 | 60 | 必须充电的持续时间 | 秒 (sec) |
| 补电时间 | 5 | 补电操作的持续时间 | 分钟 (min) |
### 3. 任务参数
| 参数名称 | 默认值 | 说明 |
|---------|-------|------|
| 允许空闲车充电的最小任务数 | 0 | 当任务数量大于此值时,允许空闲车辆充电 |
### 4. 开关参数
| 参数名称 | 默认值 | 说明 |
|---------|-------|------|
| 允许中断充电任务 | 否 | 是否允许中断正在进行的充电任务 |
| 优先使用低电量车辆充电 | 是 | 优先选择电量较低的车辆进行充电 |
| 启用充电错误检测 | 否 | 是否启用充电过程中的错误检测 |
| 使用充电站点筛选 | 否 | 是否根据站点筛选充电桩 |
## 操作说明
### 保存配置
1. 修改所需参数
2. 点击 **"保存"** 按钮
3. 系统会验证参数的有效性
4. 验证通过后保存到配置文件
配置文件位置:`Config/ChargeStrategyConfig.json`
### 应用配置
点击 **"应用"** 按钮可以保存配置但不关闭窗口,方便继续调整参数。
### 恢复默认配置
1. 点击 **"恢复默认"** 按钮
2. 确认恢复操作
3. 所有参数恢复为默认值
4. **注意**:恢复后需要点击"保存"才会生效
### 取消修改
点击 **"取消"** 按钮关闭窗口,不保存任何修改。
## 配置文件格式
配置以 JSON 格式保存,示例:
```json
{
"MustChargeSoc": 20.0,
"IdleChargeSoc": 90.0,
"TaskAvailableSoc": 60.0,
"FullChargeSoc": 90.0,
"AllowInterruptSoc": 45.0,
"IdleChargeSeconds": 30.0,
"IdleSeconds": 5.0,
"MustChargeSeconds": 60.0,
"TopUpMinutes": 5.0,
"MinAllowFreeCarToChargeTaskCnt": 0,
"AllowInterruptTask": false,
"UseLowerSocForCharge": true,
"EnableErrorChargeDetection": false,
"UseChargeSiteFilter": false
}
```
## 参数验证规则
系统会在保存时自动验证配置的有效性:
### SOC 参数验证
- ✅ 所有 SOC 值必须在 0-100 之间
- ✅ 必充电量 < 空闲充电电量
- ✅ 任务可用电量 > 必充电量
- ✅ 满电电量 ≥ 空闲充电电量
- ✅ 允许中断电量 > 必充电量
### 时间参数验证
- ✅ 所有时间值必须 ≥ 0
### 任务参数验证
- ✅ 最小任务数必须 ≥ 0
## 使用场景示例
### 场景 1:紧急任务模式
适用于任务紧急,需要快速周转车辆的情况。
```
必充电量: 15%
空闲充电电量: 80%
任务可用电量: 50%
满电电量: 85%
允许中断充电任务: 是
```
### 场景 2:节能模式
适用于任务不紧急,优先保证电池寿命的情况。
```
必充电量: 25%
空闲充电电量: 95%
任务可用电量: 70%
满电电量: 95%
允许中断充电任务: 否
```
### 场景 3:平衡模式(默认)
平衡任务效率和电池寿命。
```
必充电量: 20%
空闲充电电量: 90%
任务可用电量: 60%
满电电量: 90%
允许中断充电任务: 否
```
## 配置生效时机
- **立即生效**:保存配置后立即生效
- **自动加载**:系统启动时自动加载配置
- **实时更新**:充电逻辑会实时读取最新配置
## 常见问题
### Q1: 修改配置后没有生效?
**检查项**
1. 确认已点击"保存"按钮
2. 检查状态栏是否显示"配置保存成功"
3. 查看配置文件是否已更新
### Q2: 配置文件丢失怎么办?
系统会自动创建默认配置文件,无需担心。
### Q3: 如何备份配置?
配置文件位于 `Config/ChargeStrategyConfig.json`,直接复制此文件即可备份。
### Q4: 参数验证失败怎么办?
根据错误提示调整参数,确保满足所有验证规则。
### Q5: 可以手动编辑配置文件吗?
可以,但建议使用配置界面,因为界面会自动验证参数有效性。
## 技术细节
### 配置服务(单例模式)
```csharp
var configService = ChargeStrategyConfigService.Instance;
var config = configService.LoadConfig();
configService.SaveConfig(config);
```
### 配置模型
```csharp
public class ChargeStrategyConfig
{
// SOC 参数
public double MustChargeSoc { get; set; }
public double IdleChargeSoc { get; set; }
// ... 其他参数
// 验证方法
public bool Validate(out string errorMessage);
}
```
### 配置文件路径
- **Windows**: `应用程序目录\Config\ChargeStrategyConfig.json`
- **自动创建**: 首次运行时自动创建配置目录和默认配置文件
## 界面布局
```
┌─────────────────────────────────────────────────────────┐
│ 充电策略配置 │
├─────────────────────────────────────────────────────────┤
│ ┌─ SOC 参数 ─────────────────────────────────────────┐ │
│ │ 必充电量: [20.0] % 空闲充电电量: [90.0] % │ │
│ │ 任务可用电量: [60.0] % 满电电量: [90.0] % │ │
│ │ 允许中断电量: [45.0] % │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 时间参数 ─────────────────────────────────────────┐ │
│ │ 空闲充电时间: [30.0] 秒 空闲时间: [5.0] 秒 │ │
│ │ 必充时间: [60.0] 秒 补电时间: [5.0] 分钟 │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 任务参数 ─────────────────────────────────────────┐ │
│ │ 允许空闲车充电的最小任务数: [0] │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─ 开关参数 ─────────────────────────────────────────┐ │
│ │ ☐ 允许中断充电任务 ☑ 优先使用低电量车辆充电 │ │
│ │ ☐ 启用充电错误检测 ☐ 使用充电站点筛选 │ │
│ └───────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 就绪... [恢复默认] [保存] [应用] [取消] │
└─────────────────────────────────────────────────────────┘
```
## 注意事项
⚠️ **参数调整建议**
1. 不建议频繁修改配置
2. 修改前建议备份当前配置
3. 修改后观察系统运行情况
4. 根据实际情况逐步调整参数
⚠️ **安全提示**
1. 必充电量不宜设置过低(建议 ≥ 15%)
2. 满电电量不宜设置过高(建议 ≤ 95%)
3. 允许中断任务需谨慎开启
## 版本历史
- **v1.0** (2026-01-25): 初始版本,支持所有充电策略参数配置
---
**提示**:配置界面提供了完整的参数验证和默认值恢复功能,建议通过界面进行配置管理。
@@ -0,0 +1,527 @@
# 🔌 充电桩管理系统 - 完整文件清单
## 📁 已创建的文件
### 核心文件(必需)
| 文件名 | 类型 | 说明 | 行数 |
|--------|------|------|------|
| **ChargeStation.cs** | 数据模型 | 充电桩实体类,包含所有属性和验证逻辑 | ~150 |
| **ChargeStationDataService.cs** | 数据服务 | 单例模式数据管理类,负责增删改查和持久化 | ~300 |
| **ChargeStationManagementForm.cs** | UI主类 | 管理窗口的业务逻辑和事件处理 | ~350 |
| **ChargeStationManagementForm.Designer.cs** | UI设计 | 窗口控件的初始化和布局代码 | ~550 |
### 辅助文件(可选但推荐)
| 文件名 | 类型 | 说明 | 行数 |
|--------|------|------|------|
| **ChargeStationHelper.cs** | 工具类 | 提供静态辅助方法,简化调用 | ~350 |
| **ChargeStationManagementExample.cs** | 示例代码 | 10个使用示例,包含完整的调用代码 | ~400 |
### 文档文件
| 文件名 | 类型 | 说明 |
|--------|------|------|
| **README_ChargeStationManagement.md** | 完整文档 | 详细的功能说明、API文档和使用指南 |
| **QUICKSTART.md** | 快速开始 | 5分钟快速上手指南,包含集成步骤 |
| **INTERFACE_LAYOUT.txt** | 界面说明 | ASCII艺术格式的界面布局和操作说明 |
| **完整文件清单.md** | 本文件 | 所有文件的清单和使用说明 |
---
## 🎯 文件功能详解
### 1. ChargeStation.cs - 充电桩数据模型
**功能**
- 定义充电桩的所有属性(编号、名称、IP、端口、电压、电流等)
- 提供数据验证方法 `IsValid()`
- 自动计算功率 `Power`
- 自动生成唯一编号 `GenerateStationId()`
**关键属性**
```csharp
public string StationId { get; set; } // 唯一编号
public string Name { get; set; } // 名称
public string IpAddress { get; set; } // IP地址
public int Port { get; set; } // 端口
public double Voltage { get; set; } // 电压(V)
public double Current { get; set; } // 电流(A)
public ChargeStationStatus Status { get; set; } // 状态
public bool Enabled { get; set; } // 是否启用
public int? SiteId { get; set; } // 站点ID
public double Power => Voltage * Current; // 功率(W)
```
**状态枚举**
```csharp
public enum ChargeStationStatus {
Idle = 0, // 空闲
Charging = 1, // 充电中
Fault = 2, // 故障
Offline = 3, // 离线
Maintenance = 4, // 维护中
Reserved = 5 // 预约中
}
```
---
### 2. ChargeStationDataService.cs - 数据服务
**功能**
- 单例模式,全局唯一实例
- 数据持久化(JSON格式)
- 线程安全(使用锁机制)
- CRUD操作(增删改查)
**核心方法**
```csharp
// 单例获取
ChargeStationDataService.Instance
// 查询
List<ChargeStation> GetAllStations()
ChargeStation GetStationById(string stationId)
List<ChargeStation> GetIdleStations()
int GetChargingCount()
// 添加
bool AddStation(ChargeStation station, out string errorMessage)
// 更新
bool UpdateStation(ChargeStation station, out string errorMessage)
bool UpdateStationStatus(string stationId, ChargeStationStatus status)
// 删除
bool DeleteStation(string stationId, out string errorMessage)
// 刷新
void Reload()
```
**数据存储位置**
```
项目根目录/Data/ChargeStations.json
```
---
### 3. ChargeStationManagementForm.cs - 管理窗口
**功能**
- 充电桩列表显示(DataGridView
- 实时搜索
- 添加/编辑/删除充电桩
- 数据导出(JSON/CSV
- 统计信息显示
- 状态颜色标识
**主要方法**
```csharp
private void LoadStations() // 加载数据到列表
private void UpdateStatistics() // 更新统计信息
private void btnAdd_Click() // 新增按钮
private void btnSave_Click() // 保存按钮
private void btnDelete_Click() // 删除按钮
private void btnRefresh_Click() // 刷新按钮
private void btnExport_Click() // 导出按钮
private void dgvStations_CellDoubleClick() // 双击编辑
private void txtSearch_TextChanged() // 搜索
```
**界面布局**
- 左侧:充电桩列表 + 搜索 + 统计
- 右侧:编辑区 + 操作按钮
- 尺寸:1200×700(可调整,最小1000×600
---
### 4. ChargeStationManagementForm.Designer.cs - UI设计文件
**功能**
- 自动生成的设计器代码
- 包含所有控件的初始化
- 不建议手动修改
**主要控件**
```csharp
SplitContainer splitContainer // 分割容器
DataGridView dgvStations // 数据表格
TextBox txtSearch // 搜索框
TextBox txtName, txtIpAddress // 文本框
NumericUpDown numPort, numVoltage // 数字输入框
ComboBox cmbStatus // 下拉框
CheckBox chkEnabled // 复选框
Button btnAdd, btnSave, btnDelete // 按钮
Label lblStatistics, lblPower // 标签
```
---
### 5. ChargeStationHelper.cs - 辅助工具类
**功能**
- 提供静态辅助方法
- 简化常用操作
- 封装复杂逻辑
**常用方法**
```csharp
// 打开管理窗口
ChargeStationHelper.OpenManagementWindow()
// 获取充电桩
ChargeStation station = ChargeStationHelper.GetStationBySiteId(1001)
ChargeStation station = ChargeStationHelper.GetStationByIp("192.168.1.100")
// 检查可用性
bool available = ChargeStationHelper.IsSiteHasAvailableChargeStation(1001)
// 充电控制
bool success = ChargeStationHelper.StartCharging("CS123456", carId)
bool success = ChargeStationHelper.StopCharging("CS123456", carId)
// 故障标记
bool success = ChargeStationHelper.MarkAsFault("CS123456", "通信超时")
// 获取状态摘要
string summary = ChargeStationHelper.GetStatusSummary()
// 输出: "总数:10 | 空闲:6 | 充电中:3 | 故障:1 | 离线:0"
// 查找最近的空闲充电桩
ChargeStation station = ChargeStationHelper.FindNearestIdleStation(currentSiteId)
// 快速创建充电桩
bool success = ChargeStationHelper.QuickAddStation("1号充电桩", "192.168.1.100", 1001)
// 显示选择对话框
ChargeStation selected = ChargeStationHelper.ShowStationSelectionDialog(ChargeStationStatus.Idle)
// 批量更新在线状态
int updatedCount = ChargeStationHelper.UpdateOnlineStatus(timeout: 3000)
```
---
### 6. ChargeStationManagementExample.cs - 示例代码
**功能**
- 提供10个完整的使用示例
- 每个方法都带有 `[MethodMember]` 属性,可在系统中直接调用
**示例列表**
| 方法名 | 说明 |
|--------|------|
| `OpenManagementForm()` | 打开充电桩管理窗口 |
| `InitializeTestData()` | 初始化4个测试充电桩 |
| `ShowIdleStations()` | 显示所有空闲充电桩 |
| `ShowChargeStationStatistics()` | 显示统计信息 |
| `AssignChargeStationToCar()` | 为车辆分配充电桩 |
| `StartCharging()` | 开始充电 |
| `StopCharging()` | 结束充电 |
| `CheckChargeStationOnlineStatus()` | 检查在线状态 |
| `ExportChargeStationData()` | 导出数据 |
| `ClearAllChargeStationData()` | 清空所有数据 |
---
## 📖 使用指南
### 方式1:快速开始(推荐新手)
1. **阅读快速开始文档**
```
打开: QUICKSTART.md
```
2. **在主窗口添加菜单项**
```csharp
var menuItem = new ToolStripMenuItem("充电桩管理");
menuItem.Click += (s, e) => {
ChargeStationHelper.OpenManagementWindow();
};
```
3. **初始化测试数据**
```csharp
ChargeStationManagementExample.InitializeTestData();
```
4. **打开管理窗口测试**
- 点击菜单项
- 查看测试数据
- 尝试添加/编辑/删除
### 方式2:集成到现有代码(推荐高级用户)
1. **阅读完整文档**
```
打开: README_ChargeStationManagement.md
```
2. **在充电任务中集成**
```csharp
// 在 AbstractChargeMission.cs 中
using StandardScene.Charge;
// 选择充电站点时
var dataService = ChargeStationDataService.Instance;
var idleStations = dataService.GetIdleStations();
// 到达充电站时
ChargeStationHelper.StartCharging(stationId, carId);
// 离开充电站时
ChargeStationHelper.StopCharging(stationId, carId);
```
3. **添加实时监控**
```csharp
// 在主窗口添加定时器
private Timer statusTimer = new Timer { Interval = 3000 };
statusTimer.Tick += (s, e) => {
lblStatus.Text = ChargeStationHelper.GetStatusSummary();
};
statusTimer.Start();
```
### 方式3:参考示例代码(推荐学习)
1. **查看示例代码**
```
打开: ChargeStationManagementExample.cs
```
2. **运行示例方法**
```csharp
// 直接调用示例方法
ChargeStationManagementExample.ShowIdleStations();
ChargeStationManagementExample.ShowChargeStationStatistics();
```
3. **根据需求修改**
- 复制示例代码
- 根据实际需求调整
- 集成到项目中
---
## 🔧 配置说明
### 数据文件配置
**位置**
```
项目根目录/Data/ChargeStations.json
```
**格式**
```json
[
{
"StationId": "CS20240115123456",
"Name": "1号充电桩",
"IpAddress": "192.168.1.100",
"Port": 502,
"Voltage": 220.0,
"Current": 32.0,
"Status": 0,
"Enabled": true,
"SiteId": 1001,
"Remarks": "南区1号充电桩",
"CreatedTime": "2024-01-15T12:34:56",
"ModifiedTime": "2024-01-15T14:20:30"
}
]
```
### 权限要求
- `Data` 文件夹需要**读写权限**
- 如果保存失败,检查文件夹权限
### 性能配置
- 数据量 < 100个充电桩:无需优化
- 数据量 > 100个充电桩:考虑分页显示
- 搜索性能:实时搜索,无需优化
---
## 🎨 界面定制
### 修改窗口大小
在 `ChargeStationManagementForm.Designer.cs` 中:
```csharp
this.Size = new Size(1400, 800); // 修改为你需要的尺寸
```
### 修改按钮颜色
```csharp
btnAdd.BackColor = Color.LightGreen;
btnSave.BackColor = Color.LightBlue;
btnDelete.BackColor = Color.LightCoral;
```
### 修改字体
```csharp
this.Font = new Font("微软雅黑", 10F);
```
---
## 🐛 故障排除
### 问题1:窗口打不开
**原因**:命名空间引用错误
**解决**
```csharp
using StandardScene.Charge;
```
### 问题2:数据保存失败
**原因**:文件夹权限不足
**解决**
1. 右键 `Data` 文件夹
2. 属性 → 安全
3. 确保当前用户有"写入"权限
### 问题3:找不到数据
**原因**:首次运行未初始化
**解决**
```csharp
ChargeStationManagementExample.InitializeTestData();
```
### 问题4:编译错误
**原因**:缺少依赖项
**解决**
- 确保安装 `Newtonsoft.Json` NuGet 包
- 检查项目引用
---
## 📊 系统要求
### 软件要求
- .NET Framework 4.5 或更高版本
- Windows Forms
- Newtonsoft.JsonNuGet
### 硬件要求
- 内存:数据量小,几乎无影响
- 磁盘:每个充电桩约 1KB 数据
- CPUUI操作,几乎无影响
### 兼容性
- Windows 7/8/10/11
- 与现有 AGV 系统完全兼容
- 不影响现有功能
---
## 🚀 下一步计划
### 已完成功能 ✅
- [x] 充电桩数据模型
- [x] 数据持久化(JSON
- [x] 可视化管理界面
- [x] 增删改查功能
- [x] 搜索和过滤
- [x] 数据导出
- [x] 辅助工具类
- [x] 完整文档和示例
### 可扩展功能 💡
- [ ] 充电桩实时监控(通信状态)
- [ ] 充电历史记录
- [ ] 充电曲线图表
- [ ] 充电计费管理
- [ ] 充电桩分组管理
- [ ] 权限控制(不同用户不同权限)
- [ ] 远程控制(启动/停止充电)
- [ ] 告警推送(故障/离线)
- [ ] 数据分析(充电效率统计)
- [ ] 与现有 PLC 系统集成
---
## 📞 技术支持
### 文档位置
- **完整文档**: `README_ChargeStationManagement.md`
- **快速开始**: `QUICKSTART.md`
- **界面说明**: `INTERFACE_LAYOUT.txt`
- **本文件**: `完整文件清单.md`
### 示例代码
- **工具类**: `ChargeStationHelper.cs`
- **示例代码**: `ChargeStationManagementExample.cs`
### 日志调试
```csharp
// 查看充电桩相关日志
// 日志标签: "ChargeStation"
```
---
## ✅ 检查清单
在部署到生产环境前,请确认:
- [ ] 所有文件都已添加到项目
- [ ] `Newtonsoft.Json` NuGet 包已安装
- [ ] `Data` 文件夹有读写权限
- [ ] 已在主窗口添加菜单项或按钮
- [ ] 已初始化测试数据并测试
- [ ] 管理窗口可以正常打开
- [ ] 增删改查功能正常
- [ ] 数据保存和加载正常
- [ ] 搜索功能正常
- [ ] 导出功能正常
- [ ] 已阅读完整文档
- [ ] 已测试与现有系统的集成
---
## 📝 版本信息
**当前版本**: v1.0.0
**发布日期**: 2024-01-15
**开发者**: MDCS System
**许可**: 内部使用
---
## 🎉 恭喜!
您已经获得了一个完整的充电桩管理系统!
**快速开始**
1. 打开 `QUICKSTART.md`
2. 按照步骤操作
3. 5分钟内即可开始使用
**需要帮助?**
- 查看文档
- 运行示例代码
- 检查日志输出
祝您使用愉快! 🚀
@@ -0,0 +1,320 @@
# 充电桩报文分离解析使用示例
## 概述
系统现在支持**发送报文**和**接收报文**分开解析,然后自动合并更新到充电桩管理界面。
## 数据流向图
```
┌─────────────────┐
│ 应用程序 │
└────────┬────────┘
│ 发送充电指令
┌─────────────────────────────────────────────┐
│ CommunicationMessageService │
│ ┌─────────────────────────────────────┐ │
│ │ AddSendMessage() │ │
│ │ ├─ 记录发送报文 │ │
│ │ └─ ParseSendDataAndUpdateStation() │ │
│ │ ├─ 解析设定电压 │ │
│ │ ├─ 解析设定电流 │ │
│ │ └─ 更新充电桩设定值 │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
┌────────────────┐
│ ChargeStation │ ◄─── 设定值已更新
│ SetVoltage │
│ SetCurrent │
└────────┬───────┘
│ 同时...
┌─────────────────▼───────────────────────────┐
│ ChargeUdpService (监听端口40001) │
│ ├─ 接收充电桩返回的UDP报文 │
│ └─ 调用 AddReceiveMessage() │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ CommunicationMessageService │
│ ┌─────────────────────────────────────┐ │
│ │ AddReceiveMessage() │ │
│ │ ├─ 记录接收报文 │ │
│ │ └─ ParseReceiveDataAndUpdateStation()│ │
│ │ ├─ 解析实时电压 │ │
│ │ ├─ 解析实时电流 │ │
│ │ ├─ 解析充电状态 │ │
│ │ ├─ 解析机构状态 │ │
│ │ └─ 更新充电桩实时数据 │ │
│ └─────────────────────────────────────┘ │
└─────────────────┬───────────────────────────┘
┌────────────────┐
│ ChargeStation │ ◄─── 实时值已更新
│ RealTimeVoltage│
│ RealTimeCurrent│
│ Status │
│ MechanismStatus│
└────────┬───────┘
┌─────────────────────────────┐
│ ChargeStationManagementForm │
│ (每2秒自动刷新) │
│ 显示: │
│ - 设定电压 vs 实时电压 │
│ - 设定电流 vs 实时电流 │
│ - 充电状态 │
│ - 机构状态 │
└─────────────────────────────┘
```
## 代码示例
### 1. 发送充电指令(自动解析发送报文)
```csharp
// 在充电桩类中发送充电指令
public void StartCharging(double voltage, double current)
{
// 构造发送报文
byte[] sendData = new byte[10];
sendData[5] = 1; // 充电指令:启动
sendData[6] = (byte)((int)(voltage * 10) >> 8); // 设定电压高字节
sendData[7] = (byte)((int)(voltage * 10) & 0xFF); // 设定电压低字节
sendData[8] = (byte)((int)(current * 10) >> 8); // 设定电流高字节
sendData[9] = (byte)((int)(current * 10) & 0xFF); // 设定电流低字节
// 发送UDP报文
udpClient.Send(sendData, sendData.Length, endPoint);
// 记录发送报文(自动解析并更新设定值)
var messageService = CommunicationMessageService.Instance;
messageService.AddSendMessage(
ipAddress: endPoint.Address.ToString(),
port: endPoint.Port,
rawData: string.Join(",", sendData),
stationId: this.StationId
);
// ✅ 此时充电桩的 SetVoltage 和 SetCurrent 已自动更新
}
```
### 2. 接收充电桩反馈(自动解析接收报文)
```csharp
// 在 ChargeUdpService 中接收报文
private static async void ListenerProcess()
{
var messageService = CommunicationMessageService.Instance;
using (UdpClient udpListener = new UdpClient(40001))
{
while (true)
{
var result = await udpListener.ReceiveAsync();
var remoteEndPoint = result.RemoteEndPoint;
var message = result.Buffer;
// 记录接收报文(自动解析并更新实时数据)
messageService.AddReceiveMessage(
ipAddress: remoteEndPoint.Address.ToString(),
port: 40001,
rawData: string.Join(",", message)
);
// ✅ 此时充电桩的实时数据已自动更新:
// - RealTimeVoltage(实时电压)
// - RealTimeCurrent(实时电流)
// - Status(充电状态)
// - MechanismStatus(机构状态)
// - BatteryLevel(电量百分比)
// - 等等...
}
}
}
```
### 3. 在充电桩管理界面查看合并后的数据
```csharp
// 在 ChargeStationManagementForm 中显示数据
private void LoadStations()
{
var stations = ChargeStationDataService.Instance.GetAllStations();
foreach (var station in stations)
{
// 显示设定值(来自发送报文解析)
Console.WriteLine($"设定电压: {station.SetVoltage}V");
Console.WriteLine($"设定电流: {station.SetElectricCurrent}A");
// 显示实时值(来自接收报文解析)
Console.WriteLine($"实时电压: {station.RealTimeVoltage}V");
Console.WriteLine($"实时电流: {station.RealTimeCurrent}A");
Console.WriteLine($"充电状态: {GetStatusText(station.Status)}");
Console.WriteLine($"机构状态: {GetMechanismStatusText(station.MechanismStatus)}");
Console.WriteLine($"电量: {station.BatteryLevel}%");
// 显示通讯时间
Console.WriteLine($"最后发送: {station.LastSendTime}");
Console.WriteLine($"最后接收: {station.LastReceiveTime}");
}
}
```
## 解析流程详解
### 发送报文解析流程
```
AddSendMessage()
ParseSendDataAndUpdateStation()
ParseSendRawData() ← 解析发送报文
├─ ChargeCommand (字节5)
├─ SetVoltage (字节6-7)
└─ SetCurrent (字节8-9)
UpdateStationFromSendData() ← 更新设定值
├─ station.SetVoltage = parsedData.SetVoltage
├─ station.SetElectricCurrent = parsedData.SetCurrent
├─ station.LastSendTime = parsedData.SendTime
└─ station.ChargeCommandStatus = ...
```
### 接收报文解析流程
```
AddReceiveMessage()
ParseReceiveDataAndUpdateStation()
ParseReceiveRawData() ← 解析接收报文
├─ CommStatus (字节28)
├─ ChargeCommandStatus (字节10)
├─ MechanismStatus (字节11)
├─ RealTimeVoltage (字节12-13)
├─ RealTimeCurrent (字节14-15)
├─ BatteryLevel (字节16)
├─ HasAlarm (字节20)
├─ Status (字节25)
└─ CurrentVehicleId (字节26-29)
UpdateStationFromReceiveData() ← 更新实时值
├─ station.RealTimeVoltage = parsedData.RealTimeVoltage
├─ station.RealTimeCurrent = parsedData.RealTimeCurrent
├─ station.Status = parsedData.Status
├─ station.MechanismStatus = parsedData.MechanismStatus
├─ station.BatteryLevel = parsedData.BatteryLevel
├─ station.LastReceiveTime = parsedData.ReceiveTime
└─ ...
```
## 数据对比示例
| 数据项 | 来源 | 更新时机 | 用途 |
|-------|------|---------|------|
| SetVoltage | 发送报文 | 发送充电指令时 | 显示设定的目标电压 |
| RealTimeVoltage | 接收报文 | 接收充电桩反馈时 | 显示当前实际电压 |
| SetElectricCurrent | 发送报文 | 发送充电指令时 | 显示设定的目标电流 |
| RealTimeCurrent | 接收报文 | 接收充电桩反馈时 | 显示当前实际电流 |
| ChargeCommandStatus | 发送+接收 | 发送指令时更新,接收反馈时确认 | 显示充电指令执行状态 |
| Status | 接收报文 | 接收充电桩反馈时 | 显示充电桩当前状态 |
| MechanismStatus | 接收报文 | 接收充电桩反馈时 | 显示机构伸缩状态 |
| BatteryLevel | 接收报文 | 接收充电桩反馈时 | 显示电池电量百分比 |
| LastSendTime | 发送报文 | 发送充电指令时 | 显示最后发送时间 |
| LastReceiveTime | 接收报文 | 接收充电桩反馈时 | 显示最后接收时间 |
## 优势
**数据分离**:发送和接收数据各自独立,不会互相覆盖
**完整记录**:同时保留设定值和实时值,便于对比分析
**自动合并**:系统自动将两种数据合并到同一个充电桩对象
**实时更新**:界面每2秒自动刷新,显示最新数据
**易于调试**:可以清楚看到发送的指令和接收的反馈
## 调试技巧
### 1. 查看报文记录
```csharp
var messageService = CommunicationMessageService.Instance;
// 查看所有报文
var allMessages = messageService.GetAllMessages();
// 查看某个IP的报文
var ipMessages = messageService.GetMessagesByIp("192.168.1.101");
// 查看某个充电桩的报文
var stationMessages = messageService.GetMessagesByStationId("1");
foreach (var msg in allMessages)
{
Console.WriteLine($"[{msg.Direction}] {msg.IpAddress}:{msg.Port}");
Console.WriteLine($"时间: {msg.Timestamp}");
Console.WriteLine($"数据: {msg.RawData}");
Console.WriteLine("---");
}
```
### 2. 对比设定值与实时值
```csharp
var station = ChargeStationDataService.Instance.GetStationByIp("192.168.1.101");
if (station != null)
{
// 电压对比
double voltageDiff = Math.Abs(station.SetVoltage - station.RealTimeVoltage);
Console.WriteLine($"电压偏差: {voltageDiff}V");
// 电流对比
double currentDiff = Math.Abs(station.SetElectricCurrent - station.RealTimeCurrent);
Console.WriteLine($"电流偏差: {currentDiff}A");
// 通讯延迟
if (station.LastSendTime != null && station.LastReceiveTime != null)
{
var delay = station.LastReceiveTime.Value - station.LastSendTime.Value;
Console.WriteLine($"通讯延迟: {delay.TotalMilliseconds}ms");
}
}
```
### 3. 监控解析错误
如果数据没有更新,检查以下几点:
1. **IP地址是否匹配**:确保充电桩的IP地址在管理列表中
2. **报文长度是否足够**:发送报文至少10字节,接收报文至少30字节
3. **字节位置是否正确**:根据实际协议调整字节位置
4. **数据类型是否正确**:检查高低字节顺序、单位换算等
## 注意事项
⚠️ **字节位置**:示例中的字节位置仅供参考,请根据实际通讯协议调整
⚠️ **报文格式**:确保发送和接收的报文格式与解析规则一致
⚠️ **异常处理**:解析失败不会影响报文记录,但数据不会更新
⚠️ **线程安全**`CommunicationMessageService` 使用单例模式,内部已做线程同步
## 总结
通过分离解析发送和接收报文,系统可以:
- 准确记录每次发送的指令参数
- 准确获取充电桩的实时反馈
- 自动合并两种数据到充电桩管理界面
- 便于对比分析和故障诊断
这种设计使得充电桩管理更加精确和可靠!
@@ -0,0 +1,26 @@
UDP测试数据
[2026/01/16-16:11:55.679] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F6 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 00 F6 EE
[2026/01/16-16:11:56.260] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F7 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 80 F5 EE
[2026/01/16-16:11:56.835] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F8 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 80 EE EE
[2026/01/16-16:11:57.394] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB F9 01 00 00 03 20 00 00 01 22 00 1E 00 05 4D FF FF FD EE 00 00 01 0E 00 00 00 00 00 00 ED EE
[2026/01/16-16:14:49.300] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 28 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 38 EE
[2026/01/16-16:14:49.843] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 29 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 84 3B EE
[2026/01/16-16:14:50.410] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 2A 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 44 3D EE
[2026/01/16-16:14:50.943] >PCBsendChargeSite: 3075 IP:192.168.100.72: BB 2B 00 00 00 03 20 00 00 01 22 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C4 3E EE
[2026/01/19-17:35:58.294] >IsSafe:False ChargeIP: 192.168.100.74: BB 74 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:35:58.885] >IsSafe:False ChargeIP: 192.168.100.74: BB 75 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:35:59.465] >IsSafe:False ChargeIP: 192.168.100.74: BB 76 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
[2026/01/19-17:36:00.020] >IsSafe:False ChargeIP: 192.168.100.74: BB 77 00 00 03 28 00 00 01 22 00 00 00 00 01 29 00 0B 00 00 00 00 00 00 00 00 00 00 02 00 00 EE
TCP 发送指令
BB 00 42 48 00 00 42 5C 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EE // 启动
BB 00 42 48 00 00 42 5C 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EE // 停止
TCP返回指令
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 EE //充电
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 EE //缩回
BB 00 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 EE //充电
@@ -0,0 +1,254 @@
# 脚本生成特性汇总表
> 本文档汇总了工程中所有的 `TemplateTrackCoderSettings`、`TemplateSiteCoderSettings` 和 `ProgramTrackCoderSettings` 特性配置。
---
## 关键参数说明
| 参数 | 说明 |
|------|------|
| **priority** | 优先级,数值越大则脚本生成顺序越靠前 |
| **useVerb** | 判断条件,为 true 时生成 templateString |
| **blockVerb** | 如果为 true,且 useVerb 通过,则低于此 priority 的脚本不再继续判断 |
| **templateString** | 生成的脚本模板,支持 `${变量}` 语法 |
| **siteFields** | 站点字段类型定义 |
| **trackFields** | 路径字段类型定义 |
| **planFields** | 计划字段类型定义 |
---
## 执行流程说明
1.**priority 从大到小** 依次检查每个 CoderSettings
2. 如果 **useVerb** 条件为 true,则生成 **templateString** 脚本
3. 如果 **blockVerb** 为 true 且 useVerb 通过,则 **阻止低优先级脚本继续执行**
4. ProgramTrackCoderSettings 使用自定义的 `ITrackCoder` 程序来生成脚本
---
## 一、TemplateTrackCoderSettings(路径脚本)
### 1. MultiWheelLifterCar.cs - 多舵轮顶升车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 30 | `track.SleepTime!=0` | false | `agv.Wait();agv.Sleep(${track.SleepTime});agv.Wait();` | 路径上休眠 |
| 30 | `track.TrayTarget!=0` | false | `agv.Wait();agv.TrayControl(${track.TrayTarget});agv.Wait();` | 托盘控制 |
| 27 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceDistance(${track.StopDistance},${track.SlowDistance}); });` | 更改避障距离 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `dst.ChangeAvoidanceParam==true` | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth},${dst.CarCenterX},${dst.CarCenterY}); });` | 切换避障尺寸 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(${track.BiasAlarmThresh},${track.DthAlarmThresh}); });` | 更改跟踪误差阈值 |
| 19 | `dst.tag>0 && src.tag>0` | true | `agv.QrGo(${src.x},${src.y},${src.id},${src.tag},${dst.x},${dst.y},${dst.id},${dst.tag},${track.id},...);` | 二维码导航 |
---
### 2. Kiva.cs - Kiva车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 30 | `dst.tag>0 && src.tag>0` | true | `agv.QrGo(...);` | 二维码导航 |
| 22 | `src.Shelf && plan.curSeg==1` | true | `agv.LeaveShelf(...);` | 离开货架 |
| 20 | `plan.action=='fetch' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Fetch(...);` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Put(...);` | 放货 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceDistance(...); });` | 更改避障距离 |
| 20 | (无条件) | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth}); });` | 切换避障尺寸 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(...); });` | 更改跟踪误差阈值 |
| 17 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 10 | `track.CalibrateWheelEncoder && track.ReverseDst != dst.id` | true | `agv.CalibrateWheelEncoder(...);` | 标定轮里程计 |
---
### 3. Forklift.cs - 叉车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 22 | `src.Shelf` | true | `agv.LeaveShelf(...);agv.Wait();` | 离开货架 |
| 20 | `track.LidarArea != -2` | - | `agv.Queue(()=>{},()=>{ agv.SwitchLidarArea(${track.LidarArea}); });` | 切换激光避障区域 |
| 20 | `track.IOArea != -1` | - | `agv.Queue(()=>{},()=>{ agv.SwitchIoArea(${track.IOArea}); });` | 切换IO区域 |
| 20 | `dst.CarLength != -1 && dst.CarWidth != -1` | - | `agv.Queue(()=>{},()=>{ agv.ChangeAvoidanceParam(${dst.CarLength},${dst.CarWidth}); });` | 切换避障尺寸 |
| 20 | `plan.action=='fetch' && dst.Shelf` | true | `agv.Wait();agv.Fetch(...);agv.Wait();` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf` | true | `agv.Wait();agv.Put(...);agv.Wait();` | 放货 |
| 20 | `track.BiasAlarmThresh >0 \|\| track.DthAlarmThresh > 0` | - | `agv.Queue(()=>{},()=>{ agv.ChangeTrackingErrThresh(...); });` | 更改跟踪误差阈值 |
---
### 4. MultiWheelForkLifter.cs - 多舵轮叉车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 20 | `plan.action=='fetch' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Wait();agv.Fetch(...);agv.Wait();` | 取货 |
| 20 | `plan.action=='put' && dst.Shelf && plan.curSeg==plan.segN-2` | true | `agv.Wait();agv.Put(...);agv.Wait();` | 放货 |
---
## 二、TemplateSiteCoderSettings(站点脚本)
### 1. Kiva.cs
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 25 | `plan.action=='fetch' && plan.segN == 1 && dst.Shelf` | true | `agv.FetchInPlace(...);agv.Wait();` | 原地取货 |
### 2. Forklift.cs
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 25 | `plan.action=='fetch' && plan.segN==1 && dst.Shelf` | true | `agv.FetchInPlace(...);agv.Wait();` | 原地取货 |
### 3. ArmCar.cs - 机械臂车
| Priority | useVerb | blockVerb | templateString | 说明 |
|----------|---------|-----------|----------------|------|
| 5 | `plan.action=='pickFull' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PickFull("${dst.name}");agv.Wait();` | 满盘取货 |
| 5 | `plan.action=='pickEmpty' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PickEmpty("${dst.name}");agv.Wait();` | 空盘取货 |
| 5 | `plan.action=='putFull' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PutFull("${dst.name}");agv.Wait();` | 满盘放货 |
| 5 | `plan.action=='putEmpty' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.PutEmpty("${dst.name}");agv.Wait();` | 空盘放货 |
| 5 | `plan.action=='MoveArm' && plan.curSeg==plan.segN-1` | true | `agv.Wait();agv.MoveArm(${dst.MoveDirection});agv.Wait();` | 移动机械臂 |
---
## 三、ProgramTrackCoderSettings(程序化路径脚本)
| 车型 | Priority | Program | 说明 |
|------|----------|---------|------|
| MultiWheelLifterCar | 19 | `MagTrackCoder` | 磁导航路径编码 |
| Kiva | 19 | `AllCarMagTrackCoder` | 磁导航路径编码 |
| Kiva | 5 | `KivaCarTrackCoder` | Kiva旋转控制 |
---
## 四、字段类型定义
### BasicCarFields
```csharp
public float MagSlowSpeed = 0;
public float MagFullSpeed = 0;
```
### BasicSiteFields
```csharp
public bool Shelf = false;
public float CarLength = -1;
public float CarWidth = -1;
public float CarCenterX = 0;
public float CarCenterY = 0;
public int tag = -1;
public int TagValue = -1;// 磁导航,二维码值,或者rfid值
```
### BasicTrackFields
```csharp
public int IOArea = -1;
public int LidarArea = -2;
public float BiasAlarmThresh = -1;
public float DthAlarmThresh = -1;
public float Speed = 0.2f;
public bool Reverse = false;
public int ReverseDst = -1;
public bool SwitchBarrier = false;
public bool CalibrateWheelEncoder = false;
public float CarDirectionBias = 0;
public bool EnableCarAbsoluteDirection = false;
public float CarAbsoluteDirection = 0;
public float SlowDistance = -1;
public float StopDistance = -1;
```
### BasicPlanFields
```csharp
public string action = "/";
public float CarLength = -1;
public float CarWidth = -1;
```
---
## 五、扩展字段类型
### MultiWheelLifterTrackFields (继承 BasicTrackFields)
```csharp
public int SleepTime = 0;
public float TrayTarget = 0;
```
### MultiWheelLifterSiteFields (继承 BasicSiteFields)
```csharp
public bool ChangeAvoidanceParam = false;
```
### KivaSiteFields (继承 BasicSiteFields)
```csharp
public int AngleTarget = 0;
public bool Turn = false;
public float FetchSpeed = 0;
public int FetchLidarArea = -2;
public int FetchIOArea = -1;
public bool FetchReverse = false;
public float FetchBlindMoveDist = 0;
public float FetchLiftDownTarget = -1;
public float FetchLiftUpTarget = -1;
public bool FetchUseQr = false;
public int FetchQrMode = -1;
public bool FetchIsUpQr = false;
public bool FetchUseDetector = false;
public int FetchDetector = 0;
public float FetchDetectWidth = -1;
public float FetchDetectDepth = -1;
public bool FetchLeaveSrcEarly = false;
public float FetchShieldObstacleDist = -1;
// ... 以及 Put 和 LeaveShelf 相关字段
```
### KivaTrackFields (继承 BasicTrackFields)
```csharp
public int ManeuverDir = 0;
public int ForwardDst = 0;
public int ForwardObChooseDst = -2;
public float BlindMoveDist = 0;
public bool UseDetector = false;
public int DetectorMode = -1;
public float DetectWidth = -1;
public float DetectDepth = -1;
public bool LeaveSrcEarly = false;
public float ShieldObstacleDist = -1;
```
### KivaPlanFields (继承 BasicPlanFields)
```csharp
public bool reverse = false;
public int level = 0;
```
---
## 六、ITrackCoder 接口
程序化脚本生成器需要实现 `ITrackCoder` 接口:
```csharp
public interface ITrackCoder
{
/// <summary>
/// 生成脚本代码
/// </summary>
/// <param name="plan">路径计划</param>
/// <param name="track">当前路径段</param>
/// <param name="src">起点</param>
/// <param name="dst">终点</param>
/// <param name="i">段索引</param>
/// <returns>是否成功生成</returns>
bool Code(SegmentPlan plan, Track track, Site src, Site dst, int i);
/// <summary>
/// 是否阻止后续脚本生成
/// </summary>
bool toBlock();
}
```
---
*文档生成时间: 2024年*
Binary file not shown.
@@ -0,0 +1,944 @@
# DoorController 开发手册
## 目录
- [概述](#概述)
- [基础架构](#基础架构)
- [开发步骤](#开发步骤)
- [实现示例](#实现示例)
- [特性说明](#特性说明)
- [线程安全](#线程安全)
- [最佳实践](#最佳实践)
- [注意事项](#注意事项)
- [附录](#附录)
---
## 概述
`DoorController` 是门控制系统的核心组件,采用抽象基类设计,支持扩展不同类型的门控制器实现。本手册指导开发者如何创建自定义的门控制器。
### 核心概念
- **BasicDoorController**:门控制器抽象基类,定义通用接口和属性
- **DoorTypeAttribute**:类型特性,用于标记控制器类型,支持动态实例化
- **DoorState**:门状态枚举(Closed/Open/Unknown
- **DoorControllerState**:控制器状态枚举(Offline/Online/Connecting/Error
### 设计原则
1. **抽象化**:所有通信细节封装在具体实现类中
2. **线程安全**:状态读取和控制写入分离,通信操作在内部线程完成
3. **可扩展性**:通过 `DoorTypeAttribute` 实现类型自动识别和动态加载
4. **热更新支持**:配置变更无需重启任务
---
## 基础架构
### 1. 类继承关系
```
BasicDoorController (抽象基类)
└── ModbusDoorController (Modbus TCP 实现)
└── [其他实现...]
```
### 2. BasicDoorController 核心属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `Index` | int | 控制器索引(唯一标识) |
| `Ip` | string | IP地址 |
| `Port` | int | 端口号 |
| `State` | DoorControllerState | 控制器状态 |
| `IsOnline` | bool | 是否在线(只读) |
| `DoorStates` | Dictionary<int, DoorState> | 门状态字典 |
| `DoorControlTargets` | Dictionary<int, bool> | 门目标控制状态字典 |
| `DoorConfigs` | Dictionary<int, DoorModel> | 门配置信息字典 |
| `LastUpdateTime` | DateTime | 最后更新时间 |
| `ErrorMessage` | string | 错误信息 |
### 3. 核心方法
#### 3.1 必须实现的方法(抽象方法)
```csharp
/// <summary>
/// 读取门状态(开到位信号)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <returns>true=打开,false=关闭</returns>
public abstract bool ReadDoorState(int doorIndex);
/// <summary>
/// 写入门控制信号(开关控制)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <param name="open">true=打开,false=关闭</param>
public abstract void WriteDoorControl(int doorIndex, bool open);
```
#### 3.2 可重写的方法(虚方法)
```csharp
/// <summary>
/// 连接门控制器
/// </summary>
public virtual void Connect() { }
/// <summary>
/// 断开连接
/// </summary>
public virtual void Disconnect() { }
/// <summary>
/// 更新控制器状态
/// </summary>
public virtual void UpdateState(DoorControllerState newState, string errorMessage = "") { }
/// <summary>
/// 设置门的目标控制状态
/// </summary>
public virtual void SetDoorControlTarget(int doorIndex, bool open) { }
```
---
## 开发步骤
### 步骤1:创建控制器类
创建新类并继承 `BasicDoorController`
```csharp
using StandardScene.ExtendDevice.Door;
namespace StandardScene.ExtendDevice.Door
{
public class MyDoorController : BasicDoorController
{
// 实现抽象方法
}
}
```
### 步骤2:添加 DoorTypeAttribute
使用 `DoorTypeAttribute` 标记控制器类型:
```csharp
[DoorType("MyDoorController")]
public class MyDoorController : BasicDoorController
{
// ...
}
```
**注意**`DoorTypeAttribute``Name` 参数将显示在配置界面的类型下拉框中,并用于配置文件中的类型标识。
### 步骤3:实现抽象方法
实现 `ReadDoorState``WriteDoorControl` 方法:
```csharp
public override bool ReadDoorState(int doorIndex)
{
// 读取门状态逻辑
// 返回 true=打开,false=关闭
}
public override void WriteDoorControl(int doorIndex, bool open)
{
// 写入门控制信号逻辑
}
```
### 步骤4:重写 Connect/Disconnect 方法
如果需要初始化连接、启动后台任务等,重写 `Connect``Disconnect` 方法:
```csharp
public override void Connect()
{
base.Connect(); // 调用基类方法更新状态
// 初始化连接
// 启动后台任务
// 读取初始状态
}
public override void Disconnect()
{
// 停止后台任务
// 关闭连接
base.Disconnect(); // 调用基类方法更新状态
}
```
### 步骤5:实现线程安全的控制逻辑
如果需要定时读取状态或根据 `DoorControlTargets` 下发控制指令,实现后台任务:
```csharp
private CancellationTokenSource _cancellationTokenSource;
private Task _readTask;
public override void Connect()
{
base.Connect();
// 启动定时读取任务
_cancellationTokenSource = new CancellationTokenSource();
_readTask = Task.Run(() => ReadDoorStatesLoop(_cancellationTokenSource.Token));
}
private void ReadDoorStatesLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
// 读取所有门的状态
ReadAllDoorStates();
// 根据 DoorControlTargets 下发控制指令
ApplyDoorControlTargets();
Thread.Sleep(ReadInterval);
}
}
```
---
## 实现示例
### 示例1ModbusDoorController(完整实现)
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StandardScene.Utils;
using SimpleCore.Library;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// Modbus 门控制器实现
/// </summary>
[DoorType("ModbusDoorController")]
public class ModbusDoorController : BasicDoorController
{
private ModbusRtu _modbusClient;
private readonly object _syncLock = new object();
private bool _isStarted = false;
private readonly Dictionary<int, bool> _lastSentControl = new Dictionary<int, bool>();
private CancellationTokenSource _cancellationTokenSource;
private Task _readTask;
// 可配置参数
public int ReadInterval { get; set; } = 1000; // 读取间隔(毫秒)
public int ReconnectInterval { get; set; } = 3000; // 重连间隔(毫秒)
public byte SlaveAddress { get; set; } = 1; // Modbus 从站地址
/// <summary>
/// 线程安全的设置门控制目标
/// </summary>
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock)
{
base.SetDoorControlTarget(doorIndex, open);
}
}
/// <summary>
/// 连接门控制器
/// </summary>
public override void Connect()
{
lock (_syncLock)
{
if (_isStarted) return;
try
{
UpdateState(DoorControllerState.Connecting);
// 初始化门状态
var doorIndices = DoorConfigs.Keys.OrderBy(k => k).ToList();
InitializeDoors(doorIndices);
// 初始化最近一次已下发的控制状态
_lastSentControl.Clear();
foreach (var index in doorIndices)
{
_lastSentControl[index] = false;
if (!DoorControlTargets.ContainsKey(index))
{
DoorControlTargets[index] = false;
}
}
// 连接 Modbus TCP
try
{
_modbusClient = new ModbusRtu();
_modbusClient.StartTcpRtu(Ip, Port);
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Connecting);
Diagnosis.Log($"ModbusDoorController[{Index}] 初次连接失败: {ex.Message}", "ModbusDoorController", true);
}
// 启动定时读取任务
_cancellationTokenSource = new CancellationTokenSource();
_readTask = Task.Run(() => ReadDoorStatesLoop(_cancellationTokenSource.Token));
_isStarted = true;
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"初始化失败: {ex.Message}");
_isStarted = false;
}
}
}
/// <summary>
/// 断开连接
/// </summary>
public override void Disconnect()
{
lock (_syncLock)
{
if (!_isStarted) return;
try
{
_cancellationTokenSource?.Cancel();
_readTask?.Wait(1000);
_modbusClient?.Close();
_modbusClient = null;
_isStarted = false;
UpdateState(DoorControllerState.Offline);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"断开连接失败: {ex.Message}");
}
}
}
/// <summary>
/// 定时读取门状态循环
/// </summary>
private void ReadDoorStatesLoop(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (!_isStarted) break;
// 检查连接状态
bool isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
if (_modbusClient == null || !isConnected)
{
UpdateState(DoorControllerState.Connecting);
TryReconnect();
isConnected = _modbusClient?.modbusRtu?.Connected ?? false;
if (!isConnected)
{
Thread.Sleep(ReadInterval);
continue;
}
}
// 读取所有门的状态
ReadAllDoorStates();
// 根据目标控制状态下发控制指令
ApplyDoorControlTargets();
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 读取状态失败: {ex.Message}", "ModbusDoorController", true);
UpdateState(DoorControllerState.Error, $"读取状态失败: {ex.Message}");
TryReconnect();
}
Thread.Sleep(ReadInterval);
}
}
/// <summary>
/// 读取所有门的状态
/// </summary>
private void ReadAllDoorStates()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
try
{
var state = ReadDoorState(doorConfig.Index);
UpdateDoorState(doorConfig.Index, state ? DoorState.Open : DoorState.Closed);
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 读取门{doorConfig.Index}状态失败: {ex.Message}", "ModbusDoorController", true);
}
}
}
}
/// <summary>
/// 根据 DoorControlTargets 下发控制指令
/// </summary>
private void ApplyDoorControlTargets()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
var doorIndex = doorConfig.Index;
// 获取目标控制状态
bool target = false;
DoorControlTargets.TryGetValue(doorIndex, out target);
// 获取上一次已下发的状态
bool last;
var hasLast = _lastSentControl.TryGetValue(doorIndex, out last);
// 如果没有记录或状态发生变化,则下发控制
if (!hasLast || last != target)
{
try
{
WriteDoorControl(doorIndex, target);
_lastSentControl[doorIndex] = target;
}
catch (Exception ex)
{
Diagnosis.Log($"ModbusDoorController[{Index}] 下发门{doorIndex}控制指令失败: {ex.Message}", "ModbusDoorController", true);
}
}
}
}
}
/// <summary>
/// 读取门状态(开到位信号)
/// </summary>
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
throw new InvalidOperationException("Modbus连接未建立");
}
// 读取开到位信号(离散输入)
var data = _modbusClient.ReadDiscreteInputs_02(SlaveAddress, doorConfig.OpenStatusAddress, 1);
return data != null && data.Length > 0 && data[0];
}
}
/// <summary>
/// 写入门控制信号(开关控制)
/// </summary>
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
TryReconnect();
if (_modbusClient == null || !_modbusClient.modbusRtu.Connected)
{
throw new InvalidOperationException("Modbus连接未建立");
}
}
// 写入开关控制信号(线圈)
_modbusClient.WriteMultipleCoils_15(SlaveAddress, doorConfig.ControlAddress, new[] { open });
}
}
private void TryReconnect()
{
// 重连逻辑...
}
}
}
```
### 示例2:简单门控制器(最小实现)
如果不需要定时读取或后台任务,可以实现最简单的版本:
```csharp
using System;
using StandardScene.ExtendDevice.Door;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 简单门控制器实现(同步模式)
/// </summary>
[DoorType("SimpleDoorController")]
public class SimpleDoorController : BasicDoorController
{
private SimpleDoorClient _client;
public override void Connect()
{
base.Connect();
_client = new SimpleDoorClient(Ip, Port);
UpdateState(DoorControllerState.Online);
}
public override void Disconnect()
{
_client?.Close();
_client = null;
base.Disconnect();
}
public override bool ReadDoorState(int doorIndex)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_client == null || !_client.IsConnected)
{
throw new InvalidOperationException("连接未建立");
}
// 读取门状态
return _client.ReadDoorStatus(doorConfig.OpenStatusAddress);
}
public override void WriteDoorControl(int doorIndex, bool open)
{
if (!DoorConfigs.TryGetValue(doorIndex, out var doorConfig))
{
throw new ArgumentException($"门{doorIndex}不存在");
}
if (_client == null || !_client.IsConnected)
{
throw new InvalidOperationException("连接未建立");
}
// 写入控制信号
_client.WriteDoorControl(doorConfig.ControlAddress, open);
// 同步更新门状态(可选)
var state = _client.ReadDoorStatus(doorConfig.OpenStatusAddress);
UpdateDoorState(doorIndex, state ? DoorState.Open : DoorState.Closed);
}
/// <summary>
/// 重写 SetDoorControlTarget 以立即执行控制
/// </summary>
public override void SetDoorControlTarget(int doorIndex, bool open)
{
base.SetDoorControlTarget(doorIndex, open);
// 立即执行控制(同步模式)
try
{
WriteDoorControl(doorIndex, open);
}
catch (Exception ex)
{
Diagnosis.Log($"SimpleDoorController[{Index}] 控制门{doorIndex}失败: {ex.Message}", "SimpleDoorController", true);
}
}
}
}
```
---
## 特性说明
### DoorTypeAttribute
`DoorTypeAttribute` 用于标记门控制器类型,支持动态实例化。
**定义**
```csharp
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class DoorTypeAttribute : Attribute
{
public string Name { get; }
public DoorTypeAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
```
**使用**
```csharp
[DoorType("MyDoorController")]
public class MyDoorController : BasicDoorController
{
// ...
}
```
**作用**
1. **类型标识**`Name` 参数作为类型的唯一标识,用于配置文件中指定类型
2. **UI显示**:配置界面会自动识别并显示所有带此特性的控制器类型
3. **动态实例化**`DoorMission` 根据 `Name` 动态查找并创建实例
**注意**
- `Name` 必须唯一
- 建议使用类名作为 `Name`
- `Name` 会显示在配置界面的类型下拉框中
---
## 线程安全
### 1. 设计原则
门控制器采用**读写分离**的线程安全设计:
- **读取**`DoorMission` 通过 `controller.DoorStates` 直接访问门状态(受锁保护)
- **写入**`DoorMission` 通过 `controller.SetDoorControlTarget()` 设置目标状态,实际通信由门控制器内部线程完成
### 2. 线程安全要求
#### 2.1 SetDoorControlTarget 方法
如果多个线程可能同时调用 `SetDoorControlTarget`,必须加锁保护:
```csharp
private readonly object _syncLock = new object();
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock)
{
base.SetDoorControlTarget(doorIndex, open);
}
}
```
#### 2.2 ReadDoorState 和 WriteDoorControl 方法
如果这些方法会被多个线程调用,必须加锁保护:
```csharp
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock)
{
// 读取逻辑
}
}
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock)
{
// 写入逻辑
}
}
```
#### 2.3 后台任务访问共享资源
如果后台任务会访问 `DoorStates``DoorControlTargets` 等共享资源,必须加锁:
```csharp
private void ReadAllDoorStates()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
var state = ReadDoorState(doorConfig.Index);
UpdateDoorState(doorConfig.Index, state ? DoorState.Open : DoorState.Closed);
}
}
}
private void ApplyDoorControlTargets()
{
lock (_syncLock)
{
foreach (var doorConfig in DoorConfigs.Values)
{
// 访问 DoorControlTargets
// 调用 WriteDoorControl
}
}
}
```
### 3. 推荐实现模式
推荐使用单一锁对象保护所有共享资源:
```csharp
public class MyDoorController : BasicDoorController
{
private readonly object _syncLock = new object();
// 所有访问共享资源的方法都使用同一个锁
public override void SetDoorControlTarget(int doorIndex, bool open)
{
lock (_syncLock) { /* ... */ }
}
public override bool ReadDoorState(int doorIndex)
{
lock (_syncLock) { /* ... */ }
}
public override void WriteDoorControl(int doorIndex, bool open)
{
lock (_syncLock) { /* ... */ }
}
private void ReadAllDoorStates()
{
lock (_syncLock) { /* ... */ }
}
private void ApplyDoorControlTargets()
{
lock (_syncLock) { /* ... */ }
}
}
```
---
## 最佳实践
### 1. 错误处理
- **连接错误**:设置状态为 `Connecting``Error`,并记录错误信息
- **读取错误**:记录日志,但不抛出异常,返回默认值或保持当前状态
- **写入错误**:记录日志,尝试重连,但不影响其他门的操作
**示例**
```csharp
public override bool ReadDoorState(int doorIndex)
{
try
{
// 读取逻辑
}
catch (Exception ex)
{
Diagnosis.Log($"读取门{doorIndex}状态失败: {ex.Message}", "MyDoorController", true);
return false; // 返回默认值
}
}
```
### 2. 状态管理
- **及时更新状态**:在连接、断开、错误时及时调用 `UpdateState()`
- **更新最后更新时间**:在状态变化时更新 `LastUpdateTime`
- **错误信息**:在错误时记录详细的错误信息到 `ErrorMessage`
**示例**
```csharp
try
{
_client.Connect();
UpdateState(DoorControllerState.Online);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"连接失败: {ex.Message}");
}
```
### 3. 资源释放
- **实现 Disconnect**:确保正确关闭连接和释放资源
- **实现析构函数**:作为最后的安全网,确保资源释放
**示例**
```csharp
public override void Disconnect()
{
try
{
_cancellationTokenSource?.Cancel();
_readTask?.Wait(1000);
_client?.Close();
_client = null;
UpdateState(DoorControllerState.Offline);
}
catch (Exception ex)
{
UpdateState(DoorControllerState.Error, $"断开连接失败: {ex.Message}");
}
}
~MyDoorController()
{
Disconnect();
}
```
### 4. 配置验证
`Connect()` 中验证配置的完整性:
```csharp
public override void Connect()
{
if (string.IsNullOrWhiteSpace(Ip))
{
UpdateState(DoorControllerState.Error, "IP地址未配置");
return;
}
if (Port <= 0 || Port > 65535)
{
UpdateState(DoorControllerState.Error, "端口号无效");
return;
}
if (DoorConfigs.Count == 0)
{
UpdateState(DoorControllerState.Error, "未配置门");
return;
}
// 连接逻辑...
}
```
---
## 注意事项
### 1. DoorTypeAttribute 命名
- `Name` 必须与配置文件中使用的类型名称一致
- 建议使用类名作为 `Name`
- 避免使用特殊字符
### 2. 异常处理
- **不要抛出未处理的异常**:所有异常都应该被捕获并记录
- **不要阻塞线程**:长时间操作应该在后台线程中执行
- **提供错误信息**:通过 `ErrorMessage` 属性提供详细的错误信息
### 3. 性能考虑
- **避免频繁的连接/断开**:保持连接持久化
- **批量读取**:如果可能,批量读取多个门的状态
- **控制读取频率**:根据实际需求设置合理的读取间隔
### 4. 兼容性
- **向后兼容**:新版本应该兼容旧版本的配置格式
- **版本标识**:如果需要,可以在实现中添加版本检查
---
## 附录
### A. DoorModel 说明
```csharp
public class DoorModel
{
/// <summary>
/// 门索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// 开关控制信号地址
/// </summary>
public ushort ControlAddress { get; set; }
/// <summary>
/// 开到位信号地址
/// </summary>
public ushort OpenStatusAddress { get; set; }
}
```
### B. DoorState 枚举
```csharp
public enum DoorState
{
Closed = 0, // 关闭
Open = 1, // 打开
Unknown = 2 // 未知状态
}
```
### C. DoorControllerState 枚举
```csharp
public enum DoorControllerState
{
Offline = 0, // 离线
Online = 1, // 在线
Connecting = 2, // 连接中
Error = 3 // 错误
}
```
### D. 开发检查清单
- [ ] 继承 `BasicDoorController`
- [ ] 添加 `DoorTypeAttribute` 特性
- [ ] 实现 `ReadDoorState` 方法
- [ ] 实现 `WriteDoorControl` 方法
- [ ] 重写 `Connect` 方法(如需要)
- [ ] 重写 `Disconnect` 方法(如需要)
- [ ] 实现线程安全(如需要)
- [ ] 添加错误处理
- [ ] 添加资源释放逻辑
- [ ] 添加诊断日志
- [ ] 测试连接/断开
- [ ] 测试读取状态
- [ ] 测试控制写入
- [ ] 测试配置热更新
---
**文档更新时间**2025-01-09
@@ -0,0 +1,634 @@
# DoorMission 使用手册
## 目录
- [概述](#概述)
- [基本功能](#基本功能)
- [启动与停止](#启动与停止)
- [配置管理](#配置管理)
- [门控逻辑](#门控逻辑)
- [站点配置](#站点配置)
- [UI界面](#ui界面)
- [参数说明](#参数说明)
- [故障排查](#故障排查)
- [附录](#附录)
---
## 概述
`DoorMission` 是一个门控进程类,用于管理多个门控制器及其关联的门。它提供了以下核心功能:
- 多门控制器管理(支持不同类型)
- 自动门控逻辑(根据小车位置自动开关门)
- 手动控制功能(支持临时手动控制,优先级高于自动控制)
- 车辆占用管理(跟踪和清空门区域的车辆占用)
- 配置文件动态监控(支持热更新)
- 线程安全的门状态读写
- 可视化的配置和监控界面
### 架构特点
- **抽象化设计**:门控制器通过抽象基类 `BasicDoorController` 实现,支持扩展不同类型的控制器
- **线程安全**:门状态读取和控制写入分离,所有通信操作在门控制器内部线程完成
- **事件驱动**:与交通控制系统集成,响应小车进入/离开站点事件
- **热更新支持**:配置文件每10秒自动检查更新,无需重启任务
- **控制仲裁机制**:手动控制优先级高于自动控制,手动控制过期后自动恢复自动模式
---
## 基本功能
### 1. 门控制器管理
#### 1.1 门控制器类型
门控制器通过 `DoorTypeAttribute` 标记类型,系统会自动识别并创建实例。当前支持:
- **ModbusDoorController**:基于 Modbus TCP 的门控制器
#### 1.2 门控制器配置
每个门控制器包含以下配置:
- **Index**:控制器索引(唯一标识)
- **Ip**IP地址
- **Port**:端口号(默认502
- **Type**:控制器类型(通过 `DoorTypeAttribute.Name` 指定)
- **Doors**:门列表
#### 1.3 门配置
每个门包含以下配置:
- **Index**:门索引(在控制器内唯一)
- **ControlAddress**:开关控制信号地址(Modbus 线圈地址)
- **OpenStatusAddress**:开到位信号地址(Modbus 离散输入地址)
### 2. 自动门控逻辑
#### 2.1 门开启条件
门会在以下情况自动开启:
1. **小车即将进入区域**:当小车到达站点且站点的 `EnterDoor` 字段匹配时,门会在锁定前开启
2. **小车在区域内**:当小车已进入并锁定站点时,门保持开启状态
#### 2.2 门关闭条件
门会在以下情况自动关闭:
- 小车离开区域后,门自动关闭
#### 2.3 门标识符格式
站点配置中的门标识符格式为:**控制器索引.门索引**
**示例**
```
"EnterDoor": "1.2" // 表示控制器索引1,门索引2
"LeaveDoor": "2.3" // 表示控制器索引2,门索引3
```
### 3. 配置文件动态监控
系统每10秒自动检查 `DoorConfig.json` 文件,并根据配置变化:
- **添加**:新增的门控制器会自动创建并连接
- **删除**:已移除的门控制器会自动断开并移除
- **修改**:已修改的门控制器会自动更新(IP、端口、类型或门配置变化)
---
## 启动与停止
### 1. 启动任务
```csharp
var doorMission = new DoorMission();
doorMission.Execute(); // 执行"启动进程"
```
**启动流程**
1. 设置数据文件路径(`DoorConfig.json`
2. 订阅交通控制事件(`BeforeLock``AfterLeave``OnLockAcquired`
3. 立即加载一次配置(避免监控界面在首次轮询前无数据)
4. 启动配置监控任务(每10秒检查一次)
5. 启动门控逻辑监控任务(每500毫秒检查一次)
### 2. 停止任务
```csharp
doorMission.Stop(); // 执行"停止进程"
```
**停止流程**
1. 取消事件订阅
2. 取消所有后台任务
3. 断开所有门控制器连接
4. 等待任务完成(最多等待5秒)
### 3. 公共方法
#### 3.1 读取门状态
```csharp
bool isOpen = doorMission.GetDoorState(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**`true`=打开,`false`=关闭
#### 3.2 设置门控制目标(自动模式)
```csharp
doorMission.SetDoorControlTarget(controllerIndex, doorIndex, open);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- `open``true`=打开,`false`=关闭
**注意**:此方法仅设置目标控制状态,实际通信由门控制器内部线程完成,确保线程安全。此方法会立即生效,但可能被手动控制覆盖。
#### 3.3 设置手动控制目标
```csharp
bool success = doorMission.SetManualDoorControl(controllerIndex, doorIndex, open, holdSeconds);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- `open``true`=打开,`false`=关闭
- `holdSeconds`:手动保持秒数(可选,默认10秒)
- **返回值**`true`=成功,`false`=失败(当有车辆占用且尝试关闭时返回false)
**功能说明**
- 手动控制优先级高于自动控制
- 手动控制会在指定时间后自动过期,恢复自动模式
- **安全保护**:当门区域内有车辆占用时,禁止手动关闭门
- 手动控制过期后,系统自动恢复自动控制逻辑
#### 3.4 清除手动控制
```csharp
doorMission.ClearManualDoorControl(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
**功能说明**:立即清除手动控制请求,恢复自动控制模式。
#### 3.5 清空车辆占用
```csharp
doorMission.ClearCarsInArea(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
**功能说明**:清空指定门的车辆占用记录。清空后,如果门处于打开状态且没有其他小车需要进入,门会自动关闭。
#### 3.6 获取门控制状态
```csharp
var status = doorMission.GetDoorControlStatus(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**`DoorControlStatus` 对象,包含:
- `Target`:当前目标状态(true=打开,false=关闭)
- `Source`:控制来源(`ControlSource.Auto``ControlSource.Manual`
- `ManualRemainingSeconds`:手动控制剩余秒数(仅当Source=Manual时有效)
- `CarsInArea`:车辆占用列表
#### 3.7 获取车辆占用情况
```csharp
var cars = doorMission.GetCarsInArea(controllerIndex, doorIndex);
```
- **参数**
- `controllerIndex`:门控制器索引
- `doorIndex`:门索引
- **返回值**:车辆ID列表(`IReadOnlyList<int>`
---
## 配置管理
### 1. 配置文件格式
配置文件 `DoorConfig.json` 位于程序根目录,格式如下:
```json
[
{
"Index": 1,
"Ip": "192.168.1.100",
"Port": 502,
"Type": "ModbusDoorController",
"Doors": [
{
"Index": 1,
"ControlAddress": 0,
"OpenStatusAddress": 0
},
{
"Index": 2,
"ControlAddress": 1,
"OpenStatusAddress": 1
}
]
},
{
"Index": 2,
"Ip": "192.168.1.101",
"Port": 502,
"Type": "ModbusDoorController",
"Doors": [
{
"Index": 1,
"ControlAddress": 0,
"OpenStatusAddress": 0
}
]
}
]
```
### 2. 配置界面
通过调用 `DoorMission.OpenViewer()` 打开门控制器管理界面,可以:
- 添加、删除、修改门控制器
- 为每个门控制器添加、删除、修改门配置
- 保存配置到 `DoorConfig.json`
**打开配置界面**
```csharp
DoorMission.OpenViewer();
```
---
## 门控逻辑
### 1. 事件响应流程
#### 1.1 BeforeLock 事件
当小车即将锁定站点时触发:
1. 检查站点的 `EnterDoor` 字段
2. 检查小车当前站点的 `PreEnterDoor` 字段是否匹配
3. 如果匹配,设置 `_needOpen[(controllerIndex, doorIndex)] = true`
4. 返回门的当前状态(如果门已打开则允许锁定)
#### 1.2 OnLockAcquired 事件
当小车成功锁定站点时触发:
1. 检查站点的 `EnterDoor` 字段
2. 检查小车当前站点的 `PreEnterDoor` 字段是否匹配
3. 如果匹配,将小车ID添加到 `carsInAreas[(controllerIndex, doorIndex)]`
4. 设置 `_needOpen[(controllerIndex, doorIndex)] = false`
#### 1.3 AfterLeave 事件
当小车离开站点时触发:
1. 检查站点的 `LeaveDoor` 字段
2. 检查小车当前站点的 `RearLeaveDoor` 字段是否匹配
3. 如果匹配,从 `carsInAreas[(controllerIndex, doorIndex)]` 中移除小车ID
### 2. 门控状态监控与仲裁
`MonitorDoorLogicAsync` 任务每500毫秒执行一次,检查每个门的控制逻辑并进行仲裁:
```csharp
// 自动目标:有车或需要打开
var needOpen = _needOpen.TryGetValue(key, out var open) && open;
var hasCarsInArea = carsInAreas.TryGetValue(key, out var cars) && cars.Count > 0;
var autoTarget = needOpen || hasCarsInArea;
// 手动请求仲裁:优先级 Manual > Auto,手动过期后自动恢复
bool finalTarget = autoTarget;
if (_manualRequests.TryGetValue(key, out var manual))
{
if (manual.ExpireAt <= DateTime.Now)
{
_manualRequests.Remove(key); // 手动控制过期,移除
}
else
{
finalTarget = manual.Target; // 手动控制有效,使用手动目标
}
}
// 设置门的目标控制状态
controller.SetDoorControlTarget(doorIndex, finalTarget);
```
**逻辑说明**
- **自动目标计算**
- 如果 `_needOpen[key] = true`,门需要打开(小车即将进入)
- 如果 `carsInAreas[key]` 中有小车,门需要保持打开(小车在区域内)
- 其他情况,自动目标为关闭
- **控制仲裁**
- 手动控制优先级高于自动控制
- 如果存在有效的手动控制请求(未过期),使用手动目标
- 手动控制过期后,自动移除并恢复自动控制
- 最终目标写入门控制器的 `DoorControlTargets` 字段
---
## 站点配置
### 1. 站点字段说明
站点需要配置以下字段以实现门控功能:
| 字段名 | 类型 | 说明 | 示例 |
|--------|------|------|------|
| `EnterDoor` | string | 进站门标识符(格式:控制器索引.门索引) | `"1.2"` |
| `LeaveDoor` | string | 离站门标识符(格式:控制器索引.门索引) | `"2.3"` |
### 2. 小车站点字段说明
小车当前站点需要配置以下字段:
| 字段名 | 类型 | 说明 | 示例 |
|--------|------|------|------|
| `PreEnterDoor` | string | 前方进站门标识符(与目标站点的 `EnterDoor` 匹配) | `"1.2"` |
| `RearLeaveDoor` | string | 后方离站门标识符(与目标站点的 `LeaveDoor` 匹配) | `"2.3"` |
### 3. 配置示例
**站点配置**
```json
{
"id": 100,
"name": "站点A",
"fields": {
"EnterDoor": "1.2",
"LeaveDoor": "2.3"
}
}
```
**小车站点配置**
```json
{
"id": 50,
"name": "小车当前位置",
"fields": {
"PreEnterDoor": "1.2",
"RearLeaveDoor": "2.3"
}
}
```
**工作流程**
1. 小车从站点50驶向站点100
2. 到达站点100时,触发 `BeforeLock` 事件
3. 系统检查站点100的 `EnterDoor``"1.2"`)是否与站点50的 `PreEnterDoor``"1.2"`)匹配
4. 如果匹配,设置控制器1的门2为打开状态
5. 门打开后,小车锁定站点100
6. 触发 `OnLockAcquired` 事件,门保持打开状态
7. 小车离开站点100时,触发 `AfterLeave` 事件
8. 系统检查站点100的 `LeaveDoor``"2.3"`)是否与站点50的 `RearLeaveDoor``"2.3"`)匹配
9. 如果匹配,从区域内小车列表中移除该小车
10. 如果没有其他小车在区域内,门自动关闭
---
## UI界面
### 1. 配置管理界面
**打开方式**
```csharp
DoorMission.OpenViewer();
```
**功能**
- 门控制器列表管理(添加、删除、修改)
- 门列表管理(为每个控制器添加、删除、修改门)
- 类型选择(自动识别所有带 `DoorTypeAttribute` 的控制器类型)
- 配置保存到 `DoorConfig.json`
### 2. 监控界面
**打开方式**
```csharp
DoorMission.OpenMonitor();
```
**功能**
- 实时显示所有门的状态
- 显示控制器索引、门索引、当前状态、控制目标、车辆占用
- 显示控制来源和手动控制剩余时间
- 显示控制地址和开到位信号地址
- 手动控制门开关(打开/关闭,带安全保护)
- 清空车辆占用记录
- 自动刷新(默认1秒刷新间隔)
**显示信息**
| 列名 | 说明 |
|------|------|
| 控制器编码 | 门控制器索引 |
| 门编码 | 门索引 |
| 当前状态 | 门的当前状态(开/关,带颜色标识:绿色=打开,红色=关闭) |
| 控制目标 | 门的目标控制状态(开/关,带颜色标识:绿色=开,红色=关) |
| 车辆占用 | 门区域内的小车ID列表(多个ID用逗号分隔,无车辆显示"无" |
| 控制来源 | 当前控制来源(自动/手动) |
| 手动剩余(s) | 手动控制剩余秒数(仅当控制来源=手动时显示,自动时显示"-" |
| 控制地址 | Modbus 线圈地址 |
| 开到位地址 | Modbus 离散输入地址 |
**手动控制功能**
- **打开按钮**:设置手动打开控制,默认保持10秒
- **关闭按钮**:设置手动关闭控制,默认保持10秒
- **安全保护**:当门区域内有车辆占用时,关闭按钮自动禁用,无法执行关闭操作
- 必须先清空车辆占用,才能手动关闭门
- **清空占用按钮**:清空选中门的车辆占用记录
- 清空后,如果门处于打开状态且没有其他小车需要进入,门会自动关闭
- 清空占用后,可以执行手动关闭操作
**控制优先级说明**
- 手动控制优先级高于自动控制
- 手动控制会在指定时间(默认10秒)后自动过期,恢复自动模式
- 可以通过"清空占用"按钮清空车辆占用,然后手动关闭门
---
## 参数说明
### 1. 监控间隔
| 参数 | 默认值 | 说明 |
|------|--------|------|
| 配置监控间隔 | 10秒 | 检查配置文件的间隔 |
| 门控逻辑监控间隔 | 500毫秒 | 检查门控逻辑的间隔(已优化) |
| 监控界面刷新间隔 | 1秒 | 监控界面自动刷新间隔 |
| 手动控制默认保持时间 | 10秒 | 手动控制请求的默认过期时间 |
### 2. 线程安全说明
- **门状态读取**:通过 `controller.DoorStates` 字典访问,所有读写操作受锁保护
- **门控制写入**:通过 `controller.SetDoorControlTarget()` 设置目标状态,实际通信由门控制器内部线程完成
- **配置同步**:配置变更时使用锁保护,确保线程安全
- **控制仲裁**:手动控制请求和自动控制逻辑的仲裁在同一锁内完成,确保线程安全
- **车辆占用管理**:车辆占用的增删改查操作均受锁保护
### 3. 控制仲裁机制
系统采用**控制仲裁机制**来协调手动控制和自动控制:
- **优先级**:手动控制 > 自动控制
- **手动控制过期**:手动控制请求会在指定时间(默认10秒)后自动过期,过期后恢复自动控制
- **安全保护**:当门区域内有车辆占用时,禁止手动关闭门,确保安全
- **仲裁流程**
1. 计算自动目标(基于车辆占用和需要打开标志)
2. 检查是否存在有效的手动控制请求
3. 如果手动控制未过期,使用手动目标;否则使用自动目标
4. 将最终目标写入门控制器的 `DoorControlTargets` 字段
---
## 故障排查
### 问题1:门控制器无法连接
**排查步骤**
1. 检查配置文件中的 IP 和端口是否正确
2. 检查网络连接是否正常
3. 查看诊断日志中的错误信息
4. 确认门控制器硬件是否在线
**诊断命令**
```csharp
var controllers = doorMission.GetDoorControllers();
foreach (var controller in controllers)
{
Console.WriteLine($"控制器{controller.Index}: IP={controller.Ip}, Port={controller.Port}, State={controller.State}, Error={controller.ErrorMessage}");
}
```
### 问题2:门不自动开启
**排查步骤**
1. 确认 `DoorMission` 任务已启动
2. 检查站点的 `EnterDoor` 字段是否配置正确
3. 检查小车站点的 `PreEnterDoor` 字段是否与目标站点的 `EnterDoor` 匹配
4. 查看门控制器的连接状态是否为 `Online`
5. 检查门的状态是否正确读取
**诊断命令**
```csharp
// 检查门状态
bool isOpen = doorMission.GetDoorState(1, 2);
Console.WriteLine($"控制器1门2的状态: {(isOpen ? "" : "")}");
// 检查门控制器状态
var controllers = doorMission.GetDoorControllers();
var controller = controllers.FirstOrDefault(c => c.Index == 1);
if (controller != null)
{
Console.WriteLine($"控制器状态: {controller.State}");
Console.WriteLine($"是否在线: {controller.IsOnline}");
Console.WriteLine($"错误信息: {controller.ErrorMessage}");
}
```
### 问题3:配置文件更新后不生效
**排查步骤**
1. 确认配置文件格式正确(JSON格式)
2. 检查配置文件是否保存成功
3. 等待最多10秒,系统会自动检测更新
4. 查看诊断日志中的配置同步信息
### 问题4:监控界面无数据
**排查步骤**
1. 确认 `DoorMission` 任务已启动
2. 检查是否有配置的门控制器
3. 查看门控制器是否成功连接
4. 检查监控界面的刷新间隔设置
### 问题5:手动关闭按钮无法点击
**原因**
- 门区域内有车辆占用,系统安全保护机制禁止手动关闭
**解决方法**
1. 先点击"清空占用"按钮,清空车辆占用记录
2. 清空后,关闭按钮会自动启用
3. 然后可以执行手动关闭操作
### 问题6:手动控制不生效
**排查步骤**
1. 检查手动控制是否已过期(默认10秒)
2. 查看"控制来源"列,确认是否为"手动"
3. 查看"手动剩余(s)"列,确认剩余时间
4. 如果已过期,手动控制会自动恢复为自动模式
5. 可以通过监控界面重新设置手动控制
---
## 附录
### A. 门标识符解析
门标识符格式:`控制器索引.门索引`
**解析规则**
- 必须包含一个点号(`.`
- 点号前后必须为整数
- 解析失败时返回 `null`
**示例**
- `"1.2"``(controllerIndex: 1, doorIndex: 2)`
- `"10.5"``(controllerIndex: 10, doorIndex: 5)`
- `"1"``null` ❌(缺少点号)
- `"1.2.3"``null` ❌(多个点号)
- `"a.2"``null` ❌(非数字)
### B. 诊断日志说明
系统会在以下情况记录诊断日志:
- 门控制器连接成功/失败
- 门控制器配置变更
- 门状态读取失败
- 门控制写入失败
- 配置加载失败
**日志位置**:系统诊断日志
### C. 类结构关系图
```
DoorMission (门控进程)
├── BasicDoorController (抽象基类)
│ │
│ └── ModbusDoorController (Modbus 实现)
├── DoorManager (配置界面)
├── DoorMonitor (监控界面)
└── DoorModel (配置模型)
```
### D. 控制来源枚举
```csharp
public enum ControlSource
{
Auto = 0, // 自动控制
Manual = 1 // 手动控制
}
```
### E. 门控制状态结构
```csharp
public class DoorControlStatus
{
public bool Target { get; set; } // 当前目标状态(true=打开,false=关闭)
public ControlSource Source { get; set; } // 控制来源(Auto/Manual
public double? ManualRemainingSeconds { get; set; } // 手动控制剩余秒数(仅当Source=Manual时有效)
public IReadOnlyList<int> CarsInArea { get; set; } // 车辆占用列表
}
```
### F. 版本历史
| 版本 | 日期 | 主要更新 |
|------|------|----------|
| 1.0 | 2025-01 | 初始版本,支持 Modbus 门控制器 |
| 1.1 | 2025-01 | 新增门控仲裁机制,支持手动控制与自动控制协调 |
| 1.2 | 2025-01 | 新增车辆占用管理功能,监控界面显示车辆占用情况 |
| 1.3 | 2025-01 | 优化门控逻辑监控间隔至500ms,新增安全保护机制(占用时禁止手动关闭) |
---
**文档更新时间**2025-01-21
@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SimpleCore.Library;
namespace StandardScene.ExtendDevice.ButtonBox
{
/// <summary>
/// 按钮状态枚举
/// </summary>
public enum ButtonState
{
/// <summary>
/// 未按下
/// </summary>
Released = 0,
/// <summary>
/// 已按下
/// </summary>
Pressed = 1,
/// <summary>
/// 未知状态
/// </summary>
Unknown = 2
}
/// <summary>
/// 按钮盒状态枚举
/// </summary>
public enum ButtonBoxState
{
/// <summary>
/// 离线
/// </summary>
Offline = 0,
/// <summary>
/// 在线
/// </summary>
Online = 1,
/// <summary>
/// 连接中
/// </summary>
Connecting = 2,
/// <summary>
/// 错误
/// </summary>
Error = 3
}
/// <summary>
/// 基础按钮盒类,包含状态机
/// </summary>
public abstract class BasicButtonBox
{
/// <summary>
/// 按钮盒索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; }
/// <summary>
/// 按钮盒状态
/// </summary>
public ButtonBoxState State { get; protected set; } = ButtonBoxState.Offline;
/// <summary>
/// 是否在线
/// </summary>
public bool IsOnline => State == ButtonBoxState.Online;
/// <summary>
/// 按钮状态字典,键为按钮索引
/// </summary>
public Dictionary<int, ButtonState> ButtonStates { get; protected set; } = new Dictionary<int, ButtonState>();
/// <summary>
/// 按钮配置信息字典,键为按钮索引
/// </summary>
public Dictionary<int, ButtonModel> ButtonConfigs { get; protected set; } = new Dictionary<int, ButtonModel>();
/// <summary>
/// 按钮动作执行后清零对应寄存器:默认空实现,具体盒型(如 Azowie)按需重写。
/// 用于解耦 ButtonMission 对具体盒型的 is 判断,便于驱动外移至卫星 dll。
/// </summary>
public virtual void ClearButtonRegister(int buttonIndex)
{
}
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime LastUpdateTime { get; protected set; } = DateTime.Now;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; protected set; } = string.Empty;
/// <summary>
/// 更新按钮盒状态
/// </summary>
public virtual void UpdateState(ButtonBoxState newState, string errorMessage = "")
{
State = newState;
ErrorMessage = errorMessage;
LastUpdateTime = DateTime.Now;
}
/// <summary>
/// 更新按钮状态
/// </summary>
/// <param name="buttonIndex">按钮索引</param>
/// <param name="state">按钮状态</param>
public virtual void UpdateButtonState(int buttonIndex, ButtonState state)
{
bool hadPrev = ButtonStates.TryGetValue(buttonIndex, out var previous);
if (hadPrev && previous == state)
return;
ButtonStates[buttonIndex] = state;
if (hadPrev)
Diagnosis.Post($"按钮盒{Index}_按钮{buttonIndex}:状态变更为{state}", "ButtonBox", false);
LastUpdateTime = DateTime.Now;
}
/// <summary>
/// 获取按钮状态
/// </summary>
/// <param name="buttonIndex">按钮索引</param>
/// <returns>按钮状态,如果不存在则返回Unknown</returns>
public virtual ButtonState GetButtonState(int buttonIndex)
{
return ButtonStates.TryGetValue(buttonIndex, out var state) ? state : ButtonState.Unknown;
}
/// <summary>
/// 初始化按钮状态
/// </summary>
/// <param name="buttonIndices">按钮索引列表</param>
public virtual void InitializeButtons(List<int> buttonIndices)
{
ButtonStates.Clear();
foreach (var index in buttonIndices)
{
ButtonStates[index] = ButtonState.Released;
}
}
/// <summary>
/// 初始化按钮配置信息
/// </summary>
/// <param name="buttonConfigs">按钮配置列表</param>
public virtual void InitializeButtonConfigs(List<ButtonModel> buttonConfigs)
{
ButtonConfigs.Clear();
if (buttonConfigs != null)
{
foreach (var config in buttonConfigs)
{
ButtonConfigs[config.Index] = new ButtonModel
{
Index = config.Index,
TriggerMission = config.TriggerMission,
TriggerMethod = config.TriggerMethod,
TriggerMethodParams = config.TriggerMethodParams,
TriggerState = config.TriggerState,
TriggerDelay = config.TriggerDelay
};
}
}
}
/// <summary>
/// 更新按钮配置信息
/// </summary>
/// <param name="buttonConfigs">按钮配置列表</param>
public virtual void UpdateButtonConfigs(List<ButtonModel> buttonConfigs)
{
if (buttonConfigs == null)
{
ButtonConfigs.Clear();
return;
}
// 创建配置字典
var configDict = buttonConfigs.ToDictionary(b => b.Index);
// 删除配置中不存在的按钮
var toRemove = ButtonConfigs.Keys.Where(k => !configDict.ContainsKey(k)).ToList();
foreach (var key in toRemove)
{
ButtonConfigs.Remove(key);
}
// 添加或更新按钮配置
foreach (var config in buttonConfigs)
{
ButtonConfigs[config.Index] = new ButtonModel
{
Index = config.Index,
TriggerMission = config.TriggerMission,
TriggerMethod = config.TriggerMethod,
TriggerMethodParams = config.TriggerMethodParams,
TriggerState = config.TriggerState,
TriggerDelay = config.TriggerDelay
};
}
}
/// <summary>
/// 获取按钮配置信息
/// </summary>
/// <param name="buttonIndex">按钮索引</param>
/// <returns>按钮配置信息,如果不存在则返回null</returns>
public virtual ButtonModel GetButtonConfig(int buttonIndex)
{
return ButtonConfigs.TryGetValue(buttonIndex, out var config) ? config : null;
}
/// <summary>
/// 连接按钮盒
/// </summary>
public virtual void Connect()
{
UpdateState(ButtonBoxState.Connecting);
}
/// <summary>
/// 断开连接
/// </summary>
public virtual void Disconnect()
{
UpdateState(ButtonBoxState.Offline);
}
/// <summary>
/// 当与此按钮盒绑定的业务方法执行完成后触发的回调。
/// 子类可重写以实现按钮灯反馈、蜂鸣等效果。
/// </summary>
/// <param name="buttonConfig">触发本次调用的按钮配置</param>
/// <param name="isSuccess">业务方法是否执行成功</param>
public virtual void OnActionExecuted(ButtonModel buttonConfig, bool isSuccess)
{
// 基类默认不做任何事,由具体实现按需要重写
}
}
}
@@ -0,0 +1,608 @@
namespace StandardScene.ExtendDevice.ButtonBox
{
partial class ButtonBoxManager
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.buttonBoxListView = new System.Windows.Forms.ListView();
this.columnHeaderBoxIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderIp = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderPort = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBoxButtonBox = new System.Windows.Forms.GroupBox();
this.btnSaveButtonBox = new System.Windows.Forms.Button();
this.btnDeleteButtonBox = new System.Windows.Forms.Button();
this.btnAddButtonBox = new System.Windows.Forms.Button();
this.labelType = new System.Windows.Forms.Label();
this.comboBoxType = new System.Windows.Forms.ComboBox();
this.labelBoxIndex = new System.Windows.Forms.Label();
this.textBoxBoxIndex = new System.Windows.Forms.TextBox();
this.labelPort = new System.Windows.Forms.Label();
this.textBoxPort = new System.Windows.Forms.TextBox();
this.labelIp = new System.Windows.Forms.Label();
this.textBoxIp = new System.Windows.Forms.TextBox();
this.buttonListView = new System.Windows.Forms.ListView();
this.columnHeaderButtonIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTriggerMission = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTriggerMethod = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTriggerMethodParams = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTriggerState = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTriggerDelay = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBoxButton = new System.Windows.Forms.GroupBox();
this.btnSaveButton = new System.Windows.Forms.Button();
this.btnDeleteButton = new System.Windows.Forms.Button();
this.btnAddButton = new System.Windows.Forms.Button();
this.labelTriggerMethodParams = new System.Windows.Forms.Label();
this.textBoxTriggerMethodParams = new System.Windows.Forms.TextBox();
this.labelTriggerMethod = new System.Windows.Forms.Label();
this.textBoxTriggerMethod = new System.Windows.Forms.TextBox();
this.labelTriggerMission = new System.Windows.Forms.Label();
this.textBoxTriggerMission = new System.Windows.Forms.TextBox();
this.labelButtonIndex = new System.Windows.Forms.Label();
this.textBoxButtonIndex = new System.Windows.Forms.TextBox();
this.labelTriggerState = new System.Windows.Forms.Label();
this.comboBoxTriggerState = new System.Windows.Forms.ComboBox();
this.labelTriggerDelay = new System.Windows.Forms.Label();
this.textBoxTriggerDelay = new System.Windows.Forms.TextBox();
this.labelTitle = new System.Windows.Forms.Label();
this.groupBoxButtonBox.SuspendLayout();
this.groupBoxButton.SuspendLayout();
this.SuspendLayout();
//
// buttonBoxListView
//
this.buttonBoxListView.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.buttonBoxListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.buttonBoxListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeaderBoxIndex,
this.columnHeaderIp,
this.columnHeaderPort,
this.columnHeaderType});
this.buttonBoxListView.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.buttonBoxListView.FullRowSelect = true;
this.buttonBoxListView.GridLines = true;
this.buttonBoxListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.buttonBoxListView.HideSelection = false;
this.buttonBoxListView.Location = new System.Drawing.Point(15, 55);
this.buttonBoxListView.MultiSelect = false;
this.buttonBoxListView.Name = "buttonBoxListView";
this.buttonBoxListView.OwnerDraw = true;
this.buttonBoxListView.Size = new System.Drawing.Size(450, 290);
this.buttonBoxListView.TabIndex = 0;
this.buttonBoxListView.UseCompatibleStateImageBehavior = false;
this.buttonBoxListView.View = System.Windows.Forms.View.Details;
this.buttonBoxListView.SelectedIndexChanged += new System.EventHandler(this.buttonBoxListView_SelectedIndexChanged);
//
// columnHeaderBoxIndex
//
this.columnHeaderBoxIndex.Text = "编码";
this.columnHeaderBoxIndex.Width = 70;
//
// columnHeaderIp
//
this.columnHeaderIp.Text = "IP地址";
this.columnHeaderIp.Width = 130;
//
// columnHeaderPort
//
this.columnHeaderPort.Text = "端口";
this.columnHeaderPort.Width = 90;
//
// columnHeaderType
//
this.columnHeaderType.Text = "类型";
this.columnHeaderType.Width = 140;
//
// groupBoxButtonBox
//
this.groupBoxButtonBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.groupBoxButtonBox.Controls.Add(this.btnSaveButtonBox);
this.groupBoxButtonBox.Controls.Add(this.btnDeleteButtonBox);
this.groupBoxButtonBox.Controls.Add(this.btnAddButtonBox);
this.groupBoxButtonBox.Controls.Add(this.labelType);
this.groupBoxButtonBox.Controls.Add(this.comboBoxType);
this.groupBoxButtonBox.Controls.Add(this.labelBoxIndex);
this.groupBoxButtonBox.Controls.Add(this.textBoxBoxIndex);
this.groupBoxButtonBox.Controls.Add(this.labelPort);
this.groupBoxButtonBox.Controls.Add(this.textBoxPort);
this.groupBoxButtonBox.Controls.Add(this.labelIp);
this.groupBoxButtonBox.Controls.Add(this.textBoxIp);
this.groupBoxButtonBox.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.groupBoxButtonBox.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(68)))), ((int)(((byte)(68)))), ((int)(((byte)(68)))));
this.groupBoxButtonBox.Location = new System.Drawing.Point(15, 360);
this.groupBoxButtonBox.Name = "groupBoxButtonBox";
this.groupBoxButtonBox.Padding = new System.Windows.Forms.Padding(12, 10, 12, 12);
this.groupBoxButtonBox.Size = new System.Drawing.Size(450, 250);
this.groupBoxButtonBox.TabIndex = 1;
this.groupBoxButtonBox.TabStop = false;
this.groupBoxButtonBox.Text = "按钮盒信息";
//
// btnSaveButtonBox
//
this.btnSaveButtonBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(122)))), ((int)(((byte)(204)))));
this.btnSaveButtonBox.FlatAppearance.BorderSize = 0;
this.btnSaveButtonBox.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(92)))), ((int)(((byte)(153)))));
this.btnSaveButtonBox.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(102)))), ((int)(((byte)(170)))));
this.btnSaveButtonBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSaveButtonBox.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnSaveButtonBox.ForeColor = System.Drawing.Color.White;
this.btnSaveButtonBox.Location = new System.Drawing.Point(330, 200);
this.btnSaveButtonBox.Name = "btnSaveButtonBox";
this.btnSaveButtonBox.Size = new System.Drawing.Size(100, 38);
this.btnSaveButtonBox.TabIndex = 10;
this.btnSaveButtonBox.Text = "保存";
this.btnSaveButtonBox.UseVisualStyleBackColor = false;
this.btnSaveButtonBox.Click += new System.EventHandler(this.btnSaveButtonBox_Click);
//
// btnDeleteButtonBox
//
this.btnDeleteButtonBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(53)))), ((int)(((byte)(69)))));
this.btnDeleteButtonBox.FlatAppearance.BorderSize = 0;
this.btnDeleteButtonBox.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(165)))), ((int)(((byte)(40)))), ((int)(((byte)(52)))));
this.btnDeleteButtonBox.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(187)))), ((int)(((byte)(45)))), ((int)(((byte)(59)))));
this.btnDeleteButtonBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDeleteButtonBox.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnDeleteButtonBox.ForeColor = System.Drawing.Color.White;
this.btnDeleteButtonBox.Location = new System.Drawing.Point(220, 200);
this.btnDeleteButtonBox.Name = "btnDeleteButtonBox";
this.btnDeleteButtonBox.Size = new System.Drawing.Size(100, 38);
this.btnDeleteButtonBox.TabIndex = 9;
this.btnDeleteButtonBox.Text = "删除";
this.btnDeleteButtonBox.UseVisualStyleBackColor = false;
this.btnDeleteButtonBox.Click += new System.EventHandler(this.btnDeleteButtonBox_Click);
//
// btnAddButtonBox
//
this.btnAddButtonBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(167)))), ((int)(((byte)(69)))));
this.btnAddButtonBox.FlatAppearance.BorderSize = 0;
this.btnAddButtonBox.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(125)))), ((int)(((byte)(52)))));
this.btnAddButtonBox.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(136)))), ((int)(((byte)(56)))));
this.btnAddButtonBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnAddButtonBox.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnAddButtonBox.ForeColor = System.Drawing.Color.White;
this.btnAddButtonBox.Location = new System.Drawing.Point(110, 200);
this.btnAddButtonBox.Name = "btnAddButtonBox";
this.btnAddButtonBox.Size = new System.Drawing.Size(100, 38);
this.btnAddButtonBox.TabIndex = 8;
this.btnAddButtonBox.Text = "添加";
this.btnAddButtonBox.UseVisualStyleBackColor = false;
this.btnAddButtonBox.Click += new System.EventHandler(this.btnAddButtonBox_Click);
//
// labelType
//
this.labelType.AutoSize = true;
this.labelType.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelType.Location = new System.Drawing.Point(28, 168);
this.labelType.Name = "labelType";
this.labelType.Size = new System.Drawing.Size(65, 24);
this.labelType.TabIndex = 7;
this.labelType.Text = "类型:";
//
// comboBoxType
//
this.comboBoxType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBoxType.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.comboBoxType.FormattingEnabled = true;
this.comboBoxType.Location = new System.Drawing.Point(110, 165);
this.comboBoxType.Name = "comboBoxType";
this.comboBoxType.Size = new System.Drawing.Size(320, 32);
this.comboBoxType.TabIndex = 6;
//
// labelBoxIndex
//
this.labelBoxIndex.AutoSize = true;
this.labelBoxIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelBoxIndex.Location = new System.Drawing.Point(28, 48);
this.labelBoxIndex.Name = "labelBoxIndex";
this.labelBoxIndex.Size = new System.Drawing.Size(65, 24);
this.labelBoxIndex.TabIndex = 1;
this.labelBoxIndex.Text = "编码:";
//
// textBoxBoxIndex
//
this.textBoxBoxIndex.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxBoxIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxBoxIndex.Location = new System.Drawing.Point(110, 45);
this.textBoxBoxIndex.Name = "textBoxBoxIndex";
this.textBoxBoxIndex.Size = new System.Drawing.Size(320, 30);
this.textBoxBoxIndex.TabIndex = 0;
//
// labelIp
//
this.labelIp.AutoSize = true;
this.labelIp.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelIp.Location = new System.Drawing.Point(18, 88);
this.labelIp.Name = "labelIp";
this.labelIp.Size = new System.Drawing.Size(85, 24);
this.labelIp.TabIndex = 3;
this.labelIp.Text = "IP地址:";
//
// textBoxIp
//
this.textBoxIp.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxIp.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxIp.Location = new System.Drawing.Point(110, 85);
this.textBoxIp.Name = "textBoxIp";
this.textBoxIp.Size = new System.Drawing.Size(320, 30);
this.textBoxIp.TabIndex = 2;
this.textBoxIp.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(255)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))));
//
// labelPort
//
this.labelPort.AutoSize = true;
this.labelPort.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelPort.Location = new System.Drawing.Point(28, 128);
this.labelPort.Name = "labelPort";
this.labelPort.Size = new System.Drawing.Size(65, 24);
this.labelPort.TabIndex = 5;
this.labelPort.Text = "端口:";
//
// textBoxPort
//
this.textBoxPort.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxPort.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxPort.Location = new System.Drawing.Point(110, 125);
this.textBoxPort.Name = "textBoxPort";
this.textBoxPort.Size = new System.Drawing.Size(320, 30);
this.textBoxPort.TabIndex = 4;
//
// buttonListView
//
this.buttonListView.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.buttonListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.buttonListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeaderButtonIndex,
this.columnHeaderTriggerState,
this.columnHeaderTriggerDelay,
this.columnHeaderTriggerMission,
this.columnHeaderTriggerMethod,
this.columnHeaderTriggerMethodParams});
this.buttonListView.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.buttonListView.FullRowSelect = true;
this.buttonListView.GridLines = true;
this.buttonListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.buttonListView.HideSelection = false;
this.buttonListView.Location = new System.Drawing.Point(483, 55);
this.buttonListView.MultiSelect = false;
this.buttonListView.Name = "buttonListView";
this.buttonListView.OwnerDraw = true;
this.buttonListView.Size = new System.Drawing.Size(700, 290);
this.buttonListView.TabIndex = 2;
this.buttonListView.UseCompatibleStateImageBehavior = false;
this.buttonListView.View = System.Windows.Forms.View.Details;
this.buttonListView.SelectedIndexChanged += new System.EventHandler(this.buttonListView_SelectedIndexChanged);
//
// columnHeaderButtonIndex
//
this.columnHeaderButtonIndex.Text = "编码";
this.columnHeaderButtonIndex.Width = 70;
//
// columnHeaderTriggerState
//
this.columnHeaderTriggerState.Text = "触发状态";
this.columnHeaderTriggerState.Width = 100;
//
// columnHeaderTriggerDelay
//
this.columnHeaderTriggerDelay.Text = "触发延迟";
this.columnHeaderTriggerDelay.Width = 90;
//
// columnHeaderTriggerMission
//
this.columnHeaderTriggerMission.Text = "触发任务";
this.columnHeaderTriggerMission.Width = 140;
//
// columnHeaderTriggerMethod
//
this.columnHeaderTriggerMethod.Text = "触发方法";
this.columnHeaderTriggerMethod.Width = 140;
//
// columnHeaderTriggerMethodParams
//
this.columnHeaderTriggerMethodParams.Text = "方法参数";
this.columnHeaderTriggerMethodParams.Width = 160;
//
// groupBoxButton
//
this.groupBoxButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.groupBoxButton.Controls.Add(this.btnSaveButton);
this.groupBoxButton.Controls.Add(this.btnDeleteButton);
this.groupBoxButton.Controls.Add(this.btnAddButton);
this.groupBoxButton.Controls.Add(this.labelTriggerMethodParams);
this.groupBoxButton.Controls.Add(this.textBoxTriggerMethodParams);
this.groupBoxButton.Controls.Add(this.labelTriggerMethod);
this.groupBoxButton.Controls.Add(this.textBoxTriggerMethod);
this.groupBoxButton.Controls.Add(this.labelTriggerMission);
this.groupBoxButton.Controls.Add(this.textBoxTriggerMission);
this.groupBoxButton.Controls.Add(this.labelButtonIndex);
this.groupBoxButton.Controls.Add(this.textBoxButtonIndex);
this.groupBoxButton.Controls.Add(this.labelTriggerState);
this.groupBoxButton.Controls.Add(this.comboBoxTriggerState);
this.groupBoxButton.Controls.Add(this.labelTriggerDelay);
this.groupBoxButton.Controls.Add(this.textBoxTriggerDelay);
this.groupBoxButton.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.groupBoxButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(68)))), ((int)(((byte)(68)))), ((int)(((byte)(68)))));
this.groupBoxButton.Location = new System.Drawing.Point(483, 360);
this.groupBoxButton.Name = "groupBoxButton";
this.groupBoxButton.Padding = new System.Windows.Forms.Padding(12, 10, 12, 12);
this.groupBoxButton.Size = new System.Drawing.Size(700, 250);
this.groupBoxButton.TabIndex = 3;
this.groupBoxButton.TabStop = false;
this.groupBoxButton.Text = "按钮信息";
//
// btnSaveButton
//
this.btnSaveButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(122)))), ((int)(((byte)(204)))));
this.btnSaveButton.FlatAppearance.BorderSize = 0;
this.btnSaveButton.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(92)))), ((int)(((byte)(153)))));
this.btnSaveButton.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(102)))), ((int)(((byte)(170)))));
this.btnSaveButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSaveButton.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnSaveButton.ForeColor = System.Drawing.Color.White;
this.btnSaveButton.Location = new System.Drawing.Point(580, 180);
this.btnSaveButton.Name = "btnSaveButton";
this.btnSaveButton.Size = new System.Drawing.Size(100, 38);
this.btnSaveButton.TabIndex = 13;
this.btnSaveButton.Text = "保存";
this.btnSaveButton.UseVisualStyleBackColor = false;
this.btnSaveButton.Click += new System.EventHandler(this.btnSaveButton_Click);
//
// btnDeleteButton
//
this.btnDeleteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(53)))), ((int)(((byte)(69)))));
this.btnDeleteButton.FlatAppearance.BorderSize = 0;
this.btnDeleteButton.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(165)))), ((int)(((byte)(40)))), ((int)(((byte)(52)))));
this.btnDeleteButton.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(187)))), ((int)(((byte)(45)))), ((int)(((byte)(59)))));
this.btnDeleteButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDeleteButton.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnDeleteButton.ForeColor = System.Drawing.Color.White;
this.btnDeleteButton.Location = new System.Drawing.Point(470, 180);
this.btnDeleteButton.Name = "btnDeleteButton";
this.btnDeleteButton.Size = new System.Drawing.Size(100, 38);
this.btnDeleteButton.TabIndex = 12;
this.btnDeleteButton.Text = "删除";
this.btnDeleteButton.UseVisualStyleBackColor = false;
this.btnDeleteButton.Click += new System.EventHandler(this.btnDeleteButton_Click);
//
// btnAddButton
//
this.btnAddButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(167)))), ((int)(((byte)(69)))));
this.btnAddButton.FlatAppearance.BorderSize = 0;
this.btnAddButton.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(125)))), ((int)(((byte)(52)))));
this.btnAddButton.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(136)))), ((int)(((byte)(56)))));
this.btnAddButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnAddButton.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnAddButton.ForeColor = System.Drawing.Color.White;
this.btnAddButton.Location = new System.Drawing.Point(360, 180);
this.btnAddButton.Name = "btnAddButton";
this.btnAddButton.Size = new System.Drawing.Size(100, 38);
this.btnAddButton.TabIndex = 11;
this.btnAddButton.Text = "添加";
this.btnAddButton.UseVisualStyleBackColor = false;
this.btnAddButton.Click += new System.EventHandler(this.btnAddButton_Click);
//
// labelTriggerMethodParams
//
this.labelTriggerMethodParams.AutoSize = true;
this.labelTriggerMethodParams.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTriggerMethodParams.Location = new System.Drawing.Point(370, 128);
this.labelTriggerMethodParams.Name = "labelTriggerMethodParams";
this.labelTriggerMethodParams.Size = new System.Drawing.Size(103, 24);
this.labelTriggerMethodParams.TabIndex = 11;
this.labelTriggerMethodParams.Text = "方法参数:";
//
// textBoxTriggerMethodParams
//
this.textBoxTriggerMethodParams.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxTriggerMethodParams.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxTriggerMethodParams.Location = new System.Drawing.Point(490, 125);
this.textBoxTriggerMethodParams.Name = "textBoxTriggerMethodParams";
this.textBoxTriggerMethodParams.Size = new System.Drawing.Size(190, 30);
this.textBoxTriggerMethodParams.TabIndex = 10;
//
// labelTriggerState
//
this.labelTriggerState.AutoSize = true;
this.labelTriggerState.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTriggerState.Location = new System.Drawing.Point(370, 48);
this.labelTriggerState.Name = "labelTriggerState";
this.labelTriggerState.Size = new System.Drawing.Size(103, 24);
this.labelTriggerState.TabIndex = 3;
this.labelTriggerState.Text = "触发状态:";
//
// comboBoxTriggerState
//
this.comboBoxTriggerState.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBoxTriggerState.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.comboBoxTriggerState.FormattingEnabled = true;
this.comboBoxTriggerState.Location = new System.Drawing.Point(490, 45);
this.comboBoxTriggerState.Name = "comboBoxTriggerState";
this.comboBoxTriggerState.Size = new System.Drawing.Size(190, 32);
this.comboBoxTriggerState.TabIndex = 2;
//
// labelTriggerDelay
//
this.labelTriggerDelay.AutoSize = true;
this.labelTriggerDelay.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTriggerDelay.Location = new System.Drawing.Point(28, 88);
this.labelTriggerDelay.Name = "labelTriggerDelay";
this.labelTriggerDelay.Size = new System.Drawing.Size(103, 24);
this.labelTriggerDelay.TabIndex = 5;
this.labelTriggerDelay.Text = "触发延迟:";
//
// textBoxTriggerDelay
//
this.textBoxTriggerDelay.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxTriggerDelay.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxTriggerDelay.Location = new System.Drawing.Point(150, 85);
this.textBoxTriggerDelay.Name = "textBoxTriggerDelay";
this.textBoxTriggerDelay.Size = new System.Drawing.Size(200, 30);
this.textBoxTriggerDelay.TabIndex = 4;
//
// labelTriggerMethod
//
this.labelTriggerMethod.AutoSize = true;
this.labelTriggerMethod.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTriggerMethod.Location = new System.Drawing.Point(28, 128);
this.labelTriggerMethod.Name = "labelTriggerMethod";
this.labelTriggerMethod.Size = new System.Drawing.Size(103, 24);
this.labelTriggerMethod.TabIndex = 9;
this.labelTriggerMethod.Text = "触发方法:";
//
// textBoxTriggerMethod
//
this.textBoxTriggerMethod.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxTriggerMethod.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxTriggerMethod.Location = new System.Drawing.Point(150, 125);
this.textBoxTriggerMethod.Name = "textBoxTriggerMethod";
this.textBoxTriggerMethod.Size = new System.Drawing.Size(200, 30);
this.textBoxTriggerMethod.TabIndex = 8;
//
// labelTriggerMission
//
this.labelTriggerMission.AutoSize = true;
this.labelTriggerMission.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTriggerMission.Location = new System.Drawing.Point(370, 88);
this.labelTriggerMission.Name = "labelTriggerMission";
this.labelTriggerMission.Size = new System.Drawing.Size(103, 24);
this.labelTriggerMission.TabIndex = 7;
this.labelTriggerMission.Text = "触发任务:";
//
// textBoxTriggerMission
//
this.textBoxTriggerMission.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxTriggerMission.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxTriggerMission.Location = new System.Drawing.Point(490, 85);
this.textBoxTriggerMission.Name = "textBoxTriggerMission";
this.textBoxTriggerMission.Size = new System.Drawing.Size(190, 30);
this.textBoxTriggerMission.TabIndex = 6;
//
// labelButtonIndex
//
this.labelButtonIndex.AutoSize = true;
this.labelButtonIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelButtonIndex.Location = new System.Drawing.Point(28, 48);
this.labelButtonIndex.Name = "labelButtonIndex";
this.labelButtonIndex.Size = new System.Drawing.Size(65, 24);
this.labelButtonIndex.TabIndex = 1;
this.labelButtonIndex.Text = "编码:";
//
// textBoxButtonIndex
//
this.textBoxButtonIndex.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxButtonIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxButtonIndex.Location = new System.Drawing.Point(150, 45);
this.textBoxButtonIndex.Name = "textBoxButtonIndex";
this.textBoxButtonIndex.Size = new System.Drawing.Size(200, 30);
this.textBoxButtonIndex.TabIndex = 0;
//
// labelTitle
//
this.labelTitle.AutoSize = true;
this.labelTitle.Font = new System.Drawing.Font("微软雅黑", 16F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTitle.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.labelTitle.Location = new System.Drawing.Point(15, 12);
this.labelTitle.Name = "labelTitle";
this.labelTitle.Size = new System.Drawing.Size(150, 42);
this.labelTitle.TabIndex = 4;
this.labelTitle.Text = "按钮盒管理";
//
// ButtonBoxManager
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(245)))), ((int)(((byte)(245)))), ((int)(((byte)(247)))));
this.ClientSize = new System.Drawing.Size(1200, 620);
this.Controls.Add(this.labelTitle);
this.Controls.Add(this.groupBoxButton);
this.Controls.Add(this.buttonListView);
this.Controls.Add(this.groupBoxButtonBox);
this.Controls.Add(this.buttonBoxListView);
this.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.MinimumSize = new System.Drawing.Size(1200, 620);
this.Name = "ButtonBoxManager";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "按钮盒管理";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ButtonBoxManager_FormClosing);
this.Load += new System.EventHandler(this.ButtonBoxManager_Load);
this.groupBoxButtonBox.ResumeLayout(false);
this.groupBoxButtonBox.PerformLayout();
this.groupBoxButton.ResumeLayout(false);
this.groupBoxButton.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ListView buttonBoxListView;
private System.Windows.Forms.ColumnHeader columnHeaderBoxIndex;
private System.Windows.Forms.ColumnHeader columnHeaderIp;
private System.Windows.Forms.ColumnHeader columnHeaderPort;
private System.Windows.Forms.ColumnHeader columnHeaderType;
private System.Windows.Forms.GroupBox groupBoxButtonBox;
private System.Windows.Forms.TextBox textBoxIp;
private System.Windows.Forms.Label labelIp;
private System.Windows.Forms.Label labelPort;
private System.Windows.Forms.TextBox textBoxPort;
private System.Windows.Forms.Label labelBoxIndex;
private System.Windows.Forms.TextBox textBoxBoxIndex;
private System.Windows.Forms.Label labelType;
private System.Windows.Forms.ComboBox comboBoxType;
private System.Windows.Forms.Button btnAddButtonBox;
private System.Windows.Forms.Button btnDeleteButtonBox;
private System.Windows.Forms.Button btnSaveButtonBox;
private System.Windows.Forms.ListView buttonListView;
private System.Windows.Forms.ColumnHeader columnHeaderButtonIndex;
private System.Windows.Forms.ColumnHeader columnHeaderTriggerMission;
private System.Windows.Forms.ColumnHeader columnHeaderTriggerMethod;
private System.Windows.Forms.ColumnHeader columnHeaderTriggerMethodParams;
private System.Windows.Forms.GroupBox groupBoxButton;
private System.Windows.Forms.Label labelButtonIndex;
private System.Windows.Forms.TextBox textBoxButtonIndex;
private System.Windows.Forms.Label labelTriggerMission;
private System.Windows.Forms.TextBox textBoxTriggerMission;
private System.Windows.Forms.Label labelTriggerMethod;
private System.Windows.Forms.TextBox textBoxTriggerMethod;
private System.Windows.Forms.Label labelTriggerMethodParams;
private System.Windows.Forms.TextBox textBoxTriggerMethodParams;
private System.Windows.Forms.Label labelTriggerState;
private System.Windows.Forms.ComboBox comboBoxTriggerState;
private System.Windows.Forms.Label labelTriggerDelay;
private System.Windows.Forms.TextBox textBoxTriggerDelay;
private System.Windows.Forms.ColumnHeader columnHeaderTriggerState;
private System.Windows.Forms.ColumnHeader columnHeaderTriggerDelay;
private System.Windows.Forms.Button btnAddButton;
private System.Windows.Forms.Button btnDeleteButton;
private System.Windows.Forms.Button btnSaveButton;
private System.Windows.Forms.Label labelTitle;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.ExtendDevice.ButtonBox
{
/// <summary>
/// 按钮盒模型
/// </summary>
public class ButtonBoxModel
{
public string Ip { get; set; } = string.Empty;
public int Port { get; set; } = 0;
public int Index { get; set; } = 0;
public string Type { get; set; } = string.Empty;
public List<ButtonModel> Buttons { get; set; } = new List<ButtonModel>();
}
/// <summary>
/// 按钮模型
/// </summary>
public class ButtonModel
{
public int Index { get; set; } = 0;
public string TriggerMission { get; set; } = string.Empty;
public string TriggerMethod { get; set; } = string.Empty;
public string TriggerMethodParams { get; set; } = string.Empty;
public string TriggerState { get; set; } = string.Empty;
public ushort TriggerDelay { get; set; } = 0;
}
}
@@ -0,0 +1,806 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using LessokajiWeaverUtilities.MagicAttributes;
using LessokajiWeaverUtilities.Utilities;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore;
using SimpleCore.Library;
using StandardScene;
using StandardScene.Utils;
namespace StandardScene.ExtendDevice.ButtonBox
{
[MissionType(Name = "按钮进程")]
[I18N.DocumentTranslation(Name = "ButtonMission",locale = "en")]
public class ButtonMission:Mission
{
private const string DataFileName = "ButtonBoxConfig.json";
private string _dataFilePath;
/// <summary>
/// 当前所有按钮盒实例列表
/// </summary>
private List<BasicButtonBox> _buttonBoxes = new List<BasicButtonBox>();
/// <summary>
/// 用于管理异步循环的取消令牌源
/// </summary>
private CancellationTokenSource _cancellationTokenSource;
/// <summary>
/// 保存监控配置与状态的后台任务,便于关闭时等待
/// </summary>
private Task _configTask;
private Task _stateTask;
/// <summary>
/// 同步锁,用于保护按钮盒列表的并发访问
/// </summary>
private readonly object _syncLock = new object();
[MethodMember(Name = "启动进程")]
[I18N.DocumentTranslation(Name = "Start Mission", locale = "en")]
public override void Execute()
{
// 设置数据文件路径
_dataFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DataFileName);
// 如果已经启动,先停止之前的循环
StopInternalAsync().GetAwaiter().GetResult();
// 创建新的取消令牌源
_cancellationTokenSource = new CancellationTokenSource();
// 启动异步循环
var token = _cancellationTokenSource.Token;
_configTask = Task.Run(async () => await MonitorButtonBoxConfigAsync(token), token);
_stateTask = Task.Run(async () => await MonitorButtonStatesAsync(token), token);
status.status = "Running";
}
/// <summary>
/// 异步监控按钮盒配置文件
/// </summary>
private async Task MonitorButtonBoxConfigAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
// 读取配置文件
var configButtonBoxes = LoadButtonBoxConfig();
// 同步按钮盒列表
SyncButtonBoxes(configButtonBoxes);
// 等待10秒
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
catch (OperationCanceledException)
{
// 正常取消,退出循环
break;
}
catch (Exception ex)
{
// 记录错误,但继续运行
Diagnosis.Log($"按钮盒配置监控错误: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
// 发生错误时等待5秒后重试
try
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
}
/// <summary>
/// 停止监控任务
/// </summary>
[MethodMember(Name = "停止进程")]
[I18N.DocumentTranslation(Name = "Stop Mission", locale = "en")]
public void Stop()
{
StopInternalAsync().GetAwaiter().GetResult();
status.status = "/";
}
/// <summary>
/// 取消并释放当前的取消令牌源
/// </summary>
private async Task StopInternalAsync()
{
var cts = Interlocked.Exchange(ref _cancellationTokenSource, null);
var configTask = Interlocked.Exchange(ref _configTask, null);
var stateTask = Interlocked.Exchange(ref _stateTask, null);
if (cts == null && configTask == null && stateTask == null)
{
return;
}
try
{
cts?.Cancel();
}
catch (ObjectDisposedException)
{
// 已释放,忽略
}
var runningTasks = new[] { configTask, stateTask }
.Where(t => t != null)
.ToArray();
if (runningTasks.Length > 0)
{
var aggregateTask = Task.WhenAll(runningTasks);
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
var completedTask = await Task.WhenAny(aggregateTask, timeoutTask).ConfigureAwait(false);
if (completedTask == timeoutTask)
{
Diagnosis.Log("停止按钮监控任务超时", "ButtonMission", true);
}
else
{
try
{
await aggregateTask.ConfigureAwait(false);
}
catch (Exception ex)
{
Diagnosis.Log($"停止按钮监控任务时发生异常: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
}
}
cts?.Dispose();
DisconnectAllButtonBoxes();
}
/// <summary>
/// 断开所有按钮盒连接
/// </summary>
private void DisconnectAllButtonBoxes()
{
List<BasicButtonBox> snapshot;
lock (_syncLock)
{
snapshot = _buttonBoxes.ToList();
}
foreach (var box in snapshot)
{
try
{
box.Disconnect();
}
catch (Exception ex)
{
Diagnosis.Log($"停止按钮盒失败: Index={box.Index}, Error={ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
}
}
/// <summary>
/// 监控按钮状态,用于触发按钮动作
/// </summary>
private async Task MonitorButtonStatesAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
List<(BasicButtonBox Box, ButtonModel Config)> snapshot;
lock (_syncLock)
{
snapshot = _buttonBoxes
.SelectMany(box => box.ButtonConfigs.Values.Select(cfg => (Box: box, Config: cfg)))
.ToList();
}
foreach (var (box, config) in snapshot)
{
if (box == null || config == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(config.TriggerMission) ||
string.IsNullOrWhiteSpace(config.TriggerMethod))
{
continue;
}
var desiredState = ButtonState.Pressed;
if (!string.IsNullOrWhiteSpace(config.TriggerState) &&
Enum.TryParse(config.TriggerState, out ButtonState parsedState))
{
desiredState = parsedState;
}
var currentState = box.GetButtonState(config.Index);
bool isActive = currentState == desiredState&&box.IsOnline;
int delay = config.TriggerDelay;
if (delay <= 0)
{
delay = 1;
}
int uniqueId = unchecked((box.Index << 16) ^ config.Index);
LadderLogic.TriggerOnce(isActive, delay*1000, () =>
{
ExecuteButtonAction(config, box);
}, uniqueId);
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Diagnosis.Log($"按钮状态监控错误: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
try
{
await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
}
}
/// <summary>
/// 加载按钮盒配置文件
/// </summary>
private List<ButtonBoxModel> LoadButtonBoxConfig()
{
try
{
if (File.Exists(_dataFilePath))
{
var jsonContent = File.ReadAllText(_dataFilePath, Encoding.UTF8);
if (!string.IsNullOrWhiteSpace(jsonContent))
{
var buttonBoxes = jsonContent.JsonTo<List<ButtonBoxModel>>();
return buttonBoxes ?? new List<ButtonBoxModel>();
}
}
}
catch (Exception ex)
{
Diagnosis.Log($"加载按钮盒配置文件失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
return new List<ButtonBoxModel>();
}
/// <summary>
/// 同步按钮盒列表,根据配置文件进行增删改
/// </summary>
private void SyncButtonBoxes(List<ButtonBoxModel> configButtonBoxes)
{
var boxesToAdd = new List<ButtonBoxModel>();
lock (_syncLock)
{
// 创建配置中的按钮盒索引字典,用于快速查找
var configDict = configButtonBoxes.ToDictionary(b => b.Index);
// 创建当前按钮盒索引字典
var currentDict = _buttonBoxes.ToDictionary(b => b.Index);
// 1. 删除:在配置中不存在的按钮盒
var toRemove = _buttonBoxes.Where(b => !configDict.ContainsKey(b.Index)).ToList();
foreach (var buttonBox in toRemove)
{
try
{
// 断开连接
buttonBox.Disconnect();
_buttonBoxes.Remove(buttonBox);
Diagnosis.Post($"删除按钮盒: Index={buttonBox.Index}, IP={buttonBox.Ip}", "ButtonMission", true);
}
catch (Exception ex)
{
Diagnosis.Log($"删除按钮盒失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
}
// 2. 添加和修改:遍历配置中的按钮盒
foreach (var configBox in configButtonBoxes)
{
if (currentDict.TryGetValue(configBox.Index, out var existingBox))
{
// 修改:检查是否需要更新
if (ShouldUpdateButtonBox(existingBox, configBox))
{
try
{
UpdateButtonBox(existingBox, configBox);
Diagnosis.Post($"更新按钮盒: Index={configBox.Index}, IP={configBox.Ip}, Type={configBox.Type}", "ButtonMission", true);
}
catch (Exception ex)
{
Diagnosis.Log($"更新按钮盒失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
}
}
else
{
boxesToAdd.Add(configBox);
}
}
}
foreach (var configBox in boxesToAdd)
{
try
{
var newBox = CreateButtonBoxInstance(configBox);
if (newBox != null)
{
lock (_syncLock)
{
_buttonBoxes.Add(newBox);
}
Diagnosis.Post($"添加按钮盒: Index={configBox.Index}, IP={configBox.Ip}, Type={configBox.Type}", "ButtonMission", true);
}
else
{
Diagnosis.Log($"无法创建按钮盒实例: Index={configBox.Index}, Type={configBox.Type}", "ButtonMission", true);
}
}
catch (Exception ex)
{
Diagnosis.Log($"添加按钮盒失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
}
}
/// <summary>
/// 判断是否需要更新按钮盒
/// </summary>
private bool ShouldUpdateButtonBox(BasicButtonBox existingBox, ButtonBoxModel configBox)
{
// 检查基本属性是否变更
if (existingBox.Ip != configBox.Ip
|| existingBox.Port != configBox.Port
|| existingBox.GetType().Name != configBox.Type)
{
return true;
}
// 检查按钮信息是否变更
return HasButtonConfigsChanged(existingBox, configBox);
}
/// <summary>
/// 检查按钮配置信息是否变更
/// </summary>
private bool HasButtonConfigsChanged(BasicButtonBox existingBox, ButtonBoxModel configBox)
{
var configButtons = configBox.Buttons ?? new List<ButtonModel>();
var configDict = configButtons.ToDictionary(b => b.Index);
var existingDict = existingBox.ButtonConfigs;
// 检查按钮数量是否变化
if (existingDict.Count != configDict.Count)
{
return true;
}
// 检查每个按钮的配置是否变化
foreach (var configButton in configButtons)
{
if (!existingDict.TryGetValue(configButton.Index, out var existingButton))
{
// 新增了按钮
return true;
}
// 检查按钮配置是否变化
if (existingButton.TriggerMission != configButton.TriggerMission
|| existingButton.TriggerMethod != configButton.TriggerMethod
|| existingButton.TriggerMethodParams != configButton.TriggerMethodParams
|| existingButton.TriggerState != configButton.TriggerState
|| existingButton.TriggerDelay != configButton.TriggerDelay)
{
return true;
}
}
// 检查是否有按钮被删除
foreach (var existingKey in existingDict.Keys)
{
if (!configDict.ContainsKey(existingKey))
{
return true;
}
}
return false;
}
/// <summary>
/// 更新按钮盒属性
/// </summary>
private void UpdateButtonBox(BasicButtonBox buttonBox, ButtonBoxModel configBox)
{
// 如果类型改变,需要重新创建实例
if (buttonBox.GetType().Name != configBox.Type)
{
// 断开旧连接
buttonBox.Disconnect();
// 从列表中移除
_buttonBoxes.Remove(buttonBox);
// 创建新实例
var newBox = CreateButtonBoxInstance(configBox);
if (newBox != null)
{
_buttonBoxes.Add(newBox);
}
}
else
{
// 只更新属性
bool needReconnect = buttonBox.Ip != configBox.Ip || buttonBox.Port != configBox.Port;
buttonBox.Ip = configBox.Ip;
buttonBox.Port = configBox.Port;
// 更新按钮配置信息
buttonBox.UpdateButtonConfigs(configBox.Buttons);
// 初始化按钮状态(基于配置中的按钮索引)
var buttonIndices = configBox.Buttons?.Select(b => b.Index).ToList() ?? new List<int>();
buttonBox.InitializeButtons(buttonIndices);
// 如果IP或端口改变,需要重新连接
if (needReconnect)
{
buttonBox.Disconnect();
buttonBox.Connect();
}
}
}
/// <summary>
/// 通过类型字符串创建按钮盒实例
/// </summary>
private BasicButtonBox CreateButtonBoxInstance(ButtonBoxModel configBox)
{
if (string.IsNullOrWhiteSpace(configBox.Type))
{
return null;
}
try
{
// 获取当前命名空间下所有继承自BasicButtonBox的类
// 跨程序集发现:按钮盒具体类型可能位于卫星插件 dllStandardScene.Devices.ButtonBox),
// 用内核同款全域类型发现替代仅扫当前程序集的 GetExecutingAssembly。
var buttonBoxType = SimpleLite.Utils.UiTypeDiscovery.AllTypes()
.FirstOrDefault(t => t.IsClass
&& !t.IsAbstract
&& t.Namespace == typeof(BasicButtonBox).Namespace
&& t.IsSubclassOf(typeof(BasicButtonBox))
&& t.Name == configBox.Type);
if (buttonBoxType == null)
{
Diagnosis.Log($"未找到按钮盒类型: {configBox.Type}", "ButtonMission", true);
return null;
}
// 使用反射创建实例
var instance = (BasicButtonBox)Activator.CreateInstance(buttonBoxType);
// 设置属性
instance.Index = configBox.Index;
instance.Ip = configBox.Ip;
instance.Port = configBox.Port;
// 初始化按钮配置信息
instance.InitializeButtonConfigs(configBox.Buttons);
// 初始化按钮状态(基于配置中的按钮索引)
var buttonIndices = configBox.Buttons?.Select(b => b.Index).ToList() ?? new List<int>();
instance.InitializeButtons(buttonIndices);
// 自动连接
instance.Connect();
return instance;
}
catch (Exception ex)
{
Diagnosis.Log($"创建按钮盒实例失败: Type={configBox.Type}, Error={ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
return null;
}
}
/// <summary>
/// 获取当前所有按钮盒实例(只读)
/// </summary>
public IReadOnlyList<BasicButtonBox> GetButtonBoxes()
{
lock (_syncLock)
{
return _buttonBoxes.ToList().AsReadOnly();
}
}
/// <summary>
/// 执行按钮动作(在独立线程中异步执行,避免阻塞按钮监控循环)
/// </summary>
private void ExecuteButtonAction(ButtonModel buttonConfig, BasicButtonBox buttonBox)
{
Task.Run(() => ExecuteButtonActionInternal(buttonConfig, buttonBox));
}
/// <summary>
/// 实际执行业务方法的内部逻辑,包含成功/失败反馈。
/// </summary>
private void ExecuteButtonActionInternal(ButtonModel buttonConfig, BasicButtonBox buttonBox)
{
var success = false;
try
{
// 按钮动作执行后清零对应按钮寄存器(具体盒型按需重写,默认空实现)
buttonBox.ClearButtonRegister(buttonConfig.Index);
if (string.IsNullOrWhiteSpace(buttonConfig.TriggerMission) ||
string.IsNullOrWhiteSpace(buttonConfig.TriggerMethod))
{
// 配置不完整,直接反馈失败
Diagnosis.Log("按钮配置缺少 TriggerMission 或 TriggerMethod,无法执行动作", "ButtonMission", true);
return;
}
var mission = SimpleProject.proj?.Missions?
.FirstOrDefault(m => m.GetType().Name == buttonConfig.TriggerMission || m.name == buttonConfig.TriggerMission);
if (mission == null)
{
Diagnosis.Log($"未找到触发任务: {buttonConfig.TriggerMission}", "ButtonMission", true);
return;
}
var method = mission.GetType().GetMethod(buttonConfig.TriggerMethod,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (method == null)
{
Diagnosis.Log($"任务 {buttonConfig.TriggerMission} 中未找到方法 {buttonConfig.TriggerMethod}", "ButtonMission", true);
return;
}
var parameters = ParseMethodParameters(buttonConfig.TriggerMethodParams, method);
if (method.IsStatic)
{
var result = method.Invoke(null, parameters);
success = HandleMethodResult(result);
}
else
{
var result = method.Invoke(mission, parameters);
success = HandleMethodResult(result);
}
}
catch (Exception ex)
{
Diagnosis.Log($"执行按钮动作失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
}
finally
{
// 业务方法执行完成后,回调按钮盒进行反馈(如灯光、蜂鸣等)
try
{
buttonBox.OnActionExecuted(buttonConfig, success);
}
catch (Exception feedbackEx)
{
Diagnosis.Log($"按钮盒执行反馈失败: {ExceptionFormatter.FormatEx(feedbackEx)}", "ButtonMission", true);
}
}
}
/// <summary>
/// 处理反射调用结果:支持 Task/Task&lt;bool&gt; 等异步返回类型。
/// 返回 true 表示执行成功。
/// </summary>
private bool HandleMethodResult(object result)
{
try
{
switch (result)
{
case null:
return true;
case Task<bool> tb:
return tb.GetAwaiter().GetResult();
case Task t:
t.GetAwaiter().GetResult();
return true;
case bool b:
return b;
default:
return true;
}
}
catch (Exception ex)
{
Diagnosis.Log($"按钮动作方法异步执行失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
return false;
}
}
/// <summary>
/// 解析方法参数
/// </summary>
private object[] ParseMethodParameters(string paramsStr, MethodInfo methodInfo)
{
var paramInfos = methodInfo.GetParameters();
if (paramInfos.Length == 0)
{
return Array.Empty<object>();
}
if (string.IsNullOrWhiteSpace(paramsStr))
{
return paramInfos.Select(p => p.HasDefaultValue ? p.DefaultValue : GetDefaultValue(p.ParameterType)).ToArray();
}
try
{
var paramStrings = paramsStr.Split(',');
var parameters = new List<object>();
for (int i = 0; i < paramInfos.Length; i++)
{
var paramInfo = paramInfos[i];
var paramType = paramInfo.ParameterType;
if (i < paramStrings.Length)
{
var trimmed = paramStrings[i].Trim();
parameters.Add(ConvertParameter(trimmed, paramType));
}
else
{
parameters.Add(paramInfo.HasDefaultValue ? paramInfo.DefaultValue : GetDefaultValue(paramType));
}
}
return parameters.ToArray();
}
catch (Exception ex)
{
Diagnosis.Log($"解析按钮参数失败: {ExceptionFormatter.FormatEx(ex)}", "ButtonMission", true);
return paramInfos.Select(p => p.HasDefaultValue ? p.DefaultValue : GetDefaultValue(p.ParameterType)).ToArray();
}
}
/// <summary>
/// 转换参数
/// </summary>
private object ConvertParameter(string value, Type targetType)
{
if (targetType == typeof(string))
{
return value;
}
if (targetType == typeof(int) || targetType == typeof(int?))
{
return int.TryParse(value, out int result) ? result : (targetType == typeof(int?) ? (int?)null : 0);
}
if (targetType == typeof(double) || targetType == typeof(double?))
{
return double.TryParse(value, out double result) ? result : (targetType == typeof(double?) ? (double?)null : 0d);
}
if (targetType == typeof(float) || targetType == typeof(float?))
{
return float.TryParse(value, out float result) ? result : (targetType == typeof(float?) ? (float?)null : 0f);
}
if (targetType == typeof(bool) || targetType == typeof(bool?))
{
return bool.TryParse(value, out bool result) ? result : (targetType == typeof(bool?) ? (bool?)null : false);
}
if (targetType.IsEnum)
{
try
{
return Enum.Parse(targetType, value, true);
}
catch
{
return Enum.GetValues(targetType).GetValue(0);
}
}
return value;
}
/// <summary>
/// 获取类型默认值
/// </summary>
private object GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
/// <summary>
/// 打开按钮盒管理界面
/// </summary>
[MethodMember(Name = "打开管理界面")]
[I18N.DocumentTranslation(Name = "Open Manager", locale = "en")]
public static void OpenViewer()
{
try
{
var manager = ButtonBoxManager.Instance;
// 确保窗体没有被销毁
if (manager.IsDisposed)
{
// 如果窗体被销毁,单例会自动重新创建
manager = ButtonBoxManager.Instance;
}
if (manager.Visible)
{
// 如果界面已经可见,将其激活并置于最前
if (manager.WindowState == FormWindowState.Minimized)
{
manager.WindowState = FormWindowState.Normal;
}
manager.Activate();
manager.BringToFront();
}
else
{
// 如果界面不可见,显示它
manager.Show();
manager.Activate();
}
}
catch (Exception ex)
{
MessageBox.Show($"打开按钮盒管理界面失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
@@ -0,0 +1,278 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleCore.Library;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 门状态枚举
/// </summary>
public enum DoorState
{
/// <summary>
/// 关闭
/// </summary>
Closed = 0,
/// <summary>
/// 打开
/// </summary>
Open = 1,
/// <summary>
/// 未知状态
/// </summary>
Unknown = 2
}
/// <summary>
/// 门控制器状态枚举
/// </summary>
public enum DoorControllerState
{
/// <summary>
/// 离线
/// </summary>
Offline = 0,
/// <summary>
/// 在线
/// </summary>
Online = 1,
/// <summary>
/// 连接中
/// </summary>
Connecting = 2,
/// <summary>
/// 错误
/// </summary>
Error = 3
}
/// <summary>
/// 基础门控制器类
/// </summary>
public abstract class BasicDoorController
{
/// <summary>
/// 控制器索引
/// </summary>
public int Index { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; } = 502;
/// <summary>
/// 控制器状态
/// </summary>
public DoorControllerState State { get; protected set; } = DoorControllerState.Offline;
/// <summary>
/// 是否在线
/// </summary>
public bool IsOnline => State == DoorControllerState.Online;
/// <summary>
/// 门状态字典,键为门索引
/// </summary>
public Dictionary<int, DoorState> DoorStates { get; protected set; } = new Dictionary<int, DoorState>();
/// <summary>
/// 门目标控制字典,键为门索引,值为期望的开关状态(true=打开,false=关闭)
/// 仅作为指令缓存,实际通信由具体门控制器内部线程完成
/// </summary>
public Dictionary<int, bool> DoorControlTargets { get; protected set; } = new Dictionary<int, bool>();
/// <summary>
/// 门配置信息字典,键为门索引
/// </summary>
public Dictionary<int, DoorModel> DoorConfigs { get; protected set; } = new Dictionary<int, DoorModel>();
/// <summary>
/// 最后更新时间
/// </summary>
public DateTime LastUpdateTime { get; protected set; } = DateTime.Now;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; protected set; } = string.Empty;
/// <summary>
/// 更新控制器状态
/// </summary>
public virtual void UpdateState(DoorControllerState newState, string errorMessage = "")
{
State = newState;
ErrorMessage = errorMessage;
LastUpdateTime = DateTime.Now;
}
/// <summary>
/// 更新门状态
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <param name="state">门状态</param>
public virtual void UpdateDoorState(int doorIndex, DoorState state)
{
if (!DoorStates.ContainsKey(doorIndex))
{
Diagnosis.Post($"门控制器{Index}不存在门{doorIndex}");
}
DoorStates[doorIndex] = state;
LastUpdateTime = DateTime.Now;
}
/// <summary>
/// 获取门状态
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <returns>门状态,如果不存在则返回Unknown</returns>
public virtual DoorState GetDoorState(int doorIndex)
{
return DoorStates.TryGetValue(doorIndex, out var state) ? state : DoorState.Unknown;
}
/// <summary>
/// 初始化门状态
/// </summary>
/// <param name="doorIndices">门索引列表</param>
public virtual void InitializeDoors(List<int> doorIndices)
{
DoorStates.Clear();
DoorControlTargets.Clear();
foreach (var index in doorIndices)
{
DoorStates[index] = DoorState.Closed;
DoorControlTargets[index] = false;
}
}
/// <summary>
/// 初始化门配置信息
/// </summary>
/// <param name="doorConfigs">门配置列表</param>
public virtual void InitializeDoorConfigs(List<DoorModel> doorConfigs)
{
DoorConfigs.Clear();
if (doorConfigs != null)
{
foreach (var config in doorConfigs)
{
DoorConfigs[config.Index] = new DoorModel
{
Index = config.Index,
ControlAddress = config.ControlAddress,
OpenStatusAddress = config.OpenStatusAddress,
NoControl = config.NoControl
};
}
}
}
/// <summary>
/// 更新门配置信息
/// </summary>
/// <param name="doorConfigs">门配置列表</param>
public virtual void UpdateDoorConfigs(List<DoorModel> doorConfigs)
{
if (doorConfigs == null)
{
DoorConfigs.Clear();
return;
}
// 创建配置字典
var configDict = doorConfigs.ToDictionary(d => d.Index);
// 删除配置中不存在的门
var toRemove = DoorConfigs.Keys.Where(k => !configDict.ContainsKey(k)).ToList();
foreach (var key in toRemove)
{
DoorConfigs.Remove(key);
}
// 添加或更新门配置
foreach (var config in doorConfigs)
{
DoorConfigs[config.Index] = new DoorModel
{
Index = config.Index,
ControlAddress = config.ControlAddress,
OpenStatusAddress = config.OpenStatusAddress,
NoControl = config.NoControl
};
}
}
/// <summary>
/// 获取门配置信息
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <returns>门配置信息,如果不存在则返回null</returns>
public virtual DoorModel GetDoorConfig(int doorIndex)
{
return DoorConfigs.TryGetValue(doorIndex, out var config) ? config : null;
}
/// <summary>
/// 设置门的目标控制状态(仅修改内存字段,不直接进行通信)
/// 实际的通信写入由具体门控制器在内部线程中根据该目标状态执行
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <param name="open">true=打开,false=关闭</param>
public virtual void SetDoorControlTarget(int doorIndex, bool open)
{
// 统一支持 NoControl:当门被配置为不允许发送任何控制指令时,
// 强制将目标置为 false,并避免为其它控制器留下“需要开门”的目标。
if (DoorConfigs.TryGetValue(doorIndex, out var cfg) && cfg != null && cfg.NoControl)
{
DoorControlTargets[doorIndex] = false;
LastUpdateTime = DateTime.Now;
return;
}
DoorControlTargets[doorIndex] = open;
LastUpdateTime = DateTime.Now;
}
/// <summary>
/// 连接门控制器
/// </summary>
public virtual void Connect()
{
UpdateState(DoorControllerState.Connecting);
}
/// <summary>
/// 断开连接
/// </summary>
public virtual void Disconnect()
{
UpdateState(DoorControllerState.Offline);
}
/// <summary>
/// 读取门状态(开到位信号)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <returns>门状态</returns>
public abstract bool ReadDoorState(int doorIndex);
/// <summary>
/// 写入门控制信号(开关控制)
/// </summary>
/// <param name="doorIndex">门索引</param>
/// <param name="open">true=打开,false=关闭</param>
public abstract void WriteDoorControl(int doorIndex, bool open);
}
}
@@ -0,0 +1,521 @@
namespace StandardScene.ExtendDevice.Door
{
partial class DoorManager
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.doorControllerListView = new System.Windows.Forms.ListView();
this.columnHeaderControllerIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderIp = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderPort = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBoxController = new System.Windows.Forms.GroupBox();
this.btnSaveController = new System.Windows.Forms.Button();
this.btnDeleteController = new System.Windows.Forms.Button();
this.btnAddController = new System.Windows.Forms.Button();
this.labelType = new System.Windows.Forms.Label();
this.comboBoxType = new System.Windows.Forms.ComboBox();
this.labelControllerIndex = new System.Windows.Forms.Label();
this.textBoxControllerIndex = new System.Windows.Forms.TextBox();
this.labelPort = new System.Windows.Forms.Label();
this.textBoxPort = new System.Windows.Forms.TextBox();
this.labelIp = new System.Windows.Forms.Label();
this.textBoxIp = new System.Windows.Forms.TextBox();
this.doorListView = new System.Windows.Forms.ListView();
this.columnHeaderDoorIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderControlAddress = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderOpenStatusAddress = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBoxDoor = new System.Windows.Forms.GroupBox();
this.btnSaveDoor = new System.Windows.Forms.Button();
this.btnDeleteDoor = new System.Windows.Forms.Button();
this.btnAddDoor = new System.Windows.Forms.Button();
this.labelOpenStatusAddress = new System.Windows.Forms.Label();
this.textBoxOpenStatusAddress = new System.Windows.Forms.TextBox();
this.labelControlAddress = new System.Windows.Forms.Label();
this.textBoxControlAddress = new System.Windows.Forms.TextBox();
this.labelDoorIndex = new System.Windows.Forms.Label();
this.textBoxDoorIndex = new System.Windows.Forms.TextBox();
this.labelTitle = new System.Windows.Forms.Label();
this.groupBoxController.SuspendLayout();
this.groupBoxDoor.SuspendLayout();
this.SuspendLayout();
//
// doorControllerListView
//
this.doorControllerListView.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.doorControllerListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.doorControllerListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeaderControllerIndex,
this.columnHeaderIp,
this.columnHeaderPort,
this.columnHeaderType});
this.doorControllerListView.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.doorControllerListView.FullRowSelect = true;
this.doorControllerListView.GridLines = true;
this.doorControllerListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.doorControllerListView.HideSelection = false;
this.doorControllerListView.Location = new System.Drawing.Point(15, 55);
this.doorControllerListView.MultiSelect = false;
this.doorControllerListView.Name = "doorControllerListView";
this.doorControllerListView.OwnerDraw = true;
this.doorControllerListView.Size = new System.Drawing.Size(450, 290);
this.doorControllerListView.TabIndex = 0;
this.doorControllerListView.UseCompatibleStateImageBehavior = false;
this.doorControllerListView.View = System.Windows.Forms.View.Details;
this.doorControllerListView.SelectedIndexChanged += new System.EventHandler(this.doorControllerListView_SelectedIndexChanged);
//
// columnHeaderControllerIndex
//
this.columnHeaderControllerIndex.Text = "编码";
this.columnHeaderControllerIndex.Width = 70;
//
// columnHeaderIp
//
this.columnHeaderIp.Text = "IP地址";
this.columnHeaderIp.Width = 130;
//
// columnHeaderPort
//
this.columnHeaderPort.Text = "端口";
this.columnHeaderPort.Width = 90;
//
// columnHeaderType
//
this.columnHeaderType.Text = "类型";
this.columnHeaderType.Width = 140;
//
// groupBoxController
//
this.groupBoxController.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.groupBoxController.Controls.Add(this.btnSaveController);
this.groupBoxController.Controls.Add(this.btnDeleteController);
this.groupBoxController.Controls.Add(this.btnAddController);
this.groupBoxController.Controls.Add(this.labelType);
this.groupBoxController.Controls.Add(this.comboBoxType);
this.groupBoxController.Controls.Add(this.labelControllerIndex);
this.groupBoxController.Controls.Add(this.textBoxControllerIndex);
this.groupBoxController.Controls.Add(this.labelPort);
this.groupBoxController.Controls.Add(this.textBoxPort);
this.groupBoxController.Controls.Add(this.labelIp);
this.groupBoxController.Controls.Add(this.textBoxIp);
this.groupBoxController.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.groupBoxController.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(68)))), ((int)(((byte)(68)))), ((int)(((byte)(68)))));
this.groupBoxController.Location = new System.Drawing.Point(15, 360);
this.groupBoxController.Name = "groupBoxController";
this.groupBoxController.Padding = new System.Windows.Forms.Padding(12, 10, 12, 12);
this.groupBoxController.Size = new System.Drawing.Size(450, 250);
this.groupBoxController.TabIndex = 1;
this.groupBoxController.TabStop = false;
this.groupBoxController.Text = "门控制器信息";
//
// btnSaveController
//
this.btnSaveController.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(122)))), ((int)(((byte)(204)))));
this.btnSaveController.FlatAppearance.BorderSize = 0;
this.btnSaveController.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(92)))), ((int)(((byte)(153)))));
this.btnSaveController.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(102)))), ((int)(((byte)(170)))));
this.btnSaveController.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSaveController.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnSaveController.ForeColor = System.Drawing.Color.White;
this.btnSaveController.Location = new System.Drawing.Point(330, 200);
this.btnSaveController.Name = "btnSaveController";
this.btnSaveController.Size = new System.Drawing.Size(100, 38);
this.btnSaveController.TabIndex = 10;
this.btnSaveController.Text = "保存";
this.btnSaveController.UseVisualStyleBackColor = false;
this.btnSaveController.Click += new System.EventHandler(this.btnSaveController_Click);
//
// btnDeleteController
//
this.btnDeleteController.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(53)))), ((int)(((byte)(69)))));
this.btnDeleteController.FlatAppearance.BorderSize = 0;
this.btnDeleteController.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(165)))), ((int)(((byte)(40)))), ((int)(((byte)(52)))));
this.btnDeleteController.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(187)))), ((int)(((byte)(45)))), ((int)(((byte)(59)))));
this.btnDeleteController.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDeleteController.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnDeleteController.ForeColor = System.Drawing.Color.White;
this.btnDeleteController.Location = new System.Drawing.Point(220, 200);
this.btnDeleteController.Name = "btnDeleteController";
this.btnDeleteController.Size = new System.Drawing.Size(100, 38);
this.btnDeleteController.TabIndex = 9;
this.btnDeleteController.Text = "删除";
this.btnDeleteController.UseVisualStyleBackColor = false;
this.btnDeleteController.Click += new System.EventHandler(this.btnDeleteController_Click);
//
// btnAddController
//
this.btnAddController.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(167)))), ((int)(((byte)(69)))));
this.btnAddController.FlatAppearance.BorderSize = 0;
this.btnAddController.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(125)))), ((int)(((byte)(52)))));
this.btnAddController.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(136)))), ((int)(((byte)(56)))));
this.btnAddController.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnAddController.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnAddController.ForeColor = System.Drawing.Color.White;
this.btnAddController.Location = new System.Drawing.Point(110, 200);
this.btnAddController.Name = "btnAddController";
this.btnAddController.Size = new System.Drawing.Size(100, 38);
this.btnAddController.TabIndex = 8;
this.btnAddController.Text = "添加";
this.btnAddController.UseVisualStyleBackColor = false;
this.btnAddController.Click += new System.EventHandler(this.btnAddController_Click);
//
// labelType
//
this.labelType.AutoSize = true;
this.labelType.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelType.Location = new System.Drawing.Point(28, 168);
this.labelType.Name = "labelType";
this.labelType.Size = new System.Drawing.Size(65, 24);
this.labelType.TabIndex = 7;
this.labelType.Text = "类型:";
//
// comboBoxType
//
this.comboBoxType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBoxType.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.comboBoxType.FormattingEnabled = true;
this.comboBoxType.Location = new System.Drawing.Point(110, 165);
this.comboBoxType.Name = "comboBoxType";
this.comboBoxType.Size = new System.Drawing.Size(320, 32);
this.comboBoxType.TabIndex = 6;
//
// labelControllerIndex
//
this.labelControllerIndex.AutoSize = true;
this.labelControllerIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelControllerIndex.Location = new System.Drawing.Point(28, 48);
this.labelControllerIndex.Name = "labelControllerIndex";
this.labelControllerIndex.Size = new System.Drawing.Size(65, 24);
this.labelControllerIndex.TabIndex = 1;
this.labelControllerIndex.Text = "编码:";
//
// textBoxControllerIndex
//
this.textBoxControllerIndex.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxControllerIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxControllerIndex.Location = new System.Drawing.Point(110, 45);
this.textBoxControllerIndex.Name = "textBoxControllerIndex";
this.textBoxControllerIndex.Size = new System.Drawing.Size(320, 30);
this.textBoxControllerIndex.TabIndex = 0;
//
// labelIp
//
this.labelIp.AutoSize = true;
this.labelIp.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelIp.Location = new System.Drawing.Point(18, 88);
this.labelIp.Name = "labelIp";
this.labelIp.Size = new System.Drawing.Size(85, 24);
this.labelIp.TabIndex = 3;
this.labelIp.Text = "IP地址:";
//
// textBoxIp
//
this.textBoxIp.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxIp.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxIp.Location = new System.Drawing.Point(110, 85);
this.textBoxIp.Name = "textBoxIp";
this.textBoxIp.Size = new System.Drawing.Size(320, 30);
this.textBoxIp.TabIndex = 2;
this.textBoxIp.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(255)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))));
//
// labelPort
//
this.labelPort.AutoSize = true;
this.labelPort.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelPort.Location = new System.Drawing.Point(28, 128);
this.labelPort.Name = "labelPort";
this.labelPort.Size = new System.Drawing.Size(65, 24);
this.labelPort.TabIndex = 5;
this.labelPort.Text = "端口:";
//
// textBoxPort
//
this.textBoxPort.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxPort.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxPort.Location = new System.Drawing.Point(110, 125);
this.textBoxPort.Name = "textBoxPort";
this.textBoxPort.Size = new System.Drawing.Size(320, 30);
this.textBoxPort.TabIndex = 4;
//
// doorListView
//
this.doorListView.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.doorListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.doorListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeaderDoorIndex,
this.columnHeaderControlAddress,
this.columnHeaderOpenStatusAddress});
this.doorListView.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.doorListView.FullRowSelect = true;
this.doorListView.GridLines = true;
this.doorListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.doorListView.HideSelection = false;
this.doorListView.Location = new System.Drawing.Point(483, 55);
this.doorListView.MultiSelect = false;
this.doorListView.Name = "doorListView";
this.doorListView.OwnerDraw = true;
this.doorListView.Size = new System.Drawing.Size(500, 290);
this.doorListView.TabIndex = 2;
this.doorListView.UseCompatibleStateImageBehavior = false;
this.doorListView.View = System.Windows.Forms.View.Details;
this.doorListView.SelectedIndexChanged += new System.EventHandler(this.doorListView_SelectedIndexChanged);
//
// columnHeaderDoorIndex
//
this.columnHeaderDoorIndex.Text = "编码";
this.columnHeaderDoorIndex.Width = 100;
//
// columnHeaderControlAddress
//
this.columnHeaderControlAddress.Text = "控制地址";
this.columnHeaderControlAddress.Width = 180;
//
// columnHeaderOpenStatusAddress
//
this.columnHeaderOpenStatusAddress.Text = "开到位地址";
this.columnHeaderOpenStatusAddress.Width = 180;
//
// groupBoxDoor
//
this.groupBoxDoor.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.checkBoxNoControl = new System.Windows.Forms.CheckBox();
this.groupBoxDoor.Controls.Add(this.checkBoxNoControl);
this.groupBoxDoor.Controls.Add(this.btnSaveDoor);
this.groupBoxDoor.Controls.Add(this.btnDeleteDoor);
this.groupBoxDoor.Controls.Add(this.btnAddDoor);
this.groupBoxDoor.Controls.Add(this.labelOpenStatusAddress);
this.groupBoxDoor.Controls.Add(this.textBoxOpenStatusAddress);
this.groupBoxDoor.Controls.Add(this.labelControlAddress);
this.groupBoxDoor.Controls.Add(this.textBoxControlAddress);
this.groupBoxDoor.Controls.Add(this.labelDoorIndex);
this.groupBoxDoor.Controls.Add(this.textBoxDoorIndex);
this.groupBoxDoor.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.groupBoxDoor.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(68)))), ((int)(((byte)(68)))), ((int)(((byte)(68)))));
this.groupBoxDoor.Location = new System.Drawing.Point(483, 360);
this.groupBoxDoor.Name = "groupBoxDoor";
this.groupBoxDoor.Padding = new System.Windows.Forms.Padding(12, 10, 12, 12);
this.groupBoxDoor.Size = new System.Drawing.Size(500, 250);
this.groupBoxDoor.TabIndex = 3;
this.groupBoxDoor.TabStop = false;
this.groupBoxDoor.Text = "门信息";
//
// btnSaveDoor
//
this.btnSaveDoor.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(122)))), ((int)(((byte)(204)))));
this.btnSaveDoor.FlatAppearance.BorderSize = 0;
this.btnSaveDoor.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(92)))), ((int)(((byte)(153)))));
this.btnSaveDoor.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(102)))), ((int)(((byte)(170)))));
this.btnSaveDoor.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSaveDoor.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnSaveDoor.ForeColor = System.Drawing.Color.White;
this.btnSaveDoor.Location = new System.Drawing.Point(380, 200);
this.btnSaveDoor.Name = "btnSaveDoor";
this.btnSaveDoor.Size = new System.Drawing.Size(100, 38);
this.btnSaveDoor.TabIndex = 7;
this.btnSaveDoor.Text = "保存";
this.btnSaveDoor.UseVisualStyleBackColor = false;
this.btnSaveDoor.Click += new System.EventHandler(this.btnSaveDoor_Click);
//
// btnDeleteDoor
//
this.btnDeleteDoor.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(53)))), ((int)(((byte)(69)))));
this.btnDeleteDoor.FlatAppearance.BorderSize = 0;
this.btnDeleteDoor.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(165)))), ((int)(((byte)(40)))), ((int)(((byte)(52)))));
this.btnDeleteDoor.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(187)))), ((int)(((byte)(45)))), ((int)(((byte)(59)))));
this.btnDeleteDoor.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDeleteDoor.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnDeleteDoor.ForeColor = System.Drawing.Color.White;
this.btnDeleteDoor.Location = new System.Drawing.Point(270, 200);
this.btnDeleteDoor.Name = "btnDeleteDoor";
this.btnDeleteDoor.Size = new System.Drawing.Size(100, 38);
this.btnDeleteDoor.TabIndex = 6;
this.btnDeleteDoor.Text = "删除";
this.btnDeleteDoor.UseVisualStyleBackColor = false;
this.btnDeleteDoor.Click += new System.EventHandler(this.btnDeleteDoor_Click);
//
// btnAddDoor
//
this.btnAddDoor.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(167)))), ((int)(((byte)(69)))));
this.btnAddDoor.FlatAppearance.BorderSize = 0;
this.btnAddDoor.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(125)))), ((int)(((byte)(52)))));
this.btnAddDoor.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(136)))), ((int)(((byte)(56)))));
this.btnAddDoor.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnAddDoor.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnAddDoor.ForeColor = System.Drawing.Color.White;
this.btnAddDoor.Location = new System.Drawing.Point(160, 200);
this.btnAddDoor.Name = "btnAddDoor";
this.btnAddDoor.Size = new System.Drawing.Size(100, 38);
this.btnAddDoor.TabIndex = 5;
this.btnAddDoor.Text = "添加";
this.btnAddDoor.UseVisualStyleBackColor = false;
this.btnAddDoor.Click += new System.EventHandler(this.btnAddDoor_Click);
//
// labelOpenStatusAddress
//
this.labelOpenStatusAddress.AutoSize = true;
this.labelOpenStatusAddress.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelOpenStatusAddress.Location = new System.Drawing.Point(18, 128);
this.labelOpenStatusAddress.Name = "labelOpenStatusAddress";
this.labelOpenStatusAddress.Size = new System.Drawing.Size(103, 24);
this.labelOpenStatusAddress.TabIndex = 4;
this.labelOpenStatusAddress.Text = "开到位地址:";
//
// textBoxOpenStatusAddress
//
this.textBoxOpenStatusAddress.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxOpenStatusAddress.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxOpenStatusAddress.Location = new System.Drawing.Point(150, 125);
this.textBoxOpenStatusAddress.Name = "textBoxOpenStatusAddress";
this.textBoxOpenStatusAddress.Size = new System.Drawing.Size(330, 30);
this.textBoxOpenStatusAddress.TabIndex = 3;
//
// checkBoxNoControl
//
this.checkBoxNoControl.AutoSize = true;
this.checkBoxNoControl.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.checkBoxNoControl.Location = new System.Drawing.Point(150, 165);
this.checkBoxNoControl.Name = "checkBoxNoControl";
this.checkBoxNoControl.Size = new System.Drawing.Size(162, 28);
this.checkBoxNoControl.TabIndex = 4;
this.checkBoxNoControl.Text = "禁止门控发送指令";
this.checkBoxNoControl.UseVisualStyleBackColor = true;
//
// labelControlAddress
//
this.labelControlAddress.AutoSize = true;
this.labelControlAddress.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelControlAddress.Location = new System.Drawing.Point(18, 88);
this.labelControlAddress.Name = "labelControlAddress";
this.labelControlAddress.Size = new System.Drawing.Size(103, 24);
this.labelControlAddress.TabIndex = 2;
this.labelControlAddress.Text = "控制地址:";
//
// textBoxControlAddress
//
this.textBoxControlAddress.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxControlAddress.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxControlAddress.Location = new System.Drawing.Point(150, 85);
this.textBoxControlAddress.Name = "textBoxControlAddress";
this.textBoxControlAddress.Size = new System.Drawing.Size(330, 30);
this.textBoxControlAddress.TabIndex = 1;
//
// labelDoorIndex
//
this.labelDoorIndex.AutoSize = true;
this.labelDoorIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelDoorIndex.Location = new System.Drawing.Point(28, 48);
this.labelDoorIndex.Name = "labelDoorIndex";
this.labelDoorIndex.Size = new System.Drawing.Size(65, 24);
this.labelDoorIndex.TabIndex = 0;
this.labelDoorIndex.Text = "编码:";
//
// textBoxDoorIndex
//
this.textBoxDoorIndex.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.textBoxDoorIndex.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.textBoxDoorIndex.Location = new System.Drawing.Point(150, 45);
this.textBoxDoorIndex.Name = "textBoxDoorIndex";
this.textBoxDoorIndex.Size = new System.Drawing.Size(330, 30);
this.textBoxDoorIndex.TabIndex = 0;
//
// labelTitle
//
this.labelTitle.AutoSize = true;
this.labelTitle.Font = new System.Drawing.Font("微软雅黑", 16F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTitle.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.labelTitle.Location = new System.Drawing.Point(15, 12);
this.labelTitle.Name = "labelTitle";
this.labelTitle.Size = new System.Drawing.Size(150, 42);
this.labelTitle.TabIndex = 4;
this.labelTitle.Text = "门控制器管理";
//
// DoorManager
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(245)))), ((int)(((byte)(245)))), ((int)(((byte)(247)))));
this.ClientSize = new System.Drawing.Size(1000, 620);
this.Controls.Add(this.labelTitle);
this.Controls.Add(this.groupBoxDoor);
this.Controls.Add(this.doorListView);
this.Controls.Add(this.groupBoxController);
this.Controls.Add(this.doorControllerListView);
this.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.MinimumSize = new System.Drawing.Size(1000, 620);
this.Name = "DoorManager";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "门控制器管理";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DoorManager_FormClosing);
this.Load += new System.EventHandler(this.DoorManager_Load);
this.groupBoxController.ResumeLayout(false);
this.groupBoxController.PerformLayout();
this.groupBoxDoor.ResumeLayout(false);
this.groupBoxDoor.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ListView doorControllerListView;
private System.Windows.Forms.ColumnHeader columnHeaderControllerIndex;
private System.Windows.Forms.ColumnHeader columnHeaderIp;
private System.Windows.Forms.ColumnHeader columnHeaderPort;
private System.Windows.Forms.ColumnHeader columnHeaderType;
private System.Windows.Forms.GroupBox groupBoxController;
private System.Windows.Forms.TextBox textBoxIp;
private System.Windows.Forms.Label labelIp;
private System.Windows.Forms.Label labelPort;
private System.Windows.Forms.TextBox textBoxPort;
private System.Windows.Forms.Label labelControllerIndex;
private System.Windows.Forms.TextBox textBoxControllerIndex;
private System.Windows.Forms.Label labelType;
private System.Windows.Forms.ComboBox comboBoxType;
private System.Windows.Forms.Button btnAddController;
private System.Windows.Forms.Button btnDeleteController;
private System.Windows.Forms.Button btnSaveController;
private System.Windows.Forms.ListView doorListView;
private System.Windows.Forms.ColumnHeader columnHeaderDoorIndex;
private System.Windows.Forms.ColumnHeader columnHeaderControlAddress;
private System.Windows.Forms.ColumnHeader columnHeaderOpenStatusAddress;
private System.Windows.Forms.GroupBox groupBoxDoor;
private System.Windows.Forms.Label labelDoorIndex;
private System.Windows.Forms.TextBox textBoxDoorIndex;
private System.Windows.Forms.Label labelControlAddress;
private System.Windows.Forms.TextBox textBoxControlAddress;
private System.Windows.Forms.Label labelOpenStatusAddress;
private System.Windows.Forms.TextBox textBoxOpenStatusAddress;
private System.Windows.Forms.Button btnAddDoor;
private System.Windows.Forms.Button btnDeleteDoor;
private System.Windows.Forms.Button btnSaveDoor;
private System.Windows.Forms.Label labelTitle;
private System.Windows.Forms.CheckBox checkBoxNoControl;
}
}
@@ -0,0 +1,961 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using StandardScene.Utils;
namespace StandardScene.ExtendDevice.Door
{
public partial class DoorManager : Form
{
private static DoorManager _instance = null;
private static readonly object _lock = new object();
private const string DataFileName = "DoorConfig.json";
private string _dataFilePath;
private List<DoorControllerModel> _doorControllers = new List<DoorControllerModel>();
private DoorControllerModel _currentController = null;
private DoorModel _currentDoor = null;
private int _controllerHoverIndex = -1;
private int _doorHoverIndex = -1;
/// <summary>
/// 获取单例实例
/// </summary>
public static DoorManager Instance
{
get
{
if (_instance == null || _instance.IsDisposed)
{
lock (_lock)
{
if (_instance == null || _instance.IsDisposed)
{
_instance = new DoorManager();
}
}
}
return _instance;
}
}
/// <summary>
/// 私有构造函数,确保单例模式
/// </summary>
private DoorManager()
{
InitializeComponent();
// 设置数据文件路径
_dataFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DataFileName);
}
private void DoorManager_Load(object sender, EventArgs e)
{
// 设置ListView的视觉样式
SetupListViewStyles();
// 初始化类型下拉框
InitializeTypeComboBox();
LoadData();
RefreshControllerList();
}
/// <summary>
/// 初始化类型下拉框,显示 DoorTypeAttribute.Name
/// </summary>
private void InitializeTypeComboBox()
{
comboBoxType.Items.Clear();
try
{
// 获取当前命名空间下所有继承自BasicDoorController且带有DoorTypeAttribute特性的类
// 在插件/宿主环境下 GetExecutingAssembly 可能不是 StandardScene.dll
// 跨程序集发现:门控制器具体类型可能位于卫星插件 dllStandardScene.Devices.Door)。
var controllerTypes = SimpleLite.Utils.UiTypeDiscovery.AllTypes()
.Where(t => t.IsClass
&& !t.IsAbstract
&& t.Namespace == typeof(BasicDoorController).Namespace
&& t.IsSubclassOf(typeof(BasicDoorController))
&& t.IsDefined(typeof(DoorTypeAttribute), false))
.ToList();
var typeNames = new List<string>();
foreach (var type in controllerTypes)
{
var attr = type.GetCustomAttribute<DoorTypeAttribute>();
if (attr != null && !string.IsNullOrWhiteSpace(attr.Name))
{
typeNames.Add(attr.Name);
}
}
// 按名称排序
typeNames.Sort();
foreach (var typeName in typeNames)
{
comboBoxType.Items.Add(typeName);
}
// 如果没有找到任何类型,添加默认选项
if (comboBoxType.Items.Count == 0)
{
comboBoxType.Items.Add("ModbusDoorController");
}
}
catch (Exception ex)
{
MessageBox.Show($"初始化类型下拉框失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
comboBoxType.Items.Add("ModbusDoorController");
}
}
/// <summary>
/// 设置ListView的视觉样式
/// </summary>
private void SetupListViewStyles()
{
SetupListView(doorControllerListView,
ControllerListView_DrawItem,
ControllerListView_DrawSubItem,
ControllerListView_DrawColumnHeader,
ControllerListView_MouseMove,
ControllerListView_MouseLeave);
SetupListView(doorListView,
DoorListView_DrawItem,
DoorListView_DrawSubItem,
DoorListView_DrawColumnHeader,
DoorListView_MouseMove,
DoorListView_MouseLeave);
}
private void SetupListView(ListView listView,
DrawListViewItemEventHandler itemHandler,
DrawListViewSubItemEventHandler subItemHandler,
DrawListViewColumnHeaderEventHandler headerHandler,
MouseEventHandler mouseMoveHandler,
EventHandler mouseLeaveHandler)
{
listView.OwnerDraw = true;
listView.BackColor = Color.White;
listView.DrawItem += itemHandler;
listView.DrawSubItem += subItemHandler;
listView.DrawColumnHeader += headerHandler;
listView.MouseMove += mouseMoveHandler;
listView.MouseLeave += mouseLeaveHandler;
// 启用双缓冲
typeof(Control)?.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(listView, true, null);
}
private static readonly Color RowEvenColor = Color.FromArgb(250, 250, 252);
private static readonly Color RowOddColor = Color.White;
private static readonly Color RowHighlightColor = Color.FromArgb(230, 240, 255);
private static readonly Color TextRegularColor = Color.FromArgb(68, 68, 68);
private static readonly Color TextHighlightColor = Color.FromArgb(51, 51, 51);
private void ControllerListView_MouseMove(object sender, MouseEventArgs e)
{
UpdateHoverIndex(doorControllerListView, e, true);
}
private void ControllerListView_MouseLeave(object sender, EventArgs e)
{
ResetHoverIndex(doorControllerListView, true);
}
private void DoorListView_MouseMove(object sender, MouseEventArgs e)
{
UpdateHoverIndex(doorListView, e, false);
}
private void DoorListView_MouseLeave(object sender, EventArgs e)
{
ResetHoverIndex(doorListView, false);
}
private void UpdateHoverIndex(ListView listView, MouseEventArgs e, bool isControllerList)
{
var hoveredItem = listView.GetItemAt(e.X, e.Y);
int newIndex = hoveredItem?.Index ?? -1;
if (isControllerList)
{
if (_controllerHoverIndex != newIndex)
{
_controllerHoverIndex = newIndex;
listView.Invalidate();
}
}
else
{
if (_doorHoverIndex != newIndex)
{
_doorHoverIndex = newIndex;
listView.Invalidate();
}
}
}
private void ResetHoverIndex(ListView listView, bool isControllerList)
{
if (isControllerList)
{
if (_controllerHoverIndex != -1)
{
_controllerHoverIndex = -1;
listView.Invalidate();
}
}
else
{
if (_doorHoverIndex != -1)
{
_doorHoverIndex = -1;
listView.Invalidate();
}
}
}
private void ControllerListView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _controllerHoverIndex
|| (e.State & ListViewItemStates.Focused) != 0;
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
var textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
TextRenderer.DrawText(e.Graphics, e.Item.Text, e.Item.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
e.DrawFocusRectangle();
}
private void ControllerListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _controllerHoverIndex
|| (e.ItemState & ListViewItemStates.Focused) != 0;
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
var textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.SubItem.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
}
private void ControllerListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(245, 247, 250)), e.Bounds);
e.Graphics.DrawLine(new Pen(Color.FromArgb(220, 220, 220)),
e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
TextRenderer.DrawText(e.Graphics, e.Header.Text,
new Font("微软雅黑", 10.5F, FontStyle.Bold),
e.Bounds, Color.FromArgb(68, 68, 68),
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.HorizontalCenter);
}
private void DoorListView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _doorHoverIndex
|| (e.State & ListViewItemStates.Focused) != 0;
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
var textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
TextRenderer.DrawText(e.Graphics, e.Item.Text, e.Item.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
e.DrawFocusRectangle();
}
private void DoorListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _doorHoverIndex
|| (e.ItemState & ListViewItemStates.Focused) != 0;
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
var textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.SubItem.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
}
private void DoorListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(245, 247, 250)), e.Bounds);
e.Graphics.DrawLine(new Pen(Color.FromArgb(220, 220, 220)),
e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
TextRenderer.DrawText(e.Graphics, e.Header.Text,
new Font("微软雅黑", 10.5F, FontStyle.Bold),
e.Bounds, Color.FromArgb(68, 68, 68),
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.HorizontalCenter);
}
/// <summary>
/// 刷新门控制器列表
/// </summary>
private void RefreshControllerList()
{
doorControllerListView.Items.Clear();
foreach (var controller in _doorControllers)
{
var item = new ListViewItem(controller.Index.ToString());
item.SubItems.Add(controller.Ip);
item.SubItems.Add(controller.Port.ToString());
item.SubItems.Add(controller.Type);
item.Tag = controller;
item.UseItemStyleForSubItems = false;
doorControllerListView.Items.Add(item);
}
}
/// <summary>
/// 刷新门列表
/// </summary>
private void RefreshDoorList()
{
doorListView.Items.Clear();
if (_currentController != null)
{
foreach (var door in _currentController.Doors)
{
var item = new ListViewItem(door.Index.ToString());
item.SubItems.Add(door.ControlAddress.ToString());
item.SubItems.Add(door.OpenStatusAddress.ToString());
item.Tag = door;
item.UseItemStyleForSubItems = false;
doorListView.Items.Add(item);
}
}
}
/// <summary>
/// 门控制器列表选择改变
/// </summary>
private void doorControllerListView_SelectedIndexChanged(object sender, EventArgs e)
{
if (doorControllerListView.SelectedItems.Count > 0)
{
_currentController = doorControllerListView.SelectedItems[0].Tag as DoorControllerModel;
if (_currentController != null)
{
// 填充门控制器编辑区域
textBoxIp.Text = _currentController.Ip;
textBoxPort.Text = _currentController.Port.ToString();
textBoxControllerIndex.Text = _currentController.Index.ToString();
// 设置类型下拉框
if (comboBoxType.Items.Contains(_currentController.Type))
{
comboBoxType.SelectedItem = _currentController.Type;
}
else
{
comboBoxType.SelectedIndex = comboBoxType.Items.Count > 0 ? 0 : -1;
}
// 刷新门列表
RefreshDoorList();
}
}
else
{
_currentController = null;
ClearControllerFields();
doorListView.Items.Clear();
}
}
/// <summary>
/// 门列表选择改变
/// </summary>
private void doorListView_SelectedIndexChanged(object sender, EventArgs e)
{
if (doorListView.SelectedItems.Count > 0)
{
_currentDoor = doorListView.SelectedItems[0].Tag as DoorModel;
if (_currentDoor != null)
{
// 填充门编辑区域
textBoxDoorIndex.Text = _currentDoor.Index.ToString();
textBoxControlAddress.Text = _currentDoor.ControlAddress.ToString();
textBoxOpenStatusAddress.Text = _currentDoor.OpenStatusAddress.ToString();
checkBoxNoControl.Checked = _currentDoor.NoControl;
}
}
else
{
_currentDoor = null;
ClearDoorFields();
}
}
/// <summary>
/// 添加门控制器
/// </summary>
private void btnAddController_Click(object sender, EventArgs e)
{
try
{
string ip = textBoxIp.Text.Trim();
string portText = textBoxPort.Text.Trim();
string indexText = textBoxControllerIndex.Text.Trim();
string type = comboBoxType.SelectedItem?.ToString() ?? string.Empty;
int newIndex;
if (!string.IsNullOrWhiteSpace(indexText))
{
if (!int.TryParse(indexText, out newIndex))
{
MessageBox.Show("编码必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
else
{
newIndex = _doorControllers.Count > 0 ? _doorControllers.Max(c => c.Index) + 1 : 1;
}
string newIp;
if (!string.IsNullOrWhiteSpace(ip))
{
if (!IsValidIpAddress(ip))
{
MessageBox.Show("无效的IP地址,例如:192.168.1.100", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
newIp = ip;
}
else
{
newIp = "192.168.1.100";
}
int newPort;
if (!string.IsNullOrWhiteSpace(portText))
{
if (!int.TryParse(portText, out newPort))
{
MessageBox.Show("端口必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
else
{
newPort = 502;
}
string newType;
if (!string.IsNullOrWhiteSpace(type))
{
newType = type;
}
else
{
newType = comboBoxType.Items.Count > 0 ? comboBoxType.Items[0].ToString() : "ModbusDoorController";
}
// 检查编码是否重复
if (_doorControllers.Any(c => c.Index == newIndex))
{
MessageBox.Show($"编码 {newIndex} 已存在,请使用其他编码", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 检查IP地址是否重复
if (_doorControllers.Any(c => c.Ip == newIp))
{
MessageBox.Show($"IP地址 {newIp} 已存在,请使用其他IP地址", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var newController = new DoorControllerModel
{
Index = newIndex,
Ip = newIp,
Port = newPort,
Type = newType
};
_doorControllers.Add(newController);
RefreshControllerList();
SaveData();
// 选中新添加的门控制器
foreach (ListViewItem item in doorControllerListView.Items)
{
if (item.Tag == newController)
{
item.Selected = true;
item.EnsureVisible();
break;
}
}
}
catch (Exception ex)
{
MessageBox.Show($"添加门控制器失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 删除门控制器
/// </summary>
private void btnDeleteController_Click(object sender, EventArgs e)
{
if (_currentController == null)
{
MessageBox.Show("请选择要删除的门控制器", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var result = MessageBox.Show($"删除编码为 {_currentController.Index} 的门控制器?", "确认删除",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
_doorControllers.Remove(_currentController);
_currentController = null;
ClearControllerFields();
RefreshControllerList();
doorListView.Items.Clear();
SaveData();
}
}
/// <summary>
/// 保存门控制器
/// </summary>
private void btnSaveController_Click(object sender, EventArgs e)
{
if (_currentController == null)
{
MessageBox.Show("请选择要保存的门控制器", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
string newIp = textBoxIp.Text.Trim();
if (!IsValidIpAddress(newIp))
{
MessageBox.Show("无效的IP地址,例如:192.168.1.100", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!int.TryParse(textBoxPort.Text, out int port))
{
MessageBox.Show("端口必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_currentController.Port = port;
if (!int.TryParse(textBoxControllerIndex.Text, out int index))
{
MessageBox.Show("编码必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 检查编码是否重复(排除当前项)
if (_doorControllers.Any(c => c.Index == index && c != _currentController))
{
MessageBox.Show($"编码 {index} 已存在,请使用其他编码", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 检查IP地址是否重复(排除当前项)
if (_doorControllers.Any(c => c.Ip == newIp && c != _currentController))
{
MessageBox.Show($"IP地址 {newIp} 已存在,请使用其他IP地址", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_currentController.Index = index;
_currentController.Type = comboBoxType.SelectedItem?.ToString() ?? string.Empty;
_currentController.Ip = newIp;
RefreshControllerList();
SaveData();
MessageBox.Show("保存成功", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 添加门
/// </summary>
private void btnAddDoor_Click(object sender, EventArgs e)
{
if (_currentController == null)
{
MessageBox.Show("请先选择门控制器", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
string indexText = textBoxDoorIndex.Text.Trim();
string controlAddressText = textBoxControlAddress.Text.Trim();
string openStatusAddressText = textBoxOpenStatusAddress.Text.Trim();
int newIndex;
if (!string.IsNullOrWhiteSpace(indexText))
{
if (!int.TryParse(indexText, out newIndex))
{
MessageBox.Show("门编码必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
else
{
newIndex = _currentController.Doors.Count > 0
? _currentController.Doors.Max(d => d.Index) + 1
: 1;
}
// 检查门编码是否重复
if (_currentController.Doors.Any(d => d.Index == newIndex))
{
MessageBox.Show($"门编码 {newIndex} 已存在,请使用其他编码", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
ushort controlAddress = 0;
if (!string.IsNullOrWhiteSpace(controlAddressText))
{
if (!ushort.TryParse(controlAddressText, out controlAddress))
{
MessageBox.Show("控制地址必须是0-65535之间的数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
ushort openStatusAddress = 0;
if (!string.IsNullOrWhiteSpace(openStatusAddressText))
{
if (!ushort.TryParse(openStatusAddressText, out openStatusAddress))
{
MessageBox.Show("开到位地址必须是0-65535之间的数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
var newDoor = new DoorModel
{
Index = newIndex,
ControlAddress = controlAddress,
OpenStatusAddress = openStatusAddress,
NoControl = checkBoxNoControl.Checked
};
_currentController.Doors.Add(newDoor);
RefreshDoorList();
SaveData();
// 选中新添加的门
foreach (ListViewItem item in doorListView.Items)
{
if (item.Tag == newDoor)
{
item.Selected = true;
item.EnsureVisible();
break;
}
}
}
catch (Exception ex)
{
MessageBox.Show($"添加门失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 删除门
/// </summary>
private void btnDeleteDoor_Click(object sender, EventArgs e)
{
if (_currentController == null)
{
MessageBox.Show("请先选择门控制器", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_currentDoor == null)
{
MessageBox.Show("请选择要删除的门", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var result = MessageBox.Show($"删除编码为 {_currentDoor.Index} 的门?", "确认删除",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
_currentController.Doors.Remove(_currentDoor);
_currentDoor = null;
ClearDoorFields();
RefreshDoorList();
SaveData();
}
}
/// <summary>
/// 保存门
/// </summary>
private void btnSaveDoor_Click(object sender, EventArgs e)
{
if (_currentController == null)
{
MessageBox.Show("请先选择门控制器", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (_currentDoor == null)
{
MessageBox.Show("请选择要保存的门", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
if (!int.TryParse(textBoxDoorIndex.Text, out int index))
{
MessageBox.Show("门编码必须是数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// 检查门编码是否重复(排除当前门)
if (_currentController.Doors.Any(d => d.Index == index && d != _currentDoor))
{
MessageBox.Show($"门编码 {index} 已存在,请使用其他编码", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!ushort.TryParse(textBoxControlAddress.Text, out ushort controlAddress))
{
MessageBox.Show("控制地址必须是0-65535之间的数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (!ushort.TryParse(textBoxOpenStatusAddress.Text, out ushort openStatusAddress))
{
MessageBox.Show("开到位地址必须是0-65535之间的数字", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
_currentDoor.Index = index;
_currentDoor.ControlAddress = controlAddress;
_currentDoor.OpenStatusAddress = openStatusAddress;
_currentDoor.NoControl = checkBoxNoControl.Checked;
RefreshDoorList();
SaveData();
MessageBox.Show("保存成功", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 清空门控制器字段
/// </summary>
private void ClearControllerFields()
{
textBoxIp.Text = string.Empty;
textBoxPort.Text = string.Empty;
textBoxControllerIndex.Text = string.Empty;
comboBoxType.SelectedIndex = -1;
}
/// <summary>
/// 清空门字段
/// </summary>
private void ClearDoorFields()
{
textBoxDoorIndex.Text = string.Empty;
textBoxControlAddress.Text = string.Empty;
textBoxOpenStatusAddress.Text = string.Empty;
checkBoxNoControl.Checked = false;
}
/// <summary>
/// 加载数据
/// </summary>
private void LoadData()
{
try
{
if (File.Exists(_dataFilePath))
{
var jsonContent = File.ReadAllText(_dataFilePath, Encoding.UTF8);
if (!string.IsNullOrWhiteSpace(jsonContent))
{
_doorControllers = jsonContent.JsonTo<List<DoorControllerModel>>();
if (_doorControllers == null)
{
_doorControllers = new List<DoorControllerModel>();
}
}
else
{
_doorControllers = new List<DoorControllerModel>();
}
}
else
{
_doorControllers = new List<DoorControllerModel>();
}
}
catch (Exception ex)
{
MessageBox.Show($"加载数据失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
_doorControllers = new List<DoorControllerModel>();
}
}
/// <summary>
/// 保存数据
/// </summary>
private void SaveData()
{
try
{
var jsonContent = _doorControllers.ToJson();
File.WriteAllText(_dataFilePath, jsonContent, Encoding.UTF8);
}
catch (Exception ex)
{
MessageBox.Show($"保存数据失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 验证IP地址格式
/// </summary>
private bool IsValidIpAddress(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
{
return false;
}
string pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
if (Regex.IsMatch(ipAddress, pattern))
{
IPAddress address;
return IPAddress.TryParse(ipAddress, out address) && address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork;
}
return false;
}
/// <summary>
/// 窗体关闭事件
/// </summary>
private void DoorManager_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
// 关闭前保存数据
SaveData();
e.Cancel = true;
this.Visible = false;
}
}
/// <summary>
/// 打开管理界面(静态方法)
/// </summary>
public static void OpenViewer()
{
try
{
var manager = Instance;
if (manager.Visible)
{
if (manager.WindowState == FormWindowState.Minimized)
{
manager.WindowState = FormWindowState.Normal;
}
manager.Activate();
manager.BringToFront();
}
else
{
manager.Show();
manager.Activate();
}
}
catch (Exception ex)
{
MessageBox.Show($"打开门控制器管理界面失败: {ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timerRefresh.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 门控制器模型
/// </summary>
public class DoorControllerModel
{
/// <summary>
/// 控制器索引
/// </summary>
public int Index { get; set; } = 0;
/// <summary>
/// IP地址
/// </summary>
public string Ip { get; set; } = string.Empty;
/// <summary>
/// 端口
/// </summary>
public int Port { get; set; } = 502;
/// <summary>
/// 控制器类型(类名)
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 门列表
/// </summary>
public List<DoorModel> Doors { get; set; } = new List<DoorModel>();
}
/// <summary>
/// 门模型
/// </summary>
public class DoorModel
{
/// <summary>
/// 门索引
/// </summary>
public int Index { get; set; } = 0;
/// <summary>
/// 开关控制信号地址(Modbus 线圈地址)
/// </summary>
public ushort ControlAddress { get; set; } = 0;
/// <summary>
/// 开到位信号地址(Modbus 线圈地址)
/// </summary>
public ushort OpenStatusAddress { get; set; } = 0;
/// <summary>
/// 禁止对该门下发任何控制指令(打开或关闭)。
/// 为 true 时,门控逻辑不会对该门调用 WriteDoorControl。
/// </summary>
public bool NoControl { get; set; } = false;
}
}
@@ -0,0 +1,263 @@
namespace StandardScene.ExtendDevice.Door
{
partial class DoorMonitor
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.doorListView = new System.Windows.Forms.ListView();
this.columnHeaderControllerIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderDoorIndex = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderState = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderTarget = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderSource = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderManualRemain = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderCarsInArea = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderControlAddress = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeaderOpenStatusAddress = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.groupBoxControl = new System.Windows.Forms.GroupBox();
this.btnClose = new System.Windows.Forms.Button();
this.btnOpen = new System.Windows.Forms.Button();
this.btnClearCars = new System.Windows.Forms.Button();
this.labelDoorInfo = new System.Windows.Forms.Label();
this.labelTitle = new System.Windows.Forms.Label();
this.timerRefresh = new System.Windows.Forms.Timer();
this.groupBoxControl.SuspendLayout();
this.SuspendLayout();
//
// doorListView
//
this.doorListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.doorListView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.doorListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeaderControllerIndex,
this.columnHeaderDoorIndex,
this.columnHeaderState,
this.columnHeaderTarget,
this.columnHeaderSource,
this.columnHeaderManualRemain,
this.columnHeaderCarsInArea,
this.columnHeaderControlAddress,
this.columnHeaderOpenStatusAddress});
this.doorListView.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.doorListView.FullRowSelect = true;
this.doorListView.GridLines = true;
this.doorListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
this.doorListView.HideSelection = false;
this.doorListView.Location = new System.Drawing.Point(15, 55);
this.doorListView.MultiSelect = false;
this.doorListView.Name = "doorListView";
this.doorListView.OwnerDraw = true;
this.doorListView.Size = new System.Drawing.Size(800, 400);
this.doorListView.TabIndex = 0;
this.doorListView.UseCompatibleStateImageBehavior = false;
this.doorListView.View = System.Windows.Forms.View.Details;
this.doorListView.SelectedIndexChanged += new System.EventHandler(this.doorListView_SelectedIndexChanged);
//
// columnHeaderControllerIndex
//
this.columnHeaderControllerIndex.Text = "控制器编码";
this.columnHeaderControllerIndex.Width = 120;
//
// columnHeaderDoorIndex
//
this.columnHeaderDoorIndex.Text = "门编码";
this.columnHeaderDoorIndex.Width = 100;
//
// columnHeaderState
//
this.columnHeaderState.Text = "状态";
this.columnHeaderState.Width = 100;
//
// columnHeaderTarget
//
this.columnHeaderTarget.Text = "控制目标";
this.columnHeaderTarget.Width = 100;
//
// columnHeaderSource
//
this.columnHeaderSource.Text = "控制来源";
this.columnHeaderSource.Width = 100;
//
// columnHeaderManualRemain
//
this.columnHeaderManualRemain.Text = "手动剩余(s)";
this.columnHeaderManualRemain.Width = 110;
//
// columnHeaderCarsInArea
//
this.columnHeaderCarsInArea.Text = "车辆占用";
this.columnHeaderCarsInArea.Width = 150;
//
// columnHeaderControlAddress
//
this.columnHeaderControlAddress.Text = "控制地址";
this.columnHeaderControlAddress.Width = 120;
//
// columnHeaderOpenStatusAddress
//
this.columnHeaderOpenStatusAddress.Text = "开到位地址";
this.columnHeaderOpenStatusAddress.Width = 120;
//
// groupBoxControl
//
this.groupBoxControl.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.groupBoxControl.Controls.Add(this.btnClose);
this.groupBoxControl.Controls.Add(this.btnOpen);
this.groupBoxControl.Controls.Add(this.btnClearCars);
this.groupBoxControl.Controls.Add(this.labelDoorInfo);
this.groupBoxControl.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.groupBoxControl.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(68)))), ((int)(((byte)(68)))), ((int)(((byte)(68)))));
this.groupBoxControl.Location = new System.Drawing.Point(15, 470);
this.groupBoxControl.Name = "groupBoxControl";
this.groupBoxControl.Padding = new System.Windows.Forms.Padding(12, 10, 12, 12);
this.groupBoxControl.Size = new System.Drawing.Size(800, 120);
this.groupBoxControl.TabIndex = 1;
this.groupBoxControl.TabStop = false;
this.groupBoxControl.Text = "手动控制";
//
// btnClose
//
this.btnClose.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(220)))), ((int)(((byte)(53)))), ((int)(((byte)(69)))));
this.btnClose.FlatAppearance.BorderSize = 0;
this.btnClose.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(165)))), ((int)(((byte)(40)))), ((int)(((byte)(52)))));
this.btnClose.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(187)))), ((int)(((byte)(45)))), ((int)(((byte)(59)))));
this.btnClose.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnClose.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnClose.ForeColor = System.Drawing.Color.White;
this.btnClose.Location = new System.Drawing.Point(450, 50);
this.btnClose.Name = "btnClose";
this.btnClose.Size = new System.Drawing.Size(120, 50);
this.btnClose.TabIndex = 2;
this.btnClose.Text = "关闭";
this.btnClose.UseVisualStyleBackColor = false;
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// btnOpen
//
this.btnOpen.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(167)))), ((int)(((byte)(69)))));
this.btnOpen.FlatAppearance.BorderSize = 0;
this.btnOpen.FlatAppearance.MouseDownBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(125)))), ((int)(((byte)(52)))));
this.btnOpen.FlatAppearance.MouseOverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(136)))), ((int)(((byte)(56)))));
this.btnOpen.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnOpen.Font = new System.Drawing.Font("微软雅黑", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnOpen.ForeColor = System.Drawing.Color.White;
this.btnOpen.Location = new System.Drawing.Point(300, 50);
this.btnOpen.Name = "btnOpen";
this.btnOpen.Size = new System.Drawing.Size(120, 50);
this.btnOpen.TabIndex = 1;
this.btnOpen.Text = "打开";
this.btnOpen.UseVisualStyleBackColor = false;
this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click);
//
// btnClearCars
//
this.btnClearCars.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(108)))), ((int)(((byte)(117)))), ((int)(((byte)(125)))));
this.btnClearCars.FlatAppearance.BorderSize = 0;
this.btnClearCars.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnClearCars.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.btnClearCars.ForeColor = System.Drawing.Color.White;
this.btnClearCars.Location = new System.Drawing.Point(600, 50);
this.btnClearCars.Name = "btnClearCars";
this.btnClearCars.Size = new System.Drawing.Size(140, 50);
this.btnClearCars.TabIndex = 3;
this.btnClearCars.Text = "清空占用";
this.btnClearCars.UseVisualStyleBackColor = false;
this.btnClearCars.Click += new System.EventHandler(this.btnClearCars_Click);
//
// labelDoorInfo
//
this.labelDoorInfo.AutoSize = true;
this.labelDoorInfo.Font = new System.Drawing.Font("微软雅黑", 10.5F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelDoorInfo.Location = new System.Drawing.Point(20, 35);
this.labelDoorInfo.Name = "labelDoorInfo";
this.labelDoorInfo.Size = new System.Drawing.Size(200, 24);
this.labelDoorInfo.TabIndex = 0;
this.labelDoorInfo.Text = "请选择要控制的门";
//
// labelTitle
//
this.labelTitle.AutoSize = true;
this.labelTitle.Font = new System.Drawing.Font("微软雅黑", 16F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.labelTitle.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(51)))), ((int)(((byte)(51)))));
this.labelTitle.Location = new System.Drawing.Point(15, 12);
this.labelTitle.Name = "labelTitle";
this.labelTitle.Size = new System.Drawing.Size(150, 42);
this.labelTitle.TabIndex = 2;
this.labelTitle.Text = "门控监控";
//
// timerRefresh
//
this.timerRefresh.Interval = 1000;
this.timerRefresh.Tick += new System.EventHandler(this.timerRefresh_Tick);
//
// DoorMonitor
//
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(245)))), ((int)(((byte)(245)))), ((int)(((byte)(247)))));
this.ClientSize = new System.Drawing.Size(830, 600);
this.Controls.Add(this.labelTitle);
this.Controls.Add(this.groupBoxControl);
this.Controls.Add(this.doorListView);
this.Font = new System.Drawing.Font("微软雅黑", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
this.MinimumSize = new System.Drawing.Size(830, 600);
this.Name = "DoorMonitor";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "门控监控";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DoorMonitor_FormClosing);
this.Load += new System.EventHandler(this.DoorMonitor_Load);
this.groupBoxControl.ResumeLayout(false);
this.groupBoxControl.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ListView doorListView;
private System.Windows.Forms.ColumnHeader columnHeaderControllerIndex;
private System.Windows.Forms.ColumnHeader columnHeaderDoorIndex;
private System.Windows.Forms.ColumnHeader columnHeaderState;
private System.Windows.Forms.ColumnHeader columnHeaderTarget;
private System.Windows.Forms.ColumnHeader columnHeaderSource;
private System.Windows.Forms.ColumnHeader columnHeaderManualRemain;
private System.Windows.Forms.ColumnHeader columnHeaderCarsInArea;
private System.Windows.Forms.ColumnHeader columnHeaderControlAddress;
private System.Windows.Forms.ColumnHeader columnHeaderOpenStatusAddress;
private System.Windows.Forms.GroupBox groupBoxControl;
private System.Windows.Forms.Label labelDoorInfo;
private System.Windows.Forms.Button btnOpen;
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.Button btnClearCars;
private System.Windows.Forms.Label labelTitle;
private System.Windows.Forms.Timer timerRefresh;
}
}
@@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using SimpleLite;
namespace StandardScene.ExtendDevice.Door
{
public partial class DoorMonitor : Form
{
private static DoorMonitor _instance = null;
private static readonly object _lock = new object();
private int _doorHoverIndex = -1;
private (int ControllerIndex, int DoorIndex)? _selectedDoor = null;
private static readonly Color RowEvenColor = Color.FromArgb(250, 250, 252);
private static readonly Color RowOddColor = Color.White;
private static readonly Color RowHighlightColor = Color.FromArgb(230, 240, 255);
private static readonly Color TextRegularColor = Color.FromArgb(68, 68, 68);
private static readonly Color TextHighlightColor = Color.FromArgb(51, 51, 51);
private static readonly Color StateOpenColor = Color.FromArgb(40, 167, 69);
private static readonly Color StateClosedColor = Color.FromArgb(220, 53, 69);
/// <summary>
/// 获取单例实例
/// </summary>
public static DoorMonitor Instance
{
get
{
if (_instance == null || _instance.IsDisposed)
{
lock (_lock)
{
if (_instance == null || _instance.IsDisposed)
{
_instance = new DoorMonitor();
}
}
}
return _instance;
}
}
/// <summary>
/// 私有构造函数,确保单例模式
/// </summary>
private DoorMonitor()
{
InitializeComponent();
}
/// <summary>
/// 确保刷新定时器处于激活状态,并立即刷新一次
/// </summary>
public void EnsureRefreshActive()
{
if (IsDisposed)
{
return;
}
if (!timerRefresh.Enabled)
{
timerRefresh.Start();
}
RefreshDoorList();
}
private void DoorMonitor_Load(object sender, EventArgs e)
{
SetupListViewStyles();
// 禁用系统的悬停/热跟踪高亮,避免鼠标移动时短暂出现默认遮罩
doorListView.HoverSelection = false;
doorListView.HotTracking = false;
EnsureRefreshActive();
}
/// <summary>
/// 设置ListView的视觉样式
/// </summary>
private void SetupListViewStyles()
{
doorListView.OwnerDraw = true;
doorListView.BackColor = Color.White;
doorListView.DrawItem += DoorListView_DrawItem;
doorListView.DrawSubItem += DoorListView_DrawSubItem;
doorListView.DrawColumnHeader += DoorListView_DrawColumnHeader;
doorListView.MouseMove += DoorListView_MouseMove;
doorListView.MouseLeave += DoorListView_MouseLeave;
// 启用双缓冲
typeof(Control)?.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)?
.SetValue(doorListView, true, null);
}
private void DoorListView_MouseMove(object sender, MouseEventArgs e)
{
var hoveredItem = doorListView.GetItemAt(e.X, e.Y);
int newIndex = hoveredItem?.Index ?? -1;
if (_doorHoverIndex != newIndex)
{
_doorHoverIndex = newIndex;
doorListView.Invalidate();
}
}
private void DoorListView_MouseLeave(object sender, EventArgs e)
{
if (_doorHoverIndex != -1)
{
_doorHoverIndex = -1;
doorListView.Invalidate();
}
}
private void DoorListView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _doorHoverIndex
|| (doorListView.Focused && (e.State & ListViewItemStates.Focused) != 0);
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
var textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
TextRenderer.DrawText(e.Graphics, e.Item.Text, e.Item.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
e.DrawFocusRectangle();
}
private void DoorListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
var isHighlighted = e.Item.Selected
|| e.ItemIndex == _doorHoverIndex
|| (doorListView.Focused && (e.ItemState & ListViewItemStates.Focused) != 0);
var backColor = isHighlighted
? RowHighlightColor
: (e.ItemIndex % 2 == 0 ? RowEvenColor : RowOddColor);
using (var brush = new SolidBrush(backColor))
{
e.Graphics.FillRectangle(brush, e.Bounds);
}
Color textColor = TextRegularColor;
// 如果是状态列,根据状态设置颜色
if (e.ColumnIndex == 2) // 状态列
{
var stateText = e.SubItem.Text;
if (stateText == "打开")
{
textColor = StateOpenColor;
}
else if (stateText == "关闭")
{
textColor = StateClosedColor;
}
}
// 如果是目标控制列,按目标状态着色
else if (e.ColumnIndex == 3) // 控制目标列
{
var targetText = e.SubItem.Text;
if (targetText == "开")
{
textColor = StateOpenColor;
}
else
{
textColor = StateClosedColor;
}
}
// 其他列使用默认颜色
else
{
textColor = isHighlighted ? TextHighlightColor : TextRegularColor;
}
TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.SubItem.Font, e.Bounds,
textColor,
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis);
}
private void DoorListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(245, 247, 250)), e.Bounds);
e.Graphics.DrawLine(new Pen(Color.FromArgb(220, 220, 220)),
e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
TextRenderer.DrawText(e.Graphics, e.Header.Text,
new Font("微软雅黑", 10.5F, FontStyle.Bold),
e.Bounds, Color.FromArgb(68, 68, 68),
TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.HorizontalCenter);
}
/// <summary>
/// 刷新门列表
/// </summary>
private void RefreshDoorList()
{
doorListView.Items.Clear();
// 保存当前选中的门
(int ControllerIndex, int DoorIndex)? previousSelected = _selectedDoor;
_selectedDoor = null;
labelDoorInfo.Text = "请选择要控制的门";
// 获取所有门控制器
var mission = SimpleProject.proj?.Missions?.OfType<DoorMission>().FirstOrDefault();
if (mission == null)
{
return;
}
var doorSnapshot = mission.GetDoorMonitorSnapshot();
if (doorSnapshot.Count == 0)
{
return;
}
ListViewItem selectedItem = null;
foreach (var door in doorSnapshot)
{
var stateText = door.State == DoorState.Open ? "打开" : door.State == DoorState.Closed ? "关闭" : "未知";
var targetText = door.Target ? "开" : "关";
var sourceText = door.Source == DoorMission.ControlSource.Manual ? "手动" : "自动";
var remainText = door.Source == DoorMission.ControlSource.Manual && door.ManualRemainingSeconds.HasValue
? Math.Ceiling(door.ManualRemainingSeconds.Value).ToString()
: "-";
var carsText = door.CarsInArea.Count > 0 ? string.Join(", ", door.CarsInArea) : "无";
var item = new ListViewItem(door.ControllerIndex.ToString());
item.SubItems.Add(door.DoorIndex.ToString());
item.SubItems.Add(stateText);
item.SubItems.Add(targetText);
item.SubItems.Add(sourceText);
item.SubItems.Add(remainText);
item.SubItems.Add(carsText);
item.SubItems.Add(door.ControlAddress.ToString());
item.SubItems.Add(door.OpenStatusAddress.ToString());
item.Tag = (door.ControllerIndex, door.DoorIndex);
item.UseItemStyleForSubItems = false;
doorListView.Items.Add(item);
// 如果之前选中的门存在,恢复选中状态
if (previousSelected.HasValue &&
previousSelected.Value.ControllerIndex == door.ControllerIndex &&
previousSelected.Value.DoorIndex == door.DoorIndex)
{
selectedItem = item;
}
}
// 恢复选中状态
if (selectedItem != null)
{
selectedItem.Selected = true;
selectedItem.EnsureVisible();
doorListView_SelectedIndexChanged(doorListView, EventArgs.Empty);
}
}
/// <summary>
/// 门列表选择改变
/// </summary>
private void doorListView_SelectedIndexChanged(object sender, EventArgs e)
{
if (doorListView.SelectedItems.Count > 0)
{
var tag = doorListView.SelectedItems[0].Tag;
if (tag != null && tag is ValueTuple<int, int>)
{
var doorInfo = (ValueTuple<int, int>)tag;
_selectedDoor = doorInfo;
labelDoorInfo.Text = $"控制器编码: {doorInfo.Item1}, 门编码: {doorInfo.Item2}";
// 根据占用状态决定关闭按钮是否可用
var mission = SimpleProject.proj?.Missions?.OfType<DoorMission>().FirstOrDefault();
var carsInArea = mission?.GetCarsInArea(doorInfo.Item1, doorInfo.Item2) ?? Array.Empty<int>();
btnClose.Enabled = carsInArea.Count == 0;
}
else
{
_selectedDoor = null;
labelDoorInfo.Text = "请选择要控制的门";
btnClose.Enabled = true;
}
}
else
{
_selectedDoor = null;
labelDoorInfo.Text = "请选择要控制的门";
btnClose.Enabled = true;
}
}
/// <summary>
/// 打开门
/// </summary>
private void btnOpen_Click(object sender, EventArgs e)
{
if (!_selectedDoor.HasValue)
{
MessageBox.Show("请先选择要控制的门", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
var mission = SimpleProject.proj?.Missions?.OfType<DoorMission>().FirstOrDefault();
if (mission == null)
{
MessageBox.Show("未找到门控进程", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var (controllerIndex, doorIndex) = _selectedDoor.Value;
// 手动控制:默认保持10秒
mission.SetManualDoorControl(controllerIndex, doorIndex, true);
MessageBox.Show($"控制器 {controllerIndex} 门 {doorIndex} 已设置手动打开(10秒)", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"设置门打开目标失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 关闭门
/// </summary>
private void btnClose_Click(object sender, EventArgs e)
{
if (!_selectedDoor.HasValue)
{
MessageBox.Show("请先选择要控制的门", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
var mission = SimpleProject.proj?.Missions?.OfType<DoorMission>().FirstOrDefault();
if (mission == null)
{
MessageBox.Show("未找到门控进程", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var (controllerIndex, doorIndex) = _selectedDoor.Value;
// 车辆占用时禁止手动关闭
var success = mission.SetManualDoorControl(controllerIndex, doorIndex, false);
if (!success)
{
MessageBox.Show("门存在车辆占用,禁止手动关闭。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
MessageBox.Show($"控制器 {controllerIndex} 门 {doorIndex} 已设置手动关闭(10秒)", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"设置门关闭目标失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 清空车辆占用
/// </summary>
private void btnClearCars_Click(object sender, EventArgs e)
{
if (!_selectedDoor.HasValue)
{
MessageBox.Show("请先选择要清空占用的门", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
try
{
var mission = SimpleProject.proj?.Missions?.OfType<DoorMission>().FirstOrDefault();
if (mission == null)
{
MessageBox.Show("未找到门控进程", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
var (controllerIndex, doorIndex) = _selectedDoor.Value;
mission.ClearCarsInArea(controllerIndex, doorIndex);
MessageBox.Show($"控制器 {controllerIndex} 门 {doorIndex} 已清空占用", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
RefreshDoorList();
}
catch (Exception ex)
{
MessageBox.Show($"清空占用失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 定时刷新
/// </summary>
private void timerRefresh_Tick(object sender, EventArgs e)
{
RefreshDoorList();
}
/// <summary>
/// 窗体关闭事件
/// </summary>
private void DoorMonitor_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
timerRefresh.Stop();
e.Cancel = true;
this.Visible = false;
}
}
protected override void OnVisibleChanged(EventArgs e)
{
base.OnVisibleChanged(e);
if (Visible)
{
EnsureRefreshActive();
}
else
{
timerRefresh.Stop();
}
}
}
}
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timerRefresh.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
@@ -0,0 +1,25 @@
using System;
namespace StandardScene.ExtendDevice.Door
{
/// <summary>
/// 门控制器类型特性,用于标记门控制器类型
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class DoorTypeAttribute : Attribute
{
/// <summary>
/// 类型名称
/// </summary>
public string Name { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="name">类型名称</param>
public DoorTypeAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
}
+41
View File
@@ -0,0 +1,41 @@
using SimpleCore.Compiler;
using SimpleCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Numerics;
using SimpleCore.Library;
using SimpleCore.Traffic;
namespace StandardScene
{
public class Heuristic:HeuristicsContainer
{
//[HeuristicDef]
//public bool NoPassShelf(SegmentPlan.SearchStat stat)
//{
// if (stat.sequence.Length > 2 &&
// SimpleLib.GetSite(stat.sequence[stat.sequence.Length - 2]).fields.ContainsKey("Shelf"))
// return false;
// return true;
//}
[HeuristicDef]
public bool ToDestConstraint(SegmentPlan.SearchStat stat)
{
if (plan.Source.fields.TryGetValue("constraint", out var field))
{
foreach (var constraint in field.Split('|'))
{
var (dstStr, idStr, _) = constraint.Split(':');
if (plan.Destination.id.ToString() == dstStr && stat.CurrentSite.id.ToString() == idStr)
return false;
}
}
return true;
}
}
}
@@ -0,0 +1,413 @@
using SimpleCore.Traffic;
using System;
using System.Collections.Generic;
using System.Linq;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore.Library;
using SimpleCore.PropType;
using SimpleCore;
using System.Threading;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using SimpleLite;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace StandardScene.InterLock
{
public class MyDictionary<TKey, TItem> where TItem : new()
{
private readonly Dictionary<TKey, TItem> dictionary = new();
public TItem this[TKey key]
{
get
{
if (dictionary.TryGetValue(key, out var item)) return item;
var newItem = new TItem();
dictionary[key] = newItem;
return newItem;
}
set => dictionary[key] = value;
}
public ICollection<TKey> Keys
{
get => dictionary.Keys;
}
}
public class AbstractInterlockMission : Mission
{
public bool GetAskEnter(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return askEnter[siteId];
}
public void SetAllowEnter(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) allowEnter[siteId] = value;
}
public bool GetAskExit(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return askExit[siteId];
}
public void SetAllowExit(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) allowExit[siteId] = value;
}
public bool GetReportLeave(int siteId)
{
if (!SiteFilter(siteId)) return false;
lock (sync) return reportLeave[siteId];
}
public void SetAcknowledgeLeave(int siteId, bool value)
{
if (!SiteFilter(siteId)) return;
lock (sync) acknowledgeLeave[siteId] = value;
}
public int GetInSiteCarId(int siteId)
{
if (!SiteFilter(siteId)) return -1;
lock (sync) return inSiteCarId[siteId];
}
// 请求进站(写)
[JsonIgnore] private readonly MyDictionary<int, bool> askEnter = new();
// 允许进站(读)
[JsonIgnore] private readonly MyDictionary<int, bool> allowEnter = new();
// 请求离站(写)
[JsonIgnore] private readonly MyDictionary<int, bool> askExit = new();
// 允许离站(读)
[JsonIgnore] private readonly MyDictionary<int, bool> allowExit = new();
// 报告离开(写)
[JsonIgnore] private readonly MyDictionary<int, bool> reportLeave = new();
// 确认离开(读)
[JsonIgnore] private readonly MyDictionary<int, bool> acknowledgeLeave = new();
// 站点上所在的小车(状态)
[JsonIgnore] private readonly MyDictionary<int, int> inSiteCarId = new();
[JsonIgnore] private object sync = new();
public virtual string GetSiteDisplay(int siteId)
{
return $"{siteId}";
}
/// <summary>
/// 返回为true的站点,是当前互锁机制关注的站点
/// </summary>
/// <param name="siteId"></param>
/// <returns></returns>
public virtual bool SiteFilter(int siteId)
{
return true;
}
/// <summary>
/// 互锁站点进站条件
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
private bool EnterCondition(AbstractCar car, int siteId)
{
// todo: 是否考虑提前更多申请进入,加快节拍?
if (car.status.pendingLocks.First() != siteId) return false;
//DateTime startTime = DateTime.Now;
//bool validTime = false;
lock (sync)
{
// if (askEnter[siteId] == false)
// {
// Diagnosis.Post($"{car.name}({car.id}) 申请进站 {GetSiteDisplay(siteId)}", "intertime", true);
// //startTime = DateTime.Now;
// //validTime = true;
// }
askEnter[siteId] = true;
if (reportLeave[siteId])
{
Diagnosis.Post($"{car.name}({car.id})未被准许进站{GetSiteDisplay(siteId)},前车离开信号未被确认", "interlock", true);
return false;
}
if (allowEnter[siteId])
{
Diagnosis.Post($"{car.name}({car.id})准许进站{GetSiteDisplay(siteId)}", "interlock", true);
// Diagnosis.Post($"{car.name}({car.id}) 准许进站 {GetSiteDisplay(siteId)}", "intertime", true);
lock (sync) askEnter[siteId] = false;
return true;
}
Diagnosis.Post($"{car.name}({car.id})未被准许进站{GetSiteDisplay(siteId)}", "interlock", true);
return false;
}
}
/// <summary>
/// 互锁站点离站条件
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
private bool ExitCondition(AbstractCar car, int siteId)
{
lock (sync)
{
askExit[siteId] = true;
if (allowExit[siteId])
{
Diagnosis.Post($"{car.name}({car.id})准许离站{GetSiteDisplay(siteId)}", "interlock", true);
askExit[siteId] = false;
return true;
}
Diagnosis.Post($"{car.name}({car.id})未被准许离站{GetSiteDisplay(siteId)}", "interlock", true);
return false;
}
}
/// <summary>
/// 告知对方设备小车离开,直到对方设备确认收到离开信号
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
private void LeaveEvent(AbstractCar car, int siteId)
{
new Thread(() =>
{
while (true)
{
Thread.Sleep(100);
lock (sync)
{
if (acknowledgeLeave[siteId]) // 已经确认小车离开
{
Diagnosis.Post($"{GetSiteDisplay(siteId)}确认{car.name}({car.id})离开", "interlock", true);
break;
}
}
Diagnosis.Post($"{car.name}({car.id})告知离开{GetSiteDisplay(siteId)}", "interlock", true);
lock (sync) reportLeave[siteId] = true;
}
lock (sync) reportLeave[siteId] = false;
}).Start();
}
[JsonIgnore] private bool enableSimulation = false;
[JsonIgnore] public bool started = false;
[JsonIgnore] public Thread showT;
[MethodMember(Name = "启动进程", Description = "处理安全互锁")]
public override void Execute()
{
if (started) return;
// started = true;
status.status = "已启动";
showT = new Thread(() =>
{
while (true)
{
Thread.Sleep(300);
try
{
if (fields.TryGetValue("enableSimulation", out var userEnableSimulation))
enableSimulation = bool.Parse(userEnableSimulation);
foreach (var site in SimpleLib.GetAllSites().Where(ss => SiteFilter(ss.id)))
{
var carId = -1;
foreach (var car in SimpleLib.GetAllCars())
{
lock (TrafficControl.syncTrafficSequence)
{
if (car.status.holdingLocks.Length == 1 && car.status.holdingLocks[0] == site.id)
{
carId = car.id;
break;
}
}
}
lock (sync) inSiteCarId[site.id] = carId;
}
// if (!onDisplay) continue;
var painter = SimpleMonitor.getPainter("DemoInterlockMission");
painter.clear();
if (!onDisplay) continue;
//string str;
//lock (sync)
//{
// var keys = askEnter.Keys.Concat(askExit.Keys).Concat(reportLeave.Keys).ToHashSet()
// .OrderBy(kk => int.Parse(SimpleLib.GetSite(kk).fields["ASNub"])).ToList();
// str = $"站点\t\t请求进站\t允许进站\t请求离站\t允许离站\t上报离开\t确认离开\t小车id\n" +
// $"{string.Join("\n", keys.Select(key =>
// $"{GetSiteDisplay(key)}\t{askEnter[key]}\t\t{allowEnter[key]}\t\t" +
// $"{askExit[key]}\t\t{allowExit[key]}\t\t" +
// $"{reportLeave[key]}\t\t{acknowledgeLeave[key]}\t\t{inSiteCarId[key]}"))}";
//}
//painter.drawTextFixed(str, new SolidBrush(Color.Black), VirtualPainter.DrawPosition.RightTop, Color.AliceBlue);
}
catch (Exception ex)
{
Diagnosis.Post(ExceptionFormatter.FormatEx(ex), "interlock", true);
}
}
}) { Name = "InterlockMission" };
showT.Start();
}
[JsonIgnore] private bool onDisplay = true;
[MethodMember(Name = "切换显示", Description = "是否在右下角显示")]
public void SwitchAlwaysOnDisplay()
{
onDisplay = !onDisplay;
}
private async void SimButton(string title, MyDictionary<int, bool> toManipulate)
{
//if (!enableSimulation) return;
G.pushStatus("请选择站点");
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var site = SimpleLib.GetSite(pt.site);
if (SiteFilter(site.id))
{
var vv = await Program.UI.Input("请输入true/false", title, "true");
if (vv == null || !bool.TryParse(vv, out var allow))
{
MessageBox.Show("输入错误");
return;
}
lock (sync) toManipulate[site.id] = allow;
}
}
[MethodMember(Name = "模拟准许进站")]
public void SimAllowEnter()
{
SimButton("模拟准许进入状态", allowEnter);
}
[MethodMember(Name = "模拟准许离站")]
public void SimAllowExit()
{
SimButton("模拟准许离站状态", allowExit);
}
[MethodMember(Name = "模拟确认离开")]
public void SimLeaveNoted()
{
SimButton("模拟确认离开状态", acknowledgeLeave);
}
[MethodMember(Name = "模拟发送离开")]
public async void SimReportLeave()
{
// if (!enableSimulation) return;
G.pushStatus("请选择站点");
var pt = await Program.UI.getPoint(new UIOps.getPointOptions() { site = true });
var site = SimpleLib.GetSite(pt.site);
if (SiteFilter(site.id))
{
lock (sync) reportLeave[site.id] = true;
}
new Thread(() =>
{
var t1 = DateTime.Now;
while (true)
{
Thread.Sleep(100);
lock (sync)
{
var deltaT = (DateTime.Now - t1).TotalSeconds;
if (acknowledgeLeave[site.id]||deltaT>5) // 已经确认小车离开
{
Diagnosis.Post($"{GetSiteDisplay(site.id)} 停止发送离开", "simlikai", true);
break;
}
}
Diagnosis.Post($"模拟告知离开{GetSiteDisplay(site.id)}", "simlikai", true);
lock (sync) reportLeave[site.id] = true;
}
lock (sync) reportLeave[site.id] = false;
}).Start();
// SimButton("模拟确认离开状态", reportLeave);
}
public AbstractInterlockMission()
{
Diagnosis.Post($"InterlockMechanism loaded", "interlock", true);
TrafficControl.BeforeLock += (car, siteId) =>
{
if (SiteFilter(siteId)) // EnterCondition
return EnterCondition(car, siteId);
var site = SimpleLib.GetSite(siteId);
if (site.fields.ContainsKey("PreAskEnterSite") && int.TryParse(site.fields["PreAskEnterSite"], out int PreAskEnterSiteId))
{
var presite = SimpleLib.GetSite(PreAskEnterSiteId);
if (presite != null && SiteFilter(PreAskEnterSiteId))
{
Diagnosis.Post($"{car.name}({car.id})在{siteId}-{GetSiteDisplay(siteId)},提前申请 {PreAskEnterSiteId} 的请求进入", "interlock", true);
askEnter[PreAskEnterSiteId] = true;
}
}
// ExitCondition
lock (TrafficControl.syncTrafficSequence)
if (!car.status.holdingLocks.Any(ss => SiteFilter(ss)))
return true;
var isNeighbor = false;
var lockSiteId = -1;
foreach (var trackId in site.relatedTracks)
{
var track = SimpleLib.GetTrack(trackId);
if (SiteFilter(track.siteA))
{
isNeighbor = true;
lockSiteId = track.siteA;
break;
}
if (SiteFilter(track.siteB))
{
isNeighbor = true;
lockSiteId = track.siteB;
break;
}
}
return !isNeighbor || ExitCondition(car, lockSiteId);
};
TrafficControl.AfterLeave += (car, siteId) =>
{
if (SiteFilter(siteId)) LeaveEvent(car, siteId);
};
}
}
}
@@ -0,0 +1,167 @@
using LessokajiWeaverUtilities.Utilities;
using LoopViewerApp;
using Newtonsoft.Json;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleCore;
using SimpleCore.PropType;
using SimpleCore.Traffic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace StandardScene.InterLock
{
[MissionType(Name = "交通管制", editor = typeof(TrafficInterlockMission))]
[I18N.DocumentTranslation(Name = "Traffic Mission", locale = "en")]
public class TrafficInterlockMission : Mission
{
public static List<TrafficArea> TrafficAreaList { get; set; } = new List<TrafficArea>();
public TrafficInterlockMission()
{
TrafficControl.BeforeLock += (car, siteId) =>
{
return EnterArea(car, siteId);
};
TrafficControl.AfterLeave += (car, siteId) =>
{
LeaveArea(car, siteId);
};
}
[MethodMember(Name = "启动进程", Description = "开始交通监控")]
public void StartTrafficControl()
{
if (InitTrafficConfig()) { status.status = "已启动"; }
}
[MethodMember(Name = "停止进程", Description = "停止交通监控")]
public void StopTrafficControl()
{
status.status = "已停止";
}
[MethodMember(Name = "查看管制区")]
public void ShowViewer()
{
var viewer = new TrafficInterlockViewer();
viewer.Show();
}
/// <summary>
/// 初始化交管配置文件
/// </summary>
/// <returns></returns>
private bool InitTrafficConfig()
{
try
{
string ConfigPath = Path.Combine(Application.StartupPath, "Config/traffic.json");
if (File.Exists(ConfigPath))
{
TrafficAreaList = JsonConvert.DeserializeObject<List<TrafficArea>>(File.ReadAllText(ConfigPath));
return true;
}
return false;
}
catch (Exception ex)
{
Console.WriteLine("初始化交管配置文件失败: " + ex.Message); return false;
}
}
/// <summary>
/// 判断小车是否可以进入区域
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
public bool EnterArea(AbstractCar car, int siteId)
{
var TrafficAreas = TrafficAreaList.FindAll(area => area.SiteList.Contains(siteId) && area.IsEnable);
if (TrafficAreas.Count == 0) { return true; }
if (!TrafficAreas.Exists(t => t.IsOccupy && t.ControllerName != car.id.ToString()))
{
lock (TrafficAreaList)
{
TrafficAreas.ForEach(t => { t.IsOccupy = true; t.ControllerName = car.id.ToString(); });
}
Console.WriteLine($"小车 {car.id} 进入了交管区域 {string.Join(",", TrafficAreas.Select(t => t.AreaName))}");
return true;
}
return false;
}
/// <summary>
/// 离开区域
/// </summary>
/// <param name="car"></param>
/// <param name="siteId"></param>
/// <returns></returns>
public bool LeaveArea(AbstractCar car, int siteId)
{
var TrafficAreas = TrafficAreaList.FindAll(area => area.SiteList.Contains(siteId) && area.IsEnable);
if (TrafficAreas.Count == 0) { return true; }
foreach (var item in TrafficAreas)
{
var Sites = SimpleLib.GetAllSites().Where(s => item.SiteList.Contains(s.id)).ToList();
if (!Sites.Exists(t => t.status.owner == car.id))
{
lock (TrafficAreaList)
{
item.ControllerName = string.Empty; item.IsOccupy = false;
}
Console.WriteLine($"小车 {car.id} 离开了交管区域 {item.AreaName}");
}
}
return true;
}
}
public class TrafficArea
{
/// <summary>
/// 区域名称
/// </summary>
public string AreaName { get; set; }
/// <summary>
/// 区域站点集合
/// </summary>
public List<int> SiteList { get; set; }
/// <summary>
/// 控制权
/// </summary>
[JsonIgnore]
public string ControllerName { get; set; }
/// <summary>
/// 是否被占用
/// </summary>
[JsonIgnore]
public bool IsOccupy { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool IsEnable { get; set; }
}
}
@@ -0,0 +1,325 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LoopViewerApp
{
partial class TrafficInterlockViewer
{
private System.ComponentModel.IContainer components = null;
private ListView lstTasks;
private GroupBox grpEdit;
private ColumnHeader colAreaName;
private ColumnHeader colSites;
private ColumnHeader colControlRight;
private ColumnHeader colIsOccupied;
private ColumnHeader colIsEnabled;
private Label lblAreaName;
private TextBox txtAreaName;
private Label lblStationIds;
private TextBox txtStationIds;
private Label lblControlRight;
private TextBox txtControlRight;
private Label lblIsOccupied;
private CheckBox chkIsOccupied;
private Label lblIsEnabled;
private CheckBox chkIsEnabled;
private Label lblEditingHint;
private Button btnSave;
private Button btnRefresh;
private Button btnNew;
private Button btnDelete;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.lstTasks = new System.Windows.Forms.ListView();
this.colAreaName = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colSites = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colControlRight = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsOccupied = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.colIsEnabled = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.grpEdit = new System.Windows.Forms.GroupBox();
this.lblEditingHint = new System.Windows.Forms.Label();
this.lblAreaName = new System.Windows.Forms.Label();
this.txtAreaName = new System.Windows.Forms.TextBox();
this.lblStationIds = new System.Windows.Forms.Label();
this.txtStationIds = new System.Windows.Forms.TextBox();
this.lblControlRight = new System.Windows.Forms.Label();
this.txtControlRight = new System.Windows.Forms.TextBox();
this.lblIsOccupied = new System.Windows.Forms.Label();
this.chkIsOccupied = new System.Windows.Forms.CheckBox();
this.lblIsEnabled = new System.Windows.Forms.Label();
this.chkIsEnabled = new System.Windows.Forms.CheckBox();
this.btnSave = new System.Windows.Forms.Button();
this.btnRefresh = new System.Windows.Forms.Button();
this.btnNew = new System.Windows.Forms.Button();
this.btnDelete = new System.Windows.Forms.Button();
this.grpEdit.SuspendLayout();
this.SuspendLayout();
//
// lstTasks
//
this.lstTasks.BackColor = System.Drawing.Color.White;
this.lstTasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.colAreaName,
this.colSites,
this.colControlRight,
this.colIsOccupied,
this.colIsEnabled});
this.lstTasks.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(33)))), ((int)(((byte)(33)))), ((int)(((byte)(33)))));
this.lstTasks.FullRowSelect = true;
this.lstTasks.HideSelection = false;
this.lstTasks.Location = new System.Drawing.Point(12, 12);
this.lstTasks.Name = "lstTasks";
this.lstTasks.OwnerDraw = true;
this.lstTasks.Size = new System.Drawing.Size(760, 320);
this.lstTasks.TabIndex = 0;
this.lstTasks.UseCompatibleStateImageBehavior = false;
this.lstTasks.View = System.Windows.Forms.View.Details;
this.lstTasks.DrawColumnHeader += new System.Windows.Forms.DrawListViewColumnHeaderEventHandler(this.lstTasks_DrawColumnHeader);
this.lstTasks.DrawItem += new System.Windows.Forms.DrawListViewItemEventHandler(this.lstTasks_DrawItem);
this.lstTasks.DrawSubItem += new System.Windows.Forms.DrawListViewSubItemEventHandler(this.lstTasks_DrawSubItem);
this.lstTasks.SelectedIndexChanged += new System.EventHandler(this.lstTasks_SelectedIndexChanged);
//
// colAreaName
//
this.colAreaName.Text = "区域名称";
this.colAreaName.Width = 140;
//
// colSites
//
this.colSites.Text = "区域站点集合";
this.colSites.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colSites.Width = 280;
//
// colControlRight
//
this.colControlRight.Text = "控制权";
this.colControlRight.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colControlRight.Width = 120;
//
// colIsOccupied
//
this.colIsOccupied.Text = "是否被占用";
this.colIsOccupied.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsOccupied.Width = 100;
//
// colIsEnabled
//
this.colIsEnabled.Text = "是否启用";
this.colIsEnabled.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.colIsEnabled.Width = 100;
//
// grpEdit
//
this.grpEdit.Controls.Add(this.lblEditingHint);
this.grpEdit.Controls.Add(this.lblAreaName);
this.grpEdit.Controls.Add(this.txtAreaName);
this.grpEdit.Controls.Add(this.lblStationIds);
this.grpEdit.Controls.Add(this.txtStationIds);
this.grpEdit.Controls.Add(this.lblControlRight);
this.grpEdit.Controls.Add(this.txtControlRight);
this.grpEdit.Controls.Add(this.lblIsOccupied);
this.grpEdit.Controls.Add(this.chkIsOccupied);
this.grpEdit.Controls.Add(this.lblIsEnabled);
this.grpEdit.Controls.Add(this.chkIsEnabled);
this.grpEdit.Controls.Add(this.btnSave);
this.grpEdit.Controls.Add(this.btnRefresh);
this.grpEdit.Controls.Add(this.btnNew);
this.grpEdit.Controls.Add(this.btnDelete);
this.grpEdit.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.grpEdit.Location = new System.Drawing.Point(12, 345);
this.grpEdit.Name = "grpEdit";
this.grpEdit.Size = new System.Drawing.Size(760, 165);
this.grpEdit.TabIndex = 1;
this.grpEdit.TabStop = false;
this.grpEdit.Text = "数据新增/编辑(点击表格行可在此查看并编辑该行数据)";
//
// lblEditingHint
//
this.lblEditingHint.AutoSize = true;
this.lblEditingHint.Font = new System.Drawing.Font("微软雅黑", 10F, System.Drawing.FontStyle.Bold);
this.lblEditingHint.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(120)))), ((int)(((byte)(215)))));
this.lblEditingHint.Location = new System.Drawing.Point(12, 125);
this.lblEditingHint.Name = "lblEditingHint";
this.lblEditingHint.Size = new System.Drawing.Size(65, 19);
this.lblEditingHint.TabIndex = 0;
this.lblEditingHint.Text = "新增区域";
//
// lblAreaName
//
this.lblAreaName.AutoSize = true;
this.lblAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblAreaName.Location = new System.Drawing.Point(12, 28);
this.lblAreaName.Name = "lblAreaName";
this.lblAreaName.Size = new System.Drawing.Size(79, 20);
this.lblAreaName.TabIndex = 1;
this.lblAreaName.Text = "区域名称:";
//
// txtAreaName
//
this.txtAreaName.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtAreaName.Location = new System.Drawing.Point(100, 24);
this.txtAreaName.Name = "txtAreaName";
this.txtAreaName.Size = new System.Drawing.Size(200, 25);
this.txtAreaName.TabIndex = 2;
//
// lblStationIds
//
this.lblStationIds.AutoSize = true;
this.lblStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblStationIds.Location = new System.Drawing.Point(320, 28);
this.lblStationIds.Name = "lblStationIds";
this.lblStationIds.Size = new System.Drawing.Size(79, 20);
this.lblStationIds.TabIndex = 3;
this.lblStationIds.Text = "站点集合:";
//
// txtStationIds
//
this.txtStationIds.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtStationIds.Location = new System.Drawing.Point(418, 24);
this.txtStationIds.Name = "txtStationIds";
this.txtStationIds.Size = new System.Drawing.Size(320, 25);
this.txtStationIds.TabIndex = 4;
//
// lblControlRight
//
this.lblControlRight.AutoSize = true;
this.lblControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblControlRight.Location = new System.Drawing.Point(12, 58);
this.lblControlRight.Name = "lblControlRight";
this.lblControlRight.Size = new System.Drawing.Size(65, 20);
this.lblControlRight.TabIndex = 5;
this.lblControlRight.Text = "控制权:";
//
// txtControlRight
//
this.txtControlRight.Font = new System.Drawing.Font("微软雅黑", 10F);
this.txtControlRight.Location = new System.Drawing.Point(100, 54);
this.txtControlRight.Name = "txtControlRight";
this.txtControlRight.Size = new System.Drawing.Size(200, 25);
this.txtControlRight.TabIndex = 6;
//
// lblIsOccupied
//
this.lblIsOccupied.AutoSize = true;
this.lblIsOccupied.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsOccupied.Location = new System.Drawing.Point(320, 58);
this.lblIsOccupied.Name = "lblIsOccupied";
this.lblIsOccupied.Size = new System.Drawing.Size(93, 20);
this.lblIsOccupied.TabIndex = 7;
this.lblIsOccupied.Text = "是否被占用:";
//
// chkIsOccupied
//
this.chkIsOccupied.AutoSize = true;
this.chkIsOccupied.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsOccupied.Location = new System.Drawing.Point(418, 59);
this.chkIsOccupied.Name = "chkIsOccupied";
this.chkIsOccupied.Size = new System.Drawing.Size(39, 19);
this.chkIsOccupied.TabIndex = 8;
this.chkIsOccupied.Text = "是";
//
// lblIsEnabled
//
this.lblIsEnabled.AutoSize = true;
this.lblIsEnabled.Font = new System.Drawing.Font("微软雅黑", 10F);
this.lblIsEnabled.Location = new System.Drawing.Point(500, 58);
this.lblIsEnabled.Name = "lblIsEnabled";
this.lblIsEnabled.Size = new System.Drawing.Size(79, 20);
this.lblIsEnabled.TabIndex = 9;
this.lblIsEnabled.Text = "是否启用:";
//
// chkIsEnabled
//
this.chkIsEnabled.AutoSize = true;
this.chkIsEnabled.Checked = true;
this.chkIsEnabled.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkIsEnabled.Font = new System.Drawing.Font("Segoe UI", 9F);
this.chkIsEnabled.Location = new System.Drawing.Point(585, 59);
this.chkIsEnabled.Name = "chkIsEnabled";
this.chkIsEnabled.Size = new System.Drawing.Size(39, 19);
this.chkIsEnabled.TabIndex = 10;
this.chkIsEnabled.Text = "是";
//
// btnSave
//
this.btnSave.BackColor = System.Drawing.Color.LightBlue;
this.btnSave.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnSave.Font = new System.Drawing.Font("微软雅黑", 11F, System.Drawing.FontStyle.Bold);
this.btnSave.Location = new System.Drawing.Point(260, 118);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(140, 40);
this.btnSave.TabIndex = 12;
this.btnSave.Text = "保存";
this.btnSave.UseVisualStyleBackColor = false;
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnRefresh
//
this.btnRefresh.BackColor = System.Drawing.SystemColors.Control;
this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnRefresh.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnRefresh.Location = new System.Drawing.Point(410, 118);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(140, 40);
this.btnRefresh.TabIndex = 13;
this.btnRefresh.Text = "刷新";
this.btnRefresh.UseVisualStyleBackColor = false;
this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click);
//
// btnNew
//
this.btnNew.BackColor = System.Drawing.SystemColors.Control;
this.btnNew.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnNew.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnNew.Location = new System.Drawing.Point(560, 118);
this.btnNew.Name = "btnNew";
this.btnNew.Size = new System.Drawing.Size(140, 40);
this.btnNew.TabIndex = 14;
this.btnNew.Text = "新增";
this.btnNew.UseVisualStyleBackColor = false;
this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
//
// btnDelete
//
this.btnDelete.BackColor = System.Drawing.SystemColors.Control;
this.btnDelete.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.btnDelete.Font = new System.Drawing.Font("微软雅黑", 11F);
this.btnDelete.Location = new System.Drawing.Point(110, 118);
this.btnDelete.Name = "btnDelete";
this.btnDelete.Size = new System.Drawing.Size(140, 40);
this.btnDelete.TabIndex = 11;
this.btnDelete.Text = "删除";
this.btnDelete.UseVisualStyleBackColor = false;
this.btnDelete.Click += new System.EventHandler(this.btnDelete_Click);
//
// TrafficInterlockViewer
//
this.ClientSize = new System.Drawing.Size(784, 521);
this.Controls.Add(this.lstTasks);
this.Controls.Add(this.grpEdit);
this.Font = new System.Drawing.Font("微软雅黑", 9F);
this.MinimumSize = new System.Drawing.Size(700, 450);
this.Name = "TrafficInterlockViewer";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "交通联锁区域管理";
this.grpEdit.ResumeLayout(false);
this.grpEdit.PerformLayout();
this.ResumeLayout(false);
}
}
}
@@ -0,0 +1,370 @@
using Newtonsoft.Json;
using StandardScene.InterLock; // 数据类型采用 TrafficInterlockMission 中的 TrafficArea
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
namespace LoopViewerApp
{
public partial class TrafficInterlockViewer : Form
{
/// <summary>-1 表示新增模式;>=0 表示正在编辑对应索引</summary>
private int _editingIndex = -1;
/// <summary>选中行变化时是否允许加载到编辑区(避免在保存/取消时重复刷新)</summary>
private bool _allowLoadFromSelection = true;
public TrafficInterlockViewer()
{
InitializeComponent();
if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
return;
try
{
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"TrafficInterlockViewer init error: {ex}");
}
}
#region
private void lstTasks_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
try
{
// 与 LoopViewer 一致:深蓝表头 + 白色加粗字体
using (var backBrush = new SolidBrush(Color.FromArgb(63, 81, 181)))
using (var textBrush = new SolidBrush(Color.White))
using (var font = new Font("微软雅黑", 9, FontStyle.Bold))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
var sf = new StringFormat { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Near };
var rect = e.Bounds;
rect.Inflate(-8, 0);
e.Graphics.DrawString(e.Header.Text, font, textBrush, rect, sf);
using (var pen = new Pen(Color.FromArgb(200, 200, 200)))
e.Graphics.DrawLine(pen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
}
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
private void lstTasks_DrawItem(object sender, DrawListViewItemEventArgs e)
{
// 由 DrawSubItem 统一绘制
}
private void lstTasks_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
try
{
bool selected = e.Item.Selected;
Rectangle bounds = e.Bounds;
// 与 LoopViewer 一致:选中行蓝色强调,交替行背景,深灰文字
Color selectedBack = Color.FromArgb(0, 120, 215);
Color selectedFore = Color.White;
Color evenBack = Color.White;
Color oddBack = Color.FromArgb(250, 251, 253);
Color normalFore = Color.FromArgb(33, 33, 33);
if (selected)
{
using (var selBrush = new SolidBrush(selectedBack))
e.Graphics.FillRectangle(selBrush, bounds);
}
else
{
using (var back = new SolidBrush(e.ItemIndex % 2 == 0 ? evenBack : oddBack))
e.Graphics.FillRectangle(back, bounds);
}
string text = e.SubItem?.Text ?? string.Empty;
Color fore = selected ? selectedFore : normalFore;
var textRect = bounds;
textRect.Inflate(-6, 0);
using (var font = new Font("微软雅黑", 9))
TextRenderer.DrawText(e.Graphics, text, font, textRect, fore, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
}
catch
{
e.DrawBackground();
e.DrawText();
}
}
#endregion
#region
private void RenderListView()
{
try
{
if (lstTasks == null) return;
_allowLoadFromSelection = false;
lstTasks.BeginUpdate();
lstTasks.Items.Clear();
foreach (var a in TrafficInterlockMission.TrafficAreaList)
{
var stationStr = a.SiteList != null && a.SiteList.Count > 0 ? string.Join(", ", a.SiteList) : "";
var lvi = new ListViewItem(new[]
{
a.AreaName ?? "",
stationStr,
a.ControllerName ?? "",
a.IsOccupy ? "是" : "否",
a.IsEnable ? "是" : "否"
});
lstTasks.Items.Add(lvi);
}
lstTasks.EndUpdate();
_allowLoadFromSelection = true;
}
catch (Exception ex)
{
_allowLoadFromSelection = true;
System.Diagnostics.Debug.WriteLine($"RenderListView failed: {ex}");
}
}
private void SaveToConfig()
{
try
{
string configPath = Path.Combine(Application.StartupPath, "Config", "traffic.json");
string dir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllText(configPath, JsonConvert.SerializeObject(TrafficInterlockMission.TrafficAreaList, Formatting.Indented));
}
catch (Exception ex)
{
MessageBox.Show("保存失败:" + ex.Message);
}
}
#endregion
#region
private void lstTasks_SelectedIndexChanged(object sender, EventArgs e)
{
if (!_allowLoadFromSelection || lstTasks == null || lstTasks.SelectedIndices.Count == 0) return;
int idx = lstTasks.SelectedIndices[0];
if (idx < 0 || idx >= TrafficInterlockMission.TrafficAreaList.Count) return;
_editingIndex = idx;
LoadAreaToPanel(TrafficInterlockMission.TrafficAreaList[idx]);
}
#endregion
#region /
private void LoadAreaToPanel(TrafficArea a)
{
if (a == null) return;
try
{
if (lblEditingHint != null)
lblEditingHint.Text = $"编辑:{a.AreaName}";
if (txtAreaName != null)
txtAreaName.Text = a.AreaName ?? "";
if (txtStationIds != null)
txtStationIds.Text = a.SiteList != null && a.SiteList.Count > 0
? string.Join(", ", a.SiteList)
: "";
if (txtControlRight != null)
txtControlRight.Text = a.ControllerName ?? "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = a.IsOccupy;
if (chkIsEnabled != null)
chkIsEnabled.Checked = a.IsEnable;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"LoadAreaToPanel error: {ex}");
}
}
private void ClearPanelInputs()
{
try
{
_editingIndex = -1;
if (lblEditingHint != null)
lblEditingHint.Text = "新增区域";
if (txtAreaName != null)
txtAreaName.Text = "";
if (txtStationIds != null)
txtStationIds.Text = "";
if (txtControlRight != null)
txtControlRight.Text = "";
if (chkIsOccupied != null)
chkIsOccupied.Checked = false;
if (chkIsEnabled != null)
chkIsEnabled.Checked = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ClearPanelInputs error: {ex}");
}
}
#endregion
#region "1,2,3" -> List<int>
private static List<int> ParseStationIds(string text)
{
var list = new List<int>();
if (string.IsNullOrWhiteSpace(text)) return list;
foreach (var part in text.Split(new[] { ',', ';', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(part.Trim(), out int id))
list.Add(id);
}
return list;
}
#endregion
#region / / /
private void btnSave_Click(object sender, EventArgs e)
{
try
{
string areaName = txtAreaName?.Text?.Trim() ?? "";
if (string.IsNullOrEmpty(areaName))
{
MessageBox.Show("请输入区域名称。");
return;
}
var stationIds = ParseStationIds(txtStationIds?.Text ?? "");
string controlRight = txtControlRight?.Text?.Trim() ?? "";
bool isOccupied = chkIsOccupied?.Checked ?? false;
bool isEnabled = chkIsEnabled?.Checked ?? true;
if (_editingIndex >= 0 && _editingIndex < TrafficInterlockMission.TrafficAreaList.Count)
{
lock (TrafficInterlockMission.TrafficAreaList)
{
var existing = TrafficInterlockMission.TrafficAreaList[_editingIndex];
existing.AreaName = areaName;
existing.SiteList = stationIds;
existing.ControllerName = controlRight;
existing.IsOccupy = isOccupied;
existing.IsEnable = isEnabled;
}
}
else
{
lock (TrafficInterlockMission.TrafficAreaList)
{
TrafficInterlockMission.TrafficAreaList.Add(new TrafficArea
{
AreaName = areaName,
SiteList = stationIds,
ControllerName = controlRight,
IsOccupy = isOccupied,
IsEnable = isEnabled
});
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnSave_Click error: {ex}");
MessageBox.Show("操作失败:" + ex.Message);
}
}
private void btnRefresh_Click(object sender, EventArgs e)
{
try
{
RenderListView();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnRefresh_Click error: {ex}");
}
}
private void btnNew_Click(object sender, EventArgs e)
{
if (lstTasks != null)
lstTasks.SelectedIndices.Clear();
ClearPanelInputs();
// 进入新增模式:填写下方编辑区后点击“保存”即可新增一条数据
}
private void btnDelete_Click(object sender, EventArgs e)
{
try
{
if (lstTasks == null || lstTasks.SelectedIndices.Count == 0)
{
MessageBox.Show("请先在上方列表中选择要删除的区域。");
return;
}
var dialogResult = MessageBox.Show(
"确定要删除选中的区域吗?",
"确认删除",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (dialogResult != DialogResult.Yes)
return;
var indices = lstTasks.SelectedIndices.Cast<int>()
.OrderByDescending(i => i)
.ToList();
lock (TrafficInterlockMission.TrafficAreaList)
{
foreach (var idx in indices)
{
if (idx >= 0 && idx < TrafficInterlockMission.TrafficAreaList.Count)
{
TrafficInterlockMission.TrafficAreaList.RemoveAt(idx);
}
}
}
SaveToConfig();
RenderListView();
ClearPanelInputs();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"btnDelete_Click error: {ex}");
MessageBox.Show("删除失败:" + ex.Message);
}
}
#endregion
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+107
View File
@@ -0,0 +1,107 @@
using SimpleCore.Library;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace StandardScene
{
public class LadderLogic
{
/// below defines some useful functions...
private static ConcurrentDictionary<string, (DateTime dt, int state)> pressedDT = new ConcurrentDictionary<string, (DateTime dt, int state)>();
// if active for millis, trigger once.
public static void TriggerOnce(bool active, int millis, Action trigger,int index=0,
[CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
var id = $"{sourceFilePath}:{sourceLineNumber+ index}";
if (!active)
{
if (pressedDT.TryGetValue(id, out var pair1) && pair1.state != 1)
pressedDT.TryRemove(id, out _);
return;
}
if (pressedDT.TryGetValue(id, out var pair))
{
if (pair.state != 0) return;
if ((DateTime.Now - pair.dt).TotalMilliseconds > millis)
{
pressedDT[id] = (pair.dt, 1);
Task.Run(() =>
{
try
{
trigger();
}
catch (Exception ex)
{
Diagnosis.Post($"Ex={ExceptionFormatter.FormatEx(ex)}", $"timed_trigger-{id}");
pressedDT.TryRemove(id, out _);
}
pressedDT[id] = (pair.dt, 2);
});
}
}
else
pressedDT[id] = (DateTime.Now, 0);
}
private static ConcurrentDictionary<string, bool> isochronous = new ConcurrentDictionary<string, bool>();
public static object lockobj = new object();
public static void IsochronousFork(Action action,
[CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
var id = $"{sourceFilePath}:{sourceLineNumber}";
lock (lockobj)
{
if (isochronous.TryGetValue(id, out var calling) && calling) return;
isochronous[id] = true;
}
Task.Run(() =>
{
try
{
action();
}
catch (Exception ex)
{
Diagnosis.Post($"Ex={ExceptionFormatter.FormatEx(ex)}", $"isochrounous_fork-{id}");
}
isochronous[id] = false;
});
}
private static Dictionary<MethodInfo, object> keepTrack = new Dictionary<MethodInfo, object>();
/// <summary>
/// including first call.
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <param name="getter"></param>
/// <param name="action">action(T1 old)</param>
public static void TriggerIfChanged<T1>(Func<T1> getter, Action<T1> action)
{
var id = getter.Method;
var val = getter.Invoke();
if (keepTrack.TryGetValue(id, out var t) && t is T1 old)
{
if (val.Equals(old)) return;
action(old);
}
else action(default);
keepTrack[id] = val;
}
public static void FlipFlop<T1>(ref T1 target, int millis, params T1[] vs)
{
target = vs[(((long)DateTime.Now.TimeOfDay.TotalMilliseconds) / millis) % vs.Length];
}
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class ChargingSetting
{
// 充电电量下限,表示设备充电时最低的电量限制
public int CarMinBattery { get; set; }
// 充电电量上限,表示设备充电时最高的电量限制
public int CarMaxBattery { get; set; }
// 充电安全电量,表示设备充电时的安全电量阈值,低于此值可能会影响设备的正常使用
public int CarIdleChargeBattery { get; set; }
// 充电电量时长,表示设备充电所需的时间长度
public int CarIdleSecond { get; set; }
// 允许任务打断的充电电量下限,表示在执行某些任务时,设备可以容忍的最低电量限制
public int TaskAvailableBattery { get; set; }
// 闲时充电,表示设备在空闲状态下是否进行充电
public bool IsChargingDuringIdleTime { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class EnvelopeSetting
{
public string Name { get; set; }
public string Description { get; set; }
public string Value { get; set; }
}
}
+76
View File
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
/// <summary>
/// 任务类别
/// </summary>
public enum TaskKind
{
Loop,
BranchPoint,
JoinPoint
}
/// <summary>
/// 任务启动类型:API/PLC/按钮盒/自动循环
/// </summary>
public enum TaskStartType
{
Api,
Plc,
ButtonBox,
AutoLoop,
Charge
}
public class LoopTask
{
/// <summary>
/// 任务唯一标识ID(自增)
/// </summary>
public int Id { get; set; } = 0;
/// <summary>
/// 任务类别(枚举:循环/分流点/汇合点)
/// </summary>
public TaskKind Kind { get; set; } = TaskKind.Loop;
/// <summary>
/// 当前点 ID(整数)
/// </summary>
public int CurrentStationId { get; set; } = -1;
/// <summary>
/// 目标点 ID(整数)
/// </summary>
public int TargetStationId { get; set; } = -1;
/// <summary>
/// 流量控制(整数,可代表信号级别或策略编号)
/// </summary>
public int TrafficControl { get; set; } = 0;
/// <summary>
/// 优先级(整数)
/// </summary>
public int Priority { get; set; } = 1;
/// <summary>
/// 是否为途径点(布尔)
/// </summary>
public bool IsViaPoint { get; set; } = false;
/// <summary>
/// 启动类型(枚举:Api/Plc/ButtonBox/AutoLoop
/// </summary>
public TaskStartType StartType { get; set; } = TaskStartType.AutoLoop;
public string[] tags = Array.Empty<string>();
}
}
+241
View File
@@ -0,0 +1,241 @@
using SimpleCore.PropType;
using SimpleCore;
using System.Collections.Generic;
using System.Dynamic;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.UI;
using System.Numerics;
using System.Drawing;
using System;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
namespace StandardScene.Model
{
public class Map
{
public List<ExpandoObject> Sites { get; set; }
public List<ExpandoObject> Tracks { get; set; }
public List<ExpandoObject> CircularArcTracks { get; set; }
public List<ExpandoObject> BezierTracks { get; set; }
public static Map GetMap()
{
var map = new Map
{
Sites = new List<ExpandoObject>()
};
Site[] allSites = SimpleLib.GetAllSites();
foreach (var site in allSites)
{
dynamic theSite = new ExpandoObject();
theSite.Id = ((Prop)site).id;
theSite.Name = ((Prop)site).name;
theSite.X = site.x;
theSite.Y = site.y;
theSite.Fields = new Dictionary<string, string>();
foreach (var kv in site.fields)
{
theSite.Fields.Add(kv.Key, kv.Value);
}
theSite.Type = theSite.Fields.ContainsKey("Standby") ? "standby" : theSite.Fields.ContainsKey("Charge") ? "charge" : theSite.Fields.ContainsKey("Shelf") ? "shelf" : "default";
map.Sites.Add(theSite);
}
map.Tracks = new List<ExpandoObject>();
map.CircularArcTracks = new List<ExpandoObject>();
map.BezierTracks = new List<ExpandoObject>();
var allTracks = SimpleLib.GetAllTracks();
foreach (var track in allTracks)
{
switch (track)
{
case UITrack uiTrack:
{
dynamic theTrack = new ExpandoObject();
theTrack.Id = uiTrack.id;
theTrack.A = uiTrack.siteA;
theTrack.B = uiTrack.siteB;
theTrack.Fields = new Dictionary<string, string>();
foreach (KeyValuePair<string, string> kv in uiTrack.fields)
{
theTrack.Fields.Add(kv.Key, kv.Value);
}
theTrack.Direction = uiTrack.direction switch
{
1 => "a2b",
2 => "b2a",
_ => "both"
};
theTrack.Type = 0;
map.Tracks.Add(theTrack);
break;
}
case UICircularArcTrack circularArcTrack:
{
dynamic theTrack = new ExpandoObject();
theTrack.Id = circularArcTrack.id;
theTrack.A = circularArcTrack.siteA;
theTrack.B = circularArcTrack.siteB;
theTrack.Fields = new Dictionary<string, string>();
foreach (var kv in circularArcTrack.fields)
{
theTrack.Fields.Add(kv.Key, kv.Value);
}
theTrack.Direction = circularArcTrack.direction switch
{
1 => "a2b",
2 => "b2a",
_ => "both"
};
//添加原点坐标、半径、开始角度、结束角度
theTrack.Type = 1;
theTrack.CenterX = circularArcTrack.Arc.Center.X;
theTrack.CenterY = circularArcTrack.Arc.Center.Y;
theTrack.Radius = circularArcTrack.Arc.Radius;
theTrack.AngleStart = circularArcTrack.Arc.AngleStart;
theTrack.AngleEnd = circularArcTrack.Arc.AngleEnd;
//获取圆弧的控制点
var controlPoint = ArcHelper.CalculateControlPoint(
circularArcTrack.Arc.PointStart.X, circularArcTrack.Arc.PointStart.Y,
circularArcTrack.Arc.PointEnd.X, circularArcTrack.Arc.PointEnd.Y,
circularArcTrack.Arc.Center.X, circularArcTrack.Arc.Center.Y,
circularArcTrack.Arc.Radius,
circularArcTrack.Arc.AngleStart, circularArcTrack.Arc.AngleEnd);
theTrack.ControlPointsX = controlPoint.X;
theTrack.ControlPointsY = controlPoint.Y;
map.CircularArcTracks.Add(theTrack);
break;
}
case UIBezierTrack bezierTrack:
{
dynamic theTrack = new ExpandoObject();
theTrack.Id = bezierTrack.id;
theTrack.A = bezierTrack.siteA;
theTrack.B = bezierTrack.siteB;
theTrack.Fields = new Dictionary<string, string>();
foreach (var kv in bezierTrack.fields)
{
theTrack.Fields.Add(kv.Key, kv.Value);
}
theTrack.Direction = bezierTrack.direction switch
{
1 => "a2b",
2 => "b2a",
_ => "both"
};
//根据typeInfo获取控制点个数
theTrack.Type = 2;
var array = bezierTrack.typeInfo.Split(',');
var num = int.Parse(array[1]);//获取控制点的个数(包括起点和终点)
var list = new List<Vector2>();
for (var i = 0; i < num; i++)
{
//只记录中间的控制点
if (i <= 0 || i >= num - 1) continue;
float x = float.Parse(array[2 + i * 2]);
float y = float.Parse(array[3 + i * 2]);
list.Add(new Vector2(x, y));
}
theTrack.controlPoints = list;
map.BezierTracks.Add(theTrack);
break;
}
default:
break;
}
}
return map;
}
}
public class ArcHelper
{
/// <summary>
/// 获取圆弧控制点
/// </summary>
/// <returns></returns>
public static PointF CalculateControlPoint(float pointStartX, float pointStartY, float pointEndX, float pointEndY, float centerX, float centerY, float radius, float angleStart, float angleEnd)
{
PointF center = new PointF(centerX, centerY);
double startAngle = angleStart * 3.14159 / 180.0;
double endAngel = angleEnd * 3.14159 / 180.0;
if (angleStart > angleEnd)//如果起始角度小于结束角度就加上360度
{
endAngel += 2 * 3.14159;
}
PointF midPoint = new PointF(
(float)(center.X + radius * Math.Cos((startAngle + endAngel) / 2)),
(float)(center.Y + radius * Math.Sin((startAngle + endAngel) / 2)));
return midPoint;
}
}
public class LidarMap
{
public string LidarMapsBase64 { get; set; }
public float DistanceX { get; set; }
public float DistanceY { get; set; }
public float Ratio { get; set; }
private static readonly HttpClient hc = new HttpClient();
public static async Task<LidarMap> GetLidarMap()
{
var par = JsonConvert.DeserializeAnonymousType(await hc.GetStringAsync($"http://127.0.0.1:4321/getMapParameters"),
new { up = 0f, down = 0f, left = 0f, right = 0f, pt = 0 });
var lengthWidthRatio =Math.Abs((par.right - par.left) / (par.up - par.down));
var height = 1024;var width = 1024;
if (lengthWidthRatio > 1)
{
height = (int)(1024 / lengthWidthRatio);
}
else
{
width = (int)(1024 * lengthWidthRatio);
}
var mapBmp= new Bitmap((await hc.GetStreamAsync($"http://127.0.0.1:4321/getMapPng?width={width}&height={height}")));
for(int x =0;x<mapBmp.Width;x++)
{
for (int y = 0; y < mapBmp.Height; y++)
{
Color pixColor = mapBmp.GetPixel(x, y);
if (pixColor.R != 255 || pixColor.G != 255 || pixColor.B != 255)
{
mapBmp.SetPixel(x, y, Color.FromArgb(0,pixColor));
}
else
{
mapBmp.SetPixel(x,y,Color.Black);
}
}
}
return new LidarMap
{
LidarMapsBase64 = "data:image/png;base64," + BitmapToBase64(mapBmp, ImageFormat.Png),
DistanceX = par.left,
DistanceY = par.up,
Ratio = lengthWidthRatio > 1?Math.Abs(par.left-par.right)/1024: Math.Abs(par.up - par.down)/1024,
};
}
static string BitmapToBase64(Bitmap bmp, ImageFormat format)
{
using MemoryStream ms = new MemoryStream();
bmp.Save(ms, format); // 保存格式可以是Png, Jpeg等
byte[] imageBytes = ms.ToArray();
string base64String = Convert.ToBase64String(imageBytes);
return base64String;
}
}
}
+226
View File
@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class MapStructure
{
public FassMap Map { get; set; }
public List<FassNode> Nodes { get; set; }
public List<Edge> Edges { get; set; }
public List<object> Zones { get; set; }
public List<object> Tags { get; set; }
}
public class FassMap
{
public int Index { get; set; }
public string Id { get; set; }
public string Kind { get; set; }
public string Type { get; set; }
public Base Base { get; set; }
public Image Image { get; set; }
public List<object> Extends { get; set; }
}
public class Base
{
public bool Visible { get; set; }
public Size Size { get; set; }
public double GlobalAlpha { get; set; }
public int LineWidth { get; set; }
public List<int> LineDash { get; set; }
public string StrokeStyle { get; set; }
public string FillStyle { get; set; }
public Center Center { get; set; }
}
public class Size
{
public double W { get; set; }
public double H { get; set; }
}
public class Center
{
public double X { get; set; }
public double Y { get; set; }
}
public class Image
{
public bool Visible { get; set; }
public double GlobalAlpha { get; set; }
public string Src { get; set; }
public bool Origin { get; set; }
public bool Manual { get; set; }
public ManualPoint ManualPoint { get; set; }
public ManualSize ManualSize { get; set; }
}
public class ManualPoint
{
public double X { get; set; }
public double Y { get; set; }
}
public class ManualSize
{
public double W { get; set; }
public double H { get; set; }
}
public class FassNode
{
public int Index { get; set; }
public string Id { get; set; }
public string Kind { get; set; }
public string Type { get; set; }
public NodeBase Base { get; set; }
public Code Code { get; set; }
public Name Name { get; set; }
public Image Image { get; set; }
public Lock Lock { get; set; }
public Data Data { get; set; }
public List<object> Extends { get; set; }
}
public class NodeBase
{
public bool Visible { get; set; }
public Point Point { get; set; }
public Size Size { get; set; }
public double GlobalAlpha { get; set; }
public int LineWidth { get; set; }
public List<int> LineDash { get; set; }
public string StrokeStyle { get; set; }
public string FillStyle { get; set; }
public Center Center { get; set; }
}
public class Point
{
public double X { get; set; }
public double Y { get; set; }
}
public class Code
{
public bool Visible { get; set; }
public double GlobalAlpha { get; set; }
public string Font { get; set; }
public string FillStyle { get; set; }
public string Text { get; set; }
}
public class Name
{
public bool Visible { get; set; }
public double GlobalAlpha { get; set; }
public string Font { get; set; }
public string FillStyle { get; set; }
public string Text { get; set; }
}
public class Lock
{
public bool Enable { get; set; }
}
public class Data
{
public string NodeId { get; set; }
public int SequenceId { get; set; }
public string NodeDescription { get; set; }
public bool Released { get; set; }
public NodePosition NodePosition { get; set; }
public List<FassAction> Actions { get; set; }
}
public class NodePosition
{
public double X { get; set; }
public double Y { get; set; }
public string MapId { get; set; }
}
public class FassAction
{
public bool Action0 { get; set; }
public string ActionId { get; set; }
public List<FassActionParameter> ActionParameters { get; set; }
public string ActionType { get; set; }
public string BlockingType { get; set; }
public int SortNumber { get; set; }
}
public class FassActionParameter
{
public bool Parameter0 { get; set; }
public string Key { get; set; }
public string Value { get; set; }
}
public class Edge
{
public int Index { get; set; }
public string Id { get; set; }
public string Kind { get; set; }
public string Type { get; set; }
public EdgeBase Base { get; set; }
public Code Code { get; set; }
public Name Name { get; set; }
public Lock Lock { get; set; }
public EdgeData Data { get; set; }
public List<object> Extends { get; set; }
}
public class EdgeBase
{
public bool Visible { get; set; }
public Point Point { get; set; }
public Size Size { get; set; }
public double GlobalAlpha { get; set; }
public int LineWidth { get; set; }
public List<int> LineDash { get; set; }
public string StrokeStyle { get; set; }
public string FillStyle { get; set; }
public Center Center { get; set; }
public bool IsOneway { get; set; }
public double Width { get; set; }
public Node StartNode { get; set; }
public Node EndNode { get; set; }
}
public class EdgeData
{
public string EdgeId { get; set; }
public int SequenceId { get; set; }
public string EdgeDescription { get; set; }
public bool Released { get; set; }
public string StartNodeId { get; set; }
public string EndNodeId { get; set; }
public double MaxSpeed { get; set; }
public double MaxHeight { get; set; }
public double MinHeight { get; set; }
public double Orientation { get; set; }
public string OrientationType { get; set; }
public string Direction { get; set; }
public bool RotationAllowed { get; set; }
public double MaxRotationSpeed { get; set; }
public double Length { get; set; }
public Trajectory Trajectory { get; set; }
public List<object> Actions { get; set; }
}
public class Trajectory
{
public double Degree { get; set; }
public List<int> KnotVector { get; set; }
public List<object> ControlPoints { get; set; }
}
}
+48
View File
@@ -0,0 +1,48 @@
using System;
namespace StandardScene.Model
{
public class MissionState
{
public string MissionId { get; set; }
public string CarCode {get; set; }
public DateTime TriggerTime { get; set; }
public enum MissionStateEnum
{
/// <summary>
/// 创建任务。
/// </summary>
Created = 1,
/// <summary>
/// 子任务启动。
/// </summary>
Started = 2,
/// <summary>
/// 子任务完成。
/// </summary>
Finished = 3,
/// <summary>
/// 子任务失败。
/// </summary>
Failed = 4,
/// <summary>
/// 子任务的取货完成,发生在Started之后。
/// </summary>
Fetched = 5,
/// <summary>
/// 子任务的放货完成,发生在Started之后。
/// </summary>
Put = 6
}
public MissionStateEnum State { get; set; }
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class PlanRulesSetting
{
public string Name { get; set; }
public string Description { get; set; }
public string Value { get; set; }
public string NodeId { get; set; }
}
}
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class SimpleConfig
{
public string Autoload { get; set; }
public int Port { get; set; }
public string Ip { get; set; }
public bool AllowMultiple { get; set; }
}
}
+124
View File
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace StandardScene.Model
{
public class Configuration
{
public Conf conf { get; set; }
public Dictionary<string, SimpleMission> Missions { get; set; }
public Dictionary<string, SimpleMap> Maps { get; set; }
public Dictionary<string, SimpleSite> Sites { get; set; }
public Dictionary<string, Track> Tracks { get; set; }
public Dictionary<string, object> Cars { get; set; } // Assuming Cars is an empty object
public Dictionary<string, object> PrologScripts { get; set; } // Assuming PrologScripts is an empty object
public Configuration()
{
// Initialize Conf with default values
conf = new Conf
{
PRICE_RANGE = 10000.0,
fields = new Dictionary<string, object>(),
Car_UpdateInterval = 300,
Search_AllowDestOnRoute = false,
Search_RouteOnDestClearance = 0,
Search_MaxVisitPerSite = 1,
Search_MaxDupVisit = 1,
Search_MaxKeepResult = 16,
Traffic_MaxHoldingLocks = 3,
Traffic_DeadLockSearchDepth = 0,
DebugTypes = "S",
Auto_Reprogram = false
};
Missions = new Dictionary<string, SimpleMission>();
Maps = new Dictionary<string, SimpleMap>();
Sites = new Dictionary<string, SimpleSite>();
Tracks = new Dictionary<string, Track>();
Cars = new Dictionary<string, object>();
PrologScripts = new Dictionary<string, object>();
}
}
public class Conf
{
public double PRICE_RANGE { get; set; }
public Dictionary<string, object> fields { get; set; } // Assuming fields is an empty object
public int Car_UpdateInterval { get; set; }
public bool Search_AllowDestOnRoute { get; set; }
public int Search_RouteOnDestClearance { get; set; }
public int Search_MaxVisitPerSite { get; set; }
public int Search_MaxDupVisit { get; set; }
public int Search_MaxKeepResult { get; set; }
public int Traffic_MaxHoldingLocks { get; set; }
public int Traffic_DeadLockSearchDepth { get; set; }
public string DebugTypes { get; set; }
public bool Auto_Reprogram { get; set; }
}
public class SimpleMission
{
public string type { get; set; }
public MissionOptions options { get; set; }
}
public class MissionOptions
{
public bool autoStart { get; set; }
public int id { get; set; }
public string layerName { get; set; }
public string name { get; set; }
public Dictionary<string, object> fields { get; set; } // Assuming fields is an empty object
}
public class SimpleMap
{
public string type { get; set; }
public MapOptions options { get; set; }
}
public class MapOptions
{
public string filename { get; set; }
public int id { get; set; }
public string layerName { get; set; }
public string name { get; set; }
public Dictionary<string, object> fields { get; set; } // Assuming fields is an empty object
}
public class SimpleSite
{
public string color { get; set; }
public string displaySetting { get; set; }
public double x { get; set; }
public double y { get; set; }
public int id { get; set; }
public string layerName { get; set; }
public string name { get; set; }
public Dictionary<string, string> fields { get; set; }
public List<object> mustFree { get; set; } // Assuming mustFree is an empty array
}
public class Track
{
public string displaySetting { get; set; }
public int siteA { get; set; }
public int siteB { get; set; }
public int direction { get; set; }
public int id { get; set; }
public string layerName { get; set; }
public string name { get; set; }
public Dictionary<string, string> fields { get; set; }
public int _siteA { get; set; }
public int _siteB { get; set; }
public string typeInfo { get; set; }
// public Dictionary<string, object> fields { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More