add: 添加新的阿里云物联网库-aliyun2库,及搭配的demo, 逐步完善中
[LuatOS.git] / script / libs / aliyun2.lua
blob672a160f5a0dffbb8227a1b86356d484c5a040a3
1 --[[
2 @module aliyun2
3 @summary 阿里云物联网平台(开发中)
4 @version 1.0
5 @date 2024.05.18
6 @author wendal
7 @demo aliyun2
8 @usage
9 -- 请查阅demo
11 -- 本库基于阿里云物联网重新设计, 与aliyun.lua库不兼容
12 -- 本库尚属开发测试阶段, API随时可能变化, 也可能不变^_^
14 _G.sys = require("sys")
16 local aliyun2 = {}
17 local g_id = 1
19 --[[
20 初始化一个aliyun示例
21 @api aliyun.create(opts)
22 @table 参数表
23 @return aliyun实例
24 @usage
25 -- 初始化一个aliyun示例
26 local ali = aliyun.create(opts)
27 if ali and aliyun2.start(ali) then
28 while 1 do
29 local result, tip, params = sys.waitUntil(ali.topic, 30000)
30 if result then
31 log.info("aliyun", "event", tip, params)
32 end
33 end
34 else
35 log.error("aliyun", "初始化失败")
36 end
38 function aliyun2.create(opts)
39 if not opts then
40 log.error("aliyun2", "配置参数表不能是空")
41 return
42 end
43 -- 检查最基本的参数
44 if not opts.productKey or not opts.deviceName then
45 log.error("aliyun2", "缺失配置参数productKey")
46 return
47 end
48 if not opts.deviceName then
49 if mobile then
50 opts.deviceName = mobile.imei()
51 elseif wlan and wlan.getMac then
52 opts.deviceName = wlan.getMac()
53 else
54 opts.deviceName = mcu.unique_id():toHex()
55 end
56 log.info("aliyun2", "deviceName未指定,自动分配", opts.deviceName)
57 end
58 if opts.productSecret and #opts.productSecret == 0 then
59 opts.productSecret = nil
60 end
61 if opts.deviceSecret and #opts.deviceSecret == 0 then
62 opts.deviceSecret = nil
63 end
64 if not opts.store_key then
65 opts.store_key = opts.productKey .. "_" .. opts.deviceName
66 end
67 if opts.productSecret and not opts.deviceSecret then
68 -- 从本地存储读取设备密钥
69 local payload = nil
70 if fskv then
71 payload = fskv.get(opts.store_key)
72 end
73 if not payload or #payload < 16 then
74 payload = io.readFile(opts.store_key)
75 end
76 if payload then
77 local jdata = json.decode(payload)
78 if jdata then
79 opts.deviceSecret = jdata["deviceSecret"]
80 opts.deviceToken = jdata["deviceToken"]
81 opts.clientId = jdata["clientId"]
82 end
83 end
84 end
86 if not opts.productSecret and not opts.deviceSecret and not opts.deviceToken then
87 log.error("aliyun2", "请指定productSecret或deviceSecret")
88 return
89 end
90 local ctx = opts
91 -- 计算mqtt的host和port
92 if not ctx.mqtt_host then
93 -- ctx.mqtt_host = "iot-as-mqtt." .. ctx.productKey .. ".aliyuncs.com"
94 if ctx.instanceId then
95 ctx.mqtt_host = ctx.instanceId .. ".mqtt.iothub.aliyuncs.com"
96 else
97 ctx.mqtt_host = ctx.productKey .. ".iot-as-mqtt." .. (opts.regionId or "cn-shanghai") ..".aliyuncs.com"
98 end
99 end
100 if not ctx.mqtt_port then
101 ctx.mqtt_port = 1883
103 ctx.device_retry = 0
104 ctx.topic = "aliyun2_" .. g_id
105 g_id = g_id + 1
107 -- 生成mqtt的topic
108 if ctx.auto_topic == nil or ctx.auto_topic then
109 if not ctx.sub_topics then
110 ctx.sub_topics = {}
112 if not ctx.pub_topics then
113 ctx.pub_topics = {}
115 local topics = ctx.sub_topics
116 -- 订阅必要的topic
117 local dn = ctx.productKey .. "/" .. ctx.deviceName
118 -- 首先是OTA的topic
119 topics.ota = "/ota/device/upgrade/" .. dn
120 -- 配置更新信息
121 topics.config_push = "/sys/" .. dn .. "/thing/config/push"
122 -- 广播信息
123 topics.broadcast = "/broadcast/" .. ctx.productKey .. "/#"
124 -- NTP信息
125 topics.ntp = "/ext/ntp/" .. dn .. "/response"
127 -- 物模型, 透传信息
128 topics.raw_reply = "/sys/" .. dn .. "/thing/model/up_raw_reply"
129 topics.raw_down = "/sys/" .. dn .. "/thing/model/down_raw"
130 -- 非透传
131 topics.property_set = "/sys/".. dn .. "/thing/service/property/set"
133 -- 上行常用的topic
134 topics = ctx.pub_topics
135 topics.inform = "/ota/device/inform/".. dn
136 topics.ntp = "/ext/ntp/".. dn .. "/request"
137 topics.raw_up = "/sys/".. dn .. "/thing/model/up_raw"
140 if ctx.instanceId then
141 log.info("aliyun2", "instanceId", ctx.instanceId)
143 log.info("aliyun2", "deviceName", ctx.deviceName)
144 log.info("aliyun2", "mqtt_host", ctx.mqtt_host)
145 log.info("aliyun2", "mqtt_port", ctx.mqtt_port)
146 return ctx
149 local function aliyun_do_reg(ctx)
150 local mqttc = mqtt.create(ctx.adapter, ctx.mqtt_host, 443, true)
151 if not mqttc then
152 log.error("aliyun2", "创建mqtt实例失败")
153 return
155 -- 计算自动注册所需要的密钥
156 local tm = tostring(os.time())
157 local client_id = string.format("%s|securemode=%s,authType=%s,random=%s,signmethod=hmacsha1%s|", ctx.deviceName, ctx.regnwl and -2 or 2, ctx.regnwl and "regnwl" or "register", tm, (ctx.instanceId and ",instanceId=" .. ctx.instanceId or ""))
158 log.info("aliyun2", "开始注册流程", client_id)
159 local user_name = ctx.deviceName .. "&"..ctx.productKey
160 local content = "deviceName"..ctx.deviceName.."productKey"..ctx.productKey.."random" .. tm
161 local password = crypto.hmac_sha1(content, ctx.productSecret)
162 log.info("aliyun2", "尝试注册", client_id, user_name, password)
163 mqttc:auth(client_id, user_name, password)
164 mqttc:keepalive(240) -- 实际上会忽略该属性
165 -- mqttc:debug(true)
166 mqttc:autoreconn(false, 3000) -- 不需要自动重连
167 local regok = false -- 记录注册成功与否
168 mqttc:on(function(mqtt_client, event, data, payload)
169 log.info("aliyun2", "event", event, data, payload)
170 if event == "recv" then
171 log.info("aliyun", "downlink", "topic", data, "payload", payload)
172 if payload then
173 local jdata,res = json.decode(payload) -- TODO 搞个alijson库进行封装
174 if jdata and (jdata["deviceSecret"] or jdata["deviceToken"]) then
175 log.info("aliyun2", "获取到设备密钥")
176 regok = true
177 ctx.deviceSecret = jdata["deviceSecret"]
178 ctx.deviceToken = jdata["deviceToken"]
179 ctx.clientId = jdata["clientId"]
180 sys.publish(tm, "reg", "ok")
181 if fskv then
182 log.info("aliyun2", "密钥信息存入fskv", ctx.store_key)
183 fskv.set(ctx.store_key, payload)
185 log.info("aliyun2", "密钥信息存入文件系统", ctx.store_key)
186 io.writeFile(ctx.store_key, payload)
187 else
188 sys.publish(tm, "reg", "fail")
189 return
192 elseif event == "close" then
193 sys.publish(tm, "close")
195 end)
196 mqttc:connect()
197 sys.waitUntil(tm, 5000)
198 sys.wait(100)
199 mqttc:close()
200 if regok then
201 log.info("aliyun2", "自动注册成功,密钥信息已获取")
202 else
203 log.info("alyun2", "自动注册失败,延迟30秒后重试")
204 sys.wait(30000)
208 local function aliyun_task_once(ctx)
209 -- 几个条件: 有设备密钥, 有产品密钥, 登陆失败的次数
210 -- 情况1: 只有设备密钥, 没有产品密钥, 那就固定是一机一密
211 -- 情况2: 只有产品密钥, 没有设备密钥, 那就是一机一密
212 -- 情况3: 有产品密钥, 有设备密钥, 登录失败次数少于设定值(默认3次),那继续用一机一密去尝试登陆
213 -- 情况4: 有产品密钥, 有设备密钥, 登录失败次数大于设定值(默认3次), 使用一型一密去尝试注册一次
215 if ctx.productSecret then
216 if ctx.deviceSecret or ctx.deviceToken then
217 if ctx.device_retry < 3 then
218 -- 失败次数还不多,先尝试用一机一密
219 else
220 log.info("aliyun2", "设备密钥已存在,但已经连续失败3次,尝试重新注册")
221 aliyun_do_reg(ctx)
222 ctx.device_retry = 0
224 else
225 aliyun_do_reg(ctx)
229 if not ctx.deviceSecret and not ctx.deviceToken then
230 log.info("aliyun2", "未能获取到设备密钥,等待重试")
231 return
234 local mqttc = mqtt.create(ctx.adapter, ctx.mqtt_host, ctx.mqtt_port, ctx.mqtt_isssl, ctx.ca_file)
235 if not mqttc then
236 log.error("aliyun2", "创建mqtt实例失败")
237 return
239 if ctx.deviceSecret then
240 local client_id,user_name, password = iotauth.aliyun(ctx.productKey, ctx.deviceName, ctx.deviceSecret)
241 log.info("aliyun2", "密钥模式", client_id,user_name, password)
242 mqttc:auth(client_id, user_name, password)
243 else
244 local client_id = ctx.clientId .. "|securemode=-2,authType=connwl|"
245 local user_name = ctx.deviceName.."&"..ctx.productKey
246 log.info("aliyun2", "token模式", client_id, user_name, ctx.deviceToken)
247 mqttc:auth(client_id, user_name, ctx.deviceToken)
249 mqttc:keepalive(ctx.mqtt_keepalive or 240)
250 -- mqttc:debug(true)
251 mqttc:autoreconn(false, 3000) -- 不需要自动重连
252 mqttc:on(function(mqtt_client, event, data, payload)
253 log.info("aliyun2", "event", event, data, payload)
254 if event == "conack" then
255 log.info("aliyun2", "连接成功,鉴权完成")
256 ctx.device_retry = 0
258 -- 需要订阅的topic
259 if ctx.sub_topics then
260 for k, v in pairs(ctx.sub_topics) do
261 log.info("aliyun2", "订阅topic", v, "别名", k)
262 mqttc:subscribe(v)
266 -- 上报一些基础信息
267 -- 版本信息
268 if ctx.pub_topics and ctx.pub_topics.inform then
269 local info = ctx.inform_data or {version=_G.VERSION, module=rtos.bsp():upper()}
270 info = json.encode({id="123", params=info})
271 log.info("aliyun2", "上报版本信息", ctx.pub_topics.inform, info)
272 mqttc:publish(ctx.pub_topics.inform, info, 1)
275 sys.publish(ctx.topic, "conack")
276 elseif event == "recv" then
277 -- 收到消息
278 log.info("aliyun2", "收到下行信息", "topic", data, "payload前128字节", payload and payload:sub(1, 128))
279 sys.publish(ctx.topic, "recv", data, payload)
280 -- TODO 支持FOTA/OTA
281 if ctx.sub_topics and ctx.sub_topics.ota == data then
282 log.info("aliyun2", "收到ota信息", payload)
283 local jdata = json.decode(payload)
284 if jdata and jdata.data and jdata.data.url then
285 log.info("aliyun2", "获取到OTA所需要的URL", jdata.data.url)
286 sys.publish(ctx.topic, "ota", jdata.data)
290 end)
291 mqttc:connect()
292 sys.waitUntil(ctx.topic, 5000)
293 if mqttc:ready() then
294 ctx.mqttc = mqttc
295 ctx.device_retry = 0
296 while mqttc:ready() and ctx.running do
297 sys.waitUntil(ctx.topic, 5000)
299 else
300 ctx.device_retry = ctx.device_retry + 1
302 mqttc:close()
303 log.info("aliyun2", "单次任务结束")
304 ctx.mqttc = nil
307 local function aliyun_task (ctx)
308 -- 准备好参数
310 -- 开始主循环
311 while ctx.running do
312 aliyun_task_once(ctx)
313 sys.wait(2000)
315 log.info("aliyun2", "阿里云任务结束")
318 --[[
319 启动aliyun实例
320 @api aliyun2.start(ali)
321 @return 成功返回true,失败返回false
323 function aliyun2.start(ali)
324 ali.running = true
325 ali.c_task = sys.taskInit(aliyun_task, ali)
326 return ali.c_task ~= nil
329 --[[
330 关闭aliyun实例
331 @api aliyun2.stop(ali)
332 @return boolean 成功返回true,失败返回false
334 function aliyun2.stop(ali)
335 if ali.c_task then
336 ali.running = nil
337 sys.publish(ali.topic, "stop")
341 --[[
342 是否已经连接上
343 @api aliyun2.ready(ali)
344 @return boolean 已经成功返回true,否则一概返回false
346 function aliyun2.ready(ali)
347 if ali and ali.mqttc and ali.mqttc:ready() then
348 return true
352 --[[
353 订阅自定义的topic
354 @api aliyun2.subscribe(ali, topic)
355 @return 成功返回true,失败返回false或者nil
357 function aliyun2.subscribe(ali, topic, qos)
358 if not aliyun2.ready(ali) then
359 log.warn("aliyun2", "还没连上服务器,不能订阅", topic)
360 return
362 ali.mqttc:subscribe(topic, qos or 1)
363 return true
366 --[[
367 上报消息,上行数据
368 @api aliyun2.publish(ali, topic, payload, qos, retain)
369 @return 成功返回true,失败返回false或者nil
371 function aliyun2.publish(ali, topic, payload, qos, retain)
372 if not aliyun2.ready(ali) then
373 log.warn("aliyun2", "还没连上服务器,不能上报", topic)
374 return
376 ali.mqttc:public(topic, payload, qos or 1, retain or 0)
377 return true
380 -- TODO
381 -- 1. alijson库
382 -- 2. 对物模型做一些封装
383 -- 3. 对透传进行一些封装
385 return aliyun2