安全功能各种修改、细节调整

This commit is contained in:
zhaotielin 2026-07-03 14:16:15 +08:00
parent 61819a3139
commit a5a36d466f
41 changed files with 797 additions and 323 deletions

117
Docs/警灯代码.html Normal file
View 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>

View File

@ -93,9 +93,9 @@ public sealed class MockSspc : MockDeviceBase, ISspc
return Task.FromResult(new ChannelMeasurement return Task.FromResult(new ChannelMeasurement
{ {
PhysicalSlot = physicalSlot, PhysicalSlot = physicalSlot,
Voltage = on ? 28.0 : 0.0, Voltage = 28.0,
Current = on ? 1.0 : 0.0, Current = on ? 1.0 : 0.0,
VoltageRaw = on ? (ushort)2800 : (ushort)0, VoltageRaw = 2800,
CurrentRaw = on ? (short)167 : (short)0, CurrentRaw = on ? (short)167 : (short)0,
Timestamp = DateTimeOffset.Now Timestamp = DateTimeOffset.Now
}); });

View File

@ -9,6 +9,9 @@ public partial class ChannelResult : ObservableObject
[ObservableProperty] private double _voltageOn; [ObservableProperty] private double _voltageOn;
[ObservableProperty] private double _currentOn; [ObservableProperty] private double _currentOn;
[ObservableProperty] private double _voltageOff; [ObservableProperty] private double _voltageOff;
[ObservableProperty] private double _currentOff;
[ObservableProperty] private double _displayVoltage;
[ObservableProperty] private double _displayCurrent;
[ObservableProperty] private TestStatus _status = TestStatus.Untested; [ObservableProperty] private TestStatus _status = TestStatus.Untested;
} }
@ -152,6 +155,8 @@ public static class ProtectTestPlan
row.Trigger = item.Trigger; row.Trigger = item.Trigger;
if (string.IsNullOrWhiteSpace(row.JudgeMode)) if (string.IsNullOrWhiteSpace(row.JudgeMode))
row.JudgeMode = ManualJudgeMode; row.JudgeMode = ManualJudgeMode;
if (row.Status == TestStatus.Measured)
row.Status = TestStatus.Untested;
project.ProtectResults.Add(row); project.ProtectResults.Add(row);
} }

View File

@ -25,7 +25,7 @@ public sealed class ReportSelection
public bool Curve { get; set; } = true; public bool Curve { get; set; } = true;
public bool Power { get; set; } = true; public bool Power { get; set; } = true;
public bool Protect { 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> /// <summary>四个测试页“测试说明”手动记录内容。</summary>

View File

@ -349,12 +349,12 @@ public sealed class ReportGenerator
} }
private static void BasicTable(IContainer container, Project p) => private static void BasicTable(IContainer container, Project p) =>
DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 0.8f }, DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 1.15f, 0.8f },
new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "判定" }, new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "关后电流\nA", "判定" },
p.BasicResults.Select(r => new ReportRow(r.Status, new[] p.BasicResults.Select(r => new ReportRow(r.Status, new[]
{ {
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"), $"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) => private static void CurveTable(IContainer container, Project p) =>
@ -386,7 +386,7 @@ public sealed class ReportGenerator
ValueOrDash(r.Trigger), ValueOrDash(r.Trigger),
ValueOrDash(r.MeasuredValue), ValueOrDash(r.MeasuredValue),
ValueOrDash(r.JudgeMode), ValueOrDash(r.JudgeMode),
StatusText(r.Status) ProtectStatusText(r.Status)
}))); })));
private static void DataTable(IContainer container, IReadOnlyList<float> widths, private static void DataTable(IContainer container, IReadOnlyList<float> widths,
@ -501,6 +501,13 @@ public sealed class ReportGenerator
TestStatus.Running => "测试中", TestStatus.Measured => "已测", 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 private static string StatusColor(TestStatus status) => status switch
{ {
TestStatus.Pass => Success, TestStatus.Fail => Danger, TestStatus.Pass => Success, TestStatus.Fail => Danger,

View File

@ -13,7 +13,7 @@ public enum ReportOutputFormat
public static class ReportFileHelper 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() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
WriteIndented = true, WriteIndented = true,

View File

@ -189,15 +189,16 @@ internal sealed class LegacyReportGenerator
{ {
t.ColumnsDefinition(cd => 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) foreach (var r in p.BasicResults)
{ {
t.Cell().Padding(3).Text($"CH{r.Channel}"); t.Cell().Padding(3).Text($"CH{r.Channel}");
t.Cell().Padding(3).Text(r.VoltageOn.ToString("F2")); t.Cell().Padding(3).Text(r.VoltageOn.ToString("F2"));
t.Cell().Padding(3).Text(r.CurrentOn.ToString("F3")); t.Cell().Padding(3).Text(r.CurrentOn.ToString("F3"));
t.Cell().Padding(3).Text(r.VoltageOff.ToString("F2")); 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)); 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.Trigger);
t.Cell().Padding(3).Text(r.MeasuredValue); t.Cell().Padding(3).Text(r.MeasuredValue);
t.Cell().Padding(3).Text(r.JudgeMode); 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 => "已测", TestStatus.Measured => "已测",
_ => "未测试" _ => "未测试"
}; };
private static string ProtectStatusText(TestStatus s) => s switch
{
TestStatus.Pass => "合格",
TestStatus.Fail => "不合格",
TestStatus.Running => "测试中",
_ => "未判定"
};
private static string StatusColor(TestStatus s) => s switch private static string StatusColor(TestStatus s) => s switch
{ {

View File

@ -118,11 +118,11 @@ public sealed class WordReportGenerator
Section(body, section++, "基础测试", "BASIC FUNCTION TEST"); Section(body, section++, "基础测试", "BASIC FUNCTION TEST");
if (p.BasicResults.Count > 0) if (p.BasicResults.Count > 0)
body.Append(ResultTable( body.Append(ResultTable(
new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "判定" }, new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "关后电流 A", "判定" },
p.BasicResults.Select(r => new[] p.BasicResults.Select(r => new[]
{ {
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"), $"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); ManualNote(body, p.TestFunction?.Basic);
} }
@ -174,7 +174,7 @@ public sealed class WordReportGenerator
ValueOrDash(r.Trigger), ValueOrDash(r.Trigger),
ValueOrDash(r.MeasuredValue), ValueOrDash(r.MeasuredValue),
ValueOrDash(r.JudgeMode), ValueOrDash(r.JudgeMode),
StatusText(r.Status) ProtectStatusText(r.Status)
}))); })));
ManualNote(body, p.TestFunction?.Protect); ManualNote(body, p.TestFunction?.Protect);
} }
@ -417,6 +417,13 @@ public sealed class WordReportGenerator
TestStatus.Measured => "已测", TestStatus.Measured => "已测",
_ => "未测试" _ => "未测试"
}; };
private static string ProtectStatusText(TestStatus status) => status switch
{
TestStatus.Pass => "合格",
TestStatus.Fail => "不合格",
TestStatus.Running => "测试中",
_ => "未判定"
};
private static string StatusColor(TestStatus status) => status switch private static string StatusColor(TestStatus status) => status switch
{ {

View File

@ -26,6 +26,9 @@ public sealed class BasicTest : ITestModule
var on = await ctx.Sspc.ReadMeasurementAsync(ch, ct).ConfigureAwait(false); var on = await ctx.Sspc.ReadMeasurementAsync(ch, ct).ConfigureAwait(false);
row.VoltageOn = on.Voltage; row.VoltageOn = on.Voltage;
row.CurrentOn = Math.Abs(on.Current); 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 ctx.Sspc.TurnOffAsync(ch, ct).ConfigureAwait(false);
await Task.Delay(BasicTestCriteria.SwitchSettleDelayMs, 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) .WaitForStableOffAsync(ctx.Sspc, ch, ct)
.ConfigureAwait(false); .ConfigureAwait(false);
row.VoltageOff = offResult.Measurement.Voltage; 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; ? TestStatus.Pass : TestStatus.Fail;
} }
catch (Exception ex) catch (Exception ex)

View File

