Files
IOT_SIM/docs/教材摘要/2-项目3后半到项目6前半.md
xuyueandClaude Opus 5.5 98be8331bc 第 0 阶段:教材项目 2 任务 1 在网页中原样运行
- runtime:仿真 WinForms 子集(Form、Button、Label、TextBox、ComboBox、
  GroupBox、Timer、MessageBox、界面线程消息循环、跨线程检查)、
  教材设备类库(DeviceCtrlHelper、LED 控件)、运行器(常驻编译服务 + 运行模式)
- server:Bun + Hono,WebSocket 会话,每次运行一个进程,消息转发
- web:Vue + Naive UI,WinForms 风格窗体渲染、MessageBox、实训台 LED 屏
- 端到端测试与 100 个程序并发的压力测试
- 教材逐任务摘要(docs/教材摘要)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:15:14 +08:00

73 KiB
Raw Permalink Blame History

《物联网编程与应用 第2版》摘要 第二部分(PDF 66–125)

页码对照:PDF 66–114 对应书页 63–111(PDF = 书页 + 3)。书页 112(项目5 后的空白页)似乎没有扫描进来,所以从 PDF 115 起是 PDF = 书页 + 2(PDF 115 = 书 113 项目6 首页,PDF 125 = 书 123)。

范围开头:从 项目3 任务2【知识提炼】 中间开始,具体是知识点"1. RadioButton 单选按钮"的示例代码后半段(PDF 66 / 书 63)。项目3 的项目概述、任务1 全部、任务2 的任务描述和任务实施都在前一段范围里。 范围结尾:停在 项目6 任务2【任务实施】第15步 btnDelete_Click 的 catch 块里面(PDF 125 / 书 123)。


项目3 智慧校园系统——智慧路灯控制模块(续)

任务2【知识提炼】(续)PDF 66–73 / 书 63–70

1. RadioButton 单选按钮(示例后半段,PDF 66–67 / 书 63–64)

  • 第3步:选中窗体,在属性窗口点"事件"按钮,找到 Load 事件,双击进入代码窗口:
    private void Form1_Load(object sender, EventArgs e)
    {
        this.radioButton1.Checked = true;//设置radioButton1默认选中状态
    }
    //设置字体颜色方法
    private void type_method(String value)
    {
        switch (value)
        {
        case "Black":  this.label1.ForeColor = Color.Black;  this.label1.Text = "字体颜色[黑色]"; break;
        case "Red":    this.label1.ForeColor = Color.Red;    this.label1.Text = "字体颜色[红色]"; break;
        case "Blue":   this.label1.ForeColor = Color.Blue;   this.label1.Text = "字体颜色[蓝色]"; break;
        case "Yellow": this.label1.ForeColor = Color.Yellow; this.label1.Text = "字体颜色[黄色]"; break;
        default: break;
        }
    }
    
  • 第4步:选中4个 RadioButton,找到"Click"事件写代码。书里步骤写的是 Click,代码实际用的是 radioButtonN_CheckedChanged:radioButton1→type_method("Black"),2→"Red",3→"Blue",4→"Yellow"。
  • 第5步:运行效果见图3-7/3-8,窗体标题"设置字体颜色"。左侧4个单选按钮 黑色/红色/蓝色/黄色,右侧 label1 显示"字体颜色[黑色]"。

2. 控件:CheckBox 复选框(PDF 67–69 / 书 64–66)

  • 说明:CheckBox 提供一个选项(true/false 或 是/否)。它和 RadioButton 的区别是 CheckBox 可以多选组合,RadioButton 只能在互斥选项里选一个。Appearance 决定显示成常规外观还是按钮样式;ThreeState 决定是否支持三种状态;Checked 获取或设置是否选中。

  • 表3-6 常见属性:Appearance(外观)、AutoCheck(单击时 Checked/CheckState 和外观是否自动改变)、AutoSize、BackColor、BackgroundImage、CheckAlign(复选标记的对齐方式)、Checked、CheckState、Font、FontHeight、ForeColor、Image、ImageAlign、Location、Parent、ResizeRedraw、Text、TextAlign、ThreeState(是否允许3种状态)、Visible。

  • 表3-7 常见事件:CheckedChanged(Checked 属性的值改变时发生)、Click(单击控件时发生)。

  • 示例"复选框CheckBox":保存在 D:\物联网编程与应用\项目3\任务2\,窗体见图3-9"字体状态设置"。

    表3-8 "字体状态设置"窗体控件

    控件名称 控件类型 属性名 属性值 控件功能
    Form1 窗体 Text 字体状态设置
    checkBox_Bold 复选框 Text 字体加粗
    checkBox_Italic 复选框 Text 字体倾斜
    checkBox_Underline 复选框 Text 字体下画线
    checkBox_Strikeout 复选框 Text 字体删除线
    label1 标签 Text 普通:Hello_CheckBox

    第3步:给4个 CheckBox 的"Click"事件加代码(代码里实际是 CheckedChanged,控件名是 checkBox1–4,和表3-8 的 checkBox_Bold 等名字对不上):

    private void checkBox1_CheckedChanged(object sender, EventArgs e)
    { type_method(FontStyle.Bold, checkBox1.Checked); }
    private void checkBox2_CheckedChanged(...) { type_method(FontStyle.Italic, checkBox2.Checked); }
    private void checkBox3_CheckedChanged(...) { type_method(FontStyle.Underline, checkBox3.Checked); }
    private void checkBox4_CheckedChanged(...) { type_method(FontStyle.Strikeout, checkBox4.Checked); }
    private void type_method(FontStyle fs, bool status)//设置字体的[状态]方法
    {
        if (status)
            this.label1.Font = new Font(label1.Font, label1.Font.Style | fs);
        else
            this.label1.Font = new Font(label1.Font, label1.Font.Style & ~fs);
    }
    

    运行效果见图3-10(加粗)和图3-11(加粗+下画线)。模拟器需要支持 new Font(Font, FontStyle)、FontStyle 的位运算 | & ~,以及 Strikeout/Underline 的渲染。

3. 数组(PDF 69–73 / 书 66–70)

  • (1) 定义:相同类型的元素按顺序排列的集合;有数组名、下标、元素;C# 数组下标从0开始;声明时 [] 必须写在类型后面(int[] table;,不能写 int table[]);数组大小不属于类型的一部分(int[] numbers; numbers = new int[10]; numbers = new int[20];)。
  • (2) 声明:一维 int[] numbers;、多维 string[,] names;、交错 string[][] scores;。声明之后必须实例化。
  • (3) 创建:new int[5]、new string[5,4];交错数组 byte[][] scores = new byte[5][]; for(...) scores[x] = new byte[4];;三维 int[,,] buttons = new int[4,5,3];;混合写法 int[][,,][,] numbers;。 示例 Form1_Click:建交错数组 scores[i] = new byte[i + 3],然后 MessageBox.Show("Length of row " + i + " is " + scores[i].Length);。
  • (4) 初始化:未初始化时元素取类型默认值,字段数组默认是 null。
    • 一维:new int[5] {1,2,3,4,5}、new int[] {...}、int[] numbers = {1,2,3,4,5};、string[] names = {"Matt","Joanne","Robert"};
    • 多维:new int[3,2] {{1,2},{3,4},{5,6}}、string[,] siblings = {{"Mike","Amy"},{"Mary","Albert"}};
    • 交错:new int[2][] { new int[] {2,3,4}, new int[] {5,6,7,8,9} },可以省略第一维大小,也可以省略 new。交错数组的元素没有初始化语法。
  • (5) 访问元素:numbers[4] = 5;、numbers[1, 1] = 5;、numbers[0][0] = 100; numbers[1][1] = 200;
  • (6) 数组是对象:基类是 System.Array;int LengthOfNumbers = numbers.Length;;还有排序、搜索、复制等方法。
  • (7) 用 foreach 遍历数组:int[] numbers = {4,5,6,1,2,3,-2,-1,0}; foreach (int i in numbers) MessageBox.Show(i.ToString());。多维数组同样可以 foreach,new int[3,2]{{9,99},{8,88},{7,77}} 输出 9 99 8 88 7 77。多维数组用嵌套 for 能控制得更细。

项目3【能力拓展】PDF 73–74 / 书 70–71

(这一节在项目3 任务2 后面,是项目3 的最后一个能力拓展。)

边想边做:

