原文:MicroPython for the Internet of Things

协议:CC BY-NC-SA 4.0

九、项目 2:交通信号灯模拟器

既然我们已经有了如何设计、连接和实现 MicroPython 项目的教程,现在让我们来看一个更高级的项目。在这种情况下,我们将使用一些非常基本的组件来进一步了解如何使用硬件。该项目的硬件选择将是发光二极管,电阻和一个按钮。按钮是最基本的传感器。也就是说,当按钮被按下时,我们可以让我们的 MicroPython 代码响应这个动作。

使用 led 可能更多的是一句“你好,世界!”风格项目的硬件,因为打开和关闭发光二极管很容易,除了搞清楚什么大小的限流电阻是必要的,接线发光二极管也很容易。像大多数基于 LED 的项目一样,我们将实现一个模拟。更具体地说,我们将实现一个交通灯和一个行人行走按钮。步行按钮是一个按钮,行人可以使用它来触发交通信号以改变和停止交通,以便他们可以穿过街道。

模拟项目可以很有趣,因为我们已经有了它应该如何工作的想法,我们正在模拟我们遇到的东西。例如,除非你住在非常偏远的地区,否则你很可能在十字路口遇到过带有按钮的“步行/禁止步行”标志的交通信号。如果你生活在城市,你会在各种配置中遇到这些。当一个行人(或骑自行车的人)按下步行按钮时,交通灯全部变成红色,步行标志被点亮。一段时间后(大约 30 秒),步行标志闪烁,然后大约 15 秒后,步行信号变为禁止步行,交通信号恢复正常周期。

Note

“循环”一词指的是一组线性作用的状态。因此,循环是指从一种状态到另一种状态的变化。

我们也将增加一个新的交通灯模拟概念。我们将使用一个网页来触发 walk 请求。但首先,我们将学习如何连接和编码 LED 灯,并在第二步添加 web 界面。

概观

在这一章中,我们将实现一个带有行人行走按钮的交通信号。这个项目使用 led,它允许我们看到代码执行时的状态。对于交通灯(也称为停止灯),我们将使用红色、黄色和绿色 led 来匹配交通灯上相同颜色的灯。我们还将使用一个红色和黄色的 LED 来对应禁止行走(红色)和行走(黄色)灯。

我们将使用按钮(也称为瞬时按钮),因为它只有在被按下时才会触发(开启)。释放后,它不再被触发(关闭)。Trigger 是用于描述按钮状态的词,其中 triggered 表示从按钮的一侧到另一侧的连接被连接(on)。保持触发(锁定)状态的按钮称为锁定按钮,通常必须再次按下才能关闭。

我们将通过首先仅打开绿色交通灯 LED 和红色步行 LED 信号来模拟交通灯和步行信号。这是我们将使用的正常状态。当按钮被按下时,交通灯将变为黄色几秒钟,然后变为红色。几秒钟后,行走信号将变为黄色,几秒钟后开始闪烁。几秒钟后,步行信号将返回红色,交通灯将返回绿色。

为了让事情变得更有趣,我们还将看到如何修改这个项目来使用一个从网页模拟的按钮。是的,我们将看到如何通过网络远程控制硬件和我们的代码。如果您使用的是 Pyboard 或另一种没有任何网络功能的 MicroPython 板,您将需要一个网络模块。随着项目的发展,我们将重新审视 Pyboard 的网络。

现在让我们看看这个项目需要哪些组件,然后我们将看看如何将所有组件连接在一起。

必需的组件

表 9-1 列出了你需要的部件。您可以从 Adafruit ( adafruit.com)、Sparkfun ( sparkfun.com)或任何出售电子元件的电子商店单独购买元件。如果您想购买这些组件,可以链接到供应商。当列出同一对象的多行时,您可以选择其中一行——您不需要两个都要。此外,您可能会找到销售这些组件的其他供应商。你应该货比三家,找到最好的交易。显示的成本是估计值,不包括任何运输成本。

表 9-1。

Required Components

