🐍 附录B LINE Bot 物联网:控制家电开关
📄 一句话概括

板子上的 Python 叫 MicroPython,语法几乎一样, 但它没有操作系统、没有 pip、内存只有几百 KB。 所以第一原则是省。 板子没有 replyToken,只能用 push,不能 reply。这就是 push 要计费的另一面。 PIR 会抖:读到 4 次高电平,其实只有 3 次是「有人经过」。要数边沿,不要数电平。

📝 本章和前面几章的关系

正文第 13 章讲的是「别人来敲你的服务」。这一篇反过来:让一块板子主动去敲别人。 教材附录 B 三节是【从 MicroPython 控制板发送 LINE 讯息】【PIR 人体传感器】和【从 LINE 开关灯】。 这一篇的诚实边界要先说清:本沙箱没有板子,也没有传感器和继电器。 能真跑的是逻辑—— 电平判读、指令路由、状态机。 所以演示里用一个 FakePin 拿预设电平冒充 GPIO。 读引脚的代码形状是真的,电平来源是假的。 每一处替换正文都会点出来。

一、板子上的 Python 缺什么

MicroPython 的语法跟电脑上的 Python 几乎一样。写惯了之后切过去,除了不能 pip install,别的都熟。 不一样的是「没有操作系统」。 没有文件管理器,没有 pip(有 upip),内存只有几百 KB, 还要留一部分给固件。

要做的事 用的东西 线怎么接
发一条 HTTP 请求 network 加 urequests 连 WiFi,不用接线
读人体有没有动 PIR 传感器 VCC、GND、OUT 接一个 GPIO
开关一盏灯 继电器模块 信号脚接 GPIO,灯串在继电器上

一块板子加一个继电器,就能把「软件里的一条指令」变成「墙上的一盏灯」。 这是物联网最朴素的样子,也是它能说服人的地方。 所以板子上写代码的第一原则是省: 不要一次读整个文件,不要建大列表,字符串拼接能省就省。 这几条在电脑上无所谓, 在板子上是生死线。 举个例子:电脑上 'a' + 'b' 随便写,板子上拼一百次就可能内存不够。要用 ''.join() 或者预先分配。

二、发一条消息要三步

连 WiFi 拿到 IP、拼一个 POST 请求带上 Channel Access Token、发出去看返回码。就三步。 演示第二段那个代码块是本篇唯一的「平台原样」,跑不了——它要真的板子和 WiFi。 但形状是真的,照着抄到板子上就能用,改两个常量就行。 注意是 push 不是 reply:板子没有 replyToken,只能主动推。

这条和 13.2 篇那张对照表对上了:reply 要 token、只能应答; push 不要 token、能主动发、按条算钱。 为什么 push 要钱?因为它不需要用户先说话——你可以一天推一万条广告。计费是限制这件事的闸门。 urequests 是 requests 的精简版,接口故意做成一样(post、status_code、close)。 但少了连接池、少了自动重试,出错得自己兜。

所以板子上的请求代码,外面一定要包一层 try,加个重试。WiFi 掉一次就崩的程序,在墙上活不过一天。

三、PIR 只有「动」和「没动」

PIR 是被动红外,测的是「视野里的热源有没有移动」。输出只有两种电平:高(有人动)和低(没人动)。 但它会抖。 演示里读 13 次电平,得到 0010110000100。 直接数「读到 1 的次数」是 4 次;只数「从 0 变 1」的次数是 3 次。多出来的那一次,就是抖动。

差一次看着不多,但真实场景更糟:人从传感器前面走过去,可能产生五六个高电平脉冲。 一次经过推六条通知,用户第一天就把你的机器人屏蔽了。 所以要做边沿检测:只在信号「从 0 变 1」的那一瞬间做事。 代码里就是记住上一个值, 跟当前值比一下。 真实电路还要去抖:连续几次读到高才算数。 软件去抖比加电容省事,代价是反应慢几十毫秒——对开灯来说完全无所谓。