制作"流水路灯控制"界面,如图3-12所示。 当程序运行时,在文本框中输入1和0组成的一组数(中间用","分隔),单击"开"按钮,则按1、0数值开启和关闭灯,1表示开,0表示关,每隔2s切换,实现流水灯。运行效果如图3-13所示。

  • 参考界面(图3-12/3-13):窗体标题"ZigBee路灯"。左边是一个大灯泡(Lamp 控件,暗/亮两种状态)。右边 GroupBox"配置",里面有标签"路 灯"、串口下拉框(图中 COM3)、按钮"打开"(打开后变成"关闭")。下方有一个 TextBox(示例输入 1,0,1,1,1,0,0,1)和两个 RadioButton"开""关"。
  • 提示1:用 String.Split 拆分字符串。例:
    String sstring = "ab+cd+ef+gh";
    String [] spstring = sstring.Split(new char[] { '+' });
    
    书上写"spstring[0]="ab", spstring[1]="cd", spstring[2]="ed", spstring[3]="gh""(书里 [2] 印错了,应为 "ef")。
  • 提示2:用条件运算符 ?: 把字符"1""0"转成 True/False。例:bool pBool= 5>3 ? True:False;(书里大写的 True/False 在 C# 里无法编译,应为 true : false)。"5>3"成立就返回问号后第1个值,pBool 为 True,否则返回第2个值。

自主探究:

请为该流水路灯控制程序增加下列功能: 添加一个文本框TextBox(Name属性值为intervalTextbox)和一个命令按钮Button(name属性值为configButton,Text属性值为"设置")。文本框用来输入每次开关灯的时间间隔,在文本框输入好时间间隔后,单击"设置"按钮设置时间控件的时间间隔值为当前文本框输入的值,且"开""关"单选按钮可用,启动程序默认不可用,其他功能不变。

模拟器提示:参考项目里 项目3/任务一/ZigBee路灯 用的是 DeviceCtrlHelper.zigBeeHelper.Open/Close/OnOff(RelayNum,bool) 加 lamp1.OnOff(bool)(WinFormsDeviceControlLibrary.Lamp)。流水灯需要 Timer、Split,以及 RadioButton 的 Enabled。

项目3【项目评价】PDF 74–75 / 书 71–72

1. 项目评价指标(书里只有指标和要求,没有分值)

任务 评价指标 具体要求
任务1 窗体界面 界面布局合理,美观大方,符合使用者的习惯
任务1 功能实现 环境值能正确获取
环境值能正确显示
实时时间控制正常
任务2 窗体界面 界面布局合理,美观大方,符合使用者的习惯
任务2 功能实现 能正常采集红外传感器数据
环境值能正确推送
能实时联动路灯
职业素养 代码书写规范 应该遵从结构化的要求,采用缩进的格式
注释书写规范 做到清晰、准确地描述内容。对于程序中复杂的部分必须有注释加以说明。注释量要适中,避免过多或过少导致阅读困难
变量命名规范 选择有意义的英文进行命名,使变量名能清楚地表达其含义。尽量少使用人名、地名和汉语拼音。应使用那些约定俗成的缩写。多个单词组成的变量名,每个单词的首字母应该大写
界面常用控件命名约定 窗体控件命名以控件名称缩写前3个字母开头,以具有代表含义的词来命名,其形式为:控件缩写(前3个字母)+命名(具有代表含义的单词)

2. 个人学习总结:空白表,两栏"项目学习情况""心得与反思"(每个项目都有,后面不再重复)。


项目4 智慧校园系统——入侵信息监测模块(PDF 76–93 / 书 73–90)

项目概述:实时安防系统的入侵信息监测,是借助物联网技术把感应器、网络摄像机和警报装置嵌入各种安防监控设备,由计算机采集监测数据,并实现拍摄和报警。本项目采集红外对射入侵传感器的数据,经数字量采集器 ADAM4150 传到计算机,显示在程序窗体中;再用程序分别打开报警灯、控制网络摄像机拍照。

学习目标:

  • 能理解C#重写函数的作用。
  • 能正确实现数据的读取、摄像头图像的显示。
  • 能理解静态、switch、foreach语句的作用,并能灵活使用。
  • 形成良好的代码编写规范,具备良好的问题解决能力。
  • 培养严谨、规范的编程思维和习惯,增强逻辑思维和抽象思维能力。
  • 增强职业认识、提升职业素养、树立职业信心。

任务1 采集红外对射入侵传感器数据(PDF 76–81 / 书 73–78)

任务描述:红外对射入侵传感器由红外发射机和接收机组成,两者之间的光束被遮断时就进入报警状态。本任务模拟校园围墙安防系统,用 C# 把传感器采集的数据实时显示在窗体上。学生要学会 foreach,理解 foreach 和 for 的区别。

任务实施步骤(压缩版):

  1. ADAM4150 串口接 PC 串口,红外对射传感器接采集器 DI3 口。
  2. 新建 WinForms 项目"红外对射采集",保存在 D:\物联网编程与应用\项目4\任务1。
  3. 从配套资源中找到 ADAM4150 用的 DeviceCtrlLibrary.dll,把"API"文件夹下所有 dll 复制到 ...\红外对射采集\红外对射采集\bin\Debug。
  4. 添加引用 DeviceCtrlLibrary.dll。(注:没有硬件设备的读者,请添加仿真动态库文件,无须添加"API"文件夹下的dll文件,后面各任务都有这句注释。)
  5. 按图4-1创建窗体(表4-1)。
  6. using System.IO.Ports; using DeviceCtrlLibrary;
  7. 全局变量 bool IsOpen = false;(书上印成 boolIsOpen,少了空格)。
  8. Form1_Load:com_port.Items.Clear(); foreach (string item in SerialPort.GetPortNames()) com_port.Items.Add(item);
  9. btn_open_Click:IsOpen = !IsOpen;。打开时调用 DeviceCtrlHelper.adam4150Helper.Open(com_port.Text); timer1.Start(); btn_open.Text="关闭";;关闭时调用 adam4150Helper.Close(); timer1.Stop(); btn_open.Text="开启";。
  10. 选中 timer1,在属性窗口的事件栏(闪电图标)找到 Tick 事件(图4-2)。代码为 txt_time.Text = DateTime.Now.ToString(); if (DeviceCtrlHelper.adam4150Helper.SetData().infraredValue) txt_even.Text = "发现入侵"; else txt_even.Text = "正常";
  11. 运行,选串口,点"开启",效果见图4-3(com1,2018-12-08 14:22:45,发现入侵)。

表4-1 窗体界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 红外对射
com_port 下拉列表框 显示串口
btn_open 按钮 Text 开启
txt_time 文本框 显示时间
txt_even 文本框 显示入侵信息
timer1 时间控件 Interval 2000
lbl_port 标签 Text 环境串口
lbl_time 标签 Text 时间
lbl_even 标签 Text 入侵

图里还有一个 GroupBox"数据显示",包着"时间""入侵"两行,表里没列出来。

小贴士:DeviceCtrlHelper.adam4150Helper.SetData().infraredValue 获取红外对射入侵传感器的数据,返回 bool,true 表示入侵,false 表示正常。

知识提炼:

  • foreach 语句:列出集合中所有元素。用 in 分隔,左边是变量,右边是集合,每次循环从集合取下一个元素放进变量,取完为止。示例:string StrMsg=""; int[] array = new int[]{1,2,3,4,5,6,7,8,9}; foreach (int i in array) { StrMsg = StrMsg + i.ToString(); } MessageBox.Show(StrMsg);

能力拓展·边想边做(PDF 80–81):

通过"任务实施"的操作步骤,已经实现了获取红外对射入侵传感器数据的功能。本环节将实现该模块在有入侵警告时推送报警信息至LED显示屏,并具备警报解除功能。根据下列提示,实现上述功能。注:本题涉及串口服务器,需正确配置串口服务器与路由器来实现该功能。

  • 提示1,图4-4 检测流程图:开始 → 判断"是否入侵"。否 → 显示正常信息 → 回到"是否入侵"。是 → 显示入侵信息 → 判断"是否推送"。是 → 生成报警信息并推送 → 修改推送状态 → 结束;否 → 结束。
  • 图4-5 解除报警流程图:开始 → 推送空白信息至LED → 修改推送状态 → 显示正常信息 → 结束。
  • 提示2:根据流程图补全代码,写在 timer1 的 Tick 事件里并调试:
    private void timer1_Tick(object sender, EventArgs e)
    {
        txt_time.Text = DateTime.Now.ToString();
        if(DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)
        {
            txt_even.Text = "发现入侵";
            if( ____ )
            {
                ____
                ____ )          // 书上第二条空线以")"结尾,照原样保留
            }
        }
        else
        {
            txt_even.Text = "正常";
        }
    }
    

自主探究:

修改该模块使其成为夜间门禁防盗报警系统,要具有如下功能,效果如图4-6所示。 1)有入侵则在界面上闪烁显示。 2)将入侵信息推送至LED显示屏上显示。

参考界面(图4-6):标题"红外对射"。上方是"环境串口"下拉框(COM2)和"LED串口"下拉框(COM3),右侧有"关闭"和"解除报警"两个按钮。GroupBox"数据显示"里有时间(2017/1/17 15:37:00)、入侵(发现入侵),下面一个闪烁的标签"检测到入侵,请注意!"。

任务2 控制网络摄像机(PDF 81–87 / 书 78–84)

任务描述:网络摄像机是传统摄像机与网络视频技术结合的产品,可以通过网络访问和控制,实时监控现场。本任务模拟校园视频监控系统,用 C# 把摄像机的实时图像显示在窗体上,并实现上下左右转动和拍照。学生要学会 PictureBox 的用法和 static 的特点。

任务实施步骤:

  1. 网络摄像机和 PC 都连到无线路由器,并配置好摄像机。
  2. 新建项目"网络摄像机",保存在 D:\物联网编程与应用\项目4\任务2。
  3. 把摄像机用的 DeviceCtrlLibrary.dll 和 API 下所有 dll 复制到 ...\网络摄像机\网络摄像机\bin\Debug。
  4. 添加引用(仿真库注释同上)。
  5. 按图4-7创建窗体(表4-2):左边是大块黑色图片框;右边是"摄像机IP"文本框,下面是十字形方向键,中间是"开",再往下是"拍照"。
  6. using System.IO.Ports; using DeviceCtrlLibrary;(书里漏了 using System.IO;,可是 Directory 要用它;参考代码写的是 using System.IO;,没有 Ports)。
  7. 4个全局变量:
    static string CameraPath = AppDomain.CurrentDomain.BaseDirectory + "Camera/";//拍照保存路径
    static string IpCameraIPAddress = "192.168.14.131";//网络摄像机IP
    bool IsOpen = false;
    Image ReadImage = null;//存储摄像机图片
    
  8. void ipCameraHelper_ImageRead(ImageReadEvenArgs e) { ReadImage = e.img; pictureBox1.Image = ReadImage; }。类型名写的就是 ImageReadEvenArgs,原文如此,和库里一致。
  9. Form1_Load:if(!Directory.Exists(CameraPath)) Directory.CreateDirectory(CameraPath); DeviceCtrlHelper.ipCameraHelper.ImageRead += ipCameraHelper_ImageRead;//绑定图片显示事件
  10. btn_open_Click:IsOpen=!IsOpen;。打开时 IpCameraIPAddress = txt_ip.Text; DeviceCtrlHelper.ipCameraHelper.Open(IpCameraIPAddress, "admin", "");//用户名admin,密码为空 然后 DeviceCtrlHelper.ipCameraHelper.Start(); btn_open.Text = "关";;关闭时 ipCameraHelper.Stop(); btn_open.Text = "开";。 11–14. 方向键分别调用 DeviceCtrlHelper.ipCameraHelper.PanUp()、PanDown()、PanLeft()、PanRight()。
  11. btn_camera_Click:if (ReadImage != null) ReadImage.Save(CameraPath + DateTime.Now.ToString("yyyyMMdd_HH mm ss") + ".jpg");(格式串里本来就带空格)。
  12. 运行,填摄像机IP(图中是 192.168.2.50),点"开",效果见图4-8。

