电子标签总结、优化
# 技术点
# 网络连接流程
ESL 路由上电并连接网线后,开始通过 UDP 广播 ID(广播端口号 3066),ESL 服务器开启 UDP 监听 3066 端口即可接收到广播,收到广播后 ESL 服务器记录下 ESL 路由的 ID 和 IP 地址待以后需要时使用, 然后发 起 TCP 连接(端口号 3068),连接成功后,ESL 停止发送 UDP 广播,然后 ESL 服务器就可以发送以下协议 内容的控制指令了。 另外,ESL 服务器可以控制 ESL 路由开启 UDP 通道,实时上报收到的 ESL 标签数据。
# TCP/UDP通讯逻辑
- ESL标签按预设的频点连接ESL路由,一个ESL路由对应多个ESL标签,一个ESL标签对应单个ESL路由。
- ESL路由连接到局域网后,自动发送广播,服务器发现新设备后回传IP地址,ESL路由通过UDP上报到此IP地址上去。
- 服务器记录每个标签的价格数据和每个标签对应的路由,如果有价格需要更改,则向指定路由发送更改指令。然后路由轮询 标签实现价格更改。 注:后期可加入跳频机制,以应对2.4G频段无线信号的冲突问题
# 设置样式包步骤: 分片传送
tcp粘包处理;
而对于处理粘包的问题, 常见的解决方案有:
- 多次发送之前间隔一个等待时间
- 关闭 Nagle 算法
- 进行封包/拆包;封包/拆包是目前业内常见的解决方案了. 即
给每个数据包在发送之前, 于其前/后放一些有特征的数据, 然后收到数据的时候根据特征数据分割出来各个数据包
.
# 项目分片发送的步骤
1:传输样式包数据
2:校验样式包
3:增加样式包任务
4:任务开始
数组文件buf + 另外2条验证任务指令;
function setStylePkg (para, pn) {
const file = `./public/${para.style}`
const fileBuf = fs.readFileSync(file)
const cmdArr = []
const splices = updateStylePkg(para, fileBuf)
cmdArr.push(...splices)
cmdArr.push(checkStylePkg(para, fileBuf))
cmdArr.push(setStyleTask(para))
//console.log(cmdArr)
return cmdArr
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 优化方案
最后通过优化,把之前设置价格及样式时间,从之前的2min优化到5s内;
从多部分做优化:
- 代码处理逻辑;
- 标签硬件部分优化;
- 标签队列调整;
# 代码处理逻辑
- 没有处理好之前的buf格式问题,修改原来按照字节进制间转换, 最后直接通过buf原有api, 处理大小端;
- 优化原来的发送逻辑;
比如:之前把返回的buf数据转换成数组再切割,之后用 buf.readIntLE
可以替换;
function parseTagInfo (data) {//之前处理方式;
const str = data.toString('hex')
const arr = hexToArray(str)
let cmd = arr.slice(0, 1).reverse().join('')
cmd = parseInt(cmd, 16)
let tagId = arr.slice(1, 5).reverse().join('')
tagId = parseInt(tagId, 16)
let price1 = arr.slice(5, 7).reverse().join('')
price1 = parseInt(price1, 16)
let price2 = arr.slice(7, 9).reverse().join('')
price2 = parseInt(price2, 16)
let tagNum = arr[8]
tagNum = parseInt(tagNum, 16)
const ret = {
tag_id: tagId,
tag_num: tagNum,
price1: price1 / 100,
price2: price2 / 100
}
return ret
}
//优化后的代码
function codeToString (num) {
num = num + ''
const code = '0000000000'.substr(num.length) + num
return code
}
function parseTagInfo (data) { // <Buffer 01 04 00 00 00 21 d0 07 00 d0 07 00 00>
const buf = Buffer.from(data)
const rssi = buf.readIntLE(0, 1)
const model = buf.readIntLE(1, 1)
const code = codeToString(buf.readIntLE(2, 4))
const status = buf.readIntLE(6, 1)
const price1 = buf.readIntLE(7, 3)
const price2 = buf.readIntLE(10, 3)
const mark = buf.readIntLE(13, 1)
return {
rssi,
model,
code: code.toString(),
status, // 20 标签已关闭; 21 标签已开启;
priceShow: price1 / 100,
memberPriceShow: price2 / 100,
mark
}
}
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
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
# 标签硬件部分优化
- 硬件协议方式修改,更加明确;
- 本协议分两层Layer1(L1 Header + L1 Tail)和Layer2(L2 Header + Parameters)。(TCP层数据已校验过了,此协议不再校验);
- Layer1为传输层,用于验证数据完整性和保证传输的可靠性;Layer2为应用层,用于定义应用数据格式。
# 标签队列调整
控制任务队列数量,一个路由器对应一个任务队列;防止同时设置样式及价格,并发冲突;通过host标识;
发送策略调整;修改样式时,考虑到要分包传输避免粘包传输,一次性读取样式包,每MTU200为一组
- 原来的是:每隔0.5秒发送一次,直到数组全部发送完,最后才监听上报成功状态;
- 现在修改为:每发送一次,要等标签硬件响应一次才算完成;最后单独监听上报设置成功状态;优点:可以直接了解标签的状态,随时添加及移除监听事件;上报成功后移除全部监听事件,防止监听事件泄露;
- 添加发送逻辑总体超时时间设置。对于发送超时超过三分钟的时,设置job移除到失败后,再次重试;
# 部分实现
发送消息监听处理:价格及样式;
function sendTcpMsg (client, data, type = 0) {
return new Promise(function (resolve, reject) {
TcpClientEvents.once(`tcp_response_${client.id}`, res => {
resolve(res.toString('hex'))
})
TcpClientEvents.once(`tcp_error_${client.id}`, err => {
reject(err)
})
client.write(data)
})
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
发送标签样式处理:
const tcpResId = `tcp_response_${client.id}`
if (type === 3) {
let timeoutId = null
TcpClientEvents.on(tcpResId, res => {
if (esl.checkStyleFinished(res)) {
TcpClientEvents.removeAllListeners(tcpResId)
if (timeoutId) { clearTimeout(timeoutId) }
done(null, res.toString('hex'))
}
})
timeoutId = setTimeout(async () => {
if (timeoutId) { clearTimeout(timeoutId) }
await job.moveToFailed({ message: '响应监听超时3min' }, false)
await job.retry()
done(null, job)
agent.logger.info('Tcp send data timeout: ', '响应监听超时3min')
}, timeOut)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
队列的创建:
function createBullClient (agent, host) {
if (!agent.bull.get(host)) {
const conf = {}
conf.redis = agent.config.bull.default.redis
conf.topic = host
const queue = agent.bull.create(conf, agent)
agent.bull.clients.set(host, queue)
queue.process('sendCmd', async (job, done) => {
//xxx
})
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
价格处理:目前价格兼容最大为8W; 小端模式4位,再后面切割1位;还考虑精确度;
function setPriceTask (para, pn) {
const tagB = Buffer.alloc(4)
tagB.writeUInt16LE(para.code)
let price1B = Buffer.alloc(4)
price1B.writeInt32LE((para.price * 100).toFixed(2))
price1B = price1B.slice(0, 3)
let price2B = Buffer.alloc(4)
price2B.writeInt32LE((para.memberPrice * 100).toFixed(2))
price2B = price2B.slice(0, 3)
const PARA = [...tagB, 0x1, ...price1B, ...price2B, 0x0]
const PARA_L = Buffer.alloc(1, PARA.length)
const cmdP = [0x2, PARA_L, ...PARA]
const l2A = [L2P_TYPE.return, CMD.setTaskSet, ...cmdP]
return comm(l2A, pn)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上次更新: 2023/11/17, 05:08:18