From eedf1aa2fb9e1b528a9e1bcb077a94de2c3ccf1f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Fri, 23 Oct 2026 14:11:05 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=207=20=E6=83=85=E6=99=AF?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=EF=BC=9A=E8=99=9A=E6=8B=9F=E6=97=B6=E9=92=9F?= =?UTF-8?q?=E3=80=81=E9=A1=B9=E7=9B=AE=E8=B5=84=E6=BA=90=E3=80=81Windows?= =?UTF-8?q?=20Media=20Player=E3=80=81TrackBar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把 DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤 - 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选, 平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources 指向素材库;未添加资源时给中文提示 - Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音, 实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob) - TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器" - 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表) - 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行 - 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类 Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +- docs/设计方案.md | 13 +- .../DeviceCtrlLibrary/TrainingBoard.cs | 3 +- runtime/Sim.Runner/ApiPolicy.cs | 4 +- runtime/Sim.Runner/ClockRewriter.cs | 69 ++ runtime/Sim.Runner/CompileHints.cs | 2 + runtime/Sim.Runner/StudentCompiler.cs | 40 +- runtime/Sim.Runner/StudentProgramHost.cs | 3 + .../Forms/AxWindowsMediaPlayer.cs | 166 +++++ runtime/Sim.WinForms/Forms/DateTimePicker.cs | 2 +- runtime/Sim.WinForms/Forms/TrackBar.cs | 123 ++++ runtime/Sim.WinForms/Host/ProjectResources.cs | 20 + runtime/Sim.WinForms/Host/SimClock.cs | 46 ++ runtime/Sim.WinForms/Host/SimHost.cs | 14 +- runtime/Sim.WinForms/Resources/README.md | 7 + .../Sim.WinForms/Resources/gallery/index.json | 5 + .../Sim.WinForms/Resources/gallery/ring.mp3 | Bin 0 -> 24493 bytes .../Resources/gallery/switch_off.png | Bin 0 -> 1329 bytes .../Resources/gallery/switch_on.png | Bin 0 -> 1384 bytes runtime/Sim.WinForms/Resources/switch_off.svg | 5 + runtime/Sim.WinForms/Resources/switch_on.svg | 5 + runtime/Sim.WinForms/Sim.WinForms.csproj | 4 + samples/p7-t1-智慧教室/Program.cs | 22 + .../Properties/Resources.Designer.cs | 24 + samples/p7-t1-智慧教室/checks.yaml | 25 + samples/p7-t1-智慧教室/meta.json | 4 + .../p7-t1-智慧教室/教室情景模式.Designer.cs | 350 ++++++++++ samples/p7-t1-智慧教室/教室情景模式.cs | 126 ++++ samples/p7-t2-智慧寝室/Form1.Designer.cs | 388 +++++++++++ samples/p7-t2-智慧寝室/Form1.cs | 163 +++++ samples/p7-t2-智慧寝室/Program.cs | 22 + .../Properties/Resources.Designer.cs | 29 + samples/p7-t2-智慧寝室/checks.yaml | 31 + samples/p7-t2-智慧寝室/meta.json | 4 + samples/p7-t3-智慧会议室/Form1.Designer.cs | 611 ++++++++++++++++++ samples/p7-t3-智慧会议室/Form1.cs | 200 ++++++ samples/p7-t3-智慧会议室/Program.cs | 22 + .../Properties/Resources.Designer.cs | 24 + samples/p7-t3-智慧会议室/checks.yaml | 23 + samples/p7-t3-智慧会议室/meta.json | 4 + server/src/app.ts | 10 + server/src/board.ts | 34 + server/src/grading/checks.ts | 23 +- server/src/grading/driver.ts | 13 +- server/src/grading/grader.ts | 9 + server/test/grading.test.ts | 18 +- server/test/runner.test.ts | 53 +- shared/protocol.ts | 32 +- web/src/designer/FormDesigner.vue | 6 + web/src/designer/PropertyGrid.vue | 31 +- web/src/designer/ResourcesDialog.vue | 128 ++++ web/src/designer/catalog.ts | 45 +- web/src/designer/preview.ts | 8 + web/src/designer/resources.ts | 81 +++ web/src/designer/templates.test.ts | 8 +- web/src/designer/templates.ts | 8 +- web/src/grading/screenshots.ts | 3 +- web/src/pages/WorkbenchPage.vue | 35 +- web/src/run/RunArea.vue | 2 +- web/src/run/session.ts | 37 +- web/src/scene/TrainingBoard.vue | 159 ++++- web/src/winforms/ControlView.vue | 15 +- web/src/winforms/TrackBarControl.vue | 121 ++++ web/src/winforms/images.ts | 2 + web/src/winforms/winforms.css | 90 +++ web/vite.config.ts | 1 + 66 files changed, 3538 insertions(+), 43 deletions(-) create mode 100644 runtime/Sim.Runner/ClockRewriter.cs create mode 100644 runtime/Sim.WinForms/Forms/AxWindowsMediaPlayer.cs create mode 100644 runtime/Sim.WinForms/Forms/TrackBar.cs create mode 100644 runtime/Sim.WinForms/Host/ProjectResources.cs create mode 100644 runtime/Sim.WinForms/Host/SimClock.cs create mode 100644 runtime/Sim.WinForms/Resources/README.md create mode 100644 runtime/Sim.WinForms/Resources/gallery/index.json create mode 100644 runtime/Sim.WinForms/Resources/gallery/ring.mp3 create mode 100644 runtime/Sim.WinForms/Resources/gallery/switch_off.png create mode 100644 runtime/Sim.WinForms/Resources/gallery/switch_on.png create mode 100644 runtime/Sim.WinForms/Resources/switch_off.svg create mode 100644 runtime/Sim.WinForms/Resources/switch_on.svg create mode 100644 samples/p7-t1-智慧教室/Program.cs create mode 100644 samples/p7-t1-智慧教室/Properties/Resources.Designer.cs create mode 100644 samples/p7-t1-智慧教室/checks.yaml create mode 100644 samples/p7-t1-智慧教室/meta.json create mode 100644 samples/p7-t1-智慧教室/教室情景模式.Designer.cs create mode 100644 samples/p7-t1-智慧教室/教室情景模式.cs create mode 100644 samples/p7-t2-智慧寝室/Form1.Designer.cs create mode 100644 samples/p7-t2-智慧寝室/Form1.cs create mode 100644 samples/p7-t2-智慧寝室/Program.cs create mode 100644 samples/p7-t2-智慧寝室/Properties/Resources.Designer.cs create mode 100644 samples/p7-t2-智慧寝室/checks.yaml create mode 100644 samples/p7-t2-智慧寝室/meta.json create mode 100644 samples/p7-t3-智慧会议室/Form1.Designer.cs create mode 100644 samples/p7-t3-智慧会议室/Form1.cs create mode 100644 samples/p7-t3-智慧会议室/Program.cs create mode 100644 samples/p7-t3-智慧会议室/Properties/Resources.Designer.cs create mode 100644 samples/p7-t3-智慧会议室/checks.yaml create mode 100644 samples/p7-t3-智慧会议室/meta.json create mode 100644 web/src/designer/ResourcesDialog.vue create mode 100644 web/src/designer/resources.ts create mode 100644 web/src/winforms/TrackBarControl.vue 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 0000000000000000000000000000000000000000..65b921b49101b5d7727488558ceb537511982512 GIT binary patch literal 24493 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 ZP(Vfz-NV+-Str6W_Ri2M!T&U%{{TjzCK&(# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..08115321e222419847cf1c06566e13dcb78f03b5 GIT binary patch literal 1329 zcmV-11ZZ*X0Hq%UwPjtgqm-hwNEBF9>bB|zwNiP70!gS7JATCW*z@w-+eNS&CozvyN!7nJ zi#g}qGv}Q9pL6el|2f3V3@%^0eMr;f5rN2nif% z@j>^zQcMa>^ci|YhC*Re2oc$wfV-iT@`IpdyN<6(r5XY*SWEg>6BEar)wXLAxH@&~ zD1z&TuE&zeL|-(zB{BDe=lN8t)rRYOjXRXEiPK^>8W`Gfe}4 zm}!tnr%3k4(e-5pxvodGW>F}XY1(UwvMI1_hf=BBMDf|jr{Di>O#)Y^ZXHGAr)`;Z zEE0*J3BlOTQAW~3J)J70$S;c4jv%U zA7|{%(ani?6-?7W*CYK+Uz`Kb0F+`fnN0M-GUs1Jn_d`6r)U>ovoVlN#FP?~0O8Bm zZXXhw=yaP*I<+CELI^U$DQ4z!U2;v+*s(oBe>_I36|hvTQz(`JNDU?#N)2?`FBZ#4 zsTj$07qL>QQYxsnsn2Y{zBS&QYG_@&G6Wv(M(c))|Hlz_VO|R;#t{w<3|S zA#7(y7=`LO4E|Lk>0TT&4Wv??-~p<&dPfJKX@Ud$_5dKIa06fpz@XzRae%HH{62UT++JOjdxm@jztCZ>_wYm-0EsG$K z>*{3SI}$Ay4g&GJ=X#w@sMf3vC(sV04Xe>a2*Ge_fW=Y;fOKjQr4-GkO<&AFDab7> zuIq0;U+PIA+w(#Y{HA?!=J+2HX?f49S16V@bRa3EC=@HJ60#_jm)NuG4femai-Y_3 zvVB{cxxeywzTd;s%i(z*LaN%?(;wVhJ_n@wqFSq208QH=zgT{4r}@Pa_B!{X>BqB7 zKc2;LUFueYyAS>(w@?6}ZZ(5yAkM6TfcN>pq0212_45C&*g%`Y|Kd~Vxr z+%$D&=5m;ZzNRhqLbYZwGdJH;U#?UsSE_5|wYt^ygSXLW+BBN>y1s13!FD{WUzZo* z`AIx6am)#%I$122n!fL&l;ZyM6BY`k7t%>7#X_OT{pqc_2l>8FsZ?nQIFZd}U08LU z{`ULZ;{tVQc<7(&v}qb-hEpUHtFO~_3dPF0wAl>Ujzh6nZX(pD% z_)bWLt{ZwJzC6%u(G=MOp65}m)*6oMTA-a6|M2*)EA{J+ls9kQ)GbGy6Iy5zNEmt~ z2!+FDNDFPrcoxXO4_YnT@%#{qnw0WFzrn>eZC2u?L;aPh8wZ349YL`T)X3(9yc!~p nU?CJ}cg~!7@BY?&^FPAhs;yf!NNeia00000NkvXXu0mjf1WthP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e8355e0d660a4b2542324395a6898094acc3c1b GIT binary patch literal 1384 zcmV-u1(*7XP)1y#diI?5R7x*ggo-^?X)PF0@bVxE2~`t8 zV+=tM8y`$0w$ulC@WsRgs34_S4yjaIXrb1gcF(1| zJL3b;^mc%v9OL&iJM;Z#zit6uHE_lDr9soQ>wtPG2^+Slq+!@dtV<$zJQS|-{iK&vQY+;Mp`Lf$^tRt>zDSLy zPB0MNQGK%zqQMNApM&q@=*?K9Xpn80V**-QvH zz2_~%?hMTe7iDAR1JYvaHJi90QgK22@8hl1wQa%G9{V3dmQUG$5CT`bvr@Z4ob5JVn^B`8j@=1$kHT&VntJh2{WoN5Nw1uUN}Gdo}=-xn(TOyE>zgCHYlT z7C%|688nr`UnrH5vkX%PGX-s!yP6;4Z9Lxg9IyWI0gtpkjpHX+JM-=ljv)*-&8tD2 z|6->kF#VzpoIPd%49f_Do-L6ob#XNg9-wpgS(wA1l>>q5WJT!Nt@57$Yxjr(V z>cU$uTHJ*|qer#TLJMt5CAxi2rC?&2;39JnfG>?;rdXOPFjdeULV% z1Ep0)5k13wBin7NIFUXz(pIjY(t>WO(FeSgKWXk2nl`6{<(U1h-ta@SGw>XLG7(85Qd%Jwsybm$8SNT7|EM{Iox5(2K`*F(XjOy9>7P zY=C=--cH1sx~7N7C}9Y6BW-ZleZ+NyF> zdFrM>BrxG9?H&G;roON7w2vVqU-tjN=8o5hdkz3OGQf|6ZS=VV97%LCtUX$Xj?$hu zHmQ()H%@^Sp)$8@N(OfKH1YANuK-A%NI1&V&;Ge5_RUBsVVd@_+!^6wGxyTp#+ESI zdd-vEn1AgB@%IfJr0(z*JniE;F3xeMhki<9wT)>5;ID??yn=hmzZ!PjHCC==NmHJ0 z&VK}j;?urw$uq;u56?r0%*EX9o+h3?`U;-*@zXwut|3P#Rej{qPdZ^-IURlLt_RS1 zdw5nTWQDWy^L3?tIV+2ovLwH1OgY^$@CyyS&2%KYfYar))9obCtdFg4eE)3x1ncEH z?^>b-4OUR*1`F-55j4kBvI;-tkvf%f+>}2gm48qB`lfxO$0yBQblaUdB45{Hh + + 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) {