| 成分 | 数量 | 描述 | 费用 | 链接 | | --- | --- | --- | --- | --- | | MicroPython 板 | one | 带标题的 Pyboard v1.1 版 | $45-50 | [`https://www.adafruit.com/product/2390`](https://www.adafruit.com/product/2390)[`https://www.adafruit.com/product/3499`](https://www.adafruit.com/product/3499) | | [`https://www.sparkfun.com/products/14413`](https://www.sparkfun.com/products/14413) | | [`https://store.micropython.org/store`](https://store.micropython.org/store) | | WiPy | $25 | [`https://www.adafruit.com/product/3338`](https://www.adafruit.com/product/3338) | | [`https://www.pycom.io/product/wipy/`](https://www.pycom.io/product/wipy/) | | 发光二极管 | Two | 红色指示灯 | 装备 | [`https://www.adafruit.com/product/2975`](https://www.adafruit.com/product/2975) | | 发光二极管 | Two | 黄色发光二极管 | 装备 | [`https://www.adafruit.com/product/2975`](https://www.adafruit.com/product/2975) | | 发光二极管 | one | 绿色发光二极管 | 装备 | [`https://www.adafruit.com/product/2975`](https://www.adafruit.com/product/2975) | | 电阻器 | five | 220 或 330 欧姆电阻 | $8-12 | [`https://www.sparkfun.com/products/10969`](https://www.sparkfun.com/products/10969) | | 纽扣 | one | 瞬时按钮,试验板友好 | 装备 | [`https://www.sparkfun.com/products/12708`](https://www.sparkfun.com/products/12708) | | 面包板 | one | 原型板,半尺寸 | $5 | [`https://www.sparkfun.com/products/12002`](https://www.sparkfun.com/products/12002) | | 网络模块(Pyboard) | one | CC3000 分线板(或同等产品) | $15+ | 各种各样的 | | 跳线(WiPy) | nine | M/M 跳线,6”(10 根跳线的成本) | $4 | [`https://www.sparkfun.com/products/8431`](https://www.sparkfun.com/products/8431) | | 跳线(线路板) | Seventeen | M/M 跳线,6”(10 根跳线的成本) | $4 | [`https://www.sparkfun.com/products/8431`](https://www.sparkfun.com/products/8431) | | 力量 | one | 从电脑获取电源的 USB 电缆 |   | 使用您的备件 | | one | USB 5V 电源和电缆 |   | 使用您的备件 |

注意在成本中,led 和按钮的“套件”。这是指这些组件可以在我们在第二章看到的 Adafruit 的零件 Pal 套件中找到。其他供应商可能有类似的套件。成套购买 led、按钮和电阻等基本元件要便宜得多。

同样,你可以买一套各种尺寸的电阻,比一次买几个便宜得多。事实上,你很可能会发现,购买一个每种尺寸 5 或 10 个电阻的小套件比购买一套要贵得多。Sparkfun 的套件将为您提供大多数项目所需的所有电阻器。

另外,请注意,WiPy 需要的跳线要少得多。这是因为我们将使用网络分线板(在本例中为 CC3000 模块)来允许 Pyboard 连接到我们的网络。

最后,注意我们需要一个 Pyboard 友好的网络模块。同样,目前这必须是基于 CC3000 的板或基于 WIZNET5K 的板。有关与 Pyboard 配合使用的网络分线板的示例,请参考前面的章节。

回想一下第七章,LED 需要一个限流电阻来将电流降低到 LED 的安全水平。为了确定我们需要多大的电阻,我们需要了解 LED 的几个方面。该数据可从制造商处获得,制造商以数据表的形式提供数据,或者在商业包装产品的情况下,在包装上列出数据。我们需要的数据包括最大电压、电源电压(LED 的电压)和 LED 的额定电流。

例如,如果我有一个像 Adafruit Parts Pal 中的 LED,在这种情况下是一个 5 毫米红色 LED,我们在 Adafruit 的网站(adafruit.com/products/297)上发现该 LED 的工作电流为 1.8-2.2 伏和 20 毫安。假设我们希望使用 5V 电源电压。然后我们可以将这些值代入这个公式:

R = (Vcc-Vf)/I

使用更具描述性的变量名称,我们得到如下结果。

Resistor = (Volts_supply - Volts_forward) / Desired_current

把我们的数据代入,我们得到这个结果。注意,我们有 mA,所以我们必须使用正确的十进制值(除以 1000)。在这种情况下,它是 0.020,我们将在中间选择一个电压。

Resistor = (51.8) / 0.020
         = 3.2 / 0.020
         = 160

因此,我们需要一个 160 欧姆的电阻。然而,没有这种额定值的电阻。当这种情况发生时,我们用大一号的。例如,如果您只有 220 欧姆甚至 330 欧姆的电阻,您可以使用这些电阻。结果将是发光二极管将不会那么亮,但是具有较高的电阻比使用太小的电阻要安全得多。电流太大,LED 会烧坏。

现在,让我们看看如何将组件连接在一起。

安装硬件

虽然这个项目需要使用 WiPy 连接很多电线,Pyboard 甚至需要更多电线,但我们将使用的元件很容易插入试验板。表 9-2 显示了本项目所需的连接。

表 9-2。

Connections for the MicroPython (Pyboard and WiPy)

| MicroPython 板 |   |   | | --- | --- | --- | | WiPy | Pyboard | 成分 | 电线颜色 | | --- | --- | --- | --- | | P3 | X7 | 停车灯:红色 LED |   | | P4 | X6 | 停车灯:黄色 LED |   | | 孕烯醇酮 | X5 | 停车灯:绿色 LED |   | | P6 | X4 | 行走信号:红色 LED |   | | P7 | X3 | 行走信号:黄色 LED |   | | P23 | X1 | 纽扣 |   | | 地线 | 地线 | 试验板 |   |

让我们来复习一些元件接线的技巧。将元件连接到电路板的最佳方式是使用试验板。正如我们在第七章中看到的,试验板允许我们插入组件,并使用跳线进行连接。在本项目中,我们将使用一根跳线从 MicroPython 板接地到试验板,然后在试验板上跳线连接到按钮。事实上,我们将使用试验板一侧的接地轨来插入 led 的一侧。

只要插销的方向如图所示,按钮在任何位置都可以工作–两条腿在中心槽的一侧。如果按钮的支脚可以伸到槽的任何一侧,按钮的方向就会正确。如果你把它移开 90 度,按钮要么不起作用,要么总是被触发。如果有任何疑问,使用万用表测试按钮连接的连续性。你会发现连接在未按下时打开,按下时关闭。

唯一极化的组件是 LED(它有一个正极和一个负极引脚)。当您查看 LED 时,您会看到 LED 的一条腿(引脚)比另一条腿长。这条较长的边是正面。我们将插入 led,使负极引脚插入接地轨,正极引脚插入试验板的主要区域。然后我们插入电阻,跳过中心槽,将电阻连接到 MicroPython 板上的 GPIO 引脚。不管你往哪个方向插电阻,它们都可以双向工作。

如果这听起来令人困惑,不要担心,因为接线图使连接更加明显。让我们看看如何连接 WiPy 和 Pyboard,如图所示。

WiPy

WiPy 的布线最好也是将 USB 连接器朝向扩展板的右侧。图 9-1 显示了 WiPy 的接线图。注意发光二极管、电阻和按钮的方向。您应该能够使用图纸和布线图来连接您自己的组件。另外,请注意 WiPy 的方向。这应该有助于您更容易地将引脚与试验板的导线对齐,但只要您使用正确的 GPIO 引脚,物理方向并不重要。

A447395_1_En_9_Fig1_HTML.jpg

图 9-1。

Wiring the Stoplight Simulation (WiPy)

现在,让我们看看 Pyboard 的接线图。由于需要添加一个网络分线板,所以变得有点复杂。

Pyboard

Pyboard 的布线最好将 USB 连接器朝向左侧。这将允许您读取电路板上的引脚数,即使在电线插入电路板之后。网络模块需要额外的连接。回想一下,CC3000 分线板需要额外的布线来将 SPI 接口连接到 Pyboard。就像我们对 led、电阻器和按钮所做的那样,我们应该计划如何连接分线板。表 9-3 显示了与 CC3000 分线板一起使用时 Pyboard 所需的连接。如果您使用不同的分线板,请确保使用您的连接对此计划进行注释。

表 9-3。

Additional Connections for the Pyboard and CC3000 Breakout Board

| Pyboard | CC3000 | 电线颜色 | | --- | --- | --- | | Y3 | 伊拉克 |   | | Y4 | VBEN .好吧 |   | | Y5 | 特许测量员 |   | | Y6 | 时钟信号 |   | | Y7 | 军事情报部门组织(Military Intelligence Service Organization) |   | | Y8 | 工业博物馆 |   | | V+ | 车辆识别号码 |   | | 地线 | 地线 |   |

由于需要这些额外的导线,Pyboard 布线图有点复杂。图 9-2 显示了带有 CC3000 分线板的 Pyboard 的接线图。

A447395_1_En_9_Fig2_HTML.jpg

图 9-2。

Wiring the Stoplight Simulation (WiPy)

哇,好多电线啊!您可以看到使用内置 WiFi 的 MicroPython 板的优势——您不必使用额外的电线,这些电线有时会使一个简单的项目变得更加复杂,或者当您看着您的项目全部连接起来时,它看起来像一个电线窝。

Note

如果您使用 Pyboard,您可以使用板载指示灯,而不是单独的指示灯。

最后,一定要确保仔细连接项目,仔细检查所有连接,尤其是电源、接地和用于信号的任何引脚(将设置为“高”或“开”),如用于 SPI 接口的引脚。最重要的是,当项目(或您的板)通电时,切勿插拔跳线。这很可能会损坏您的主板或组件。

Caution

项目通电时,切勿插拔跳线。您可能会损坏您的主板或组件。

同样,在给主板通电之前,请务必仔细检查您的连接。现在,我们来谈谈我们需要编写的代码。暂时不要启动您的主板——在我们准备好测试该项目之前,还需要进行大量的讨论。

写代码

现在是时候为我们的项目编写代码了。代码并不太复杂,但是比迄今为止的例子要长一点。因此,我们将分两部分编写代码。在第一部分中,我们将看到如何编写代码来模拟行人人行横道按钮和交通灯。在第二部分中,我们将放弃使用硬件按钮,而是使用 web 浏览器来远程控制按钮动作。

Note

这些部分演示了 WiPy 的代码,并描述了 Pyboard 的不同之处。有关每块电路板的代码,请参见本章末尾的完整代码列表。

正如您将看到的,第二部分将重用第一部分的大部分代码,但是 HTML 服务器代码会稍微复杂一些。让我们从项目的第一部分开始。

第一部分:交通信号灯模拟器-使用按钮

该项目的第一部分的代码将需要监控按钮,当按下时,如上所述循环灯。我们还需要代码来初始化 led,将它们设置为初始关闭。我们可以编写函数来监控按钮和循环发光二极管。我们将使用一个中断来将按钮的功能绑定到硬件上,这样我们就可以避免使用轮询循环。

进口

该项目的导入将需要来自machine库和utime库的Pin类。下面显示了 WiPy 的导入。

from machine import Pin
import utime

Pyboard 的导入还需要来自pyb库的Pin类以及delay函数和ExtInt类。ExtInt类用于设置按钮被按下时触发的中断。

from pyb import Pin, delay, ExtInt

设置

这个项目的设置代码将需要初始化按钮和 LED 实例,然后关闭所有的 LED(作为一种预防措施),打开绿色的交通信号灯 LED 和红色的步行信号 LED。清单 9-1 显示了设置和初始化的代码。

# Setup the button and LEDs
button = Pin('P23', Pin.IN, Pin.PULL_UP)
led1 = Pin('P3', Pin.OUT)
led2 = Pin('P4', Pin.OUT)
led3 = Pin('P5', Pin.OUT)
led4 = Pin('P6', Pin.OUT)
led5 = Pin('P7', Pin.OUT)

# Setup lists for the LEDs
stoplight = [led1, led2, led3]
walklight = [led4, led5]

# Turn off the LEDs
for led in stoplight:
    led.value(0)
for led in walklight:
    led.value(0)

# Start with green stoplight and red walklight
stoplight[2].value(1)
walklight[0].value(1)

Listing 9-1.Setup and Initialization of the Button and LEDs (WiPy)

需要注意的一点是按钮是如何初始化的。这是一个被设置为输入(读取)的Pin对象实例,上拉电阻打开。这使得电路板能够检测到按钮何时被按下,因为在建立连接时(按钮被按下),引脚的值将为正值。

还要注意,我创建了一个列表,其中包含交通灯和步行信号的 led(代码中称为 walklight)。这主要是为了演示,这样您可以看到如何管理类对象列表。正如您所看到的,使用一个循环为列表中的所有对象调用同一个函数更容易。请注意这项技术,因为在其他项目中您会不时地需要它。

Pyboard 的代码基本相同。不同之处包括 led 和按钮的不同引脚编号(见接线图),引脚初始化的不同选项(Pin.OUT_PP而非Pin.OUT),以及Pin类使用不同的功能:high()用于value(1),而low()用于value(0)

功能

项目的这一部分需要两个功能。首先,我们需要一个函数来循环灯光。其次,我们需要一个功能来监控按钮的按下。我们来看一下循环灯功能。

我们将周期灯功能命名为cycle_lights()。回想一下,我们需要控制灯光如何改变状态。如前所述,我们以特定的周期来完成这项工作。总的来说,当我们想要模拟按下行走请求按钮时交通灯的变化时,我们调用这个函数。因此,这个函数将从按钮的代码中调用。清单 9-2 显示了cycle_lights()按钮的代码。正如您将看到的,代码相当简单。唯一棘手的部分可能是用于闪烁黄色行走 LED 的回路。一定要通读一遍,这样你才能理解它是如何工作的。

# We need a method to cycle the stoplight and walklight
#
# We toggle from green to yellow for 2 seconds
# then red for 20 seconds.
def cycle_lights():
    # Go yellow.
    stoplight[2].value(0)
    stoplight[1].value(1)
    # Wait 2 seconds
    utime.sleep(2)
    # Go red and turn on walk light
    stoplight[1].value(0)
    stoplight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    walklight[0].value(0)
    walklight[1].value(1)
    # After 10 seconds, start blinking the walk light
    utime.sleep(1)
    for i in range(0,10):
        walklight[1].value(0)
        utime.sleep_ms(500)
        walklight[1].value(1)
        utime.sleep_ms(500)

    # Stop=green, walk=red
    walklight[1].value(0)
    walklight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    stoplight[0].value(0)
    stoplight[2].value(1)

Listing 9-2.The 
cycle_lights() Function

(WiPy)

Pyboard 的代码与前面提到的控制 led 和使用delay()功能代替utime类睡眠功能的变化非常相似。

我们将这个按钮功能命名为button_pressed()。该函数用作按钮按下中断的回调函数。从技术上讲,我们告诉 MicroPython 将这个方法与 pin 中断相关联,但是我们马上就会看到这一点。然而,这一功能还有另一个要素需要解释。

当我们使用像按钮这样的组件并且用户(您)按下按钮时,按钮中的触点不会立即从关闭状态变为打开状态。在很短的一段时间内,读数不稳定。因此,我们不能简单地说“当引脚变高时”,因为引脚上读取的值可能会从低到高(或从高到低)快速“反弹”。这叫弹跳。我们可以通过代码(以及其他技术)人为地克服这一点,称为去抖动。

在这种情况下,我们可以随时检查 pin(按钮)的值,并且只有当值在这段时间内保持稳定时才“触发”按钮按压。清单 9-3 中显示了引脚去抖动的代码。注意,在循环中,我们等待的值是 50。这是 50 毫秒。如果触发器足够长,我们调用cycle_lights()函数。

def button_pressed(line):
    cur_value = button.value()
    active = 0
    while (active < 50):
        if button.value() != cur_value:
            active += 1
        else:
            active = 0
        utime.sleep_ms(1)
        print("")
    if active:
        cycle_lights()
    else:
        print("False press")
Listing 9-3.The 
button_pressed() Function

(WiPy)

Tip

有关去抖和避免去抖技术的更多信息,请参见 http://www.eng.utah.edu/∼cs5780/debouncing.pdf .

最后,我们需要设置当板卡检测到中断时调用button_pressed()函数的按钮。下面设置了 WiPy 上的回调函数。

# Create an interrupt for the button
button.callback(Pin.IRQ_FALLING, button_pressed)

Pyboard 上的代码也是单行代码,但是在这种情况下,我们必须使用ExtInt类来设置中断处理程序,如下所示。

# Create an interrupt for the button
e = ExtInt('X1', ExtInt.IRQ_FALLING, Pin.PULL_UP, button_pressed)

Tip

有关对 WiPy 使用引脚回调和对 Pyboard 使用中断的更多信息,请参见在线 MicroPython 文档。

现在我们已经准备好测试代码了。继续打开一个名为ped_part1_wipy.py(或者 Pyboard 的ped_part1_pyb.py)的新文件,输入上面的代码。清单 9-4 显示了项目第一部分的完整代码。如果你正在使用 Pyboard,通过查看本章末尾的完整列表来作弊是可以的。

# MicroPython for the IOT - Chapter 9
#
# Project 2: A MicroPython Pedestrian Crosswalk Simulator
#            Part 1 - controlling LEDs and button as input
#
# Required Components:
# - WiPy
# - (2) Red LEDs
# - (2) Yellow LEDs
# - (1) Green LED
# - (5) 220 Ohm resistors
# - (1) breadboard friendly momentary button
#
# Note: this only runs on the WiPy.
#
# Imports for the project
from machine import Pin
import utime

# Setup the button and LEDs
button = Pin('P23', Pin.IN, Pin.PULL_UP)
led1 = Pin('P3', Pin.OUT)
led2 = Pin('P4', Pin.OUT)
led3 = Pin('P5', Pin.OUT)
led4 = Pin('P6', Pin.OUT)
led5 = Pin('P7', Pin.OUT)

# Setup lists for the LEDs
stoplight = [led1, led2, led3]
walklight = [led4, led5]

# Turn off the LEDs
for led in stoplight:
    led.value(0)
for led in walklight:
    led.value(0)

# Start with green stoplight and red walklight
stoplight[2].value(1)
walklight[0].value(1)

# We need a method to cycle the stoplight and walklight
#
# We toggle from green to yellow for 2 seconds
# then red for 20 seconds.
def cycle_lights():
    # Go yellow.
    stoplight[2].value(0)
    stoplight[1].value(1)
    # Wait 2 seconds
    utime.sleep(2)
    # Go red and turn on walk light
    stoplight[1].value(0)
    stoplight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    walklight[0].value(0)
    walklight[1].value(1)
    # After 10 seconds, start blinking the walk light
    utime.sleep(1)
    for i in range(0,10):
        walklight[1].value(0)
        utime.sleep_ms(500)
        walklight[1].value(1)
        utime.sleep_ms(500)

    # Stop=green, walk=red
    walklight[1].value(0)
    walklight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    stoplight[0].value(0)
    stoplight[2].value(1)

# Create callback for the button
def button_pressed(line):
    cur_value = button.value()
    active = 0
    while (active < 50):
        if button.value() != cur_value:
            active += 1
        else:
            active = 0
        utime.sleep_ms(1)
        print("")
    if active:
        cycle_lights()
    else:
        print("False press")

# Create an interrupt for the button
button.callback(Pin.IRQ_FALLING, button_pressed)

Listing 9-4.Stoplight Simulation – Part 1 (WiPy)

测试和调试代码

测试项目的这一部分需要将源代码文件复制到板上,然后执行它。由于代码是在没有run()函数的情况下编写的,所以只需简单地导入它就可以运行代码了。在给主板通电之前,请务必检查所有连接。回想一下,连接到电路板后,我们用来导入和运行代码的命令如下所示。

>>> import ped_part1_wipy

一旦导入,代码将运行,您可以按下按钮来查看灯光循环通过阶段。如果您没有看到任何灯亮起(绿色停车灯和红色行走信号灯应该亮起),请检查设置和初始化代码。如果按下按钮时灯不闪烁,请检查按钮的代码以确保它是正确的。如果它们没有按顺序亮起,可能是针脚接线不正确。遇到问题时,请务必检查所有接线连接,在断开或重新连接任何电线或组件之前,请务必关闭主板电源。

当项目运行时,尝试几次,以确保它按预期运行。那你应该恭喜你自己!您刚刚将几个分立的电子元件连接在一起,制作了一个行人行走按钮和交通灯的工作模拟。酷!

现在,让我们把这个项目提升一个档次,让它可以通过互联网访问。毕竟,这就是 IOT 的全部!

第二部分:交通信号灯模拟器 HTML 远程控制

这一部分的代码将使用第一部分的所有代码,但我们不需要按钮的代码。相反,我们将使用Socket类创建一个监听器来监听来自 web 浏览器的连接。该代码将向客户端发送一个简短的基于 HTML 的响应(一个简单的 web 页面),其中包括一个包含两个按钮的表单——一个用于 walk 请求,另一个用于关闭服务器代码。侦听器将在端口 80 上侦听。

Note

如果您正在使用 WiPy,您不需要添加任何网络代码,但是如果您想在自己的网络上运行该项目或者将它连接到 Internet,我将向您展示如何添加代码。

HTML 服务器的概念非常简单。代码监听套接字端口上的连接,然后接收请求(在本例中以 HTML GET 方法的形式)并发送 HTML 响应(网页)。然后,代码检查请求是否包含来自两个按钮之一的表单数据:一个按钮请求步行周期,另一个按钮关闭服务器。正如您将看到的,关闭服务器迫使我们将代码写得更加整洁。

如果您以前从未使用过 HTML 代码,不要担心,因为示例代码将提供您需要的一切。您不必学习 HTML 来使用本章(或下一章)中的项目,但是如果您想详细说明该项目或在您自己的项目中使用 HTML 服务器概念,一些基础知识会很有帮助。在 https://www.w3schools.com/html/ 可以找到关于 HTML 的一个很好的信息源。

让我们看看 imports 部分需要做的更改。

进口

在导入部分我们还需要几个库。我们需要socket库(为了清楚起见,用别名重新命名了)machine库,以及来自network库的WLAN类。以这种方式编写导入是完全正常的,但是只要有一点想象力,您就可以简化它们。你明白了吗?下面显示了 WiPy 的完整导入部分。

from machine import Pin
import usocket as socket
import utime
import machine
from network import WLAN

Pyboard 的导入有点短。我们需要为网卡添加SPI库和network库以及socket库。下面显示了 Pyboard 的完整导入部分。

from pyb import Pin, delay, ExtInt, SPI
import network
import usocket as socket

现在,让我们看看设置部分的变化。

设置

对于项目的这一部分,我们需要添加代码,通过无线连接将我们的电路板连接到我们的网络(或互联网)。我们还需要将 HTML 代码以字符串的形式放在这里。这通常是在 Python 中定义大字符串的地方(和方式)。在任一侧使用三重引号会使字符串成为文档字符串(也称为 docstring)。详见 https://www.python.org/dev/peps/pep-0257/

WiPy 的网络代码与我们在本书前面看到的代码相同。在这种情况下,它设置无线网络功能以连接到现有的无线网络,而不是 WiPy 的默认接入点行为。清单 9-5 显示了 WiPy 的网络代码。

# Setup the board to connect to our network.
wlan = WLAN(mode=WLAN.STA)
nets = wlan.scan()
for net in nets:
    if net.ssid == 'YOUR_SSID':
        print('Network found!')
        wlan.connect(net.ssid, auth=(net.sec, 'YOUR_PASSWORD'), timeout=5000)
        while not wlan.isconnected():
            machine.idle() # save power while waiting
        print('WLAN connection succeeded!')
        print("My IP address is: {0}".format(wlan.ifconfig()[0]))
        break
Listing 9-5.
Wireless Network Setup

(WiPy)

Pyboard 的网络代码与我们在本书前面看到的代码相同。在这种情况下,它首先设置 SPI 接口到网络板(在本例中是 CC3000 分线板),然后启动到现有无线网络的无线连接,而不是 WiPy 的默认接入点行为。清单 9-6 显示了 Pyboard 的网络代码。

# Setup network connection
nic = network.CC3K(SPI(2), Pin.board.Y5, Pin.board.Y4, Pin.board.Y3)
# Replace the following with yout SSID and password
nic.connect("YOUR_SSID", "YOUR_PASSWORD")
print("Connecting...")
while not nic.isconnected():
    delay(50)
print("Connected!")
print("My IP address is: {0}".format(nic.ifconfig()[0]))
Listing 9-6.Wireless Network Setup (Pyboard)

Tip

您必须更改代码中的 SSID 和密码以匹配您的网络。

设置的另一部分是 HTML 响应代码。这似乎是代码中很难的部分,但是很简单。我们正在构造一个字符串,我们将通过套接字把它发送回客户机。该字符串包含以头开始的 HTML 代码。然后,我们提供一个标题(出现在浏览器的标题栏中),一些显示在页面上的文本,以及一个包含这两个按钮的表单。这些按钮的形式是,当每个按钮被按下时,都会通过套接字向服务器发送一个 HTML GET 请求。简单!清单 9-7 显示了项目的 HTML 字符串。还是那句话,不用担心细节。你可以以后再改进网页。

# HTML web page for the project
HTML_RESPONSE = """<!DOCTYPE html>
<html>
  <head>
    <title>MicroPython for the IOT - Project 2</title>
  </head>
  <center><h2>MicroPython for the IOT - Project 2</h2></center><br>
  <center>A simple project to demonstrate how to control hardware over the Internet.</center><br><br>
  <form>
    <center>
        <button name="WALK" value="PLEASE" type="submit" style="height: 50px; width: 100px">REQUEST WALK</button>
        <br><br>
        <button name="shutdown" value="now" type="submit" style="height: 50px; width: 100px">Stop Server</button>
    </center>
  </form>
</html>
"""
Listing 9-7.
HTML Response String

注意代码(<button></button>标签中的按钮)。名称和值将在请求中发送到服务器。按钮的类型被定义为 submit,当按钮被放置在表单上时,当按钮被按下时,会使客户端将表单数据发送到服务器。最后,结束标记前的字符串是将显示在按钮上的标签。

如果你担心这是一个占用内存的大字符串,你是对的,确实如此。如果您正在计划一个使用大量 HTML 响应或者多个响应的项目,您可能希望考虑将这些响应存储在一个文件中(每个文件一个响应),并在将数据发送到客户端之前从文件中读取数据。这将节省一些数据空间,并可能对使用更多内存的大型项目产生影响。

不要在意这里使用的行数。空白主要是用来装饰的,所以如果你想减少它的整体视觉尺寸,你可以去掉空白,但是通常的做法是像这样缩进 HTML 代码以便于阅读。

Tip

如果您担心安全性(谁不担心呢?),您可以使用安全套接字层连接来使您与 MicroPython 板的连接更加安全。MicroPython 提供了一个名为ussl的类供您使用。更多信息参见ussl类文档,或者你可以看看 https://github.com/micropython/micropython/blob/master/examples/network/http_server_ssl.py 的例子。

关于在 MicroPython 板上使用 HTML,还有一件重要的事情需要弄清楚:这个例子和您可能在互联网上找到的许多其他例子不应该与健壮的 web 服务器混淆。更具体地说,这个例子只是在侦听网络套接字时,一旦检测到来自客户机的 HTML GET 方法(请求),就将 HTML 发送回客户机。所以,这个例子仅仅是一个简单的 HTML 服务器,而不是一个 web 服务器,因为它能做 web 服务器能做的所有事情——它不能。因此,您应该注意对项目进行编码,将操作限制在特定的 GET(或 POST)请求上,并返回适当的 HTML 响应。

Using Web Pages with Images

由于 MicroPython 中还没有内置的 web 服务器,因此为 web 页面提供图像或任何其他媒体是有问题的。然而,你可以使用一个小技巧和你的电脑在你的项目中使用图像。我们可以在我们的 PC 上使用 Python 中的SimpleHTTPServer来提供一个基本的 web 服务器。在我们的 MicroPython 板的 HTML 中,我们可以对图像使用img标签,它使用一个 URL 指向我们 PC 上的文件。例如,下面的 HTML 将引用与我们 PC 上的 HTTP 服务器在同一个文件夹中的图像。

<html>
  <head>
    <title>MicroPython for the IOT - Project 2</title>
    <meta http-equiv="refresh" content="10">
  </head>
  <body>
    <p>Pedestrian Stoplight Simulation</p>
    <form>
        <img src="http://localhost:8000/red.png">
    </form>
  </body>
</html>

注意,img标签使用了一个到本地主机 8000 端口的 URL。当我们在 PC 上运行 Python 中的SimpleHTTPServer时,我们确定了这一点,如下所示。在您想要使用的图像所在的文件夹中运行此命令。

$ python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
127.0.0.1 - - [07/Aug/2017 14:42:30] "GET /red.png HTTP/1.1" 200 -

这创建了一个混合解决方案,它将允许您在您的 MicroPython 基于 web 的项目中使用图像,尽管需要我们的 PC(或您网络上的任何其他 PC)的一点帮助。

现在我们来看看如何制作run()函数。

功能

代码中剩下要做的就是更改硬件按钮的代码,并创建一个包含代码主要部分的run()函数。在这个项目中,包括设置套接字的代码以及监听和响应代码。您可以保留button_pressed()功能,但它不是必需的。清单 9-8 显示了run()函数的代码。

# Setup the socket and respond to HTML requests
def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.listen(5)
    server_on = True
    while server_on:
        client, address = sock.accept()
        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)
        # Process the request from the client. Payload should appear in pos 6.
        # Check for walk button click
        walk_clicked = (request[:17] == b'GET /?WALK=PLEASE')
        # Check for stop server button click
        stop_clicked = (request[:18] == b'GET /?shutdown=now')
        if walk_clicked:
            print('Requesting walk now!')
            cycle_lights()
        elif stop_clicked:
            server_on = False
            print("Goodbye!")
        client.send(HTML_RESPONSE)
        client.close()
