板子上的 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 秒实验
- 把演示第三段的
seq改成[0, 1, 1, 1, 0, 0, 1, 0],看两种数法各得几次——这次差了几次? - 给演示第五段的
handle加一条指令「闪一下」,看它落到哪个分支; 再想想为什么它「应该」落到「不认识」。 - 把演示第五段的
state初始值改成{'light': 1},看第一句「状态」回什么。
常见误区
- 「板子上写 Python 跟电脑一样。 」没有操作系统、没有 pip、内存几百 KB。 电脑上随手写的大列表,板子上直接内存不足。
- 「传感器读到高电平就是有人经过。」它会抖。要数边沿,不要数电平,否则一次经过推六条通知。
- 「用 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 拼成一个门禁。
- ☐ (本篇完,下一篇 C.1 人脸识别+RFID 门禁系统)