四、板子在 NAT 后面

路子 怎么做 代价
板子主动轮询 每几秒问一次服务器 延迟几秒,但不用公网
板子开 webhook 板子自己起服务,公网打进来 要内网穿透,内存也吃紧
中间加服务器 服务器收 LINE,板子去取 多一台机器,但最稳

这一节是整篇最像工程的地方。板子在局域网里,公网连不进来——这是 NAT 决定的,不是配置问题。 所以 13.1 篇那个 webhook 模型在设备侧正好反过来:不是别人敲你,是你定时去敲别人。 同一件事,从两个方向看。 教材用的是第三条的简化版:板子定时去取。 这也是绝大多数物联网产品的实际做法——设备永远在 NAT 后面,只能主动往外连。

第三条为什么最稳?因为它把「对外服务」和「设备接入」拆开了。 服务器可以换、可以扩容,板子只管连一个固定地址。

五、一条指令的完整来回

演示第五段跑五句话:说「状态」回「灯现在是关」,说「开灯」回「灯已开」, 再说「状态」回「灯现在是开」,说「关灯」回「灯已关」,最后说「乱说一句」回「不认识这个指令」。 整段逻辑就这么点:一个状态变量,几个 if。 物联网的门槛不在代码。 门槛在「板子怎么连上网」「断电重启后状态怎么恢复」这些坑上。

最后那句「不认识这个指令」别小看。 板子必须能应对乱输入——用户会在群里 @ 机器人说各种话,不兜住就是一片报错。 还有个坑演示里没演示:state 断电就归零了,但灯的实际状态可能还是开着的。 要么把状态存进闪存,要么启动时读一次引脚。 这两条路各有代价:存闪存会磨损、读引脚要求你有反馈线路。 做硬件最花时间的从来是这一类问题,不是算法。

六、本沙箱没有板子的部分

这一篇唯一没真跑的是硬件本身。 演示里那个 FakePin 类,用一段预设电平冒充 GPIO,value() 每调一次就吐下一个数。 读引脚的代码形状是真的,电平来源是假的。 这个边界要说清楚。 所以「灯真的亮了吗」这件事,本课答不了。能在本机验证的是逻辑,不是电路。

真要动手,需要四样东西:一块支持 MicroPython 的板子、一个 PIR 模块、一个继电器模块、几根杜邦线。 接线顺序别搞反:共地一定要接。 VCC 接错电压会烧模块,OUT 接错引脚只是不工作——后者好修, 前者要重新买。 还有一条安全上的:继电器那边是 220V,板子这边是 3.3V。 不懂电就别碰,找个懂的人一起做。

七、动手 Demo:MicroPython:让板子说话、让灯亮

先看板子能做什么、发一条消息要走几步(代码块是平台原样,不执行)。 然后真跑两段纯逻辑:PIR 的电平判读、以及从指令到开关灯的路由。

print('  ① 板子能做的两件事')
print('  ' + '-' * 68)
print()
print('| 要做的事 | 用的东西 | 线怎么接 |')
print('|---|---|---|')
for r in (('发一条 HTTP 请求', 'network + urequests', '连 WiFi,不用接线'),
          ('读人体有没有动', 'PIR 传感器', 'VCC、GND、OUT 接一个 GPIO'),
          ('开关一盏灯', '继电器模块', '信号脚接 GPIO,灯串在继电器上')):
    print('| %s | %s | %s |' % r)