Listing 9-8.The 
run() Function

(WiPy)

注意,我们从设置套接字并将其绑定到端口 80 开始这个函数。bind()函数使用空字符串来指示套接字使用当前的 IP 地址。接下来,我们使用listen(5)函数告诉套接字侦听套接字,超时时间为 5 秒。然后,我们设置一个布尔变量,当它为真时,在监听和响应代码上循环。在这种情况下,我们使用布尔值,并在按下关闭按钮时将其设置为 False。

循环内部是事情变得有趣的地方。我们做的第一件事是使用sock.accept()接受来自套接字的连接,它返回两项:客户机对象实例和客户机的地址。我们可以打印出客户端的地址以供调试之用。

接下来,我们告诉客户端通过client.recv(1024)调用接收多达 1024 个字节。这将一直等到有来自客户端的响应,所以请注意这不是一个抢占式循环——它将一直等到客户端响应。当客户端发送数据时(比如第一次连接或按下按钮时),我们可以搜索为命令发送的字符串。

注意,在代码中,我们检查搜索b'GET /?WALK=PLEASE'的行走请求或搜索b'GET /?shutdown=now'的关闭事件。这里有一点需要解释的诡计。我们使用请求字符串作为一个数组,从字符串中的字符数(17 或 18)开始搜索,用[:17][:18]表示。

如果检测到 walk 请求,我们就调用cycle_lights()函数,这是第一部分中没有修改的函数。如果检测到关机事件,我们将循环设置为终止,然后退出run()功能。

花些时间通读代码,直到你理解它是如何工作的。准备好后,打开一个名为ped_part2_wipy.py(或者 Pyboard 的ped_part2_pyb.py)的新文件,输入上面的代码。

最后,您必须删除设置按钮回调(或 Pyboard 上的ExtInt()调用)的代码行。把它留在里面不会损害代码,但是因为我们不使用按钮,所以不需要它,所有不必要的代码都应该删除。

现在,让我们看看 WiPy 和 Pyboard 的完整代码。

完整代码

在本节中,我们将看到 WiPy 和 Pyboard 的最终完整代码。这些列表供您参考,以确保您拥有适合您的主板的正确代码。清单 9-9 显示了在 WiPy 上运行项目的完整代码。

# MicroPython for the IOT - Chapter 9
#
# Project 2: A MicroPython Pedestrian Crosswalk Simulator
#            Part 3 - controlling LEDs over the Internet
#
# Required Components:
# - WiPy
# - (2) Red LEDs
# - (2) Yellow LEDs
# - (1) Green LED
# - (5) 220 Ohm resistors
#
# Note: this only runs on the WiPy.
#
# Imports for the project
from machine import Pin
import usocket as socket
import utime
import machine
from network import WLAN

# Setup the board to connect to our network.
wlan = WLAN(mode=WLAN.STA)
nets = wlan.scan()
for net in nets:
    if net.ssid == 'YOUR_SSID':
        print('Network found!')
        wlan.connect(net.ssid, auth=(net.sec, 'YOUR_PASSWORD'), timeout=5000)
        while not wlan.isconnected():
            machine.idle() # save power while waiting
        print('WLAN connection succeeded!')
        print("My IP address is: {0}".format(wlan.ifconfig()[0]))
        break

# HTML web page for the project
HTML_RESPONSE = """<!DOCTYPE html>
<html>
  <head>
    <title>MicroPython for the IOT - Project 2</title>
  </head>
  <center><h2>MicroPython for the IOT - Project 2</h2></center><br>
  <center>A simple project to demonstrate how to control hardware over the Internet.</center><br><br>
  <form>
    <center>
        <button name="WALK" value="PLEASE" type="submit" style="height: 50px; width: 100px">REQUEST WALK</button>
        <br><br>
        <button name="shutdown" value="now" type="submit" style="height: 50px; width: 100px">Stop Server</button>
    </center>
  </form>
</html>
"""

# Setup the LEDs (button no longer needed)
led1 = Pin('P3', Pin.OUT)
led2 = Pin('P4', Pin.OUT)
led3 = Pin('P5', Pin.OUT)
led4 = Pin('P6', Pin.OUT)
led5 = Pin('P7', Pin.OUT)

# Setup lists for the LEDs
stoplight = [led1, led2, led3]
walklight = [led4, led5]

# Turn off the LEDs
for led in stoplight:
    led.value(0)
for led in walklight:
    led.value(0)

# Start with green stoplight and red walklight
stoplight[2].value(1)
walklight[0].value(1)

# We need a method to cycle the stoplight and walklight
#
# We toggle from green to yellow for 2 seconds
# then red for 20 seconds.
def cycle_lights():
    # Go yellow.
    stoplight[2].value(0)
    stoplight[1].value(1)
    # Wait 2 seconds
    utime.sleep(2)
    # Go red and turn on walk light
    stoplight[1].value(0)
    stoplight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    walklight[0].value(0)
    walklight[1].value(1)
    # After 10 seconds, start blinking the walk light
    utime.sleep(1)
    for i in range(0,10):
        walklight[1].value(0)
        utime.sleep_ms(500)
        walklight[1].value(1)
        utime.sleep_ms(500)

    # Stop=green, walk=red
    walklight[1].value(0)
    walklight[0].value(1)
    utime.sleep_ms(500)  # Give the pedestrian a chance to see it
    stoplight[0].value(0)
    stoplight[2].value(1)

