diff --git a/bc-tests.lua b/bc-tests.lua new file mode 100644 index 000000000000..b0a76ee81b18 --- /dev/null +++ b/bc-tests.lua @@ -0,0 +1,334 @@ +require("lunit") + +local network = require("network") +local serialization = require("serialization") +local BaseControl = require("bc") +local Event = BaseControl.Event + +module("bc-tests", package.seeall, lunit.testcase) + +-- Basics {{{ +function test_init() + local bc = BaseControl({test_noun=true}, {test_verb=function() end}) + assert_true(bc:has_noun("test_noun"), "Local nouns were not initialized") + assert_true(bc:has_verb("test_verb"), "Local verbs were not initialized") +end + +function test_local_noun() + local bc = BaseControl({test_noun2=1234}) + assert_equal(1234, bc:get_noun("test_noun2"), "Invalid initial value") + bc:set_noun("test_noun2", 4321) + assert_equal(4321, bc:get_noun("test_noun2"), "Invalid new value") +end + +function test_local_verb() + local bc = BaseControl(nil, { + test_verb2 = function(_, _, a, b) + assert_equal(12, a, "Invalid parameter A") + assert_equal(34, b, "Invalid parameter B") + end, + }) + + bc:call_verb("test_verb2", 12, 34) +end + +function test_unknown() + local bc = BaseControl() + + assert_false(bc:has_noun("unknown"), "has_noun failed") + assert_equal(nil, bc:get_noun("unknown"), "get_noun failed") + assert_error("Unknown noun writable", function() + bc:set_noun("unknown", 1234) + end) +end +-- }}} + +-- Networking {{{ +function test_hello() + local bc1 = BaseControl({hello1=true}, {hello1v=function() end}) + local bc2 = BaseControl({hello2=true}, {hello2v=function() end}) + + assert_true(bc1:has_noun("hello1"), "Announcement did not come through") + assert_true(bc2:has_noun("hello1"), "Announcement did not come through") + assert_true(bc1:has_noun("hello2"), "Announcement did not come through") + assert_true(bc2:has_noun("hello2"), "Announcement did not come through") + + assert_true(bc1:has_verb("hello1v"), "Anverbcement did not come through") + assert_true(bc2:has_verb("hello1v"), "Anverbcement did not come through") + assert_true(bc1:has_verb("hello2v"), "Anverbcement did not come through") + assert_true(bc2:has_verb("hello2v"), "Anverbcement did not come through") +end + +function test_remote_noun() + local a = {n = "A", bc = BaseControl({noun1=1234}), addr = network.get_scene()} + local b = {n = "B", bc = BaseControl({noun2=4321}), addr = network.get_scene()} + local c = {n = "C", bc = BaseControl(), addr = network.get_scene()} + local abc = {a, b, c, a, c} + + for _, bc in ipairs(abc) do + network.set_scene(bc.addr) + assert_equal(1234, bc.bc:get_noun("noun1"), bc.n..": noun1 failed!") + assert_equal(4321, bc.bc:get_noun("noun2"), bc.n..": noun2 failed!") + end +end + +function test_noun_nv() + local bc1 = BaseControl({noun_nv=1234}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + + assert_equal(1234, bc2:get_noun("noun_nv"), "Wrong answer!") + network.set_scene(addr1) + bc1:set_noun("noun_nv", 4321) + network.set_scene(addr2) + assert_equal(4321, bc2:get_noun("noun_nv"), "Wrong answer!") +end + +function test_remote_verb() + local tmp_a = 1 + local tmp_b = 64 + local a = {n = "A", bc = BaseControl(), addr = network.get_scene()} + local b = {n = "B", bc = BaseControl(nil, {verb1 = function(_, _, a, b) + assert_equal(tmp_a, a, "A is wrong!") + assert_equal(tmp_b, b, "B is wrong!") + end}), addr = network.get_scene()} + local c = {n = "C", bc = BaseControl(), addr = network.get_scene()} + local abc = {b, a, b, c, a, c} + + for _, bc in ipairs(abc) do + network.set_scene(bc.addr) + bc.bc:call_verb("verb1", tmp_a, tmp_b) + tmp_a = tmp_a + 1 + tmp_b = tmp_b + 1 + end +end + +function test_two_way_verb() + local bc1 = BaseControl(nil, {twoway1 = function(bc, _, verb, par) + bc:call_verb(verb, par * 2) + end}) + local addr1 = network.get_scene() + + local result + local bc2 = BaseControl(nil, {twoway2 = function(bc, _, res) + result = res + end}) + local addr2 = network.get_scene() + + bc2:call_verb("twoway1", "twoway2", 1234) + assert_equal(2468, result, "Double call failed") +end +-- }}} + +-- Listening {{{ +function test_listen_attach() + local bc1 = BaseControl({listen1=4321}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + + local tmp + local tmp2 + + local id = bc2:listen("listen1", Event.Change, function(newval) + if tmp == nil then + fail("Should not have been called") + end + assert_equal(tmp, newval, "Change not correct") + tmp2 = newval + end) + + network.set_scene(addr1) + for _, t in ipairs{1, 2, 3, 4} do + tmp = t + bc1:set_noun("listen1", t) + assert_equal(t, tmp2, "Listener not called") + end + + bc2:cancel(id) + + bc1:set_noun("listen1", 1234) +end + +function test_listen_modes() + local bc1 = BaseControl({listen2=4321}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + + local values = { + {e=Event.Change, p=nil, s=1234, snot=1234, s2=1235}, + {e=Event.Rising, p=nil, s=1236, snot=1233, s2=1237}, + {e=Event.Falling, p=nil, s=1236, snot=1237, s2=1235}, + {e=Event.Equals, p=1337, s=1337, snot=1234, s2=1337}, + {e=Event.Above, p=4321, s=4333, snot=1234, s2=4334}, + {e=Event.Below, p=4321, s=1234, snot=4444, s2=1212}, + } + + for _, test in ipairs(values) do + local active = false + local fired = false + network.set_scene(addr2) + local id = bc2:listen("listen2", test.e, test.p, function(new) + if not active then + fail("Should not have been called") + end + assert_equal(test.s, new, "Wrong value!") + fired = true + end) + + network.set_scene(addr1) + active = true + bc1:set_noun("listen2", test.s) + assert_true(fired, "Listener was not called") + + fired = false + bc1:set_noun("listen2", test.snot) + assert_false(fired, "Listener was accidentally called") + + network.set_scene(addr2) + active = false + bc2:cancel(id) + + network.set_scene(addr1) + bc1:set_noun("listen2", test.s2) + end +end +-- }}} + +-- Dirty Business {{{ +function test_connectivity_loss_listening() + local bc1 = BaseControl({connectivity2=12345}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + + local called = 0 + local id = bc2:listen("connectivity2", Event.Change, nil, function() + called = called + 1 + end) + + network.set_scene(addr1) + bc1:set_noun("connectivity2", 321) + assert_equal(1, called, "Listener was not called!") + + network.deregister(addr2) + + network.allow_blackhole = true + do + bc1:set_noun("connectivity2", 123) + assert_equal(1, called, "Listener was called?!") + + local bc2 = BaseControl() + local addr2 = network.get_scene() + + local called2 = false + local id2 = bc2:listen("connectivity2", Event.Change, nil, function() + called2 = true + end) + + network.set_scene(addr1) + bc1:set_noun("connectivity2", 321) + assert_true(called2, "Listener was not called!") + assert_equal(1, called, "Original listener was called?!") + end + network.allow_blackhole = false +end + +function test_cleanup() + local bc1 = BaseControl({cleanup1=1234}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + + assert_true(bc2:has_noun("cleanup1"), "Setup failed") + assert_equal(1234, bc2:get_noun("cleanup1"), "Setup failed") + + network.set_scene(addr1) + bc1:cleanup() + + network.set_scene(addr2) + assert_equal(nil, bc2:get_noun("cleanup1"), "Cleanup failed") + assert_false(bc2:has_noun("cleanup1"), "Cleanup failed") +end + +function test_connectivity_loss() + local bc1 = BaseControl({connectivity=4433}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + + assert_true(bc2:has_noun("connectivity"), "Setup failed") + assert_equal(4433, bc2:get_noun("connectivity"), "Setup failed") + + network.deregister(addr1) + + network.allow_blackhole = true + assert_true(bc2:has_noun("connectivity"), "Test error") + assert_equal(nil, bc2:get_noun("connectivity"), "Invalid response from offline node") + network.allow_blackhole = false + + -- Come back online + local bc1 = BaseControl({connectivity=4432}) + network.set_scene(addr2) + assert_true(bc2:has_noun("connectivity"), "Reconnect failed") + assert_equal(4432, bc2:get_noun("connectivity"), "Reconnect failed") +end + +function test_isolation() + local bc_outside = BaseControl({isolation1=4321}) + local bc1 = BaseControl({isolation2=1234}, nil, { + port=1234, + }) + local bc2 = BaseControl(nil, nil, { + port=1234, + }) + + assert_equal(nil, bc2:get_noun("isolation1"), "Leak!") + assert_equal(1234, bc2:get_noun("isolation2"), "Port failure") +end + +function test_malicious_messages() + local bc1 = BaseControl({malicious=1234}) + local addr1 = network.get_scene() + local bc2 = BaseControl({malicious2=4321}) + local addr2 = network.get_scene() + + network.send(addr1, bc1.port, serialization.serialize{ + ty=7, -- LISTEN_REQUEST + noun="unknown", event=Event.Change, id="dead-beef", + }) + network.send(addr1, bc1.port, serialization.serialize{ + ty=8, -- LISTEN_NOTIFY + id="dead-beef", value=1234567, + }) + network.send(addr1, bc1.port, serialization.serialize{ + ty=9, -- LISTEN_CANCEL + noun="unknown", id="dead-beef", + }) + + assert_equal(1234, bc2:get_noun("malicious")) +end + +function test_malicious_broadcast() + local bc1 = BaseControl({malicious3=1234}) + local addr1 = network.get_scene() + local bc2 = BaseControl() + local addr2 = network.get_scene() + local bc3 = BaseControl() + local addr3 = network.get_scene() + + network.broadcast(bc1.port, serialization.serialize{ + ty=2, -- ANNOUNCE + nouns={"malicious3"}, verbs={}, + }) + + -- bc1 can't be fooled, as it owns this noun + network.set_scene(addr1) + assert_equal(1234, bc1:get_noun("malicious3"), "Injection successful") + + -- bc2 unfortunately can be fooled and must be + network.set_scene(addr2) + assert_equal(nil, bc2:get_noun("malicious3"), "Injection failed") +end +-- }}} diff --git a/bc.lua b/bc.lua index 2c5f3f0565c2..65a0f095deab 100644 --- a/bc.lua +++ b/bc.lua @@ -1,323 +1,261 @@ +local component = require("component") local event = require("event") -local modem = require("component").modem local serializer = require("serialization") +local uuid = require("uuid") -local List = {last = -1} +local BC_VERSION = {0, 1} +local BC_PORT = 0xBC00 | (BC_VERSION[1] << 4) | BC_VERSION[2] -function List:new() - local o = {} - setmetatable(o, self) - self.__index = self - return o -end - -function List:insert(value) - local last = self.last + 1 - self.last = last - self[last] = value - return last -end - -function List:iter() - local i = -1 - local last = self.last - local list = self - return function() - while true do - i = i + 1 - if i <= last then - if list[i] ~= nil then - return i, list[i] - end - else - return nil - end - end - end -end - -function List:remove(id) - local value = self[id] - self[id] = nil - return value -end - -local message_type = { - request_noun = 1, - call_verb = 2, - noun_response = 3, - hello = 4, - goodbye = 5, - hello_response = 6, - request_listening = 7, - request_stop_listening = 8, - listener_update = 9, +local Message = { + Hello = 0x48454c4f, + Register = 0x00524547, + Disband = 0x44524547, + NounRequest = 0x4e524551, + NounResponse = 0x4e524553, + VerbRequest = 0x56524551, + ListenRequest = 0x4c524551, + ListenNotify = 0x4c4e4f54, + ListenCancel = 0x4c535450, } -local CFG_PORT = 1234 - -local bc = {} - -function bc:init(local_nouns, local_verbs) - local o = { - local_nouns = local_nouns or {}, - local_verbs = local_verbs or {}, +local Event = { + Change = 1, + Rising = 2, + Falling = 3, + Equals = 4, + Above = 5, + Below = 6, +} - remote_verbs = {}, - remote_nouns = {}, +local bc = { + _version = BC_VERSION, + _default_port = BC_PORT, + Event = Event, + Message = Message, +} +bc.__index = bc - local_listeners = {}, - remote_listeners = {}, - listening_remotes = {}, - } - setmetatable(o, self) - self.__index = self +-- Helpers +local function send_msg(self, remote, msg_table) + self.modem.send(remote, self.port, serializer.serialize(msg_table)) +end - -- Modem listener - function modem_listener(moden_msg, localAddress, remoteAddress, port, dist, message) - local message = serializer.unserialize(message) - if port == CFG_PORT then - if message.ty == message_type.request_noun then - modem.send(remoteAddress, port, serializer.serialize({ - ty=message_type.noun_response, - noun=message.noun, - value=o.local_nouns[message.noun], - })) - elseif message.ty == message_type.noun_response then - -- Ignore, this is handled via pull - elseif message.ty == message_type.call_verb then - o.local_verbs[message.verb](o, message.param) - elseif message.ty == message_type.hello then - for i,verb in ipairs(message.verbs) do - o.remote_verbs[verb] = remoteAddress - end +local function broadcast_msg(self, msg_table) + self.modem.broadcast(self.port, serializer.serialize(msg_table)) +end - for i,noun in ipairs(message.nouns) do - o.remote_nouns[noun] = remoteAddress - -- Check for potential dormant listeners - if o.remote_listeners[noun] ~= nil then - for id, listener in o.remote_listeners[noun]:iter() do - modem.send(remoteAddress, port, serializer.serialize({ - ty=message_type.request_listening, - noun=noun, - query=listener.query, - qparam=listener.qparam, - id=id, - })) - end - end - end - -- Respond with own verbs and nouns - local mynouns = {} - for noun in pairs(o.local_nouns) do - table.insert(mynouns, noun) - end - local myverbs = {} - for verb in pairs(o.local_verbs) do - table.insert(myverbs, verb) - end +function bc:has_noun(noun) + return self.local_nouns[noun] ~= nil or self.remote_nouns[noun] ~= nil +end - modem.send(remoteAddress, port, serializer.serialize({ - ty=message_type.hello_response, - verbs=myverbs, - nouns=mynouns, - })) - elseif message.ty == message_type.hello_response then - for i,verb in ipairs(message.verbs) do - o.remote_verbs[verb] = remoteAddress - end - for i,noun in ipairs(message.nouns) do - o.remote_nouns[noun] = remoteAddress - end - elseif message.ty == message_type.request_listening then - o.listening_remotes[message.noun] = o.listening_remotes[message.noun] or {} - o.listening_remotes[message.noun][remoteAddress] = o.listening_remotes[message.noun][remoteAddress] or {} - o.listening_remotes[message.noun][remoteAddress][message.id] = {query=message.query, qparam=message.qparam} - elseif message.ty == message_type.request_stop_listening then - o.listening_remotes[message.noun][remoteAddress][message.id] = nil - elseif message.ty == message_type.listener_update then - o.remote_listeners[message.noun][message.id].callback(o, message.value) +function bc:set_noun(noun, value) + local last_value = self.local_nouns[noun] + if last_value ~= nil then + self.local_nouns[noun] = value + for id, par in pairs(self.remote_listeners[noun]) do + if (par.event == Event.Change and value ~= last_value) + or (par.event == Event.Rising and value > last_value) + or (par.event == Event.Falling and value < last_value) + or (par.event == Event.Equals and value == par.evparam) + or (par.event == Event.Above and value > par.evparam) + or (par.event == Event.Below and value < par.evparam) + then + send_msg(self, par.addr, {ty=BC_MESSAGE.LISTEN_NOTIFY, id=id, value=value}) end end + else + error("Noun \""..noun.."\" does not exist or is non-local!") end +end - -- Setup connections - modem.open(CFG_PORT) - event.listen("modem_message", modem_listener) - - -- Save listener function for deinit - o.modem_cb = modem_listener +function bc:get_noun(noun) + if self.local_nouns[noun] ~= nil then + return self.local_nouns[noun] + elseif self.remote_nouns[noun] ~= nil then + send_msg(self, self.remote_nouns[noun], { + ty = BC_MESSAGE.NOUN_REQUEST, + noun = noun, + }) + local value + event.pullFiltered(self.timeout, function(ev, ...) + if ev ~= "modem_message" then return false end + if select(2, ...) ~= self.remote_nouns[noun] then return false end + if select(3, ...) ~= self.port then return false end + local msg = serializer.unserialize(select(5, ...)) + if msg.ty ~= BC_MESSAGE.NOUN_RESPONSE then return false end + if msg.noun ~= noun then return false end - -- Send own nouns and verbs and request all remotes to send theirs - local mynouns = {} - for noun in pairs(o.local_nouns) do - table.insert(mynouns, noun) - end - local myverbs = {} - for verb in pairs(o.local_verbs) do - table.insert(myverbs, verb) + value = msg.value + return true + end) + return value + else -- Not found at all + return nil end - modem.broadcast(CFG_PORT, serializer.serialize({ - ty=message_type.hello, - verbs=myverbs, - nouns=mynouns, - })) - - return o end -function bc:get_noun(n) - if self.local_nouns[n] ~= nil then -- This noun is local, makes it easy - return self.local_nouns[n] - elseif self.remote_nouns[n] ~= nil then -- This noun is remote - modem.send(self.remote_nouns[n], CFG_PORT, serializer.serialize({ - ty=message_type.request_noun, - noun=n, - })) - local i = 0 - while i < 5 do - local _, _, remoteAddr, port, _, msg = event.pull("modem_message") - if port == CFG_PORT and remoteAddr == self.remote_nouns[n] then - local message = serializer.unserialize(msg) - if message.ty == message_type.noun_response and message.noun == n then - return message.value - end - end - i = i + 1 - end - return nil +function bc:listen(noun, event, evparam, callback) + -- You can leave out evparam + if type(evparam) == "function" then + callback = evparam + evparam = nil + end + + local remote_addr = self.remote_nouns[noun] + if remote_addr == nil then + error("Noun \""..noun.."\" is not listenable!") else - -- Noun not found - return nil + local id = uuid.next() + self.local_listeners[id] = {addr=remote_addr, callback=callback} + send_msg(self, remote_addr, { + ty=BC_MESSAGE.LISTEN_REQUEST, + noun=noun, + event=event, + evparam=evparam, + id=id, + }) + return id end end -function bc:has_noun(n) - return self.local_nouns[n] ~= nil or self.remote_nouns[n] ~= nil +function bc:cancel(id) + local l = self.local_listeners[id] + send_msg(self, l.addr, {ty=BC_MESSAGE.LISTEN_CANCEL, noun=l.noun, id=id}) + self.local_listeners[id] = nil end -function bc:set_noun(n, value) - if self.local_nouns[n] ~= nil then - -- Call local listeners - if self.local_listeners[n] ~= nil then - for id, listener in self.local_listeners[n]:iter() do - if (listener.query == "onchange" and self.local_nouns[n] ~= value) - or (listener.query == "onrising" and self.local_nouns[n] < value) - or (listener.query == "onfalling" and self.local_nouns[n] > value) - or (listener.query == "onvalue" and value == listener.qparam) - or (listener.query == "onabove" and value > listener.qparam) - or (listener.query == "onbelow" and value < listener.qparam) - then - self.local_nouns[n] = value -- Apply here, too, because else there could be glitches - listener.callback(self, value) - end - end - end - -- Call remote listeners - if self.listening_remotes[n] ~= nil then - for address,remote in pairs(self.listening_remotes[n]) do - for id, listener in pairs(remote) do - if (listener.query == "onchange" and self.local_nouns[n] ~= value) - or (listener.query == "onrising" and self.local_nouns[n] < value) - or (listener.query == "onfalling" and self.local_nouns[n] > value) - or (listener.query == "onvalue" and value == listener.qparam) - or (listener.query == "onabove" and value > listener.qparam) - or (listener.query == "onbelow" and value < listener.qparam) - then - self.local_nouns[n] = value -- Apply here, too, because else there could be glitches - -- Send update - modem.send(address, CFG_PORT, serializer.serialize({ty=message_type.listener_update, noun=n, value=value, id=id})) - end - end - end +function bc:has_verb(verb) + return self.local_verbs[verb] ~= nil or self.remote_verbs[verb] ~= nil +end - end - -- Apply change - self.local_nouns[n] = value +function bc:call_verb(verb, ...) + if self.local_verbs[verb] ~= nil then + self.local_verbs[verb](self, nil, ...) + elseif self.remote_verbs[verb] ~= nil then + send_msg(self, self.remote_verbs[verb], { + ty=BC_MESSAGE.VERB_REQUEST, verb=verb, param={...}, + }) else - error("Can't set remote nouns") + error("Verb \""..verb.."\" does not exist!") end end -function bc:call_verb(v, param) - if self.local_verbs[v] ~= nil then - self.local_verbs[v](self, param) - return true - elseif self.remote_verbs[v] ~= nil then - modem.send(self.remote_verbs[v], CFG_PORT, serializer.serialize({ - ty=message_type.call_verb, - verb=v, - param=param, - })) - return true - else - return false - end +function bc:cleanup() + event.ignore("modem_message", self._modem_listener) + local nouns, verbs = self:locals() + broadcast_msg(self, { + ty=BC_MESSAGE.DISBAND, + nouns=nouns, + verbs=verbs, + }) end -function bc:has_verb(v) - return self.local_verbs[v] ~= nil or self.remote_verbs[v] ~= nil +function bc:locals() + local nouns, verbs = {}, {} + for noun in pairs(self.local_nouns) do table.insert(nouns, noun) end + for verb in pairs(self.local_verbs) do table.insert(verbs, verb) end + return nouns, verbs end -function bc:listen_noun(n, query, qparam, callback) - if self.local_nouns[n] ~= nil then -- Local listening - self.local_listeners[n] = self.local_listeners[n] or List:new() - local id = self.local_listeners[n]:insert({ - query=query, - qparam=qparam, - callback=callback, - }) - return id - elseif self.remote_nouns[n] ~= nil then -- Remote listening - self.remote_listeners[n] = self.remote_listeners[n] or List:new() - local id = self.remote_listeners[n]:insert({ - query=query, - qparam=qparam, - callback=callback, - }) - -- Request remote listening - modem.send(self.remote_nouns[n], CFG_PORT, serializer.serialize({ - ty=message_type.request_listening, - noun=n, - query=query, - qparam=qparam, - id=id, - })) - return id - else - -- Install it, with hope, that the node will come online later - self.remote_listeners[n] = self.remote_listeners[n] or List:new() - local id = self.remote_listeners[n]:insert({ - query=query, - qparam=qparam, - callback=callback, - }) - return id +local function new(_, local_nouns, local_verbs, overrides) + local self = { + local_nouns=local_nouns or {}, + local_verbs=local_verbs or {}, + remote_nouns={}, + remote_verbs={}, + + local_listeners={}, + remote_listeners={}, + + port=BC_PORT, + modem=component.modem, + timeout=5, + } + if overrides ~= nil then + for name, value in pairs(overrides) do + self[name] = value + end + end + setmetatable(self, bc) + + for noun in pairs(self.local_nouns) do + self.remote_listeners[noun] = {} end -end -function bc:listen_cancel(n, id) - if self.local_nouns[n] ~= nil then -- Local listening - if self.local_listeners[n] ~= nil then - self.local_listeners[n]:remove(id) + self._modem_listener = function(_, _, remote_addr, port, _, msg) + if port ~= self.port then -- Ignore other ports + return end - elseif self.remote_nouns[n] ~= nil then -- Remote listening - if self.remote_listeners[n] ~= nil then - self.remote_listeners[n]:remove(id) - modem.send(self.remote_nouns[n], CFG_PORT, serializer.serialize({ - ty=message_type.request_stop_listening, - noun=n, - id=id, - })) + + local msg = serializer.unserialize(msg) + if msg.ty == BC_MESSAGE.HELLO then + local nouns, verbs = self:locals() + send_msg(self, remote_addr, { + ty=BC_MESSAGE.REGISTER, + nouns=nouns, + verbs=verbs, + }) + elseif msg.ty == BC_MESSAGE.REGISTER then + for _, noun in ipairs(msg.nouns or {}) do + self.remote_nouns[noun] = remote_addr + end + for _, verb in ipairs(msg.verbs or {}) do + self.remote_verbs[verb] = remote_addr + end + elseif msg.ty == BC_MESSAGE.NOUN_REQUEST then + send_msg(self, remote_addr, { + ty=BC_MESSAGE.NOUN_RESPONSE, + noun=msg.noun, + value=self.local_nouns[msg.noun], + }) + elseif msg.ty == BC_MESSAGE.VERB_REQUEST then + local callback = self.local_verbs[msg.verb] + if callback ~= nil then + callback(self, remote_addr, table.unpack(msg.param)) + end + elseif msg.ty == BC_MESSAGE.LISTEN_REQUEST then + if self.local_nouns[msg.noun] ~= nil then + self.remote_listeners[msg.noun][msg.id] = { + event=msg.event, + evparam=msg.evparam, + addr=remote_addr, + } + end + elseif msg.ty == BC_MESSAGE.LISTEN_NOTIFY then + local listener = self.local_listeners[msg.id] + if listener ~= nil and listener.addr == remote_addr then + listener.callback(msg.value) + end + elseif msg.ty == BC_MESSAGE.LISTEN_CANCEL then + if self.remote_listeners[msg.noun] ~= nil then + self.remote_listeners[msg.noun][msg.id] = nil + end + elseif msg.ty == BC_MESSAGE.DISBAND then + for _, noun in ipairs(msg.nouns or {}) do + if self.remote_nouns[noun] == remote_addr then + self.remote_nouns[noun] = nil + end + end + for _, verb in ipairs(msg.verbs or {}) do + if self.remote_verbs[verb] == remote_addr then + self.remote_verbs[verb] = nil + end + end end - else - error("Trying to cancel non existing listener") end -end --- Deinit -function bc:cleanup() - event.ignore("modem_message", self.modem_cb) + -- Setup connection and say hello + self.modem.open(self.port) + event.listen("modem_message", self._modem_listener) + broadcast_msg(self, {ty=BC_MESSAGE.HELLO}) + local nouns, verbs = self:locals() + broadcast_msg(self, { + ty=BC_MESSAGE.REGISTER, + nouns=nouns, + verbs=verbs, + }) + + return self end -return bc +return setmetatable(bc, {__call=new, __index={new=new}}) diff --git a/event.lua b/event.lua index 3299ac3f36ae..4d7aca95b32d 100644 --- a/event.lua +++ b/event.lua @@ -6,39 +6,54 @@ addr_num = 0 last_msg = nil function event.listen(event, callback) - if event ~= "modem_message" then - error("Event '"..event"' is not supported!") - end - addr = "A"..addr_num - addr_num = addr_num + 1 - function ev_callback(ev, addr1, addr2, port, dist, msg) - last_msg = { - ev=ev, - addr1=addr1, - addr2=addr2, - port=port, - dist=dist, - msg=msg, - } - callback(ev, addr1, addr2, port, dist, msg) - end - network.register(addr, ev_callback) + if event ~= "modem_message" then + error("Event '"..event"' is not supported!") + end + addr = "A"..addr_num + addr_num = addr_num + 1 + function ev_callback(ev, addr1, addr2, port, dist, msg) + last_msg = { + ev=ev, + addr1=addr1, + addr2=addr2, + port=port, + dist=dist, + msg=msg, + } + callback(ev, addr1, addr2, port, dist, msg) + end + network.register(addr, ev_callback) end function event.ignore(event, callback) - if event ~= "modem_message" then - error("Event '"..event"' is not supported!") - end - error("Not implemented yet") + if event ~= "modem_message" then + error("Event '"..event"' is not supported!") + end + network.deregister(network.get_scene()) end -function event.pull(event, callback) - -- Just return the last message and hope it is the - -- right one ... - if last_msg == nil then - error("No previous message found") - end - return last_msg.ev, last_msg.addr1, last_msg.addr2, last_msg.port, last_msg.dist, last_msg.msg +function event.pull(event) + -- Just return the last message and hope it is the + -- right one ... + if last_msg == nil then + return nil + end + local lmsg = last_msg + last_msg = nil + return lmsg.ev, lmsg.addr1, lmsg.addr2, lmsg.port, lmsg.dist, lmsg.msg +end + +function event.pullFiltered(timeout, filter) + if last_msg == nil then + return nil + end + local lmsg = last_msg + last_msg = nil + if filter(lmsg.ev, lmsg.addr1, lmsg.addr2, lmsg.port, lmsg.dist, lmsg.msg) then + return lmsg.ev, lmsg.addr1, lmsg.addr2, lmsg.port, lmsg.dist, lmsg.msg + else + return nil + end end return event diff --git a/network.lua b/network.lua index 15ecf396de20..f8868c8e1bcd 100644 --- a/network.lua +++ b/network.lua @@ -1,4 +1,5 @@ network = {} +network.allow_blackhole = false nodes = {} @@ -14,17 +15,17 @@ function network.register(addr, callback) table.insert(active_node, addr) end -function network.deregister(addr, callback) - if nodes[addr] ~= callback then - error("Callback was not registered for "..addr) - end +function network.deregister(addr) nodes[addr] = nil end function network.send(addr, port, msg) local callback = nodes[addr] if callback == nil then - error("Sent message to offline node ("..addr..")!") + if not network.allow_blackhole then + error("Send message to offline node: "..addr) + end + return nil end local current_node = active_node[#active_node] diff --git a/test_bc.lua b/test_bc.lua deleted file mode 100644 index 3fb35c8f73d7..000000000000 --- a/test_bc.lua +++ /dev/null @@ -1,276 +0,0 @@ -require "lunit" - -network = require("network") -bc = require("bc") -ser = require("serialization") - -module("test_bc", package.seeall, lunit.testcase) - -function test_init() - local bci = bc:init({["light"]=false}, {["toggle_light"]=function() end}) - assert_true(bci:has_noun("light"), "Nouns were not initialized") - assert_true(bci:has_verb("toggle_light"), "Verbs were not initialized") -end - -function test_set_get_noun() - local bci = bc:init({["light"]=true}, {["toggle_light"]=function() end}) - assert_equal(true, bci:get_noun("light"), "First get failed with wrong value") - bci:set_noun("light", false) - assert_equal(false, bci:get_noun("light"), "Second get failed with wrong value") -end - -function test_request_noun() - local bci = bc:init({["light"]=true}, {["toggle_light"]=function() end}) - local addr = network.get_scene() - local i = false - local expct = true - - network.register("tester0", function(m, laddr, raddr, p, d, msg) - m = ser.unserialize(msg) - if m.ty == 3 and m.noun == "light" and m.value == expct then - i = true - end - end) - - network.send(addr, 1234, ser.serialize({ty=1, noun="light"})) - assert_true(i, "Noun response did not happen or was incorrect") - -- Reset and change noun - i = false - bci:set_noun("light", false) - expct = false - network.send(addr, 1234, ser.serialize({ty=1, noun="light"})) - assert_true(i, "Noun response did not happen or was incorrect") -end - -function test_call_verb() - local bci = bc:init({["light"]=true}, { - ["toggle_light"]=function(b) - b:set_noun("light", not b:get_noun("light")) - end, - ["set_light"]=function(b, state) - b:set_noun("light", state) - end, - }) - local addr = network.get_scene() - -- Call verb - network.send(addr, 1234, ser.serialize({ty=2, verb="toggle_light"})) - assert_equal(false, bci:get_noun("light"), "Verb did not do its job") - - network.send(addr, 1234, ser.serialize({ty=2, verb="set_light", param=true})) - assert_equal(true, bci:get_noun("light"), "Verb did not do its job") - - network.send(addr, 1234, ser.serialize({ty=2, verb="set_light", param=false})) - assert_equal(false, bci:get_noun("light"), "Verb did not do its job") -end - -function test_multinode_call_verb() - local bc1 = bc:init({["light0"]=true}, { - ["toggle_light0"]=function(b) - b:set_noun("light0", not b:get_noun("light0")) - end, - ["set_light0"]=function(b, state) - b:set_noun("light0", state) - end, - }) - local a1 = network.get_scene() - local bc2 = bc:init({}, {}) - local a2 = network.get_scene() - - network.set_scene(a1) - assert_true(bc1:call_verb("toggle_light0")) - assert_equal(false, bc1:get_noun("light0"), "First verb invocation did not go to plan") - - network.set_scene(a2) - assert_true(bc2:call_verb("toggle_light0")) - - network.set_scene(a1) - assert_equal(true, bc1:get_noun("light0"), "Second verb invocation did not go to plan") - - -- Check calling with parameter - for _, b in pairs({true, false}) do - network.set_scene(a2) - assert_true(bc2:call_verb("set_light0", b)) - - network.set_scene(a1) - assert_equal(b, bc1:get_noun("light0"), "Parameter verb invocation did not go to plan") - end -end - -function test_multinode_get_noun() - local key = "foobar" - local bc1 = bc:init({["light1"]=key}, {["toggle_light1"]=function(b) - b:set_noun("light1", not b:get_noun("light1")) - end}) - local a1 = network.get_scene() - local bc2 = bc:init({}, {}) - local a2 = network.get_scene() - - network.set_scene(a1) - assert_equal(key, bc1:get_noun("light1"), "Local get failed") - - network.set_scene(a2) - assert_equal(key, bc2:get_noun("light1"), "Remote get failed") -end - -function test_multinode_listening() - local bc1 = bc:init({["foo"]=123}, {}) - local a1 = network.get_scene() - local bc2 = bc:init({}, {}) - local a2 = network.get_scene() - - network.set_scene(a1) - local i = false - -- Local listening - local id = bc1:listen_noun("foo", "onchange", nil, function(bc, foo) - i = true - end) - bc1:set_noun("foo", 111) - assert_true(i, "Local listening failed") - i = false - bc1:listen_cancel("foo", id) -- Test wether cancelling works - - network.set_scene(a2) - local j = false - local rid = bc2:listen_noun("foo", "onchange", nil, function(bc, foo) - j = true - end) - - network.set_scene(a1) - bc1:set_noun("foo", 1234) - assert_true(j, "Remote listening failed") - assert_false(i, "Cancelling local listener failed") - - network.set_scene(a2) - bc2:listen_cancel("foo", rid) - j = false - - network.set_scene(a1) - bc1:set_noun("foo", 34) - assert_false(j, "Cancelling remote listener failed") -end - -function test_listen_modes() - local bci = bc:init({["noun"]=10}, {}) - local i = false - local id = 0 - -- onchange - local id = bci:listen_noun("noun", "onchange", nil, function(bc, noun) - i = true - end) - bci:set_noun("noun", 11) - assert_true(i, "Listening \"onchange\" failed") - bci:listen_cancel("noun", id) - i = false - -- onrising - id = bci:listen_noun("noun", "onrising", nil, function(bc, noun) - i = true - end) - bci:set_noun("noun", 10) - assert_false(i, "Listening \"onrising\" failed(A)") - bci:set_noun("noun", 11) - assert_true(i, "Listening \"onrising\" failed(B)") - bci:listen_cancel("noun", id) - i = false - -- onfalling - id = bci:listen_noun("noun", "onfalling", nil, function(bc, noun) - i = true - end) - bci:set_noun("noun", 12) - assert_false(i, "Listening \"onfalling\" failed(A)") - bci:set_noun("noun", 10) - assert_true(i, "Listening \"onfalling\" failed(B)") - bci:listen_cancel("noun", id) - i = false - -- onvalue - id = bci:listen_noun("noun", "onvalue", 99, function(bc, noun) - i = true - end) - bci:set_noun("noun", 100) - assert_false(i, "Listening \"onvalue\" failed(A)") - bci:set_noun("noun", 99) - assert_true(i, "Listening \"onvalue\" failed(B)") - bci:listen_cancel("noun", id) - i = false - -- onabove - id = bci:listen_noun("noun", "onabove", 100, function(bc, noun) - i = true - end) - bci:set_noun("noun", 10) - assert_false(i, "Listening \"onabove\" failed(A)") - bci:set_noun("noun", 110) - assert_true(i, "Listening \"onabove\" failed(B)") - bci:listen_cancel("noun", id) - i = false - -- onbelow - id = bci:listen_noun("noun", "onbelow", 10, function(bc, noun) - i = true - end) - bci:set_noun("noun", 11) - assert_false(i, "Listening \"onbelow\" failed(A)") - bci:set_noun("noun", 1) - assert_true(i, "Listening \"onbelow\" failed(B)") - bci:listen_cancel("noun", id) -end - -function test_get_unknown_noun() - local bci = bc:init() - - assert_false(bci:has_noun("bar")) - assert_equal(nil, bci:get_noun("bar")) -end - --- function test_get_offline_node() --- local bc1 = bc:init() --- local bc2 = bc:init({["foo"]=true}) --- --- -- Simulate node going offline --- bc2.modem:remove_self() --- bc2 = nil --- --- local val = bc1:get_noun("foo") --- assert_equal(nil, val) --- end - -function test_call_unknown_verb() - local bci = bc:init() - - assert_false(bci:has_verb("unknown")) - assert_equal(false, bci:call_verb("unknown")) -end - -function test_install_listen_on_unknown_noun() - local bci = bc:init() - - assert_false(bci:has_noun("unknown")) - bci:listen_noun("unknown", "onchange", nil, function() end) -end - -function test_late_install() - local bc1 = bc:init() - - local i = false - - assert_false(bc1:has_noun("foo123")) - bc1:listen_noun("foo123", "onchange", nil, function() - i = true - end) - - local bc2 = bc:init({["foo123"] = 123}) - bc2:set_noun("foo123", 321) - - assert_true(i, "Listener was not called") -end - -function test_verb_multicall_bug() - local var = 0 - local bc1 = bc:init(nil, { - ["verb"] = function(bc, foo) - var = var + foo - end - }) - local bc2 = bc:init() - bc2:call_verb("verb", 1) - assert_equal(1, var, "Verb was not called correctly") - bc2:call_verb("verb", 2) - assert_equal(3, var, "Verb was not called correctly(Second attempt)") -end diff --git a/uuid.lua b/uuid.lua new file mode 100644 index 000000000000..0007e60e43a4 --- /dev/null +++ b/uuid.lua @@ -0,0 +1,30 @@ +local bit32 = require("bit32") +local uuid = {} + +function uuid.next() + -- e.g. 3c44c8a9-0613-46a2-ad33-97b6ba2e9d9a + -- 8-4-4-4-12 (halved sizes because bytes make hex pairs) + local sets = {4, 2, 2, 2, 6} + local result = "" + local pos = 0 + + for _,set in ipairs(sets) do + if result:len() > 0 then + result = result .. "-" + end + for _ = 1,set do + local byte = math.random(0, 255) + if pos == 6 then + byte = bit32.bor(bit32.band(byte, 0x0F), 0x40) + elseif pos == 8 then + byte = bit32.bor(bit32.band(byte, 0x3F), 0x80) + end + result = result .. string.format("%02x", byte) + pos = pos + 1 + end + end + + return result +end + +return uuid