Skip to content

Commit d7af001

Browse files
committed
feat(generics): support can
1 parent fc0ddf6 commit d7af001

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed

README.md

+60
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,63 @@ power_relay:open() -- open contact
6969
power_relay:close() -- close contact
7070
local power_is_on = power_relay:is_closed() -- check contact status
7171
```
72+
73+
### CAN
74+
75+
```lua
76+
local can_pkg = require('enapter.ucm.generics.can')
77+
78+
local can = can_pkg.new() -- creates a new instance of client to generic CAN UCM
79+
local err = can:setup('AABBCC', subscriptions) -- setup to operate with UCM with hardware id AABBCC (about subscriptions see below)
80+
local payload, err = can:get('telemetry') -- get can messages for 'my_subscription' messages
81+
```
82+
83+
#### CAN Subscriptions
84+
Subscriptions is a table:
85+
- key is a name of subscritption
86+
- value is an array of talbes describig CAN messages parsing rules
87+
88+
The parsing rule contains:
89+
- `name` or `names` of parsed values (one CAN message can contain more then one value).
90+
- CAN message ID `msg_id`.
91+
- `parser` function which received CAN message and return parsed value(s).
92+
- `multi_msg` flag if parser function receives array of message.
93+
94+
```lua
95+
subscriptoins = {
96+
example = {
97+
{ name = 'fw_ver', msg_id = 0x318, parser = software_version },
98+
{
99+
name = 'dump_0x400',
100+
msg_id = 0x400,
101+
multi_msg = true,
102+
parser = dump_0x400,
103+
},
104+
}
105+
}
106+
107+
function dump_0x400(datas)
108+
local str_0x400 = nil
109+
for _, data in pairs(datas) do
110+
str_0x400 = str_0x400 or ''
111+
str_0x400 = str_0x400 .. ' ' .. data
112+
end
113+
return str_0x400
114+
end
115+
116+
function software_version(data)
117+
data = convert_input_data(data)
118+
return string.format('%u.%u.%u', string.byte(data, 1), string.byte(data, 2), string.byte(data, 3))
119+
end
120+
121+
--- Converts message from Generic CAN to bytes
122+
-- Generic CAN passes message as a 16-char string where every 2 char represent a byte in hex format.
123+
function convert_input_data(data)
124+
local v = { string.unpack('c2c2c2c2c2c2c2c2', data) }
125+
local vv = {}
126+
for j = 1, 8 do
127+
vv[j] = tonumber(v[j], 16)
128+
end
129+
return string.pack('BBBBBBBB', table.unpack(vv))
130+
end
131+
```

spec/generic_can_spec.lua

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
local stubs = require('enapter.ucm.stubs')
2+
3+
_G.inspect = require('inspect')
4+
5+
describe('generic can', function()
6+
local can, ucm_stubs, cursor
7+
8+
local function prepare_peer_stub()
9+
local peer_stub = stubs.new_dummy_ucm_peer()
10+
ucm_stubs.stubs = { peer_stub }
11+
peer_stub.execute_command = function() return 'completed', { cursor = cursor } end
12+
return peer_stub
13+
end
14+
15+
before_each(function()
16+
ucm_stubs = stubs.setup_enapter_ucm()
17+
18+
package.loaded['enapter.ucm.generics.can'] = false
19+
can = require('enapter.ucm.generics.can').new()
20+
can:setup('test_ucm_id', {})
21+
end)
22+
23+
after_each(function()
24+
stubs.teardown_generic_can()
25+
stubs.teardown_enapter_ucm()
26+
end)
27+
28+
it('shoud execute read command when setup', function()
29+
local messages = {
30+
{ msg_id = 0x123 },
31+
{ msg_id = 0x789 },
32+
}
33+
local timeout = math.random(5000)
34+
35+
local peer_stub = prepare_peer_stub()
36+
local s = spy.on(peer_stub, 'execute_command')
37+
assert.is_nil(can:setup('', { test_cmd = messages }, timeout))
38+
assert.spy(s).was_called(1)
39+
assert
40+
.spy(s)
41+
.was_called_with(peer_stub, 'read', match.same({ msg_ids = { 0x123, 0x789 } }), { timeout = timeout })
42+
end)
43+
44+
it('should store cursor', function()
45+
cursor = math.random(100)
46+
assert.is_nil(can:setup('', { test_cur = {} }))
47+
48+
local peer_stub = prepare_peer_stub()
49+
50+
local s = spy.on(peer_stub, 'execute_command')
51+
local _, err = can:get('test_cur')
52+
assert.is_nil(err)
53+
assert.spy(s).was_called(1)
54+
assert
55+
.spy(s)
56+
.was_called_with(peer_stub, 'read', match.same({ msg_ids = {} }), { timeout = 1000 })
57+
58+
peer_stub = prepare_peer_stub()
59+
local s2 = spy.on(peer_stub, 'execute_command')
60+
_, err = can:get('test_cur')
61+
assert.is_nil(err)
62+
assert.spy(s2).was_called(1)
63+
assert
64+
.spy(s2)
65+
.was_called_with(peer_stub, 'read', match.same({ cursor = cursor, msg_ids = {} }), { timeout = 1000 })
66+
end)
67+
68+
it('should not get for unknow subscription', function()
69+
local _, err = can:get('unknown')
70+
assert.is.equals("subscritpion with name 'unknown' is not exists", err)
71+
end)
72+
73+
it('should handle error from generic can io', function()
74+
assert.is_nil(can:setup('', { test_can_error = {} }))
75+
76+
local peer_stub = prepare_peer_stub()
77+
peer_stub.execute_command = function() return 'completed', nil, 'can error' end
78+
79+
local _, err = can:get('test_can_error')
80+
assert.is.equals('command failed: can error', err)
81+
end)
82+
83+
it('should handle non-completed state from generic can io', function()
84+
assert.is_nil(can:setup('', { test_non_completed = {} }))
85+
86+
local peer_stub = prepare_peer_stub()
87+
peer_stub.execute_command = function() return 'non-completed', { errmsg = 'error msg' } end
88+
89+
local _, err = can:get('test_non_completed')
90+
assert.is.equals('command failed: non-completed: {errmsg = "error msg"}', err)
91+
end)
92+
93+
it('should get with name and names', function()
94+
local messages = {
95+
{
96+
name = 't_name',
97+
msg_id = 0x318,
98+
parser = function() return 'test name' end,
99+
},
100+
{
101+
names = { 'n_1', 'n_2' },
102+
msg_id = 0x418,
103+
parser = function() return { 1, 2 } end,
104+
},
105+
}
106+
107+
assert.is_nil(can:setup('', { test_names = messages }))
108+
109+
local peer_stub = prepare_peer_stub()
110+
peer_stub.execute_command = function() return 'completed', { results = { { 'r1' }, { 'r2' } } } end
111+
112+
local ret, err = can:get('test_names')
113+
assert.is_nil(err)
114+
115+
assert.is_same({ t_name = 'test name', n_1 = 1, n_2 = 2 }, ret)
116+
end)
117+
118+
it('should differ pass data to multi_msg', function()
119+
local test_data = { 'r1', 'r2' }
120+
local messages = {
121+
{
122+
name = 't_multi',
123+
msg_id = 0x318,
124+
multi_msg = true,
125+
parser = function(datas) return datas[1] .. datas[2] end,
126+
},
127+
{
128+
name = 't_single',
129+
msg_id = 0x418,
130+
parser = function(data) return data end,
131+
},
132+
}
133+
134+
assert.is_nil(can:setup('', { test_multi = messages }))
135+
136+
local peer_stub = prepare_peer_stub()
137+
peer_stub.execute_command = function()
138+
return 'completed', { results = { test_data, test_data } }
139+
end
140+
141+
local ret, err = can:get('test_multi')
142+
assert.is_nil(err)
143+
144+
assert.is_same({ t_multi = 'r1r2', t_single = 'r2' }, ret)
145+
end)
146+
147+
it('should handle errors from parser function', function()
148+
local messages = {
149+
{
150+
name = 'fail',
151+
msg_id = 0x404,
152+
parser = function() error('not found') end,
153+
},
154+
}
155+
156+
assert.is_nil(can:setup('', { test_names = messages }))
157+
158+
local peer_stub = prepare_peer_stub()
159+
peer_stub.execute_command = function() return 'completed', { results = { { 'any data' } } } end
160+
161+
local _, err = can:get('test_names')
162+
assert.is_equals(
163+
'data processing failed [msg_id=0x404 data={97 110 121 32 100 97 116 97 }]: <place>: not found',
164+
err:gsub('spec/generic_can_spec.lua:%d+', '<place>')
165+
)
166+
end)
167+
end)

src/enapter/ucm/generics/can.lua

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
local GENERAL_IO_CHANNEL = 'gio'
2+
local UCM_COMMAND_DEFAULT_TIMEOUT_MS = 1000
3+
4+
local function extract_msg_ids(messages)
5+
local msg_ids = {}
6+
for _, msg in ipairs(messages) do
7+
if msg.msg_id then table.insert(msg_ids, msg.msg_id) end
8+
end
9+
return msg_ids
10+
end
11+
12+
local can_read = function(self, name)
13+
local sub = self.subscriptions[name]
14+
if not sub then return nil, nil, "subscritpion with name '" .. name .. "' is not exists" end
15+
16+
local peer = ucm.new(self.ucm_id, GENERAL_IO_CHANNEL)
17+
local state, payload, err = peer:execute_command(
18+
'read',
19+
{ cursor = sub.cursor, msg_ids = extract_msg_ids(sub.messages) },
20+
{ timeout = self.timeout }
21+
)
22+
23+
if err then return nil, nil, 'command failed: ' .. err end
24+
25+
if state ~= 'completed' then
26+
local payload_str = inspect(payload, { newline = '', indent = '' })
27+
return nil, nil, 'command failed: ' .. tostring(state) .. ': ' .. payload_str
28+
end
29+
30+
return sub, payload, nil
31+
end
32+
33+
return {
34+
new = function()
35+
local can = { subscriptions = {} }
36+
37+
function can:setup(ucm_id, subscriptions, timeout)
38+
self.ucm_id = ucm_id
39+
self.timeout = timeout or UCM_COMMAND_DEFAULT_TIMEOUT_MS
40+
self.subscriptions = {}
41+
42+
local sub_err
43+
for name, messages in pairs(subscriptions) do
44+
self.subscriptions[name] = { messages = messages }
45+
local _, _, err = can_read(self, name)
46+
sub_err = sub_err or err
47+
end
48+
49+
return sub_err
50+
end
51+
52+
function can:get(name)
53+
local sub, payload, err = can_read(self, name)
54+
if err then return nil, err end
55+
56+
sub.cursor = payload.cursor
57+
58+
local ret = {}
59+
for i, h in ipairs(sub.messages) do
60+
local data = payload.results[i]
61+
if #data > 0 then
62+
if not h.multi_msg then data = data[#data] end
63+
64+
local rr
65+
local ok, err = pcall(function() rr = h.parser(data) end)
66+
if not ok then
67+
local data_arr = data:gsub('.', function(c) return string.byte(c) .. ' ' end)
68+
return nil,
69+
'data processing failed [msg_id='
70+
.. string.format('0x%x', h.msg_id)
71+
.. ' data={'
72+
.. data_arr
73+
.. '}]: '
74+
.. err
75+
end
76+
77+
if h.name ~= nil then
78+
ret[h.name] = rr
79+
else
80+
for j, k in ipairs(h.names) do
81+
ret[k] = rr[j]
82+
end
83+
end
84+
end
85+
end
86+
87+
return ret, nil
88+
end
89+
90+
return can
91+
end,
92+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
local function new_dummy_can()
2+
return {
3+
setup = function() end,
4+
get = function() return {} end,
5+
}
6+
end
7+
8+
local function setup_generic_can()
9+
local stub = { stubs = {} }
10+
package.loaded['enapter.ucm.generics.can'] = false
11+
package.preload['enapter.ucm.generics.can'] = function()
12+
return {
13+
new = function() return table.remove(stub.stubs, 1) or new_dummy_can() end,
14+
}
15+
end
16+
return stub
17+
end
18+
19+
local function teardown_generic_can()
20+
package.loaded['enapter.ucm.generics.can'] = false
21+
package.preload['enapter.ucm.generics.can'] = nil
22+
end
23+
24+
return {
25+
new_dummy_can = new_dummy_can,
26+
setup_generic_can = setup_generic_can,
27+
teardown_generic_can = teardown_generic_can,
28+
}

src/enapter/ucm/stubs/init.lua

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local t = {}
33
for _, pkg in ipairs({
44
'enapter.ucm.stubs.enapter_ucm',
55
'enapter.ucm.stubs.generics_rl6',
6+
'enapter.ucm.stubs.generics_can',
67
}) do
78
for k, v in pairs(require(pkg)) do
89
t[k] = v

0 commit comments

Comments
 (0)