diff --git a/README.md b/README.md index 9398fe6..2a0f0fd 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到 已完成: - **虚拟实训台**:按教材实训台布局图排列(红外对射、警示灯、LED 屏、照明灯、人体红外、温湿度、光照、烟雾、火焰、ADAM 模块)。每个浏览器会话一台,状态由服务器统一保存:传感器读数由网页上的滑块和开关设定,经服务器推给学生程序;灯、继电器、LED 屏由学生程序控制。 -- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致)、ListBox、PictureBox,教材控件库的 Lamp、Alarm。 -- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1–3、项目 5 任务 1–3、项目 6 任务 2–3,共 15 个(项目 6 任务 1 在数据库页面上完成)。网址带 `?sample=示例编号` 可直接打开指定示例。 +- 新控件:CheckBox、RadioButton(事件顺序与 WinForms 一致)、ListBox、PictureBox、DataGridView、DateTimePicker、TrackBar、Windows Media Player,教材控件库的 Lamp、Alarm。 +- 示例工程(按书上代码整理):项目 1 任务 1–2、项目 2 任务 1–3、项目 3 任务 1–2、项目 4 任务 1–3、项目 5 任务 1–3、项目 6 任务 2–3、项目 7 任务 1–3,共 18 个(项目 6 任务 1 在数据库页面上完成)。网址带 `?sample=示例编号` 可直接打开指定示例。 - 代码编辑器(Monaco):C# 高亮、编译错误波浪线、点错误跳到对应行、F5 运行 / Shift+F5 停止、草稿自动保存在浏览器本地。 - 窗体设计器(仿 VS,界面中文):工具箱、拖放与改大小、属性窗口、事件窗口、双击生成事件处理方法、撤销 / 重做;读写 VS 格式的 Designer.cs。 - 多窗体:新建、删除窗体;运行时多个窗口可切换前后。 @@ -78,4 +78,4 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到 尚未完成: -- 教材项目 7–8(需要 TrackBar、资源图片、图表、虚拟时钟、无边框窗体等)。 +- 教材项目 8(需要图表、无边框窗体、Panel 停靠、背景图片等)。 diff --git a/docs/设计方案.md b/docs/设计方案.md index 575d86e..8991a3e 100644 --- a/docs/设计方案.md +++ b/docs/设计方案.md @@ -214,7 +214,7 @@ IOT/ | 4 入侵监测 | 红外对射检测;网络摄像机画面与云台;报警灯 | PictureBox、摄像头仿真、Alarm 控件、图片保存 | 已完成 | | 5 灾情监测 | 火焰(Timer)、烟雾(Thread)、灾情记录与推送 | Thread(含 Suspend / Resume / Abort)、跨线程改界面、ListBox、FormClosing | 已完成 | | 6 数据存储 | 建库建表、采集入库与增删改、按时间段查询 | SQL 窗口(代替 SSMS)、SqlClient 仿冒(SQLite)、DataGridView、DateTimePicker | 已完成 | -| 7 情景控制 | 智慧教室、寝室、会议室三种情景联动 | TrackBar、资源图片、PictureBox 当按钮、mp3 播放、虚拟时钟 | | +| 7 情景控制 | 智慧教室、寝室、会议室三种情景联动 | TrackBar、资源图片、PictureBox 当按钮、mp3 播放、虚拟时钟 | 已完成 | | 8 管理平台 | 主界面、校园环境(自动控制、7 天温度图表)、校园安防(抓拍入库、历史图片) | Panel Dock、无边框窗体、透明背景、Chart、ShowDialog、图片存数据库 | | 每个实验以数据形式存放(目前在 `samples/`,以后迁到 `content/`),由以下部分组成: @@ -237,7 +237,7 @@ flowchart TB P0["阶段 0 · 技术验证(已完成 2026-10-22)
教材项目 2 任务 1 原样运行;实测编译、延迟、内存"] P1["阶段 1 · MVP(进行中,约 3 个月)
虚拟实训台 ✔、代码编辑器 ✔、窗体设计器 ✔、多窗体 ✔
教材示例:项目 1–3 全部 ✔,项目 4–5 中平台已支持的 3 个任务 ✔
部署(沙箱、Docker Compose、GitHub Actions)✔"] P2["阶段 2 · 教学闭环(进行中,约 2 个月)
账号与班级 ✔、作品保存在服务器 ✔、自动评分 ✔、布置与提交作业 ✔
评阅(看代码、运行、老师评分、评语)✔、成绩表与导出 ✔;一个班试用"] - P3["阶段 3 · 扩展(进行中)
教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、图表、虚拟时钟"] + P3["阶段 3 · 扩展(进行中)
教材项目 4–8:摄像头 ✔、线程 ✔、SQLite 模拟数据库 ✔、虚拟时钟 ✔、图表"] P4["阶段 4 · 进阶(之后)
校园街景场景、故障注入题库、导出 VS 工程"] P0 -->|"关口:链路跑通,延迟和资源占用可接受"| P1 P1 -->|"关口:教材项目 1–3 能在平台上完整做完"| P2 @@ -291,6 +291,13 @@ flowchart TB - **评分**:每个实验可以带 `database.sql`,评分时每个检查项用一份新建的实验库,不碰学生自己的数据;评分规则加"点 / 修改表格单元格""在日期时间选择器里选日期""表格里有 / 没有某行"。新增示例项目 6 任务 2、3(任务 1 在数据库页面上完成),共 15 个。 - 实测:用到数据库的学生程序比不用的多约 11 MB 内存(PSS 17 → 28 MB)。另外修正了平台上的时间格式:Linux 的中文区域数据是 09:05:03,改成和中文 Windows 一样的 9:05:03。 +- 2026-10-23:项目 7 情景控制(3 个任务),共 18 个示例。 + - **虚拟时钟**:实训台加时钟面板,默认真实时间,可以拨到任意日期时刻、选 1×/10×/60× 倍速、一键恢复真实时间;时钟状态由服务器保存(`BoardState.clock`),运行器按它算当前时间。编译时把学生代码里的 `DateTime.Now / Today / UtcNow` 改写成 `SimClock`(和线程 API 的改写一样不换行、编辑器里看到的是原代码)。任务 2 按 6:45 起床、7:30 上课、21:00 回寝、22:00 熄灯切换,任务 3 按预约时刻开启,都靠它来测试。评分规则加 `clock` 步骤(拨到今天的某个时刻,可设倍速)。 + - **项目资源**(`Properties.Resources`):只用平台素材库(自绘的开关图片、ffmpeg 合成的门铃声,名字和教材一致,`runtime/Sim.WinForms/Resources/gallery`)。实验页"项目资源"对话框勾选素材(图片看缩略图、声音可试听),平台生成 `Properties/Resources.Designer.cs`(图片是 Bitmap、声音是 byte[],用法和 VS 的强类型资源类一样,不在标签页里显示);设计器里图片框的"图片"属性可以从项目资源里选,写进 Designer.cs 的是 `global::命名空间.Properties.Resources.名字`,预览直接显示素材。学生程序的程序目录下有 Resources 文件夹指向素材库(书上用 `BaseDirectory + "//Resources//ring.mp3"`)。没添加资源就用时给中文提示。 + - **Windows Media Player**(`AxWMPLib.AxWindowsMediaPlayer`):设置 URL 后和真的一样自动播放,`Ctlcontrols.play() / stop()`;声音在浏览器里播放,实训台上加"广播"喇叭,播放时亮起。VS 为它生成的 `ComponentResourceManager(...).GetObject("…OcxState")`:编译时给每个窗体嵌一份空资源,取到 null 不报错。图片、声音数据统一按内容哈希缓存(`blob` 消息),每份只传一次。 + - **TrackBar**:拖动、点轨道(LargeChange)、方向键(SmallChange),先 ValueChanged 再 Scroll,竖向时最小值在下面;设计器工具箱加"滑动条""媒体播放器"。评分规则加 `value`(滑动条的值)、`media`(播放了某个文件)检查。 + - 示例:书上任务 1 手动开关灯的判断写反(参考工程是对的),任务 3 漏了填充串口列表,示例里都已改正;任务 3 的"每 5 分钟整点拍照"依赖定时器正好在 0 秒触发,自动评分不检查这一项,由老师评阅。编译检查修正:`System.AppDomain.CurrentDomain.BaseDirectory`(带命名空间)也放行。 + **其他改进**: - 2026-10-22:代码编辑器的语言功能:代码补全(输入 `btn_Open.` 列出按钮的属性、方法、事件,只列出平台仿真了的成员)、悬停提示、参数提示(`MessageBox.Show(` 列出重载,标出正在填的参数)、停止输入 1 秒后自动检查编译错误(提示和点"运行"时一样)。语言服务是一个常驻进程,每个浏览器标签页一份工程,只更新有变化的文件。实测(限制 2 核):50 个标签页同时检查 1.2 秒内全部完成,同时补全 0.7 秒;内存约 260 MB。 @@ -308,7 +315,7 @@ flowchart TB | T-SQL 转 SQLite | 教材用 SQL Server 写法(`TOP`、`CONVERT`、DataAdapter 回写、日期字符串) | 已实现:ScriptDom 解析后按白名单转写;连接串只按库名打开学生自己的库;日期统一存为标准格式;STRICT 表和 CHECK 约束让类型、长度错误和 SQL Server 一样报出 | | 旧版线程 API | `Thread.Suspend` / `Resume` / `Abort` 在新版 .NET 上会报错 | 已实现:编译时改写成平台的协作式实现,被挂起或终止的线程在 Sleep、改控件、调设备类库时停下 | | 图片处理 | System.Drawing 在 Linux 上不受支持 | 已实现:自己的 `Image` / `Bitmap` 只保存图片文件数据和尺寸,能显示、保存、读回,不能逐像素绘制 | -| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 提供可调、可加速的虚拟时钟 | +| 时间相关逻辑 | 项目 3 起就有按时刻开关灯的练习,部分定时器间隔长达 2 小时 | 已实现:实训台上的虚拟时钟可拨时间、1×/10×/60× 倍速,编译时把 DateTime.Now / Today / UtcNow 改写成虚拟时钟 | | 事件延迟 | 浏览器和服务器之间来回传递事件,点击是否有卡顿感 | 已实测:单次约 15 ms,100 个程序同时操作中位数约 60 ms | | 并发资源 | 100 个程序同时运行时的内存和 CPU | 已压测:100 个程序合计约 1.1 GB;2 核下 100 人同时启动约 8 秒;CPU 时间上限防死循环,运行超时回收 | | 公网安全 | 恶意或无意的危险代码 | bubblewrap 沙箱 + 编译检查 + 账号登录(已完成) | diff --git a/runtime/Sim.Devices/DeviceCtrlLibrary/TrainingBoard.cs b/runtime/Sim.Devices/DeviceCtrlLibrary/TrainingBoard.cs index 66acef8..aedc392 100644 --- a/runtime/Sim.Devices/DeviceCtrlLibrary/TrainingBoard.cs +++ b/runtime/Sim.Devices/DeviceCtrlLibrary/TrainingBoard.cs @@ -22,9 +22,10 @@ public static class TrainingBoard if (!string.IsNullOrEmpty(initial) && JsonNode.Parse(initial) is JsonObject json) Update(json); } - /// 平台内部使用:应用服务器推送的 {"analog":[…],"digital":[…]} + /// 平台内部使用:应用服务器推送的 {"analog":[…],"digital":[…],"clock":…}(clock 不带时表示不变) public static void Update(JsonObject json) { + if (json.ContainsKey("clock")) Sim.Host.SimClock.Update(json["clock"]); lock (Lock) { if (json["analog"] is JsonArray analog) diff --git a/runtime/Sim.Runner/ApiPolicy.cs b/runtime/Sim.Runner/ApiPolicy.cs index 9381b0a..5d83d7c 100644 --- a/runtime/Sim.Runner/ApiPolicy.cs +++ b/runtime/Sim.Runner/ApiPolicy.cs @@ -64,7 +64,9 @@ internal static class ApiPolicy if (name.Parent is QualifiedNameSyntax q && q.Left == name) continue; // var 推断出的类型在初始化表达式里已经检查过 if (name is IdentifierNameSyntax { IsVar: true }) continue; - if (name.Parent is MemberAccessExpressionSyntax m && m.Expression == name && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue; + // 类型名只是用来限定成员(AppDomain.CurrentDomain、System.AppDomain.CurrentDomain):检查后面的成员就够了 + ExpressionSyntax qualifier = name.Parent is MemberAccessExpressionSyntax inner && inner.Name == name ? inner : name; + if (qualifier.Parent is MemberAccessExpressionSyntax m && m.Expression == qualifier && model.GetSymbolInfo(name).Symbol is INamespaceOrTypeSymbol) continue; var symbol = model.GetSymbolInfo(name).Symbol; if (symbol == null) continue; diff --git a/runtime/Sim.Runner/ClockRewriter.cs b/runtime/Sim.Runner/ClockRewriter.cs new file mode 100644 index 0000000..4164374 --- /dev/null +++ b/runtime/Sim.Runner/ClockRewriter.cs @@ -0,0 +1,69 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Sim.Runner; + +/// +/// 编译前把学生代码里的 DateTime.Now / Today / UtcNow 改写成虚拟时钟 Sim.Host.SimClock(实训台上可以拨时间、调倍速, +/// 教材项目 7 按 6:45 起床、22:00 熄灯等时刻切换情景模式)。只改写语义上确实是 System.DateTime 这三个属性的地方; +/// 改写不换行,报错和异常里的行号不变。学生在编辑器里看到的仍是原来的代码。 +/// +internal static class ClockRewriter +{ + private const string Target = "global::Sim.Host.SimClock"; + + public static CSharpCompilation Rewrite(CSharpCompilation compilation) + { + var result = compilation; + foreach (var tree in compilation.SyntaxTrees) + { + var text = tree.GetText().ToString(); + if (!text.Contains("Now") && !text.Contains("Today")) continue; + var rewriter = new Rewriter(compilation.GetSemanticModel(tree)); + var root = rewriter.Visit(tree.GetRoot()); + if (!rewriter.Changed) continue; + result = result.ReplaceSyntaxTree(tree, + CSharpSyntaxTree.ParseText(SourceText.From(root.ToFullString(), Encoding.UTF8), (CSharpParseOptions)tree.Options, tree.FilePath)); + } + return result; + } + + private static string? Replacement(ISymbol? symbol) => + symbol is IPropertySymbol { IsStatic: true, Name: "Now" or "Today" or "UtcNow" } property + && property.ContainingType.SpecialType == SpecialType.System_DateTime + ? $"{Target}.{property.Name}" + : null; + + private sealed class Rewriter(SemanticModel model) : CSharpSyntaxRewriter + { + public bool Changed { get; private set; } + + public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + // DateTime.Now、System.DateTime.Now + if (Replacement(model.GetSymbolInfo(node).Symbol) is { } replacement) + { + Changed = true; + return SyntaxFactory.ParseExpression(replacement).WithTriviaFrom(node); + } + return base.VisitMemberAccessExpression(node); + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + // using static System.DateTime; 之后直接写 Now + if (node.Parent is not MemberAccessExpressionSyntax access || access.Expression == node) + { + if (Replacement(model.GetSymbolInfo(node).Symbol) is { } replacement) + { + Changed = true; + return SyntaxFactory.ParseExpression(replacement).WithTriviaFrom(node); + } + } + return base.VisitIdentifierName(node); + } + } +} diff --git a/runtime/Sim.Runner/CompileHints.cs b/runtime/Sim.Runner/CompileHints.cs index 95b145f..9a77d17 100644 --- a/runtime/Sim.Runner/CompileHints.cs +++ b/runtime/Sim.Runner/CompileHints.cs @@ -17,6 +17,8 @@ internal static class CompileHints ("CS0103", "Catch") or ("CS0246", "Catch") => "catch 要小写,括号里写异常类型,例如 catch (Exception ex)。", ("CS0246", var name) when name.StartsWith("bool") || name.StartsWith("private") || name.StartsWith("public") || name.StartsWith("string") || name.StartsWith("int") => $"“{name}”像是类型和名字之间少了一个空格。", + ("CS0234", var s) when s.Contains("Properties") => "要用 Properties.Resources,先点代码上方的“项目资源”,添加需要的图片或声音。", + ("CS0117", var s) when s.Contains("Resources.") => $"项目资源里还没有“{s[(s.LastIndexOf('.') + 1)..]}”:点代码上方的“项目资源”添加(名字要和代码里一致)。", ("CS0103", _) => $"找不到“{source}”。检查拼写和大小写(C# 区分大小写),以及控件的 Name 是否和代码里一致。", ("CS1002", _) => "C# 每条语句末尾要有英文分号 ;", ("CS1056", _) or ("CS1525", _) when source.Length == 1 && source[0] > 127 => "这里出现了中文标点,C# 代码里的符号要用英文半角。", diff --git a/runtime/Sim.Runner/StudentCompiler.cs b/runtime/Sim.Runner/StudentCompiler.cs index eec0450..f9288e9 100644 --- a/runtime/Sim.Runner/StudentCompiler.cs +++ b/runtime/Sim.Runner/StudentCompiler.cs @@ -56,9 +56,10 @@ internal static class StudentCompiler using var peStream = new MemoryStream(); using var pdbStream = new MemoryStream(); - // 编译改写后的代码(旧版线程 API 换成平台实现),API 检查仍针对学生写的原始代码 - var emit = LegacyThreadRewriter.Rewrite(compilation).Emit(peStream, pdbStream, - options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb)); + // 编译改写后的代码(旧版线程 API 换成平台实现,DateTime.Now 换成虚拟时钟),API 检查仍针对学生写的原始代码 + var emit = ClockRewriter.Rewrite(LegacyThreadRewriter.Rewrite(compilation)).Emit(peStream, pdbStream, + options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb), + manifestResources: EmptyControlResources(compilation)); var denied = ApiPolicy.Check(compilation).ToList(); var diagnostics = new JsonArray(); @@ -71,6 +72,39 @@ internal static class StudentCompiler return new Result(emit.Success && denied.Count == 0, peStream.ToArray(), pdbStream.ToArray(), diagnostics); } + private static readonly Lazy EmptyResources = new(() => + { + var stream = new MemoryStream(); + using (var writer = new System.Resources.ResourceWriter(stream)) writer.Generate(); + return stream.ToArray(); + }); + + /// + /// 给学生工程里的每个窗体、控件类嵌一份空的 .resources:VS 生成的 Designer.cs 会用 + /// ComponentResourceManager(typeof(窗体)).GetObject("…OcxState") 读窗体的 .resx(如 Windows Media Player 控件), + /// 平台没有 .resx,有了空资源时取到 null,而不是报"找不到资源"。 + /// + private static List EmptyControlResources(CSharpCompilation compilation) + { + var control = compilation.GetTypeByMetadataName("System.Windows.Forms.Control"); + var resources = new List(); + if (control == null) return resources; + var names = new HashSet(); + foreach (var tree in compilation.SyntaxTrees) + { + var model = compilation.GetSemanticModel(tree); + foreach (var declaration in tree.GetRoot().DescendantNodes().OfType()) + { + if (model.GetDeclaredSymbol(declaration) is not { } type) continue; + var derives = false; + for (var t = type.BaseType; t != null && !derives; t = t.BaseType) derives = SymbolEqualityComparer.Default.Equals(t, control); + var name = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "") + ".resources"; + if (derives && names.Add(name)) resources.Add(new ResourceDescription(name, () => new MemoryStream(EmptyResources.Value), isPublic: true)); + } + } + return resources; + } + internal static JsonObject ToJson(Diagnostic diagnostic) { var span = diagnostic.Location.GetMappedLineSpan(); diff --git a/runtime/Sim.Runner/StudentProgramHost.cs b/runtime/Sim.Runner/StudentProgramHost.cs index 17b85d1..e9dbc49 100644 --- a/runtime/Sim.Runner/StudentProgramHost.cs +++ b/runtime/Sim.Runner/StudentProgramHost.cs @@ -104,6 +104,9 @@ internal static class StudentProgramHost baseDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "StudentApp", "bin", "Debug")).FullName; } baseDir = Path.TrimEndingDirectorySeparator(baseDir) + Path.DirectorySeparatorChar; + // 程序目录下的 Resources 文件夹指向素材库:教材用 BaseDirectory + "//Resources//ring.mp3" 播放铃声 + var resources = Path.Combine(baseDir, "Resources"); + if (!Path.Exists(resources)) Directory.CreateSymbolicLink(resources, ProjectResources.Directory); AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", baseDir); Directory.SetCurrentDirectory(baseDir); SimHost.AppBaseDirectory = baseDir; diff --git a/runtime/Sim.WinForms/Forms/AxWindowsMediaPlayer.cs b/runtime/Sim.WinForms/Forms/AxWindowsMediaPlayer.cs new file mode 100644 index 0000000..c424323 --- /dev/null +++ b/runtime/Sim.WinForms/Forms/AxWindowsMediaPlayer.cs @@ -0,0 +1,166 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Security.Cryptography; +using System.Text.Json.Nodes; +using System.Windows.Forms; +using Sim.Host; + +namespace System.Windows.Forms +{ + /// + /// ActiveX 控件的基类。VS 把 COM 控件拖到窗体上时生成 OcxState = resources.GetObject("…OcxState"), + /// 平台编译时给每个窗体嵌了空的资源,这里取到的是 null,不影响使用。 + /// + public abstract class AxHost : Control, ISupportInitialize + { + protected AxHost(string clsid) { } + + public State? OcxState { get; set; } + + public void BeginInit() { } + public void EndInit() { } + + public class State { } + } +} + +namespace WMPLib +{ + public enum WMPPlayState + { + wmppsUndefined = 0, wmppsStopped = 1, wmppsPaused = 2, wmppsPlaying = 3, wmppsScanForward = 4, wmppsScanReverse = 5, + wmppsBuffering = 6, wmppsWaiting = 7, wmppsMediaEnded = 8, wmppsTransitioning = 9, wmppsReady = 10, wmppsReconnecting = 11, + } + + /// 播放控制:axWindowsMediaPlayer1.Ctlcontrols.play() + public interface IWMPControls + { + void play(); + void pause(); + void stop(); + double currentPosition { get; set; } + } + + /// 播放设置:axWindowsMediaPlayer1.settings.volume + public interface IWMPSettings + { + bool autoStart { get; set; } + int volume { get; set; } + bool mute { get; set; } + void setMode(string bstrMode, bool varfMode); + } +} + +namespace AxWMPLib +{ + using WMPLib; + + /// + /// Windows Media Player 控件(教材项目 7 的"广播"播放 ring.mp3)。和真的一样:设置 URL 后自动开始播放(settings.autoStart), + /// 也可以调用 Ctlcontrols.play()。声音在浏览器里播放;实训台上的广播喇叭随之亮起。只支持本机文件。 + /// + [ClientType("AxWindowsMediaPlayer")] + public class AxWindowsMediaPlayer : AxHost + { + private string _url = ""; + private WMPPlayState _playState = WMPPlayState.wmppsUndefined; + + public AxWindowsMediaPlayer() : base("6bf52a52-394a-11d3-b153-00c04f79faa6") + { + Ctlcontrols = new Controls(this); + settings = new Settings(); + } + + protected override Size DefaultSize => new(245, 240); + + public string URL + { + get => _url; + set + { + _url = value ?? ""; + if (settings.autoStart && _url.Length > 0) Play(); + } + } + + public IWMPControls Ctlcontrols { get; } + + // 属性名和 COM 接口一样是小写 + public IWMPSettings settings { get; } + + public WMPPlayState playState => _playState; + + public string uiMode { get; set; } = "full"; + public bool stretchToFit { get; set; } + public bool enableContextMenu { get; set; } = true; + public string status => _playState == WMPPlayState.wmppsPlaying ? "正在播放" : "已停止"; + + public event EventHandler? PlayStateChange; + + private void Play() + { + LegacyThreads.CheckPoint(); + // 教材写 BaseDirectory + "//Resources//ring.mp3":路径里多余的斜杠在 Linux 上也能用 + var path = _url.StartsWith("file:///", StringComparison.OrdinalIgnoreCase) ? new Uri(_url).LocalPath : _url; + if (!File.Exists(path)) + { + SimHost.SendConsole($"Windows Media Player:找不到要播放的文件 {_url}\n"); + SetState(WMPPlayState.wmppsStopped); + return; + } + var data = File.ReadAllBytes(path); + var mime = Path.GetExtension(path).ToLowerInvariant() switch + { + ".wav" => "audio/wav", + ".ogg" => "audio/ogg", + ".m4a" or ".aac" => "audio/aac", + _ => "audio/mpeg", + }; + var key = SimHost.DataKey(Convert.ToHexString(SHA1.HashData(data)).ToLowerInvariant(), data, mime); + SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "play", ["key"] = key, ["name"] = Path.GetFileName(path) }); + SetState(WMPPlayState.wmppsPlaying); + } + + private void Stop() + { + if (_playState != WMPPlayState.wmppsPlaying) return; + SimHost.Send(new JsonObject { ["type"] = "media", ["id"] = SimId, ["action"] = "stop" }); + SetState(WMPPlayState.wmppsStopped); + } + + private void SetState(WMPPlayState state) + { + if (_playState == state) return; + _playState = state; + PlayStateChange?.Invoke(this, EventArgs.Empty); + } + + protected override void WriteProps(JsonObject props) + { + base.WriteProps(props); + props["URL"] = _url; + } + + private sealed class Controls(AxWindowsMediaPlayer owner) : IWMPControls + { + public void play() + { + if (owner._playState != WMPPlayState.wmppsPlaying && owner._url.Length > 0) owner.Play(); + } + + public void pause() => owner.Stop(); + public void stop() => owner.Stop(); + public double currentPosition { get; set; } + } + + private sealed class Settings : IWMPSettings + { + public bool autoStart { get; set; } = true; + public int volume { get; set; } = 50; + public bool mute { get; set; } + public void setMode(string bstrMode, bool varfMode) { } + } + } +} diff --git a/runtime/Sim.WinForms/Forms/DateTimePicker.cs b/runtime/Sim.WinForms/Forms/DateTimePicker.cs index 4b3e8b3..3fb0f49 100644 --- a/runtime/Sim.WinForms/Forms/DateTimePicker.cs +++ b/runtime/Sim.WinForms/Forms/DateTimePicker.cs @@ -13,7 +13,7 @@ public class DateTimePicker : Control public static readonly DateTime MinimumDateTime = new(1753, 1, 1); public static readonly DateTime MaximumDateTime = new(9998, 12, 31); - private DateTime _value = DateTime.Now; + private DateTime _value = SimClock.Now; private DateTimePickerFormat _format = DateTimePickerFormat.Long; private string? _customFormat; private bool _showUpDown; diff --git a/runtime/Sim.WinForms/Forms/TrackBar.cs b/runtime/Sim.WinForms/Forms/TrackBar.cs new file mode 100644 index 0000000..644402a --- /dev/null +++ b/runtime/Sim.WinForms/Forms/TrackBar.cs @@ -0,0 +1,123 @@ +using System.ComponentModel; +using System.Drawing; +using System.Text.Json.Nodes; +using Sim.Host; + +namespace System.Windows.Forms; + +public enum TickStyle { None, TopLeft, BottomRight, Both } + +public enum Orientation { Horizontal, Vertical } + +/// 滑动条(教材项目 7 模拟空调温度、窗帘开合) +public class TrackBar : Control, ISupportInitialize +{ + private int _minimum; + private int _maximum = 10; + private int _value; + private int _tickFrequency = 1; + private TickStyle _tickStyle = TickStyle.BottomRight; + private Orientation _orientation = Orientation.Horizontal; + private bool _initializing; + + public TrackBar() => AutoSize = true; + + protected override Size DefaultSize => new(104, 45); + + public int Minimum + { + get => _minimum; + set => SetRange(value, Math.Max(value, _maximum)); + } + + public int Maximum + { + get => _maximum; + set => SetRange(Math.Min(value, _minimum), value); + } + + public int Value + { + get => _value; + set + { + if (!_initializing && (value < _minimum || value > _maximum)) + throw new ArgumentOutOfRangeException(nameof(value), $"“{value}”的值对于“Value”无效。“Value”应介于 'Minimum' 和 'Maximum' 之间。"); + if (SetProp(ref _value, value, nameof(Value))) OnValueChanged(EventArgs.Empty); + } + } + + public int TickFrequency + { + get => _tickFrequency; + set => SetProp(ref _tickFrequency, value, nameof(TickFrequency)); + } + + public TickStyle TickStyle + { + get => _tickStyle; + set => SetProp(ref _tickStyle, value, nameof(TickStyle)); + } + + public Orientation Orientation + { + get => _orientation; + set + { + if (value == _orientation) return; + SetProp(ref _orientation, value, nameof(Orientation)); + Size = new Size(Height, Width); + } + } + + public int LargeChange { get; set; } = 5; + public int SmallChange { get; set; } = 1; + + public event EventHandler? Scroll; + public event EventHandler? ValueChanged; + + protected virtual void OnScroll(EventArgs e) => Scroll?.Invoke(this, e); + protected virtual void OnValueChanged(EventArgs e) => ValueChanged?.Invoke(this, e); + + public void SetRange(int minValue, int maxValue) + { + SetProp(ref _minimum, minValue, nameof(Minimum)); + SetProp(ref _maximum, Math.Max(minValue, maxValue), nameof(Maximum)); + if (_value < _minimum) Value = _minimum; + else if (_value > _maximum) Value = _maximum; + } + + // Designer.cs 里 VS 生成 ((ISupportInitialize)trackBar1).BeginInit() / EndInit():期间先设 Value 再设范围也不报错 + public void BeginInit() => _initializing = true; + + public void EndInit() + { + _initializing = false; + if (_value < _minimum || _value > _maximum) Value = Math.Clamp(_value, _minimum, _maximum); + } + + /// 用户拖动滑块、点轨道或按方向键:和 WinForms 一样先改 Value(ValueChanged),再触发 Scroll + protected override void OnUserAction(UserAction action) + { + if (action.Action != "scroll" || !int.TryParse(action.Value, out var value)) + { + base.OnUserAction(action); + return; + } + Value = Math.Clamp(value, _minimum, _maximum); + OnScroll(EventArgs.Empty); + } + + protected override void WriteProps(JsonObject props) + { + base.WriteProps(props); + props["Minimum"] = _minimum; + props["Maximum"] = _maximum; + props["Value"] = _value; + props["TickFrequency"] = _tickFrequency; + props["TickStyle"] = _tickStyle.ToString(); + props["Orientation"] = _orientation.ToString(); + props["LargeChange"] = LargeChange; + props["SmallChange"] = SmallChange; + } +} diff --git a/runtime/Sim.WinForms/Host/ProjectResources.cs b/runtime/Sim.WinForms/Host/ProjectResources.cs new file mode 100644 index 0000000..46e70d7 --- /dev/null +++ b/runtime/Sim.WinForms/Host/ProjectResources.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using System.Drawing; +using System.IO; + +namespace Sim.Host; + +/// +/// 项目资源(Properties.Resources)的素材库。平台为学生工程生成的 Properties/Resources.Designer.cs 通过这里读取素材, +/// 和 VS 一样每次取图片都得到一个新的 Bitmap。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ProjectResources +{ + /// 素材文件所在目录(运行器目录下的 Resources/gallery) + public static string Directory { get; } = Path.Combine(Path.GetDirectoryName(typeof(ProjectResources).Assembly.Location)!, "Resources", "gallery"); + + public static Bitmap Image(string file) => (Bitmap)System.Drawing.Image.FromStream(new MemoryStream(Bytes(file))); + + public static byte[] Bytes(string file) => File.ReadAllBytes(Path.Combine(Directory, Path.GetFileName(file))); +} diff --git a/runtime/Sim.WinForms/Host/SimClock.cs b/runtime/Sim.WinForms/Host/SimClock.cs new file mode 100644 index 0000000..ad5539d --- /dev/null +++ b/runtime/Sim.WinForms/Host/SimClock.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Text.Json.Nodes; + +namespace Sim.Host; + +/// +/// 虚拟时钟(实训台上可以拨时间、调倍速;教材项目 7 按时刻切换情景模式)。 +/// 编译时把学生代码里的 DateTime.Now / Today / UtcNow 改写成这里的属性(见 Sim.Runner/ClockRewriter.cs)。 +/// 没有拨过时就是真实时间。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class SimClock +{ + private sealed record Setting(DateTime Wall, long RealAt, double Speed); + + private static volatile Setting? _setting; + + public static DateTime Now + { + get + { + var setting = _setting; + if (setting == null) return DateTime.Now; + var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - setting.RealAt; + return setting.Wall.AddMilliseconds(elapsed * setting.Speed); + } + } + + public static DateTime Today => Now.Date; + + public static DateTime UtcNow => Now.ToUniversalTime(); + + /// 平台内部使用:应用服务器推送的 {"wall":"2024-01-16T06:40:00","realAt":…,"speed":60},null 恢复真实时间 + public static void Update(JsonNode? clock) + { + if (clock is not JsonObject json + || !DateTime.TryParseExact((string?)json["wall"], "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var wall)) + { + _setting = null; + return; + } + _setting = new Setting(DateTime.SpecifyKind(wall, DateTimeKind.Local), (long?)json["realAt"] ?? 0, (double?)json["speed"] ?? 1); + } +} diff --git a/runtime/Sim.WinForms/Host/SimHost.cs b/runtime/Sim.WinForms/Host/SimHost.cs index 9b7a1bf..cac9264 100644 --- a/runtime/Sim.WinForms/Host/SimHost.cs +++ b/runtime/Sim.WinForms/Host/SimHost.cs @@ -86,19 +86,23 @@ public static class SimHost public static string AppBaseDirectory { get; set; } = AppContext.BaseDirectory; /// 图片在浏览器端的编号;第一次用到时先把图片数据发过去,浏览器缓存后同一张图不再重发 - public static string ImageKey(System.Drawing.Image image) + public static string ImageKey(System.Drawing.Image image) => DataKey(image.SimKey, image.Data, image.MimeType); + + /// + /// 图片、声音等数据在浏览器端的编号(key 通常是内容的哈希):第一次用到时先把数据发过去,之后只发编号。 + /// + public static string DataKey(string key, byte[] data, string mime) { - var key = image.SimKey; bool isNew; lock (SentImages) isNew = SentImages.Add(key); if (isNew) { Send(new JsonObject { - ["type"] = "image", + ["type"] = "blob", ["key"] = key, - ["mime"] = image.MimeType, - ["data"] = Convert.ToBase64String(image.Data), + ["mime"] = mime, + ["data"] = Convert.ToBase64String(data), }); } return key; diff --git a/runtime/Sim.WinForms/Resources/README.md b/runtime/Sim.WinForms/Resources/README.md new file mode 100644 index 0000000..668d4eb --- /dev/null +++ b/runtime/Sim.WinForms/Resources/README.md @@ -0,0 +1,7 @@ +# 项目资源素材库 + +学生在"项目资源"里添加的图片和声音(`Properties.Resources.xxx`),名字和教材一致,全部由平台自绘或合成(不使用教材配套资料里的素材)。 + +- `gallery/`:发布给运行器和网页的文件,`index.json` 是清单(名字、文件、中文说明、类型)。 +- `*.svg`:图片的源文件,改了以后用 `rsvg-convert xxx.svg -o gallery/xxx.png` 重新生成。 +- `gallery/ring.mp3`:用 ffmpeg 合成的门铃声(叮—咚),命令见提交记录。 diff --git a/runtime/Sim.WinForms/Resources/gallery/index.json b/runtime/Sim.WinForms/Resources/gallery/index.json new file mode 100644 index 0000000..bcac970 --- /dev/null +++ b/runtime/Sim.WinForms/Resources/gallery/index.json @@ -0,0 +1,5 @@ +[ + { "name": "switch_on", "file": "switch_on.png", "label": "开关:开", "kind": "image" }, + { "name": "switch_off", "file": "switch_off.png", "label": "开关:关", "kind": "image" }, + { "name": "ring", "file": "ring.mp3", "label": "铃声", "kind": "audio" } +] diff --git a/runtime/Sim.WinForms/Resources/gallery/ring.mp3 b/runtime/Sim.WinForms/Resources/gallery/ring.mp3 new file mode 100644 index 0000000..65b921b Binary files /dev/null and b/runtime/Sim.WinForms/Resources/gallery/ring.mp3 differ diff --git a/runtime/Sim.WinForms/Resources/gallery/switch_off.png b/runtime/Sim.WinForms/Resources/gallery/switch_off.png new file mode 100644 index 0000000..0811532 Binary files /dev/null and b/runtime/Sim.WinForms/Resources/gallery/switch_off.png differ diff --git a/runtime/Sim.WinForms/Resources/gallery/switch_on.png b/runtime/Sim.WinForms/Resources/gallery/switch_on.png new file mode 100644 index 0000000..8e8355e Binary files /dev/null and b/runtime/Sim.WinForms/Resources/gallery/switch_on.png differ diff --git a/runtime/Sim.WinForms/Resources/switch_off.svg b/runtime/Sim.WinForms/Resources/switch_off.svg new file mode 100644 index 0000000..48e2c19 --- /dev/null +++ b/runtime/Sim.WinForms/Resources/switch_off.svg @@ -0,0 +1,5 @@ + + + OFF + + diff --git a/runtime/Sim.WinForms/Resources/switch_on.svg b/runtime/Sim.WinForms/Resources/switch_on.svg new file mode 100644 index 0000000..9edd3c3 --- /dev/null +++ b/runtime/Sim.WinForms/Resources/switch_on.svg @@ -0,0 +1,5 @@ + + + ON + + diff --git a/runtime/Sim.WinForms/Sim.WinForms.csproj b/runtime/Sim.WinForms/Sim.WinForms.csproj index 10e2b3c..f7fca0f 100644 --- a/runtime/Sim.WinForms/Sim.WinForms.csproj +++ b/runtime/Sim.WinForms/Sim.WinForms.csproj @@ -7,4 +7,8 @@ $(NoWarn);CS1591;CS8618 + + + + diff --git a/samples/p7-t1-智慧教室/Program.cs b/samples/p7-t1-智慧教室/Program.cs new file mode 100644 index 0000000..d8e568c --- /dev/null +++ b/samples/p7-t1-智慧教室/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace 教室情景模式 +{ + static class Program + { + /// + /// 应用程序的主入口点。 + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new frm_control()); + } + } +} diff --git a/samples/p7-t1-智慧教室/Properties/Resources.Designer.cs b/samples/p7-t1-智慧教室/Properties/Resources.Designer.cs new file mode 100644 index 0000000..ca07b06 --- /dev/null +++ b/samples/p7-t1-智慧教室/Properties/Resources.Designer.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。 +// +//------------------------------------------------------------------------------ + +namespace 教室情景模式.Properties { + + /// + /// 项目资源:Properties.Resources.资源名 + /// + internal class Resources { + + /// 开关:开 + internal static System.Drawing.Bitmap switch_on { + get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); } + } + + /// 开关:关 + internal static System.Drawing.Bitmap switch_off { + get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); } + } + } +} diff --git a/samples/p7-t1-智慧教室/checks.yaml b/samples/p7-t1-智慧教室/checks.yaml new file mode 100644 index 0000000..2f9279c --- /dev/null +++ b/samples/p7-t1-智慧教室/checks.yaml @@ -0,0 +1,25 @@ +# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。 +# 格式说明见 server/src/grading/checks.ts +- title: 能实时显示环境数据 + steps: + - select: { control: com_import, item: COM2 } + - click: btn_apply + - expect: { control: btn_apply, text: 关闭, desc: 按钮文字变成"关闭" } + - board: { light: 600, temperature: 22, humidity: 48 } + - expect: { control: txt_light, text: '600', within: 5s } + - expect: { control: txt_temperature, text: '22', within: 5s } + - expect: { control: txt_humidity, text: '48', within: 5s } + +- title: 能根据环境数据的变化实时控制智能设备 + steps: + - select: { control: com_import, item: COM2 } + - click: btn_apply + - board: { light: 300, temperature: 22 } + - expect: { relay: 1, on: true, within: 5s, desc: 光照低于 400 时打开照明灯 } + - expect: { control: lamp, on: true, desc: 窗体上的灯亮 } + - board: { light: 900 } + - expect: { relay: 1, on: false, within: 5s, desc: 光照高于 800 时关闭照明灯 } + - board: { temperature: 33 } + - expect: { control: trb_airconditione, value: 26, within: 5s, desc: 温度高于 30℃ 时空调设为 26℃ } + - board: { temperature: 3 } + - expect: { control: trb_airconditione, value: 25, within: 5s, desc: 温度低于 5℃ 时空调设为 25℃ } diff --git a/samples/p7-t1-智慧教室/meta.json b/samples/p7-t1-智慧教室/meta.json new file mode 100644 index 0000000..3dfd084 --- /dev/null +++ b/samples/p7-t1-智慧教室/meta.json @@ -0,0 +1,4 @@ +{ + "title": "项目7 任务1 实现智慧教室", + "source": "教材书 p133–137,界面和代码取自配套参考工程(手动开关灯的判断书上写反了,参考工程是对的);开关图片来自平台素材库" +} diff --git a/samples/p7-t1-智慧教室/教室情景模式.Designer.cs b/samples/p7-t1-智慧教室/教室情景模式.Designer.cs new file mode 100644 index 0000000..37ec4f1 --- /dev/null +++ b/samples/p7-t1-智慧教室/教室情景模式.Designer.cs @@ -0,0 +1,350 @@ +namespace 教室情景模式 +{ + partial class frm_control + { + /// + /// 必需的设计器变量。 + /// + private System.ComponentModel.IContainer components = null; + + /// + /// 清理所有正在使用的资源。 + /// + /// 如果应释放托管资源,为 true;否则为 false。 + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows 窗体设计器生成的代码 + + /// + /// 设计器支持所需的方法 - 不要 + /// 使用代码编辑器修改此方法的内容。 + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.lbl_mode = new System.Windows.Forms.Label(); + this.btn_apply = new System.Windows.Forms.Button(); + this.grp_mode = new System.Windows.Forms.GroupBox(); + this.grp_input = new System.Windows.Forms.GroupBox(); + this.txt_temperature = new System.Windows.Forms.TextBox(); + this.txt_humidity = new System.Windows.Forms.TextBox(); + this.com_import = new System.Windows.Forms.ComboBox(); + this.lbl_temperature = new System.Windows.Forms.Label(); + this.txt_light = new System.Windows.Forms.TextBox(); + this.lbl_import = new System.Windows.Forms.Label(); + this.lbl_humidity = new System.Windows.Forms.Label(); + this.lbl_light = new System.Windows.Forms.Label(); + this.grp_status = new System.Windows.Forms.GroupBox(); + this.pic_airconditioneStatus = new System.Windows.Forms.PictureBox(); + this.lbl_airconditionmin = new System.Windows.Forms.Label(); + this.lbl_airconditionmax = new System.Windows.Forms.Label(); + this.trb_airconditione = new System.Windows.Forms.TrackBar(); + this.lamp = new WinFormsDeviceControlLibrary.Lamp(); + this.lbl_airconditione = new System.Windows.Forms.Label(); + this.pic_lightsStatus = new System.Windows.Forms.PictureBox(); + this.lbl_lights = new System.Windows.Forms.Label(); + this.timer = new System.Windows.Forms.Timer(this.components); + this.grp_mode.SuspendLayout(); + this.grp_input.SuspendLayout(); + this.grp_status.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pic_airconditioneStatus)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.trb_airconditione)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).BeginInit(); + this.SuspendLayout(); + // + // lbl_mode + // + this.lbl_mode.AutoSize = true; + this.lbl_mode.Location = new System.Drawing.Point(109, 30); + this.lbl_mode.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_mode.Name = "lbl_mode"; + this.lbl_mode.Size = new System.Drawing.Size(89, 12); + this.lbl_mode.TabIndex = 1; + this.lbl_mode.Text = "智慧教室将开启"; + // + // btn_apply + // + this.btn_apply.Location = new System.Drawing.Point(285, 22); + this.btn_apply.Margin = new System.Windows.Forms.Padding(2); + this.btn_apply.Name = "btn_apply"; + this.btn_apply.Size = new System.Drawing.Size(55, 28); + this.btn_apply.TabIndex = 2; + this.btn_apply.Text = "启用"; + this.btn_apply.UseVisualStyleBackColor = true; + this.btn_apply.Click += new System.EventHandler(this.btn_apply_Click); + // + // grp_mode + // + this.grp_mode.Controls.Add(this.lbl_mode); + this.grp_mode.Controls.Add(this.btn_apply); + this.grp_mode.Location = new System.Drawing.Point(8, 8); + this.grp_mode.Margin = new System.Windows.Forms.Padding(2); + this.grp_mode.Name = "grp_mode"; + this.grp_mode.Padding = new System.Windows.Forms.Padding(2); + this.grp_mode.Size = new System.Drawing.Size(396, 59); + this.grp_mode.TabIndex = 3; + this.grp_mode.TabStop = false; + this.grp_mode.Text = "模式选择"; + // + // grp_input + // + this.grp_input.Controls.Add(this.txt_temperature); + this.grp_input.Controls.Add(this.txt_humidity); + this.grp_input.Controls.Add(this.com_import); + this.grp_input.Controls.Add(this.lbl_temperature); + this.grp_input.Controls.Add(this.txt_light); + this.grp_input.Controls.Add(this.lbl_import); + this.grp_input.Controls.Add(this.lbl_humidity); + this.grp_input.Controls.Add(this.lbl_light); + this.grp_input.Location = new System.Drawing.Point(8, 71); + this.grp_input.Margin = new System.Windows.Forms.Padding(2); + this.grp_input.Name = "grp_input"; + this.grp_input.Padding = new System.Windows.Forms.Padding(2); + this.grp_input.Size = new System.Drawing.Size(133, 236); + this.grp_input.TabIndex = 4; + this.grp_input.TabStop = false; + this.grp_input.Text = "采集量"; + // + // txt_temperature + // + this.txt_temperature.Location = new System.Drawing.Point(56, 113); + this.txt_temperature.Margin = new System.Windows.Forms.Padding(2); + this.txt_temperature.Name = "txt_temperature"; + this.txt_temperature.Size = new System.Drawing.Size(65, 21); + this.txt_temperature.TabIndex = 3; + // + // txt_humidity + // + this.txt_humidity.Location = new System.Drawing.Point(57, 160); + this.txt_humidity.Margin = new System.Windows.Forms.Padding(2); + this.txt_humidity.Name = "txt_humidity"; + this.txt_humidity.Size = new System.Drawing.Size(65, 21); + this.txt_humidity.TabIndex = 3; + // + // com_import + // + this.com_import.FormattingEnabled = true; + this.com_import.Location = new System.Drawing.Point(57, 20); + this.com_import.Margin = new System.Windows.Forms.Padding(2); + this.com_import.Name = "com_import"; + this.com_import.Size = new System.Drawing.Size(65, 20); + this.com_import.TabIndex = 6; + // + // lbl_temperature + // + this.lbl_temperature.AutoSize = true; + this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_temperature.Location = new System.Drawing.Point(22, 117); + this.lbl_temperature.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_temperature.Name = "lbl_temperature"; + this.lbl_temperature.Size = new System.Drawing.Size(31, 12); + this.lbl_temperature.TabIndex = 1; + this.lbl_temperature.Text = "温度"; + // + // txt_light + // + this.txt_light.Location = new System.Drawing.Point(57, 67); + this.txt_light.Margin = new System.Windows.Forms.Padding(2); + this.txt_light.Name = "txt_light"; + this.txt_light.Size = new System.Drawing.Size(65, 21); + this.txt_light.TabIndex = 2; + // + // lbl_import + // + this.lbl_import.AutoSize = true; + this.lbl_import.Location = new System.Drawing.Point(23, 23); + this.lbl_import.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_import.Name = "lbl_import"; + this.lbl_import.Size = new System.Drawing.Size(29, 12); + this.lbl_import.TabIndex = 1; + this.lbl_import.Text = "串口"; + // + // lbl_humidity + // + this.lbl_humidity.AutoSize = true; + this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_humidity.Location = new System.Drawing.Point(23, 163); + this.lbl_humidity.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_humidity.Name = "lbl_humidity"; + this.lbl_humidity.Size = new System.Drawing.Size(31, 12); + this.lbl_humidity.TabIndex = 1; + this.lbl_humidity.Text = "湿度"; + // + // lbl_light + // + this.lbl_light.AutoSize = true; + this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_light.Location = new System.Drawing.Point(11, 70); + this.lbl_light.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_light.Name = "lbl_light"; + this.lbl_light.Size = new System.Drawing.Size(44, 12); + this.lbl_light.TabIndex = 0; + this.lbl_light.Text = "光照度"; + // + // grp_status + // + this.grp_status.Controls.Add(this.pic_airconditioneStatus); + this.grp_status.Controls.Add(this.lbl_airconditionmin); + this.grp_status.Controls.Add(this.lbl_airconditionmax); + this.grp_status.Controls.Add(this.trb_airconditione); + this.grp_status.Controls.Add(this.lamp); + this.grp_status.Controls.Add(this.lbl_airconditione); + this.grp_status.Controls.Add(this.pic_lightsStatus); + this.grp_status.Controls.Add(this.lbl_lights); + this.grp_status.Location = new System.Drawing.Point(145, 71); + this.grp_status.Margin = new System.Windows.Forms.Padding(2); + this.grp_status.Name = "grp_status"; + this.grp_status.Padding = new System.Windows.Forms.Padding(2); + this.grp_status.Size = new System.Drawing.Size(259, 236); + this.grp_status.TabIndex = 5; + this.grp_status.TabStop = false; + this.grp_status.Text = "状态栏"; + // + // pic_airconditioneStatus + // + this.pic_airconditioneStatus.Image = global::教室情景模式.Properties.Resources.switch_off; + this.pic_airconditioneStatus.Location = new System.Drawing.Point(58, 27); + this.pic_airconditioneStatus.Margin = new System.Windows.Forms.Padding(2); + this.pic_airconditioneStatus.Name = "pic_airconditioneStatus"; + this.pic_airconditioneStatus.Size = new System.Drawing.Size(33, 17); + this.pic_airconditioneStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_airconditioneStatus.TabIndex = 6; + this.pic_airconditioneStatus.TabStop = false; + // + // lbl_airconditionmin + // + this.lbl_airconditionmin.AutoSize = true; + this.lbl_airconditionmin.Location = new System.Drawing.Point(10, 87); + this.lbl_airconditionmin.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_airconditionmin.Name = "lbl_airconditionmin"; + this.lbl_airconditionmin.Size = new System.Drawing.Size(29, 12); + this.lbl_airconditionmin.TabIndex = 5; + this.lbl_airconditionmin.Text = "18℃"; + // + // lbl_airconditionmax + // + this.lbl_airconditionmax.AutoSize = true; + this.lbl_airconditionmax.Location = new System.Drawing.Point(95, 87); + this.lbl_airconditionmax.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_airconditionmax.Name = "lbl_airconditionmax"; + this.lbl_airconditionmax.Size = new System.Drawing.Size(29, 12); + this.lbl_airconditionmax.TabIndex = 5; + this.lbl_airconditionmax.Text = "32℃"; + // + // trb_airconditione + // + this.trb_airconditione.Location = new System.Drawing.Point(12, 53); + this.trb_airconditione.Margin = new System.Windows.Forms.Padding(2); + this.trb_airconditione.Maximum = 32; + this.trb_airconditione.Minimum = 18; + this.trb_airconditione.Name = "trb_airconditione"; + this.trb_airconditione.Size = new System.Drawing.Size(107, 45); + this.trb_airconditione.TabIndex = 4; + this.trb_airconditione.Value = 18; + // + // lamp + // + this.lamp.BackColor = System.Drawing.Color.Transparent; + this.lamp.Location = new System.Drawing.Point(139, 40); + this.lamp.Name = "lamp"; + this.lamp.Size = new System.Drawing.Size(105, 180); + this.lamp.TabIndex = 3; + // + // lbl_airconditione + // + this.lbl_airconditione.AutoSize = true; + this.lbl_airconditione.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_airconditione.Location = new System.Drawing.Point(10, 27); + this.lbl_airconditione.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_airconditione.Name = "lbl_airconditione"; + this.lbl_airconditione.Size = new System.Drawing.Size(31, 12); + this.lbl_airconditione.TabIndex = 2; + this.lbl_airconditione.Text = "空调"; + // + // pic_lightsStatus + // + this.pic_lightsStatus.Image = global::教室情景模式.Properties.Resources.switch_off; + this.pic_lightsStatus.Location = new System.Drawing.Point(204, 19); + this.pic_lightsStatus.Margin = new System.Windows.Forms.Padding(2); + this.pic_lightsStatus.Name = "pic_lightsStatus"; + this.pic_lightsStatus.Size = new System.Drawing.Size(33, 17); + this.pic_lightsStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_lightsStatus.TabIndex = 1; + this.pic_lightsStatus.TabStop = false; + this.pic_lightsStatus.Click += new System.EventHandler(this.pic_lightsStatus_Click); + // + // lbl_lights + // + this.lbl_lights.AutoSize = true; + this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_lights.Location = new System.Drawing.Point(161, 19); + this.lbl_lights.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_lights.Name = "lbl_lights"; + this.lbl_lights.Size = new System.Drawing.Size(31, 12); + this.lbl_lights.TabIndex = 0; + this.lbl_lights.Text = "灯光"; + // + // timer + // + this.timer.Interval = 2000; + this.timer.Tick += new System.EventHandler(this.timer_Tick); + // + // frm_control + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(417, 315); + this.Controls.Add(this.grp_status); + this.Controls.Add(this.grp_input); + this.Controls.Add(this.grp_mode); + this.Margin = new System.Windows.Forms.Padding(2); + this.Name = "frm_control"; + this.Text = "智慧教室情景控制"; + this.Load += new System.EventHandler(this.frm_control_Load); + this.grp_mode.ResumeLayout(false); + this.grp_mode.PerformLayout(); + this.grp_input.ResumeLayout(false); + this.grp_input.PerformLayout(); + this.grp_status.ResumeLayout(false); + this.grp_status.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pic_airconditioneStatus)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.trb_airconditione)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label lbl_mode; + private System.Windows.Forms.Button btn_apply; + private System.Windows.Forms.GroupBox grp_mode; + private System.Windows.Forms.GroupBox grp_input; + private System.Windows.Forms.TextBox txt_humidity; + private System.Windows.Forms.TextBox txt_light; + private System.Windows.Forms.Label lbl_humidity; + private System.Windows.Forms.Label lbl_light; + private System.Windows.Forms.GroupBox grp_status; + private System.Windows.Forms.Label lbl_lights; + private System.Windows.Forms.TextBox txt_temperature; + private System.Windows.Forms.Label lbl_temperature; + private System.Windows.Forms.ComboBox com_import; + private System.Windows.Forms.Label lbl_import; + private System.Windows.Forms.Timer timer; + private System.Windows.Forms.PictureBox pic_lightsStatus; + private WinFormsDeviceControlLibrary.Lamp lamp; + private System.Windows.Forms.Label lbl_airconditionmin; + private System.Windows.Forms.Label lbl_airconditionmax; + private System.Windows.Forms.TrackBar trb_airconditione; + private System.Windows.Forms.Label lbl_airconditione; + private System.Windows.Forms.PictureBox pic_airconditioneStatus; + } +} + diff --git a/samples/p7-t1-智慧教室/教室情景模式.cs b/samples/p7-t1-智慧教室/教室情景模式.cs new file mode 100644 index 0000000..1242f31 --- /dev/null +++ b/samples/p7-t1-智慧教室/教室情景模式.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.IO.Ports;//引用系统IO端口库 +using DeviceCtrlLibrary;//引用自定义设备控制库 +using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库 + +namespace 教室情景模式 +{ + public partial class frm_control : Form + { + bool IsOpen = false;//定义布尔变量记录串口打开情况,默认为false + bool IsLampOpen = false;//定义布尔变量记录电灯打开情况,默认为false + bool IsairconditioneOpen = false;//定义布尔变量记录空调打开情况,默认为false + ADAM4017ResModel ResModel;//创建4017接收数据模型 + + public frm_control() + { + InitializeComponent(); + } + + private void frm_control_Load(object sender, EventArgs e) + { + //将系统串口名依次加入下拉列表框中 + foreach (string item in SerialPort.GetPortNames()) + { + com_import.Items.Add(item); + } + } + + private void btn_apply_Click(object sender, EventArgs e) + { + if (!IsOpen)//判断串口是否打开 + { + DeviceCtrlHelper.adam4017Helper.Open(com_import.Text);//打开串口 + DeviceCtrlHelper.adam4150Helper.Open(com_import.Text);//打开串口 + btn_apply.Text = "关闭";//按钮btn_apply显示“关闭” + lbl_mode.Text = "智慧教室已开启";//标签lbl_mode显示“智慧教室已开启” + IsOpen = true; + timer.Start();//通过timer_Tick实时把采集到的数据显示出来,并判断是否需要开灯、关窗帘 + + } + else + { + DeviceCtrlHelper.adam4017Helper.Close();//关闭采集 + DeviceCtrlHelper.adam4150Helper.Close();//关闭4150 + timer.Stop();//停止数据采集与自动控制 + btn_apply.Text = "启用"; + lbl_mode.Text = "智慧教室将开启"; + IsOpen = false; + } + } + + + + private void timer_Tick(object sender, EventArgs e) + { + ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据 + //显示采集到的数据 + txt_light.Text = ResModel.lightValue; + txt_temperature.Text = ResModel.temperatureValue; + txt_humidity.Text = ResModel.humidityValue; + + if (Convert.ToInt16(ResModel.lightValue) < 400)//光照度过低则打开灯 + { + pic_lightsStatus.Image = Properties.Resources.switch_on; + IsLampOpen = true; + lamp.OnOff(true);//打开界面灯 + DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯 + } + else if (Convert.ToInt16(ResModel.lightValue) > 800 ) + { + pic_lightsStatus.Image = Properties.Resources.switch_off; + IsLampOpen = false; + lamp.OnOff(false);//关闭界面灯 + DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯 + } + + if (Convert.ToInt16(ResModel.temperatureValue) > 30 ) + { + pic_airconditioneStatus.Image = Properties.Resources.switch_on; + IsairconditioneOpen = true; + trb_airconditione.Value = 26; + + } + else if (Convert.ToInt16(ResModel.temperatureValue) < 5) + { + pic_airconditioneStatus.Image = Properties.Resources.switch_on; + IsairconditioneOpen = true; + trb_airconditione.Value = 25; + } + else + { + pic_airconditioneStatus.Image = Properties.Resources.switch_off; + IsairconditioneOpen = false; + trb_airconditione.Value = 18; + } + } + + private void pic_lightsStatus_Click(object sender, EventArgs e) + { + if (IsLampOpen) + { + pic_lightsStatus.Image = Properties.Resources.switch_off; + IsLampOpen = false; + lamp.OnOff(false); + DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯 + + } + else + { + pic_lightsStatus.Image = Properties.Resources.switch_on; + IsLampOpen = true; + lamp.OnOff(true); + DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//打开1通道灯 + } + } + + } +} diff --git a/samples/p7-t2-智慧寝室/Form1.Designer.cs b/samples/p7-t2-智慧寝室/Form1.Designer.cs new file mode 100644 index 0000000..7a56d11 --- /dev/null +++ b/samples/p7-t2-智慧寝室/Form1.Designer.cs @@ -0,0 +1,388 @@ +namespace 寝室情景模式 +{ + partial class frm_control + { + /// + /// 必需的设计器变量。 + /// + private System.ComponentModel.IContainer components = null; + + /// + /// 清理所有正在使用的资源。 + /// + /// 如果应释放托管资源,为 true;否则为 false。 + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows 窗体设计器生成的代码 + + /// + /// 设计器支持所需的方法 - 不要 + /// 使用代码编辑器修改此方法的内容。 + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(frm_control)); + this.grp_status = new System.Windows.Forms.GroupBox(); + this.alarm1 = new WinFormsDeviceControlLibrary.Alarm(); + this.lamp = new WinFormsDeviceControlLibrary.Lamp(); + this.lbl_windowopen = new System.Windows.Forms.Label(); + this.lbl_windowclose = new System.Windows.Forms.Label(); + this.trb_curtain = new System.Windows.Forms.TrackBar(); + this.lbl_boardcast = new System.Windows.Forms.Label(); + this.lbl_curtain = new System.Windows.Forms.Label(); + this.pic_boardcastStatus = new System.Windows.Forms.PictureBox(); + this.lbl_lights = new System.Windows.Forms.Label(); + this.grp_input = new System.Windows.Forms.GroupBox(); + this.txt_temperature = new System.Windows.Forms.TextBox(); + this.txt_humidity = new System.Windows.Forms.TextBox(); + this.com_import = new System.Windows.Forms.ComboBox(); + this.lbl_temperature = new System.Windows.Forms.Label(); + this.txt_light = new System.Windows.Forms.TextBox(); + this.lbl_import = new System.Windows.Forms.Label(); + this.lbl_humidity = new System.Windows.Forms.Label(); + this.lbl_light = new System.Windows.Forms.Label(); + this.grp_mode = new System.Windows.Forms.GroupBox(); + this.lbl_time = new System.Windows.Forms.Label(); + this.txt_time = new System.Windows.Forms.TextBox(); + this.lbl_mode = new System.Windows.Forms.Label(); + this.btn_apply = new System.Windows.Forms.Button(); + this.timer = new System.Windows.Forms.Timer(this.components); + this.axWindowsMediaPlayer1 = new AxWMPLib.AxWindowsMediaPlayer(); + this.grp_status.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_boardcastStatus)).BeginInit(); + this.grp_input.SuspendLayout(); + this.grp_mode.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).BeginInit(); + this.SuspendLayout(); + // + // grp_status + // + this.grp_status.Controls.Add(this.alarm1); + this.grp_status.Controls.Add(this.lamp); + this.grp_status.Controls.Add(this.lbl_windowopen); + this.grp_status.Controls.Add(this.lbl_windowclose); + this.grp_status.Controls.Add(this.trb_curtain); + this.grp_status.Controls.Add(this.lbl_boardcast); + this.grp_status.Controls.Add(this.lbl_curtain); + this.grp_status.Controls.Add(this.pic_boardcastStatus); + this.grp_status.Controls.Add(this.lbl_lights); + this.grp_status.Location = new System.Drawing.Point(193, 97); + this.grp_status.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_status.Name = "grp_status"; + this.grp_status.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_status.Size = new System.Drawing.Size(328, 286); + this.grp_status.TabIndex = 8; + this.grp_status.TabStop = false; + this.grp_status.Text = "状态栏"; + // + // alarm1 + // + this.alarm1.BackColor = System.Drawing.Color.White; + this.alarm1.Location = new System.Drawing.Point(37, 130); + this.alarm1.Margin = new System.Windows.Forms.Padding(5); + this.alarm1.Name = "alarm1"; + this.alarm1.Size = new System.Drawing.Size(7, 6); + this.alarm1.TabIndex = 8; + // + // lamp + // + this.lamp.BackColor = System.Drawing.Color.White; + this.lamp.Location = new System.Drawing.Point(172, 48); + this.lamp.Margin = new System.Windows.Forms.Padding(5); + this.lamp.Name = "lamp"; + this.lamp.Size = new System.Drawing.Size(148, 232); + this.lamp.TabIndex = 6; + // + // lbl_windowopen + // + this.lbl_windowopen.AutoSize = true; + this.lbl_windowopen.Location = new System.Drawing.Point(13, 101); + this.lbl_windowopen.Name = "lbl_windowopen"; + this.lbl_windowopen.Size = new System.Drawing.Size(37, 15); + this.lbl_windowopen.TabIndex = 5; + this.lbl_windowopen.Text = "全开"; + // + // lbl_windowclose + // + this.lbl_windowclose.AutoSize = true; + this.lbl_windowclose.Location = new System.Drawing.Point(127, 101); + this.lbl_windowclose.Name = "lbl_windowclose"; + this.lbl_windowclose.Size = new System.Drawing.Size(37, 15); + this.lbl_windowclose.TabIndex = 5; + this.lbl_windowclose.Text = "全关"; + // + // trb_curtain + // + this.trb_curtain.Location = new System.Drawing.Point(16, 59); + this.trb_curtain.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.trb_curtain.Name = "trb_curtain"; + this.trb_curtain.Size = new System.Drawing.Size(143, 56); + this.trb_curtain.TabIndex = 4; + this.trb_curtain.Value = 10; + // + // lbl_boardcast + // + this.lbl_boardcast.AutoSize = true; + this.lbl_boardcast.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_boardcast.Location = new System.Drawing.Point(23, 186); + this.lbl_boardcast.Name = "lbl_boardcast"; + this.lbl_boardcast.Size = new System.Drawing.Size(39, 15); + this.lbl_boardcast.TabIndex = 2; + this.lbl_boardcast.Text = "广播"; + // + // lbl_curtain + // + this.lbl_curtain.AutoSize = true; + this.lbl_curtain.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_curtain.Location = new System.Drawing.Point(13, 34); + this.lbl_curtain.Name = "lbl_curtain"; + this.lbl_curtain.Size = new System.Drawing.Size(39, 15); + this.lbl_curtain.TabIndex = 2; + this.lbl_curtain.Text = "窗帘"; + // + // pic_boardcastStatus + // + this.pic_boardcastStatus.Enabled = false; + this.pic_boardcastStatus.Image = global::寝室情景模式.Properties.Resources.switch_off; + this.pic_boardcastStatus.Location = new System.Drawing.Point(105, 182); + this.pic_boardcastStatus.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.pic_boardcastStatus.Name = "pic_boardcastStatus"; + this.pic_boardcastStatus.Size = new System.Drawing.Size(44, 21); + this.pic_boardcastStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_boardcastStatus.TabIndex = 1; + this.pic_boardcastStatus.TabStop = false; + // + // lbl_lights + // + this.lbl_lights.AutoSize = true; + this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_lights.Location = new System.Drawing.Point(235, 25); + this.lbl_lights.Name = "lbl_lights"; + this.lbl_lights.Size = new System.Drawing.Size(39, 15); + this.lbl_lights.TabIndex = 0; + this.lbl_lights.Text = "灯光"; + // + // grp_input + // + this.grp_input.Controls.Add(this.txt_temperature); + this.grp_input.Controls.Add(this.txt_humidity); + this.grp_input.Controls.Add(this.com_import); + this.grp_input.Controls.Add(this.lbl_temperature); + this.grp_input.Controls.Add(this.txt_light); + this.grp_input.Controls.Add(this.lbl_import); + this.grp_input.Controls.Add(this.lbl_humidity); + this.grp_input.Controls.Add(this.lbl_light); + this.grp_input.Location = new System.Drawing.Point(10, 97); + this.grp_input.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_input.Name = "grp_input"; + this.grp_input.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_input.Size = new System.Drawing.Size(177, 238); + this.grp_input.TabIndex = 7; + this.grp_input.TabStop = false; + this.grp_input.Text = "采集量"; + // + // txt_temperature + // + this.txt_temperature.Location = new System.Drawing.Point(75, 141); + this.txt_temperature.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.txt_temperature.Name = "txt_temperature"; + this.txt_temperature.Size = new System.Drawing.Size(85, 25); + this.txt_temperature.TabIndex = 3; + // + // txt_humidity + // + this.txt_humidity.Location = new System.Drawing.Point(76, 200); + this.txt_humidity.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.txt_humidity.Name = "txt_humidity"; + this.txt_humidity.Size = new System.Drawing.Size(85, 25); + this.txt_humidity.TabIndex = 3; + // + // com_import + // + this.com_import.FormattingEnabled = true; + this.com_import.Location = new System.Drawing.Point(76, 25); + this.com_import.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.com_import.Name = "com_import"; + this.com_import.Size = new System.Drawing.Size(85, 23); + this.com_import.TabIndex = 6; + // + // lbl_temperature + // + this.lbl_temperature.AutoSize = true; + this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_temperature.Location = new System.Drawing.Point(29, 146); + this.lbl_temperature.Name = "lbl_temperature"; + this.lbl_temperature.Size = new System.Drawing.Size(39, 15); + this.lbl_temperature.TabIndex = 1; + this.lbl_temperature.Text = "温度"; + // + // txt_light + // + this.txt_light.Location = new System.Drawing.Point(76, 84); + this.txt_light.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.txt_light.Name = "txt_light"; + this.txt_light.Size = new System.Drawing.Size(85, 25); + this.txt_light.TabIndex = 2; + // + // lbl_import + // + this.lbl_import.AutoSize = true; + this.lbl_import.Location = new System.Drawing.Point(31, 29); + this.lbl_import.Name = "lbl_import"; + this.lbl_import.Size = new System.Drawing.Size(37, 15); + this.lbl_import.TabIndex = 1; + this.lbl_import.Text = "串口"; + // + // lbl_humidity + // + this.lbl_humidity.AutoSize = true; + this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_humidity.Location = new System.Drawing.Point(31, 204); + this.lbl_humidity.Name = "lbl_humidity"; + this.lbl_humidity.Size = new System.Drawing.Size(39, 15); + this.lbl_humidity.TabIndex = 1; + this.lbl_humidity.Text = "湿度"; + // + // lbl_light + // + this.lbl_light.AutoSize = true; + this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_light.Location = new System.Drawing.Point(15, 88); + this.lbl_light.Name = "lbl_light"; + this.lbl_light.Size = new System.Drawing.Size(55, 15); + this.lbl_light.TabIndex = 0; + this.lbl_light.Text = "光照度"; + // + // grp_mode + // + this.grp_mode.Controls.Add(this.lbl_time); + this.grp_mode.Controls.Add(this.txt_time); + this.grp_mode.Controls.Add(this.lbl_mode); + this.grp_mode.Controls.Add(this.btn_apply); + this.grp_mode.Location = new System.Drawing.Point(11, 10); + this.grp_mode.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_mode.Name = "grp_mode"; + this.grp_mode.Padding = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.grp_mode.Size = new System.Drawing.Size(511, 83); + this.grp_mode.TabIndex = 6; + this.grp_mode.TabStop = false; + this.grp_mode.Text = "模式选择"; + // + // lbl_time + // + this.lbl_time.AutoSize = true; + this.lbl_time.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_time.Location = new System.Drawing.Point(68, 20); + this.lbl_time.Name = "lbl_time"; + this.lbl_time.Size = new System.Drawing.Size(71, 15); + this.lbl_time.TabIndex = 9; + this.lbl_time.Text = "当前时间"; + // + // txt_time + // + this.txt_time.Location = new System.Drawing.Point(145, 16); + this.txt_time.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.txt_time.Name = "txt_time"; + this.txt_time.Size = new System.Drawing.Size(127, 25); + this.txt_time.TabIndex = 10; + // + // lbl_mode + // + this.lbl_mode.AutoSize = true; + this.lbl_mode.Location = new System.Drawing.Point(82, 54); + this.lbl_mode.Name = "lbl_mode"; + this.lbl_mode.Size = new System.Drawing.Size(0, 15); + this.lbl_mode.TabIndex = 1; + // + // btn_apply + // + this.btn_apply.Location = new System.Drawing.Point(300, 16); + this.btn_apply.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.btn_apply.Name = "btn_apply"; + this.btn_apply.Size = new System.Drawing.Size(129, 25); + this.btn_apply.TabIndex = 2; + this.btn_apply.Text = "开启情景模式"; + this.btn_apply.UseVisualStyleBackColor = true; + this.btn_apply.Click += new System.EventHandler(this.btn_apply_Click); + // + // timer + // + this.timer.Interval = 1000; + this.timer.Tick += new System.EventHandler(this.timer_Tick); + // + // axWindowsMediaPlayer1 + // + this.axWindowsMediaPlayer1.Enabled = true; + this.axWindowsMediaPlayer1.Location = new System.Drawing.Point(12, 345); + this.axWindowsMediaPlayer1.Name = "axWindowsMediaPlayer1"; + this.axWindowsMediaPlayer1.OcxState = ((System.Windows.Forms.AxHost.State)(resources.GetObject("axWindowsMediaPlayer1.OcxState"))); + this.axWindowsMediaPlayer1.Size = new System.Drawing.Size(175, 38); + this.axWindowsMediaPlayer1.TabIndex = 9; + this.axWindowsMediaPlayer1.Visible = false; + // + // frm_control + // + this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(539, 396); + this.Controls.Add(this.axWindowsMediaPlayer1); + this.Controls.Add(this.grp_status); + this.Controls.Add(this.grp_input); + this.Controls.Add(this.grp_mode); + this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2); + this.Name = "frm_control"; + this.Text = "智慧寝室情景控制"; + this.Load += new System.EventHandler(this.frm_control_Load); + this.grp_status.ResumeLayout(false); + this.grp_status.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_boardcastStatus)).EndInit(); + this.grp_input.ResumeLayout(false); + this.grp_input.PerformLayout(); + this.grp_mode.ResumeLayout(false); + this.grp_mode.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.axWindowsMediaPlayer1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.GroupBox grp_status; + private WinFormsDeviceControlLibrary.Lamp lamp; + private System.Windows.Forms.Label lbl_windowopen; + private System.Windows.Forms.Label lbl_windowclose; + private System.Windows.Forms.TrackBar trb_curtain; + private System.Windows.Forms.Label lbl_boardcast; + private System.Windows.Forms.Label lbl_curtain; + private System.Windows.Forms.PictureBox pic_boardcastStatus; + private System.Windows.Forms.Label lbl_lights; + private System.Windows.Forms.GroupBox grp_input; + private System.Windows.Forms.TextBox txt_temperature; + private System.Windows.Forms.TextBox txt_humidity; + private System.Windows.Forms.ComboBox com_import; + private System.Windows.Forms.Label lbl_temperature; + private System.Windows.Forms.TextBox txt_light; + private System.Windows.Forms.Label lbl_import; + private System.Windows.Forms.Label lbl_humidity; + private System.Windows.Forms.Label lbl_light; + private System.Windows.Forms.GroupBox grp_mode; + private System.Windows.Forms.Label lbl_mode; + private System.Windows.Forms.Button btn_apply; + private System.Windows.Forms.Label lbl_time; + private System.Windows.Forms.TextBox txt_time; + private System.Windows.Forms.Timer timer; + private WinFormsDeviceControlLibrary.Alarm alarm1; + private AxWMPLib.AxWindowsMediaPlayer axWindowsMediaPlayer1; + } +} + diff --git a/samples/p7-t2-智慧寝室/Form1.cs b/samples/p7-t2-智慧寝室/Form1.cs new file mode 100644 index 0000000..d636837 --- /dev/null +++ b/samples/p7-t2-智慧寝室/Form1.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.IO.Ports;//引用系统IO端口库 +using DeviceCtrlLibrary;//引用自定义设备控制库 +using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库 +using System.Threading;//引用线程库 + +namespace 寝室情景模式 +{ + public partial class frm_control : Form + { + bool IsOpen = false;//定义布尔变量记录串口打开情况,默认为false + bool IsLampOopen = false;//定义布尔变量记录灯光打开情况,默认为false + bool IsBoardcastOpen = false;//定义布尔变量记录广播打开情况,默认为false + ADAM4017ResModel ResModel;//创建4017接收数据模型 + DateTime now; + DateTime GetUp = DateTime.Today.AddHours(6.75);//6:45起床时间 + DateTime ClassBegin = DateTime.Today.AddHours(7.5);//7:30上课开始时间 + DateTime ClassEnd = DateTime.Today.AddHours(21.0);//21:00上课结束时间 + DateTime Sleep = DateTime.Today.AddHours(22.0);//22:00睡觉时间 + public frm_control() + { + InitializeComponent(); + } + private void frm_control_Load(object sender, EventArgs e) + { + com_import.Items.Clear(); + //将系统串口名依次加入下拉列表框中 + foreach (string item in SerialPort.GetPortNames()) + { + com_import.Items.Add(item); + } + + } + + private void btn_apply_Click(object sender, EventArgs e) + { + if (!IsOpen) + { + IsOpen = true; + btn_apply.Text = "停止情景模式"; + DeviceCtrlHelper.adam4017Helper.Open(com_import.Text); + DeviceCtrlHelper.adam4150Helper.Open(com_import.Text); + timer.Start(); + + } + else + { + btn_apply.Text = "开启情景模式"; + timer.Stop(); + IsOpen = false; + DeviceCtrlHelper.adam4017Helper.Close(); + DeviceCtrlHelper.adam4150Helper.Close(); + } + } + + private void CheckMode()//切换各个模式 + { + if (now >= GetUp && now < ClassBegin) + { + lbl_mode.Text = "智慧寝室正处于起床模式"; + if (IsLampOopen == false) + { + IsLampOopen = true; + DeviceCtrlHelper.adam4150Helper.OnOff(1, true); + lamp.OnOff(true); + } + if (IsBoardcastOpen == false) + { + IsBoardcastOpen = true; + Thread playthd = new Thread(PlayMp3); + playthd.Start(); + } + trb_curtain.Value = 5; + + } + else if (now >= ClassBegin && now < ClassEnd) + { + lbl_mode.Text = "智慧寝室正处于上课模式"; + if (IsLampOopen == true) + { + IsLampOopen = false; + DeviceCtrlHelper.adam4150Helper.OnOff(1, false); + lamp.OnOff(false); + } + if (IsBoardcastOpen == true) + { + IsBoardcastOpen = false; + pic_boardcastStatus.Image = Properties.Resources.switch_off; + } + trb_curtain.Value = 0; + + } + else if (now >= ClassEnd && now < Sleep) + { + lbl_mode.Text = "智慧寝室正处于回寝模式"; + if (IsLampOopen == false) + { + IsLampOopen = true; + DeviceCtrlHelper.adam4150Helper.OnOff(1, true); + lamp.OnOff(true); + } + if (IsBoardcastOpen == false) + { + IsBoardcastOpen = true; + Thread playthd = new Thread(PlayMp3); + playthd.Start(); + } + + trb_curtain.Value = 10; + + } + else + { + lbl_mode.Text = "智慧寝室正处于熄灯模式"; + if (IsLampOopen == true) + { + IsLampOopen = false; + DeviceCtrlHelper.adam4150Helper.OnOff(1, false); + lamp.OnOff(false); + } + if (IsBoardcastOpen == true) + { + IsBoardcastOpen = false; + pic_boardcastStatus.Image = Properties.Resources.switch_off; + } + trb_curtain.Value = 10; + } + + } + private void timer_Tick(object sender, EventArgs e) + { + + now = DateTime.Now; + txt_time.Text = now.ToString("HH:mm:ss");//显示时间 + ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据 + //显示采集到的数据 + txt_light.Text = ResModel.lightValue;//ResModel.airPressureValue + txt_temperature.Text = ResModel.temperatureValue;//ResModel.airQualityValue + txt_humidity.Text = ResModel.humidityValue;//ResModel.CO2Value + CheckMode(); + } + private void PlayMp3()//播放mp3音乐 + { + + pic_boardcastStatus.Image = Properties.Resources.switch_on; + axWindowsMediaPlayer1.URL = System.AppDomain.CurrentDomain.BaseDirectory + "//Resources//ring.mp3"; + axWindowsMediaPlayer1.Ctlcontrols.play(); + Thread.Sleep(4000); + pic_boardcastStatus.Image = Properties.Resources.switch_off; + + } + + } + +} diff --git a/samples/p7-t2-智慧寝室/Program.cs b/samples/p7-t2-智慧寝室/Program.cs new file mode 100644 index 0000000..78a13f0 --- /dev/null +++ b/samples/p7-t2-智慧寝室/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace 寝室情景模式 +{ + static class Program + { + /// + /// 应用程序的主入口点。 + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new frm_control()); + } + } +} diff --git a/samples/p7-t2-智慧寝室/Properties/Resources.Designer.cs b/samples/p7-t2-智慧寝室/Properties/Resources.Designer.cs new file mode 100644 index 0000000..f2c00d0 --- /dev/null +++ b/samples/p7-t2-智慧寝室/Properties/Resources.Designer.cs @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +// +// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。 +// +//------------------------------------------------------------------------------ + +namespace 寝室情景模式.Properties { + + /// + /// 项目资源:Properties.Resources.资源名 + /// + internal class Resources { + + /// 开关:开 + internal static System.Drawing.Bitmap switch_on { + get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); } + } + + /// 开关:关 + internal static System.Drawing.Bitmap switch_off { + get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); } + } + + /// 铃声 + internal static byte[] ring { + get { return global::Sim.Host.ProjectResources.Bytes("ring.mp3"); } + } + } +} diff --git a/samples/p7-t2-智慧寝室/checks.yaml b/samples/p7-t2-智慧寝室/checks.yaml new file mode 100644 index 0000000..2faa72d --- /dev/null +++ b/samples/p7-t2-智慧寝室/checks.yaml @@ -0,0 +1,31 @@ +# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。 +# 情景模式按时刻切换:用 clock 拨实训台的虚拟时钟(今天的某个时刻)。格式说明见 server/src/grading/checks.ts +- title: 能实时显示环境数据 + steps: + - select: { control: com_import, item: COM2 } + - click: btn_apply + - expect: { control: btn_apply, text: 停止情景模式, desc: 按钮文字变成"停止情景模式" } + - board: { light: 650, temperature: 23, humidity: 51 } + - expect: { control: txt_light, text: '650', within: 5s } + - expect: { control: txt_temperature, text: '23', within: 5s } + - expect: { control: txt_time, matches: '^\d{2}:\d{2}:\d{2}$', desc: 显示当前时间 } + +- title: 能根据时间段的变化判断寝室情景实时控制智能设备 + steps: + - clock: '06:50:00' + - select: { control: com_import, item: COM2 } + - click: btn_apply + - expect: { control: lbl_mode, contains: 起床模式, within: 5s, desc: 6:50 是起床模式 } + - expect: { relay: 1, on: true, desc: 起床模式打开灯光 } + - expect: { control: trb_curtain, value: 5, desc: 起床模式窗帘半开 } + - expect: { media: ring.mp3, desc: 起床模式广播播放铃声 } + - clock: '08:00:00' + - expect: { control: lbl_mode, contains: 上课模式, within: 5s, desc: 8:00 是上课模式 } + - expect: { relay: 1, on: false, desc: 上课模式关闭灯光 } + - expect: { control: trb_curtain, value: 0, desc: 上课模式窗帘全开 } + - clock: '21:30:00' + - expect: { control: lbl_mode, contains: 回寝模式, within: 5s, desc: 21:30 是回寝模式 } + - expect: { control: trb_curtain, value: 10, desc: 回寝模式关窗帘 } + - clock: '23:00:00' + - expect: { control: lbl_mode, contains: 熄灯模式, within: 5s, desc: 23:00 是熄灯模式 } + - expect: { relay: 1, on: false, desc: 熄灯模式关闭灯光 } diff --git a/samples/p7-t2-智慧寝室/meta.json b/samples/p7-t2-智慧寝室/meta.json new file mode 100644 index 0000000..da539ce --- /dev/null +++ b/samples/p7-t2-智慧寝室/meta.json @@ -0,0 +1,4 @@ +{ + "title": "项目7 任务2 实现智慧寝室", + "source": "教材书 p141–145,界面和代码取自配套参考工程;开关图片和铃声来自平台素材库。按时刻切换情景模式:在实训台的时钟上拨时间" +} diff --git a/samples/p7-t3-智慧会议室/Form1.Designer.cs b/samples/p7-t3-智慧会议室/Form1.Designer.cs new file mode 100644 index 0000000..1021b97 --- /dev/null +++ b/samples/p7-t3-智慧会议室/Form1.Designer.cs @@ -0,0 +1,611 @@ +namespace 会议室情景模式 +{ + partial class frm_control + { + /// + /// 必需的设计器变量。 + /// + private System.ComponentModel.IContainer components = null; + + /// + /// 清理所有正在使用的资源。 + /// + /// 如果应释放托管资源,为 true;否则为 false。 + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows 窗体设计器生成的代码 + + /// + /// 设计器支持所需的方法 - 不要 + /// 使用代码编辑器修改此方法的内容。 + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.grp_status = new System.Windows.Forms.GroupBox(); + this.lbl_subMicrophone = new System.Windows.Forms.Label(); + this.lbl_mainMicrophone = new System.Windows.Forms.Label(); + this.lamp = new WinFormsDeviceControlLibrary.Lamp(); + this.lbl_curtainopen = new System.Windows.Forms.Label(); + this.lbl_curtainclose = new System.Windows.Forms.Label(); + this.trb_curtain = new System.Windows.Forms.TrackBar(); + this.lbl_microphone = new System.Windows.Forms.Label(); + this.lbl_curtain = new System.Windows.Forms.Label(); + this.pic_lightsStatus = new System.Windows.Forms.PictureBox(); + this.pic_subMicrophone = new System.Windows.Forms.PictureBox(); + this.pic_mainMicrophone = new System.Windows.Forms.PictureBox(); + this.lbl_lights = new System.Windows.Forms.Label(); + this.grp_input = new System.Windows.Forms.GroupBox(); + this.lbl_camera = new System.Windows.Forms.Label(); + this.txt_camera = new System.Windows.Forms.TextBox(); + this.txt_temperature = new System.Windows.Forms.TextBox(); + this.txt_airQuality = new System.Windows.Forms.TextBox(); + this.txt_humidity = new System.Windows.Forms.TextBox(); + this.com_import = new System.Windows.Forms.ComboBox(); + this.lbl_temperature = new System.Windows.Forms.Label(); + this.txt_airPressure = new System.Windows.Forms.TextBox(); + this.txt_light = new System.Windows.Forms.TextBox(); + this.lbl_import = new System.Windows.Forms.Label(); + this.lbl_airQuality = new System.Windows.Forms.Label(); + this.lbl_airPressure = new System.Windows.Forms.Label(); + this.lbl_humidity = new System.Windows.Forms.Label(); + this.lbl_light = new System.Windows.Forms.Label(); + this.grp_mode = new System.Windows.Forms.GroupBox(); + this.com_mode = new System.Windows.Forms.ComboBox(); + this.lbl_second = new System.Windows.Forms.Label(); + this.lbl_minute = new System.Windows.Forms.Label(); + this.lbl_hour = new System.Windows.Forms.Label(); + this.lbl_time = new System.Windows.Forms.Label(); + this.txt_second = new System.Windows.Forms.TextBox(); + this.txt_minute = new System.Windows.Forms.TextBox(); + this.txt_hour = new System.Windows.Forms.TextBox(); + this.chk_record = new System.Windows.Forms.CheckBox(); + this.lbl_mode = new System.Windows.Forms.Label(); + this.btn_exit = new System.Windows.Forms.Button(); + this.btn_reserve = new System.Windows.Forms.Button(); + this.timer = new System.Windows.Forms.Timer(this.components); + this.grp_status.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_subMicrophone)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_mainMicrophone)).BeginInit(); + this.grp_input.SuspendLayout(); + this.grp_mode.SuspendLayout(); + this.SuspendLayout(); + // + // grp_status + // + this.grp_status.Controls.Add(this.lbl_subMicrophone); + this.grp_status.Controls.Add(this.lbl_mainMicrophone); + this.grp_status.Controls.Add(this.lamp); + this.grp_status.Controls.Add(this.lbl_curtainopen); + this.grp_status.Controls.Add(this.lbl_curtainclose); + this.grp_status.Controls.Add(this.trb_curtain); + this.grp_status.Controls.Add(this.lbl_microphone); + this.grp_status.Controls.Add(this.lbl_curtain); + this.grp_status.Controls.Add(this.pic_lightsStatus); + this.grp_status.Controls.Add(this.pic_subMicrophone); + this.grp_status.Controls.Add(this.pic_mainMicrophone); + this.grp_status.Controls.Add(this.lbl_lights); + this.grp_status.Location = new System.Drawing.Point(145, 100); + this.grp_status.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_status.Name = "grp_status"; + this.grp_status.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_status.Size = new System.Drawing.Size(294, 270); + this.grp_status.TabIndex = 11; + this.grp_status.TabStop = false; + this.grp_status.Text = "状态栏"; + // + // lbl_subMicrophone + // + this.lbl_subMicrophone.AutoSize = true; + this.lbl_subMicrophone.Location = new System.Drawing.Point(113, 140); + this.lbl_subMicrophone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_subMicrophone.Name = "lbl_subMicrophone"; + this.lbl_subMicrophone.Size = new System.Drawing.Size(17, 12); + this.lbl_subMicrophone.TabIndex = 7; + this.lbl_subMicrophone.Text = "客"; + // + // lbl_mainMicrophone + // + this.lbl_mainMicrophone.AutoSize = true; + this.lbl_mainMicrophone.Location = new System.Drawing.Point(64, 140); + this.lbl_mainMicrophone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_mainMicrophone.Name = "lbl_mainMicrophone"; + this.lbl_mainMicrophone.Size = new System.Drawing.Size(17, 12); + this.lbl_mainMicrophone.TabIndex = 7; + this.lbl_mainMicrophone.Text = "主"; + // + // lamp + // + this.lamp.BackColor = System.Drawing.Color.White; + this.lamp.Location = new System.Drawing.Point(175, 65); + this.lamp.Name = "lamp"; + this.lamp.Size = new System.Drawing.Size(104, 177); + this.lamp.TabIndex = 6; + // + // lbl_curtainopen + // + this.lbl_curtainopen.AutoSize = true; + this.lbl_curtainopen.Location = new System.Drawing.Point(10, 74); + this.lbl_curtainopen.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_curtainopen.Name = "lbl_curtainopen"; + this.lbl_curtainopen.Size = new System.Drawing.Size(29, 12); + this.lbl_curtainopen.TabIndex = 5; + this.lbl_curtainopen.Text = "全开"; + // + // lbl_curtainclose + // + this.lbl_curtainclose.AutoSize = true; + this.lbl_curtainclose.Location = new System.Drawing.Point(95, 74); + this.lbl_curtainclose.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_curtainclose.Name = "lbl_curtainclose"; + this.lbl_curtainclose.Size = new System.Drawing.Size(29, 12); + this.lbl_curtainclose.TabIndex = 5; + this.lbl_curtainclose.Text = "全关"; + // + // trb_curtain + // + this.trb_curtain.Location = new System.Drawing.Point(12, 47); + this.trb_curtain.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.trb_curtain.Name = "trb_curtain"; + this.trb_curtain.Size = new System.Drawing.Size(107, 45); + this.trb_curtain.TabIndex = 4; + this.trb_curtain.Value = 10; + // + // lbl_microphone + // + this.lbl_microphone.AutoSize = true; + this.lbl_microphone.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_microphone.Location = new System.Drawing.Point(10, 121); + this.lbl_microphone.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_microphone.Name = "lbl_microphone"; + this.lbl_microphone.Size = new System.Drawing.Size(31, 12); + this.lbl_microphone.TabIndex = 2; + this.lbl_microphone.Text = "话筒"; + // + // lbl_curtain + // + this.lbl_curtain.AutoSize = true; + this.lbl_curtain.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_curtain.Location = new System.Drawing.Point(10, 35); + this.lbl_curtain.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_curtain.Name = "lbl_curtain"; + this.lbl_curtain.Size = new System.Drawing.Size(31, 12); + this.lbl_curtain.TabIndex = 2; + this.lbl_curtain.Text = "窗帘"; + // + // pic_lightsStatus + // + this.pic_lightsStatus.Image = global::会议室情景模式.Properties.Resources.switch_off; + this.pic_lightsStatus.Location = new System.Drawing.Point(211, 35); + this.pic_lightsStatus.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.pic_lightsStatus.Name = "pic_lightsStatus"; + this.pic_lightsStatus.Size = new System.Drawing.Size(33, 17); + this.pic_lightsStatus.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_lightsStatus.TabIndex = 1; + this.pic_lightsStatus.TabStop = false; + this.pic_lightsStatus.Click += new System.EventHandler(this.pic_lightsStatus_Click); + // + // pic_subMicrophone + // + this.pic_subMicrophone.Image = global::会议室情景模式.Properties.Resources.switch_off; + this.pic_subMicrophone.Location = new System.Drawing.Point(105, 118); + this.pic_subMicrophone.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.pic_subMicrophone.Name = "pic_subMicrophone"; + this.pic_subMicrophone.Size = new System.Drawing.Size(33, 17); + this.pic_subMicrophone.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_subMicrophone.TabIndex = 1; + this.pic_subMicrophone.TabStop = false; + this.pic_subMicrophone.Click += new System.EventHandler(this.pic_subMicrophone_Click); + // + // pic_mainMicrophone + // + this.pic_mainMicrophone.Image = global::会议室情景模式.Properties.Resources.switch_off; + this.pic_mainMicrophone.Location = new System.Drawing.Point(57, 118); + this.pic_mainMicrophone.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.pic_mainMicrophone.Name = "pic_mainMicrophone"; + this.pic_mainMicrophone.Size = new System.Drawing.Size(33, 17); + this.pic_mainMicrophone.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pic_mainMicrophone.TabIndex = 1; + this.pic_mainMicrophone.TabStop = false; + this.pic_mainMicrophone.Click += new System.EventHandler(this.pic_mainMicrophone_Click); + // + // lbl_lights + // + this.lbl_lights.AutoSize = true; + this.lbl_lights.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_lights.Location = new System.Drawing.Point(213, 21); + this.lbl_lights.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_lights.Name = "lbl_lights"; + this.lbl_lights.Size = new System.Drawing.Size(31, 12); + this.lbl_lights.TabIndex = 0; + this.lbl_lights.Text = "灯光"; + // + // grp_input + // + this.grp_input.Controls.Add(this.lbl_camera); + this.grp_input.Controls.Add(this.txt_camera); + this.grp_input.Controls.Add(this.txt_temperature); + this.grp_input.Controls.Add(this.txt_airQuality); + this.grp_input.Controls.Add(this.txt_humidity); + this.grp_input.Controls.Add(this.com_import); + this.grp_input.Controls.Add(this.lbl_temperature); + this.grp_input.Controls.Add(this.txt_airPressure); + this.grp_input.Controls.Add(this.txt_light); + this.grp_input.Controls.Add(this.lbl_import); + this.grp_input.Controls.Add(this.lbl_airQuality); + this.grp_input.Controls.Add(this.lbl_airPressure); + this.grp_input.Controls.Add(this.lbl_humidity); + this.grp_input.Controls.Add(this.lbl_light); + this.grp_input.Location = new System.Drawing.Point(8, 100); + this.grp_input.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_input.Name = "grp_input"; + this.grp_input.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_input.Size = new System.Drawing.Size(133, 270); + this.grp_input.TabIndex = 10; + this.grp_input.TabStop = false; + this.grp_input.Text = "采集量"; + // + // lbl_camera + // + this.lbl_camera.AutoSize = true; + this.lbl_camera.Location = new System.Drawing.Point(26, 222); + this.lbl_camera.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_camera.Name = "lbl_camera"; + this.lbl_camera.Size = new System.Drawing.Size(77, 12); + this.lbl_camera.TabIndex = 8; + this.lbl_camera.Text = "摄像头IP地址"; + // + // txt_camera + // + this.txt_camera.Location = new System.Drawing.Point(19, 244); + this.txt_camera.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_camera.Name = "txt_camera"; + this.txt_camera.Size = new System.Drawing.Size(95, 21); + this.txt_camera.TabIndex = 9; + this.txt_camera.Text = "192.168.14.131"; + // + // txt_temperature + // + this.txt_temperature.Location = new System.Drawing.Point(62, 80); + this.txt_temperature.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_temperature.Name = "txt_temperature"; + this.txt_temperature.Size = new System.Drawing.Size(65, 21); + this.txt_temperature.TabIndex = 3; + // + // txt_airQuality + // + this.txt_airQuality.Location = new System.Drawing.Point(62, 190); + this.txt_airQuality.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_airQuality.Name = "txt_airQuality"; + this.txt_airQuality.Size = new System.Drawing.Size(65, 21); + this.txt_airQuality.TabIndex = 3; + // + // txt_humidity + // + this.txt_humidity.Location = new System.Drawing.Point(62, 117); + this.txt_humidity.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_humidity.Name = "txt_humidity"; + this.txt_humidity.Size = new System.Drawing.Size(65, 21); + this.txt_humidity.TabIndex = 3; + // + // com_import + // + this.com_import.FormattingEnabled = true; + this.com_import.Location = new System.Drawing.Point(57, 13); + this.com_import.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.com_import.Name = "com_import"; + this.com_import.Size = new System.Drawing.Size(65, 20); + this.com_import.TabIndex = 6; + // + // lbl_temperature + // + this.lbl_temperature.AutoSize = true; + this.lbl_temperature.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_temperature.Location = new System.Drawing.Point(30, 83); + this.lbl_temperature.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_temperature.Name = "lbl_temperature"; + this.lbl_temperature.Size = new System.Drawing.Size(31, 12); + this.lbl_temperature.TabIndex = 1; + this.lbl_temperature.Text = "温度"; + // + // txt_airPressure + // + this.txt_airPressure.Location = new System.Drawing.Point(62, 153); + this.txt_airPressure.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_airPressure.Name = "txt_airPressure"; + this.txt_airPressure.Size = new System.Drawing.Size(65, 21); + this.txt_airPressure.TabIndex = 2; + // + // txt_light + // + this.txt_light.Location = new System.Drawing.Point(62, 43); + this.txt_light.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_light.Name = "txt_light"; + this.txt_light.Size = new System.Drawing.Size(65, 21); + this.txt_light.TabIndex = 2; + // + // lbl_import + // + this.lbl_import.AutoSize = true; + this.lbl_import.Location = new System.Drawing.Point(23, 17); + this.lbl_import.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_import.Name = "lbl_import"; + this.lbl_import.Size = new System.Drawing.Size(29, 12); + this.lbl_import.TabIndex = 1; + this.lbl_import.Text = "串口"; + // + // lbl_airQuality + // + this.lbl_airQuality.AutoSize = true; + this.lbl_airQuality.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_airQuality.Location = new System.Drawing.Point(5, 193); + this.lbl_airQuality.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_airQuality.Name = "lbl_airQuality"; + this.lbl_airQuality.Size = new System.Drawing.Size(57, 12); + this.lbl_airQuality.TabIndex = 1; + this.lbl_airQuality.Text = "空气质量"; + // + // lbl_airPressure + // + this.lbl_airPressure.AutoSize = true; + this.lbl_airPressure.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_airPressure.Location = new System.Drawing.Point(30, 157); + this.lbl_airPressure.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_airPressure.Name = "lbl_airPressure"; + this.lbl_airPressure.Size = new System.Drawing.Size(31, 12); + this.lbl_airPressure.TabIndex = 0; + this.lbl_airPressure.Text = "气压"; + // + // lbl_humidity + // + this.lbl_humidity.AutoSize = true; + this.lbl_humidity.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_humidity.Location = new System.Drawing.Point(30, 120); + this.lbl_humidity.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_humidity.Name = "lbl_humidity"; + this.lbl_humidity.Size = new System.Drawing.Size(31, 12); + this.lbl_humidity.TabIndex = 1; + this.lbl_humidity.Text = "湿度"; + // + // lbl_light + // + this.lbl_light.AutoSize = true; + this.lbl_light.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_light.Location = new System.Drawing.Point(17, 47); + this.lbl_light.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_light.Name = "lbl_light"; + this.lbl_light.Size = new System.Drawing.Size(44, 12); + this.lbl_light.TabIndex = 0; + this.lbl_light.Text = "光照度"; + // + // grp_mode + // + this.grp_mode.Controls.Add(this.com_mode); + this.grp_mode.Controls.Add(this.lbl_second); + this.grp_mode.Controls.Add(this.lbl_minute); + this.grp_mode.Controls.Add(this.lbl_hour); + this.grp_mode.Controls.Add(this.lbl_time); + this.grp_mode.Controls.Add(this.txt_second); + this.grp_mode.Controls.Add(this.txt_minute); + this.grp_mode.Controls.Add(this.txt_hour); + this.grp_mode.Controls.Add(this.chk_record); + this.grp_mode.Controls.Add(this.lbl_mode); + this.grp_mode.Controls.Add(this.btn_exit); + this.grp_mode.Controls.Add(this.btn_reserve); + this.grp_mode.Location = new System.Drawing.Point(8, 8); + this.grp_mode.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_mode.Name = "grp_mode"; + this.grp_mode.Padding = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.grp_mode.Size = new System.Drawing.Size(431, 88); + this.grp_mode.TabIndex = 9; + this.grp_mode.TabStop = false; + this.grp_mode.Text = "模式选择"; + // + // com_mode + // + this.com_mode.FormattingEnabled = true; + this.com_mode.Location = new System.Drawing.Point(191, 53); + this.com_mode.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.com_mode.Name = "com_mode"; + this.com_mode.Size = new System.Drawing.Size(61, 20); + this.com_mode.TabIndex = 12; + this.com_mode.Text = "会议"; + // + // lbl_second + // + this.lbl_second.AutoSize = true; + this.lbl_second.Location = new System.Drawing.Point(127, 57); + this.lbl_second.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_second.Name = "lbl_second"; + this.lbl_second.Size = new System.Drawing.Size(65, 12); + this.lbl_second.TabIndex = 11; + this.lbl_second.Text = "秒将会有场"; + // + // lbl_minute + // + this.lbl_minute.AutoSize = true; + this.lbl_minute.Location = new System.Drawing.Point(91, 57); + this.lbl_minute.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_minute.Name = "lbl_minute"; + this.lbl_minute.Size = new System.Drawing.Size(17, 12); + this.lbl_minute.TabIndex = 11; + this.lbl_minute.Text = "分"; + // + // lbl_hour + // + this.lbl_hour.AutoSize = true; + this.lbl_hour.Location = new System.Drawing.Point(56, 57); + this.lbl_hour.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_hour.Name = "lbl_hour"; + this.lbl_hour.Size = new System.Drawing.Size(17, 12); + this.lbl_hour.TabIndex = 11; + this.lbl_hour.Text = "时"; + // + // lbl_time + // + this.lbl_time.AutoSize = true; + this.lbl_time.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(134))); + this.lbl_time.Location = new System.Drawing.Point(17, 27); + this.lbl_time.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_time.Name = "lbl_time"; + this.lbl_time.Size = new System.Drawing.Size(126, 12); + this.lbl_time.TabIndex = 9; + this.lbl_time.Text = "当前时间:00:00:00"; + // + // txt_second + // + this.txt_second.Location = new System.Drawing.Point(109, 53); + this.txt_second.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_second.Name = "txt_second"; + this.txt_second.Size = new System.Drawing.Size(19, 21); + this.txt_second.TabIndex = 10; + this.txt_second.Text = "00"; + // + // txt_minute + // + this.txt_minute.Location = new System.Drawing.Point(73, 53); + this.txt_minute.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_minute.Name = "txt_minute"; + this.txt_minute.Size = new System.Drawing.Size(19, 21); + this.txt_minute.TabIndex = 10; + this.txt_minute.Text = "20"; + // + // txt_hour + // + this.txt_hour.Location = new System.Drawing.Point(38, 53); + this.txt_hour.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.txt_hour.Name = "txt_hour"; + this.txt_hour.Size = new System.Drawing.Size(19, 21); + this.txt_hour.TabIndex = 10; + this.txt_hour.Text = "13"; + // + // chk_record + // + this.chk_record.AutoSize = true; + this.chk_record.Location = new System.Drawing.Point(344, 26); + this.chk_record.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.chk_record.Name = "chk_record"; + this.chk_record.Size = new System.Drawing.Size(72, 16); + this.chk_record.TabIndex = 3; + this.chk_record.Text = "拍照记录"; + this.chk_record.UseVisualStyleBackColor = true; + // + // lbl_mode + // + this.lbl_mode.AutoSize = true; + this.lbl_mode.Location = new System.Drawing.Point(162, 27); + this.lbl_mode.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.lbl_mode.Name = "lbl_mode"; + this.lbl_mode.Size = new System.Drawing.Size(149, 12); + this.lbl_mode.TabIndex = 1; + this.lbl_mode.Text = "智慧会议室未开启情景模式"; + // + // btn_exit + // + this.btn_exit.Location = new System.Drawing.Point(345, 49); + this.btn_exit.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.btn_exit.Name = "btn_exit"; + this.btn_exit.Size = new System.Drawing.Size(51, 27); + this.btn_exit.TabIndex = 2; + this.btn_exit.Text = "退出"; + this.btn_exit.UseVisualStyleBackColor = true; + this.btn_exit.Click += new System.EventHandler(this.btn_exit_Click); + // + // btn_reserve + // + this.btn_reserve.Location = new System.Drawing.Point(277, 49); + this.btn_reserve.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.btn_reserve.Name = "btn_reserve"; + this.btn_reserve.Size = new System.Drawing.Size(51, 27); + this.btn_reserve.TabIndex = 2; + this.btn_reserve.Text = "预约"; + this.btn_reserve.UseVisualStyleBackColor = true; + this.btn_reserve.Click += new System.EventHandler(this.btn_reserve_Click); + // + // timer + // + this.timer.Interval = 1000; + this.timer.Tick += new System.EventHandler(this.timer_Tick); + // + // frm_control + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(447, 376); + this.Controls.Add(this.grp_status); + this.Controls.Add(this.grp_input); + this.Controls.Add(this.grp_mode); + this.Margin = new System.Windows.Forms.Padding(2, 2, 2, 2); + this.Name = "frm_control"; + this.Text = "智慧会议室情景模式"; + this.Load += new System.EventHandler(this.frm_control_Load); + this.grp_status.ResumeLayout(false); + this.grp_status.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trb_curtain)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_lightsStatus)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_subMicrophone)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.pic_mainMicrophone)).EndInit(); + this.grp_input.ResumeLayout(false); + this.grp_input.PerformLayout(); + this.grp_mode.ResumeLayout(false); + this.grp_mode.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.GroupBox grp_status; + private WinFormsDeviceControlLibrary.Lamp lamp; + private System.Windows.Forms.Label lbl_curtainopen; + private System.Windows.Forms.Label lbl_curtainclose; + private System.Windows.Forms.TrackBar trb_curtain; + private System.Windows.Forms.Label lbl_microphone; + private System.Windows.Forms.Label lbl_curtain; + private System.Windows.Forms.PictureBox pic_lightsStatus; + private System.Windows.Forms.PictureBox pic_mainMicrophone; + private System.Windows.Forms.Label lbl_lights; + private System.Windows.Forms.GroupBox grp_input; + private System.Windows.Forms.TextBox txt_temperature; + private System.Windows.Forms.TextBox txt_humidity; + private System.Windows.Forms.ComboBox com_import; + private System.Windows.Forms.Label lbl_temperature; + private System.Windows.Forms.TextBox txt_light; + private System.Windows.Forms.Label lbl_import; + private System.Windows.Forms.Label lbl_humidity; + private System.Windows.Forms.Label lbl_light; + private System.Windows.Forms.GroupBox grp_mode; + private System.Windows.Forms.Label lbl_time; + private System.Windows.Forms.TextBox txt_hour; + private System.Windows.Forms.CheckBox chk_record; + private System.Windows.Forms.Label lbl_mode; + private System.Windows.Forms.Button btn_reserve; + private System.Windows.Forms.TextBox txt_airQuality; + private System.Windows.Forms.TextBox txt_airPressure; + private System.Windows.Forms.Label lbl_airQuality; + private System.Windows.Forms.Label lbl_airPressure; + private System.Windows.Forms.ComboBox com_mode; + private System.Windows.Forms.Label lbl_second; + private System.Windows.Forms.Label lbl_minute; + private System.Windows.Forms.Label lbl_hour; + private System.Windows.Forms.TextBox txt_second; + private System.Windows.Forms.TextBox txt_minute; + private System.Windows.Forms.Timer timer; + private System.Windows.Forms.Button btn_exit; + private System.Windows.Forms.Label lbl_subMicrophone; + private System.Windows.Forms.Label lbl_mainMicrophone; + private System.Windows.Forms.PictureBox pic_subMicrophone; + private System.Windows.Forms.Label lbl_camera; + private System.Windows.Forms.TextBox txt_camera; + } +} + diff --git a/samples/p7-t3-智慧会议室/Form1.cs b/samples/p7-t3-智慧会议室/Form1.cs new file mode 100644 index 0000000..d85b616 --- /dev/null +++ b/samples/p7-t3-智慧会议室/Form1.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.IO;//引用系统IO库 +using System.IO.Ports;//引用系统IO端口库 +using DeviceCtrlLibrary;//引用自定义设备控制库 +using DeviceCtrlLibrary.Model;//引用自定义设备控制模型库 + +namespace 会议室情景模式 +{ + public partial class frm_control : Form + { + bool IsLightsOpen = false;//定义布尔变量记录电灯打开情况,默认为false + bool IsmainMicrophoneOpen = false;//定义布尔变量记录主麦克风打开情况,默认为false + bool IssubMicrophoneOpen = false;//定义布尔变量记录客麦克风打开情况,默认为false + bool IsInitialized = false;//定义布尔变量记录情景模式是否初始化,默认为false + ADAM4017ResModel ResModel;//创建4017接收数据模型 + DateTime now;//定义时间变量记录现在的时间 + DateTime reserve=DateTime.Now.AddDays(1) ;//定义时间变量记录预约的时间,默认为明天,防止初始值小于当前时间的假预约情况 + Image ReadImage = null;//定义图片变量记录拍摄的图片,默认为空 + + public frm_control() + { + InitializeComponent(); + } + + private void frm_control_Load(object sender, EventArgs e) + { + com_import.Items.Clear(); + com_mode.Items.Clear(); + //将系统串口名依次加入下拉列表框中 + foreach (string item in SerialPort.GetPortNames()) + { + com_import.Items.Add(item); + } + com_mode.Items.Add("会议"); + com_mode.Items.Add("讨论"); + com_mode.Items.Add("展示"); + timer.Start(); + //判断拍照保存文件夹是否存在,若无则先创建 + if (!Directory.Exists(AppDomain.CurrentDomain.BaseDirectory + "Camera/")) + Directory.CreateDirectory(AppDomain.CurrentDomain.BaseDirectory + "Camera/"); + + DeviceCtrlHelper.ipCameraHelper.ImageRead += ipCameraHelper_ImageRead; + } + + private void btn_reserve_Click(object sender, EventArgs e) + { + reserve = DateTime.Today.AddHours(Convert.ToInt16(txt_hour.Text));//设定预约时间 + reserve = reserve.AddMinutes(Convert.ToInt16(txt_minute.Text)); + reserve = reserve.AddSeconds(Convert.ToInt16(txt_second.Text)); + } + + void ipCameraHelper_ImageRead(ImageReadEvenArgs e) + { + ReadImage = e.img; + } + + private void ChooseMode() + { + lbl_mode.Text = "智慧会议室已开启" + com_mode.Text + "模式"; + switch (com_mode.Text) + { + case "会议": + pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片 + lamp.OnOff(true);//开启界面灯光 + DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯 + trb_curtain.Value = 0;//打开窗帘 + pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦 + pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭客麦 + break; + case "讨论": + pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片 + lamp.OnOff(true);//开启界面灯光 + DeviceCtrlHelper.adam4150Helper.OnOff(1,true);//打开1通道灯 + trb_curtain.Value = 0;//打开窗帘 + pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦 + pic_subMicrophone.Image = Properties.Resources.switch_on;//打开客麦 + break; + case "展示": + pic_lightsStatus.Image = Properties.Resources.switch_on;//转换灯光图片 + lamp.OnOff(true);//开启界面灯光 + DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//打开1通道灯 + trb_curtain.Value = 10;//关闭窗帘 + pic_mainMicrophone.Image = Properties.Resources.switch_on;//打开主麦 + pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭主麦 + break; + } + } + + private void timer_Tick(object sender, EventArgs e) + { + DeviceCtrlHelper.adam4017Helper.Open(com_import.Text); + DeviceCtrlHelper.adam4150Helper.Open(com_import.Text); + now = DateTime.Now; + lbl_time.Text = now.ToString("HH:mm:ss");//显示时间 + ResModel = DeviceCtrlHelper.adam4017Helper.SetData();//采集数据 + //显示采集到的数据 + txt_light.Text = ResModel.lightValue; + txt_temperature.Text = ResModel.temperatureValue; + txt_humidity.Text = ResModel.humidityValue; + txt_airPressure.Text = ResModel.airPressureValue; + txt_airQuality.Text = ResModel.airQualityValue; + //模式判断 + if (now >= reserve) + { + if (!IsInitialized) + { + ChooseMode();//进入选择的模式 + if (chk_record.Checked) + { + DeviceCtrlHelper.ipCameraHelper.Open(txt_camera.Text, "admin", "");//初始化视频监控参数 + DeviceCtrlHelper.ipCameraHelper.Start();//开启视频监控 + } + IsInitialized = true; + } + if (now.Second == 0 && now.Minute % 5 == 0)//每次分钟数被5整除,秒钟数为00拍照,实现每隔5分钟拍照存储 + { + if (ReadImage != null) + { + ReadImage.Save(AppDomain.CurrentDomain.BaseDirectory + "Camera/" + DateTime.Now.ToString("yyyyMMdd_HH mm ss") + ".jpg"); + } + } + } + + } + + private void pic_lightsStatus_Click(object sender, EventArgs e) + {//电灯可以手动开关 + if (IsLightsOpen) + { + lamp.OnOff(false); + pic_lightsStatus.Image = Properties.Resources.switch_off; + DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯 + IsLightsOpen = false; + } + else + { + lamp.OnOff(true); + pic_lightsStatus.Image = Properties.Resources.switch_on; + DeviceCtrlHelper.adam4150Helper.OnOff(1, true);//关闭1通道灯 + IsLightsOpen = true; + } + } + + private void pic_subMicrophone_Click(object sender, EventArgs e) + {//客麦可以手动开关 + if (IssubMicrophoneOpen) + { + pic_subMicrophone.Image = Properties.Resources.switch_off; + IssubMicrophoneOpen = false; + } + else + { + pic_subMicrophone.Image = Properties.Resources.switch_on; + IssubMicrophoneOpen = true; + } + + } + + private void btn_exit_Click(object sender, EventArgs e) + { + timer.Stop(); + reserve = DateTime.Today.AddDays(1); + chk_record.Enabled = false; + pic_lightsStatus.Image = Properties.Resources.switch_off;//转换灯光图片 + lamp.OnOff(false);//关闭界面灯光 + DeviceCtrlHelper.adam4150Helper.OnOff(1, false);//关闭1通道灯 + trb_curtain.Value = 0;//关闭窗帘 + pic_mainMicrophone.Image = Properties.Resources.switch_off;//关闭主麦 + pic_subMicrophone.Image = Properties.Resources.switch_off;//关闭客麦 + IsInitialized = false;//初始化状态 + DeviceCtrlHelper.adam4150Helper.Close(); + DeviceCtrlHelper.adam4017Helper.Close();//关闭串口 + DeviceCtrlHelper.ipCameraHelper.Stop();//关闭视频监控 + + } + + private void pic_mainMicrophone_Click(object sender, EventArgs e) + {//主麦可以手动开关 + if (IsmainMicrophoneOpen) + { + pic_mainMicrophone.Image = Properties.Resources.switch_off; + IsmainMicrophoneOpen = false; + } + else + { + pic_mainMicrophone.Image = Properties.Resources.switch_on; + IsmainMicrophoneOpen = true; + } + } + + } +} diff --git a/samples/p7-t3-智慧会议室/Program.cs b/samples/p7-t3-智慧会议室/Program.cs new file mode 100644 index 0000000..6d282cd --- /dev/null +++ b/samples/p7-t3-智慧会议室/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace 会议室情景模式 +{ + static class Program + { + /// + /// 应用程序的主入口点。 + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new frm_control()); + } + } +} diff --git a/samples/p7-t3-智慧会议室/Properties/Resources.Designer.cs b/samples/p7-t3-智慧会议室/Properties/Resources.Designer.cs new file mode 100644 index 0000000..6a7b1d3 --- /dev/null +++ b/samples/p7-t3-智慧会议室/Properties/Resources.Designer.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// 项目资源(平台素材库)。在"项目资源"里添加或移除,不要手动修改这个文件。 +// +//------------------------------------------------------------------------------ + +namespace 会议室情景模式.Properties { + + /// + /// 项目资源:Properties.Resources.资源名 + /// + internal class Resources { + + /// 开关:开 + internal static System.Drawing.Bitmap switch_on { + get { return global::Sim.Host.ProjectResources.Image("switch_on.png"); } + } + + /// 开关:关 + internal static System.Drawing.Bitmap switch_off { + get { return global::Sim.Host.ProjectResources.Image("switch_off.png"); } + } + } +} diff --git a/samples/p7-t3-智慧会议室/checks.yaml b/samples/p7-t3-智慧会议室/checks.yaml new file mode 100644 index 0000000..cf7aaa8 --- /dev/null +++ b/samples/p7-t3-智慧会议室/checks.yaml @@ -0,0 +1,23 @@ +# 评分规则:书上评价表的功能项。每项单独启动一次程序,按步骤操作,核对结果。 +# 预约时刻用 clock 拨实训台的虚拟时钟。格式说明见 server/src/grading/checks.ts +- title: 能实时显示环境数据 + steps: + - select: { control: com_import, item: COM2 } + - board: { light: 640, temperature: 24, airQuality: 88 } + - expect: { control: txt_light, text: '640', within: 5s } + - expect: { control: txt_temperature, text: '24', within: 5s } + - expect: { control: txt_airQuality, text: '88', within: 5s } + +- title: 能根据预约时间控制智能设备 + steps: + - clock: '13:19:54' + - select: { control: com_import, item: COM2 } + - select: { control: com_mode, item: 讨论 } + - text: { control: txt_hour, value: '13' } + - text: { control: txt_minute, value: '20' } + - text: { control: txt_second, value: '00' } + - click: btn_reserve + - expect: { relay: 1, on: false, desc: 预约时间(13:20:00)没到时不开灯 } + - expect: { control: lbl_mode, contains: 讨论模式, within: 15s, desc: 到了预约时间开启讨论模式 } + - expect: { relay: 1, on: true, desc: 讨论模式打开灯光 } + - expect: { control: trb_curtain, value: 0, desc: 讨论模式打开窗帘 } diff --git a/samples/p7-t3-智慧会议室/meta.json b/samples/p7-t3-智慧会议室/meta.json new file mode 100644 index 0000000..a2c9837 --- /dev/null +++ b/samples/p7-t3-智慧会议室/meta.json @@ -0,0 +1,4 @@ +{ + "title": "项目7 任务3 实现智慧会议室", + "source": "教材书 p148–153,界面和代码取自配套参考工程;补上书上漏写的填充串口列表。预约时刻:在实训台的时钟上拨时间" +} diff --git a/server/src/app.ts b/server/src/app.ts index a1e618a..2021007 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,5 +1,6 @@ // HTTP 接口。除登录外都要先登录;/api/teacher/* 只有老师能用。 import type { Database } from 'bun:sqlite' +import { dirname } from 'node:path' import { Hono, type Context } from 'hono' import { bearerToken, login, logout, refresh, tokenUser, tooManyFailures, type Tokens, type User } from './auth' import { createClass, deleteClass, deleteStudent, importStudents, listClasses, listStudents, renameClass, resetPassword, type ImportRow } from './classes' @@ -71,6 +72,15 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ return c.json({}) }) + // 项目资源素材库(Properties.Resources 的图片、声音):设计器预览用 直接显示,不需要登录 + app.get('/resources/:file', async (c) => { + const name = c.req.param('file') + if (!/^[\w.-]+$/.test(name)) return c.notFound() + const file = Bun.file(`${dirname(config.runnerDll)}/Resources/gallery/${name}`) + if (!(await file.exists())) return c.notFound() + return new Response(file, { headers: { 'Cache-Control': 'public, max-age=3600' } }) + }) + // 其余接口都要先登录(请求头 Authorization: Bearer <令牌>) app.use('/api/*', async (c, next) => { const user = await tokenUser(db, bearerToken(c.req.header('authorization'))) diff --git a/server/src/board.ts b/server/src/board.ts index 19f64dc..b697d67 100644 --- a/server/src/board.ts +++ b/server/src/board.ts @@ -1,4 +1,5 @@ import { + clockWall, ANALOG_CHANNELS, ANALOG_INFO, type BoardState, @@ -7,6 +8,7 @@ import { type RunnerInput, type RunnerMessage, } from '@iot/shared' +import { config } from './config' export function defaultBoardState(): BoardState { return { @@ -16,6 +18,7 @@ export function defaultBoardState(): BoardState { zigbee: [false, false], ledText: '', camera: { on: false, x: 3, y: 1 }, + clock: null, } } @@ -29,6 +32,7 @@ export class Board { type: 'board', analog: ANALOG_CHANNELS.map((key) => this.state.analog[key]), digital: DIGITAL_CHANNELS.map((key) => this.state.digital[key]), + clock: this.state.clock, } } @@ -49,9 +53,31 @@ export class Board { this.state.digital[name] = value changed = true } + if (message.clock === null) { + changed ||= this.state.clock !== null + this.state.clock = null + } else if (message.clock && typeof message.clock === 'object') { + changed = this.setClock(message.clock.time, message.clock.speed) || changed + } return changed } + /** + * 拨虚拟时钟:time 是墙上时间(yyyy-MM-ddTHH:mm:ss,不给就从现在的时刻接着走),speed 是倍速(不给就保持原来的)。 + * 恢复真实时间是 clock = null。 + */ + setClock(time: string | undefined, speed: number | undefined) { + if (time !== undefined && (typeof time !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(time) || Number.isNaN(Date.parse(`${time}Z`)))) return false + if (speed !== undefined && (typeof speed !== 'number' || !Number.isFinite(speed) || speed < 1 || speed > 3600)) return false + const now = Date.now() + this.state.clock = { + wall: time ?? (this.state.clock ? clockWall(this.state.clock, now) : wallNow()), + realAt: now, + speed: speed ?? this.state.clock?.speed ?? 1, + } + return true + } + /** 学生程序控制了输出设备;返回 true 表示实训台状态有变化 */ applyDevice(message: Extract) { switch (`${message.device}/${message.action}`) { @@ -93,3 +119,11 @@ export class Board { this.state = defaultBoardState() } } + +/** 服务器所在时区(config.timeZone)现在的墙上时间 yyyy-MM-ddTHH:mm:ss */ +export function wallNow(now = new Date()) { + return new Intl.DateTimeFormat('sv-SE', { + timeZone: config.timeZone, + year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', + }).format(now).replace(' ', 'T') +} diff --git a/server/src/grading/checks.ts b/server/src/grading/checks.ts index 2503d0f..3c3c64d 100644 --- a/server/src/grading/checks.ts +++ b/server/src/grading/checks.ts @@ -17,13 +17,15 @@ export type Step = | { kind: 'edit'; control: string; row: number; column: number | string; value: string } /** 在日期时间选择器里选日期(和时间):2024-01-15 或 2024-01-15 08:30:00 */ | { kind: 'pick'; control: string; value: string } + /** 拨虚拟时钟到今天的某个时刻(HH:mm:ss),可以同时设倍速 */ + | { kind: 'clock'; time: string; speed?: number } | { kind: 'board'; analog: Partial>; digital: Partial> } | { kind: 'wait'; ms: number } /** desc:给学生看的期望说明,如"显示当前时间",不写就按期望自动生成 */ | { kind: 'expect'; expect: Expectation; within: number; desc?: string } export type Expectation = - /** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯) */ + /** 控件属性等于某个值:text → Text,ledText → LedText(LED 控件),enabled → Enabled,checked → Checked,on → IsOn(灯泡、报警灯),value → Value(滑动条) */ | { kind: 'prop'; control: string; prop: string; value: unknown } /** 控件文字包含某段文字;列表框是任意一个列表项包含 */ | { kind: 'contains'; control: string; text: string } @@ -38,6 +40,8 @@ export type Expectation = | { kind: 'image'; control: string } /** 摄像头在传画面(on)/ 已关闭(off),或云台比开始时朝某个方向转过 */ | { kind: 'camera'; state: 'on' | 'off' | 'up' | 'down' | 'left' | 'right' } + /** 程序用媒体播放器播放了这个文件(如 ring.mp3) */ + | { kind: 'media'; name: string } /** 程序用 Image.Save 保存了图片,路径包含这段文字(如 Camera/) */ | { kind: 'savedImage'; path: string } /** 数据表格里有(present)/ 没有一行同时满足这些"列标题=值" */ @@ -47,8 +51,9 @@ export type Expectation = /** 期望默认最多等多久(毫秒);依赖定时器的检查在规则里写 within */ const DEFAULT_WITHIN = 2000 -const PROP_KEYS: Record = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn' } +const PROP_KEYS: Record = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', on: 'IsOn', value: 'Value' } const STRING_PROPS = new Set(['Text', 'LedText']) +const NUMBER_PROPS = new Set(['Value']) /** "6s"、"500ms" 或毫秒数 */ function duration(value: unknown, where: string): number { @@ -78,6 +83,7 @@ function parseExpectation(raw: Record, where: string): Expectat if (!['on', 'off', 'up', 'down', 'left', 'right'].includes(state)) throw new Error(`${where}:camera 要写 on、off、up、down、left 或 right`) return { kind: 'camera', state: state as Extract['state'] } } + if ('media' in raw) return { kind: 'media', name: str(raw.media, where, '播放的文件名') } if ('savedImage' in raw) return { kind: 'savedImage', path: str(raw.savedImage, where, '保存的图片路径应包含的文字') } if ('relay' in raw) return { kind: 'relay', index: Number(raw.relay), on: bool(raw.on, where, 'on') } if ('zigbee' in raw) return { kind: 'zigbee', index: Number(raw.zigbee), on: bool(raw.on, where, 'on') } @@ -102,7 +108,10 @@ function parseExpectation(raw: Record, where: string): Expectat } if ('image' in raw) return { kind: 'image', control } for (const [key, prop] of Object.entries(PROP_KEYS)) { - if (key in raw) return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : bool(raw[key], where, key) } + if (key in raw) { + if (NUMBER_PROPS.has(prop) && typeof raw[key] !== 'number') throw new Error(`${where}:${key} 要写数字`) + return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : NUMBER_PROPS.has(prop) ? raw[key] : bool(raw[key], where, key) } + } } throw new Error(`${where}:不认识的期望 ${JSON.stringify(raw)}`) } @@ -140,6 +149,14 @@ function parseStep(raw: unknown, where: string): Step { return { kind, control: str(obj.control, where, '控件名 control'), value: String(obj.value ?? '') } case 'select': return { kind, control: str(obj.control, where, '控件名 control'), item: str(obj.item, where, '要选的项 item') } + case 'clock': { + // clock: 06:44:50 或 clock: { time: 06:44:50, speed: 60 } + const time = typeof value === 'string' ? value : str(obj.time, where, '时刻 time') + if (!/^\d{2}:\d{2}:\d{2}$/.test(time)) throw new Error(`${where}:时刻要写成 06:44:50(加引号)`) + const speed = typeof value === 'object' && obj.speed !== undefined ? Number(obj.speed) : undefined + if (speed !== undefined && !(speed >= 1 && speed <= 3600)) throw new Error(`${where}:倍速 speed 要在 1 到 3600 之间`) + return { kind, time, ...(speed === undefined ? {} : { speed }) } + } case 'pick': { const value = str(obj.value, where, '日期 value') if (!/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(value)) throw new Error(`${where}:日期要写成 2024-01-15 或 2024-01-15 08:30:00`) diff --git a/server/src/grading/driver.ts b/server/src/grading/driver.ts index 0751b58..fbcaa18 100644 --- a/server/src/grading/driver.ts +++ b/server/src/grading/driver.ts @@ -2,7 +2,7 @@ // 提示框自动点第一个按钮("确定"/"是"),免得程序一直等在 MessageBox.Show 上。 import type { Subprocess } from 'bun' import type { ControlNode, RunnerInput, RunnerMessage, UserAction } from '@iot/shared' -import { Board } from '../board' +import { Board, wallNow } from '../board' import { readLines } from '../runner-process' import { runCommand } from '../sandbox' import type { Step } from './checks' @@ -18,6 +18,8 @@ export class ProgramDriver { readonly messageBoxes: { text: string; caption: string }[] = [] /** 程序用 Image.Save 保存的图片路径 */ readonly savedImages: string[] = [] + /** 媒体播放器播放过的文件名 */ + readonly played: string[] = [] readonly exceptions: string[] = [] console = '' exited = false @@ -111,6 +113,9 @@ export class ProgramDriver { case 'imageSaved': this.savedImages.push(message.path) break + case 'media': + if (message.action === 'play' && message.name) this.played.push(message.name) + break case 'console': this.console += message.text break @@ -139,6 +144,12 @@ export class ProgramDriver { this.send({ type: 'action', id: node.id, ...action }) } + /** 拨虚拟时钟到今天的某个时刻(服务器所在时区) */ + setClock(time: string, speed?: number) { + this.board.setClock(`${wallNow().slice(0, 10)}T${time}`, speed) + this.send(this.board.runnerInputs()) + } + setBoard(step: BoardStep) { this.board.applyInput({ type: 'boardInput', analog: step.analog, digital: step.digital }) this.send(this.board.runnerInputs()) diff --git a/server/src/grading/grader.ts b/server/src/grading/grader.ts index 6c0857d..09100a3 100644 --- a/server/src/grading/grader.ts +++ b/server/src/grading/grader.ts @@ -141,6 +141,10 @@ async function runStep(driver: ProgramDriver, step: Step) { driver.setBoard(step) await driver.settle() break + case 'clock': + driver.setClock(step.time, step.speed) + await driver.settle() + break case 'wait': await Bun.sleep(step.ms) break @@ -227,6 +231,8 @@ function satisfied(driver: ProgramDriver, e: Exclude path.includes(e.path)) + case 'media': + return driver.played.includes(e.name) case 'row': { const node = driver.find(e.control) return node !== undefined && rowMatches(node, e.cells) === e.present @@ -244,6 +250,7 @@ function describeFailure(driver: ProgramDriver, e: Exclude `"${p}"`).join('、') return `期望${prefix}${desc ?? `保存图片到"${e.path}"`},${saved ? `实际保存的是 ${saved}` : '实际没有保存图片'}` } + case 'media': + return `期望${prefix}${desc ?? `播放 ${e.name}`},${driver.played.length ? `实际播放的是 ${driver.played.join('、')}` : '实际没有播放声音'}` case 'row': { const node = need(driver, e.control) const wanted = Object.entries(e.cells).map(([k, v]) => `${k}=${v}`).join(',') diff --git a/server/test/grading.test.ts b/server/test/grading.test.ts index c808a76..d91a3a6 100644 --- a/server/test/grading.test.ts +++ b/server/test/grading.test.ts @@ -35,7 +35,7 @@ async function prepare(id: string, name: string, edit?: { file: string; from: st describe('参考答案全部通过', async () => { const ids = (await Array.fromAsync(new Bun.Glob('*/checks.yaml').scan({ cwd: SAMPLES }))).map((p) => p.split('/')[0]).sort() - test('每个示例都有评分规则', () => expect(ids.length).toBe(15)) + test('每个示例都有评分规则', () => expect(ids.length).toBe(18)) for (const id of ids) { test(id, async () => { const result = await grade(await prepare(id, id), await checksOf(id), id) @@ -110,6 +110,22 @@ describe('改坏的代码', () => { expect(result.results[0].message).toStartWith('程序出错:System.ArgumentNullException') }, 60000) + test('情景控制:照书上把手动开关灯的判断写反,点了灯不亮', async () => { + const id = 'p7-t1-智慧教室' + const dir = await prepare(id, 'lamp-reversed', { file: '教室情景模式.cs', from: ' if (IsLampOpen)\n', to: ' if (!IsLampOpen)\n' }) + const checks = [ + ...(await checksOf(id)), + ...parseChecks(` +- title: 能手动开关灯 + steps: + - click: pic_lightsStatus + - expect: { control: lamp, on: true, desc: 点开关后灯亮 } +`), + ] + const result = await grade(dir, checks, id) + expect(result.results.at(-1)).toMatchObject({ passed: false, message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的' }) + }, 60000) + test('关闭时忘了挂起线程', async () => { const id = 'p5-t2-烟雾' const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' }) diff --git a/server/test/runner.test.ts b/server/test/runner.test.ts index cfe008d..b2e66b2 100644 --- a/server/test/runner.test.ts +++ b/server/test/runner.test.ts @@ -4,6 +4,7 @@ import { afterAll, afterEach, describe, expect, test } from 'bun:test' import { cp, mkdir, rm } from 'node:fs/promises' import { resolve } from 'node:path' import type { ControlNode, RunnerInput, RunnerMessage } from '@iot/shared' +import { wallNow } from '../src/board' import { compileService } from '../src/compile-service' import { config, ROOT } from '../src/config' import { cpuLimitMessage, runCommand } from '../src/sandbox' @@ -332,7 +333,7 @@ test('项目4 任务2:打开摄像机显示画面,转动后画面变化, const center = imageOf(await first) expect(center).toMatchObject({ width: 640, height: 480 }) // 图片数据先于(或随)属性变化发给浏览器,同一张图只发一次 - const sent = runner.messages.filter((m) => m.type === 'image' && m.key === center.key) + const sent = runner.messages.filter((m) => m.type === 'blob' && m.key === center.key) expect(sent.length).toBe(1) expect(sent[0]).toMatchObject({ mime: 'image/jpeg' }) @@ -630,6 +631,56 @@ test('数据库:照书上的写法建库建表、插入、查询、SqlCommandB } }, 30000) +test('虚拟时钟:拨时间、倍速后 DateTime.Now / Today 跟着走,恢复后是真实时间', async () => { + const { runner } = await runMain( + 'clock', + ` + for (int i = 0; i < 40; i++) + { + Console.WriteLine("now=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " today=" + DateTime.Today.ToString("yyyy-MM-dd")); + System.Threading.Thread.Sleep(100); + }`, + ) + expect((await runner.waitFor('compiled')).ok).toBe(true) + await runner.waitFor('console') + runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: { wall: '2024-01-16T06:44:58', realAt: Date.now(), speed: 60 } }) + // 60 倍速:真实的 1 秒是虚拟的 1 分钟 + await runner.next('console', (m) => /now=2024-01-16 06:4[6-9]/.test(m.text), 5000) + runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: null }) + const real = await runner.next('console', (m) => !m.text.includes('2024-01-16'), 3000) + // 学生程序按服务器配置的时区(config.timeZone)计时,CI 机器本身是 UTC + expect(real.text).toContain(`today=${wallNow().slice(0, 10)}`) + expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true) +}, 30000) + +test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => { + const { runner } = await runMain( + 'trackbar', + ` + var form = new Form(); + var bar = new TrackBar { Name = "bar", Minimum = 18, Maximum = 32 }; + bar.ValueChanged += (s, e) => Console.WriteLine("changed=" + bar.Value); + bar.Scroll += (s, e) => Console.WriteLine("scroll=" + bar.Value); + form.Load += (s, e) => + { + try { bar.Value = 40; } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("error=" + ex.Message); } + }; + form.Controls.Add(bar); + Application.Run(form);`, + 'using System.Windows.Forms;', + ) + const { form } = await runner.waitFor('formShown') + const bar = findByName(form, 'bar') + expect(bar.props).toMatchObject({ Minimum: 18, Maximum: 32, Value: 18 }) + runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '26' }) + await runner.waitFor('console', (m) => m.text === 'scroll=26\n') + runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '99' }) + await runner.waitFor('console', (m) => m.text === 'scroll=32\n') + const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('') + expect(output).toContain("error=“40”的值对于“Value”无效。") + expect(output).toContain('changed=26\nscroll=26\nchanged=32\nscroll=32\n') +}, 30000) + test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => { process.env.SIM_TEST_SECRET = '不该被看到' const { runner } = await runMain( diff --git a/shared/protocol.ts b/shared/protocol.ts index 562045a..eb6c530 100644 --- a/shared/protocol.ts +++ b/shared/protocol.ts @@ -64,6 +64,23 @@ export interface BoardState { ledText: string /** 网络摄像机:是否在传画面,云台位置(左右 0–6、上下 0–2,开始时在正中) */ camera: { on: boolean; x: number; y: number } + /** 虚拟时钟;null 表示真实时间 */ + clock: VirtualClock | null +} + +/** + * 虚拟时钟:从真实时刻 realAt(毫秒时间戳)起,墙上时间从 wall(本地时间 yyyy-MM-ddTHH:mm:ss)开始按 speed 倍速走。 + * 学生程序的 DateTime.Now 跟着它(教材项目 7 按时刻切换情景模式)。 + */ +export interface VirtualClock { + wall: string + realAt: number + speed: number +} + +/** 虚拟时钟现在的墙上时间(yyyy-MM-ddTHH:mm:ss);按 UTC 做加减,只用来算时刻,不涉及时区 */ +export function clockWall(clock: VirtualClock, now = Date.now()) { + return new Date(Date.parse(`${clock.wall}Z`) + (now - clock.realAt) * clock.speed).toISOString().slice(0, 19) } export type AnalogKey = keyof BoardState['analog'] @@ -94,8 +111,10 @@ export type RunnerMessage = | { type: 'controlAdded'; parentId: string; index: number; node: ControlNode } | { type: 'controlRemoved'; id: string } | { type: 'messageBox'; requestId: number; text: string; caption: string; buttons: string; icon: string } - /** 图片数据(base64),同一张图只发一次;控件属性和 imageSaved 里用 key 引用 */ - | { type: 'image'; key: string; mime: string; data: string } + /** 图片、声音数据(base64),同一份数据只发一次;控件属性、imageSaved、media 里用 key 引用 */ + | { type: 'blob'; key: string; mime: string; data: string } + /** 媒体播放器(教材项目 7 的广播):开始 / 停止播放;name 是文件名,id 是播放器控件 */ + | { type: 'media'; id: string; action: 'play' | 'stop'; key?: string; name?: string } /** 学生程序用 Image.Save 保存了图片;path 相对程序所在目录,如 Camera/20241022_10 30 00.jpg */ | { type: 'imageSaved'; path: string; image: string } /** 学生程序操作了实训台输出:led/text、adam4150/do、zigbee/relay、camera/on、camera/pan,data 见 DeviceCtrlHelper.cs、IPCameraHelper.cs */ @@ -120,6 +139,8 @@ export type UserAction = | { action: 'cellEdit'; row: number; column: number; value: string } /** DateTimePicker 选了日期或时间:value 是本地时间 yyyy-MM-ddTHH:mm:ss */ | { action: 'pick'; value: string } + /** TrackBar 拖动滑块、点轨道或按方向键:value 是新值 */ + | { action: 'scroll'; value: string } | { action: 'mouseDown'; button: 'left' | 'right'; x: number; y: number } | { action: 'mouseUp'; button: 'left' | 'right'; x: number; y: number } | { action: 'close' } @@ -128,15 +149,16 @@ export type UserAction = export type RunnerInput = | ({ type: 'action'; id: string } & UserAction) | { type: 'reply'; requestId: number; result: unknown } - /** 实训台传感器读数,按通道号排列(见 ANALOG_CHANNELS、DIGITAL_CHANNELS) */ - | { type: 'board'; analog: number[]; digital: boolean[] } + /** 实训台传感器读数,按通道号排列(见 ANALOG_CHANNELS、DIGITAL_CHANNELS),以及虚拟时钟(不带时表示不变) */ + | { type: 'board'; analog: number[]; digital: boolean[]; clock?: VirtualClock | null } | { type: 'stop' } /** 浏览器发给服务器的消息 */ export type ClientMessage = | { type: 'run'; files: SourceFile[] } /** 调节实训台环境:拖动滑块、触发传感器 */ - | { type: 'boardInput'; analog?: Partial; digital?: Partial } + /** clock:拨到某个时刻(time:yyyy-MM-ddTHH:mm:ss)、改倍速,或 null 恢复真实时间 */ + | { type: 'boardInput'; analog?: Partial; digital?: Partial; clock?: { time?: string; speed?: number } | null } /** 实训台复位:关掉所有灯和继电器、清空 LED 屏,传感器恢复默认 */ | { type: 'boardReset' } | Exclude diff --git a/web/src/designer/FormDesigner.vue b/web/src/designer/FormDesigner.vue index 925c59a..a6f7c3d 100644 --- a/web/src/designer/FormDesigner.vue +++ b/web/src/designer/FormDesigner.vue @@ -31,6 +31,8 @@ const props = defineProps<{ codeText: string canUndo: boolean canRedo: boolean + /** 项目资源(图片属性的选项) */ + resources?: { names: string[]; prefix: string } }>() const emit = defineEmits<{ @@ -42,6 +44,8 @@ const emit = defineEmits<{ openDesignerFile: [] undo: [] redo: [] + /** 属性窗口里要添加项目资源 */ + addResource: [] }>() /** Ctrl+Z 撤销,Ctrl+Y / Ctrl+Shift+Z 重做;在属性窗口的输入框里打字时不拦截(输入框有自己的撤销) */ @@ -492,11 +496,13 @@ const noop = () => {} class="properties" :doc="doc" :selected="selected" + :resources="resources" @set-prop="onSetProp" @set-items="onSetItems" @rename="onRename" @open-event="openEvent" @clear-event="clearEvent" + @add-resource="emit('addResource')" /> diff --git a/web/src/designer/PropertyGrid.vue b/web/src/designer/PropertyGrid.vue index f71c39e..526afdf 100644 --- a/web/src/designer/PropertyGrid.vue +++ b/web/src/designer/PropertyGrid.vue @@ -4,12 +4,15 @@ import { computed, ref, watch } from 'vue' import { type ControlDef, FORM_DEF, type PropDef, controlDef } from './catalog' import type { DesignerDocument } from './document' import { checkName, getEventHandler, getItems, getPropText, typeOf } from './edit' +import { resourceNameOf } from './resources' import { NAMED_COLORS, hexToRgb, parseValue, rgb, type Value } from './values' const props = defineProps<{ doc: DesignerDocument /** '' 表示窗体本身 */ selected: string + /** 项目资源(图片属性从这里选)和引用资源的前缀 global::命名空间.Properties.Resources. */ + resources?: { names: string[]; prefix: string } }>() const emit = defineEmits<{ @@ -19,6 +22,8 @@ const emit = defineEmits<{ /** 双击事件或点"生成":绑定(必要时生成)事件处理方法并跳到代码 */ openEvent: [target: string, event: string, handler?: string] clearEvent: [target: string, event: string] + /** 图片属性里选了"添加资源…":打开项目资源对话框 */ + addResource: [] }>() const tab = ref<'props' | 'events'>('props') @@ -37,6 +42,22 @@ function set(prop: PropDef, value: Value) { emit('setProp', props.selected, prop.name, isDefault ? null : value) } +/** 图片属性:项目资源里的名字,没有设置时为空 */ +function imageOf(prop: PropDef) { + const text = getPropText(props.doc, props.selected, prop.name) + return text === undefined ? '' : resourceNameOf(text) ?? text +} + +function setImage(prop: PropDef, choice: string, select: HTMLSelectElement) { + if (choice === '+') { + select.value = imageOf(prop) + emit('addResource') + return + } + if (choice === '') reset(prop) + else emit('setProp', props.selected, prop.name, { kind: 'raw', text: `${props.resources?.prefix ?? ''}${choice}` }) +} + function reset(prop: PropDef) { emit('setProp', props.selected, prop.name, null) } @@ -152,7 +173,7 @@ function renameHandler(event: string, value: string) {
- {{ raw(prop) }} + {{ raw(prop) }} @@ -213,6 +234,14 @@ function renameHandler(event: string, value: string) {