@ -8,6 +8,7 @@ public static class BasicTestCriteria
{ {
public const double OnVoltageThreshold = 1.0; public const double OnVoltageThreshold = 1.0;
public const double OffVoltageThreshold = 0.5; public const double OffVoltageThreshold = 0.5;
public const double CurrentPresentThreshold = 0.001;
public const int SwitchSettleDelayMs = 1_000; public const int SwitchSettleDelayMs = 1_000;
public const int OffSettleTimeoutMs = 2_000; public const int OffSettleTimeoutMs = 2_000;
public const int OffInitialDelayMs = 100; 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 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>
/// 等待关断电压稳定。连续多次低于阈值才确认关断,避免旧帧和瞬时噪声造成误判。 /// 等待关状态稳定。连续多次满足“电压有数据、电流无数据”才确认关断,避免旧帧和瞬时噪声造成误判。
/// </summary> /// </summary>
public static async Task<OffStateResult> WaitForStableOffAsync( public static async Task<OffStateResult> WaitForStableOffAsync(
ISspc sspc, ISspc sspc,
@ -35,7 +46,7 @@ public static class BasicTestCriteria
while (stopwatch.ElapsedMilliseconds < OffSettleTimeoutMs) while (stopwatch.ElapsedMilliseconds < OffSettleTimeoutMs)
{ {
latest = await sspc.ReadMeasurementAsync(channel, ct).ConfigureAwait(false); 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) if (stableSamples >= RequiredStableOffSamples)
return new OffStateResult(true, latest, stopwatch.Elapsed); return new OffStateResult(true, latest, stopwatch.Elapsed);

View File

@ -22,6 +22,9 @@ public sealed class PowerTest : ITestModule
{ {
if (!_power.IsConnected) await _power.ConnectAsync(ct).ConfigureAwait(false); if (!_power.IsConnected) await _power.ConnectAsync(ct).ConfigureAwait(false);
if (!ctx.Load.IsConnected) await ctx.Load.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; int n = ctx.Sspc.ChannelCount;
ctx.Project.PowerResults.Clear(); ctx.Project.PowerResults.Clear();

View File

@ -24,7 +24,7 @@ public sealed class ProtectTest : ITestModule
await Task.Delay(150, ct).ConfigureAwait(false); await Task.Delay(150, ct).ConfigureAwait(false);
if (!preserveManualJudgement && row.Status == TestStatus.Running) 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 }); progress.Report(new TestProgress { ModuleName = Name, Step = i + 1, TotalSteps = ctx.Project.ProtectResults.Count, StepStatus = row.Status });
} }
} }

View File

@ -22,4 +22,23 @@ public sealed class BasicTestCriteriaTests
{ {
Assert.Equal(expected, BasicTestCriteria.IsOff(voltage)); 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));
}
} }

View File

@ -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); var context = new TestContext(project, sspc, forbiddenPower, load, daq);
await test.RunAsync(context, new Progress<TestProgress>(), CancellationToken.None); 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.TurnOffCount);
Assert.Equal(0, sspc.ReadMeasurementCount); Assert.Equal(0, sspc.ReadMeasurementCount);
Assert.Equal(1, power.OutputOnCount);
Assert.Equal(1, load.InputOnCount);
Assert.Equal(24, load.ReadPowerCount); Assert.Equal(24, load.ReadPowerCount);
Assert.Equal(0, daq.ReadInstantCount); Assert.Equal(0, daq.ReadInstantCount);
} }
@ -127,6 +130,7 @@ public sealed class InterfacePowerTests
private sealed class MeasuringLoad : ILoad private sealed class MeasuringLoad : ILoad
{ {
public int ReadPowerCount { get; private set; } public int ReadPowerCount { get; private set; }
public int InputOnCount { get; private set; }
public bool IsConnected => true; public bool IsConnected => true;
public string DisplayName => "it8702p"; public string DisplayName => "it8702p";
public event EventHandler<bool>? ConnectionChanged; public event EventHandler<bool>? ConnectionChanged;
@ -134,7 +138,11 @@ public sealed class InterfacePowerTests
public Task DisconnectAsync() => Task.CompletedTask; public Task DisconnectAsync() => Task.CompletedTask;
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException(); public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
public Task SetValueAsync(double value, 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<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default)
{ {
@ -146,6 +154,7 @@ public sealed class InterfacePowerTests
private sealed class FixedPowerSupply : IPowerSupply private sealed class FixedPowerSupply : IPowerSupply
{ {
public int OutputOnCount { get; private set; }
public bool IsConnected => true; public bool IsConnected => true;
public string DisplayName => "fixed power"; public string DisplayName => "fixed power";
public event EventHandler<bool>? ConnectionChanged; public event EventHandler<bool>? ConnectionChanged;
@ -153,7 +162,11 @@ public sealed class InterfacePowerTests
public Task DisconnectAsync() => Task.CompletedTask; public Task DisconnectAsync() => Task.CompletedTask;
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException(); public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
public Task SetCurrentLimitAsync(double amps, 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<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed"); public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
} }

View File

@ -29,7 +29,7 @@ public sealed class ProtectTestTests
row => AssertProtection(row, 4, "过压", "≥32V", "28.193 V"), row => AssertProtection(row, 4, "过压", "≥32V", "28.193 V"),
row => AssertProtection(row, 5, "欠压", "≤22V", "28.193 V"), row => AssertProtection(row, 5, "欠压", "≤22V", "28.193 V"),
row => AssertProtection(row, 6, "限流", "≥1.2A", "1.049 A")); 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); Assert.Contains(progress, x => x.ModuleName == ProtectTest.ModuleName);
} }

View File

@ -57,11 +57,11 @@ public sealed class ReportFileHelperTests
[Fact] [Fact]
public void ResolveFileName_UsesFallbackForBlankTemplate() 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)); 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] [Theory]

View File

@ -59,7 +59,8 @@ public sealed class ReportLayoutTests
Channel = channel, Channel = channel,
VoltageOn = failed ? 0.12 : 28.01, VoltageOn = failed ? 0.12 : 28.01,
CurrentOn = failed ? 0 : 1.002, CurrentOn = failed ? 0 : 1.002,
VoltageOff = 0.01, VoltageOff = 28.01,
CurrentOff = failed ? 0.05 : 0,
Status = failed ? TestStatus.Fail : TestStatus.Pass Status = failed ? TestStatus.Fail : TestStatus.Pass
}); });
project.CurveResults.Add(new CurveResultRow project.CurveResults.Add(new CurveResultRow

View File

@ -31,6 +31,7 @@ public sealed class WordReportGeneratorTests
Assert.Contains("基础测试", text); Assert.Contains("基础测试", text);
Assert.Contains("功率损耗", text); Assert.Contains("功率损耗", text);
Assert.Contains("PROTECTION FUNCTION", text); Assert.Contains("PROTECTION FUNCTION", text);
Assert.Contains("未判定", text);
var fontNames = document.MainDocumentPart.Document.Descendants<RunFonts>() var fontNames = document.MainDocumentPart.Document.Descendants<RunFonts>()
.SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value }) .SelectMany(x => new[] { x.Ascii?.Value, x.HighAnsi?.Value, x.EastAsia?.Value })
@ -88,7 +89,8 @@ public sealed class WordReportGeneratorTests
Channel = 1, Channel = 1,
VoltageOn = 28.0, VoltageOn = 28.0,
CurrentOn = 1.0, CurrentOn = 1.0,
VoltageOff = 0.01, VoltageOff = 28.0,
CurrentOff = 0.0,
Status = TestStatus.Pass Status = TestStatus.Pass
}); });
project.CurveResults.Add(new CurveResultRow project.CurveResults.Add(new CurveResultRow
@ -115,7 +117,7 @@ public sealed class WordReportGeneratorTests
ProtectType = "短路", ProtectType = "短路",
Trigger = "输出短路", Trigger = "输出短路",
TripTimeMs = 2.1, TripTimeMs = 2.1,
Status = TestStatus.Pass Status = TestStatus.Untested
}); });
return project; return project;
} }

View File

@ -1,10 +1,16 @@
<UserControl x:Class="SSPCTester.UI.Controls.CurvePlotControl" <UserControl x:Class="SSPCTester.UI.Controls.CurvePlotControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sp="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
MinHeight="120" MinHeight="120"
Background="{StaticResource WaveBgBrush}"> Background="{StaticResource WaveBgBrush}">
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1" CornerRadius="{StaticResource RadiusMd}"> <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> </Border>
</UserControl> </UserControl>

View File

