init commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace StandardScene.CarTypes
|
||||
{
|
||||
// Kiva 系字段袋。原内联于 Kiva.cs;车型按平台拆分(Kiva→Magnetic,ArmCar→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→Magnetic,MultiVehicleCar→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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 ctor(CLR 保证线程安全)
|
||||
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-2:OnMissionFinished--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=TransportMission,TriggerMethod=EnqueueTransportByButton,TriggerMethodParams=取货站点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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
// 跨程序集解析:充电桩具体类型可能位于卫星插件 dll(StandardScene.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(方案 B:4 参 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 && dst.CarWidth!=-1):
|
||||
/// - Forklift:useVerb/模板逐字一致,行为不变;
|
||||
/// - 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.000Z(Unix 毫秒)
|
||||
// 如果你希望生成的数字更短,可以在构造函数里传入更“近”的 _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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. ChargeUdpService(UDP监听服务)
|
||||
|
||||
**文件位置**: `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报文自动解析和更新
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.Json(NuGet)
|
||||
|
||||
### 硬件要求
|
||||
- 内存:数据量小,几乎无影响
|
||||
- 磁盘:每个充电桩约 1KB 数据
|
||||
- CPU:UI操作,几乎无影响
|
||||
|
||||
### 兼容性
|
||||
- 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现示例
|
||||
|
||||
### 示例1:ModbusDoorController(完整实现)
|
||||
|
||||
```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的类
|
||||
// 跨程序集发现:按钮盒具体类型可能位于卫星插件 dll(StandardScene.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<bool> 等异步返回类型。
|
||||
/// 返回 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
|
||||
// 跨程序集发现:门控制器具体类型可能位于卫星插件 dll(StandardScene.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user