安全功能各种修改、细节调整
This commit is contained in:
parent
61819a3139
commit
a5a36d466f
117
Docs/警灯代码.html
Normal file
117
Docs/警灯代码.html
Normal file
@ -0,0 +1,117 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>警示灯图标预览 v2</title>
|
||||
<style>
|
||||
body{font-family:Segoe UI,Microsoft YaHei,sans-serif;background:#f5f5f5;margin:28px;color:#222}
|
||||
h2{margin:0 0 8px;font-size:20px} p{margin:0 0 22px;color:#555;font-size:14px}
|
||||
.row{display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap}
|
||||
.card{background:white;border:1px solid #ddd;border-radius:12px;padding:18px;width:180px;text-align:center;box-shadow:0 2px 10px #0001}
|
||||
.stage{height:100px;display:flex;align-items:center;justify-content:center}
|
||||
.small .stage{height:58px}
|
||||
.label{margin-top:10px;font-size:14px;color:#555}
|
||||
svg.big{width:82px;height:82px} svg.smallIcon{width:34px;height:34px}
|
||||
|
||||
.inactive .glassMain{fill:url(#grayGlass)}
|
||||
.inactive .glassCap{fill:url(#grayCap)}
|
||||
.inactive .rib{stroke:#555}
|
||||
.inactive .baseMain{fill:url(#grayBase)}
|
||||
.inactive .redOnly{opacity:0}
|
||||
|
||||
.active .blinkLayer{animation:hardBlink .72s steps(2,end) infinite}
|
||||
.active .sweep{animation:sweepMove .95s linear infinite}
|
||||
.active .rim{animation:rimBlink .72s steps(2,end) infinite}
|
||||
.active .baseFlash{animation:baseFlash .72s steps(2,end) infinite}
|
||||
|
||||
@keyframes hardBlink{0%,44%{opacity:1}45%,100%{opacity:.42}}
|
||||
@keyframes rimBlink{0%,44%{opacity:1;stroke-width:2.3}45%,100%{opacity:.35;stroke-width:1.2}}
|
||||
@keyframes baseFlash{0%,44%{opacity:1}45%,100%{opacity:.15}}
|
||||
@keyframes sweepMove{from{transform:translateX(-24px)}to{transform:translateX(30px)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>警示灯图标预览 v2</h2>
|
||||
<p>去掉外部红色光晕,改为灯罩本体强闪 + 内部扫光。小尺寸下动态更明显。</p>
|
||||
<div class="row">
|
||||
<div class="card inactive">
|
||||
<div class="stage">
|
||||
<svg class="big" viewBox="0 0 64 64" role="img" aria-label="inactive warning light">
|
||||
<defs>
|
||||
<radialGradient id="redGlass" cx="34%" cy="16%" r="78%"><stop offset="0" stop-color="#ff8a8a"/><stop offset=".36" stop-color="#df0010"/><stop offset="1" stop-color="#720006"/></radialGradient>
|
||||
<radialGradient id="redCap" cx="38%" cy="12%" r="85%"><stop offset="0" stop-color="#ffb0b0"/><stop offset=".5" stop-color="#f00012"/><stop offset="1" stop-color="#8a0008"/></radialGradient>
|
||||
<radialGradient id="grayGlass" cx="34%" cy="16%" r="78%"><stop offset="0" stop-color="#dedede"/><stop offset=".36" stop-color="#969696"/><stop offset="1" stop-color="#4b4b4b"/></radialGradient>
|
||||
<radialGradient id="grayCap" cx="38%" cy="12%" r="85%"><stop offset="0" stop-color="#f0f0f0"/><stop offset=".5" stop-color="#aaa"/><stop offset="1" stop-color="#5d5d5d"/></radialGradient>
|
||||
<linearGradient id="base" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#3b3b3b"/><stop offset=".45" stop-color="#101010"/><stop offset="1" stop-color="#303030"/></linearGradient>
|
||||
<linearGradient id="grayBase" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#666"/><stop offset=".45" stop-color="#303030"/><stop offset="1" stop-color="#565656"/></linearGradient>
|
||||
<clipPath id="clipGlass"><path d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z"/></clipPath>
|
||||
</defs>
|
||||
<path class="glassMain" d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z" fill="url(#redGlass)"/>
|
||||
<path class="glassCap" d="M15 26C16 11.8 23.8 5 32 5s16 6.8 17 21c-8-3.1-25.9-3.1-34 0z" fill="url(#redCap)" opacity=".95"/>
|
||||
<g opacity=".38" clip-path="url(#clipGlass)"><path class="rib" d="M18 22v23M22 18v27M26 15v30M30 14v31M34 14v31M38 15v30M42 18v27M46 22v23" stroke="#880007" stroke-width="1"/></g>
|
||||
<path d="M25 13l7-3 3 6-8 3zM38 11c4 1 6 3 8 7l-5 2c-1-3-3-4-6-5z" fill="#fff" opacity=".72"/>
|
||||
<ellipse cx="32" cy="44" rx="22" ry="5" fill="#1a0000" opacity=".48"/>
|
||||
<path class="redOnly baseFlash" d="M13 42h38v2H13z" fill="#ff0014"/>
|
||||
<path d="M11 43h42v6H11z" fill="#240004" opacity=".88"/>
|
||||
<path class="baseMain" d="M9 48h46v8c0 3-3 5-6 5H15c-3 0-6-2-6-5z" fill="url(#base)"/>
|
||||
<path d="M13 50h38" stroke="#777" stroke-width="1" opacity=".45"/>
|
||||
<path d="M42 56v4M47 55v4M52 54v3" stroke="#050505" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="label">未激活:灰色静态</div>
|
||||
</div>
|
||||
|
||||
<div class="card active">
|
||||
<div class="stage">
|
||||
<svg class="big" viewBox="0 0 64 64" role="img" aria-label="active warning light">
|
||||
<defs>
|
||||
<radialGradient id="redGlass2" cx="34%" cy="16%" r="78%"><stop offset="0" stop-color="#ff8a8a"/><stop offset=".36" stop-color="#df0010"/><stop offset="1" stop-color="#720006"/></radialGradient>
|
||||
<radialGradient id="redCap2" cx="38%" cy="12%" r="85%"><stop offset="0" stop-color="#ffb0b0"/><stop offset=".5" stop-color="#f00012"/><stop offset="1" stop-color="#8a0008"/></radialGradient>
|
||||
<linearGradient id="base2" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#3b3b3b"/><stop offset=".45" stop-color="#101010"/><stop offset="1" stop-color="#303030"/></linearGradient>
|
||||
<clipPath id="clipGlass2"><path d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z"/></clipPath>
|
||||
</defs>
|
||||
<g class="blinkLayer">
|
||||
<path d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z" fill="url(#redGlass2)"/>
|
||||
<path d="M15 26C16 11.8 23.8 5 32 5s16 6.8 17 21c-8-3.1-25.9-3.1-34 0z" fill="url(#redCap2)" opacity=".95"/>
|
||||
</g>
|
||||
<path class="rim" d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z" fill="none" stroke="#ff2635" stroke-width="2"/>
|
||||
<g opacity=".38" clip-path="url(#clipGlass2)"><path d="M18 22v23M22 18v27M26 15v30M30 14v31M34 14v31M38 15v30M42 18v27M46 22v23" stroke="#650005" stroke-width="1"/></g>
|
||||
<g clip-path="url(#clipGlass2)">
|
||||
<path class="sweep" d="M17 6h7l-11 42h-7z" fill="#fff" opacity=".62"/>
|
||||
</g>
|
||||
<path d="M25 13l7-3 3 6-8 3zM38 11c4 1 6 3 8 7l-5 2c-1-3-3-4-6-5z" fill="#fff" opacity=".78"/>
|
||||
<ellipse cx="32" cy="44" rx="22" ry="5" fill="#1a0000" opacity=".48"/>
|
||||
<path class="baseFlash" d="M13 42h38v2H13z" fill="#ff0014"/>
|
||||
<path d="M11 43h42v6H11z" fill="#240004" opacity=".88"/>
|
||||
<path d="M9 48h46v8c0 3-3 5-6 5H15c-3 0-6-2-6-5z" fill="url(#base2)"/>
|
||||
<path d="M13 50h38" stroke="#777" stroke-width="1" opacity=".45"/>
|
||||
<path d="M42 56v4M47 55v4M52 54v3" stroke="#050505" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="label">激活:本体强闪 + 扫光</div>
|
||||
</div>
|
||||
|
||||
<div class="card active small">
|
||||
<div class="stage">
|
||||
<svg class="smallIcon" viewBox="0 0 64 64" role="img" aria-label="small active warning light">
|
||||
<defs>
|
||||
<radialGradient id="redGlass3" cx="34%" cy="16%" r="78%"><stop offset="0" stop-color="#ff8a8a"/><stop offset=".36" stop-color="#df0010"/><stop offset="1" stop-color="#720006"/></radialGradient>
|
||||
<linearGradient id="base3" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#3b3b3b"/><stop offset=".45" stop-color="#101010"/><stop offset="1" stop-color="#303030"/></linearGradient>
|
||||
<clipPath id="clipGlass3"><path d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z"/></clipPath>
|
||||
</defs>
|
||||
<path class="blinkLayer" d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z" fill="url(#redGlass3)"/>
|
||||
<path class="rim" d="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z" fill="none" stroke="#ff2635" stroke-width="2"/>
|
||||
<g clip-path="url(#clipGlass3)"><path class="sweep" d="M17 6h8l-11 42h-8z" fill="#fff" opacity=".7"/></g>
|
||||
<path d="M25 13l7-3 3 6-8 3zM38 11c4 1 6 3 8 7l-5 2c-1-3-3-4-6-5z" fill="#fff" opacity=".78"/>
|
||||
<ellipse cx="32" cy="44" rx="22" ry="5" fill="#1a0000" opacity=".48"/>
|
||||
<path class="baseFlash" d="M13 42h38v2H13z" fill="#ff0014"/>
|
||||
<path d="M11 43h42v6H11z" fill="#240004" opacity=".88"/>
|
||||
<path d="M9 48h46v8c0 3-3 5-6 5H15c-3 0-6-2-6-5z" fill="url(#base3)"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="label">34px 小图标预览</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -93,9 +93,9 @@ public sealed class MockSspc : MockDeviceBase, ISspc
|
||||
return Task.FromResult(new ChannelMeasurement
|
||||
{
|
||||
PhysicalSlot = physicalSlot,
|
||||
Voltage = on ? 28.0 : 0.0,
|
||||
Voltage = 28.0,
|
||||
Current = on ? 1.0 : 0.0,
|
||||
VoltageRaw = on ? (ushort)2800 : (ushort)0,
|
||||
VoltageRaw = 2800,
|
||||
CurrentRaw = on ? (short)167 : (short)0,
|
||||
Timestamp = DateTimeOffset.Now
|
||||
});
|
||||
|
||||
@ -9,6 +9,9 @@ public partial class ChannelResult : ObservableObject
|
||||
[ObservableProperty] private double _voltageOn;
|
||||
[ObservableProperty] private double _currentOn;
|
||||
[ObservableProperty] private double _voltageOff;
|
||||
[ObservableProperty] private double _currentOff;
|
||||
[ObservableProperty] private double _displayVoltage;
|
||||
[ObservableProperty] private double _displayCurrent;
|
||||
[ObservableProperty] private TestStatus _status = TestStatus.Untested;
|
||||
}
|
||||
|
||||
@ -152,6 +155,8 @@ public static class ProtectTestPlan
|
||||
row.Trigger = item.Trigger;
|
||||
if (string.IsNullOrWhiteSpace(row.JudgeMode))
|
||||
row.JudgeMode = ManualJudgeMode;
|
||||
if (row.Status == TestStatus.Measured)
|
||||
row.Status = TestStatus.Untested;
|
||||
|
||||
project.ProtectResults.Add(row);
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ public sealed class ReportSelection
|
||||
public bool Curve { get; set; } = true;
|
||||
public bool Power { get; set; } = true;
|
||||
public bool Protect { get; set; } = true;
|
||||
public string FileName { get; set; } = "report_{yyyyMMdd_HHmmss}.pdf";
|
||||
public string FileName { get; set; } = "{ProjectName}_{yyyyMMdd_HHmmss}.pdf";
|
||||
}
|
||||
|
||||
/// <summary>四个测试页“测试说明”手动记录内容。</summary>
|
||||
|
||||
@ -349,12 +349,12 @@ public sealed class ReportGenerator
|
||||
}
|
||||
|
||||
private static void BasicTable(IContainer container, Project p) =>
|
||||
DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 0.8f },
|
||||
new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "判定" },
|
||||
DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 1.15f, 0.8f },
|
||||
new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "关后电流\nA", "判定" },
|
||||
p.BasicResults.Select(r => new ReportRow(r.Status, new[]
|
||||
{
|
||||
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
|
||||
Format(r.VoltageOff, "F2"), StatusText(r.Status)
|
||||
Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), StatusText(r.Status)
|
||||
})));
|
||||
|
||||
private static void CurveTable(IContainer container, Project p) =>
|
||||
@ -386,7 +386,7 @@ public sealed class ReportGenerator
|
||||
ValueOrDash(r.Trigger),
|
||||
ValueOrDash(r.MeasuredValue),
|
||||
ValueOrDash(r.JudgeMode),
|
||||
StatusText(r.Status)
|
||||
ProtectStatusText(r.Status)
|
||||
})));
|
||||
|
||||
private static void DataTable(IContainer container, IReadOnlyList<float> widths,
|
||||
@ -501,6 +501,13 @@ public sealed class ReportGenerator
|
||||
TestStatus.Running => "测试中", TestStatus.Measured => "已测",
|
||||
_ => "未测试"
|
||||
};
|
||||
private static string ProtectStatusText(TestStatus status) => status switch
|
||||
{
|
||||
TestStatus.Pass => "合格",
|
||||
TestStatus.Fail => "不合格",
|
||||
TestStatus.Running => "测试中",
|
||||
_ => "未判定"
|
||||
};
|
||||
private static string StatusColor(TestStatus status) => status switch
|
||||
{
|
||||
TestStatus.Pass => Success, TestStatus.Fail => Danger,
|
||||
|
||||
@ -13,7 +13,7 @@ public enum ReportOutputFormat
|
||||
|
||||
public static class ReportFileHelper
|
||||
{
|
||||
public const string DefaultTemplate = "report_{yyyyMMdd_HHmmss}.pdf";
|
||||
public const string DefaultTemplate = "{ProjectName}_{yyyyMMdd_HHmmss}.pdf";
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
|
||||
@ -189,15 +189,16 @@ internal sealed class LegacyReportGenerator
|
||||
{
|
||||
t.ColumnsDefinition(cd =>
|
||||
{
|
||||
cd.ConstantColumn(60); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(70);
|
||||
cd.ConstantColumn(60); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(70);
|
||||
});
|
||||
HeaderRow(t, "通道", "电压(V)", "电流(A)", "关后电压(V)", "结果");
|
||||
HeaderRow(t, "通道", "开电压(V)", "开电流(A)", "关电压(V)", "关电流(A)", "结果");
|
||||
foreach (var r in p.BasicResults)
|
||||
{
|
||||
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
||||
t.Cell().Padding(3).Text(r.VoltageOn.ToString("F2"));
|
||||
t.Cell().Padding(3).Text(r.CurrentOn.ToString("F3"));
|
||||
t.Cell().Padding(3).Text(r.VoltageOff.ToString("F2"));
|
||||
t.Cell().Padding(3).Text(r.CurrentOff.ToString("F3"));
|
||||
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
||||
}
|
||||
});
|
||||
@ -271,7 +272,7 @@ internal sealed class LegacyReportGenerator
|
||||
t.Cell().Padding(3).Text(r.Trigger);
|
||||
t.Cell().Padding(3).Text(r.MeasuredValue);
|
||||
t.Cell().Padding(3).Text(r.JudgeMode);
|
||||
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
||||
t.Cell().Padding(3).Text(ProtectStatusText(r.Status)).FontColor(StatusColor(r.Status));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -292,6 +293,13 @@ internal sealed class LegacyReportGenerator
|
||||
TestStatus.Measured => "已测",
|
||||
_ => "未测试"
|
||||
};
|
||||
private static string ProtectStatusText(TestStatus s) => s switch
|
||||
{
|
||||
TestStatus.Pass => "合格",
|
||||
TestStatus.Fail => "不合格",
|
||||
TestStatus.Running => "测试中",
|
||||
_ => "未判定"
|
||||
};
|
||||
|
||||
private static string StatusColor(TestStatus s) => s switch
|
||||
{
|
||||
|
||||
@ -118,11 +118,11 @@ public sealed class WordReportGenerator
|
||||
Section(body, section++, "基础测试", "BASIC FUNCTION TEST");
|
||||
if (p.BasicResults.Count > 0)
|
||||
body.Append(ResultTable(
|
||||
new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "判定" },
|
||||
new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "关后电流 A", "判定" },
|
||||
p.BasicResults.Select(r => new[]
|
||||
{
|
||||
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
|
||||
Format(r.VoltageOff, "F2"), StatusText(r.Status)
|
||||
Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), StatusText(r.Status)
|
||||
})));
|
||||
ManualNote(body, p.TestFunction?.Basic);
|
||||
}
|
||||
@ -174,7 +174,7 @@ public sealed class WordReportGenerator
|
||||
ValueOrDash(r.Trigger),
|
||||
ValueOrDash(r.MeasuredValue),
|
||||
ValueOrDash(r.JudgeMode),
|
||||
StatusText(r.Status)
|
||||
ProtectStatusText(r.Status)
|
||||
})));
|
||||
ManualNote(body, p.TestFunction?.Protect);
|
||||
}
|
||||
@ -417,6 +417,13 @@ public sealed class WordReportGenerator
|
||||
TestStatus.Measured => "已测",
|
||||
_ => "未测试"
|
||||
};
|
||||
private static string ProtectStatusText(TestStatus status) => status switch
|
||||
{
|
||||
TestStatus.Pass => "合格",
|
||||
TestStatus.Fail => "不合格",
|
||||
TestStatus.Running => "测试中",
|
||||
_ => "未判定"
|
||||
};
|
||||
|
||||
private static string StatusColor(TestStatus status) => status switch
|
||||
{
|
||||
|
||||
@ -26,6 +26,9 @@ public sealed class BasicTest : ITestModule
|
||||
var on = await ctx.Sspc.ReadMeasurementAsync(ch, ct).ConfigureAwait(false);
|
||||
row.VoltageOn = on.Voltage;
|
||||
row.CurrentOn = Math.Abs(on.Current);
|
||||
row.DisplayVoltage = row.VoltageOn;
|
||||
row.DisplayCurrent = row.CurrentOn;
|
||||
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
||||
|
||||
await ctx.Sspc.TurnOffAsync(ch, ct).ConfigureAwait(false);
|
||||
await Task.Delay(BasicTestCriteria.SwitchSettleDelayMs, ct).ConfigureAwait(false);
|
||||
@ -33,7 +36,10 @@ public sealed class BasicTest : ITestModule
|
||||
.WaitForStableOffAsync(ctx.Sspc, ch, ct)
|
||||
.ConfigureAwait(false);
|
||||
row.VoltageOff = offResult.Measurement.Voltage;
|
||||
row.Status = BasicTestCriteria.IsOn(row.VoltageOn) && offResult.IsStableOff
|
||||
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOff;
|
||||
row.DisplayCurrent = row.CurrentOff;
|
||||
row.Status = onOk && offResult.IsStableOff
|
||||
? TestStatus.Pass : TestStatus.Fail;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -8,6 +8,7 @@ public static class BasicTestCriteria
|
||||
{
|
||||
public const double OnVoltageThreshold = 1.0;
|
||||
public const double OffVoltageThreshold = 0.5;
|
||||
public const double CurrentPresentThreshold = 0.001;
|
||||
public const int SwitchSettleDelayMs = 1_000;
|
||||
public const int OffSettleTimeoutMs = 2_000;
|
||||
public const int OffInitialDelayMs = 100;
|
||||
@ -18,8 +19,18 @@ public static class BasicTestCriteria
|
||||
|
||||
public static bool IsOff(double voltage) => Math.Abs(voltage) < OffVoltageThreshold;
|
||||
|
||||
public static bool HasVoltageData(double voltage) => voltage > OnVoltageThreshold;
|
||||
|
||||
public static bool HasCurrentData(double current) => Math.Abs(current) > CurrentPresentThreshold;
|
||||
|
||||
public static bool IsOnState(double voltage, double current) =>
|
||||
HasVoltageData(voltage) && HasCurrentData(current);
|
||||
|
||||
public static bool IsOffState(double voltage, double current) =>
|
||||
HasVoltageData(voltage) && !HasCurrentData(current);
|
||||
|
||||
/// <summary>
|
||||
/// 等待关断电压稳定。连续多次低于阈值才确认关断,避免旧帧和瞬时噪声造成误判。
|
||||
/// 等待关状态稳定。连续多次满足“电压有数据、电流无数据”才确认关断,避免旧帧和瞬时噪声造成误判。
|
||||
/// </summary>
|
||||
public static async Task<OffStateResult> WaitForStableOffAsync(
|
||||
ISspc sspc,
|
||||
@ -35,7 +46,7 @@ public static class BasicTestCriteria
|
||||
while (stopwatch.ElapsedMilliseconds < OffSettleTimeoutMs)
|
||||
{
|
||||
latest = await sspc.ReadMeasurementAsync(channel, ct).ConfigureAwait(false);
|
||||
stableSamples = IsOff(latest.Voltage) ? stableSamples + 1 : 0;
|
||||
stableSamples = IsOffState(latest.Voltage, latest.Current) ? stableSamples + 1 : 0;
|
||||
|
||||
if (stableSamples >= RequiredStableOffSamples)
|
||||
return new OffStateResult(true, latest, stopwatch.Elapsed);
|
||||
|
||||
@ -22,6 +22,9 @@ public sealed class PowerTest : ITestModule
|
||||
{
|
||||
if (!_power.IsConnected) await _power.ConnectAsync(ct).ConfigureAwait(false);
|
||||
if (!ctx.Load.IsConnected) await ctx.Load.ConnectAsync(ct).ConfigureAwait(false);
|
||||
await _power.OutputAsync(true, ct).ConfigureAwait(false);
|
||||
await ctx.Load.InputAsync(true, ct).ConfigureAwait(false);
|
||||
|
||||
int n = ctx.Sspc.ChannelCount;
|
||||
ctx.Project.PowerResults.Clear();
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ public sealed class ProtectTest : ITestModule
|
||||
|
||||
await Task.Delay(150, ct).ConfigureAwait(false);
|
||||
if (!preserveManualJudgement && row.Status == TestStatus.Running)
|
||||
row.Status = TestStatus.Measured;
|
||||
row.Status = TestStatus.Untested;
|
||||
progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = ctx.Project.ProtectResults.Count, StepStatus = row.Status });
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,4 +22,23 @@ public sealed class BasicTestCriteriaTests
|
||||
{
|
||||
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(28.0, 1.0, true)]
|
||||
[InlineData(28.0, 0.0, false)]
|
||||
[InlineData(0.0, 1.0, false)]
|
||||
public void IsOnState_RequiresVoltageAndCurrentData(double voltage, double current, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, BasicTestCriteria.IsOnState(voltage, current));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(28.0, 0.0, true)]
|
||||
[InlineData(28.0, 0.0005, true)]
|
||||
[InlineData(28.0, 0.01, false)]
|
||||
[InlineData(0.0, 0.0, false)]
|
||||
public void IsOffState_RequiresVoltageDataAndNoCurrentData(double voltage, double current, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, BasicTestCriteria.IsOffState(voltage, current));
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,8 @@ public sealed class InterfacePowerTests
|
||||
}
|
||||
}
|
||||
});
|
||||
var test = new PowerTest(new FixedPowerSupply()) { SamplesPerChannel = 1, IntervalMs = 0 };
|
||||
var power = new FixedPowerSupply();
|
||||
var test = new PowerTest(power) { SamplesPerChannel = 1, IntervalMs = 0 };
|
||||
var context = new TestContext(project, sspc, forbiddenPower, load, daq);
|
||||
|
||||
await test.RunAsync(context, new Progress<TestProgress>(), CancellationToken.None);
|
||||
@ -48,6 +49,8 @@ public sealed class InterfacePowerTests
|
||||
});
|
||||
Assert.Equal(0, sspc.TurnOffCount);
|
||||
Assert.Equal(0, sspc.ReadMeasurementCount);
|
||||
Assert.Equal(1, power.OutputOnCount);
|
||||
Assert.Equal(1, load.InputOnCount);
|
||||
Assert.Equal(24, load.ReadPowerCount);
|
||||
Assert.Equal(0, daq.ReadInstantCount);
|
||||
}
|
||||
@ -127,6 +130,7 @@ public sealed class InterfacePowerTests
|
||||
private sealed class MeasuringLoad : ILoad
|
||||
{
|
||||
public int ReadPowerCount { get; private set; }
|
||||
public int InputOnCount { get; private set; }
|
||||
public bool IsConnected => true;
|
||||
public string DisplayName => "it8702p";
|
||||
public event EventHandler<bool>? ConnectionChanged;
|
||||
@ -134,7 +138,11 @@ public sealed class InterfacePowerTests
|
||||
public Task DisconnectAsync() => Task.CompletedTask;
|
||||
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task InputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task InputAsync(bool on, CancellationToken ct = default)
|
||||
{
|
||||
if (on) InputOnCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
|
||||
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default)
|
||||
{
|
||||
@ -146,6 +154,7 @@ public sealed class InterfacePowerTests
|
||||
|
||||
private sealed class FixedPowerSupply : IPowerSupply
|
||||
{
|
||||
public int OutputOnCount { get; private set; }
|
||||
public bool IsConnected => true;
|
||||
public string DisplayName => "fixed power";
|
||||
public event EventHandler<bool>? ConnectionChanged;
|
||||
@ -153,7 +162,11 @@ public sealed class InterfacePowerTests
|
||||
public Task DisconnectAsync() => Task.CompletedTask;
|
||||
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
||||
public Task OutputAsync(bool on, CancellationToken ct = default)
|
||||
{
|
||||
if (on) OutputOnCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
|
||||
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class ProtectTestTests
|
||||
row => AssertProtection(row, 4, "过压", "≥32V", "28.193 V"),
|
||||
row => AssertProtection(row, 5, "欠压", "≤22V", "28.193 V"),
|
||||
row => AssertProtection(row, 6, "限流", "≥1.2A", "1.049 A"));
|
||||
Assert.All(project.ProtectResults, row => Assert.Equal(TestStatus.Measured, row.Status));
|
||||
Assert.All(project.ProtectResults, row => Assert.Equal(TestStatus.Untested, row.Status));
|
||||
Assert.Contains(progress, x => x.ModuleName == ProtectTest.ModuleName);
|
||||
}
|
||||
|
||||
|
||||
@ -57,11 +57,11 @@ public sealed class ReportFileHelperTests
|
||||
[Fact]
|
||||
public void ResolveFileName_UsesFallbackForBlankTemplate()
|
||||
{
|
||||
var project = new Project { Report = { FileName = " " } };
|
||||
var project = new Project { Name = "ProjectA", Report = { FileName = " " } };
|
||||
|
||||
string fileName = ReportFileHelper.ResolveFileName(project, new DateTime(2026, 6, 28, 1, 2, 3));
|
||||
|
||||
Assert.Equal("report_20260628_010203.pdf", fileName);
|
||||
Assert.Equal("ProjectA_20260628_010203.pdf", fileName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@ -59,7 +59,8 @@ public sealed class ReportLayoutTests
|
||||
Channel = channel,
|
||||
VoltageOn = failed ? 0.12 : 28.01,
|
||||
CurrentOn = failed ? 0 : 1.002,
|
||||
VoltageOff = 0.01,
|
||||
VoltageOff = 28.01,
|
||||
CurrentOff = failed ? 0.05 : 0,
|
||||
Status = failed ? TestStatus.Fail : TestStatus.Pass
|
||||
});
|
||||
project.CurveResults.Add(new CurveResultRow
|
||||
|
||||
@ -31,6 +31,7 @@ public sealed class WordReportGeneratorTests
|
||||
Assert.Contains("基础测试", text);
|
||||
Assert.Contains("功率损耗", text);
|
||||
Assert.Contains("PROTECTION FUNCTION", text);
|
||||
Assert.Contains("未判定", text);
|
||||
|
||||
var fontNames = document.MainDocumentPart.Document.Descendants<RunFonts>()
|
||||
.SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value })
|
||||
@ -88,7 +89,8 @@ public sealed class WordReportGeneratorTests
|
||||
Channel = 1,
|
||||
VoltageOn = 28.0,
|
||||
CurrentOn = 1.0,
|
||||
VoltageOff = 0.01,
|
||||
VoltageOff = 28.0,
|
||||
CurrentOff = 0.0,
|
||||
Status = TestStatus.Pass
|
||||
});
|
||||
project.CurveResults.Add(new CurveResultRow
|
||||
@ -115,7 +117,7 @@ public sealed class WordReportGeneratorTests
|
||||
ProtectType = "短路",
|
||||
Trigger = "输出短路",
|
||||
TripTimeMs = 2.1,
|
||||
Status = TestStatus.Pass
|
||||
Status = TestStatus.Untested
|
||||
});
|
||||
return project;
|
||||
}
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
<UserControl x:Class="SSPCTester.UI.Controls.CurvePlotControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sp="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
|
||||
MinHeight="120"
|
||||
Background="{StaticResource WaveBgBrush}">
|
||||
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="{StaticResource RadiusMd}">
|
||||
<sp:WpfPlot x:Name="Plot" />
|
||||
<Grid x:Name="RootContainer">
|
||||
<Border x:Name="DesignPlaceholder" Visibility="Collapsed" Background="{StaticResource WaveBgBrush}">
|
||||
<TextBlock Text="曲线图设计时占位"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
@ -1,16 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using ScottPlot;
|
||||
using ScottPlot.WPF;
|
||||
using SSPCTester.Devices.Interfaces;
|
||||
|
||||
namespace SSPCTester.UI.Controls;
|
||||
|
||||
public partial class CurvePlotControl : UserControl
|
||||
{
|
||||
private readonly bool _isDesignMode;
|
||||
private WpfPlot? _plot;
|
||||
|
||||
public CurvePlotControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
_isDesignMode = DesignerProperties.GetIsInDesignMode(this);
|
||||
if (_isDesignMode)
|
||||
{
|
||||
DesignPlaceholder.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
Loaded += (_, _) => InitPlot();
|
||||
}
|
||||
|
||||
@ -42,30 +53,39 @@ public partial class CurvePlotControl : UserControl
|
||||
|
||||
private void InitPlot()
|
||||
{
|
||||
Plot.Plot.Title("");
|
||||
Plot.Plot.XLabel("t (ms)");
|
||||
Plot.Plot.YLabel("V");
|
||||
Plot.Plot.Axes.AntiAlias(true);
|
||||
Plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
||||
Plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
||||
Plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
|
||||
_plot ??= CreateRuntimePlot();
|
||||
_plot.Plot.Title("");
|
||||
_plot.Plot.XLabel("t (ms)");
|
||||
_plot.Plot.YLabel("V");
|
||||
_plot.Plot.Axes.AntiAlias(true);
|
||||
_plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
||||
_plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
|
||||
_plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
|
||||
Render();
|
||||
}
|
||||
|
||||
private WpfPlot CreateRuntimePlot()
|
||||
{
|
||||
var plot = new WpfPlot();
|
||||
RootContainer.Children.Clear();
|
||||
RootContainer.Children.Add(plot);
|
||||
return plot;
|
||||
}
|
||||
|
||||
private void Render()
|
||||
{
|
||||
if (!IsLoaded || Plot == null) return;
|
||||
Plot.Plot.Clear();
|
||||
if (_isDesignMode || !IsLoaded || _plot == null) return;
|
||||
_plot.Plot.Clear();
|
||||
var c = Curve;
|
||||
if (c == null || c.Voltage.Length == 0)
|
||||
{
|
||||
Plot.Refresh();
|
||||
_plot.Refresh();
|
||||
return;
|
||||
}
|
||||
var points = BuildViewPoints(c);
|
||||
if (points.Count == 0)
|
||||
{
|
||||
Plot.Refresh();
|
||||
_plot.Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -78,31 +98,31 @@ public partial class CurvePlotControl : UserControl
|
||||
ys[i] = points[i].Voltage;
|
||||
}
|
||||
var color = IsRising ? "#1A73C4" : "#D23B3B";
|
||||
var sig = Plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
|
||||
var sig = _plot.Plot.Add.ScatterLine(xs, ys, ScottPlot.Color.FromHex(color));
|
||||
sig.LineWidth = 2;
|
||||
sig.MarkerSize = 0;
|
||||
|
||||
// 阈值线
|
||||
double threshold = IsRising ? VMax * 0.9 : VMax * 0.1;
|
||||
var hLine = Plot.Plot.Add.HorizontalLine(threshold);
|
||||
var hLine = _plot.Plot.Add.HorizontalLine(threshold);
|
||||
hLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
||||
hLine.LinePattern = ScottPlot.LinePattern.Dashed;
|
||||
hLine.LineWidth = 1;
|
||||
var label = Plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
|
||||
var label = _plot.Plot.Add.Text($"{(IsRising ? 90 : 10)}%", xs[0], threshold);
|
||||
label.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
|
||||
label.LabelFontSize = 10;
|
||||
|
||||
// 触发线 t=0
|
||||
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 0))
|
||||
{
|
||||
var vLine = Plot.Plot.Add.VerticalLine(0);
|
||||
var vLine = _plot.Plot.Add.VerticalLine(0);
|
||||
vLine.Color = ScottPlot.Color.FromHex("#98A1AD");
|
||||
vLine.LinePattern = ScottPlot.LinePattern.Dotted;
|
||||
vLine.LineWidth = 1;
|
||||
}
|
||||
|
||||
Plot.Plot.Axes.AutoScale();
|
||||
Plot.Refresh();
|
||||
_plot.Plot.Axes.AutoScale();
|
||||
_plot.Refresh();
|
||||
}
|
||||
|
||||
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
|
||||
@ -131,6 +151,9 @@ public partial class CurvePlotControl : UserControl
|
||||
/// <summary>导出为 PNG 字节。</summary>
|
||||
public byte[] ExportPng(int width = 800, int height = 300)
|
||||
{
|
||||
return Plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
|
||||
if (_plot == null)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
return _plot.Plot.GetImageBytes(width, height, ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ public sealed class StatusToTextConverter : IValueConverter
|
||||
TestStatus.Fail => "不合格",
|
||||
TestStatus.Running => "测试中",
|
||||
TestStatus.Measured => "已测",
|
||||
_ => "未测试"
|
||||
_ => "未判定"
|
||||
} : "—";
|
||||
public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture) => Binding.DoNothing;
|
||||
}
|
||||
@ -75,6 +75,22 @@ public sealed class BoolToVisConverter : IValueConverter
|
||||
=> value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>bool 取反;用于 IsEnabled 这类布尔属性。</summary>
|
||||
public sealed class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
bool b = value is bool x && x;
|
||||
return !b;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
bool b = value is bool x && x;
|
||||
return !b;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>null → Collapsed,非 null → Visible(参数 "Invert" 取反)。</summary>
|
||||
public sealed class NullToVisConverter : IValueConverter
|
||||
{
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<cnv:StatusToSoftBrushConverter x:Key="StatusToSoftBrush" />
|
||||
<cnv:StatusToTextConverter x:Key="StatusToText" />
|
||||
<cnv:BoolToVisConverter x:Key="BoolToVis" />
|
||||
<cnv:InverseBoolConverter x:Key="InverseBoolConverter" />
|
||||
<cnv:NullToVisConverter x:Key="NullToVis" />
|
||||
<cnv:LogLevelToBrushConverter x:Key="LogLevelToBrush" />
|
||||
<cnv:NumberOrDashConverter x:Key="NumberOrDash" />
|
||||
|
||||
@ -16,6 +16,7 @@ public sealed class SafetyMonitorService : IDisposable
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
private Timer? _timer;
|
||||
private bool _handlingEmergency;
|
||||
private bool _softwareEmergencyActive;
|
||||
private bool _disposed;
|
||||
|
||||
public SafetyMonitorService(
|
||||
@ -56,18 +57,66 @@ public sealed class SafetyMonitorService : IDisposable
|
||||
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入");
|
||||
}
|
||||
|
||||
public async Task ResetAsync(CancellationToken ct = default)
|
||||
public async Task TriggerSoftwareEmergencyAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_softwareEmergencyActive = true;
|
||||
_handlingEmergency = true;
|
||||
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "正在执行软件急停安全关断");
|
||||
_log.Add(UiLogLevel.Error, "软件触发报警急停,开始执行报警灯、电源、负载关断。");
|
||||
|
||||
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
|
||||
await SetAlarmOutputAsync(true, ct).ConfigureAwait(false);
|
||||
await TryShutdownPowerAsync().ConfigureAwait(false);
|
||||
await TryShutdownLoadAsync().ConfigureAwait(false);
|
||||
SetStatus(true, true, "软件急停已触发", "报警灯已接通,电源输出和负载输入已发送关闭指令");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_softwareEmergencyActive = false;
|
||||
_handlingEmergency = false;
|
||||
_logger.LogError(ex, "Software emergency shutdown failed.");
|
||||
_log.Add(UiLogLevel.Error, $"软件急停执行异常:{ex.Message}");
|
||||
SetStatus(true, IsAlarmOutputOn, "设备故障", ex.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SafetyResetResult> ResetAsync(CancellationToken ct = default)
|
||||
{
|
||||
await _gate.WaitAsync(ct).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
return await ResetCoreAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SafetyResetResult> ResetCoreAsync(CancellationToken ct)
|
||||
{
|
||||
var options = _options.Pci2312;
|
||||
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
|
||||
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
|
||||
{
|
||||
SetStatus(true, IsAlarmOutputOn, "急停已触发", "急停输入仍处于接通状态,无法复位报警灯");
|
||||
return;
|
||||
const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
|
||||
SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
|
||||
return new SafetyResetResult(false, message);
|
||||
}
|
||||
|
||||
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
|
||||
SetStatus(false, false, "设备正常", "急停已复位,正在监控 PCI2312 输入");
|
||||
_softwareEmergencyActive = false;
|
||||
_handlingEmergency = false;
|
||||
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
|
||||
return new SafetyResetResult(true, "急停状态已重置。");
|
||||
}
|
||||
|
||||
private void OnTimer(object? state)
|
||||
@ -92,11 +141,13 @@ public sealed class SafetyMonitorService : IDisposable
|
||||
{
|
||||
await HandleEmergencyAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (_softwareEmergencyActive)
|
||||
{
|
||||
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
|
||||
}
|
||||
else if (IsEmergencyActive || IsAlarmOutputOn)
|
||||
{
|
||||
await SetAlarmOutputAsync(false, CancellationToken.None).ConfigureAwait(false);
|
||||
_handlingEmergency = false;
|
||||
SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 已复位,报警灯已关闭");
|
||||
SetStatus(true, IsAlarmOutputOn, "急停已触发", $"DI{options.EmergencyInputChannel} 已复位,请点击状态重置关闭报警灯");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -185,6 +236,16 @@ public sealed class SafetyMonitorService : IDisposable
|
||||
IsAlarmOutputOn = on;
|
||||
}
|
||||
|
||||
private async Task EnsureSafetyDioConnectedAsync(CancellationToken ct)
|
||||
{
|
||||
var options = _options.Pci2312;
|
||||
if (!options.Enabled)
|
||||
throw new InvalidOperationException("PCI2312 安全输入输出未启用,无法控制报警灯。");
|
||||
|
||||
if (!_safetyDio.IsConnected)
|
||||
await _safetyDio.ConnectAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
|
||||
{
|
||||
IsEmergencyActive = emergencyActive;
|
||||
@ -201,3 +262,5 @@ public sealed class SafetyMonitorService : IDisposable
|
||||
_gate.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record SafetyResetResult(bool Success, string Message);
|
||||
|
||||
@ -87,8 +87,13 @@ public partial class BasicTestViewModel : ObservableObject
|
||||
var measurement = await _sspc.ReadMeasurementAsync(row.Channel);
|
||||
row.VoltageOn = measurement.Voltage;
|
||||
row.CurrentOn = Math.Abs(measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOn;
|
||||
row.DisplayCurrent = row.CurrentOn;
|
||||
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
||||
row.Status = onOk ? TestStatus.Running : TestStatus.Fail;
|
||||
StartPolling(row.Channel);
|
||||
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A");
|
||||
_log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error,
|
||||
$"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -112,12 +117,15 @@ public partial class BasicTestViewModel : ObservableObject
|
||||
await Task.Delay(SwitchSettleDelayMs);
|
||||
var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel);
|
||||
row.VoltageOff = offResult.Measurement.Voltage;
|
||||
bool onOk = BasicTestCriteria.IsOn(row.VoltageOn);
|
||||
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOff;
|
||||
row.DisplayCurrent = row.CurrentOff;
|
||||
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
||||
row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
|
||||
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
|
||||
string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时";
|
||||
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
|
||||
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V {offState} {verdict}");
|
||||
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -191,24 +199,37 @@ public partial class BasicTestViewModel : ObservableObject
|
||||
if (!_sspc.IsConnected) await _sspc.ConnectAsync(ct);
|
||||
|
||||
row.Status = TestStatus.Running;
|
||||
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 基础测试开始:执行开状态测试。");
|
||||
await _sspc.TurnOnAsync(row.Channel, ct);
|
||||
await Task.Delay(SwitchSettleDelayMs, ct);
|
||||
|
||||
var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct);
|
||||
row.VoltageOn = measurement.Voltage;
|
||||
row.CurrentOn = Math.Abs(measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOn;
|
||||
row.DisplayCurrent = row.CurrentOn;
|
||||
bool onOk = BasicTestCriteria.IsOnState(row.VoltageOn, row.CurrentOn);
|
||||
_log.Add(onOk ? UiLogLevel.Info : UiLogLevel.Error,
|
||||
$"CH{row.Channel} 开 -> V={row.VoltageOn:F2}V I={row.CurrentOn:F3}A {(onOk ? "开状态合格" : "开状态不合格")}");
|
||||
StartPolling(row.Channel);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
await StopPollingAsync(row.Channel);
|
||||
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 执行关状态测试。");
|
||||
await _sspc.TurnOffAsync(row.Channel, ct);
|
||||
await Task.Delay(SwitchSettleDelayMs, ct);
|
||||
|
||||
var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel, ct);
|
||||
row.VoltageOff = offResult.Measurement.Voltage;
|
||||
bool onOk = BasicTestCriteria.IsOn(row.VoltageOn);
|
||||
row.CurrentOff = Math.Abs(offResult.Measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOff;
|
||||
row.DisplayCurrent = row.CurrentOff;
|
||||
row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
|
||||
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
|
||||
string offState = offResult.IsStableOff ? "关状态合格" : "关状态不合格";
|
||||
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error,
|
||||
$"CH{row.Channel} 关 -> V={row.VoltageOff:F2}V I={row.CurrentOff:F3}A {offState} {verdict}");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@ -216,7 +237,8 @@ public partial class BasicTestViewModel : ObservableObject
|
||||
{
|
||||
foreach (var r in Rows)
|
||||
{
|
||||
r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0;
|
||||
r.VoltageOn = 0; r.CurrentOn = 0; r.VoltageOff = 0; r.CurrentOff = 0;
|
||||
r.DisplayVoltage = 0; r.DisplayCurrent = 0;
|
||||
r.Status = TestStatus.Untested;
|
||||
}
|
||||
}
|
||||
@ -302,6 +324,8 @@ public partial class BasicTestViewModel : ObservableObject
|
||||
|
||||
row.VoltageOn = measurement.Voltage;
|
||||
row.CurrentOn = Math.Abs(measurement.Current);
|
||||
row.DisplayVoltage = row.VoltageOn;
|
||||
row.DisplayCurrent = row.CurrentOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,13 @@ public partial class CurveTestViewModel : ObservableObject
|
||||
_ = LoadSelectedChannelCurvesAsync();
|
||||
}
|
||||
|
||||
partial void OnSelectedRowChanged(CurveResultRow? value)
|
||||
{
|
||||
if (_syncingViewWindow || value == null) return;
|
||||
if (SelectedChannel != value.Channel)
|
||||
SelectedChannel = value.Channel;
|
||||
}
|
||||
|
||||
partial void OnRisingViewStartMsChanged(double? value)
|
||||
{
|
||||
if (_syncingViewWindow || SelectedRow == null) return;
|
||||
@ -117,10 +124,10 @@ public partial class CurveTestViewModel : ObservableObject
|
||||
{
|
||||
SelectedRow = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
|
||||
?? Project?.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel);
|
||||
RisingViewStartMs = SelectedRow?.RisingViewStartMs ?? SelectedRow?.ViewStartMs;
|
||||
RisingViewEndMs = SelectedRow?.RisingViewEndMs ?? SelectedRow?.ViewEndMs;
|
||||
FallingViewStartMs = SelectedRow?.FallingViewStartMs ?? SelectedRow?.ViewStartMs;
|
||||
FallingViewEndMs = SelectedRow?.FallingViewEndMs ?? SelectedRow?.ViewEndMs;
|
||||
RisingViewStartMs = SelectedRow?.RisingViewStartMs;
|
||||
RisingViewEndMs = SelectedRow?.RisingViewEndMs;
|
||||
FallingViewStartMs = SelectedRow?.FallingViewStartMs;
|
||||
FallingViewEndMs = SelectedRow?.FallingViewEndMs;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -228,9 +235,9 @@ public partial class CurveTestViewModel : ObservableObject
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenPopup()
|
||||
private void OpenPopup(string? curveKind)
|
||||
{
|
||||
var win = new Views.CurvePopupWindow(this) { Owner = System.Windows.Application.Current?.MainWindow };
|
||||
var win = new Views.CurvePopupWindow(this, curveKind) { Owner = System.Windows.Application.Current?.MainWindow };
|
||||
win.Show();
|
||||
}
|
||||
|
||||
|
||||
@ -30,8 +30,6 @@ public partial class MainViewModel : ObservableObject
|
||||
private readonly UiLogSink _logSink;
|
||||
private readonly DeviceOptions _deviceOptions;
|
||||
private readonly DispatcherTimer _deviceMonitorTimer;
|
||||
private readonly DispatcherTimer _safetyReadingsTimer;
|
||||
private bool _safetyReadingsBusy;
|
||||
private object? _viewBeforeSettings;
|
||||
|
||||
public MainViewModel(
|
||||
@ -54,8 +52,6 @@ public partial class MainViewModel : ObservableObject
|
||||
_deviceMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_deviceMonitorTimer.Tick += (_, _) => RefreshDeviceStatus();
|
||||
_deviceMonitorTimer.Start();
|
||||
_safetyReadingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_safetyReadingsTimer.Tick += async (_, _) => await RefreshSafetyReadingsAsync().ConfigureAwait(true);
|
||||
|
||||
AllProjects = new ObservableCollection<Project>();
|
||||
TreeRoots = new ObservableCollection<ProjectNode>();
|
||||
@ -81,8 +77,8 @@ public partial class MainViewModel : ObservableObject
|
||||
[ObservableProperty] private object? _currentView;
|
||||
[ObservableProperty] private bool _devicesConnected;
|
||||
[ObservableProperty] private bool _devicePanelOpen;
|
||||
[ObservableProperty] private bool _safetyPanelOpen;
|
||||
[ObservableProperty] private bool _deviceOperationBusy;
|
||||
[ObservableProperty] private bool _emergencyOperationBusy;
|
||||
[ObservableProperty] private string _deviceSummary = "设备状态检查中";
|
||||
[ObservableProperty] private string _sspcConnectionStatus = "检查中";
|
||||
[ObservableProperty] private string _powerConnectionStatus = "检查中";
|
||||
@ -91,26 +87,13 @@ public partial class MainViewModel : ObservableObject
|
||||
[ObservableProperty] private string _pci2312ConnectionStatus = "检查中";
|
||||
[ObservableProperty] private string _safetyStatusText = "设备故障";
|
||||
[ObservableProperty] private string _safetyDetailText = "设备安全监控未启动";
|
||||
[ObservableProperty] private string _safetyPowerCurrentText = "--";
|
||||
[ObservableProperty] private string _safetyLoadCurrentText = "--";
|
||||
[ObservableProperty] private bool _alarmLampActive;
|
||||
[ObservableProperty] private string _emergencyButtonText = "报警急停(状态重置)";
|
||||
[ObservableProperty] private string _testingMessage = "";
|
||||
[ObservableProperty] private TestSessionViewModel? _currentSession;
|
||||
|
||||
partial void OnStateChanged(AppState value) => RebuildTree();
|
||||
partial void OnCurrentProjectChanged(Project? value) => RebuildTree();
|
||||
partial void OnSafetyPanelOpenChanged(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
_safetyReadingsTimer.Start();
|
||||
_ = RefreshSafetyReadingsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_safetyReadingsTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectConfiguredDevicesAsync()
|
||||
{
|
||||
@ -180,55 +163,11 @@ public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
SafetyStatusText = _safetyMonitor.StatusText;
|
||||
SafetyDetailText = _safetyMonitor.DetailText;
|
||||
AlarmLampActive = _safetyMonitor.IsEmergencyActive || _safetyMonitor.IsAlarmOutputOn;
|
||||
AlarmLampActive = _safetyMonitor.IsAlarmOutputOn;
|
||||
EmergencyButtonText = AlarmLampActive ? "状态重置" : "报警急停";
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RefreshSafetyReadingsAsync()
|
||||
{
|
||||
if (_safetyReadingsBusy) return;
|
||||
_safetyReadingsBusy = true;
|
||||
try
|
||||
{
|
||||
SafetyPowerCurrentText = await ReadPowerCurrentTextAsync().ConfigureAwait(true);
|
||||
SafetyLoadCurrentText = await ReadLoadCurrentTextAsync().ConfigureAwait(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_safetyReadingsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadPowerCurrentTextAsync()
|
||||
{
|
||||
if (!_power.IsConnected) return "未连接";
|
||||
try
|
||||
{
|
||||
var (_, amps) = await _power.ReadAsync().ConfigureAwait(true);
|
||||
return $"{amps:F3} A";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Read UDP5080 current for safety panel failed.");
|
||||
return "读取失败";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadLoadCurrentTextAsync()
|
||||
{
|
||||
if (!_load.IsConnected) return "未连接";
|
||||
try
|
||||
{
|
||||
var (_, amps) = await _load.ReadAsync().ConfigureAwait(true);
|
||||
return $"{amps:F3} A";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Read IT8702P current for safety panel failed.");
|
||||
return "读取失败";
|
||||
}
|
||||
}
|
||||
|
||||
private static string DeviceStatusText(IDeviceConnection device) =>
|
||||
device.IsConnected ? "已连接" : "未连接";
|
||||
|
||||
@ -240,18 +179,51 @@ public partial class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleSafetyPanel()
|
||||
private async Task ToggleEmergencyAlarm()
|
||||
{
|
||||
if (EmergencyOperationBusy) return;
|
||||
EmergencyOperationBusy = true;
|
||||
try
|
||||
{
|
||||
if (AlarmLampActive)
|
||||
{
|
||||
var result = await _safetyMonitor.ResetAsync().ConfigureAwait(false);
|
||||
RefreshSafetyStatus();
|
||||
SafetyPanelOpen = !SafetyPanelOpen;
|
||||
if (!result.Success)
|
||||
{
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
MessageBox.Show(
|
||||
Application.Current?.MainWindow,
|
||||
result.Message,
|
||||
"状态重置受阻",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ResetSafetyAlarm()
|
||||
}
|
||||
else
|
||||
{
|
||||
await _safetyMonitor.ResetAsync().ConfigureAwait(false);
|
||||
await _safetyMonitor.TriggerSoftwareEmergencyAsync().ConfigureAwait(false);
|
||||
RefreshSafetyStatus();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Toggle emergency alarm failed.");
|
||||
_logSink.Add(UiLogLevel.Error, $"报警急停操作失败:{ex.Message}");
|
||||
Application.Current?.Dispatcher.Invoke(() =>
|
||||
MessageBox.Show(
|
||||
Application.Current?.MainWindow,
|
||||
$"报警急停操作失败:{ex.Message}",
|
||||
"报警急停",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error));
|
||||
RefreshSafetyStatus();
|
||||
}
|
||||
finally
|
||||
{
|
||||
EmergencyOperationBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConnectAllDevices()
|
||||
@ -427,8 +399,7 @@ public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
CurrentProject = p;
|
||||
State = AppState.Testing;
|
||||
TestingMessage = $"测试进行中:{p.Name}";
|
||||
|
||||
TestingMessage = $"测试页面:{p.Name}";
|
||||
var session = new TestSessionViewModel(_sp, p, _sspc, _power, _load, _daq, _logSink);
|
||||
CurrentSession = session;
|
||||
|
||||
@ -439,30 +410,31 @@ public partial class MainViewModel : ObservableObject
|
||||
|
||||
try
|
||||
{
|
||||
await session.RunAllAsync();
|
||||
_logSink.Add(UiLogLevel.Success, "全部测试完成。");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logSink.Add(UiLogLevel.Warning, "测试已中止。");
|
||||
await _store.SaveAsync(p);
|
||||
_logSink.Add(UiLogLevel.Info, "已进入测试页面,请在各测试页手动执行当前项目。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Test failed.");
|
||||
_logSink.Add(UiLogLevel.Error, "测试出错:" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _store.SaveAsync(p);
|
||||
State = AppState.Browsing;
|
||||
CurrentSession = null;
|
||||
_logger.LogError(ex, "Failed to enter test view.");
|
||||
_logSink.Add(UiLogLevel.Error, "进入测试页面出错:" + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AbortTest()
|
||||
private async Task AbortTest()
|
||||
{
|
||||
CurrentSession?.Cancel();
|
||||
if (CurrentView is TestHostViewModel host && host.Basic.IsBatchRunning)
|
||||
await host.Basic.CancelRunAllCommand.ExecuteAsync(null);
|
||||
|
||||
if (CurrentProject != null)
|
||||
await _store.SaveAsync(CurrentProject);
|
||||
|
||||
CurrentSession = null;
|
||||
TestingMessage = string.Empty;
|
||||
State = AppState.Browsing;
|
||||
if (CurrentProject != null)
|
||||
OpenProjectInfo(CurrentProject);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@ -131,6 +131,9 @@ public partial class PowerTestViewModel : ObservableObject
|
||||
throw new InvalidOperationException($"IT8702P connection failed: {ex.Message}", ex);
|
||||
}
|
||||
|
||||
await _power.OutputAsync(true);
|
||||
await _load.InputAsync(true);
|
||||
|
||||
double sVin = 0, sIin = 0, sVout = 0, sIout = 0, sPout = 0;
|
||||
for (int s = 0; s < SamplesPerChannel; s++)
|
||||
{
|
||||
@ -165,7 +168,7 @@ public partial class PowerTestViewModel : ObservableObject
|
||||
row.MeasuredPowerOut = sPout / SamplesPerChannel;
|
||||
row.Status = TestStatus.Measured;
|
||||
Current = row;
|
||||
_log.Add(UiLogLevel.Success, $"CH{ch} 功率损耗 {row.PowerLossW:F3} W,效率 {row.EfficiencyPct:F3}%");
|
||||
_log.Add(UiLogLevel.Success, $"CH{ch} 功率损耗 {row.PowerLossW:F3} W,效率 {row.EfficiencyPct:F1}%");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -61,8 +61,8 @@ public partial class ProtectViewModel : ObservableObject
|
||||
"过压" or "欠压" => $"{volts:F3} V",
|
||||
_ => $"{amps:F3} A"
|
||||
};
|
||||
if (row.Status is TestStatus.Untested or TestStatus.Running)
|
||||
row.Status = TestStatus.Measured;
|
||||
if (row.Status == TestStatus.Running)
|
||||
row.Status = TestStatus.Untested;
|
||||
_log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -60,7 +60,8 @@ public partial class ReportViewModel : ObservableObject
|
||||
{
|
||||
Project = p;
|
||||
if (string.IsNullOrWhiteSpace(p.Report.FileName) ||
|
||||
string.Equals(p.Report.FileName, "report.pdf", StringComparison.OrdinalIgnoreCase))
|
||||
string.Equals(p.Report.FileName, "report.pdf", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(p.Report.FileName, "report_{yyyyMMdd_HHmmss}.pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
p.Report.FileName = ReportFileHelper.DefaultTemplate;
|
||||
}
|
||||
@ -318,15 +319,15 @@ public partial class ReportViewModel : ObservableObject
|
||||
: CurveImageRenderer.RenderRisingChannel(
|
||||
row.Channel,
|
||||
rising,
|
||||
row.RisingViewStartMs ?? row.ViewStartMs,
|
||||
row.RisingViewEndMs ?? row.ViewEndMs),
|
||||
row.RisingViewStartMs,
|
||||
row.RisingViewEndMs),
|
||||
falling == null
|
||||
? null
|
||||
: CurveImageRenderer.RenderFallingChannel(
|
||||
row.Channel,
|
||||
falling,
|
||||
row.FallingViewStartMs ?? row.ViewStartMs,
|
||||
row.FallingViewEndMs ?? row.ViewEndMs));
|
||||
row.FallingViewStartMs,
|
||||
row.FallingViewEndMs));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Header="电压(V)" Width="*">
|
||||
<DataGridTextColumn.Binding>
|
||||
<Binding Path="VoltageOn" Converter="{StaticResource NumberOrDash}" ConverterParameter="F2" />
|
||||
<Binding Path="DisplayVoltage" Converter="{StaticResource NumberOrDash}" ConverterParameter="F2" />
|
||||
</DataGridTextColumn.Binding>
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
||||
@ -95,7 +95,7 @@
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Header="电流(A)" Width="*">
|
||||
<DataGridTextColumn.Binding>
|
||||
<Binding Path="CurrentOn" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
||||
<Binding Path="DisplayCurrent" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
||||
</DataGridTextColumn.Binding>
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
||||
|
||||
@ -3,20 +3,20 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ctl="clr-namespace:SSPCTester.UI.Controls"
|
||||
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="曲线查看器"
|
||||
Width="1100" Height="720"
|
||||
MinWidth="600" MinHeight="400"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
WindowState="Maximized"
|
||||
Background="{StaticResource BgAppBrush}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:CurveTestViewModel}"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Margin="0,0,0,12">
|
||||
@ -31,64 +31,26 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" FontSize="{StaticResource FsH1}" FontWeight="Bold">
|
||||
<Run Text="曲线查看器 " />
|
||||
<Run x:Name="TitleRun" Text="曲线查看器 " />
|
||||
<Run Text="CH" Foreground="{StaticResource TextSecondaryBrush}" />
|
||||
<Run Text="{Binding SelectedChannel, Mode=OneWay}" Foreground="{StaticResource PrimaryBrush}" />
|
||||
</TextBlock>
|
||||
<ComboBox Grid.Column="1" Width="120" Margin="0,0,8,0"
|
||||
ItemsSource="{Binding Channels}" SelectedItem="{Binding SelectedChannel}" />
|
||||
<TextBlock Grid.Column="2" Text="起点" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Grid.Column="3" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding RisingViewStartMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
<TextBox x:Name="StartTextBox" Grid.Column="3" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<TextBlock Grid.Column="4" Text="终点" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Grid.Column="5" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding RisingViewEndMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
<TextBox x:Name="EndTextBox" Grid.Column="5" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<Button Grid.Column="6" Content="默认" Width="54" Height="26" Margin="0,0,8,0" Padding="0"
|
||||
Command="{Binding ResetRisingCurveViewCommand}" />
|
||||
<Button x:Name="ResetButton" Grid.Column="6" Content="默认" Width="54" Height="26" Margin="0,0,8,0" Padding="0" />
|
||||
<Button Grid.Column="7" Content="关闭" Width="80" IsCancel="True" Click="Close_Click" />
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1" Style="{StaticResource Card}" Margin="0,0,0,6">
|
||||
<Border Grid.Row="1" Style="{StaticResource Card}">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="开启曲线(电压上升,90% 阈值)" />
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,6">
|
||||
<TextBlock Text="璧风偣" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding RisingViewStartMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<TextBlock Text="缁堢偣" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding RisingViewEndMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<Button Content="榛樿" Width="54" Height="26" Margin="0,0,8,0" Padding="0"
|
||||
Command="{Binding ResetRisingCurveViewCommand}" />
|
||||
</StackPanel>
|
||||
<ctl:CurvePlotControl Curve="{Binding RisingCurve}" IsRising="True" VMax="{Binding VMax}"
|
||||
ViewStartMs="{Binding RisingViewStartMs}"
|
||||
ViewEndMs="{Binding RisingViewEndMs}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Style="{StaticResource Card}" Margin="0,6,0,0">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="关断曲线(电压下降,10% 阈值)" />
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,6">
|
||||
<TextBlock Text="璧风偣" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding FallingViewStartMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<TextBlock Text="缁堢偣" VerticalAlignment="Center" Margin="0,0,4,0" />
|
||||
<TextBox Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
|
||||
Text="{Binding FallingViewEndMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
|
||||
FontFamily="{StaticResource FontMono}" />
|
||||
<Button Content="榛樿" Width="54" Height="26" Margin="0,0,8,0" Padding="0"
|
||||
Command="{Binding ResetFallingCurveViewCommand}" />
|
||||
</StackPanel>
|
||||
<ctl:CurvePlotControl Curve="{Binding FallingCurve}" IsRising="False" VMax="{Binding VMax}"
|
||||
ViewStartMs="{Binding FallingViewStartMs}"
|
||||
ViewEndMs="{Binding FallingViewEndMs}" />
|
||||
<TextBlock x:Name="CurveTitleText" DockPanel.Dock="Top" Style="{StaticResource CardTitle}" />
|
||||
<ctl:CurvePlotControl x:Name="CurvePlot" VMax="{Binding VMax}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@ -1,14 +1,50 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using SSPCTester.UI.Controls;
|
||||
using SSPCTester.UI.ViewModels;
|
||||
|
||||
namespace SSPCTester.UI.Views;
|
||||
|
||||
public partial class CurvePopupWindow : Window
|
||||
{
|
||||
public CurvePopupWindow(CurveTestViewModel vm)
|
||||
public CurvePopupWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public CurvePopupWindow(CurveTestViewModel vm, string? curveKind)
|
||||
: this()
|
||||
{
|
||||
DataContext = vm;
|
||||
ConfigureCurve(curveKind);
|
||||
}
|
||||
|
||||
private void ConfigureCurve(string? curveKind)
|
||||
{
|
||||
bool isFalling = string.Equals(curveKind, "Falling", StringComparison.OrdinalIgnoreCase);
|
||||
string title = isFalling ? "关断曲线(电压下降,10% 阈值)" : "开启曲线(电压上升,90% 阈值)";
|
||||
|
||||
Title = title;
|
||||
TitleRun.Text = $"{title} ";
|
||||
CurveTitleText.Text = title;
|
||||
CurvePlot.IsRising = !isFalling;
|
||||
|
||||
CurvePlot.SetBinding(CurvePlotControl.CurveProperty, new Binding(isFalling ? "FallingCurve" : "RisingCurve"));
|
||||
CurvePlot.SetBinding(CurvePlotControl.ViewStartMsProperty, new Binding(isFalling ? "FallingViewStartMs" : "RisingViewStartMs"));
|
||||
CurvePlot.SetBinding(CurvePlotControl.ViewEndMsProperty, new Binding(isFalling ? "FallingViewEndMs" : "RisingViewEndMs"));
|
||||
|
||||
StartTextBox.SetBinding(TextBox.TextProperty, new Binding(isFalling ? "FallingViewStartMs" : "RisingViewStartMs")
|
||||
{
|
||||
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
|
||||
TargetNullValue = ""
|
||||
});
|
||||
EndTextBox.SetBinding(TextBox.TextProperty, new Binding(isFalling ? "FallingViewEndMs" : "RisingViewEndMs")
|
||||
{
|
||||
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
|
||||
TargetNullValue = ""
|
||||
});
|
||||
ResetButton.SetBinding(Button.CommandProperty, new Binding(isFalling ? "ResetFallingCurveViewCommand" : "ResetRisingCurveViewCommand"));
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
@ -54,12 +54,10 @@
|
||||
</Grid>
|
||||
<Button Style="{StaticResource PrimaryButton}" Content="采集开启曲线" Margin="0,16,0,0"
|
||||
Command="{Binding CaptureRisingCommand}"
|
||||
IsEnabled="{Binding Busy, Converter={StaticResource BoolToVis}, ConverterParameter=Invert}" />
|
||||
IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
|
||||
<Button Style="{StaticResource PrimaryButton}" Content="采集关断曲线" Margin="0,8,0,0"
|
||||
Command="{Binding CaptureFallingCommand}"
|
||||
IsEnabled="{Binding Busy, Converter={StaticResource BoolToVis}, ConverterParameter=Invert}" />
|
||||
<Button Content="在新窗口查看曲线" Margin="0,8,0,0"
|
||||
Command="{Binding OpenPopupCommand}" />
|
||||
IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
|
||||
<TextBlock Margin="0,12,0,0" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Text="开启/关断时间以软件发送 Modbus 命令前的标记为基准,包含串口通信延迟,不等同于硬件同步触发精度。" />
|
||||
@ -92,7 +90,8 @@
|
||||
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
||||
Command="{Binding ResetRisingCurveViewCommand}" />
|
||||
<Button Content="放大" Width="64" Height="26" Padding="0"
|
||||
Command="{Binding OpenPopupCommand}" />
|
||||
Command="{Binding OpenPopupCommand}"
|
||||
CommandParameter="Rising" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ctl:CurvePlotControl Curve="{Binding RisingCurve}" IsRising="True" VMax="{Binding VMax}"
|
||||
@ -120,7 +119,8 @@
|
||||
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
|
||||
Command="{Binding ResetFallingCurveViewCommand}" />
|
||||
<Button Content="放大" Width="64" Height="26" Padding="0"
|
||||
Command="{Binding OpenPopupCommand}" />
|
||||
Command="{Binding OpenPopupCommand}"
|
||||
CommandParameter="Falling" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ctl:CurvePlotControl Curve="{Binding FallingCurve}" IsRising="False" VMax="{Binding VMax}"
|
||||
@ -141,7 +141,8 @@
|
||||
<Border Style="{StaticResource Card}" Margin="0,0,12,0">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道时间参数" />
|
||||
<DataGrid ItemsSource="{Binding Rows}">
|
||||
<DataGrid ItemsSource="{Binding Rows}"
|
||||
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="通道" Width="70">
|
||||
<DataGridTextColumn.Binding>
|
||||
|
||||
@ -37,58 +37,152 @@
|
||||
FontSize="{StaticResource FsTitle}" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Grid.Column="2" VerticalAlignment="Center">
|
||||
<Grid Margin="0,0,8,0">
|
||||
<Button x:Name="SafetyStatusButton" Width="34" Height="30" Padding="0"
|
||||
Command="{Binding ToggleSafetyPanelCommand}"
|
||||
ToolTip="{Binding SafetyStatusText}"
|
||||
Background="Transparent" BorderBrush="{StaticResource BorderBrush}">
|
||||
<Grid>
|
||||
<Ellipse Width="18" Height="18" Stroke="{StaticResource BorderBrush}" StrokeThickness="1">
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Setter Property="Fill" Value="{StaticResource SuccessBrush}" />
|
||||
<Viewbox Width="26" Height="26" Margin="0,0,6,0"
|
||||
ToolTip="{Binding SafetyDetailText}">
|
||||
<Canvas Width="64" Height="64">
|
||||
<Canvas.Resources>
|
||||
<RadialGradientBrush x:Key="AlarmRedGlassBrush" Center="0.34,0.16" GradientOrigin="0.34,0.16" RadiusX="0.78" RadiusY="0.78">
|
||||
<GradientStop Color="#FFFF8A8A" Offset="0" />
|
||||
<GradientStop Color="#FFDF0010" Offset="0.36" />
|
||||
<GradientStop Color="#FF720006" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
<RadialGradientBrush x:Key="AlarmGrayGlassBrush" Center="0.34,0.16" GradientOrigin="0.34,0.16" RadiusX="0.78" RadiusY="0.78">
|
||||
<GradientStop Color="#FFDEDEDE" Offset="0" />
|
||||
<GradientStop Color="#FF969696" Offset="0.36" />
|
||||
<GradientStop Color="#FF4B4B4B" Offset="1" />
|
||||
</RadialGradientBrush>
|
||||
<LinearGradientBrush x:Key="AlarmBaseBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#FF3B3B3B" Offset="0" />
|
||||
<GradientStop Color="#FF101010" Offset="0.45" />
|
||||
<GradientStop Color="#FF303030" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Canvas.Resources>
|
||||
<Path x:Name="AlarmGlass" Data="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Fill" Value="{StaticResource AlarmGrayGlassBrush}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
|
||||
<Setter Property="Fill" Value="{StaticResource DangerBrush}" />
|
||||
<Setter Property="Fill" Value="{StaticResource AlarmRedGlassBrush}" />
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="AlarmGlassBlink">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||
From="1" To=".42" Duration="0:0:0.36" AutoReverse="True" RepeatBehavior="Forever" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<StopStoryboard BeginStoryboardName="AlarmGlassBlink" />
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<TextBlock Text="!" Foreground="White" FontSize="12" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
<Path x:Name="AlarmRim" Data="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z"
|
||||
Fill="Transparent">
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Stroke" Value="#FF555555" />
|
||||
<Setter Property="StrokeThickness" Value="1.4" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
|
||||
<Setter Property="Stroke" Value="#FFFF2635" />
|
||||
<Setter Property="StrokeThickness" Value="2.2" />
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="AlarmRimBlink">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||
From="1" To=".35" Duration="0:0:0.36" AutoReverse="True" RepeatBehavior="Forever" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<StopStoryboard BeginStoryboardName="AlarmRimBlink" />
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
<Path x:Name="AlarmSweep" Data="M17 6h8l-11 42h-8z"
|
||||
Fill="White">
|
||||
<Path.RenderTransform>
|
||||
<TranslateTransform />
|
||||
</Path.RenderTransform>
|
||||
<Path.Style>
|
||||
<Style TargetType="Path">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
|
||||
<Setter Property="Opacity" Value=".65" />
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="AlarmSweepMove">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
|
||||
From="-24" To="30" Duration="0:0:0.95" RepeatBehavior="Forever" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<StopStoryboard BeginStoryboardName="AlarmSweepMove" />
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Path.Style>
|
||||
</Path>
|
||||
<Path Data="M25 13l7-3 3 6-8 3zM38 11c4 1 6 3 8 7l-5 2c-1-3-3-4-6-5z"
|
||||
Fill="White" Opacity=".72" />
|
||||
<Ellipse Canvas.Left="10" Canvas.Top="39" Width="44" Height="10" Fill="#771A0000" />
|
||||
<Rectangle x:Name="AlarmBaseFlash" Canvas.Left="13" Canvas.Top="42" Width="38" Height="2"
|
||||
Fill="#FFFF0014">
|
||||
<Rectangle.Style>
|
||||
<Style TargetType="Rectangle">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="AlarmBaseBlink">
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||
From="1" To=".15" Duration="0:0:0.36" AutoReverse="True" RepeatBehavior="Forever" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<StopStoryboard BeginStoryboardName="AlarmBaseBlink" />
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Rectangle.Style>
|
||||
</Rectangle>
|
||||
<Path Data="M11 43h42v6H11z" Fill="#DD240004" />
|
||||
<Path Data="M9 48h46v8c0 3-3 5-6 5H15c-3 0-6-2-6-5z"
|
||||
Fill="{StaticResource AlarmBaseBrush}" />
|
||||
<Path Data="M13 50h38" Stroke="#777777" StrokeThickness="1" Opacity=".45" />
|
||||
<Path Data="M42 56v4M47 55v4M52 54v3" Stroke="#050505" StrokeThickness="2" StrokeStartLineCap="Round" StrokeEndLineCap="Round" />
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
<Button Margin="0,0,8,0" Padding="12,4"
|
||||
Command="{Binding ToggleEmergencyAlarmCommand}"
|
||||
IsEnabled="{Binding EmergencyOperationBusy, Converter={StaticResource InverseBoolConverter}}"
|
||||
ToolTip="{Binding SafetyDetailText}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource DangerButton}">
|
||||
<Setter Property="Content" Value="{Binding EmergencyButtonText}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AlarmLampActive}" Value="False">
|
||||
<Setter Property="Background" Value="{StaticResource WarningBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
</Button>
|
||||
<Popup IsOpen="{Binding SafetyPanelOpen}" PlacementTarget="{Binding ElementName=SafetyStatusButton}"
|
||||
Placement="Bottom" StaysOpen="False" AllowsTransparency="True">
|
||||
<Border Width="300" Margin="0,8,0,0" Padding="16"
|
||||
Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource BorderBrush}"
|
||||
BorderThickness="1" CornerRadius="{StaticResource RadiusMd}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="设备安全" FontWeight="Bold" FontSize="{StaticResource FsH2}" Margin="0,0,0,12" />
|
||||
<Grid Margin="0,3">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PCI2312" /><TextBlock Grid.Column="1" Text="{Binding Pci2312ConnectionStatus}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,3">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||
<TextBlock Text="设备状态" /><TextBlock Grid.Column="1" Text="{Binding SafetyStatusText}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,3">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||
<TextBlock Text="电源电流" /><TextBlock Grid.Column="1" Text="{Binding SafetyPowerCurrentText}" FontFamily="{StaticResource FontMono}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,3">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||
<TextBlock Text="负载电流" /><TextBlock Grid.Column="1" Text="{Binding SafetyLoadCurrentText}" FontFamily="{StaticResource FontMono}" />
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding SafetyDetailText}" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,12" />
|
||||
<Button Content="复位报警灯" Command="{Binding ResetSafetyAlarmCommand}" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button x:Name="DeviceStatusButton" Padding="10,4" BorderThickness="0"
|
||||
Background="{Binding DevicesConnected, Converter={StaticResource StatusToSoftBrush}, ConverterParameter=Pass}"
|
||||
@ -124,7 +218,7 @@
|
||||
</Grid>
|
||||
<Grid Margin="0,3,0,12">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PCI2312 安全输入输出" /><TextBlock Grid.Column="1" Text="{Binding Pci2312ConnectionStatus}" />
|
||||
<TextBlock Text="报警/急停" /><TextBlock Grid.Column="1" Text="{Binding Pci2312ConnectionStatus}" />
|
||||
</Grid>
|
||||
<WrapPanel>
|
||||
<Button Content="连接全部" Command="{Binding ConnectAllDevicesCommand}" Margin="0,0,8,8" />
|
||||
@ -225,7 +319,7 @@
|
||||
<TextBlock Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource TestingBarTextBrush}"
|
||||
Text="{Binding TestingMessage}" />
|
||||
<Button Grid.Column="2" Style="{StaticResource DangerButton}" Content="中止测试"
|
||||
<Button Grid.Column="2" Style="{StaticResource DangerButton}" Content="退出测试"
|
||||
Command="{Binding AbortTestCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@ -1,24 +1,31 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.ComponentModel;
|
||||
using SSPCTester.UI.ViewModels;
|
||||
|
||||
namespace SSPCTester.UI.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainViewModel _vm;
|
||||
private readonly MainViewModel? _vm;
|
||||
|
||||
public MainWindow(MainViewModel vm)
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public MainWindow(MainViewModel vm) : this()
|
||||
{
|
||||
_vm = vm;
|
||||
DataContext = vm;
|
||||
if (!DesignerProperties.GetIsInDesignMode(this))
|
||||
Loaded += (_, _) => _vm.Initialize();
|
||||
}
|
||||
|
||||
private void ProjectTree_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_vm == null) return;
|
||||
if (sender is TreeView tv && tv.SelectedItem is ProjectNode node)
|
||||
{
|
||||
_vm.HandleNodeActivated(node);
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<TextBox Text="{Binding SamplesPerChannel}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
||||
<TextBlock Style="{StaticResource FieldLabel}" Text="采集间隔 (ms)" />
|
||||
<TextBox Text="{Binding IntervalMs}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
|
||||
<UniformGrid Columns="2" Margin="0,16,0,0">
|
||||
<UniformGrid Columns="2" Margin="0,0,0,0">
|
||||
<Button Content="开启当前通道" Margin="0,0,6,0"
|
||||
Command="{Binding TurnOnCurrentChannelCommand}"
|
||||
IsEnabled="{Binding CanOperate}" />
|
||||
@ -38,7 +38,7 @@
|
||||
Command="{Binding TurnOffCurrentChannelCommand}"
|
||||
IsEnabled="{Binding CanOperate}" />
|
||||
</UniformGrid>
|
||||
<Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,16,0,0"
|
||||
<Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,10,0,0"
|
||||
Command="{Binding MeasureCurrentCommand}"
|
||||
IsEnabled="{Binding CanOperate}" />
|
||||
</StackPanel>
|
||||
@ -161,7 +161,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="效率" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
|
||||
<TextBlock Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource PrimaryBrush}" FontFamily="{StaticResource FontMono}" FontSize="20" FontWeight="Bold"
|
||||
Text="{Binding Current.EfficiencyPct, Converter={StaticResource NumberOrDash}, ConverterParameter=F3}" />
|
||||
Text="{Binding Current.EfficiencyPct, Converter={StaticResource NumberOrDash}, ConverterParameter=F1}" />
|
||||
<TextBlock Grid.Column="2" Text="%" Margin="6,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@ -236,7 +236,7 @@
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Header="效率(%)" Width="*">
|
||||
<DataGridTextColumn.Binding>
|
||||
<Binding Path="EfficiencyPct" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
|
||||
<Binding Path="EfficiencyPct" Converter="{StaticResource NumberOrDash}" ConverterParameter="F1" />
|
||||
</DataGridTextColumn.Binding>
|
||||
<DataGridTextColumn.ElementStyle>
|
||||
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
|
||||
|
||||
@ -90,7 +90,6 @@
|
||||
MinWidth="90">
|
||||
<ComboBoxItem Content="合格" Tag="{x:Static model:TestStatus.Pass}" />
|
||||
<ComboBoxItem Content="不合格" Tag="{x:Static model:TestStatus.Fail}" />
|
||||
<ComboBoxItem Content="已测量" Tag="{x:Static model:TestStatus.Measured}" />
|
||||
<ComboBoxItem Content="未判定" Tag="{x:Static model:TestStatus.Untested}" />
|
||||
</ComboBox>
|
||||
</DataTemplate>
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="34" />
|
||||
<RowDefinition Height="218" />
|
||||
<RowDefinition Height="354" />
|
||||
<RowDefinition Height="252" />
|
||||
<RowDefinition Height="320" />
|
||||
<RowDefinition Height="90" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
@ -28,15 +28,15 @@
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="36" />
|
||||
<RowDefinition Height="36" />
|
||||
<RowDefinition Height="36" />
|
||||
<RowDefinition Height="36" />
|
||||
<RowDefinition Height="38" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Style="{StaticResource CardTitle}" Margin="0,0,0,6" Text="设备连接配置" />
|
||||
|
||||
<Grid Grid.Row="1" Margin="0,0,0,3">
|
||||
<Grid Grid.Row="1" Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="150" />
|
||||
@ -57,7 +57,7 @@
|
||||
<TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding SspcStatus}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" Margin="0,0,0,3">
|
||||
<Grid Grid.Row="2" Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="220" />
|
||||
@ -76,7 +76,7 @@
|
||||
<TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding PowerStatus}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="3" Margin="0,0,0,3">
|
||||
<Grid Grid.Row="3" Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="220" />
|
||||
@ -95,7 +95,7 @@
|
||||
<TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding LoadStatus}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="4">
|
||||
<Grid Grid.Row="4" Margin="0,0,0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="260" />
|
||||
@ -116,37 +116,53 @@
|
||||
<TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding DaqStatus}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="5">
|
||||
<Grid Grid.Row="5" Margin="0,0,74,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90" />
|
||||
<ColumnDefinition Width="48" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="58" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="85" />
|
||||
<ColumnDefinition Width="58" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="85" />
|
||||
<ColumnDefinition Width="70" />
|
||||
<ColumnDefinition Width="70" />
|
||||
<ColumnDefinition Width="70" />
|
||||
<ColumnDefinition Width="70" />
|
||||
<ColumnDefinition Width="239" />
|
||||
<ColumnDefinition Width="237" />
|
||||
<ColumnDefinition Width="67" />
|
||||
<ColumnDefinition Width="66" />
|
||||
<ColumnDefinition Width="71" />
|
||||
<ColumnDefinition Width="93" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock FontWeight="SemiBold" VerticalAlignment="Center" Height="18" ><Run Language="zh-cn" Text="报警、急停"/></TextBlock>
|
||||
<TextBlock Grid.Column="1" Text="设备ID" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" Height="16" />
|
||||
<TextBox Grid.Column="2" Text="{Binding Devices.Pci2312.DeviceId, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" TextChanged="DraftChanged" Margin="0,4,0,4" />
|
||||
<Border Grid.Column="3" Margin="8,0,4,0" Padding="8,0"
|
||||
Background="{StaticResource NeutralSoftBrush}" CornerRadius="{StaticResource RadiusSm}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="74" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="PCI2312" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Text="设备ID" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" />
|
||||
<TextBox Grid.Column="2" Text="{Binding Devices.Pci2312.DeviceId, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" TextChanged="DraftChanged" />
|
||||
<TextBlock Grid.Column="3" Text="急停DI" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" />
|
||||
<ComboBox Grid.Column="4" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.EmergencyInputChannel}" SelectionChanged="DraftChanged" />
|
||||
<CheckBox Grid.Column="5" Margin="10,0,0,0" Content="DI高有效" IsChecked="{Binding Devices.Pci2312.EmergencyInputActiveHigh}" Click="DraftChanged" />
|
||||
<TextBlock Grid.Column="6" Text="报警DO" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" />
|
||||
<ComboBox Grid.Column="7" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.AlarmOutputChannel}" SelectionChanged="DraftChanged" />
|
||||
<CheckBox Grid.Column="8" Margin="10,0,65,0" Content="DO高有效" IsChecked="{Binding Devices.Pci2312.AlarmOutputActiveHigh}" Click="DraftChanged" Grid.ColumnSpan="2" />
|
||||
<CheckBox Grid.Column="9" Margin="10,0,0,0" Content="启用" IsChecked="{Binding Devices.Pci2312.Enabled}" Click="DraftChanged" />
|
||||
<CheckBox Grid.Column="10" Margin="10,0,0,0" Content="Mock" IsChecked="{Binding Devices.Pci2312.UseMock}" Click="DraftChanged" />
|
||||
<CheckBox Grid.Column="11" Margin="10,0,0,0" Content="自连" IsChecked="{Binding Devices.Pci2312.AutoConnect}" Click="DraftChanged" />
|
||||
<Button Grid.Column="12" Margin="8,0,0,0" Content="测试" Command="{Binding TestPci2312Command}" />
|
||||
<TextBlock Grid.Column="13" Margin="10,0,0,0" Text="{Binding Pci2312Status}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="急停" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.EmergencyInputChannel}" SelectionChanged="DraftChanged" />
|
||||
<CheckBox Grid.Column="2" Margin="8,0,0,0" Content="DI高有效" IsChecked="{Binding Devices.Pci2312.EmergencyInputActiveHigh}" Click="DraftChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Column="4" Margin="4,0,4,0" Padding="8,0"
|
||||
Background="{StaticResource NeutralSoftBrush}" CornerRadius="{StaticResource RadiusSm}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="74" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="报警" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Column="1" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.AlarmOutputChannel}" SelectionChanged="DraftChanged" />
|
||||
<CheckBox Grid.Column="2" Margin="8,0,0,0" Content="DO高有效" IsChecked="{Binding Devices.Pci2312.AlarmOutputActiveHigh}" Click="DraftChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<CheckBox Grid.Column="5" Margin="10,0,0,0" Content="启用" IsChecked="{Binding Devices.Pci2312.Enabled}" Click="DraftChanged" />
|
||||
<CheckBox Grid.Column="6" Margin="10,0,0,0" Content="Mock" IsChecked="{Binding Devices.Pci2312.UseMock}" Click="DraftChanged" />
|
||||
<CheckBox Grid.Column="7" Margin="10,0,0,0" Content="自连" IsChecked="{Binding Devices.Pci2312.AutoConnect}" Click="DraftChanged" />
|
||||
<Button Grid.Column="8" Margin="8,4,0,4" Content="测试" Command="{Binding TestPci2312Command}" />
|
||||
<TextBlock Grid.Column="9" Margin="10,0,0,0" Text="{Binding Pci2312Status}" VerticalAlignment="Center" Height="18" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
BIN
SSPCTester.zip
Normal file
BIN
SSPCTester.zip
Normal file
Binary file not shown.
22
publish.ps1
22
publish.ps1
@ -34,6 +34,28 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Initialize-ConsoleUtf8 {
|
||||
try {
|
||||
if ($IsWindows) {
|
||||
& chcp.com 65001 > $null
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "无法切换到 UTF-8 代码页,将继续使用当前代码页。"
|
||||
}
|
||||
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[Console]::InputEncoding = $utf8NoBom
|
||||
[Console]::OutputEncoding = $utf8NoBom
|
||||
Set-Variable -Name OutputEncoding -Value $utf8NoBom -Scope Global
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:DOTNET_CLI_UI_LANGUAGE)) {
|
||||
$env:DOTNET_CLI_UI_LANGUAGE = 'zh-Hans'
|
||||
}
|
||||
}
|
||||
|
||||
Initialize-ConsoleUtf8
|
||||
|
||||
$root = $PSScriptRoot
|
||||
Set-Location $root
|
||||
|
||||
|
||||
22
run.ps1
22
run.ps1
@ -19,6 +19,28 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Initialize-ConsoleUtf8 {
|
||||
try {
|
||||
if ($IsWindows) {
|
||||
& chcp.com 65001 > $null
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "无法切换到 UTF-8 代码页,将继续使用当前代码页。"
|
||||
}
|
||||
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[Console]::InputEncoding = $utf8NoBom
|
||||
[Console]::OutputEncoding = $utf8NoBom
|
||||
Set-Variable -Name OutputEncoding -Value $utf8NoBom -Scope Global
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($env:DOTNET_CLI_UI_LANGUAGE)) {
|
||||
$env:DOTNET_CLI_UI_LANGUAGE = 'zh-Hans'
|
||||
}
|
||||
}
|
||||
|
||||
Initialize-ConsoleUtf8
|
||||
|
||||
$root = $PSScriptRoot
|
||||
Set-Location $root
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user