@ -1,16 +1,27 @@
using System.ComponentModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using ScottPlot; using ScottPlot;
using ScottPlot.WPF;
using SSPCTester.Devices.Interfaces; using SSPCTester.Devices.Interfaces;
namespace SSPCTester.UI.Controls; namespace SSPCTester.UI.Controls;
public partial class CurvePlotControl : UserControl public partial class CurvePlotControl : UserControl
{ {
private readonly bool _isDesignMode;
private WpfPlot? _plot;
public CurvePlotControl() public CurvePlotControl()
{ {
InitializeComponent(); InitializeComponent();
_isDesignMode = DesignerProperties.GetIsInDesignMode(this);
if (_isDesignMode)
{
DesignPlaceholder.Visibility = Visibility.Visible;
return;
}
Loaded += (_, _) => InitPlot(); Loaded += (_, _) => InitPlot();
} }
@ -42,30 +53,39 @@ public partial class CurvePlotControl : UserControl
private void InitPlot() private void InitPlot()
{ {
Plot.Plot.Title(""); _plot ??= CreateRuntimePlot();
Plot.Plot.XLabel("t (ms)"); _plot.Plot.Title("");
Plot.Plot.YLabel("V"); _plot.Plot.XLabel("t (ms)");
Plot.Plot.Axes.AntiAlias(true); _plot.Plot.YLabel("V");
Plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD"); _plot.Plot.Axes.AntiAlias(true);
Plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD"); _plot.Plot.FigureBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
Plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA"); _plot.Plot.DataBackground.Color = ScottPlot.Color.FromHex("#FBFCFD");
_plot.Plot.Grid.MajorLineColor = ScottPlot.Color.FromHex("#DFE4EA");
Render(); Render();
} }
private WpfPlot CreateRuntimePlot()
{
var plot = new WpfPlot();
RootContainer.Children.Clear();
RootContainer.Children.Add(plot);
return plot;
}
private void Render() private void Render()
{ {
if (!IsLoaded || Plot == null) return; if (_isDesignMode || !IsLoaded || _plot == null) return;
Plot.Plot.Clear(); _plot.Plot.Clear();
var c = Curve; var c = Curve;
if (c == null || c.Voltage.Length == 0) if (c == null || c.Voltage.Length == 0)
{ {
Plot.Refresh(); _plot.Refresh();
return; return;
} }
var points = BuildViewPoints(c); var points = BuildViewPoints(c);
if (points.Count == 0) if (points.Count == 0)
{ {
Plot.Refresh(); _plot.Refresh();
return; return;
} }
@ -78,31 +98,31 @@ public partial class CurvePlotControl : UserControl
ys[i] = points[i].Voltage; ys[i] = points[i].Voltage;
} }
var color = IsRising ? "#1A73C4" : "#D23B3B"; 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.LineWidth = 2;
sig.MarkerSize = 0; sig.MarkerSize = 0;
// 阈值线 // 阈值线
double threshold = IsRising ? VMax * 0.9 : VMax * 0.1; 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.Color = ScottPlot.Color.FromHex("#98A1AD");
hLine.LinePattern = ScottPlot.LinePattern.Dashed; hLine.LinePattern = ScottPlot.LinePattern.Dashed;
hLine.LineWidth = 1; 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.LabelFontColor = ScottPlot.Color.FromHex("#98A1AD");
label.LabelFontSize = 10; label.LabelFontSize = 10;
// 触发线 t=0 // 触发线 t=0
if (!HasViewWindow(out double start, out double end) || (start <= 0 && end >= 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.Color = ScottPlot.Color.FromHex("#98A1AD");
vLine.LinePattern = ScottPlot.LinePattern.Dotted; vLine.LinePattern = ScottPlot.LinePattern.Dotted;
vLine.LineWidth = 1; vLine.LineWidth = 1;
} }
Plot.Plot.Axes.AutoScale(); _plot.Plot.Axes.AutoScale();
Plot.Refresh(); _plot.Refresh();
} }
private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve) private List<(double TimeMs, double Voltage)> BuildViewPoints(CurveData curve)
@ -131,6 +151,9 @@ public partial class CurvePlotControl : UserControl
/// <summary>导出为 PNG 字节。</summary> /// <summary>导出为 PNG 字节。</summary>
public byte[] ExportPng(int width = 800, int height = 300) 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);
} }
} }

View File

@ -57,7 +57,7 @@ public sealed class StatusToTextConverter : IValueConverter
TestStatus.Fail => "不合格", TestStatus.Fail => "不合格",
TestStatus.Running => "测试中", TestStatus.Running => "测试中",
TestStatus.Measured => "已测", TestStatus.Measured => "已测",
_ => "未测试" _ => "未判定"
} : "—"; } : "—";
public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture) => Binding.DoNothing; 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; => 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> /// <summary>null → Collapsed非 null → Visible参数 "Invert" 取反)。</summary>
public sealed class NullToVisConverter : IValueConverter public sealed class NullToVisConverter : IValueConverter
{ {

View File

@ -7,6 +7,7 @@
<cnv:StatusToSoftBrushConverter x:Key="StatusToSoftBrush" /> <cnv:StatusToSoftBrushConverter x:Key="StatusToSoftBrush" />
<cnv:StatusToTextConverter x:Key="StatusToText" /> <cnv:StatusToTextConverter x:Key="StatusToText" />
<cnv:BoolToVisConverter x:Key="BoolToVis" /> <cnv:BoolToVisConverter x:Key="BoolToVis" />
<cnv:InverseBoolConverter x:Key="InverseBoolConverter" />
<cnv:NullToVisConverter x:Key="NullToVis" /> <cnv:NullToVisConverter x:Key="NullToVis" />
<cnv:LogLevelToBrushConverter x:Key="LogLevelToBrush" /> <cnv:LogLevelToBrushConverter x:Key="LogLevelToBrush" />
<cnv:NumberOrDashConverter x:Key="NumberOrDash" /> <cnv:NumberOrDashConverter x:Key="NumberOrDash" />

View File

@ -16,6 +16,7 @@ public sealed class SafetyMonitorService : IDisposable
private readonly SemaphoreSlim _gate = new(1, 1); private readonly SemaphoreSlim _gate = new(1, 1);
private Timer? _timer; private Timer? _timer;
private bool _handlingEmergency; private bool _handlingEmergency;
private bool _softwareEmergencyActive;
private bool _disposed; private bool _disposed;
public SafetyMonitorService( public SafetyMonitorService(
@ -56,18 +57,66 @@ public sealed class SafetyMonitorService : IDisposable
SetStatus(false, false, "设备正常", "正在监控 PCI2312 急停输入"); 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; var options = _options.Pci2312;
await EnsureSafetyDioConnectedAsync(ct).ConfigureAwait(false);
if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false)) if (await IsEmergencyInputActiveAsync(options, ct).ConfigureAwait(false))
{ {
SetStatus(true, IsAlarmOutputOn, "急停已触发", "急停输入仍处于接通状态,无法复位报警灯"); const string message = "硬件急停按钮仍处于触发状态,无法执行状态重置。请先旋转或释放设备上的急停按钮,确认急停输入复位后再重试。";
return; SetStatus(true, IsAlarmOutputOn, "急停已触发", message);
return new SafetyResetResult(false, message);
} }
await SetAlarmOutputAsync(false, ct).ConfigureAwait(false); await SetAlarmOutputAsync(false, ct).ConfigureAwait(false);
SetStatus(false, false, "设备正常", "急停已复位,正在监控 PCI2312 输入"); _softwareEmergencyActive = false;
_handlingEmergency = false; _handlingEmergency = false;
SetStatus(false, false, "设备正常", "急停状态已重置,报警灯已关闭,正在监控 PCI2312 输入");
return new SafetyResetResult(true, "急停状态已重置。");
} }
private void OnTimer(object? state) private void OnTimer(object? state)
@ -92,11 +141,13 @@ public sealed class SafetyMonitorService : IDisposable
{ {
await HandleEmergencyAsync().ConfigureAwait(false); await HandleEmergencyAsync().ConfigureAwait(false);
} }
else if (_softwareEmergencyActive)
{
SetStatus(true, IsAlarmOutputOn, "软件急停已触发", "等待状态重置,电源和负载不会自动恢复");
}
else if (IsEmergencyActive || IsAlarmOutputOn) else if (IsEmergencyActive || IsAlarmOutputOn)
{ {
await SetAlarmOutputAsync(false, CancellationToken.None).ConfigureAwait(false); SetStatus(true, IsAlarmOutputOn, "急停已触发", $"DI{options.EmergencyInputChannel} 已复位,请点击状态重置关闭报警灯");
_handlingEmergency = false;
SetStatus(false, false, "设备正常", $"DI{options.EmergencyInputChannel} 已复位,报警灯已关闭");
} }
else else
{ {
@ -185,6 +236,16 @@ public sealed class SafetyMonitorService : IDisposable
IsAlarmOutputOn = on; 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) private void SetStatus(bool emergencyActive, bool alarmOutputOn, string statusText, string detailText)
{ {
IsEmergencyActive = emergencyActive; IsEmergencyActive = emergencyActive;
@ -201,3 +262,5 @@ public sealed class SafetyMonitorService : IDisposable
_gate.Dispose(); _gate.Dispose();
} }
} }
public sealed record SafetyResetResult(bool Success, string Message);

