Luat全系列模块支持免费OTA远程升级,并提供例程将该功能部署到自己服务器上

准备姿势, update升级流程

先看一眼链接哦, 对升级过程有个认识
http://oldask.openluat.com/article/93

**升级流程

模块读取自身信息–拼接字符串–get请求服务器–服务器收到请求–处理请求–如果版本号低于最新版–返回200–模块下载升级包–下载成功自动重启–升级成功
如果版本号大于等于最新版–返回404错误码–模块停止升级检查。

再次强调重点

  1. 服务器返回200, 设备将读取响应,作为升级文件
  2. 服务器返回3XX, 重定向到新的地址下载
  3. 服务器返回4XX, 设备无需升级

设备端代码

1
2
3
4
require "update"
update.request(nil,"iot.nutz.cn/api/site/firmware_upgrade")
// 域名 + 升级URI, 用ip也是可以的
// 仅支持http协议,除非自行扩展update.lua

4G模块的升级报错代码,请查阅 http://oldask.openluat.com/article/90

划重点: 要用外网服务器!!! 局域网可访问不了!!!

服务器端

设备会发什么上来:

  1. URI: /luat/update 自定义即可,服务器与设备要一致
  2. 参数5个,均通过URL传递

服务器端接收到这些参数后,根据业务逻辑决定返回的内容

例如, 代码这样写,用的lod是Luat_V0017_ASR1802

1
2
3
PRODUCT_KEY = "sadfsaqwerOKMGUFI"
PROJECT = "ALIYUN"
VERSION = "2.0.0"

那么, URL参数会是这样

1
2
3
4
5
project_key=sadfsaqwerOKMGUFI
imei=86902342332452
firmware_name=ALIYUN_Luat_V0007_ASR1802
version=2.0.0
need_oss_url=0

另有完整服务器端实现,请查阅 http://oldask.openluat.com/article/878

代码示例

伪代码版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // 根据imei查出设备
var dev = sql("select * from t_dev where imie=" + imei);
if (dev == null) {
// 没有这个设备,不准升级, or 自动插入记录
return resp(403);
}
// 根据设备查出升级计划
var updatePlan = sql("select * from t_update_plan where dev_id=" + dev.id)
if (updatePlan == null) {
// 没有对应的升级计划,不准升级
return resp(403);
}
// 固件版本是否对应
if (firmware_name != updatePlan.firmware_name) {
// 固件版本不对应,无法升级
return resp(404); // 不是返回200就行
}
// 软件版本号是否一致
if (version == updatePlan.version) {
// 软件版本一样,无需升级
return resp(404);
}
// 需要升级,写入升级文件
resp.write(updatePlan.file)
// 结束.