print()
print('  板子上的 Python 叫 MicroPython,语法跟电脑上几乎一样,')
print('  不一样的是「没有操作系统」——没有文件管理器,没有 pip(有 upip),内存只有几百 KB。')
print('  所以板子上写代码的第一原则是省:不要一次读整个文件,不要建大列表,字符串拼接能省就省。')
print()
print('  ② 板子发一条消息要几步')
print('  ' + '-' * 68)
print('    1 连 WiFi,拿到 IP')
print('    2 拼一个 POST 请求,带 Channel Access Token')
print('    3 发出去,看返回码')
SRC = """# 本书附录 B-1(平台原样,本沙箱不执行:要 MicroPython 板子与 WiFi)
import network, urequests, json
sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.connect('SSID', 'password')
while not sta.isconnected():
    pass
body = json.dumps({'to': USER_ID,
                    'messages': [{'type': 'text', 'text': '有人经过'} ]})
r = urequests.post('https://api.line.me/v2/bot/message/push',
                   headers={'Authorization': 'Bearer ' + TOKEN,
                            'Content-Type': 'application/json'},
                   data=body)
print(r.status_code)
r.close()"""
print()
print('  [平台原样 · 本沙箱不执行]')
for ln in SRC.split('\n'):
    print('  | ' + ln)
print('  注意是 push 不是 reply:板子没有 replyToken,只能主动推')
print()
print('  板子上没有 replyToken,所以只能用 push。这也是 push 会被计费的原因——它不需要用户先说话。')
print('  urequests 是 requests 的精简版,接口故意做成一样。但少了连接池、少了自动重试,出错得自己兜。')
print()
print('  ③ PIR 传感器:只有「动」和「没动」')
print('  ' + '-' * 68)
print('    PIR(被动红外)测的是「视野里的热源有没有移动」')
print('    输出只有两种电平:高(有人动)和低(没人动)')
print('    但它会抖:人走过去可能产生好几个高电平脉冲')
print()


class FakePin(object):
    u"""用一段预设的电平序列,冒充 GPIO 读到的值。"""
    def __init__(self, seq):
        self.seq = seq
        self.i = 0

    def value(self):
        v = self.seq[self.i % len(self.seq)]
        self.i += 1
        return v


seq = [0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0]
pin = FakePin(seq)
print('    读 13 次电平:%s' % ''.join(str(pin.value()) for _ in range(13)))
print('    直接数「读到 1 的次数」→ %d 次(这就是抖动的结果)'
      % sum(1 for x in seq if x == 1))

pin = FakePin(seq)
events = 0
last = 0
for _ in range(13):
    v = pin.value()
    if v == 1 and last == 0:
        events += 1
    last = v
print('    只数「从 0 变 1」的次数 → %d 次(这才是真的「有人经过」)' % events)
print('  这叫边沿检测:只在信号「变化的那一瞬间」做事。')
print('  不这么做,一次经过会触发五六条通知,用户第一天就把你的机器人屏蔽了。')
print('  真实电路里还要加去抖:连续几次读到高才算数。软件去抖比硬件加电容更省事,代价是反应慢几十毫秒。')
print()
print('  ④ 从 LINE 收指令开灯')
print('  ' + '-' * 68)
print('    板子在局域网里,公网连不进来。所以两条路:')
print()
print('| 路子 | 怎么做 | 代价 |')
print('|---|---|---|')
for r in (('板子主动轮询', '每几秒问一次服务器「有指令吗」', '延迟几秒,但不用公网'),
          ('板子开 webhook', '板子自己起个小服务,公网打进来', '要内网穿透,板子内存也吃紧'),
          ('中间加一台服务器', '服务器收 LINE 消息,板子去服务器取', '多一台机器,但最稳')):
    print('| %s | %s | %s |' % r)
print()
print('  教材用的是第三条的简化版:板子定时去取。')
print('  这也是绝大多数物联网产品的实际做法——设备永远在 NAT 后面,只能主动往外连。')
print()
print('  ⑤ 本机模拟:一条指令的完整来回')
print('  ' + '-' * 68)
state = {'light': 0}


def handle(msg):
    msg = msg.strip()
    if msg in ('开灯', '灯开'):
        state['light'] = 1
        return '灯已开'
    if msg in ('关灯', '灯关'):
        state['light'] = 0
        return '灯已关'
    if msg == '状态':
        return '灯现在是%s' % ('开' if state['light'] else '关')
    return '不认识这个指令'