# Setup the socket and respond to HTML requests
def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.listen(5)
    server_on = True
    while server_on:
        client, address = sock.accept()
        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)
        # Process the request from the client. Payload should appear in pos 6.
        # Check for walk button click
        walk_clicked = (request[:17] == b'GET /?WALK=PLEASE')
        # Check for stop server button click
        stop_clicked = (request[:18] == b'GET /?shutdown=now')
        if walk_clicked:
            print('Requesting walk now!')
            cycle_lights()
        elif stop_clicked:
            server_on = False
            print("Goodbye!")
        client.send(HTML_RESPONSE)
        client.close()
    sock.close()

Listing 9-9.Complete Code for the Stoplight Simulation (WiPy)

现在,让我们看看 Pyboard 的代码。正如您将看到的,除了网络代码、导入和 pin 函数调用之外,它基本上是相同的。清单 9-10 显示了在 Pyboard 上运行项目的完整代码。

# MicroPython for the IOT - Chapter 9
#
# Project 2: A MicroPython Pedestrian Crosswalk Simulator
#            Part 2 - Controlling the walklight remotely
#
# Required Components:
# - Pyboard
# - (2) Red LEDs
# - (2) Yellow LEDs
# - (1) Green LED
# - (5) 220 Ohm resistors
# - (1) breadboard friendly momentary button
# - (1) CC3000 breakout board
#
# Note: this only runs on the Pyboard.
#
# Imports for the project
from pyb import Pin, delay, ExtInt, SPI
import network
import usocket as socket

# Setup network connection
nic = network.CC3K(SPI(2), Pin.board.Y5, Pin.board.Y4, Pin.board.Y3)
# Replace the following with yout SSID and password
nic.connect("YOUR_SSID", "YOUR_PASSWORD")
print("Connecting...")
while not nic.isconnected():
    delay(50)
print("Connected!")
print("My IP address is: {0}".format(nic.ifconfig()[0]))

# HTML web page for the project
HTML_RESPONSE = """<!DOCTYPE html>
<html>
  <head>
    <title>MicroPython for the IOT - Project 2</title>
  </head>
  <center><h2>MicroPython for the IOT - Project 2</h2></center><br>
  <center>A simple project to demonstrate how to control hardware over the Internet.</center><br><br>
  <form>
    <center>
        <button name="WALK" value="PLEASE" type="submit" style="height: 50px; width: 100px">REQUEST WALK</button>
        <br><br>
        <button name="shutdown" value="now" type="submit" style="height: 50px; width: 100px">Stop Server</button>
    </center>
  </form>
</html>
"""

# Setup the LEDs
led1 = Pin('X7', Pin.OUT_PP)
led2 = Pin('X6', Pin.OUT_PP)
led3 = Pin('X5', Pin.OUT_PP)
led4 = Pin('X4', Pin.OUT_PP)
led5 = Pin('X3', Pin.OUT_PP)

# Setup lists for the LEDs
stoplight = [led1, led2, led3]
walklight = [led4, led5]

# Turn off the LEDs
for led in stoplight:
    led.low()
for led in walklight:
    led.low()

# Start with green stoplight and red walklight
stoplight[2].high()
walklight[0].high()

# We need a method to cycle the stoplight and walklight
#
# We toggle from green to yellow for 2 seconds
# then red for 20 seconds.
def cycle_lights():
    # Go yellow.
    stoplight[2].low()
    stoplight[1].high()
    # Wait 2 seconds
    delay(2000)
    # Go red and turn on walk light
    stoplight[1].low()
    stoplight[0].high()
    delay(500)  # Give the pedestrian a chance to see it
    walklight[0].low()
    walklight[1].high()
    # After 10 seconds, start blinking the walk light
    delay(10000)
    for i in range(0,10):
        walklight[1].low()
        delay(500)
        walklight[1].high()
        delay(500)

    # Stop=green, walk=red
    walklight[1].low()
    walklight[0].high()
    delay(500)  # Give the pedestrian a chance to see it
    stoplight[0].low()
    stoplight[2].high()

# Setup the socket and respond to HTML requests
def run():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.listen(5)
    server_on = True
    while server_on:
        client, address = sock.accept()
        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)
        # Process the request from the client. Payload should appear in pos 6.
        # Check for walk button click
        walk_clicked = (request[:17] == b'GET /?WALK=PLEASE')
        # Check for stop server button click
        stop_clicked = (request[:18] == b'GET /?shutdown=now')
        if walk_clicked:
            print('Requesting walk now!')
            cycle_lights()
        elif stop_clicked:
            server_on = False
            print("Goodbye!")
        client.send(HTML_RESPONSE)
        client.close()
    sock.close()

Listing 9-10.Complete Code for the Stoplight Simulation (Pyboard)

好了,现在我们准备好执行这个项目了。

执行!

现在是有趣的部分!我们已经设置好了控制 led 的代码,从第一部分我们就知道它是有效的。我们还有代码来设置一个套接字侦听器,以通过我们的 MicroPython 板上的端口 80 接受连接。我们现在所需要的就是那块板的 IP 地址来指向我们的网络浏览器。我们可以通过运行代码从我们的调试语句中得到答案。清单 9-11 显示了项目在 WiPy 上的初始运行(Pyboard 的结果类似)。

MicroPython v1.8.6-694-g25826866 on 2017-06-29; WiPy with ESP32
Type "help()" for more information.
>>> import ped_part3_wipy as w
Network found!
WLAN connection succeeded!
My IP address is: 192.168.42.128
>>> w.run()
Got a connection from a client at: ('192.168.42.127', 49236)
Got a connection from a client at: ('192.168.42.127', 49237)
Got a connection from a client at: ('192.168.42.127', 49243)
Requesting walk now!
Got a connection from a client at: ('192.168.42.127', 49254)
Goodbye!
>>>
Listing 9-11.Running the Stoplight Simulation (WiPy)

注意,在这种情况下,IP 地址是 192.168.42.127。我们需要做的就是把它放入我们的浏览器,如图 9-3 所示。

Caution

如果您使用 WiPy 和 telnet 打开 REPL 控制台,请注意,一旦您运行代码的网络部分,您的 REPL 控制台将断开连接。这是因为 IP 地址会变!因此,当使用 WiPy 连接到您的网络时,最好通过 USB 使用屏幕或其他终端程序来获得 REPL 控制台。

A447395_1_En_9_Fig3_HTML.jpg

图 9-3。

Stoplight Simulation Project

输入 URL 后,您应该会看到如图所示的网页。如果没有,一定要检查代码中的 HTML,确保它和显示的完全一样;否则,页面可能无法正常显示。您还应该确保您的 PC 所连接的网络可以连接到您的主板所连接的网络。如果您的家庭办公室像我一样设置,可能有几个 WiFi 网络可供您使用。最好你的主板和你的电脑在同一个网络(和同一个子网)。

一旦你解决了这个问题,继续按下按钮。请记住,步行按钮将启用,您将看到灯光循环,但您将无法做任何事情,直到步行循环完成。这是因为我们直到循环完成后才返回响应 HTML(请看代码来说服自己)。此外,当您单击 shutdown 按钮时,您将需要重新启动代码来重新运行它。只需再次调用run()函数。

再看一下上面的清单。注意,每次客户端连接时都会打印调试消息(代码接受连接和 GET 请求)以及一个关于它正在做什么的声明。您应该会在 REPL 控制台中看到类似的内容。

在这一点上,你应该沐浴在你的第一个成功的 MicroPython IOT 远程控制硬件项目的奇迹中。花些时间享受出色完成的工作。

这个过程的最后一步对于这个项目来说是可选的,因为除了测试之外,您不太可能希望它运行更多。然而,如果您确实想让它在每次启动您的板时运行,您可以修改板上的main.py代码模块来导入您的代码并调用run()函数。

When Things Go Wonky1

有时,当处理更大的脚本或更复杂的逻辑、许多库(驱动程序),甚至当使用许多字符串(内存)时,有时会遇到奇怪、简洁的错误。下面是一个例子。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ped_part2_wipy.py", line 97, in run
OSError: [Errno 12] ENOMEM

在这种情况下,该错误是一个内存不足错误,意味着您的 MicroPython 板已经为您的代码分配了所有(或几乎所有)内存,并且它无法继续。此时你唯一能做的就是重启主板。您可以尝试软启动(CTRL-D),但这通常不能解决问题,但硬重启可以。

至此,您已经完成了第一个真正的 MicroPython IOT 项目。我们可以称之为 IOT 项目,因为它使用互联网,但它只是一个模拟,并不实际控制真正的交通灯,但我们在这个项目中看到的技术允许我们将我们的 MicroPython 板连接到互联网并与之交互-这就是 IOT 的全部内容!

更进一步