表4-2 "网络摄像机"窗体界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 网络摄像机
pictureBox1 图片框 BackColor / SizeMode Black / StretchImage 显示图像
label1 标签 Text 摄像机IP
txt_ip 文本框 输入摄像机IP
btn_open 按钮 Text 开 打开/关闭摄像机
btn_up 按钮 Text ↑ 向上转
btn_down 按钮 Text ↓ 向下转
btn_left 按钮 Text ← 向左转
btn_right 按钮 Text → 向右转
btn_camera 按钮 Text 拍照 截取图像

知识提炼:

  1. PictureBox 图片框:能显示 .bmp、.ico、.wmf、.jpg、.gif。表4-3 常见属性:
    • BorderStyle:边框样式,枚举(书里写成"Emun")。None 无边框,FixedSingle 单线边框,fixed3D 立体边框。
    • Image:要显示的图片,也可以用 Image.FromFile 加载。
    • SizeMode:显示模式,枚举。Normal 置于左上角,图片大于控件时被剪切;AutoSize 自动调整图片框大小,和图片一样大;CenterImage 图片框比图片大时居中,比图片小时剪切边沿;StretchImage 拉伸或收缩以适合图片框;zoom 按原比例缩放。
  2. static:用 static 声明的成员属于类本身,不属于对象,不用实例化,直接通过类名调用。示例:public class StaticClass { public static int x = 10; },然后在 Form1_Load 里 MessageBox.Show(StaticClass.x.ToString());。

能力拓展·边想边做:

通过"任务实施"的操作步骤,已经实现了摄像机显示、控制与抓拍的功能。本环节将为该模块添加每天定时抓拍的功能,固定时间间隔为:2h、1h、45min、30min、15min、5min。请根据下列提示,实现上述功能。 提示:根据之前所学利用if条件语句完善该代码,然后在程序中双击"开"按钮进入代码编写窗口,输入完整代码并调试。

private void btn_open_Click(object sender, EventArgs e)
{
    IsOpen = !IsOpen;//取反IsOpen变量,变更按下按钮后串口的打开或关闭状态
    if (IsOpen)
    {
        IpCameraIPAddress = txt_ip.Text;
        DeviceCtrlHelper.ipCameraHelper.Open(IpCameraIPAddress, "admin", "");//打开网络摄像机
        DeviceCtrlHelper.ipCameraHelper.Start();//开启网络摄像机监控
        btn_open.Text = "关";//将按钮文字改为"关"
        if (comboBoxDateSelect.Text.Trim()=="2h")
        {
            timer1.Interval = 7200000;
        }
        else if (____)
        {
            timer1.Interval = 3600000;
        }
        else if (comboBoxDateSelect.Text.Trim() == "45min")   // 书中这两行带下划线,是示范答案
        {
            timer1.Interval = 2700000;
        }
        else if ( ____ )
        {
            ____
        }
        else if ( ____ )
        {
            ____
        }
        timer1.Start();
    }
    else
    {
        DeviceCtrlHelper.ipCameraHelper.Stop();//关闭网络摄像机
        btn_open.Text = "开";//将按钮文字改为"开"
        timer1.Stop();
    }
}

注意:一共列了6档间隔,骨架只有5个分支(5min 没有位置)。另外要新增 ComboBox comboBoxDateSelect 和 timer1,Tick 里应该做抓拍,书里都没给控件表,也没给 Tick 代码。

自主探究:

修改该模块使其成为智慧校园监控系统,在东围墙安装一台红外对射入侵传感器与一台网络摄像机,要具有如下功能,运行效果如图4-9所示。 1)实时监控围墙情况。 2)如发现入侵则自动抓拍。

参考界面(图4-9):标题"网络摄像机",左边大图像区。右边三个 GroupBox:

  • "设置":摄像机IP(192.168.2.90)、串口配置下拉框、"关闭"按钮。
  • "入侵监控区":标签"发现入侵"。
  • "控制区":↑ ← → ↓ 四个按钮。

任务3 控制报警灯(PDF 87–92 / 书 84–89)

任务描述:报警灯用在危险场所,靠闪光发出警示。发生事故、火灾等紧急情况时,控制器发出信号启动报警电路,灯就闪光报警。本任务模拟校园安防报警系统,用 C# 打开和关闭报警灯。学生要掌握 switch 语句。

任务实施步骤:

  1. 采集器串口接 PC 串口,报警灯通过继电器接到采集器 DO0 口。
  2. 新建"报警灯"项目,保存在 D:\物联网编程与应用\项目4\任务3。
  3. 把自定义控件 WinFormsDeviceControlLibrary.dll 添加到工具箱。
  4. 把 ADAM4150 的 DeviceCtrlLibrary.dll 和 API 下的 dll 复制到 ...\报警灯\报警灯\bin\Debug。
  5. 添加引用(仿真库注释同上)。
  6. 按图4-10建窗体(表4-4):左边是报警灯图,下面"开""关"两个按钮;右边 GroupBox"配置",里面是"报警灯"下拉框和"开启"按钮。
  7. using System.IO.Ports; using DeviceCtrlLibrary;
  8. bool IsOpen = false;(书上又印成 boolIsOpen)
  9. Form1_Load 填充串口,和任务1 一样。
  10. 定义方法:
    private void AlarmOnOff(string Device, bool IsOpen)
    {
        //利用switch语句打开或关闭继电器模块
        switch(Device)
        {
            case "Alarm": DeviceCtrlHelper.adam4150Helper.OnOff(0, IsOpen);
                alarm1.OnOff(IsOpen); break;
                //开启或关闭继电器0模块,报警灯接入DO0,更改界面报警灯状态
            case "LED1": DeviceCtrlHelper.adam4150Helper.OnOff(1, IsOpen); break;//开启或关闭继电器1模块
            case "LED2": DeviceCtrlHelper.adam4150Helper.OnOff(2, IsOpen); break;//开启或关闭继电器2模块
            default: MessageBox.Show("没有合适的接口程序!", "错误提示!"); break;//若为其他模块,则提示错误
        }
    }
    
  11. btn_open_Click:只开关串口并改按钮文字("关闭"/"开启"),没有 timer。
  12. btn_alarmOpen_Click 里调用 AlarmOnOff("Alarm", true);13. btn_alarmClose_Click 里调用 AlarmOnOff("Alarm", false)。
  13. 运行,选串口点"开启",再点"开""关"控制报警灯,见图4-11(灯变红亮)。

表4-4 "报警灯"界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 报警灯
Alarm 自定义控件 显示报警灯状态
btn_alarmOpen 按钮 Text 开
btn_alarmClose 按钮 Text 关
groupBox1 组合框 Text 配置
label1 标签 Text 报警灯
com_port 下拉列表框 显示串口
btn_open 按钮 Text 开启

表里的控件名写的是"Alarm"(其实是控件类型名),代码和参考项目里的实例名都是 alarm1,类型是 WinFormsDeviceControlLibrary.Alarm。

知识提炼:switch 语句。

  • 作用:类似 if,用于从一组互斥分支里选一个执行。格式为 switch(表达式){ case "常量一":语句;break; … case "常量N":语句;break; default:语句;break; }。
  • 规则:每个 case 指定一个常数值,匹配上就跳进去执行,直到 break 跳出;都不匹配就执行 default;没有 default 就什么都不做,直接执行 switch 后面的语句。
  • 示例 Form1_Load:string grade="A"; switch(grade){ case "A":MessageBox.Show("您的成绩为:优秀");break; case "B":…"良好"; case "C":…"一般"; case "D":…"合格"; case "E":MessageBox.Show("您的成绩为:不合格,要好好努力了!");break; default: MessageBox.Show("无效的成绩等级!");break; }

能力拓展·边想边做:

通过"任务实施"的操作步骤,已经实现了报警灯的打开与关闭功能。本环节为该模块添加有入侵警告时打开报警灯的功能。请根据下列提示,实现上述功能。图4-12所示为实现本功能的流程图,供参考。

图4-12 监测流程图:

  • 开始 → 判断"是否入侵"。
  • 是 → 判断"报警灯是否已打开"。否 → 打开报警灯 → 回到"是否入侵";是 → 结束。
  • 否 → 判断"报警灯是否打开"。是 → 关闭报警灯 → 回到"是否入侵";否 → 结束。

提示:根据流程图补全代码,写在 timer1 的 Tick 事件里:

private void timer1_Tick(object sender, EventArgs e)
{
    if (DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)
    {
        txt_even.Text = "发现入侵";
        if( ____ )
        {
            ____
            AlarmState = true;
        }
    }
    else
    {
        txt_even.Text = "正常";
        if ( ____ )
        {
            ____
            AlarmState = false;
        }
    }
}

