第 0 阶段:教材项目 2 任务 1 在网页中原样运行

- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、
  GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、
  教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式)
- server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发
- web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏
- 端到端测试与 100 个程序并发的压力测试
- 教材逐任务摘要(docs/教材摘要)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 10:15:14 +08:00
commit 98be8331bc
71 files changed
+7919

No files matched your search

@@ -0,0 +1,151 @@
using System;
using System.Linq;
using DeviceCtrlLibrary.Model;
using Sim.Host;
namespace DeviceCtrlLibrary;
/// <summary>
/// 教材的设备控制入口:DeviceCtrlHelper.ledHelper.SendLedString(...) 等。
/// 返回值约定与教材仿真版 dll 保持一致;不同之处是数值来自虚拟实训台,而不是随机数。
/// </summary>
public class DeviceCtrlHelper
{
// 教材 dll 里这些是公开静态字段(不是属性),这里保持一致
public static SerialPortHelper serialPortHelper = new();
public static LEDHelper ledHelper = new();
public static ADAM4017Helper adam4017Helper = new();
public static ADAM4150Helper adam4150Helper = new();
public static ZigBeeHelper zigBeeHelper = new();
}
public class SerialPortHelper
{
private string? _portName;
private int _baudRate;
public void Open(string portName, int BaudRate)
{
_portName = portName;
_baudRate = BaudRate;
}
/// <summary>返回一串十六进制字节(大写、空格分隔、末尾带空格),未打开时返回 "Error"</summary>
public string GetStringData()
{
if (string.IsNullOrWhiteSpace(_portName) || _baudRate == 0) return "Error";
var bytes = new byte[27];
Random.Shared.NextBytes(bytes);
return string.Concat(bytes.Select(b => $"{b:X2} "));
}
public void Close() => _portName = null;
}
public class LEDHelper
{
private const string SendFailed = "Error:发送失败";
private string? _portName;
public void OpenLedCom(string portName) => _portName = portName;
/// <summary>成功时原样返回发送的文字,并显示到实训台的 LED 屏上;串口未打开时返回 "Error:发送失败"</summary>
public string SendLedString(string sendString)
{
if (string.IsNullOrWhiteSpace(_portName)) return SendFailed;
SimHost.SendDevice("led", "text", sendString);
return sendString;
}
/// <summary>教材 dll 里没有这个方法,但书上项目 2 任务 3 调用了它;保留空方法让书上代码能编译</summary>
public void Close() => _portName = null;
}
public class ADAM4017Helper
{
private string? _portName;
private int[] _channels = { 0, 1, 2, 3, 4, 5, 6 };
public void Open(string PortName, int Temperature_Port = 0, int Humidity_Port = 1, int Light_Port = 2,
int AirPressure_Port = 3, int AirQuality_Port = 4, int CO2_Port = 5, int Wind_Port = 6)
{
_portName = PortName;
_channels = new[] { Temperature_Port, Humidity_Port, Light_Port, AirPressure_Port, AirQuality_Port, CO2_Port, Wind_Port };
}
/// <summary>
/// 读取各通道数值。数值一律是整数字符串(教材项目 7 用 Convert.ToInt16 转换,带小数会出错);
/// 串口未打开时所有属性为 null,与教材仿真版一致。
/// </summary>
public ADAM4017ResModel SetData()
{
var model = new ADAM4017ResModel();
if (string.IsNullOrWhiteSpace(_portName)) return model;
model.temperatureValue = Read(0);
model.humidityValue = Read(1);
model.lightValue = Read(2);
model.airPressureValue = Read(3);
model.airQualityValue = Read(4);
model.CO2Value = Read(5);
model.windValue = Read(6);
return model;
}
private string Read(int quantity) =>
TrainingBoard.AnalogInputs.TryGetValue(_channels[quantity], out var value) ? value.ToString() : "0";
public void Close() => _portName = null;
}
public class ADAM4150Helper
{
private string? _portName;
private int[] _channels = { 0, 1, 2, 3 };
public void Open(string PortName, int BodyInfrared_Port = 0, int Fire_Port = 1, int Smoke_Port = 2, int Infrared_Port = 3)
{
_portName = PortName;
_channels = new[] { BodyInfrared_Port, Fire_Port, Smoke_Port, Infrared_Port };
}
/// <summary>读取人体红外、火焰、烟雾、红外对射;串口未打开时全部为 false</summary>
public ADAM4150ResModel SetData()
{
var model = new ADAM4150ResModel();
if (string.IsNullOrWhiteSpace(_portName)) return model;
model.bodyInfraredValue = Read(0);
model.fireValue = Read(1);
model.smokeValue = Read(2);
model.infraredValue = Read(3);
return model;
}
private bool Read(int sensor) => TrainingBoard.DigitalInputs.TryGetValue(_channels[sensor], out var value) && value;
/// <summary>控制继电器 DO0–DO4(DO0 报警灯、DO1 照明灯、DO2 路灯),通道号有效时返回 true</summary>
public bool OnOff(int DO, bool IsOpen)
{
if (DO < 0 || DO > 4) return false;
SimHost.SendDevice("adam4150", "do", new[] { DO, IsOpen ? 1 : 0 });
return true;
}
public void Close() => _portName = null;
}
public class ZigBeeHelper
{
private string? _portName;
public void Open(string PortName) => _portName = PortName;
/// <summary>控制 ZigBee 继电器节点 0、1,通道号有效时返回 true</summary>
public bool OnOff(int RelayNum, bool IsOpen)
{
if (RelayNum < 0 || RelayNum > 1) return false;
SimHost.SendDevice("zigbee", "relay", new[] { RelayNum, IsOpen ? 1 : 0 });
return true;
}
public void Close() => _portName = null;
}
@@ -0,0 +1,30 @@
// 与教材 dll 一致:ADAM-4017 的数值全部是字符串,ADAM-4150 的数值全部是 bool。
namespace DeviceCtrlLibrary.Model;
public class ADAM4017ResModel
{
public string temperatureValue { get; set; }
public string humidityValue { get; set; }
public string lightValue { get; set; }
public string airPressureValue { get; set; }
public string airQualityValue { get; set; }
public string CO2Value { get; set; }
public string windValue { get; set; }
}
public class ADAM4150ResModel
{
public bool bodyInfraredValue { get; set; }
public bool fireValue { get; set; }
public bool smokeValue { get; set; }
public bool infraredValue { get; set; }
}
public class ZigBeeResModel
{
public string waterLevelValue { get; set; }
public string waterTemperatureValue { get; set; }
public string soilMoistureValue { get; set; }
// 拼写与教材 dll 一致
public string soilTemperatuerValue { get; set; }
}
@@ -0,0 +1,25 @@
using System.Collections.Concurrent;
namespace DeviceCtrlLibrary;
/// <summary>
/// 虚拟实训台的设备状态。第 0 阶段先放在运行器进程里、用固定的默认值;
/// 第 1 阶段改为向 Bun 服务器上的实训台查询,数值由网页上的滑块和开关控制。
/// </summary>
internal static class TrainingBoard
{
/// <summary>ADAM-4017 各通道的读数(Vin0 温度、Vin1 湿度、Vin2 光照……),按教材格式保存为整数</summary>
public static readonly ConcurrentDictionary<int, int> AnalogInputs = new()
{
[0] = 25, // 温度 ℃
[1] = 60, // 湿度 %
[2] = 500, // 光照 lx
[3] = 101, // 气压 kPa
[4] = 60, // 空气质量
[5] = 600, // CO2 ppm
[6] = 20, // 风速
};
/// <summary>ADAM-4150 数字量输入(DI0 人体红外、DI1 火焰、DI2 烟雾、DI3 红外对射)</summary>
public static readonly ConcurrentDictionary<int, bool> DigitalInputs = new();
}
+12
View File
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- 教材配套类库的仿真版:DeviceCtrlLibrary(设备控制)和 WinFormsDeviceControlLibrary(设备控件)。
类名、方法名、参数名都与教材 dll 一致(包括原文拼写,如 IpAdderss),保证教材代码原样编译。 -->
<PropertyGroup>
<AssemblyName>Sim.Devices</AssemblyName>
<RootNamespace>DeviceCtrlLibrary</RootNamespace>
<NoWarn>$(NoWarn);CS1591;CS8618;IDE1006</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Sim.WinForms/Sim.WinForms.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,36 @@
using System.Drawing;
using System.Text.Json.Nodes;
using System.Windows.Forms;
namespace WinFormsDeviceControlLibrary;
/// <summary>
/// 教材控件库的 LED 显示屏:默认黑底、宋体 70pt 红字,setLedText 后文字从右向左滚动(滚动由浏览器完成)。
/// </summary>
[ClientType("WinFormsDeviceControlLibrary.LED")]
public class LED : UserControl
{
private string _ledText = "";
public LED()
{
BackColor = Color.Black;
ForeColor = Color.Red;
Font = new Font("宋体", 70F);
}
protected override Size DefaultSize => new(800, 130);
public void setLedText(string LedText)
{
EnsureThreadAccess();
_ledText = LedText ?? "";
NotifyChanged("LedText", _ledText);
}
protected override void WriteProps(JsonObject props)
{
base.WriteProps(props);
props["LedText"] = _ledText;
}
}