这个项目展示了在其他项目中重用这些技术的良好前景。对于 HTML 服务器方面来说尤其如此。如果您喜欢通过互联网控制您的 MicroPython 板,您应该考虑花时间探索一些修饰。这里有一些你可以考虑的。有些很容易,有些可能是一个挑战或需要更多的研究。 2

  • 使用新像素( https://www.adafruit.com/category/168 )代替 led。这些是 RGB 发光二极管,所以你只需要两个——一个用于交通信号灯,一个用于步行灯。更多信息和示例见 https://github.com/JanBednarik/micropython-ws2812
  • 修改 HTML 响应以显示灯的状态。
  • 探索 HTML 代码,根据您的喜好更改网页。考虑使用级联样式表来更改按钮被按下时的背景。
  • 将您的板连接到互联网,并呼叫朋友连接到您的板并进行测试。
  • 使用显示屏代替行走标志的发光二极管来显示“行走”或“不要行走”

当然,如果你想继续下一个项目,欢迎你这样做,但是花些时间探索这些潜在的修饰——这将是一个很好的实践。

如果您准备迎接真正的挑战,您可以在这个项目中重用代码,并用继电器板替换按钮逻辑,这样您就可以使用 MicroPython 板等低压设备来打开或关闭高压电路。在这种情况下,您可以使用 HTML 按钮通过互联网打开或关闭中继。

摘要

使用分立的电子元件会非常有趣。当你刚开始接触电子学时,光是让电路工作就令人兴奋不已。现在我们对 MicroPython 有了更多的了解,我们可以看到拥有像 Python 这样的易于编程的语言来直接处理硬件——甚至通过互联网——是多么强大。

在这一章中,我们实现了一个行人人行横道按钮和交通信号灯的模拟。我们用一系列发光二极管来代表交通信号灯和步行信号。我们还添加了一个硬件按钮来模拟按下真正的步行按钮,然后通过简单的 HTML 从我们的 MicroPython 板上将它转换成一个遥控按钮。如果你喜欢这个项目,你会更喜欢接下来的两个项目。

在下一章中,我们将探索一个项目,该项目使用传感器读取值,然后将数据存档,并在需要时显示在网页上。这是我们构建真正的 MicroPython IOT 项目的倒数第二步。本章和下一章将向您展示如何通过互联网提供数据,项目的最后一章将向您展示如何通过云服务提供您的传感器数据。

Footnotes 1

一个高度技术性的术语,描述一种极度混乱和普遍执行失败的状态。不要与 hinky 混淆,hinky 表示当某件事情工作但不太正确时,情况稍微不太严重。

2

我把这些包括在内,这样你可以增长你的知识,超越这本书的范围。当你完成书中的其他项目时,试试这些。

十、项目 3:工厂监控

IOT 项目最常见的形式之一是那些使用传感器监控事件的项目,这些传感器将数据提供给另一台机器、云服务或本地服务器(如 HTML 服务器)。一种方法是将 MicroPython 板连接到一组传感器上,然后记录数据。您可以在互联网上找到几个通用数据记录器的示例,但是很少将数据记录与可视化组件结合起来。事实上,理解数据是制定成功的 IOT 解决方案的关键。

在这个项目中,我们将探索如何将数据记录与数据可视化结合起来。我们将使用与上一章相同的 HTML 服务器技术以及前几章的一些技术。我们还将了解如何使用模拟传感器——这种传感器产生模拟数据,然后我们必须对其进行解释。事实上,我们将依靠电路板的模数转换(ADC)功能来将电压读数变为我们可以使用的值。当我们利用从第四章中学到的关于类和模块的知识时,我们还会在代码中看到更多的复杂性。

这个项目增加的复杂性不是新的硬件或接口,尽管我们将看到如何使用模数转换器类;复杂性在于代码的复杂性。正如你将看到的,本章中使用的代码比以前的项目更加模块化,使用了更多的功能。仅仅因为这个原因,它就更复杂了。但是,正如您将看到的,代码并不难学,并且使用了我们在前几章中看到的概念。

概观

在这一章中,我们将实现一个植物土壤湿度监测解决方案(简称植物监测)。这将涉及到使用一个或多个连接到我们的 MicroPython 板上的土壤湿度传感器。我们将设置一个定时警报(中断)来定期运行,以从传感器读取数据,并将其存储在一个逗号分隔值(CSV)文件中。

图 10-1 描绘了该项目的概念图。MicroPython 板将从土壤湿度传感器读取数据,然后根据请求通过 HTML 网页显示出来。

A447395_1_En_10_Fig1_HTML.jpg

图 10-1。

Plant Monitoring Project Concept

这个项目的用户界面是一个由一个表组成的网页,该表包含从日志文件中读取的所有数据。这就是我们如何克服在循环中运行 HTML 服务器的潜在问题。也就是说,我们不必中断循环来读取传感器——这是通过定时器警报回调来完成的。遗憾的是,这种技术只适用于 WiPy 和类似的电路板。我们将不得不对 Pyboard 使用不同的技术。

通过将传感器读数从显示中分离出来,我们可以重用或修改它们,而不会在钻研代码时感到困惑。例如,只要可视化组件从文件中读取传感器数据,读取代码的传感器如何使用它就无关紧要。这两部分之间唯一的接口或连接是文件的格式,因为我们使用的是 CSV 文件,所以在我们的代码中很容易阅读和使用。

为了让事情变得更有趣和更容易编码,我们将把所有传感器代码放在一个单独的代码模块中。回想一下,这是一种技术,用于帮助减少任何一个模块中的代码量,从而使其更容易编写和维护。

现在让我们看看这个项目需要哪些组件,然后我们将看看如何将所有组件连接在一起。

Note

由于我们已经进入了第三个项目,并且已经看到了该项目中采用的许多技术,所以关于所需组件、布线和硬件设置的讨论应该是简短的。

必需的组件

表 10-1 列出了你需要的部件。您可以从 Adafruit ( adafruit.com)、Sparkfun ( sparkfun.com)或任何出售电子元件的电子商店单独购买元件。如果您想购买这些组件,可以链接到供应商。当列出同一对象的多行时,您可以选择其中一行——您不需要两个都要。此外,您可能会找到销售这些组件的其他供应商。你应该货比三家,找到最好的交易。显示的成本是估计值,不包括任何运输成本。

表 10-1。

Required Components

| 成分 | 数量 | 描述 | 费用 | 链接 | | --- | --- | --- | --- | --- | | MicroPython 板 | one | 带标题的 Pyboard v1.1 版 | $45-50 | [`https://www.adafruit.com/product/2390`](https://www.adafruit.com/product/2390)[`https://www.adafruit.com/product/3499`](https://www.adafruit.com/product/3499) | | [`https://www.sparkfun.com/products/14413`](https://www.sparkfun.com/products/14413) | | [`https://store.micropython.org/store`](https://store.micropython.org/store) | | WiPy | $25 | [`https://www.adafruit.com/product/3338`](https://www.adafruit.com/product/3338) | | [`https://www.pycom.io/product/wipy/`](https://www.pycom.io/product/wipy/) | | 土壤湿度传感器 | 1+ | 土壤湿度传感器 | $6 | [`https://www.sparkfun.com/products/13637`](https://www.sparkfun.com/products/13637) | | 网络模块(Pyboard) | one | CC3000 分线板(或同等产品) | $15+ | 各种各样的 | | 实时时钟 | one | 对于 Pyboard 和其他不支持 NTP 的板,RTC 是可选的 | $10+ | [`https://www.sparkfun.com/products/12708`](https://www.sparkfun.com/products/12708) | | 跳线(WiPy) | 3* | M/M 跳线,6”(10 根跳线的成本) | $4 | [`https://www.sparkfun.com/products/8431`](https://www.sparkfun.com/products/8431) | | 跳线(线路板) | 15+ | M/M 跳线,6”(10 根跳线的成本) | $4 | [`https://www.sparkfun.com/products/8431`](https://www.sparkfun.com/products/8431) | | 力量 | one | 从电脑获取电源的 USB 电缆 |   | 使用您的备件 | | one | USB 5V 电源和电缆 |   | 使用您的备件 |

需要的跳线数量将根据您使用的传感器数量以及您是否使用需要网络模块的 MicroPython 板而有所不同。每个传感器需要三根跳线,如果使用 CC3000 SPI 分线板,还需要八根额外的电线。如果您计划使用 Pyboard,您还需要添加一个实时时钟,以便在关机时保持时间。

Pyboard 固件不支持网络时间协议(NTP)服务器。因此,每次给电路板上电时,您必须初始化板载 RTC,将备用电池连接到电路板上(查看电路板的底部,您将看到需要焊接的引脚),或者添加实时时钟模块。

土壤湿度传感器有多种形式,但大多数都有两个插入土壤的插脚,使用少量电荷测量插脚之间的电阻。读数越高,土壤中的水分越多。但是,要获得可靠或现实的阈值,需要进行一些配置。虽然制造商会有阈值建议,但可能需要一些实验来找到正确的值。

这些传感器也会受到环境因素的影响,包括植物所处的花盆类型、土壤成分和其他因素。因此,用一种已知的过度湿润的土壤、干燥的土壤和适当养护的土壤做实验,将有助于你缩小你的环境门槛。

图 10-2 显示了 Sparkfun 的土壤湿度传感器,它有一个端子座,而不是引脚。你可以找到几种不同的传感器。只需选择您想要使用的一个,记住您可能需要不同的跳线将其连接到您的主板。

A447395_1_En_10_Fig2_HTML.jpg

图 10-2。

Soil Moisture Sensor (courtesy of sparkfun.com)

特别值得注意的是这些土壤湿度传感器是如何工作的。如果让传感器一直通电,它们会随着时间的推移而退化。尖头上的金属会由于电解而降解,从而大大缩短其寿命。当您想要读取值时,可以通过打开 GPIO 引脚来使用 GPIO 引脚技术为传感器供电。请记住,传感器稳定时会有一个小的延迟,但我们可以使用一个简单的延迟来等待,然后读取值并关闭传感器。这样,我们可以大大延长传感器的寿命。

幸运的是,这个项目的布线没有前两个项目复杂。现在,让我们看看如何将组件连接在一起。

安装硬件

表 10-2 显示了本项目所需的连接。这里只显示了两个传感器,但是如果你愿意的话,你可以增加几个。但是,建议您从一个传感器开始,直到项目正常运行,然后添加其他传感器。如果更简单,您可以使用试验板将传感器连接到 MicroPython 板,但根据您计划放置板的位置,您可能不需要它。这是你的选择。

表 10-2。

Connections for the MicroPython (Pyboard and WiPy)

| MicroPython 板 |   | | --- | --- | | WiPy | Pyboard | 成分 | 电线颜色 | | --- | --- | --- | --- | | P13 | X19 | 传感器 1: VCC |   | | 地线 | 地线 | 传感器 1: GND |   | | P19 | X20 | 传感器 1:Mr |   | | P14 | X21 | 传感器 2: VCC |   | | 地线 | 地线 | 传感器 2: GND |   | | P20 | X22 | 传感器 2:Mr |   |

当然,你必须将土壤湿度传感器插入植物的土壤中。如果您的工厂离您的电源较远,您可能需要使用更长的电线来连接传感器。您应该从一个小型设备和一个传感器(或者为了测试,一个设备中有两个传感器)开始,将它们放在靠近您的 PC(或电源)的地方。

Caution

你需要能够在 3.3-5V 电压下工作的土壤湿度传感器。一些 MicroPython 板可能会将引脚上的输出限制为 3.3v。spark fun 的传感器是兼容的。

让我们看看如何连接 WiPy 和 Pyboard,如图所示。

WiPy

WiPy 的布线最好是将 USB 连接器朝向左侧。图 10-3 显示了 WiPy 的接线图。注意使用小试验板来帮助接地连接。还要注意,传感器 1 在左侧,传感器 2 在右侧。

A447395_1_En_10_Fig3_HTML.jpg

图 10-3。

Wiring the Plant Monitor (WiPy)

Pyboard

Pyboard 的布线最好将 USB 连接器朝向左侧。图 10-4 显示了 Pyboard 的接线图。您可能需要使用试验板来连接网络模块,如图所示。还要注意,传感器 1 在左侧,传感器 2 在右侧。

A447395_1_En_10_Fig4_HTML.jpg

图 10-4。

Wiring the Plant Monitor (Pyboard)

同样,在给主板通电之前,请务必仔细检查您的连接。现在,我们来谈谈我们需要编写的代码。暂时不要启动您的主板——在我们准备好测试该项目之前,还需要进行大量的讨论。

写代码

现在是时候为我们的项目编写代码了。代码比我们到目前为止看到的要长,由于我们正在处理的所有零零碎碎的东西,最好将项目分成几个部分。因此,我们将分两个阶段编写代码。我们直到最后才会有一个工作项目,所以大部分的讨论都是关于各个部分的。在测试这个项目之前,我们将把它们放在一起。

回想一下概述,我们将有两个主要组件:主代码和封装土壤传感器的代码模块。我们将把 HTML 服务器代码和支持函数放在主代码模块中。然而,在我们开始项目的代码之前,我们应该校准我们的传感器。让我们现在做那件事。

Note

这些部分演示了 WiPy 的代码。该项目最适合具有类似功能的 WiPy 和主板:即 WiFi 和 NTP 支持。本章末尾给出了在 Pyboard 和 Pyboard 克隆板上实现项目的差异。正如您将看到的,让项目在这些板上运行需要做更多的工作。

校准传感器

传感器的校准非常重要。对于土壤湿度传感器来说尤其如此,因为有许多不同的版本可供选择。这些传感器对土壤成分、温度,甚至植物所在的花盆类型也非常敏感。因此,我们应该用已知的土壤湿度进行实验,这样我们就知道在我们的代码中使用什么样的范围。

更具体地说,我们希望对来自传感器的观察结果进行分类,以便我们可以确定植物是否需要浇水。我们将使用值“干”、“正常”和“湿”来对从传感器读取的值进行分类。看到这些标签,我们一眼就能轻松确定植物是否需要浇水。在这种情况下,原始数据如 1756 的值可能没有太大的意义,但如果我们看到“干燥”,我们就知道它需要水。

由于传感器是模拟传感器,我们将在电路板上使用模数转换。当我们从 pin 读取数据时,我们将得到一个 0-4096 范围内的值。该值与传感器在土壤中读取的电阻有关。低值表示土壤干燥,高值表示土壤潮湿。

但是,不同供应商的传感器读取的值可能会有很大差异。例如,Sparkfun 的传感器倾向于读取 0-1024 范围内的值,但其他供应商的传感器可以读取高达 4096 的值。幸运的是,它们似乎都是一致的,数值越低,土壤越干燥。

因此,我们必须确定这三种分类的阈值。同样,有几个因素会影响从传感器读取的值。因此,你应该选择几盆土壤,其中一盆你觉得干燥,另一盆浇水适当,第三盆浇水过多。最好的办法是选择一个干燥的,测量,然后浇水,直到土壤湿度合适,测量,然后再次浇水,直到有太多的水。 1

为了确定阈值,我们必须首先编写一小段代码来设置我们的电路板,以便从传感器读取值。这包括选择支持 ADC 的 GPIO 引脚。查看您的主板的参考图,确定包括哪些 GPIO 引脚。例如,在 WiPy 上,有几个 ADC GPIO 部分,包括 P13 至 P18 范围内的部分。

我们还需要选择一个引脚来为电路板供电。这也是一个模拟输出引脚。最后,我们将编写一个循环,每 5 秒钟读取几个值,然后对它们进行平均。五秒是一个任意值,它是通过阅读传感器的数据手册得出的。检查您的传感器,查看读数稳定所需的时间(可能在“读数频率”标题下)。清单 10-1 显示了设置模数通道所需的代码、用于为传感器供电的引脚以及读取 10 个值并求平均值的循环。

# MicroPython for the IOT - Chapter 10
#
# Project 3: MicroPython Plant Monitoring
#
# Threshold calibration for soil moisture sensors
#
# Note: this only runs on the WiPy.
from machine import ADC, Pin
import utime

# Setup the ADC channel for the read (signal)
# Here we choose pin P13 and set attn to 3 to stabilize voltage
adc = ADC(0)
sensor = adc.channel(pin='P13', attn=3)

# Setup the GPIO pin for powering the sensor. We use Pin 19
power = Pin('P19', Pin.OUT)
# Turn sensor off
power.value(0)

# Loop 10 times and average the values read
print("Reading 10 values.")
total = 0
for i in range (0,10):
    # Turn power on
    power.value(1)
    # Wait for sensor to power on and settle
    utime.sleep(5)
    # Read the value
    value = sensor.value()
    print("Value read: {0}".format(value))
    total += value
    # Turn sensor off
    power.value(0)

# Now average the values
print("The average value read is: {0}".format(total/10))

Listing 10-1.Calibrating the Soil Moisture Threshold (WiPy)

如果您在一个名为 threshold.py 的文件中输入这个代码,那么您可以将它复制到您的 WiPy 中并执行它。清单 10-2 显示了在正确灌溉的植物中运行此校准代码的输出。

>>> import threshold
Reading 10 values.
Value read: 1724
Value read: 1983
Value read: 1587
Value read: 1702
Value read: 1634
Value read: 1525
Value read: 1874
Value read: 1707
Value read: 1793
Value read: 1745
The average value read is: 1727.4
>>>
Listing 10-2.Running the Calibration Code (WiPy)

这里我们看到的平均值是 1727(总是取整数——你需要整数)。在干燥的土壤上运行该代码的进一步测试得到的值是 425,而在潮湿的植物上是 3100。因此,这个例子的阈值是 500 代表干,2500 代表湿。但是,您的结果可能会有很大差异,因此请确保使用您选择的传感器、电路板和设备运行此代码。

Tip

为了简化阈值校准,请使用同一供应商的传感器。否则,您可能需要为每个受支持的传感器使用不同的阈值集。

请注意读取的值。如您所见,这些值随时都可能发生变化。这对于这些传感器来说是正常的。众所周知,它们会产生一些跳动的值。因此,您应该考虑对传感器进行多次采样,以获得短时间内的平均值,而不是单个值。如果一个或多个样本相差很大,即使取平均值也会有轻微的偏差。然而,即使取样 10 个值并取平均值也将有助于降低获得异常读数的可能性。我们将在项目代码中这样做。

现在我们已经有了传感器的阈值,我们可以从传感器的代码模块开始。

第一部分:传感器代码模块

该项目的第一部分将是创建一个代码模块,包含一个名为PlantMonitor的新类,该类包含从传感器读取数据并将数据保存到文件中的所有功能。在这一节中,我们将看到如何编写模块的代码。如果您想继续编写代码,您可以打开一个新文件并将其命名为plant_monitor.py。先从高层设计来看。

高层设计

正如我们前面所学的,为我们想要使用的每个代码模块(类)创建一个设计是一个好主意。我们将使用主代码中的代码模块。因此,我们需要一些函数来告诉这个类读取传感器,以及一种方法来获取这个类用于数据的文件名。

通常,人们会设计一个代码模块来完全隐藏一个文件和其上的所有操作,但在这种情况下,该类只关心读取传感器和写入数据。另外,由于主代码需要读取数据并用 HTML 标记对其进行格式化,因此将 read 函数放在主代码中更合适。也就是说,您应该努力将相似的代码功能放在一起,这有助于维护代码。例如,将所有的 HTML 代码保存在一个文件中使得修改 HTML 代码(或者重用它)变得更加容易。

我们将使用定时器中断来读取传感器。这允许我们设置一个函数来定期调用,而不需要监控或轮询时间,直接调用函数。为了方便起见,我们提供了清除日志功能。因此,我们只需要两个公共函数:一个清除日志,另一个获取文件名。除了初始化类之外,我们只需要在需要刷新数据时获取文件名(将数据发送给客户机)。表 10-3 显示了工厂监控类的功能。

表 10-3。

High-Level Design (Functions) Plant Monitor Class

| 功能 | 因素 | 描述 | | --- | --- | --- | | `__init__()` | 雷达跟踪中心(Radar Tracking Centre 的缩写) | 类的初始化(构造函数) | | `clear_log()` |   | 清除日志 | | `get_filename()` |   | 检查 SD 卡后,获取我们正在使用的文件名 | | `_get_value()` | 传感器、电源 | 读取传感器 10 次,并计算读数的平均值 | | `_read_sensors()` | 线条 | 监控传感器,读取数值,并保存它们 | | `_convert_value()` | 价值 | 将原始传感器值转换为枚举 |

注意第一个名为__init__()的函数。这是该类的构造函数,将在从我们的主代码实例化该类时被调用。还要注意私有方法是用一个下划线命名的。

以下部分解释了初始化代码和所需的函数。我们将在后面的部分看到完整的代码。

设置和初始化

在本节中,我们将讨论设置和初始化代码模块所需的代码。首先,我们需要一些导入,包括模数转换器、pin、安全磁盘(SD)、定时器和操作系统库。

我们还需要为这个类定义一些常量。回想一下,我们想用枚举法对土壤湿度读数进行分类。为此,我们需要使用我们为分类确定的阈值。我们可以在文件的顶部使用常量,以便在以后需要调整代码以用于其他传感器或我们的工厂条件发生变化(不同的花盆、土壤、环境等)时更容易更改它们。).我们可以使用相同的原理来设置包含数据的文件名。

我们还使用一个常数来定义读取传感器的频率。由于我们将使用一个循环来读取传感器,每次读取需要等待 5 秒,因此我们至少需要 50-55 秒来读取 10 个值。因此,我们不能将更新频率设置为少于一分钟。频率以秒为单位。虽然您可能希望将此设置为较低的测试值,但您肯定不希望每分钟都检查植物的土壤湿度。也就是说,你多久检查一次你的植物?几天一次还是一天一次?为什么比平时检查得早?

Sampling Frequency

在设计传感器网络时,经常会忽略从传感器采样数据的频率(也称为采样率)。趋势是存储尽可能多的值;认为数据越多越好。但这在一般情况下是不适用的。考虑工厂监控项目。如果您通常每天检查一次工厂,那么每 5 分钟对传感器进行一次采样对您有什么好处呢?不会,而且只会产生多余的数据!

采样率必须仔细计算,以提供您需要的数据,从而在不创建太多数据的情况下得出结论。虽然更多的数据总是比太少的数据好,但以不切实际的频率过于频繁地保存数据会产生太多的数据,可能会超出设备的存储容量。

设计传感器采样项目时,应仔细考虑采样速率。选择基于现实预期的采样率。通常,如果您正在对变化非常缓慢的数据进行采样,则采样率应该较低。变化更快的采样数据应该具有更高(采样间隔时间更短)的采样率。

最后,我们需要一个将时间结构转换成字符串的函数。回想一下前面的例子,我们可以使用一个简单的格式规范。我们将为该特性使用一个模块级的私有函数。

清单 10-3 显示了设置和初始化部分的代码。把这个放在文件的顶部。

from machine import ADC, Pin, SD, Timer
import os
import utime

# Thresholds for the sensors
LOWER_THRESHOLD = 500
UPPER_THRESHOLD = 2500
UPDATE_FREQ = 120   # seconds

# File name for the data
SENSOR_DATA = "plant_data.csv"

# Format the time (epoch) for a better view
def _format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)