这里要额外加 txt_even、timer1 和 bool AlarmState,任务3 的控件表里都没有。

自主探究:

修改该模块使其成为智慧校园围墙监控报警系统,要具有如下功能,运行效果如图4-13所示。 1)实时检测入侵信息。 2)若有入侵则推送报警信息至LED显示屏。 3)若有入侵则打开报警灯。

参考界面(图4-13):标题"报警灯",左边是报警灯。右边 GroupBox"配置":串口配置(COM2)、LED串口(COM3)、"关闭"按钮。下面 GroupBox"数据显示":入侵文本框(发现入侵)。

项目4【项目评价】PDF 92–93 / 书 89–90

任务 评价指标 具体要求
任务1 窗体界面 窗体控件正确,布局美观
开关功能 按钮能打开、关闭串口;开启、停止定时器;更改按钮显示文字
实时显示 能实时显示入侵数据
任务2 窗体界面 窗体控件正确,布局美观
开关功能 能正确打开、关闭摄像机
图像实时显示 能正确显示图像
旋转控制功能 能控制摄像机上下左右旋转
抓拍功能 能抓拍摄像机当前图像
任务3 窗体界面 窗体控件正确,布局美观
开关功能 能正确打开、关闭报警灯
职业素养 代码书写规范 应该遵从结构化的要求,采用缩进的格式
注释书写规范 做到清晰、准确地描述内容。对于程序中复杂的部分必须有注释加以说明。注释量要适中,避免过多或过少导致阅读困难
变量命名规范 选择有意义的英文进行命名,使变量名能清楚地表达其含义。尽量少使用人名、地名和汉语拼音。应使用那些约定俗成的缩写。多个单词组成的变量名,每个单词的首字母应该大写

(没有分值。和项目3 比,少了"界面常用控件命名约定"这一行。)后面跟个人学习总结空表。


项目5 智慧校园系统——灾情信息监测模块(PDF 94–114 / 书 91–111)

项目概述:实时安防系统的灾情信息监测,是借助物联网技术把火焰、烟雾传感器嵌入各种安防监控设备,由计算机采集灾情数据,并实现推送和报警。本项目采集火焰传感器和烟雾传感器的数据,经数字量采集器 ADAM4150 传到计算机,显示在程序窗体中;再用程序把灾情信息推送到 LED 显示屏。

学习目标:

  • 能正确理解for、while循环语句,并能使用循环语句解决简单问题。
  • 能理解Datetime类和continue语句。
  • 了解多线程的概念,学会简单的多线程编写。
  • 形成良好的代码编写规范,培养代码优化和性能调优的能力。
  • 培养严谨、规范的编程思维和习惯,增强逻辑思维和抽象思维能力。
  • 增强职业认识,提升职业素养,树立职业信心。

任务1 采集火焰传感器数据(PDF 94–101 / 书 91–98)

任务描述:火焰传感器利用红外线对火焰非常敏感的特点,用特制的红外接收管检测火焰。本任务模拟校园安防的灾情监测系统,用 C# 把火焰传感器的数据实时显示在窗体上。学生要掌握 for 和 return 语句,了解 DateTime 类常见的属性和方法。

任务实施步骤:

  1. ADAM4150 接 PC,火焰传感器接 DI1。
  2. 新建"火焰传感器数据采集"项目,保存在 D:\物联网编程与应用\项目5\任务1。
  3. 复制 dll 到 ...\火焰传感器数据采集\火焰传感器数据采集\bin\Debug。
  4. 添加引用(仿真库注释同上)。
  5. 按图5-1建窗体(表5-1)。
  6. using System.IO.Ports; using DeviceCtrlLibrary;
  7. bool IsOpen=false;
  8. Form1_Load 这次用 for 循环:for (int i=0; i<SerialPort.GetPortNames().Length; i++) { com_port.Items.Add(SerialPort.GetPortNames()[i].ToString()); }
  9. btn_open_Click:和项目4 任务1 一样(Open/Close 串口,"关闭"/"开启",timer1 Start/Stop)。
  10. 自定义方法 GetFireInfo:
    private String GetFireInfo()
    {
        //判断火焰传感器返回bool值是否为true,若为true,则返回火情信息
        if (DeviceCtrlHelper.adam4150Helper.SetData().fireValue)
        {
            return "发现火情!";
        }
        return "正常!";//无火情返回正常信息
    }
    
  11. timer1 的 Tick:txt_time.Text = DateTime.Now.ToString(); txt_even.Text = GetFireInfo();
  12. 运行效果见图5-2(COM1,2017/1/17 13:13:16,发现火情!)。

表5-1 火焰传感器数据采集界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 火焰
com_port 下拉列表框 输入/显示串口
btn_open 按钮 Text 开启
txt_time 文本框 显示时间
txt_even 文本框 显示入侵信息
timer1 时间控件 Interval 2000
lbl_port 标签 Text 串口
lbl_time 标签 Text 时间
lbl_even 标签 Text 事件

图里还有 GroupBox"数据显示"。

小贴士:DeviceCtrlHelper.adam4150Helper.SetData().fireValue 获取火焰传感器数据,返回 bool,true 表示有火情,false 表示正常。

知识提炼:

  1. for 循环:适合次数确定的循环,格式 for(表达式1;表达式2;表达式3){语句;}。表达式1 给循环变量赋初值;表达式2 是循环条件,为真就执行循环体,为假退出;表达式3 让循环变量自增或自减。示例求1到100的和:int sum=0; for(int i=1;i<=100;i++){sum=sum+i;} MessageBox.Show("1+2+3+…+100="+sum.ToString());
  2. return 语句:结束当前方法,并返回 return 后面的值(可以不带值);void 方法可以省略 return。示例:MessageBox.Show(Area(3.1,2.5).ToString()); private double Area(double x,double y){ return x * y; }
  3. DateTime 类。
    • 表5-2 常见属性:Now(当前日期和时间)、Today(当前日期)、Date(日期部分)、Day(当月第几天)、Month、Year、DayOfWeek(星期几)、DayOfYear(当年第几天)、Hour、Minute、Second。 示例:DateTime dt=DateTime.Now; StrMsg+=dt.ToString()+"\n"; DateTime dt1=DateTime.Today; …dt.Date/Day/Month/Year/DayOfWeek/DayOfYear/Hour/Minute/Second…; MessageBox.Show(StrMsg);

    • 表5-3 常见方法:

      • AddDays / AddMonths / AddYears / AddHours / AddMinutes / AddSeconds:把指定的天数/月数/年数/小时数/分钟数/秒数加到当前实例上。
      • IsLeapYear:判断指定年份是不是闰年。
      • Parse:把日期时间字符串转成 DateTime。
      • ToLongDateString / ToLongTimeString / ToShortDateString / ToShortTimeString:转成长日期、长时间、短日期、短时间字符串。
      • ToString:转成字符串。

      示例 Form2_Load:dt.AddDays(10)、AddMonths(1)、AddYears(1)、AddHours(10)、AddMinutes(20)、AddSeconds(20),string str="2016-12-12"; DateTime dt7=DateTime.Parse(str);,再输出 ToLongDateString()、ToLongTimeString()、ToShortDateString()、ToShortTimeString()、dt.ToString("yyyy*MM*dd")。

    • 模拟器注意:MessageBox.Show 要能显示 "\n" 换行;DateTime 的输出格式要和中文 Windows 一致(如 2017/1/17 13:13:16)。

能力拓展·边想边做:

通过"任务实施"的操作步骤,已经实现了实时获取火焰传感器数据的功能。本环节将使用多线程机制来实现该功能,为任务2做好准备。请根据下列提示,实现上述功能。

  • 提示1,图5-3 线程运行流程图:开始 → 判断"是否检测到火焰",是 → 显示火情,否 → 显示正常 → 线程休眠2s → 回到判断(图上有"结束"框,但循环不会自己退出)。
  • 图5-4 开关监测流程图:开始 → 判断"串口是否打开"。
    • 否 → 打开串口 → 判断"是否第一次运行线程",是 → 开启线程,否 → 继续已挂起的线程 → 结束。
    • 是 → 挂起线程 → 结束。
  • 提示2:进入代码窗口,在 btn_open_Click 事件里实现功能,线程方法见表5-4。表5-4 线程方法:
    名称 说明
    Start() 开启线程
    Resume() 继续已挂起的线程
    Suspend() 挂起线程
    Sleep(int32) 线程休眠,Thread.Sleep(2000)为休眠2s
    Abort() 终止线程
  • 提示3:找到 GetData() 方法,按流程图补全:
    private void GetData()
    {
        while ( ____ )
        {
            if ( ____ )
            {
                txt_time.Text=DateTime.Now.ToString();
                txt_even.Text="发现火情";
            }
            else
            {
                txt_time.Text=DateTime.Now.ToString();
                txt_even.Text="正常";
            }
        }
    }
    
    骨架里没有 Thread.Sleep(2000),流程图里却有。

自主探究:

分别在教室A、B安装火焰传感器模拟智慧教室火焰检测系统,要具有如下功能,运行效果如图5-5所示。注:本题使用DI1与DI2接口,接入火焰传感器。 1)有火焰灾情时在界面上闪烁显示,并显示教室位置。 2)有火焰灾情时打开报警灯。

参考界面(图5-5):标题"教室火焰检测"。顶部是"串口"下拉框(COM1)和"关闭"按钮。下面两个 GroupBox"教室A""教室B",每个里有标签"火焰状态"和状态文字("正常!" / "发现火情!")。 (注意 DI2 在任务实施里接的是烟雾传感器,这里两路都当火焰用,模拟器要能把 DI1、DI2 都设成火焰。)

