213 lines
7.1 KiB
Lua
213 lines
7.1 KiB
Lua
-- TrapOS networking: service-name bus on a single channel.
|
|
--
|
|
-- Servers register handlers on the boot eventloop:
|
|
-- net.serve('ping', function(msg, reply) reply('pong') end)
|
|
-- net.listen('events.foo', function(msg, packet) ... end)
|
|
--
|
|
-- Clients (CLI programs) call or send without an eventloop:
|
|
-- local ok, res = net.call('ping', 'ping', { destId = 5, timeout = 0.5 })
|
|
-- net.send('events.foo', payload, { destId = 'alice' })
|
|
--
|
|
-- A router service (servers/router-server.lua) must run on exactly one machine.
|
|
-- It resolves label-addressed packets, stamps routerId, and rebroadcasts.
|
|
local createEventLoop = require('/apis/eventloop');
|
|
|
|
local BUS_CHANNEL = 10;
|
|
local DEFAULT_TIMEOUT = 0.5;
|
|
|
|
local nextRequestSeq = 1;
|
|
local function newRequestId()
|
|
local id = tostring(os.getComputerID()) .. ':' .. tostring(nextRequestSeq) .. ':' .. tostring(os.clock());
|
|
nextRequestSeq = nextRequestSeq + 1;
|
|
return id;
|
|
end
|
|
|
|
local function isPacketOk(packet)
|
|
if type(packet) ~= 'table' then return false end
|
|
if not packet.routerId or not packet.sourceId then return false end
|
|
if packet.destId == nil then return true end
|
|
if type(packet.destId) == 'number' and packet.destId == os.getComputerID() then return true end
|
|
if type(packet.destId) == 'string' and packet.destId == os.getComputerLabel() then return true end
|
|
return false
|
|
end
|
|
|
|
local function createNetwork(el, modem, modemSide)
|
|
el = el or createEventLoop();
|
|
modem = modem or peripheral.find('modem') or error('modem not found');
|
|
modem.open(BUS_CHANNEL);
|
|
|
|
local isRouter = false;
|
|
modemSide = modemSide or peripheral.getName(modem);
|
|
|
|
local function buildPacket(service, kind, payload, destId, requestId)
|
|
return {
|
|
sourceId = os.getComputerID(),
|
|
sourceLabel = os.getComputerLabel(),
|
|
destId = tonumber(destId) or destId,
|
|
service = service,
|
|
kind = kind,
|
|
requestId = requestId,
|
|
payload = payload,
|
|
routerId = isRouter and os.getComputerID() or nil,
|
|
};
|
|
end
|
|
|
|
local function transmit(packet)
|
|
local selfId = os.getComputerID();
|
|
local selfLabel = os.getComputerLabel();
|
|
|
|
local destIsSelfId = packet.destId == selfId;
|
|
local destIsSelfLabel = selfLabel ~= nil and packet.destId == selfLabel;
|
|
local destIsSelf = destIsSelfId or destIsSelfLabel;
|
|
local matchesSelf = packet.destId == nil or destIsSelf;
|
|
|
|
if matchesSelf then
|
|
local localPacket = {};
|
|
for k, v in pairs(packet) do localPacket[k] = v end
|
|
localPacket.routerId = localPacket.routerId or selfId;
|
|
os.queueEvent('modem_message', modemSide, BUS_CHANNEL, BUS_CHANNEL, localPacket, 0);
|
|
end
|
|
|
|
if not destIsSelf then
|
|
modem.transmit(BUS_CHANNEL, BUS_CHANNEL, packet);
|
|
end
|
|
end
|
|
|
|
local function serve(serviceName, handler)
|
|
return el.register('modem_message', function(_, _, replyChannel, packet)
|
|
if replyChannel ~= BUS_CHANNEL then return end
|
|
if not isPacketOk(packet) then return end
|
|
if packet.service ~= serviceName or packet.kind ~= 'req' then return end
|
|
|
|
local function reply(responsePayload)
|
|
local response = buildPacket(serviceName, 'res', responsePayload, packet.sourceId, packet.requestId);
|
|
transmit(response);
|
|
end
|
|
|
|
handler(packet.payload, reply, packet);
|
|
end);
|
|
end
|
|
|
|
local function listen(serviceName, handler)
|
|
return el.register('modem_message', function(_, _, replyChannel, packet)
|
|
if replyChannel ~= BUS_CHANNEL then return end
|
|
if not isPacketOk(packet) then return end
|
|
if packet.service ~= serviceName or packet.kind ~= 'evt' then return end
|
|
|
|
handler(packet.payload, packet);
|
|
end);
|
|
end
|
|
|
|
local function send(serviceName, payload, opts)
|
|
opts = opts or {};
|
|
transmit(buildPacket(serviceName, 'evt', payload, opts.destId, nil));
|
|
end
|
|
|
|
local function awaitResponse(serviceName, requestId, timeout, collectMultiple)
|
|
local timerId = os.startTimer(timeout);
|
|
local results = {};
|
|
local packets = {};
|
|
|
|
while true do
|
|
local event, p1, _, p3, p4 = os.pullEvent();
|
|
if event == 'timer' and p1 == timerId then
|
|
if collectMultiple then
|
|
if #results == 0 then return false, 'net.call timeout', {} end
|
|
return true, results, packets;
|
|
end
|
|
return false, 'net.call timeout', nil;
|
|
elseif event == 'modem_message' then
|
|
local replyChannel = p3;
|
|
local recvPacket = p4;
|
|
if replyChannel == BUS_CHANNEL
|
|
and isPacketOk(recvPacket)
|
|
and recvPacket.service == serviceName
|
|
and recvPacket.kind == 'res'
|
|
and recvPacket.requestId == requestId then
|
|
if collectMultiple then
|
|
table.insert(results, recvPacket.payload);
|
|
table.insert(packets, recvPacket);
|
|
else
|
|
os.cancelTimer(timerId);
|
|
return true, recvPacket.payload, recvPacket;
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function call(serviceName, payload, opts)
|
|
opts = opts or {};
|
|
local timeout = opts.timeout or DEFAULT_TIMEOUT;
|
|
local requestId = newRequestId();
|
|
transmit(buildPacket(serviceName, 'req', payload, opts.destId, requestId));
|
|
return awaitResponse(serviceName, requestId, timeout, false);
|
|
end
|
|
|
|
local function callMultiple(serviceName, payload, opts)
|
|
opts = opts or {};
|
|
local timeout = opts.timeout or DEFAULT_TIMEOUT;
|
|
local requestId = newRequestId();
|
|
transmit(buildPacket(serviceName, 'req', payload, opts.destId, requestId));
|
|
return awaitResponse(serviceName, requestId, timeout, true);
|
|
end
|
|
|
|
local function setRouter(enabled)
|
|
isRouter = enabled and true or false;
|
|
end
|
|
|
|
local function onUnrouted(handler)
|
|
return el.register('modem_message', function(_, _, replyChannel, packet)
|
|
if replyChannel ~= BUS_CHANNEL then return end
|
|
if type(packet) ~= 'table' then return end
|
|
if packet.routerId then return end
|
|
if not packet.sourceId then return end
|
|
handler(packet);
|
|
end);
|
|
end
|
|
|
|
local function rebroadcast(packet)
|
|
packet.routerId = packet.routerId or os.getComputerID();
|
|
local selfId = os.getComputerID();
|
|
local selfLabel = os.getComputerLabel();
|
|
local destIsSelfId = packet.destId == selfId;
|
|
local destIsSelfLabel = selfLabel ~= nil and packet.destId == selfLabel;
|
|
local destIsSelf = destIsSelfId or destIsSelfLabel;
|
|
local matchesSelf = packet.destId == nil or destIsSelf;
|
|
|
|
if matchesSelf then
|
|
os.queueEvent('modem_message', modemSide, BUS_CHANNEL, BUS_CHANNEL, packet, 0);
|
|
end
|
|
if not destIsSelf then
|
|
modem.transmit(BUS_CHANNEL, BUS_CHANNEL, packet);
|
|
end
|
|
end
|
|
|
|
return {
|
|
BUS_CHANNEL = BUS_CHANNEL,
|
|
DEFAULT_TIMEOUT = DEFAULT_TIMEOUT,
|
|
eventloop = el,
|
|
isPacketOk = isPacketOk,
|
|
serve = serve,
|
|
listen = listen,
|
|
send = send,
|
|
call = call,
|
|
callMultiple = callMultiple,
|
|
setRouter = setRouter,
|
|
onUnrouted = onUnrouted,
|
|
rebroadcast = rebroadcast,
|
|
};
|
|
end
|
|
|
|
local singleton = nil;
|
|
|
|
return function(el, modem, modemSide)
|
|
if el == nil and modem == nil and _G.bootEventLoop then
|
|
if not singleton then
|
|
singleton = createNetwork(_G.bootEventLoop, nil, nil);
|
|
end
|
|
return singleton;
|
|
end
|
|
return createNetwork(el, modem, modemSide);
|
|
end
|