diff --git a/oc/app/chat-cli.lua b/oc/app/chat-cli.lua new file mode 100644 index 0000000..28d228e --- /dev/null +++ b/oc/app/chat-cli.lua @@ -0,0 +1,43 @@ +-- Chat client +local serv = ({...})[1] +local event = require("event") +local term = require("term") +local gpu = term.gpu() +local occure = require("occure") +-- show a message onscreen +local sw, sh = gpu.getResolution() +-- The idea is to use the term API and the normal stuff at the same time. +-- Ehehe. +local function postMessage(s) + gpu.copy(1, 2, sw, sh - 3, 0, -1) + gpu.fill(1, sh - 2, sw, 2, " ") + gpu.set(1, sh - 2, s) +end +local function sysCallback(tp, nfrom, nto, port, data, un) + if tp == "copper_packet" then + if nfrom == serv then + if nto == occure.getHostname() then + if port == 3 then + postMessage(data) + end + end + end + end +end +event.listen("copper_packet", sysCallback) +local cancelMeLater = event.timer(1, function() + occure.output(serv, 2, "") +end, math.huge) +pcall(function() + while true do + term.setCursor(1, sh) + local text = term.read({nowrap = true}) + -- Because Term Sucks (tm) + gpu.fill(1, sh - 1, sw, 1, " ") + gpu.copy(1, 1, sw, sh - 1, 0, 1) + gpu.fill(1, 1, sw, 1, " ") + occure.output(serv, 2, text:sub(1, text:len() - 1)) + end +end) +event.ignore("copper_packet", sysCallback) +event.cancel(cancelMeLater) diff --git a/oc/app/chat-srv.lua b/oc/app/chat-srv.lua new file mode 100644 index 0000000..6c27cc0 --- /dev/null +++ b/oc/app/chat-srv.lua @@ -0,0 +1,47 @@ +-- Chat server. +local occure = require("occure") +local event = require("event") + +local subscriberPool = {} +local maxSubscribers = 100 +local function removeSubscriber(a) + for i = 1, #subscriberPool do + if subscriberPool[i] == a then + table.remove(subscriberPool, i) + return true + end + end + return false +end +local function addSubscriber(a) + if removeSubscriber(a) then + table.insert(subscriberPool, a) + return + end + table.insert(subscriberPool, a) + if #subscriberPool > maxSubscribers then + table.remove(subscriberPool, 1) + end +end +local repeatMessage = nil +function repeatMessage(msg) + print(msg) + for _, v in ipairs(subscriberPool) do + occure.output(v, 3, msg) + end +end +while true do + -- Null packets: "subscribe me" + local tp, nfrom, nto, nport, data = event.pull("copper_packet") + if tp == "copper_packet" then + if nto == occure.getHostname() then + if nport == 2 then + if data == "" then + addSubscriber(nfrom) + else + repeatMessage("<" .. nfrom .. "> " .. data) + end + end + end + end +end diff --git a/oc/app/ping.lua b/oc/app/ping.lua new file mode 100644 index 0000000..82c676f --- /dev/null +++ b/oc/app/ping.lua @@ -0,0 +1,20 @@ +local occure = require("occure") +local computer = require("computer") +local event = require("event") + +local args = {...} +local startTime = computer.uptime() +local completed = 0 +for _, v in ipairs(args) do + occure.output(v, 0, "", false, function () + print("Ping response from " .. v .. " @ " .. (computer.uptime() - startTime)) + completed = completed + 1 + end, function () + print("Gave up trying to ping " .. v) + completed = completed + 1 + end) +end + +while completed < #args do + event.pull(5) +end diff --git a/oc/app/protocol.chat b/oc/app/protocol.chat new file mode 100644 index 0000000..c15557b --- /dev/null +++ b/oc/app/protocol.chat @@ -0,0 +1,24 @@ +The chat.lua Protocol + - 20kdc, 2017 + +The chat.lua protocol is a simple chat protocol built on Copper with the reliability layer in use. +All packets/messages referred to here are Reliability Layer reliable packets. +Note that this protocol, assuming no more than one connection between two peers has to be maintained at a time, + should be extremely versatile. +However, IRC on TCP Emulation Over Copper should be the go-to standard when that becomes a thing. + + +The server will receive messages on port 2 and send them back out on port 3. + +All messages received by the client from the server on port 3 should be displayed as-is. +The client must ignore messages received on port 2, due to the possibility of a client and server on the same machine. + +The client should, every so often, send a blank string, to confirm the client is alive and wants to hear from the server. +When sending a message, the client should send just the message text, with no annotation, + and should not display the message locally. +The message, with modifications applied, will be returned by the server if all goes well. + +The server should keep a 'subscription pool' of all clients that have sent blank strings "recently". +The server can implement the subscription pool - and it's definition of "recently" - however it likes, + though the standard should be to wait until at least 16 seconds after the last packet from the client. +When the server sends a message, it sends an individual packet to each subscriber. diff --git a/oc/app/protocol.iot b/oc/app/protocol.iot new file mode 100644 index 0000000..f9141d8 --- /dev/null +++ b/oc/app/protocol.iot @@ -0,0 +1,63 @@ +The IoT Protocol + - 20kdc, 2017 + +One of the core uses of Copper is network-connected small devices. +These have a set list of requirements: +1. The device itself can't be forced to do much in the way of work, + at least for simple cases. +2. The protocol needs to be relatively flexible. + +The protocol is multi-part, each part noting a different aspect of the system. + +Part 1. General Mainport Packet Description + +The "Main port" is port 4. +This port is the port via which most data exchange happens. + +First things first - in case of error, do nothing at all. +If it's possible to just ignore a protocol violation, do so. +For example, a get request with data should be treated as a get request without it. + +In all packets on the main port, + the first byte's upper 2 bits indicate what kind of packet this is - + the lower 6 bits is the 'variable number'. +The remainder of the packet is the parameter data. + +00: Request: Get (This has no further data.) +01: Request: Set (The data is the new variable contents. + Success is determined by checking acknowledgement, then performing a Get.) +10: Perform Action: (The data is the parameter.) +11: Response: Get (The data is the variable contents.) + +Part 2. Variable Types + +Variable types indicate to the program what values are expected in variables. +Firstly, the upper bit being set indicates an Action - only the lower bits actually give the variable type-code. +Secondly, if it's not an Action, the second-to-upper bit indicates if it can be set. + +They are, as follows: + +0: Void (Only useful for Actions. Think 'ignore anything out of here'. +1: Human-Readable String (Generic string that a user could feasibly type. No special characters.) +2: Boolean (Length > 0 means true, otherwise false. Odd definition, but also convenient.) +3: Float (See Human-Readable String, but translated to a number by the device.) +4: Descriptor (See Part 3.) + +Part 3. Discovery & Description + +Upon the receipt of any unreliable packet on port 1 that is addressed to "*" or the IoT device name, + it should send a packet back on port 4 formatted as a Get response packet for variable 0. + +Variable 0 is always the Descriptor, which describes the variables and actions available. + +The Descriptor is simply a list of variable types and 7-byte variable names, + starting from variable index 1 (as 0 is always the descriptor) +It's recommended the variable names are in camel-case. + +Example descriptor for a networked lightbulb: + +"\x42lActive" + +Example descriptor for a networked turtle (Lua-escaped): + +"\x80turnLft\x80turnRgt\x80forward\x80backwrd\x02fwd!air" diff --git a/oc/app/protocol.ping b/oc/app/protocol.ping new file mode 100644 index 0000000..cca8762 --- /dev/null +++ b/oc/app/protocol.ping @@ -0,0 +1,11 @@ +The Copper Ping Protocol + - 20kdc, 2017 + +The Ping protocol for Copper is very simple: + +For the server: +Run a Reliability Layer node. + +For the client: +Send a Reliability Layer packet to port 0, no data. +The time from sending to acknowledgement is the ping. diff --git a/oc/hierarchi.lua b/oc/hierarchi.lua index feba0d4..3a6b51d 100644 --- a/oc/hierarchi.lua +++ b/oc/hierarchi.lua @@ -2,46 +2,62 @@ -- No warranty is provided, implied or otherwise. -- Copper Hierarchial Gateway implementation for OpenComputers. --- Should be run in a Server Rack, with two servers, connected by Linked Cards. --- Each should be responsible for "it's" side. --- This is a piece of dedicated hardware for a specific purpose. --- The only reason it's not run on two microcontrollers is because +-- Best run in a Server with two modems, +-- but can be run with a support program on another computer with a modem, +-- one modem in the main computer, and a LC pair. + +-- The only reason this isn't run on two microcontrollers is because -- they're inconvenient to use and cdlib needs to be there - -- I'm sure you can port it yourself. local args = {...} --- Does the single modem connected to this server connect to the outside world? -local outbound = false - -if #args ~= 2 then error("Expecting args: outbound ('true'/'false'), network-name") end - -if args[1] == "true" then - outbound = true -elseif args[1] ~= "false" then - error("Only 'true' or 'false' are allowed for the 'outbound' argument.") +if #args ~= 2 then + error("Expecting args: outboundModem (or 'relay' to use Linked Cards), network-name") end -- What is the name of this division, including forward-slash? local netname = args[2] .. "/" +local outboundModemAdr = args[1] + +-- These names are chosen by sending direction. +local outboundModem, inboundModem + +if outboundModemAdr == "relay" then outboundModemAdr = nil end + local event = require("event") local component = require("component") local cdlib = require("cdlib") -local modem = component.modem -local tunnel = component.tunnel - --- It is possible that this is meant to be used --- on public wireless infrastructure - --- for example, if this was a server-level domain, --- perhaps solely connected via wireless... --- Oh well. It's the sysadmin's decision to connect it this way. --- Any wireless-abuse is the local regulator's decision. -if modem.isWireless() then - modem.setStrength(400) +for a, _ in component.list("modem") do + if outboundModemAdr and (a:sub(1, outboundModemAdr:len()) == outboundModemAdr) then + if outboundModem then error("Outbound modem ambiguous.") end + outboundModem = component.proxy(a) + else + if inboundModem then error("More than one internal-side modem.") end + inboundModem = component.proxy(a) + end end -modem.open(4957) + +inboundModem.open(4957) +if not outboundModem then + local tunnel = component.tunnel + -- Implement just enough of an outbound modem to be useful. + outboundModem = { + address = tunnel.address, + broadcast = function (port, ...) + tunnel.send(...) + end + } +else + outboundModem.open(4957) +end + +------ By this point, inboundModem and outboundModem must be: +-- 1. non-nil +-- 2. have the address and broadcast(port, ...) fields +-- (Also, if outboundModemAdr == nil then the port will be ignored for it.) -- Rules used on messages coming in from the 'modem' side. -- (This implies Tunnel packets are trusted absolutely - which is correct.) @@ -49,27 +65,27 @@ local processFrom, processTo -- Implementation of the rules described in protocol.1 for more or -- less unambiguous name translation. -if outbound then - processFrom = function (from) +-- "incoming" is parent-side, incoming being false means child-side. +processFrom = function (incoming, from) + if incoming then if from:sub(1, netname:len()) == netname then return end return "<" .. from - end - processTo = function (nto) - if nto:sub(1, netname:len()) ~= netname then - return - end - return nto:sub(netname:len() + 1) - end -else - processFrom = function (from) + else if from:sub(1, 1) == "<" then return end return netname .. from end - processTo = function (nto) +end +processTo = function (incoming, nto) + if incoming then + if nto:sub(1, netname:len()) ~= netname then + return + end + return nto:sub(netname:len() + 1) + else if nto:sub(1, 1) ~= "<" then return end @@ -84,28 +100,31 @@ local function checkLen(s) return s end +local function handlePacket(incoming, dat) + local hops, nfrom, nto, data = cdlib.decode(dat) + if not data then return end -- corrupt packet + if hops == 255 then return end + + local tfrom, tto = checkLen(processFrom(nfrom)), checkLen(processTo(nto)) + if tfrom and tto then + local resdata = cdlib.encode(hops + 1, tfrom, tto, data) + if incoming then + inboundModem.broadcast(4957, "copper", resdata) + else + outboundModem.broadcast(4957, "copper", resdata) + end + end +end + while true do local e = {event.pull("modem_message")} if e[1] == "modem_message" then - -- type, to, from, port, dist, magic[6], data[7] - if ((e[2] == tunnel.address) or (e[4] == 4957)) then - if e[6] == "copper" then - if type(e[7]) == "string" then - local hops, nfrom, nto, data = cdlib.decode(e[7]) - if data then - if e[2] == tunnel.address then - -- Pass it on as given. - modem.broadcast(4957, "copper", e[7]) - elseif e[2] == modem.address then - -- Process it, then give to tunnel - if hops ~= 255 then - local tfrom, tto = checkLen(processFrom(nfrom)), checkLen(processTo(nto)) - if tfrom and tto then - tunnel.send("copper", cdlib.encode(hops + 1, tfrom, tto, data)) - end - end - end - end + -- type, receiver, sender, port, dist, magic, data + if (e[2] == inboundModem.address) or (e[2] == outboundModem.address) then + local incoming = e[2] == outboundModem.address + if (e[4] == 4957) or (incoming and (outboundModemAdr == nil)) then + if (e[6] == "copper") and e[7] then + handlePacket(incoming, e[7]) end end end diff --git a/ports b/ports new file mode 100644 index 0000000..b037e69 --- /dev/null +++ b/ports @@ -0,0 +1,5 @@ +0: Ping +1: 'IoT' Protocol: Discovery +2: Chat (Client to Server) +3: Chat (Server to Client) +4: 'IoT' Protocol: Main