cc-libs/apis/net.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