Listing 10-3.Plant Monitor Class Setup and Initialization (WiPy)

构造器

类的构造函数是所有主要工作发生的地方。我们需要做几件事,包括以下几件。

  • 规范化数据文件的位置(路径)
  • 在存储在列表中的字典中设置传感器
  • 设置定时器中断以定期读取传感器

我们通过尝试使用 SD 卡来规范化数据文件的路径。如果找不到 SD 卡,我们默认使用闪存盘。但是,您应该避免将数据写入闪存驱动器,因为驱动程序较小,可能会被填满,并且写入闪存驱动器会增加损坏驱动器或在执行过程中导致问题的风险。

我们为每个传感器使用一个字典,这样我们就可以定义传感器的 pin、为传感器供电的 pin、传感器号(任意标识)以及传感器的位置。然后,我们将字典放在一个列表中,以便使用循环同时读取所有传感器。

最后,我们通过 timer alarm 类设置一个中断来定期读取传感器。清单 10-4 显示了类构造函数的代码。

def __init__(self, rtc):
    self.rtc = rtc

    # Try to access the SD card and make the new path
    try:
        sd = SD()
        os.mount(sd, '/sd')
        self.sensor_file = "/sd/{0}".format(SENSOR_DATA)
        print("INFO: Using SD card for data.")
    except:
        print("ERROR: cannot mount SD card, reverting to flash!")
        self.sensor_file = SENSOR_DATA
    print("Data filename = {0}".format(self.sensor_file))

    # Setup the dictionary for each soil moisture sensor
    adc = ADC(0)
    soil_moisture1 = {
        'sensor': adc.channel(pin='P13', attn=3),
        'power': Pin('P19', Pin.OUT),
        'location': 'Green ceramic pot on top shelf',
        'num': 1,
    }
    soil_moisture2 = {
        'sensor': adc.channel(pin='P14', attn=3),
        'power': Pin('P20', Pin.OUT),
        'location': 'Fern on bottom shelf',
        'num': 2,
    }
    # Setup a list for each sensor dictionary
    self.sensors = [soil_moisture1, soil_moisture2]
    # Setup the alarm to read the sensors
    a = Timer.Alarm(handler=self._read_sensors, s=UPDATE_FREQ,
                    periodic=True)
    print("Plant Monitor class is ready...")

Listing 10-4.Plant Monitor Class Constructor (WiPy)

请注意计时器警报的代码。在这里,我们定义了中断的处理程序(回调),使用我们在图块顶部定义的常量来定义频率,并将其设置为每 N 秒触发一次(定期)。

公共职能

只有两个公共函数。第一个,clear_log(),简单地打开文件进行写入,然后关闭它。这实际上清空了文件。提供该功能是为了方便。第二个函数get_filename()只是返回用于存储数据的文件的名称。这个名字与常量SENSOR_DATA中的名字不同,因为我们在构造函数中规范化了路径,如前一节所示。

私人功能

有三个私有函数。_get_value()函数与我们的阈值校准代码相同,我们对传感器采样 10 次并取平均值。_read_sensors()函数是定时器报警中断的回调函数,它读取我们定义的所有传感器并将数据保存到文件中。_convert_value()功能是基于传感器数据确定土壤分类的辅助功能。该函数返回一个字符串或“干”、“好”或“湿”。

完全码

现在我们已经看到了代码模块的所有部分,让我们来看看完整的代码。清单 10-5 显示了工厂监控代码模块的完整代码。同样,我们可以将这个文件保存为plant_monitor.py

# MicroPython for the IOT - Chapter 10
#
# Project 3: MicroPython Plant Monitoring
#
# Plant monitor class
#
# Note: this only runs on the WiPy.
from machine import ADC, Pin, SD, Timer
import os
import utime

# Thresholds for the sensors
LOWER_THRESHOLD = 500
UPPER_THRESHOLD = 2500
UPDATE_FREQ = 120   # seconds

# File name for the data
SENSOR_DATA = "plant_data.csv"

# Format the time (epoch) for a better view
def _format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)

class PlantMonitor:
    """
    This class reads soil moisture from one or more sensors and writes the
    data to a comma-separated value (csv) file as specified in the constructor.
    """

    # Initialization for the class (the constructor)
    def __init__(self, rtc):
        self.rtc = rtc

        # Try to access the SD card and make the new path
        try:
            sd = SD()
            os.mount(sd, '/sd')
            self.sensor_file = "/sd/{0}".format(SENSOR_DATA)
            print("INFO: Using SD card for data.")
        except:
            print("ERROR: cannot mount SD card, reverting to flash!")
            self.sensor_file = SENSOR_DATA
        print("Data filename = {0}".format(self.sensor_file))

        # Setup the dictionary for each soil moisture sensor
        adc = ADC(0)
        soil_moisture1 = {
            'sensor': adc.channel(pin='P13', attn=3),
            'power': Pin('P19', Pin.OUT),
            'location': 'Green ceramic pot on top shelf',
            'num': 1,
        }
        soil_moisture2 = {
            'sensor': adc.channel(pin='P14', attn=3),
            'power': Pin('P20', Pin.OUT),
            'location': 'Fern on bottom shelf',
            'num': 2,
        }
        # Setup a list for each sensor dictionary
        self.sensors = [soil_moisture1, soil_moisture2]
        # Setup the alarm to read the sensors
        a = Timer.Alarm(handler=self._read_sensors, s=UPDATE_FREQ,
                        periodic=True)
        print("Plant Monitor class is ready...")

    # Clear the log
    def clear_log(self):
        log_file = open(self.sensor_file, 'w')
        log_file.close()

    # Get the filename we're using after the check for SD card
    def get_filename(self):
        return self.sensor_file

    # Read the sensor 10 times and average the values read
    def _get_value(self, sensor, power):
        total = 0
        # Turn power on
        power.value(1)
        for i in range (0,10):
            # Wait for sensor to power on and settle
            utime.sleep(5)
            # Read the value
            value = sensor.value()
            total += value
        # Turn sensor off
        power.value(0)
        return int(total/10)

    # Monitor the sensors, read the values and save them
    def _read_sensors(self, line):
        log_file = open(self.sensor_file, 'a')
        for sensor in self.sensors:
            # Read the data from the sensor and convert the value
            value = self._get_value(sensor['sensor'], sensor['power'])
            print("Value read: {0}".format(value))
            time_data = self.rtc.now()
            # datetime,num,value,enum,location
            log_file.write(
                "{0},{1},{2},{3},{4}\n".format(_format_time(time_data),
                                               sensor['num'], value,
                                               self._convert_value(value),
                                               sensor['location']))
        log_file.close()

    # Convert the raw sensor value to an enumeration
    def _convert_value(self, value):
        # If value is less than lower threshold, soil is dry else if it
        # is greater than upper threshold, it is wet, else all is well.
        if (value <= LOWER_THRESHOLD):
            return "dry"
        elif (value >= UPPER_THRESHOLD):
            return "wet"
        return "ok"