任务2 采集烟雾传感器数据(PDF 101–106 / 书 98–103)

任务描述:烟雾传感器通过监测烟雾浓度来做火灾报警提示。本任务模拟校园安防的灾情监测系统,用线程把烟雾传感器的数据实时显示在窗体上。学生要掌握 while 语句,理解 while 和 for 各自的特点,初步了解线程 Thread 的概念和用法。

任务实施步骤:

  1. 烟雾传感器接 DI2。
  2. 新建"烟雾传感器数据采集"项目,保存在 D:\物联网编程与应用\项目5\任务2。
  3. 复制 dll 到 ...\烟雾传感器数据采集\烟雾传感器数据采集\bin\Debug。
  4. 添加引用(仿真注释同上)。
  5. 按图5-6建窗体(表5-5,没有 timer)。
  6. using System.IO.Ports; using DeviceCtrlLibrary; using System.Threading;//引用线程命名空间
  7. 全局变量:bool IsOpen=false; bool IsFirst=true;//记录线程是否第一次打开 Thread thd;//定义Thread线程
  8. GetData:
    private void GetData()
    {
    //线程运行方法,不断循环运行
        while (true)
        {
            if (DeviceCtrlHelper.adam4150Helper.SetData().smokeValue)//判断烟雾状态,True为发现烟雾灾情,False为正常状态
            {
                txt_time.Text = DateTime.Now.ToString();
                txt_even.Text = "发现烟情";
            }
            else
            {
                txt_time.Text = DateTime.Now.ToString();
                txt_even.Text = "正常";
            }
            Thread.Sleep(2000);//线程休眠2s
        }
    }
    
  9. Form1_Load:用 foreach 填串口,然后 thd = new Thread(GetData);//实例化线程,绑定线程运行方法 和 Control.CheckForIllegalCrossThreadCalls = false;//允许跨线程调用。
  10. btn_open_Click:
    • IsOpen=!IsOpen;
    • 打开时:adam4150Helper.Open(com_port.Text); btn_open.Text="关闭"; if (IsFirst) { IsFirst=false; thd.Start(); } else { thd.Resume(); }
    • 关闭时:adam4150Helper.Close(); btn_open.Text="开启"; thd.Suspend();
  11. 窗体 FormClosing 事件:private void Form1_FormClosing(object sender, FormClosingEventArgs e) { thd.Abort();//终止线程 }
  12. 运行效果见图5-7(COM1,2016-12-15 17 10 29,正常)。

表5-5 烟雾采集窗体界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 烟雾
com_port 下拉列表框 输入/显示串口
btn_open 按钮 Text 开启
txt_time 文本框 显示时间
txt_even 文本框 显示入侵信息
lbl_port 标签 Text 串口
lbl_time 标签 Text 时间
lbl_even 标签 Text 事件

模拟器需要支持:

  • System.Threading.Thread 的 Start、Suspend、Resume、Abort、Sleep,以及 Control.CheckForIllegalCrossThreadCalls = false(让后台线程直接改 UI 也不报错)。
  • Suspend、Resume、Abort 在新版 .NET 里已过时,.NET Core/5+ 调 Abort 会抛 PlatformNotSupportedException,模拟器得自己模拟这几个行为。
  • 参考项目的 FormClosing 和书不一样:if(thd.ThreadState==ThreadState.Unstarted) thd.Abort();。这种写法对已挂起的线程不 Abort;对没启动过的线程调 Abort,在原 .NET Framework 上也有问题。

知识提炼:

  1. while 循环:条件为真就反复执行,直到条件为假,格式 while(条件){语句;}。示例求1到10的和:int i = 1, sum = 0; while(i<11){ sum = sum + i; i = i + 1; } MessageBox.Show(sum.ToString());
  2. Thread 线程:.NET 是多线程环境,C# 程序从一个由系统自动创建的主线程开始,可以再创建额外线程。示例是一个控制台程序:
    class ThreadTest
    {
        static void Main(string[] args)
        {
            Thread thd = new Thread(PrintB);
            thd.Start();
            while (true) Console.Write("A");
        }
        static void PrintB ()
        {
            while (true) Console.Write("B");
        }
    }
    

能力拓展·边想边做:

通过"任务实施"的操作步骤,已经实现了获取烟雾传感器数据的功能。本环节将实现当有烟雾报警时,使用多线程在程序界面闪烁红色文字。请根据下列提示,实现上述功能。

提示1:根据以下提示,完善GetData()方法,进入代码编写窗口,输入代码。

private void GetData()
{
    while (true)
    {
        if (DeviceCtrlHelper.adam4150Helper.SetData().smokeValue)
        {
            txt_time.Text=DateTime.Now.ToString();
            txt_even.Text="发现烟情";
            btn_remove.Visible=true;
            btn_open.Visible=true;
            ____ //挂起thd线程
            ____ //开启hdAlarm线程
        }
        else
        {
            txt_time.Text=DateTime.Now.ToString();
            txt_even.Text="正常";
        }
        ____ //调用Thread.Sleep(int)方法使线程休眠一段时间
    }
}

提示2:根据以下提示,完善该代码。选择"Alarm"方法,进入代码编写窗口,输入代码。

private void Alarm()
{
    while( ____ )//设置while条件,使线程方法体循环运行
    {
        if (lbl_alarm.Visible == true)
        {
            lbl_alarm.Visible = false;
        }
        else
        {
            lbl_alarm.Visible =true;
        }
    ____ //调用Thread.Sleep(int)方法,线程休眠一段时间,调试后设置合适的时间,使线程达到理想效果
    }
}

提示3:根据以下提示,完善"btn_remove"按钮控件的"Click"事件,进入代码编写窗口,输入代码并调试。

private void btn_remove_Click(object sender, EventArgs e)
{
    txt_even.ForeColor = Color.Black;
    ____ //重新开启thd实时采集线程
    btn_remove.Visible = false;
    lbl_alarm.Visible = false;
    ____ //终止thdAlarm线程
}

还需要补的东西:btn_remove(解除报警按钮,默认隐藏)、lbl_alarm(红色闪烁标签)、Thread thdAlarm = new Thread(Alarm)。注释里的"hdAlarm"是原文笔误,指的就是 thdAlarm。

自主探究:

分别在教室A、B安装烟雾传感器模拟智慧教室烟雾检测系统,要具有如下功能,运行效果如图5-8所示。 1)有烟雾灾情时开启报警灯。 2)推送灾情位置和发现时间至LED显示屏。

参考界面(图5-8):标题"烟雾"。上方"串口"(COM2)、"LED串口"(COM3)两个下拉框和"关闭"按钮。下面 GroupBox"教室A"(烟雾状态 / 发现烟情)和"教室B"(烟雾状态 / 正常)。

任务3 实时推送灾情信息(PDF 107–111 / 书 104–108)

任务描述:本任务模拟校园安防的灾情监测系统,结合任务1 和任务2,用 C# 把烟雾、火焰传感器的灾情信息推送到 LED 屏,用数组存储灾情信息并在窗体上显示。学生要进一步掌握数组,理解 continue 的作用,掌握 continue 和 break 的区别。

任务实施步骤:

  1. 采集器串口和 LED 屏串口都接到串口服务器上;火焰、烟雾传感器分别接 DI1、DI2;正确配置串口服务器和路由器。
  2. 新建"实时推送灾情信息"项目,保存在 D:\物联网编程与应用\项目5\任务3。
  3. 复制 ADAM4150 的 dll 和 API 到 ...\实时推送灾情信息\实时推送灾情信息\bin\Debug。
  4. 添加引用(仿真注释同上)。
  5. 新建 "RecordClass" 类文件:
    public static string[,] record = new string[50, 2] ;//定义一个二维数组,用于存储灾情信息的时间和内容
    public static int recoudeindex = 0;//用于记录灾情信息的索引位置
    
    参考项目里 RecordClass 是没有 namespace 的全局 public class RecordClass。
  6. 按图5-9建窗体(表5-6):顶部是一条很宽的黑色 LED 显示控件 led1。下方 GroupBox"数据采集"里有时间、火焰、烟雾三个文本框和一个竖长的"获取"按钮。右边 GroupBox"配置"里有"L E D"下拉框、"环境采集"下拉框、"开启"按钮和"历史灾情信息"按钮。
  7. using System.IO.Ports; using DeviceCtrlLibrary; using DeviceCtrlLibrary.Model;//引用设备控制库下的Model命名空间
  8. 全局变量:ADAM4150ResModel ResModel; bool IsOpen = false; string LED_MSG = "";//存储LED显示屏所需发送的文字信息
  9. Form1_Load:
    com_environmentPort.Items.Clear(); com_ledPort.Items.Clear();
    //将系统串口名依次加入下拉列表框中,由于本题使用串口服务器,所以COM1计算机串口可不加入串口列表
    for (int i = 0; i < SerialPort.GetPortNames().Length;i++ )
    {
        //判断计算机的默认串口,将其排除
        if (SerialPort.GetPortNames()[i].ToUpper()=="COM1")
        {
            continue;//跳出此次循环
        }
        com_environmentPort.Items.Add(SerialPort.GetPortNames()[i].ToString());
        com_ledPort.Items.Add(SerialPort.GetPortNames()[i].ToString());
    }
    
    参考项目没有排除 COM1,用两个 foreach 分别填充。书上的运行截图里 LED 用的却正是 COM1,自相矛盾。
  10. btn_Open_Click:
    • 打开时:DeviceCtrlHelper.adam4150Helper.Open(com_environmentPort.Text);、DeviceCtrlHelper.ledHelper.OpenLedCom(com_ledPort.Text);、btn_Open.Text = "关闭";
    • 关闭时:只执行 adam4150Helper.Close(); btn_Open.Text="开启";,LED 串口没有关。
  11. btn_get_Click("获取"按钮):
    ResModel = DeviceCtrlHelper.adam4150Helper.SetData();//获取数字量采集器ADAM 4150的最新数据
    LED_MSG = "";
    txt_time.Text = DateTime.Now.ToString();
    txt_fire.Text = "正常"; txt_smoke.Text = "正常";
    if (ResModel.fireValue)
    {
        txt_fire.Text = "发现火情";
        LED_MSG += "发现火情";
        RecordClass.record[RecordClass.recoudeindex, 0] = txt_time.Text;//记录灾情时间至数组
        RecordClass.record[RecordClass.recoudeindex, 1] = " 发现火情";//记录灾情内容至数组
        RecordClass.recoudeindex++;
    }
    if (ResModel.smokeValue)
    {
        txt_smoke.Text = "发现烟情";
        LED_MSG += "发现烟情";
        RecordClass.record[RecordClass.recoudeindex, 0] = txt_time.Text; /记录灾情时间至数组
        RecordClass.record[RecordClass.recoudeindex, 1] = " 发现火情";//记录灾情内容至数组
        RecordClass.recoudeindex++;
    }
    string ResStr = DeviceCtrlHelper.ledHelper.SendLedString(LED_MSG);
    if (!(ResStr.IndexOf("Error") >= 0))
        led1.setLedText(ResStr); //推送灾情信息至LED显示屏
    
    这段有三个问题:
    • 烟雾分支记录的内容写成了"发现火情",是 bug,参考项目里也一样。
    • 烟雾分支的注释只有一个 /,书上的代码编译不过,参考项目正确。
    • 数组只有 50 条,没有越界检查。
  12. "历史灾情信息"按钮:private void OpenRecord_Click(...) { Form2 fr2 = new Form2(); fr2.Show(); }。这个按钮和"获取"按钮 btn_get 都不在表5-6 里。
  13. 按图5-10建 Form2(表5-7)。
  14. Form2_Load:for (int i = 0; i < RecordClass.recoudeindex;i++ ) listBox1.Items.Add(RecordClass.record[i,0]+" "+RecordClass.record[i,1]);。书里代码用的是 listBox1,表5-7 和参考项目里都叫 lstbRecored。
  15. 运行:点"开启"后效果见图5-11,LED 区显示大字"发现火情",时间 2024/1/23 10 40.51,LED 用 COM1,环境采集用 COM2;点"历史灾情信息"后见图5-12,列表一行一条,如 2024/1/23 10:41:30 发现火情。