View File

@ -87,8 +87,13 @@ public partial class BasicTestViewModel : ObservableObject
var measurement = await _sspc.ReadMeasurementAsync(row.Channel); var measurement = await _sspc.ReadMeasurementAsync(row.Channel);
row.VoltageOn = measurement.Voltage; row.VoltageOn = measurement.Voltage;
row.CurrentOn = Math.Abs(measurement.Current); 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); 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) catch (Exception ex)
{ {
@ -112,12 +117,15 @@ public partial class BasicTestViewModel : ObservableObject
await Task.Delay(SwitchSettleDelayMs); await Task.Delay(SwitchSettleDelayMs);
var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel); var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel);
row.VoltageOff = offResult.Measurement.Voltage; 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; row.Status = (onOk && offResult.IsStableOff) ? TestStatus.Pass : TestStatus.Fail;
string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格"; string verdict = row.Status == TestStatus.Pass ? "合格" : "不合格";
string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时"; string offState = offResult.IsStableOff ? "已稳定关断" : "关断超时";
_log.Add(row.Status == TestStatus.Pass ? UiLogLevel.Success : UiLogLevel.Error, _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) catch (Exception ex)
{ {
@ -191,24 +199,37 @@ public partial class BasicTestViewModel : ObservableObject
if (!_sspc.IsConnected) await _sspc.ConnectAsync(ct); if (!_sspc.IsConnected) await _sspc.ConnectAsync(ct);
row.Status = TestStatus.Running; row.Status = TestStatus.Running;
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 基础测试开始:执行开状态测试。");
await _sspc.TurnOnAsync(row.Channel, ct); await _sspc.TurnOnAsync(row.Channel, ct);
await Task.Delay(SwitchSettleDelayMs, ct); await Task.Delay(SwitchSettleDelayMs, ct);
var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct); var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct);
row.VoltageOn = measurement.Voltage; row.VoltageOn = measurement.Voltage;
row.CurrentOn = Math.Abs(measurement.Current); 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); StartPolling(row.Channel);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
await StopPollingAsync(row.Channel); await StopPollingAsync(row.Channel);
_log.Add(UiLogLevel.Info, $"CH{row.Channel} 执行关状态测试。");
await _sspc.TurnOffAsync(row.Channel, ct); await _sspc.TurnOffAsync(row.Channel, ct);
await Task.Delay(SwitchSettleDelayMs, ct); await Task.Delay(SwitchSettleDelayMs, ct);
var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel, ct); var offResult = await BasicTestCriteria.WaitForStableOffAsync(_sspc, row.Channel, ct);
row.VoltageOff = offResult.Measurement.Voltage; 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; 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] [RelayCommand]
@ -216,7 +237,8 @@ public partial class BasicTestViewModel : ObservableObject
{ {
foreach (var r in Rows) 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; r.Status = TestStatus.Untested;
} }
} }
@ -302,6 +324,8 @@ public partial class BasicTestViewModel : ObservableObject
row.VoltageOn = measurement.Voltage; row.VoltageOn = measurement.Voltage;
row.CurrentOn = Math.Abs(measurement.Current); row.CurrentOn = Math.Abs(measurement.Current);
row.DisplayVoltage = row.VoltageOn;
row.DisplayCurrent = row.CurrentOn;
} }
} }
} }

View File

@ -82,6 +82,13 @@ public partial class CurveTestViewModel : ObservableObject
_ = LoadSelectedChannelCurvesAsync(); _ = LoadSelectedChannelCurvesAsync();
} }
partial void OnSelectedRowChanged(CurveResultRow? value)
{
if (_syncingViewWindow || value == null) return;
if (SelectedChannel != value.Channel)
SelectedChannel = value.Channel;
}
partial void OnRisingViewStartMsChanged(double? value) partial void OnRisingViewStartMsChanged(double? value)
{ {
if (_syncingViewWindow || SelectedRow == null) return; if (_syncingViewWindow || SelectedRow == null) return;
@ -117,10 +124,10 @@ public partial class CurveTestViewModel : ObservableObject
{ {
SelectedRow = Rows.FirstOrDefault(x => x.Channel == SelectedChannel) SelectedRow = Rows.FirstOrDefault(x => x.Channel == SelectedChannel)
?? Project?.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel); ?? Project?.CurveResults.FirstOrDefault(x => x.Channel == SelectedChannel);
RisingViewStartMs = SelectedRow?.RisingViewStartMs ?? SelectedRow?.ViewStartMs; RisingViewStartMs = SelectedRow?.RisingViewStartMs;
RisingViewEndMs = SelectedRow?.RisingViewEndMs ?? SelectedRow?.ViewEndMs; RisingViewEndMs = SelectedRow?.RisingViewEndMs;
FallingViewStartMs = SelectedRow?.FallingViewStartMs ?? SelectedRow?.ViewStartMs; FallingViewStartMs = SelectedRow?.FallingViewStartMs;
FallingViewEndMs = SelectedRow?.FallingViewEndMs ?? SelectedRow?.ViewEndMs; FallingViewEndMs = SelectedRow?.FallingViewEndMs;
} }
finally finally
{ {
@ -228,9 +235,9 @@ public partial class CurveTestViewModel : ObservableObject
} }
[RelayCommand] [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(); win.Show();
} }

View File