for m in ('状态', '开灯', '状态', '关灯', '乱说一句'):
    print('    LINE 那边说:%-8s 板子回:%s' % (m, handle(m)))
print('  整段逻辑就这么点:一个状态变量,几个 if。')
print('  物联网的门槛不在代码,在「板子怎么连上网」「断电重启后状态怎么恢复」这些坑上。')
print('  注意最后一个「不认识这个指令」。')
print('  板子必须能应对乱输入——用户会在群里 @ 机器人说各种话,不兜住就是一片报错。')

实跑输出:

--------------------------------------------------------------------
MicroPython:让板子说话、让灯亮
--------------------------------------------------------------------

  ① 板子能做的两件事
  --------------------------------------------------------------------

| 要做的事 | 用的东西 | 线怎么接 |
|---|---|---|
| 发一条 HTTP 请求 | network + urequests | 连 WiFi,不用接线 |
| 读人体有没有动 | PIR 传感器 | VCC、GND、OUT 接一个 GPIO |
| 开关一盏灯 | 继电器模块 | 信号脚接 GPIO,灯串在继电器上 |

  板子上的 Python 叫 MicroPython,语法跟电脑上几乎一样,
  不一样的是「没有操作系统」——没有文件管理器,没有 pip(有 upip),内存只有几百 KB。
  所以板子上写代码的第一原则是省:不要一次读整个文件,不要建大列表,字符串拼接能省就省。

  ② 板子发一条消息要几步
  --------------------------------------------------------------------
    1 连 WiFi,拿到 IP
    2 拼一个 POST 请求,带 Channel Access Token
    3 发出去,看返回码

  [平台原样 · 本沙箱不执行]
  | # 本书附录 B-1(平台原样,本沙箱不执行:要 MicroPython 板子与 WiFi)
  | import network, urequests, json
  | sta = network.WLAN(network.STA_IF)
  | sta.active(True)
  | sta.connect('SSID', 'password')
  | while not sta.isconnected():
  |     pass
  | body = json.dumps({'to': USER_ID,
  |                     'messages': [{'type': 'text', 'text': '有人经过'} ]})
  | r = urequests.post('https://api.line.me/v2/bot/message/push',
  |                    headers={'Authorization': 'Bearer ' + TOKEN,
  |                             'Content-Type': 'application/json'},
  |                    data=body)
  | print(r.status_code)
  | r.close()
  注意是 push 不是 reply:板子没有 replyToken,只能主动推

  板子上没有 replyToken,所以只能用 push。这也是 push 会被计费的原因——它不需要用户先说话。
  urequests 是 requests 的精简版,接口故意做成一样。但少了连接池、少了自动重试,出错得自己兜。

  ③ PIR 传感器:只有「动」和「没动」
  --------------------------------------------------------------------
    PIR(被动红外)测的是「视野里的热源有没有移动」
    输出只有两种电平:高(有人动)和低(没人动)
    但它会抖:人走过去可能产生好几个高电平脉冲

    读 13 次电平:0010110000100
    直接数「读到 1 的次数」→ 4 次(这就是抖动的结果)
    只数「从 0 变 1」的次数 → 3 次(这才是真的「有人经过」)
  这叫边沿检测:只在信号「变化的那一瞬间」做事。
  不这么做,一次经过会触发五六条通知,用户第一天就把你的机器人屏蔽了。
  真实电路里还要加去抖:连续几次读到高才算数。软件去抖比硬件加电容更省事,代价是反应慢几十毫秒。

  ④ 从 LINE 收指令开灯
  --------------------------------------------------------------------
    板子在局域网里,公网连不进来。所以两条路:

| 路子 | 怎么做 | 代价 |
|---|---|---|
| 板子主动轮询 | 每几秒问一次服务器「有指令吗」 | 延迟几秒,但不用公网 |
| 板子开 webhook | 板子自己起个小服务,公网打进来 | 要内网穿透,板子内存也吃紧 |
| 中间加一台服务器 | 服务器收 LINE 消息,板子去服务器取 | 多一台机器,但最稳 |

  教材用的是第三条的简化版:板子定时去取。
  这也是绝大多数物联网产品的实际做法——设备永远在 NAT 后面,只能主动往外连。

  ⑤ 本机模拟:一条指令的完整来回
  --------------------------------------------------------------------
    LINE 那边说:状态       板子回:灯现在是关
    LINE 那边说:开灯       板子回:灯已开
    LINE 那边说:状态       板子回:灯现在是开
    LINE 那边说:关灯       板子回:灯已关
    LINE 那边说:乱说一句     板子回:不认识这个指令
  整段逻辑就这么点:一个状态变量,几个 if。
  物联网的门槛不在代码,在「板子怎么连上网」「断电重启后状态怎么恢复」这些坑上。
  注意最后一个「不认识这个指令」。
  板子必须能应对乱输入——用户会在群里 @ 机器人说各种话,不兜住就是一片报错。

第二段那个 [平台原样 · 本沙箱不执行] 块是唯一跑不了的。 注意它是 push 不是 reply——板子没有 replyToken。 第三段那 13 个 0 和 1 是本篇最实在的东西。 数一数:0010110000100 里有 4 个 1,但只有 3 次「从 0 变 1」。 差的那一次就是抖动。真实场景里差的不止一次,所以边沿检测不是优化,是必须。 第五段那五行看着像玩具,但它们就是全部逻辑。真做产品,外面再包一层「丢进队列、后台处理」,就成了。 最后提醒一句:演示里的 state 存在内存里,断电就没了。真板子上要么存闪存、要么启动读引脚。

30 秒实验

  1. 把演示第三段的 seq 改成 [0, 1, 1, 1, 0, 0, 1, 0],看两种数法各得几次——这次差了几次?
  2. 给演示第五段的 handle 加一条指令「闪一下」,看它落到哪个分支; 再想想为什么它「应该」落到「不认识」。
  3. 把演示第五段的 state 初始值改成 {'light': 1},看第一句「状态」回什么。

常见误区

  1. 「板子上写 Python 跟电脑一样。 」没有操作系统、没有 pip、内存几百 KB。 电脑上随手写的大列表,板子上直接内存不足。
  2. 「传感器读到高电平就是有人经过。」它会抖。要数边沿,不要数电平,否则一次经过推六条通知。
  3. 「用 reply 回消息更省钱。」板子没有 replyToken。 设备只能 push,而 push 是按条计费的。
自测三问(点开对答案) 1. 为什么板子只能用 push 不能用 reply? —— 因为 reply 需要一个 `replyToken`,而那个 token 是用户发消息时 LINE 一起送过来的。 板子主动发消息时没有这个 token,所以只能走 push。 push 不要 token,用 `userId` 就行,但它是按条计费的。 2. `0010110000100` 这段电平里,为什么「数 1 的个数」和「数边沿」结果不一样? —— 因为信号会抖:人走过去会产生好几个高电平脉冲,中间还可能短暂回落。 数 1 得到 4 次,数「从 0 变 1」得到 3 次,多出来那次就是抖动。 只有边沿才对应「一次真实事件」。 3. 板子在局域网里,公网连不进来,那怎么接收 LINE 的指令? —— 让板子主动往外连。 三种做法:板子定时轮询服务器、板子自己做内网穿透(费内存)、或者中间加一台公网服务器, 板子去服务器取指令。第三种最稳,也是绝大多数物联网产品的实际做法——设备永远在 NAT 后面。

附录 B 结束。下一篇进附录 C:把人脸识别和 RFID 拼成一个门禁。

相关笔记