表5-6 火焰烟雾采集窗体界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 火焰烟雾
com_ledPort 下拉列表框 输入/显示LED串口
com_environmentPort 下拉列表框 输入/显示ADAM4150串口
btn_Open 按钮 Text 开启
txt_time 文本框 显示时间
txt_fire 文本框 显示火焰信息
txt_smoke 文本框 显示烟雾信息
groupBox1 组合框 Text 配置
lbl_Ledport 标签 Text LED
lbl_4150port 标签 Text 环境采集
lbl_time 标签 Text 时间
lbl_fire 标签 Text 火焰
lbl_smoke 标签 Text 烟雾
led1 自定义控件 信息显示在LED控件上

表里没有、参考项目里有的控件:

  • btn_get(Text"获取")
  • OpenRecord(Text"历史灾情信息")
  • groupBox2(Text"数据采集")
  • led1 的类型是 WinFormsDeviceControlLibrary.LED

表5-7 历史灾情记录界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form2 窗体 Text 历史灾情记录
lstbRecored 列表框 显示灾情记录

模拟器需要支持:

  • 多窗体 new Form2().Show(),以及多个类文件(RecordClass)。
  • DeviceCtrlHelper.ledHelper.OpenLedCom(port) 和 SendLedString(string)。后者返回字符串,出错时字符串里含"Error"。
  • led1.setLedText(string)。
  • ADAM4150ResModel 的 fireValue 和 smokeValue。

知识提炼:continue 语句。

  • 作用:结束本次循环,开始下一次循环。只能用在 while、do/while、for、foreach/in 的循环体里,用在别处会出错。
  • 示例找出1到20内不能被3整除的数:string StrMsg = ""; for (int i = 1; i <= 20; i++) { if (i % 3==0) continue; StrMsg += i.ToString() + "\n"; } MessageBox.Show(StrMsg);

能力拓展·边想边做:

通过"任务实施"的操作步骤,已经实现了获取火焰及烟雾传感器数据的功能。本环节将实现实时获取相关数据,在发现灾情时推送信息至LED屏并打开报警灯,直至灾情解除。请根据下列提示,实现上述功能。

提示1:根据以下提示,完善"GetData"方法,进入代码编写窗口,输入完整代码并调试。

private void GetData()
{
    while (true)
    {
        if ( ____ )//判断是否有烟雾或火焰报警
        {
            txt_time.Text = DateTime.Now.ToString();
            txt_even.Text = "发现灾情";
            btn_remove.Visible = true;
            if( ____ )//判断LED灯与报警灯的状态是否为false
            {
                ____
                ____
                LEDState = true;
                AlarmLampState = true;
            }
            if(LedDisplay==false)
            {
                ____ //打开LED显示屏串口
                ____ //推送灾情信息至LED显示屏
                LedDisplay = true;
            }
            btn_open.Visible = true;
            thd.Suspend();
        }
        else
        {
            txt_time.Text = DateTime.Now.ToString();
            txt_even.Text = "正常";
        }
        Thread.Sleep(2000);
    }
}

提示2:根据以下提示,完善"btn_remove"按钮控件的"Click"事件,进入代码编写窗口,输入完整代码并调试。

private void btn_remove_Click(object sender, EventArgs e)
{
    txt_even.ForeColor = Color.Black;
    btn_remove.Visible = false;
    lbl_alarm.Visible = false;
    ____ //重新开启thd实时采集线程
    ____ //结束thdAlarm线程
}

这道题沿用了任务2 的线程结构(thd、thdAlarm、txt_even、btn_remove、lbl_alarm),还要加 LEDState、AlarmLampState、LedDisplay 三个 bool。"LED灯"应该是接在 adam4150 DO1/DO2 上的 LED 灯(对应项目4 任务3 里 switch 的 "LED1""LED2"),不是 LED 显示屏。任务3 的控件表里这些都没有。

自主探究:

模拟智慧校园灾情监测系统的历史灾情显示功能,运行效果如图5-13所示,需具有如下功能: 1)在同一界面内利用数组记录灾情信息。 2)每隔10min将最新5条灾情信息推送至LED显示屏。

参考界面(图5-13):标题"火焰烟雾"。左上是时间、火焰、烟雾三个文本框;右上 GroupBox"配置"有"L E D"(COM2)、"环境采集"(COM3)和"关闭"按钮。下面 GroupBox"历史记录"是一个大的多行列表,内容如 发现火情 发现烟情 发现时间:2017/1/17 15.48:08、发现火情 发现时间:2017/1/17 15:48:10……

项目5【项目评价】PDF 113 / 书 110

任务 评价指标 具体要求
任务1 窗体界面 窗体控件正确,布局美观
开关功能 能正确打开、关闭串口;开启、停止定时器;更改按钮显示文字
实时显示 能实时显示火焰传感器数据
任务2 窗体界面 窗体控件正确,布局美观
开关功能 按钮能打开、关闭串口;开启、停止线程;更改按钮显示文字
实时显示 能正确利用线程实时显示烟雾传感器数据
任务3 窗体界面 窗体控件正确,布局美观
开关功能 能正确打开、关闭报警灯
数组使用 能利用数组实时存储数据
灾情实时显示 能实时显示火焰、烟雾传感器数据
灾情推送 能正确推送灾情信息,并保证不重复推送
职业素养 代码书写规范 应该遵从结构化的要求,采用缩进的格式
注释书写规范 做到清晰,准确地描述内容。对于程序中复杂的部分必须有注释加以说明。注释量要适中,避免过多或过少导致阅读困难
变量命名规范 选择有意义的英文进行命名,使变量名能清楚地表达其含义。尽量少使用人名、地名和汉语拼音。应使用那些约定俗成的缩写。多个单词组成的变量名,每个单词的首字母应该大写

(没有分值。PDF 114 / 书 111 是个人学习总结空表。)


项目6 智慧校园系统——数据存储模块(PDF 115 起 / 书 113 起)

项目概述:数据是物联网的核心驱动力,数据存储是智慧校园系统开发的重要组成部分。本项目把各传感器采集的数据存进数据库,实现存储和分析处理。数据库用 Microsoft SQL Server 2008 R2,结合 C# 编程实现采集数据的存储、更新、删除、修改等基本操作。

学习目标:

  • 理解数据库和数据表的基本概念,包括数据类型、表、字段、主键等概念。
  • 了解数据库设计的流程和关键点。
  • 掌握数据库SQL的基本语法。
  • 能够通过数据库对各类型物联网传感器采集的数据进行存取。
  • 能够在实际操作中连接摄像头,实现对摄像头开关和截图的操作。
  • 树立数据安全保密意识,依规保护数据安全。
  • 增强团队协作沟通能力,共同推动项目进展。

任务1 制作环境数据表(PDF 115–121 / 书 113–119)

任务描述:SQL Server 2008 R2 是微软的关系型数据库管理系统,它的数据库引擎为关系型数据和结构化数据提供更安全可靠的存储。本任务熟悉用数据库软件创建数据库和数据表,并能手动往表里添加、修改数据。学生要学会用 SQL Server 2008 R2 建库建表。