java版(使用nutz实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@IocBean
@At("/luat")
public class LuatUpdateModule {

@Fail("http:500")
@Ok("void")
@At("/update")
public void update(String project_key, String imei, String firmware_name, String version, int need_oss_url, HttpServletResponse resp) throws FileNotFoundException, IOException {
// TODO 根据 imei 查出设备
// TODO 根据设备查出升级计划
String expectVersion = "2.0.1";
// 判断版本号
if (expectVersion.equalsIgnoreCase(version)) {
resp.setStatus(404); // 不需要升级
return;
}
try (FileInputStream ins = new FileInputStream("/data/luat/update/" + expectVersion + "/update.bin")) {
Streams.writeAndClose(resp.getOutputStream(), ins);
}
}
}

php例子,由罗耀锋提供

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function actionUpgrade() { 
$imei = UtilsApp::getParamterByParamterName("imei");
$ver = UtilsApp::getParamterByParamterName("version");
$updates = CardUpgrade::find()->all();
foreach ( $updates as $update) {
$verInt = intval($ver);
$updateVer = intval($update->ver);
if ($updateVer>$verInt) {
Yii::$app->getResponse()->sendFile(Yii::$app->basePath . '/web/' . $update->filepath, 'update.bin');
return;
}
}
Yii::$app->response->statusCode=500;
}

基于UDP的升级检查服务 python版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""

device_upgrade_server.py

python3, ubuntu16.04, mysql.

简单的升级服务,无数据校验等,仅供参考。
升级的配置存在mysql,是由另外的网页程序实现的。

需要安装sqlalchemy和gevent
pip3 install sqlalchemy
pip3 install gevent

mysql的python接口
apt-get install python3-mysql.connector

"""

from gevent.server import DatagramServer # 使用gevent的udp服务器。也可以直接用socket收。
from gevent import monkey
monkey.patch_all()

import gevent
import os
import sys
import time
import datetime
import re
import traceback
import logging

logging.basicConfig(level = logging.DEBUG)
dbg = logging.debug

def print_exception_info(): # 打印trace信息
#dbg(type(sys.exc_info()[1]))
#dbg(sys.exc_info()[1])
traceback.print_exc()
#exc_type, exc_obj, exc_tb = sys.exc_info()
#fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
#dbg(exc_type, fname, exc_tb.tb_lineno)


import sqlalchemy
from sqlalchemy import *
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# mysql数据库配置
DB = 'mysql+mysqlconnector://name:[email protected]:3306/device_framework_upgrade'
dbengine = create_engine(DB, echo = False, pool_size = 512, max_overflow = 0, pool_timeout = 0, pool_recycle = 5)
Session = sessionmaker(bind = dbengine)

'''

device's data look like "862991443753386,A9335_V1723_B3633_WD1,1.1.8" and "Get6,12"

数据格式

1.recv [imei,project_name,version]
2.send [LUAUPDATE,upgrade_id,package_count,last_package_size]

3.recv [Getn,upgrade_id]
4.send [data]

...

'''

PACKAGE_SIZE = 1022

start_cmd_pattern = re.compile("^(\d{15}),(\w+),(\d+\.\d+\.\d+)$")
get_cmd_pattern = re.compile("^Get(\d+),(\d+)$")

class SingleUpgradeServer(DatagramServer):
def __init__(self, *args, **kwargs):
DatagramServer.__init__(self, *args, **kwargs)

def handle(self, data_org, address): # 接收数据
try:
dbg('=================================================')
dbg(('=== %s %s: got data_org') % (time.ctime(), address[0]))
dbg('=================================================')

# 连接数据库
dbsession = Session()

data = data_org.decode('ascii') # ascii解码
dbg(data)

function = None
reply = bytearray()

# 匹配命令
match = start_cmd_pattern.match(data)
if match:
function = 0
else:
match = get_cmd_pattern.match(data)
if match:
function = 1

dbg('function = {}'.format(function))

if function == 0: # match start_cmd_pattern
imei, project_name, version = match.groups()

# 在数据库中根据项目名查找固件
upgrade = dbsession.execute(text("select * from device_framework_upgrade.single_upgrade where project_name = :project_name and status = 1"), {"project_name":project_name}).fetchone()

if upgrade is None:
reply = "no upgrade".encode('ascii')
dbg("no upgrade")

# 检查imei是否在升级范围内
in_range = False
imei_ranges = dbsession.execute(text("select * from device_framework_upgrade.single_upgrade_imei_range where upgrade_id = :upgrade_id"), {"upgrade_id":upgrade.id}).fetchall()

for imei_range in imei_ranges:
if imei_range.starting_imei <= imei and imei <= imei_range.ending_imei:
in_range = True
break

if not in_range:
reply = "imei range error".encode('ascii')
dbg("range error")

# 检查设备版本号是否最新
v1, v2, v3 = upgrade["version"].split('.')
server_version = (int(v1) << 16) + (int(v2) << 8) + int(v3)

v1, v2, v3 = version.split('.')
device_version = (int(v1) << 16) + (int(v2) << 8) + int(v3)

if device_version >= server_version:
reply = "version error".encode('ascii')
dbg("version error")
else:
# 读取文件信息
file_path = upgrade['file_path']
dbg(file_path)
file_size = os.path.getsize(file_path)

last_package_size = file_size % PACKAGE_SIZE
package_count = int(file_size / PACKAGE_SIZE)

if last_package_size != 0:
package_count += 1

dbg((file_size, package_count, last_package_size))

reply = ("LUAUPDATE,%d,%d,%d" % (upgrade["id"], package_count, last_package_size)).encode('ascii')
elif function == 1: # match get_cmd_pattern
index, upgrade_id = match.groups()
dbg("get")

# 查找文件
upgrade = dbsession.execute(text("select * from device_framework_upgrade.single_upgrade where id = :id and status = 1"), {"id":int(upgrade_id)}).fetchone()

file_path = upgrade['file_path']
dbg("file is %s" % file_path)

# 读取文件数据
fd = open(file_path, 'rb')
fd.seek((int(index) - 1) * PACKAGE_SIZE)
reply = bytearray([int(int(index) / 256), int(index) % 256]) + fd.read(PACKAGE_SIZE)
else:
reply = "unknown function".encode('ascii')
except:
dbg("error")
print_exception_info()
reply = "error all".encode('ascii')
finally:
# 回复数据给设备
self.socket.sendto(reply, address)
if dbsession:
dbsession.close()

if __name__ == '__main__':
dbg('device_upgrade_server.py receiving datagrams on %s:%d' % ('', 2234))
try:
# 启动udp服务器
SingleUpgradeServer('%s:%d' % ('', 2234)).serve_forever()
except:
print_exception_info()

上次更新 2021-01-28