@ -30,8 +30,6 @@ public partial class MainViewModel : ObservableObject
private readonly UiLogSink _logSink; private readonly UiLogSink _logSink;
private readonly DeviceOptions _deviceOptions; private readonly DeviceOptions _deviceOptions;
private readonly DispatcherTimer _deviceMonitorTimer; private readonly DispatcherTimer _deviceMonitorTimer;
private readonly DispatcherTimer _safetyReadingsTimer;
private bool _safetyReadingsBusy;
private object? _viewBeforeSettings; private object? _viewBeforeSettings;
public MainViewModel( public MainViewModel(
@ -54,8 +52,6 @@ public partial class MainViewModel : ObservableObject
_deviceMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _deviceMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_deviceMonitorTimer.Tick += (_, _) => RefreshDeviceStatus(); _deviceMonitorTimer.Tick += (_, _) => RefreshDeviceStatus();
_deviceMonitorTimer.Start(); _deviceMonitorTimer.Start();
_safetyReadingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_safetyReadingsTimer.Tick += async (_, _) => await RefreshSafetyReadingsAsync().ConfigureAwait(true);
AllProjects = new ObservableCollection<Project>(); AllProjects = new ObservableCollection<Project>();
TreeRoots = new ObservableCollection<ProjectNode>(); TreeRoots = new ObservableCollection<ProjectNode>();
@ -81,8 +77,8 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty] private object? _currentView; [ObservableProperty] private object? _currentView;
[ObservableProperty] private bool _devicesConnected; [ObservableProperty] private bool _devicesConnected;
[ObservableProperty] private bool _devicePanelOpen; [ObservableProperty] private bool _devicePanelOpen;
[ObservableProperty] private bool _safetyPanelOpen;
[ObservableProperty] private bool _deviceOperationBusy; [ObservableProperty] private bool _deviceOperationBusy;
[ObservableProperty] private bool _emergencyOperationBusy;
[ObservableProperty] private string _deviceSummary = "设备状态检查中"; [ObservableProperty] private string _deviceSummary = "设备状态检查中";
[ObservableProperty] private string _sspcConnectionStatus = "检查中"; [ObservableProperty] private string _sspcConnectionStatus = "检查中";
[ObservableProperty] private string _powerConnectionStatus = "检查中"; [ObservableProperty] private string _powerConnectionStatus = "检查中";
@ -91,26 +87,13 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty] private string _pci2312ConnectionStatus = "检查中"; [ObservableProperty] private string _pci2312ConnectionStatus = "检查中";
[ObservableProperty] private string _safetyStatusText = "设备故障"; [ObservableProperty] private string _safetyStatusText = "设备故障";
[ObservableProperty] private string _safetyDetailText = "设备安全监控未启动"; [ObservableProperty] private string _safetyDetailText = "设备安全监控未启动";
[ObservableProperty] private string _safetyPowerCurrentText = "--";
[ObservableProperty] private string _safetyLoadCurrentText = "--";
[ObservableProperty] private bool _alarmLampActive; [ObservableProperty] private bool _alarmLampActive;
[ObservableProperty] private string _emergencyButtonText = "报警急停(状态重置)";
[ObservableProperty] private string _testingMessage = ""; [ObservableProperty] private string _testingMessage = "";
[ObservableProperty] private TestSessionViewModel? _currentSession; [ObservableProperty] private TestSessionViewModel? _currentSession;
partial void OnStateChanged(AppState value) => RebuildTree(); partial void OnStateChanged(AppState value) => RebuildTree();
partial void OnCurrentProjectChanged(Project? 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() private async Task ConnectConfiguredDevicesAsync()
{ {
@ -180,55 +163,11 @@ public partial class MainViewModel : ObservableObject
{ {
SafetyStatusText = _safetyMonitor.StatusText; SafetyStatusText = _safetyMonitor.StatusText;
SafetyDetailText = _safetyMonitor.DetailText; 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) => private static string DeviceStatusText(IDeviceConnection device) =>
device.IsConnected ? "已连接" : "未连接"; device.IsConnected ? "已连接" : "未连接";
@ -240,17 +179,50 @@ public partial class MainViewModel : ObservableObject
} }
[RelayCommand] [RelayCommand]
private void ToggleSafetyPanel() private async Task ToggleEmergencyAlarm()
{ {
RefreshSafetyStatus(); if (EmergencyOperationBusy) return;
SafetyPanelOpen = !SafetyPanelOpen; EmergencyOperationBusy = true;
} try
{
[RelayCommand] if (AlarmLampActive)
private async Task ResetSafetyAlarm() {
{ var result = await _safetyMonitor.ResetAsync().ConfigureAwait(false);
await _safetyMonitor.ResetAsync().ConfigureAwait(false); RefreshSafetyStatus();
RefreshSafetyStatus(); if (!result.Success)
{
Application.Current?.Dispatcher.Invoke(() =>
MessageBox.Show(
Application.Current?.MainWindow,
result.Message,
"状态重置受阻",
MessageBoxButton.OK,
MessageBoxImage.Warning));
}
}
else
{
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] [RelayCommand]
@ -427,8 +399,7 @@ public partial class MainViewModel : ObservableObject
{ {
CurrentProject = p; CurrentProject = p;
State = AppState.Testing; State = AppState.Testing;
TestingMessage = $"测试进行中:{p.Name}"; TestingMessage = $"测试页面:{p.Name}";
var session = new TestSessionViewModel(_sp, p, _sspc, _power, _load, _daq, _logSink); var session = new TestSessionViewModel(_sp, p, _sspc, _power, _load, _daq, _logSink);
CurrentSession = session; CurrentSession = session;
@ -439,30 +410,31 @@ public partial class MainViewModel : ObservableObject
try try
{ {
await session.RunAllAsync(); await _store.SaveAsync(p);
_logSink.Add(UiLogLevel.Success, "全部测试完成。"); _logSink.Add(UiLogLevel.Info, "已进入测试页面,请在各测试页手动执行当前项目。");
}
catch (OperationCanceledException)
{
_logSink.Add(UiLogLevel.Warning, "测试已中止。");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Test failed."); _logger.LogError(ex, "Failed to enter test view.");
_logSink.Add(UiLogLevel.Error, "测试出错:" + ex.Message); _logSink.Add(UiLogLevel.Error, "进入测试页面出错:" + ex.Message);
}
finally
{
await _store.SaveAsync(p);
State = AppState.Browsing;
CurrentSession = null;
} }
} }
[RelayCommand] [RelayCommand]
private void AbortTest() private async Task AbortTest()
{ {
CurrentSession?.Cancel(); 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] [RelayCommand]

View File

@ -131,6 +131,9 @@ public partial class PowerTestViewModel : ObservableObject
throw new InvalidOperationException($"IT8702P connection failed: {ex.Message}", ex); 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; double sVin = 0, sIin = 0, sVout = 0, sIout = 0, sPout = 0;
for (int s = 0; s < SamplesPerChannel; s++) for (int s = 0; s < SamplesPerChannel; s++)
{ {
@ -165,7 +168,7 @@ public partial class PowerTestViewModel : ObservableObject
row.MeasuredPowerOut = sPout / SamplesPerChannel; row.MeasuredPowerOut = sPout / SamplesPerChannel;
row.Status = TestStatus.Measured; row.Status = TestStatus.Measured;
Current = row; 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) catch (Exception ex)
{ {

View File

@ -61,8 +61,8 @@ public partial class ProtectViewModel : ObservableObject
"过压" or "欠压" => $"{volts:F3} V", "过压" or "欠压" => $"{volts:F3} V",
_ => $"{amps:F3} A" _ => $"{amps:F3} A"
}; };
if (row.Status is TestStatus.Untested or TestStatus.Running) if (row.Status == TestStatus.Running)
row.Status = TestStatus.Measured; row.Status = TestStatus.Untested;
_log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}"); _log.Add(UiLogLevel.Success, $"保护功能 {row.ProtectType} 实测值:{row.MeasuredValue}");
} }
catch (Exception ex) catch (Exception ex)

View File

@ -60,7 +60,8 @@ public partial class ReportViewModel : ObservableObject
{ {
Project = p; Project = p;
if (string.IsNullOrWhiteSpace(p.Report.FileName) || 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; p.Report.FileName = ReportFileHelper.DefaultTemplate;
} }
@ -318,15 +319,15 @@ public partial class ReportViewModel : ObservableObject
: CurveImageRenderer.RenderRisingChannel( : CurveImageRenderer.RenderRisingChannel(
row.Channel, row.Channel,
rising, rising,
row.RisingViewStartMs ?? row.ViewStartMs, row.RisingViewStartMs,
row.RisingViewEndMs ?? row.ViewEndMs), row.RisingViewEndMs),
falling == null falling == null
? null ? null
: CurveImageRenderer.RenderFallingChannel( : CurveImageRenderer.RenderFallingChannel(
row.Channel, row.Channel,
falling, falling,
row.FallingViewStartMs ?? row.ViewStartMs, row.FallingViewStartMs,
row.FallingViewEndMs ?? row.ViewEndMs)); row.FallingViewEndMs));
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -87,7 +87,7 @@
</DataGridTextColumn> </DataGridTextColumn>
<DataGridTextColumn Header="电压(V)" Width="*"> <DataGridTextColumn Header="电压(V)" Width="*">
<DataGridTextColumn.Binding> <DataGridTextColumn.Binding>
<Binding Path="VoltageOn" Converter="{StaticResource NumberOrDash}" ConverterParameter="F2" /> <Binding Path="DisplayVoltage" Converter="{StaticResource NumberOrDash}" ConverterParameter="F2" />
</DataGridTextColumn.Binding> </DataGridTextColumn.Binding>
<DataGridTextColumn.ElementStyle> <DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" /> <Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />
@ -95,7 +95,7 @@
</DataGridTextColumn> </DataGridTextColumn>
<DataGridTextColumn Header="电流(A)" Width="*"> <DataGridTextColumn Header="电流(A)" Width="*">
<DataGridTextColumn.Binding> <DataGridTextColumn.Binding>
<Binding Path="CurrentOn" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" /> <Binding Path="DisplayCurrent" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" />
</DataGridTextColumn.Binding> </DataGridTextColumn.Binding>
<DataGridTextColumn.ElementStyle> <DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" /> <Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />

View File

@ -3,20 +3,20 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctl="clr-namespace:SSPCTester.UI.Controls" xmlns:ctl="clr-namespace:SSPCTester.UI.Controls"
xmlns:vm="clr-namespace:SSPCTester.UI.ViewModels" 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="曲线查看器" Title="曲线查看器"
Width="1100" Height="720" Width="1100" Height="720"
MinWidth="600" MinHeight="400" MinWidth="600" MinHeight="400"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
WindowState="Maximized"
Background="{StaticResource BgAppBrush}" Background="{StaticResource BgAppBrush}"
d:DataContext="{d:DesignInstance Type=vm:CurveTestViewModel}" 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"> mc:Ignorable="d">
<Grid Margin="16"> <Grid Margin="16">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Margin="0,0,0,12"> <Grid Margin="0,0,0,12">
@ -31,64 +31,26 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" FontSize="{StaticResource FsH1}" FontWeight="Bold"> <TextBlock VerticalAlignment="Center" FontSize="{StaticResource FsH1}" FontWeight="Bold">
<Run Text="曲线查看器 " /> <Run x:Name="TitleRun" Text="曲线查看器 " />
<Run Text="CH" Foreground="{StaticResource TextSecondaryBrush}" /> <Run Text="CH" Foreground="{StaticResource TextSecondaryBrush}" />
<Run Text="{Binding SelectedChannel, Mode=OneWay}" Foreground="{StaticResource PrimaryBrush}" /> <Run Text="{Binding SelectedChannel, Mode=OneWay}" Foreground="{StaticResource PrimaryBrush}" />
</TextBlock> </TextBlock>
<ComboBox Grid.Column="1" Width="120" Margin="0,0,8,0" <ComboBox Grid.Column="1" Width="120" Margin="0,0,8,0"
ItemsSource="{Binding Channels}" SelectedItem="{Binding SelectedChannel}" /> ItemsSource="{Binding Channels}" SelectedItem="{Binding SelectedChannel}" />
<TextBlock Grid.Column="2" Text="起点" VerticalAlignment="Center" Margin="0,0,4,0" /> <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" <TextBox x:Name="StartTextBox" Grid.Column="3" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
Text="{Binding RisingViewStartMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
FontFamily="{StaticResource FontMono}" /> FontFamily="{StaticResource FontMono}" />
<TextBlock Grid.Column="4" Text="终点" VerticalAlignment="Center" Margin="0,0,4,0" /> <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" <TextBox x:Name="EndTextBox" Grid.Column="5" Width="64" Height="26" Margin="0,0,8,0" Padding="4,0"
Text="{Binding RisingViewEndMs, UpdateSourceTrigger=PropertyChanged, TargetNullValue=''}"
FontFamily="{StaticResource FontMono}" /> FontFamily="{StaticResource FontMono}" />
<Button Grid.Column="6" Content="默认" Width="54" Height="26" Margin="0,0,8,0" Padding="0" <Button x:Name="ResetButton" Grid.Column="6" Content="默认" Width="54" Height="26" Margin="0,0,8,0" Padding="0" />
Command="{Binding ResetRisingCurveViewCommand}" />
<Button Grid.Column="7" Content="关闭" Width="80" IsCancel="True" Click="Close_Click" /> <Button Grid.Column="7" Content="关闭" Width="80" IsCancel="True" Click="Close_Click" />
</Grid> </Grid>
<Border Grid.Row="1" Style="{StaticResource Card}" Margin="0,0,0,6"> <Border Grid.Row="1" Style="{StaticResource Card}">
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="开启曲线电压上升90% 阈值)" /> <TextBlock x:Name="CurveTitleText" DockPanel.Dock="Top" Style="{StaticResource CardTitle}" />
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,6"> <ctl:CurvePlotControl x:Name="CurvePlot" VMax="{Binding VMax}" />
<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}" />
</DockPanel> </DockPanel>
</Border> </Border>
</Grid> </Grid>

View File

@ -1,14 +1,50 @@
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using SSPCTester.UI.Controls;
using SSPCTester.UI.ViewModels; using SSPCTester.UI.ViewModels;
namespace SSPCTester.UI.Views; namespace SSPCTester.UI.Views;
public partial class CurvePopupWindow : Window public partial class CurvePopupWindow : Window
{ {
public CurvePopupWindow(CurveTestViewModel vm) public CurvePopupWindow()
{ {
InitializeComponent(); InitializeComponent();
}
public CurvePopupWindow(CurveTestViewModel vm, string? curveKind)
: this()
{
DataContext = vm; 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(); private void Close_Click(object sender, RoutedEventArgs e) => Close();

View File

@ -54,12 +54,10 @@
</Grid> </Grid>
<Button Style="{StaticResource PrimaryButton}" Content="采集开启曲线" Margin="0,16,0,0" <Button Style="{StaticResource PrimaryButton}" Content="采集开启曲线" Margin="0,16,0,0"
Command="{Binding CaptureRisingCommand}" 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" <Button Style="{StaticResource PrimaryButton}" Content="采集关断曲线" Margin="0,8,0,0"
Command="{Binding CaptureFallingCommand}" Command="{Binding CaptureFallingCommand}"
IsEnabled="{Binding Busy, Converter={StaticResource BoolToVis}, ConverterParameter=Invert}" /> IsEnabled="{Binding Busy, Converter={StaticResource InverseBoolConverter}}" />
<Button Content="在新窗口查看曲线" Margin="0,8,0,0"
Command="{Binding OpenPopupCommand}" />
<TextBlock Margin="0,12,0,0" TextWrapping="Wrap" <TextBlock Margin="0,12,0,0" TextWrapping="Wrap"
Foreground="{StaticResource TextMutedBrush}" Foreground="{StaticResource TextMutedBrush}"
Text="开启/关断时间以软件发送 Modbus 命令前的标记为基准,包含串口通信延迟,不等同于硬件同步触发精度。" /> Text="开启/关断时间以软件发送 Modbus 命令前的标记为基准,包含串口通信延迟,不等同于硬件同步触发精度。" />
@ -92,7 +90,8 @@
<Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0" <Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
Command="{Binding ResetRisingCurveViewCommand}" /> Command="{Binding ResetRisingCurveViewCommand}" />
<Button Content="放大" Width="64" Height="26" Padding="0" <Button Content="放大" Width="64" Height="26" Padding="0"
Command="{Binding OpenPopupCommand}" /> Command="{Binding OpenPopupCommand}"
CommandParameter="Rising" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<ctl:CurvePlotControl Curve="{Binding RisingCurve}" IsRising="True" VMax="{Binding VMax}" <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" <Button Content="默认" Width="54" Height="26" Padding="0" Margin="0,0,8,0"
Command="{Binding ResetFallingCurveViewCommand}" /> Command="{Binding ResetFallingCurveViewCommand}" />
<Button Content="放大" Width="64" Height="26" Padding="0" <Button Content="放大" Width="64" Height="26" Padding="0"
Command="{Binding OpenPopupCommand}" /> Command="{Binding OpenPopupCommand}"
CommandParameter="Falling" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<ctl:CurvePlotControl Curve="{Binding FallingCurve}" IsRising="False" VMax="{Binding VMax}" <ctl:CurvePlotControl Curve="{Binding FallingCurve}" IsRising="False" VMax="{Binding VMax}"
@ -141,7 +141,8 @@
<Border Style="{StaticResource Card}" Margin="0,0,12,0"> <Border Style="{StaticResource Card}" Margin="0,0,12,0">
<DockPanel> <DockPanel>
<TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道时间参数" /> <TextBlock DockPanel.Dock="Top" Style="{StaticResource CardTitle}" Text="各通道时间参数" />
<DataGrid ItemsSource="{Binding Rows}"> <DataGrid ItemsSource="{Binding Rows}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="通道" Width="70"> <DataGridTextColumn Header="通道" Width="70">
<DataGridTextColumn.Binding> <DataGridTextColumn.Binding>

View File

@ -37,58 +37,152 @@
FontSize="{StaticResource FsTitle}" FontWeight="Bold" /> FontSize="{StaticResource FsTitle}" FontWeight="Bold" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="2" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Grid.Column="2" VerticalAlignment="Center">
<Grid Margin="0,0,8,0"> <Viewbox Width="26" Height="26" Margin="0,0,6,0"
<Button x:Name="SafetyStatusButton" Width="34" Height="30" Padding="0" ToolTip="{Binding SafetyDetailText}">
Command="{Binding ToggleSafetyPanelCommand}" <Canvas Width="64" Height="64">
ToolTip="{Binding SafetyStatusText}" <Canvas.Resources>
Background="Transparent" BorderBrush="{StaticResource BorderBrush}"> <RadialGradientBrush x:Key="AlarmRedGlassBrush" Center="0.34,0.16" GradientOrigin="0.34,0.16" RadiusX="0.78" RadiusY="0.78">
<Grid> <GradientStop Color="#FFFF8A8A" Offset="0" />
<Ellipse Width="18" Height="18" Stroke="{StaticResource BorderBrush}" StrokeThickness="1"> <GradientStop Color="#FFDF0010" Offset="0.36" />
<Ellipse.Style> <GradientStop Color="#FF720006" Offset="1" />
<Style TargetType="Ellipse"> </RadialGradientBrush>
<Setter Property="Fill" Value="{StaticResource SuccessBrush}" /> <RadialGradientBrush x:Key="AlarmGrayGlassBrush" Center="0.34,0.16" GradientOrigin="0.34,0.16" RadiusX="0.78" RadiusY="0.78">
<Style.Triggers> <GradientStop Color="#FFDEDEDE" Offset="0" />
<DataTrigger Binding="{Binding AlarmLampActive}" Value="True"> <GradientStop Color="#FF969696" Offset="0.36" />
<Setter Property="Fill" Value="{StaticResource DangerBrush}" /> <GradientStop Color="#FF4B4B4B" Offset="1" />
</DataTrigger> </RadialGradientBrush>
</Style.Triggers> <LinearGradientBrush x:Key="AlarmBaseBrush" StartPoint="0,0" EndPoint="0,1">
</Style> <GradientStop Color="#FF3B3B3B" Offset="0" />
</Ellipse.Style> <GradientStop Color="#FF101010" Offset="0.45" />
</Ellipse> <GradientStop Color="#FF303030" Offset="1" />
<TextBlock Text="!" Foreground="White" FontSize="12" FontWeight="Bold" </LinearGradientBrush>
HorizontalAlignment="Center" VerticalAlignment="Center" /> </Canvas.Resources>
</Grid> <Path x:Name="AlarmGlass" Data="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z">
</Button> <Path.Style>
<Popup IsOpen="{Binding SafetyPanelOpen}" PlacementTarget="{Binding ElementName=SafetyStatusButton}" <Style TargetType="Path">
Placement="Bottom" StaysOpen="False" AllowsTransparency="True"> <Setter Property="Fill" Value="{StaticResource AlarmGrayGlassBrush}" />
<Border Width="300" Margin="0,8,0,0" Padding="16" <Style.Triggers>
Background="{StaticResource BgSurfaceBrush}" BorderBrush="{StaticResource BorderBrush}" <DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
BorderThickness="1" CornerRadius="{StaticResource RadiusMd}"> <Setter Property="Fill" Value="{StaticResource AlarmRedGlassBrush}" />
<StackPanel> <DataTrigger.EnterActions>
<TextBlock Text="设备安全" FontWeight="Bold" FontSize="{StaticResource FsH2}" Margin="0,0,0,12" /> <BeginStoryboard x:Name="AlarmGlassBlink">
<Grid Margin="0,3"> <Storyboard>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions> <DoubleAnimation Storyboard.TargetProperty="Opacity"
<TextBlock Text="PCI2312" /><TextBlock Grid.Column="1" Text="{Binding Pci2312ConnectionStatus}" /> From="1" To=".42" Duration="0:0:0.36" AutoReverse="True" RepeatBehavior="Forever" />
</Grid> </Storyboard>
<Grid Margin="0,3"> </BeginStoryboard>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions> </DataTrigger.EnterActions>
<TextBlock Text="设备状态" /><TextBlock Grid.Column="1" Text="{Binding SafetyStatusText}" /> <DataTrigger.ExitActions>
</Grid> <StopStoryboard BeginStoryboardName="AlarmGlassBlink" />
<Grid Margin="0,3"> </DataTrigger.ExitActions>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions> </DataTrigger>
<TextBlock Text="电源电流" /><TextBlock Grid.Column="1" Text="{Binding SafetyPowerCurrentText}" FontFamily="{StaticResource FontMono}" /> </Style.Triggers>
</Grid> </Style>
<Grid Margin="0,3"> </Path.Style>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions> </Path>
<TextBlock Text="负载电流" /><TextBlock Grid.Column="1" Text="{Binding SafetyLoadCurrentText}" FontFamily="{StaticResource FontMono}" /> <Path x:Name="AlarmRim" Data="M14 44V23C14 10.5 22.1 4 32 4s18 6.5 18 19v21z"
</Grid> Fill="Transparent">
<TextBlock Text="{Binding SafetyDetailText}" TextWrapping="Wrap" <Path.Style>
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,12" /> <Style TargetType="Path">
<Button Content="复位报警灯" Command="{Binding ResetSafetyAlarmCommand}" HorizontalAlignment="Stretch" /> <Setter Property="Stroke" Value="#FF555555" />
</StackPanel> <Setter Property="StrokeThickness" Value="1.4" />
</Border> <Style.Triggers>
</Popup> <DataTrigger Binding="{Binding AlarmLampActive}" Value="True">
</Grid> <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>
<Grid> <Grid>
<Button x:Name="DeviceStatusButton" Padding="10,4" BorderThickness="0" <Button x:Name="DeviceStatusButton" Padding="10,4" BorderThickness="0"
Background="{Binding DevicesConnected, Converter={StaticResource StatusToSoftBrush}, ConverterParameter=Pass}" Background="{Binding DevicesConnected, Converter={StaticResource StatusToSoftBrush}, ConverterParameter=Pass}"
@ -124,7 +218,7 @@
</Grid> </Grid>
<Grid Margin="0,3,0,12"> <Grid Margin="0,3,0,12">
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions> <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> </Grid>
<WrapPanel> <WrapPanel>
<Button Content="连接全部" Command="{Binding ConnectAllDevicesCommand}" Margin="0,0,8,8" /> <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" <TextBlock Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="{StaticResource TestingBarTextBrush}" FontWeight="SemiBold" Foreground="{StaticResource TestingBarTextBrush}"
Text="{Binding TestingMessage}" /> Text="{Binding TestingMessage}" />
<Button Grid.Column="2" Style="{StaticResource DangerButton}" Content="中止测试" <Button Grid.Column="2" Style="{StaticResource DangerButton}" Content="退出测试"
Command="{Binding AbortTestCommand}" /> Command="{Binding AbortTestCommand}" />
</Grid> </Grid>
</Border> </Border>

View File

@ -1,24 +1,31 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.ComponentModel;
using SSPCTester.UI.ViewModels; using SSPCTester.UI.ViewModels;
namespace SSPCTester.UI.Views; namespace SSPCTester.UI.Views;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
private readonly MainViewModel _vm; private readonly MainViewModel? _vm;
public MainWindow(MainViewModel vm) public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
}
public MainWindow(MainViewModel vm) : this()
{
_vm = vm; _vm = vm;
DataContext = vm; DataContext = vm;
Loaded += (_, _) => _vm.Initialize(); if (!DesignerProperties.GetIsInDesignMode(this))
Loaded += (_, _) => _vm.Initialize();
} }
private void ProjectTree_MouseDoubleClick(object sender, MouseButtonEventArgs e) private void ProjectTree_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{ {
if (_vm == null) return;
if (sender is TreeView tv && tv.SelectedItem is ProjectNode node) if (sender is TreeView tv && tv.SelectedItem is ProjectNode node)
{ {
_vm.HandleNodeActivated(node); _vm.HandleNodeActivated(node);

View File

@ -30,7 +30,7 @@
<TextBox Text="{Binding SamplesPerChannel}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" /> <TextBox Text="{Binding SamplesPerChannel}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" />
<TextBlock Style="{StaticResource FieldLabel}" Text="采集间隔 (ms)" /> <TextBlock Style="{StaticResource FieldLabel}" Text="采集间隔 (ms)" />
<TextBox Text="{Binding IntervalMs}" Margin="0,0,0,12" FontFamily="{StaticResource FontMono}" /> <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" <Button Content="开启当前通道" Margin="0,0,6,0"
Command="{Binding TurnOnCurrentChannelCommand}" Command="{Binding TurnOnCurrentChannelCommand}"
IsEnabled="{Binding CanOperate}" /> IsEnabled="{Binding CanOperate}" />
@ -38,7 +38,7 @@
Command="{Binding TurnOffCurrentChannelCommand}" Command="{Binding TurnOffCurrentChannelCommand}"
IsEnabled="{Binding CanOperate}" /> IsEnabled="{Binding CanOperate}" />
</UniformGrid> </UniformGrid>
<Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,16,0,0" <Button Style="{StaticResource PrimaryButton}" Content="测量当前通道" Margin="0,10,0,0"
Command="{Binding MeasureCurrentCommand}" Command="{Binding MeasureCurrentCommand}"
IsEnabled="{Binding CanOperate}" /> IsEnabled="{Binding CanOperate}" />
</StackPanel> </StackPanel>
@ -161,7 +161,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="效率" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" /> <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" <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}" /> <TextBlock Grid.Column="2" Text="%" Margin="6,0,0,0" VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}" />
</Grid> </Grid>
</StackPanel> </StackPanel>
@ -236,7 +236,7 @@
</DataGridTextColumn> </DataGridTextColumn>
<DataGridTextColumn Header="效率(%)" Width="*"> <DataGridTextColumn Header="效率(%)" Width="*">
<DataGridTextColumn.Binding> <DataGridTextColumn.Binding>
<Binding Path="EfficiencyPct" Converter="{StaticResource NumberOrDash}" ConverterParameter="F3" /> <Binding Path="EfficiencyPct" Converter="{StaticResource NumberOrDash}" ConverterParameter="F1" />
</DataGridTextColumn.Binding> </DataGridTextColumn.Binding>
<DataGridTextColumn.ElementStyle> <DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" /> <Style TargetType="TextBlock" BasedOn="{StaticResource MonoText}" />

View File

@ -90,7 +90,6 @@
MinWidth="90"> MinWidth="90">
<ComboBoxItem Content="合格" Tag="{x:Static model:TestStatus.Pass}" /> <ComboBoxItem Content="合格" Tag="{x:Static model:TestStatus.Pass}" />
<ComboBoxItem Content="不合格" Tag="{x:Static model:TestStatus.Fail}" /> <ComboBoxItem Content="不合格" Tag="{x:Static model:TestStatus.Fail}" />
<ComboBoxItem Content="已测量" Tag="{x:Static model:TestStatus.Measured}" />
<ComboBoxItem Content="未判定" Tag="{x:Static model:TestStatus.Untested}" /> <ComboBoxItem Content="未判定" Tag="{x:Static model:TestStatus.Untested}" />
</ComboBox> </ComboBox>
</DataTemplate> </DataTemplate>

View File

@ -9,8 +9,8 @@
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="34" /> <RowDefinition Height="34" />
<RowDefinition Height="218" /> <RowDefinition Height="252" />
<RowDefinition Height="354" /> <RowDefinition Height="320" />
<RowDefinition Height="90" /> <RowDefinition Height="90" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@ -28,15 +28,15 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="30" /> <RowDefinition Height="36" />
<RowDefinition Height="30" /> <RowDefinition Height="36" />
<RowDefinition Height="30" /> <RowDefinition Height="36" />
<RowDefinition Height="30" /> <RowDefinition Height="36" />
<RowDefinition Height="30" /> <RowDefinition Height="38" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Style="{StaticResource CardTitle}" Margin="0,0,0,6" Text="设备连接配置" /> <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> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120" /> <ColumnDefinition Width="120" />
<ColumnDefinition Width="150" /> <ColumnDefinition Width="150" />
@ -57,7 +57,7 @@
<TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding SspcStatus}" VerticalAlignment="Center" /> <TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding SspcStatus}" VerticalAlignment="Center" />
</Grid> </Grid>
<Grid Grid.Row="2" Margin="0,0,0,3"> <Grid Grid.Row="2" Margin="0,0,0,5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120" /> <ColumnDefinition Width="120" />
<ColumnDefinition Width="220" /> <ColumnDefinition Width="220" />
@ -76,7 +76,7 @@
<TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding PowerStatus}" VerticalAlignment="Center" /> <TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding PowerStatus}" VerticalAlignment="Center" />
</Grid> </Grid>
<Grid Grid.Row="3" Margin="0,0,0,3"> <Grid Grid.Row="3" Margin="0,0,0,5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120" /> <ColumnDefinition Width="120" />
<ColumnDefinition Width="220" /> <ColumnDefinition Width="220" />
@ -95,7 +95,7 @@
<TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding LoadStatus}" VerticalAlignment="Center" /> <TextBlock Grid.Column="6" Margin="10,0,0,0" Text="{Binding LoadStatus}" VerticalAlignment="Center" />
</Grid> </Grid>
<Grid Grid.Row="4"> <Grid Grid.Row="4" Margin="0,0,0,5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="120" /> <ColumnDefinition Width="120" />
<ColumnDefinition Width="260" /> <ColumnDefinition Width="260" />
@ -116,37 +116,53 @@
<TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding DaqStatus}" VerticalAlignment="Center" /> <TextBlock Grid.Column="7" Margin="10,0,0,0" Text="{Binding DaqStatus}" VerticalAlignment="Center" />
</Grid> </Grid>
<Grid Grid.Row="5"> <Grid Grid.Row="5" Margin="0,0,74,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="90" /> <ColumnDefinition Width="90" />
<ColumnDefinition Width="48" /> <ColumnDefinition Width="48" />
<ColumnDefinition Width="60" /> <ColumnDefinition Width="60" />
<ColumnDefinition Width="58" /> <ColumnDefinition Width="239" />
<ColumnDefinition Width="80" /> <ColumnDefinition Width="237" />
<ColumnDefinition Width="85" /> <ColumnDefinition Width="67" />
<ColumnDefinition Width="58" /> <ColumnDefinition Width="66" />
<ColumnDefinition Width="80" /> <ColumnDefinition Width="71" />
<ColumnDefinition Width="85" /> <ColumnDefinition Width="93" />
<ColumnDefinition Width="70" /> <ColumnDefinition />
<ColumnDefinition Width="70" />
<ColumnDefinition Width="70" />
<ColumnDefinition Width="70" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="PCI2312" FontWeight="SemiBold" VerticalAlignment="Center" /> <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" /> <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" /> <TextBox Grid.Column="2" Text="{Binding Devices.Pci2312.DeviceId, UpdateSourceTrigger=PropertyChanged}" FontFamily="{StaticResource FontMono}" TextChanged="DraftChanged" Margin="0,4,0,4" />
<TextBlock Grid.Column="3" Text="急停DI" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" /> <Border Grid.Column="3" Margin="8,0,4,0" Padding="8,0"
<ComboBox Grid.Column="4" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.EmergencyInputChannel}" SelectionChanged="DraftChanged" /> Background="{StaticResource NeutralSoftBrush}" CornerRadius="{StaticResource RadiusSm}">
<CheckBox Grid.Column="5" Margin="10,0,0,0" Content="DI高有效" IsChecked="{Binding Devices.Pci2312.EmergencyInputActiveHigh}" Click="DraftChanged" /> <Grid>
<TextBlock Grid.Column="6" Text="报警DO" Style="{StaticResource FieldLabel}" Margin="8,0,4,0" /> <Grid.ColumnDefinitions>
<ComboBox Grid.Column="7" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.AlarmOutputChannel}" SelectionChanged="DraftChanged" /> <ColumnDefinition Width="40" />
<CheckBox Grid.Column="8" Margin="10,0,65,0" Content="DO高有效" IsChecked="{Binding Devices.Pci2312.AlarmOutputActiveHigh}" Click="DraftChanged" Grid.ColumnSpan="2" /> <ColumnDefinition Width="74" />
<CheckBox Grid.Column="9" Margin="10,0,0,0" Content="启用" IsChecked="{Binding Devices.Pci2312.Enabled}" Click="DraftChanged" /> <ColumnDefinition Width="*" />
<CheckBox Grid.Column="10" Margin="10,0,0,0" Content="Mock" IsChecked="{Binding Devices.Pci2312.UseMock}" Click="DraftChanged" /> </Grid.ColumnDefinitions>
<CheckBox Grid.Column="11" Margin="10,0,0,0" Content="自连" IsChecked="{Binding Devices.Pci2312.AutoConnect}" Click="DraftChanged" /> <TextBlock Text="急停" FontWeight="SemiBold" VerticalAlignment="Center" />
<Button Grid.Column="12" Margin="8,0,0,0" Content="测试" Command="{Binding TestPci2312Command}" /> <ComboBox Grid.Column="1" ItemsSource="{Binding Pci2312Channels}" SelectedItem="{Binding Devices.Pci2312.EmergencyInputChannel}" SelectionChanged="DraftChanged" />
<TextBlock Grid.Column="13" Margin="10,0,0,0" Text="{Binding Pci2312Status}" VerticalAlignment="Center" /> <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>
</Grid> </Grid>
</Border> </Border>

BIN
SSPCTester.zip Normal file

Binary file not shown.

View File

@ -34,6 +34,28 @@ param(
) )
$ErrorActionPreference = 'Stop' $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 $root = $PSScriptRoot
Set-Location $root Set-Location $root

22
run.ps1
View File

@ -19,6 +19,28 @@ param(
) )
$ErrorActionPreference = 'Stop' $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 $root = $PSScriptRoot
Set-Location $root Set-Location $root