任务实施(全程在 SSMS 里操作,没有 C# 代码):

  1. 安装 SQL Server 2008 R2,打开 SQL Server Management Studio 并登录,主界面见图6-1(对象资源管理器里有:数据库、安全性、服务器对象、复制、管理、SQL Server 代理)。
  2. 在"对象资源管理器"里右击"数据库",选"新建数据库",名称填 "环境数据表",其余默认,点确定(图6-2/6-3)。新建数据库对话框里:所有者"<默认值>","使用全文索引"打勾;数据文件初始 3MB、按 1MB 增长、不限制;日志文件初始 1MB、按 10% 增长。
  3. 展开"环境数据表"数据库,右击"表",选"新建表"(图6-4/6-5)。
  4. 列名填 light、temperature、humidity,数据类型都选 varchar(MAX),"允许 Null 值"打勾(图6-6)。
  5. 点"保存",在"选择名称"对话框填表名 "EnvironmentWork"(图6-7)。
  6. 右击表 EnvironmentWork,选"编辑前200行",就能直接录入数据(图6-8)。右键菜单里还有:新建表、设计、选择前1000行、编写表脚本为、查看依赖关系、全文索引、存储、策略、方面、启动PowerShell、报表、重命名、删除、刷新、属性。
  7. 在 EnvironmentWork 里录入一周的环境数据,light 是光照、temperature 是温度、humidity 是湿度,内容自定。

对应的建表 SQL(书里没有,模拟器可以内置):

CREATE DATABASE 环境数据表;
CREATE TABLE EnvironmentWork (light varchar(MAX) NULL, temperature varchar(MAX) NULL, humidity varchar(MAX) NULL);
-- 没有主键,也没有时间列

知识提炼:

  1. 数据库(database,DB):长期存储在计算机内、有组织、可共享的数据集合,按一定的数据模型组织、描述和存储,冗余度小、数据独立性高、容易扩展,可以在一定范围内供多个用户共享。
  2. 数据库管理系统(DBMS):用户和操作系统之间的一层数据管理软件(如 SQL 数据库管理系统),用来建立、使用和维护数据库,统一管理和控制数据库,保证安全性和完整性。
  3. 数据表:数据库的组成部分,是实际存储数据的地方,每个数据库都有自己的数据表。表由行和列组成,建表就是定义列的过程。
  4. 字段与记录:表里的每一行叫一条记录,表示所存物体各方面的属性;每个数据项叫字段,表示物体某方面的性质,比如学生信息里的学号、姓名、出生日期。
  5. 主键:能唯一标识表中每一行,可以是一个字段,也可以是多个字段的组合,用来强制实体完整性。一个表只能有一个主键,主键列不能为空。例如学生表(学号、姓名、性别、班级)中学号唯一,就是主键。
  6. SQL 数据类型:SQL Server 有 20 多种系统数据类型,插入的值超出允许范围就会报错。常用的有:
    1. bit:只能是0、1或空值
    2. tinyint:0~255,占1B
    3. smallint:-32768~32767,占2字节
    4. int:-2147483648~2147483647,占4B
    5. money:-9220亿~9220亿,精确到货币单位的万分之一
    6. float:-1.79×10^308~1.79×10^308,是近似值
    7. smalldatetime:精确到1min
    8. datetime:精确到1/300s
    9. uniqueidentifier:GUID 全局唯一标识
    10. char:定长、非统一编码,必须指定列长
    11. varchar:变长,必须指定最大长度
    12. text:最多 2^31-1(约20亿)个字符
    13. nvarchar:统一编码、变长,能存4000个字符,占用字节翻倍
    14. ntext:2^30-1(近10亿)个字符,占用字节翻倍
    15. binary:最多8000B的定长二进制
    16. varbinary:最多8000B的变长二进制,内容大小会变时用它
    17. image:变长二进制,最多 2^31-1(约20亿)B

能力拓展·边想边做:

在数据库"环境数据表"中建立表"EnvironmentWorkNew",主键为id,整型,可以自增,"EnvironmentDay"为日期型date,"EnvironmentWork"为varchar(200),如图6-9所示。

图6-9 的表结构:id int 主键、不允许 Null;EnvironmentDay date 允许 Null;EnvironmentWork varchar(200) 允许 Null。自增要设 IDENTITY(1,1),对应 SQL 为 CREATE TABLE EnvironmentWorkNew (id int IDENTITY(1,1) PRIMARY KEY, EnvironmentDay date NULL, EnvironmentWork varchar(200) NULL);。

自主探究:

使用SQL语句,为新表"EnvironmentWorkNew"添加一行内容,"EnvironmentDay"的值为日期"2017年9月","EnvironmentWork"的值为"环境适宜"。

"2017年9月"不是完整的 date 值,参考答案应写成类似 INSERT INTO EnvironmentWorkNew (EnvironmentDay, EnvironmentWork) VALUES ('2017-09-01', '环境适宜');。模拟器的 SQL 引擎要支持 date 类型和 IDENTITY。

任务2 存储校园环境数据(PDF 121–125 / 书 119–123,范围在任务中间结束)

任务描述:动态采集环境数据,用程序代码存进 SQL 数据库。学生要了解 ADO.NET,学会表内容的添加、删除、修改。

任务实施步骤:

  1. 在"环境数据表"库里新建 "CampusEnvironment" 表,共5个字段:自增主键 ID,light(光照度)、humidity(湿度)、temperature(温度)三个都用 varchar(50),addtime 用 Datetime。对应 SQL:CREATE TABLE CampusEnvironment (ID int IDENTITY(1,1) PRIMARY KEY, light varchar(50), humidity varchar(50), temperature varchar(50), addtime datetime);
  2. 把 ADAM4017 的串口直接接到 PC(书上把 ADAM4017 叫成"数字量采集器",其实它是模拟量采集器)。注:没有硬件的读者跳过这一步。
  3. 新建"实时存储校园数据"项目,保存在 D:\物联网编程与应用\项目6\任务2。
  4. 复制 ADAM4017 的 DeviceCtrlLibrary.dll 和 API 到 ...\实时存储校园数据\实时存储校园数据\bin\Debug\。
  5. 添加引用(仿真注释同上)。
  6. 按图6-10建主窗体(表6-1),标题"校园灾情数据存储":
    • 左上 GroupBox"配置":串口下拉框、"开启"按钮。
    • 右上 GroupBox"数据显示":光照度、温度、湿度三个文本框,"获取""保存""修改""删除"四个按钮。
    • 下方 GroupBox"历史记录":一个大 DataGridView。
  7. 引用:
    using System.Data.SqlClient; //引用数据库操作库
    using System.IO.Ports;
    using DeviceCtrlLibrary;
    using DeviceCtrlLibrary.Model;//引用ADAM4017ResModel
    
  8. 7个全局变量:
    string sqlConnectString = @"Data Source= PC-20150908SCJF\SQLSERVER1026;Initial Catalog=环境数据表;User ID=sa;Password=123456";//数据库的连接字符串
    SqlConnection mySqlConnection;
    SqlCommand mySqlCmd;
    SqlDataAdapter sqldata;
    bool IsOpen = false;
    ADAM4017ResModel ResModel;
    DataSet ds;
    
    小贴士:其中"PC-20150908SCJF\SQLSERVER1026"为自己SQL数据库的名称,也可以用"."号来表示本机默认数据库名称,该代码采用"sa"用户名来连接数据库,密码为"123456",请根据实际情况修改数据库名称和"sa"的密码。
  9. Form1_Load:mySqlConnection = new SqlConnection(sqlConnectString); mySqlConnection.Open();,再用 foreach 填充 com_port。(Load 里没调用 Databind(),所以刚启动时表格是空的。)
  10. btn_Open_Click:if(!IsOpen){ DeviceCtrlHelper.adam4017Helper.Open(com_port.Text); btn_Open.Text="关闭"; IsOpen=True; } else { DeviceCtrlHelper.adam4017Helper.Close(); btn_Open.Text="开启"; IsOpen=False; }(书上的 True/False 是大写,引号也是全角 ”关闭”,原样都编译不过)。
  11. btn_get_Click(书上把 private void 印成了 privatevoid):ResModel = DeviceCtrlHelper.adam4017Helper.SetData(); txt_light.Text = ResModel.lightValue; txt_humidity.Text = ResModel.humidityValue; txt_temperature.Text = ResModel.temperatureValue;。这三个值都是 string。
  12. 数据绑定函数 Databind():
    mySqlConnection = new SqlConnection(sqlConnectString);
    mySqlConnection.Open();
    SqlCommand sqlcmd = new SqlCommand("select ID as '编号',temperature as '温度',humidity as '湿度',light as '光照度', addtime as '时间' from CampusEnvironment", mySqlConnection);
    sqldata = new SqlDataAdapter(sqlcmd);
    ds = new DataSet();
    sqldata.Fill(ds);
    dataGridView1.DataSource = ds.Tables[0].DefaultView;
    
    小贴士:select语句主要用于从表中选取数据。 (每次调用都新开一个连接,从不关闭。)
  13. btn_save_Click:
    string light = txt_light.Text; string temperature = txt_temperature.Text; string humidity = txt_humidity.Text;
    try
    {
        mySqlCmd = new SqlCommand("insert into CampusEnvironment  values (" + temperature + "," + humidity + "," + light + ", '"+DateTime.Now.ToString()+"')", mySqlConnection);
        mySqlCmd.ExecuteNonQuery();
        MessageBox.Show("保存成功!");
        Databind();
    }
    catch (Exception ex) { MessageBox.Show("保存失败:"+ex.ToString()); }
    
    问题:
    • INSERT 没写列名,值的顺序是 temperature、humidity、light,第1步建表的列顺序却是 light、humidity、temperature,数据会存错列。除非学生建表时按 temperature、humidity、light 的顺序。模拟器的 SQL 引擎必须严格按列顺序插入,才能让这个错误暴露出来,或者在题目说明里提醒学生。
    • 数值直接拼进 SQL、没加引号,存 varchar 列时靠隐式转换;值一旦不是数字就会报错。
    • DateTime.Now.ToString() 生成的字符串要能被 SQL 解析成 datetime。
  14. btnUpdate_Click:
    SqlCommandBuilder cmdb = new SqlCommandBuilder(sqldata);
    sqldata.UpdateCommand = cmdb.GetUpdateCommand();
    if (sqldata.Update(ds.Tables[0]) == 1) { MessageBox.Show("修改成功!"); Databind(); }
    else { MessageBox.Show("修改失败!"); }
    
    做法是让用户直接在 DataGridView 里改单元格,再提交。需要:DataGridView 可编辑并回写到 DataTable;SqlCommandBuilder 能根据带别名的 SELECT 和主键 ID 生成 UPDATE;== 1 表示只有恰好改了一行才算成功。
  15. btnDelete_Click:
    try
    {
        mySqlCmd = new SqlCommand("delete from CampusEnvironment where id="+dataGridView1.CurrentRow.Cells[0].Value.ToString(), mySqlConnection);
        mySqlCmd.ExecuteNonQuery();
        MessageBox.Show("删除成功!");
        Databind();
    }
    catch (Exception ex) //捕获数据库操作异常信息
    {
    
    范围到这里结束。后面的 catch 内容在下一段里,参考代码是 MessageBox.Show("修改失败:" + ex.ToString());。

表6-1 校园数据存储窗体界面控件属性

控件名称 控件类型 属性名 属性值 控件功能
Form1 窗体 Text 校园灾情数据存储
groupBox1 组合框 Text 数据显示
groupBox2 组合框 Text 配置
groupBox3 组合框 Text 历史记录
txt_light 文本框 输入/显示光照度值
txt_temperature 文本框 输入/显示温度值
txt_humidity 文本框 输入/显示湿度值
btn_get 按钮 Text 获取 控制数据获取
btn_save 按钮 Text 保存 保存数据到SQL数据库
btnUpdate 按钮 Text 修改 更新修改的数据
btnDelete 按钮 Text 删除 删除修改的数据
btn_Open 按钮 Text 开启 打开串口
com_port 下拉列表框 显示串口
lbl_light 标签 Text 光照度
lbl_temperature 标签 Text 温度
lbl_humidity 标签 Text 湿度
dataGridView1 数据网格控件 用于绑定数据库数据

书和参考项目的差异汇总(只列本段范围内的)

位置 书中 参考项目 / 实际情况
项目3 任务2 RadioButton 示例 步骤说"Click"事件 代码是 CheckedChanged(书里的示例,参考项目里没有)
项目3 任务2 CheckBox 示例 表3-8 控件名 checkBox_Bold/Italic/Underline/Strikeout 代码用 checkBox1–4
项目3 能力拓展 Split 提示 spstring[2]="ed" 应为 "ef"
项目3 能力拓展 ?: 提示 5>3 ? True:False C# 要写小写 true/false
项目4 任务1 表4-1 里标签名 lbl_port/lbl_time/lbl_even,没有 GroupBox 参考项目是 label1/label2/label3,还有 groupBox1"数据显示"
项目4 任务1/3 boolIsOpen = false; bool IsOpen = false;
项目4 任务2 using System.IO.Ports; 参考项目是 using System.IO;(Directory 要用它)
项目4 任务2 label1 Text"摄像机IP" 参考项目里是"摄像头IP"
项目4 任务3 表4-4 控件名"Alarm" 实例名 alarm1,类型 WinFormsDeviceControlLibrary.Alarm
项目5 任务1/2 表5-1/5-5 标签名 lbl_port/lbl_time/lbl_even 参考项目是 label1/2/3,加 groupBox1"数据显示"
项目5 任务2 FormClosing thd.Abort(); if(thd.ThreadState==ThreadState.Unstarted) thd.Abort();
项目5 任务3 Form1_Load for + continue 排除 COM1 两个 foreach,不排除 COM1
项目5 任务3 btn_get 烟雾分支 注释只有一个 /;内容记成" 发现火情" 参考项目注释正确,但同样记成" 发现火情"
项目5 任务3 Form2_Load listBox1.Items.Add 参考项目是 lstbRecored.Items.Add,和表5-7 一致
项目5 任务3 表5-6 没有 btn_get、OpenRecord、groupBox2 参考项目里有(获取 / 历史灾情信息 / 数据采集);Form1 里还有空的 com_ledPort_Click、com_environmentPort_Click、button1_Click
项目6 任务1 全部是 SSMS 操作(建库"环境数据表"、建表 EnvironmentWork),没有代码 参考项目 项目6/任务一 却是一个 WinForms 程序:连接串 Password=123,往 CampusEnvironment 插入3个值、不含 addtime。它像是任务2 的早期版本,和书上的任务1 对不上
项目6 任务2 连接串 Data Source= PC-20150908SCJF\SQLSERVER1026;…Password=123456 参考项目是 Data Source=.;…Password=123456
项目6 任务2 Form1_Load 打开连接、填充 com_port,不调 Databind 参考项目只调 Databind(),不填 com_port,串口下拉框一直是空的
项目6 任务2 Databind 别名 light as '光照度' 参考项目是 '光照';表6-1 标签"光照度",参考项目 lbl_light 是"光照"
项目6 任务2 groupBox3 历史记录 参考项目叫 groupBox4
项目6 任务2 btn_Open IsOpen=True/False,全角引号 参考项目是小写 true/false,代码正确
项目6 没有 SQL 脚本 配套资源里也没有 .sql/.mdf,模拟器要自己预置这些表

模拟器需要支持的、光看代码不明显的东西(本段范围)

  • WinForms 控件:
    • RadioButton、CheckBox:Checked 属性和 CheckedChanged 事件;CheckBox 还有 ThreeState、Appearance。
    • PictureBox:SizeMode 的 StretchImage/Zoom 等模式,BackColor,Image.FromFile,Image.Save(path)。
    • ListBox,DataGridView(可编辑,CurrentRow.Cells[0].Value)。
    • GroupBox,Timer(Interval 最大要能到 7200000),以及 Visible、Enabled、ForeColor 的动态切换(用于闪烁效果)。
    • Font(Font, FontStyle) 和 FontStyle 的位运算。
    • 多个窗体(Form2.Show()),多个 .cs 文件(RecordClass,带 static 字段)。
    • 窗体的 FormClosing 事件。
    • MessageBox.Show(text, caption)。
  • 自定义控件(WinFormsDeviceControlLibrary):Alarm.OnOff(bool)、LED.setLedText(string),项目3 用 Lamp.OnOff(bool)。这个控件库要先"添加到工具箱"才能用。
  • 设备 API(DeviceCtrlLibrary):
    • adam4150Helper(数字量):
      • 方法:Open(port)、Close()、SetData()、OnOff(int DO, bool)。
      • SetData() 返回 ADAM4150ResModel,字段有 infraredValue(DI3)、fireValue(DI1)、smokeValue(DI2)。
      • 继电器 DO0 是报警灯,DO1、DO2 在代码里叫 LED1、LED2。
    • adam4017Helper(模拟量):Open/Close/SetData(),SetData() 返回 ADAM4017ResModel,有 lightValue、humidityValue、temperatureValue 三个 string。
    • ipCameraHelper:
      • 方法:Open(ip, "admin", "")、Start()、Stop()、PanUp/PanDown/PanLeft/PanRight()。
      • 事件:ImageRead += handler(ImageReadEvenArgs e),用 e.img 取图。
      • 这个事件回调要能直接给 pictureBox1.Image 赋值(书里没做 Invoke)。
    • ledHelper:OpenLedCom(port);SendLedString(msg) 返回字符串,出错时含"Error"。
    • 命名空间 DeviceCtrlLibrary.Model(ADAM4150ResModel、ADAM4017ResModel)。
  • 串口和硬件:
    • SerialPort.GetPortNames() 要返回一组虚拟串口。项目5 任务3 会排除 COM1,书上的截图里却用了 COM1/COM2/COM3,所以至少要提供 COM1–COM3。
    • 串口服务器、路由器的配置只是硬件准备步骤,模拟器里不用体现。
    • 教室A/B 那类拓展题需要同时把 DI1、DI2 当两路火焰(或两路烟雾)传感器,所以模拟器里每个 DI 接什么传感器最好能配置。
  • 线程:
    • Thread 的 Start、Suspend、Resume、Abort、Sleep,ThreadState。
    • Control.CheckForIllegalCrossThreadCalls = false:后台线程直接改 UI 也要能生效。
  • 数据库(ADO.NET + T-SQL 子集):
    • ADO.NET 对象:SqlConnection(连接串).Open(),SqlCommand.ExecuteNonQuery(),SqlDataAdapter.Fill(DataSet),ds.Tables[0].DefaultView 作为 DataSource,SqlCommandBuilder.GetUpdateCommand() 和 adapter.Update(DataTable)(返回受影响行数)。
    • SQL 语句:带中文别名的 select ... as '编号' ...,不写列名的 insert into T values (...),delete from T where id=...。
    • 类型和库名:varchar(MAX)、varchar(50)、varchar(200)、date、datetime、int IDENTITY 主键;中文数据库名"环境数据表"。
    • 连接串:服务器名、sa 密码这些要能宽松接受,不管写的是 . 还是 PC-…\SQLSERVER1026 都映射到学生自己的沙箱库。
    • 任务1 的 SSMS 操作(新建数据库、新建表、编辑前200行)需要一个简化的网页版"表设计器 + 数据编辑"界面,或者改成让学生写 SQL。