Listing 10-5.Plant Monitor Code Module Complete Code (WiPy)

哇,代码真多!花些时间通读它,直到你理解了代码的所有部分。

白板的更改

要在 Pyboard 上运行这个项目,还需要进行相当多的更改。其主要原因包括我们需要对导入进行的常规更改、引脚类别高/低与值的对比,以及我们使用实时时钟的方式的差异。最后,Pyboard 不支持定时器警报类中断,因此我们必须使用轮询技术来读取传感器。这最后一个变化意味着我们必须使read_sensors()函数成为一个公共函数,这样我们就可以从主代码中调用它。

由于变化很多,差异文件几乎与实际代码一样长。因此,我们将看到 Pyboard 的完整代码。清单 10-6 显示了代码模块的完整代码,不同之处在于代码模块需要适应 Pyboard(粗体)。虽然大多数变化很小,但如果您使用 Pyboard 或 Pyboard 克隆,请注意传感器使用的引脚。

# MicroPython for the IOT - Chapter 10
#
# Project 3: MicroPython Plant Monitoring
#
# Plant monitor class
#

# Note: this only runs on the Pyboard.

from pyb import ADC, delay, Pin, SD

import os

import pyb

# Thresholds for the sensors
LOWER_THRESHOLD = 500
UPPER_THRESHOLD = 2500
UPDATE_FREQ = 120   # seconds

# File name for the data
SENSOR_DATA = "plant_data.csv"

# Format the time (epoch) for a better view
def _format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)

class PlantMonitor:
    """
    This class reads soil moisture from one or more sensors and writes the
    data to a comma-separated value (csv) file as specified in the constructor.
    """

    # Initialization for the class (the constructor)
    def __init__(self, rtc):
        self.rtc = rtc

        # Try to access the SD card and make the new path
        try:
            self.sensor_file = "/sd/{0}".format(filename)

            f = open(self.sensor_file, 'r')

            f.close()

            print("INFO: Using SD card for data.")
        except:
            print("ERROR: cannot mount SD card, reverting to flash!")
            self.sensor_file = SENSOR_DATA
        print("Data filename = {0}".format(self.sensor_file))

        # Setup the dictionary for each soil moisture sensor
        soil_moisture1 = {
            'sensor': ADC(Pin('X19')),

            'power': Pin('X20', Pin.OUT_PP),

            'location': 'Green ceramic pot on top shelf',
            'num': 1,
        }
        soil_moisture2 = {
            'sensor': ADC(Pin('X20')),

            'power': Pin('X21', Pin.OUT_PP),

            'location': 'Fern on bottom shelf',
            'num': 2,
        }
        # Setup a list for each sensor dictionary
        self.sensors = [soil_moisture1, soil_moisture2]
        # Setup the alarm to read the sensors

        self.alarm = pyb.millis()

        print("Plant Monitor class is ready...")

    # Clear the log
    def clear_log(self):
        log_file = open(self.sensor_file, 'w')
        log_file.close()

    # Get the filename we're using after the check for SD card
    def get_filename(self):
        return self.sensor_file

    # Read the sensor 10 times and average the values read
    def _get_value(self, sensor, power):
        total = 0
        # Turn power on
        power.high()

        for i in range (0,10):
            # Wait for sensor to power on and settle
            delay(5000)

            # Read the value
            value = sensor.read()

            total += value
        # Turn sensor off
        power.low()

        return int(total/10)

    # Monitor the sensors, read the values and save them
    def read_sensors(self):

        if pyb.elapsed_millis(self.alarm) < (UPDATE_FREQ * 1000):

            return

        self.alarm = pyb.millis()

        log_file = open(self.sensor_file, 'a')
        for sensor in self.sensors:
            # Read the data from the sensor and convert the value
            value = self._get_value(sensor['sensor'], sensor['power'])
            print("Value read: {0}".format(value))
            time_data = self.rtc.datetime()

            # datetime,num,value,enum,location
            log_file.write(
                "{0},{1},{2},{3},{4}\n".format(_format_time(time_data),
                                               sensor['num'], value,
                                               self._convert_value(value),
                                               sensor['location']))
        log_file.close()

    # Convert the raw sensor value to an enumeration
    def _convert_value(self, value):
        # If value is less than lower threshold, soil is dry else if it
        # is greater than upper threshold, it is wet, else all is well.
        if (value <= LOWER_THRESHOLD):
            return "dry"
        elif (value >= UPPER_THRESHOLD):
            return "wet"
        return "ok"

Listing 10-6.Plant Monitor Code Module Complete Code (Pyboard)

好了,现在我们准备看看主要代码。

第二部分:主代码

主要代码就像上一个项目的代码。但是,这一次我们将使用一个文件来存储 HTML 代码(因为它不会改变)和一个单独的 HTML 字符串,以便用文件中的数据填充 HTML 表。我们还将添加代码来从网络时间协议(NTP)服务器读取日期和时间。

与上一个项目不同,HTML 代码不包括按钮,但我们可以在 URL 上手动设置命令的格式。我们可以使用这种技术来允许在不使用按钮或其他用户界面功能的情况下访问命令。这也有助于使这些命令更难使用,以防止过度使用。例如,我们可以提供一个清除日志命令。我们将使用类似于http://192.168.42.140/CLEAR_LOG的 URL,它向 HTML 服务器提交一个GET请求。我们可以捕获该命令,并在发出该命令时清除日志。

Caution

如果您像这样构建命令,请务必小心使用它们。也就是说,将您的 URL 设置为http://192.168.42.140/CLEAR_LOG并按下回车键发出命令。刷新页面将重新发出命令!当您使用该命令时,请确保在刷新之前清除您的 URL,或者最好使用一次并关闭页面/选项卡。

以下部分解释了初始化代码和所需的函数。我们将在后面的部分看到完整的代码。让我们从 HTML 代码开始。

HTML 代码(文件)

我们将把所需的 HTML 代码存储在文件中以节省内存。回想一下一次读取一行——我们不必在代码中用字符串来占用空间。随着您的项目变得越来越复杂,这可能会成为一个问题。因此,这个项目演示了一种节省内存的方法。

这个项目的 HTML 创建了一个带有简单表格的网页,该表格包含了请求时文件中的所有数据。为了方便起见,我们将使用三个文件。第一个文件(名为part1.html)将包含直到表格行的 HTML 代码;第二个文件(名为plant_data.csv),由 PlantMonitor 类填充;第三个(名为part2.html)将包含剩余的 HTML 代码。

第一个文件part1.html,如清单 10-7 所示。这个文件建立了表格 HTML 代码。它还建立了表格的特征,包括文本对齐、边框大小和填充——全部通过级联样式(<style>标记。不要担心这看起来奇怪或陌生。你可以谷歌一下 W3C 标准,看看我们如何使用标签来控制网页的样式。

<!DOCTYPE html>
<html>
  <head>
    <title>MicroPython for the IOT - Project 3</title>
    <meta http-equiv="refresh" content="30">
    <style>
      table, th, td {
          border: 1px solid black;
          border-collapse: collapse;
      }
      th, td {
          padding: 5px;
      }
      th {
          text-align: left;
      }
    </style>
  </head>
  <center><h2>MicroPython for the IOT - Project 3</h2></center><br>
  <center>A simple project to demonstrate how to retrieve sensor data over the Internet.</center>
  <center><br><b>Plant Monitoring Data</b><br><br>
    <table style="width:75%">
      <col width="180">
      <col width="120">
      <col width="100">
      <col width="100">
      <tr><th>Datetime</th><th>Sensor Number</th><th>Raw Value</th><th>Moisture</th><th>Location</th></tr>
Listing 10-7.HTML Code (part1.html)

请注意表格代码。同样,如果这看起来很奇怪,也不要担心。它是可行的,而且是非常基本的。熟悉 HTML 的人可能想修饰和改进代码。最后一行建立了表格的标题。

第二个文件 plant_data.csv 包含数据。我们将使用一个常量来填充一个格式正确的 HTML 表格行。下面的示例展示了文件中的一行数据是什么样子,以及这些数据是如何转换成 HTML 的。我们将在下一节看到表格行的 HTML。

# Raw data
2017-08-08 20:26:17,1,78,dry,Small fern on bottom shelf
# HTML table row
<tr><td>2017-08-08 20:26:17</td><td>1</td><td>78</td><td>dry</td><td>Small fern on bottom shelf </td></tr>

最后一个文件part2.html包含结束标记,所以它不是很大。但是因为我们是从文件中读取,所以我们包含了这个文件。下面显示了第二个文件中的代码。

    </table>
  </center>
</html>

那么,我们如何使用这些文件呢?当我们将响应发送回客户端(网页)时,我们一次读取发送一行的第一个文件,然后一次读取发送一行的数据文件,然后一次读取发送一行的最后一个文件。我们将使用一个助手函数来读取数据文件。清单 10-8 显示了用来做这件事的代码。

# Read HTML from file and send to client a row at a time.
def send_html_file(filename, client):
    html = open(filename, 'r')
    for row in html:
        client.send(row)
    html.close()

# Send the sensor data to the client.
def send_sensor_data(client, filename):
    send_html_file("part1.html", client)
    log_file = open(filename, 'r')
    for row in log_file:
        cols = row.strip("\n").split(",") # split row by commas
        # build the table string if all parts are there
        if len(cols) >= 5:
            html = HTML_TABLE_ROW.format(cols[0], cols[1], cols[2],
                                         cols[3], cols[4])
            # send the row to the client
            client.send(html)
    log_file.close()
    send_html_file("part2.html", client)

Listing 10-8.Reading the HTML and Data File (WiPy)

进口

项目所需的导入包括实时时钟、sys、usocket、utime、machine 和 WLAN。这些现在已经很熟悉了。最后一行从plant_monitor代码模块导入PlantMonitor类。完整的导入列表如下所示。如果你想继续,打开一个新文件,命名为plant_wipy.py

# Imports for the project
from machine import RTC
import sys
import usocket as socket
import utime
import machine
from network import WLAN
from plant_monitor import PlantMonitor

我们还需要一个字符串来创建表的行。出现在这一行之前的 HTML 代码如上所述保存在文件中。下面显示了使用的字符串。注意,我们使用替换语法,这样我们就可以使用format()函数来填充细节。

# HTML web page for the project
HTML_TABLE_ROW = "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"

进口

我们还想将项目连接到我们的网络。我们使用了与之前的项目和示例中相同的代码,但是将它变成了一个名为connect()的函数,我们将从主run()函数中调用它。确保更改 SSID 和密码以匹配您的网络。

网络时间协议

由于我们保存的数据具有时间元素(您想知道何时对土壤湿度进行采样),因此我们需要存储数据的日期和时间。最简单的方法是使用网络时间协议(NPT)服务器:也就是说,假设电路板连接到互联网。如果它没有连接到互联网,我们必须使用 RTC 模块或在启动时初始化板载 RTC。我们在第五章中看到了如何使用 NTP 服务器。我们在这个项目中将其作为名为get_ntp()的函数重复,我们将从主run()函数中调用它。

run()函数

run()函数的 HTML 服务器部分类似于上一个项目,但是我们没有处理表单请求,而是默认将网页发送回客户端。唯一支持的命令是CLEAR_LOG命令,它需要在客户端的 URL 上指定它,如上所述。

另一个区别是,我们没有将代码放在代码文件的全局部分(这样当我们在 REPL 控制台或main.py文件中导入文件时,它就会执行),而是使用函数连接到网络,设置 NTP,并将 HTML 代码发送到客户端。这是一种复杂性的升级,您应该开始将其作为常规做法使用。我们在早期的项目中没有看到这一点,因此我们可以专注于完成代码。在编写自己的项目时,一定要使用函数来包含代码,并从其他代码中调用这些函数。

由于这与上一个项目不同,我们将查看代码。清单 10-9 显示了run()函数的代码。

# Setup the socket and respond to HTML requests
def run():
    # Connect to the network
    if not connect():
        sys.exit(-1)

    # Setup the real time clock
    rtc = get_ntp()

    # Setup the plant monitoring object instance from the plant_monitoring class
    plants = PlantMonitor(rtc)
    filename = plants.get_filename()

    # Setup the HTML server
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.listen(5)
    print("Ready for connections...")
    server_on = True
    while server_on:
        client, address = sock.accept()
        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)

        # Allow for clearing of the log, but be careful! The auto refresh
        # will resend this command if you do not clear it from your URL
        # line.

        if (request[:14] == b'GET /CLEAR_LOG'):
            print('Requesting clear log.')
            plants.clear_log()
        send_sensor_data(client, filename)
        client.close()
    sock.close()

Listing 10-9.Plant Monitor Run() Function (WiPy)

花点时间通读这段代码。请注意我们是如何以更加模块化的方式实现该功能的。将公共代码放在函数中不仅有助于将问题分解成几个部分,还可以缩短主代码(run()函数)。

让我们看看完整的代码。

完全码

现在我们已经看到了代码模块的所有部分,让我们来看看完整的代码。清单 10-10 显示了工厂监控代码模块的完整代码。同样,我们可以将这个文件保存为 Pyboard 的plant_wipy.pyplant_pyboard.py

# MicroPython for the IOT - Chapter 10
#
# Project 3: MicroPython Plant Monitoring
#
# Required Components:
# - WiPy
# - (N) Soil moisture sensors (one for each plant)
#
# Note: this only runs on the WiPy.
#
# Imports for the project
from machine import RTC
import sys
import usocket as socket
import utime
import machine
from network import WLAN
from plant_monitor import PlantMonitor

# HTML web page for the project
HTML_TABLE_ROW = "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"

# Setup the board to connect to our network.
def connect():
    wlan = WLAN(mode=WLAN.STA)
    nets = wlan.scan()
    for net in nets:
        if net.ssid == 'YOUR_SSID':
            print('Network found!')
            wlan.connect(net.ssid, auth=(net.sec, 'YOUR_PASSWORD'), timeout=5000)
            while not wlan.isconnected():
                machine.idle() # save power while waiting
            print('WLAN connection succeeded!')
            print("My IP address is: {0}".format(wlan.ifconfig()[0]))
            return True
    return False

# Setup the real time clock with the NTP service
def get_ntp():
    rtc = RTC()
    print("Time before sync:", rtc.now())
    rtc.ntp_sync("pool.ntp.org")
    while not rtc.synced():
        utime.sleep(3)
        print("Waiting for NTP server...")
    print("Time after sync:", rtc.now())
    return rtc

# Read HTML from file and send to client a row at a time.
def send_html_file(filename, client):
    html = open(filename, 'r')
    for row in html:
        client.send(row)
    html.close()

# Send the sensor data to the client.
def send_sensor_data(client, filename):
    send_html_file("part1.html", client)
    log_file = open(filename, 'r')
    for row in log_file:
        cols = row.strip("\n").split(",") # split row by commas
        # build the table string if all parts are there
        if len(cols) >= 5:
            html = HTML_TABLE_ROW.format(cols[0], cols[1], cols[2],
                                         cols[3], cols[4])
            # send the row to the client
            client.send(html)
    log_file.close()
    send_html_file("part2.html", client)

# Setup the socket and respond to HTML requests
def run():
    # Connect to the network
    if not connect():
        sys.exit(-1)

    # Setup the real time clock
    rtc = get_ntp()

    # Setup the plant monitoring object instance from the plant_monitoring class
    plants = PlantMonitor(rtc)
    filename = plants.get_filename()

    # Setup the HTML server
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.listen(5)
    print("Ready for connections...")
    server_on = True
    while server_on:
        client, address = sock.accept()
        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)

        # Allow for clearing of the log, but be careful! The auto refresh
        # will resend this command if you do not clear it from your URL
        # line.

        if (request[:14] == b'GET /CLEAR_LOG'):
            print('Requesting clear log.')
            plants.clear_log()
        send_sensor_data(client, filename)
        client.close()
    sock.close()

Listing 10-10.Plant Monitor Main Code (WiPy)

白板的更改

像前面的代码模块一样,在 Pyboard 上运行代码需要做大量的修改。其主要原因包括通常的更改,但代码也有很大不同,因为我们必须使用轮询方法,因为没有像在 WiPy 上那样设置定时器中断的(简单)方法。

由于变化很多,差异文件几乎与实际代码一样长。因此,我们将看到 Pyboard 的完整代码。清单 10-11 显示了代码模块的完整代码,其中包含使代码模块适应 Pyboard 所需的差异(粗体)。虽然大多数变化很小,但如果您使用 Pyboard 或 Pyboard 克隆,请注意传感器使用的引脚。

run()功能中需要两个主要的改变。首先,Pyboard 固件中不支持 NTP,因此我们必须在每次启动项目时使用 RTC 模块或初始化板载 RTC。第二,由于没有办法设置允许访问文件的定时器中断,我们必须改变 HTML 服务器以使用非阻塞套接字技术。这些变化以粗体突出显示。

# MicroPython for the IOT - Chapter 10
#
# Project 3: MicroPython Plant Monitoring
#
# Required Components:
# - Pyboard
# - (N) Soil moisture sensors (one for each plant)
#
# Note: this only runs on the Pyboard.
#
# Imports for the project
from pyb import delay, SPI
from pyb import I2C
import network
import urtc
import usocket as socket
from plant_monitor import PlantMonitor

# Setup the I2C interface for the rtc
i2c = I2C(1, I2C.MASTER)
i2c.init(I2C.MASTER, baudrate=500000)

# HTML web page for the project
HTML_TABLE_ROW = "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"

# Setup the board to connect to our network.
def connect():
    nic = network.CC3K(SPI(2), Pin.board.Y5, Pin.board.Y4, Pin.board.Y3)
    # Replace the following with yout SSID and password
    print("Connecting...")
    nic.connect("YOUR_SSID", "YOUR_PASSWORD")
    while not nic.isconnected():
        delay(50)
    print("Connected!")
    print("My IP address is: {0}".format(nic.ifconfig()[0]))
    return True

# Read HTML from file and send to client a row at a time.
def send_html_file(filename, client):
    html = open(filename, 'r')
    for row in html:
        client.send(row)
    html.close()

# Send the sensor data to the client.
def send_sensor_data(client, filename):
    send_html_file("part1.html", client)
    log_file = open(filename, 'r')
    for row in log_file:
        cols = row.strip("\n").split(",") # split row by commas
        # build the table string if all parts are there
        if len(cols) >= 5:
            html = HTML_TABLE_ROW.format(cols[0], cols[1], cols[2],
                                         cols[3], cols[4])
            # send the row to the client
            client.send(html)
            delay(50)
    log_file.close()
    send_html_file("part2.html", client)

# Setup the socket and respond to HTML requests
def run():
    # Connect to the network
    if not connect():
        sys.exit(-1)

    # Setup the real time clock

    rtc = urtc.DS1307(i2c)

    #
    # NOTE: We only need to set the datetime once. Uncomment these
    #       lines only on the first run of a new RTC module or
    #       whenever you change the battery.
    #       (year, month, day, weekday, hour, minute, second, millisecond)
    #start_datetime = (2017,07,20,4,9,0,0,0)
    #rtc.datetime(start_datetime)

    # Setup the plant monitoring object instance from the plant_monitoring class
    plants = PlantMonitor(rtc)
    filename = plants.get_filename()

    # Setup the HTML server
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('', 80))
    sock.setblocking(False)   # We must use polling for Pyboard.

    sock.listen(5)
    print("Ready for connections...")
    server_on = True
    while server_on:
        try:

            client, address = sock.accept()

        except OSError as err:

            # Do check for reading sensors here

            plants.read_sensors()

            delay(50)

            continue

        print("Got a connection from a client at: %s" % str(address))
        request = client.recv(1024)

        # Allow for clearing of the log, but be careful! The auto refresh
        # will resend this command if you do not clear it from your URL
        # line.

        if (request[:14] == b'GET /CLEAR_LOG'):
            print('Requesting clear log.')
            plants.clear_log()
        send_sensor_data(client, filename)
        client.close()
    sock.close()

Listing 10-11.Plant Monitor Main Code (Pyboard)

注意run()函数中的 try 块。当没有客户端连接时,sock.accept()函数将抛出异常。这与阻塞调用的工作方式不同。阻塞调用将简单地等待,直到客户端连接。这里,我们必须尝试接受一个连接,如果失败,继续等待连接。也就是说,我们不能发送 HTML 到客户端,因为没有客户端!

这些变化突出了在没有网络支持的情况下使用 Pyboard 和其他 MicroPython 板的额外困难,那些需要 RTC 模块的,以及那些对定时器报警中断支持有限以允许准异步执行的(它不是真正的异步)。换句话说,我们可以断开一些代码,由中断来调用(触发)。

现在,让我们运行这个项目!

Wait, What about the Data File?

如果您对数据文件有疑问,您不必担心。该代码旨在创建文件,即使它不存在。下面显示了您可以在测试中使用的数据模型。

2017-08-08 20:26:17,1,78,dry,Small fern on bottom shelf on porch
2017-08-08 20:26:32,2,136,dry,Green pot creeper thing on floor in living room
2017-08-08 20:26:47,1,128,dry,Small fern on bottom shelf on porch
2017-08-08 20:27:02,2,112,dry,Green pot creeper thing on floor in living room

如果您想从一些示例数据开始,您可以这样做,但是要确保用逗号分隔,没有空格,每行一行数据。

执行!

现在是有趣的部分!我们已经设置好代码,可以从我们的工厂读取土壤湿度,并将收集到的所有传感器数据发送给客户。回想一下,我们需要将代码复制到我们的板上。我们可以 ftp 两个代码文件(plant_monitor.pyplant_wipy.py)以及两个 HTML 文件(part1.htmlpart2.html)。现在这样做,你就可以测试这个项目了。

我们现在所需要的就是那块板的 IP 地址来指向我们的网络浏览器。我们可以通过运行代码从我们的调试语句中得到答案。清单 10-12 显示了项目在 WiPy 上的初始运行(Pyboard 的结果类似)。

MicroPython v1.8.6-694-g25826866 on 2017-06-29; WiPy with ESP32
Type "help()" for more information.
>>> import plant_wipy as p
>>> p.run()
Network found!
WLAN connection succeeded!

My IP address is: 192.168.42.128

Time before sync: (1970, 1, 1, 0, 1, 44, 382593, None)
Waiting for NTP server...
Time after sync: (2017, 8, 9, 14, 26, 1, 92051, None)
INFO: Using SD card for data.
Data filename = /sd/plant_data.csv
Plant Monitor class is ready...
Ready for connections...
Got a connection from a client at: ('192.168.42.110', 50395)
Listing 10-12.Running the Plant Monitor (WiPy)

注意,在这种情况下,IP 地址是 192.168.42.128。我们需要做的就是把它放入我们的浏览器,如图 10-5 所示。

A447395_1_En_10_Fig5_HTML.jpg

图 10-5。

Plant Monitor Project

输入 URL 后,您应该会看到如图所示的网页。如果没有,一定要检查代码中的 HTML,确保它和显示的完全一样;否则,页面可能无法正常显示。您还应该确保您的 PC 所连接的网络可以连接到您的主板所连接的网络。如果您的家庭办公室像我一样设置,可能有几个 WiFi 网络可供您使用。最好你的主板和你的电脑在同一个网络(和同一个子网)。

至此,您已经完成了另一个真正的 MicroPython IOT 项目。在这种情况下,我们看到了一个收集和显示数据的 IOT 项目。酷!

更进一步

和上一个项目一样,这个项目展示了在其他项目中重用这些技术的良好前景。对于 HTML 服务器方面来说尤其如此。如果你喜欢在互联网上看到你的传感器数据,你应该考虑花时间探索一些修饰。这里有一些你可以考虑的。有些很容易,有些可能是一个挑战或需要更多的研究。

  • 添加更多传感器,将您的项目扩展到更多工厂。
  • 添加一个温度传感器来记录环境温度并显示在网页上。
  • 重写 HTML 代码以生成 JSON 字符串。
  • 重写 HTML 代码以生成 XML。
  • 探索 HTML 代码,根据您的喜好更改网页。考虑使用级联样式表来更改按钮被按下时的背景。
  • 将您的板连接到互联网,并呼叫朋友连接到您的板并进行测试。
  • 在你的板上安装发光二极管,当植物需要浇水时,发光二极管就会发光。

当然,如果你想继续下一个项目,欢迎你这样做,但是花些时间探索这些潜在的修饰——这将是一个很好的实践。

摘要

IOT 解决方案可以采取多种形式。一种更常见的形式是生成我们可以通过互联网查看的数据的形式(有时称为数据收集器)。数据收集器的实现可以有很大不同,但它们通常将数据存储在某个位置,并提供查看数据的方法。最简单的形式是在本地、远程服务器、数据库或云服务中记录数据(有时称为数据记录器)。数据的可视化也可以随着最基本的通过网页提供数据而变化。

在本章中,我们看到了一个 MicroPython IOT 项目,它记录从一系列土壤湿度传感器读取的数据。我们创建了一个工厂监控解决方案,将数据保存到本地 SD 卡中。该项目还通过 HTML 服务器提供数据,这样我们就可以随时看到数据。这个项目可以作为许多数据收集项目的模板。您可以简单地遵循本章中建立的模式,构建自己的基于 HTML 的数据记录器。

在下一章中,我们将通过让我们的 MicroPython 板将数据发送到基于云的 4 存储和可视化服务来结束我们的 MicroPython IOT 项目之旅。酷!

Footnotes 1

一定要选择一种足够健壮的植物来抵御过度浇水。

2

这种情况经常发生。如果出现这种情况,最好查看实际代码,因为差异文件可能更难阅读。

3

这种情况经常发生。如果出现这种情况,最好查看实际代码,因为差异文件可能更难阅读。

4

可悲的是,有些人会认为这不是 IOT,除非它涉及某种形式的云服务。

更多推荐