1
0
mirror of https://github.com/XeonSquared/OC-Copper.git synced 2024-11-23 09:58:06 +11:00

Move workspace forward to include some applications & application protocol documents

This commit is contained in:
20kdc 2017-03-22 23:05:31 +00:00
parent bf3ec0ec26
commit 2c13ad9469
8 changed files with 288 additions and 56 deletions

43
oc/app/chat-cli.lua Normal file
View File

@ -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)

47
oc/app/chat-srv.lua Normal file
View File

@ -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

20
oc/app/ping.lua Normal file
View File

@ -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

24
oc/app/protocol.chat Normal file
View File

@ -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.

63
oc/app/protocol.iot Normal file
View File

@ -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"

11
oc/app/protocol.ping Normal file
View File

@ -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.

View File

@ -2,46 +2,62 @@
-- No warranty is provided, implied or otherwise. -- No warranty is provided, implied or otherwise.
-- Copper Hierarchial Gateway implementation for OpenComputers. -- Copper Hierarchial Gateway implementation for OpenComputers.
-- Should be run in a Server Rack, with two servers, connected by Linked Cards. -- Best run in a Server with two modems,
-- Each should be responsible for "it's" side. -- but can be run with a support program on another computer with a modem,
-- This is a piece of dedicated hardware for a specific purpose. -- one modem in the main computer, and a LC pair.
-- The only reason it's not run on two microcontrollers is because
-- The only reason this isn't run on two microcontrollers is because
-- they're inconvenient to use and cdlib needs to be there - -- they're inconvenient to use and cdlib needs to be there -
-- I'm sure you can port it yourself. -- I'm sure you can port it yourself.
local args = {...} local args = {...}
-- Does the single modem connected to this server connect to the outside world? if #args ~= 2 then
local outbound = false error("Expecting args: outboundModem (or 'relay' to use Linked Cards), network-name")
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.")
end end
-- What is the name of this division, including forward-slash? -- What is the name of this division, including forward-slash?
local netname = args[2] .. "/" 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 event = require("event")
local component = require("component") local component = require("component")
local cdlib = require("cdlib") local cdlib = require("cdlib")
local modem = component.modem for a, _ in component.list("modem") do
local tunnel = component.tunnel if outboundModemAdr and (a:sub(1, outboundModemAdr:len()) == outboundModemAdr) then
if outboundModem then error("Outbound modem ambiguous.") end
-- It is possible that this is meant to be used outboundModem = component.proxy(a)
-- on public wireless infrastructure - else
-- for example, if this was a server-level domain, if inboundModem then error("More than one internal-side modem.") end
-- perhaps solely connected via wireless... inboundModem = component.proxy(a)
-- Oh well. It's the sysadmin's decision to connect it this way. end
-- Any wireless-abuse is the local regulator's decision.
if modem.isWireless() then
modem.setStrength(400)
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. -- Rules used on messages coming in from the 'modem' side.
-- (This implies Tunnel packets are trusted absolutely - which is correct.) -- (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 -- Implementation of the rules described in protocol.1 for more or
-- less unambiguous name translation. -- less unambiguous name translation.
if outbound then -- "incoming" is parent-side, incoming being false means child-side.
processFrom = function (from) processFrom = function (incoming, from)
if incoming then
if from:sub(1, netname:len()) == netname then if from:sub(1, netname:len()) == netname then
return return
end end
return "<" .. from return "<" .. from
end else
processTo = function (nto)
if nto:sub(1, netname:len()) ~= netname then
return
end
return nto:sub(netname:len() + 1)
end
else
processFrom = function (from)
if from:sub(1, 1) == "<" then if from:sub(1, 1) == "<" then
return return
end end
return netname .. from return netname .. from
end 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 if nto:sub(1, 1) ~= "<" then
return return
end end
@ -84,28 +100,31 @@ local function checkLen(s)
return s return s
end 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 while true do
local e = {event.pull("modem_message")} local e = {event.pull("modem_message")}
if e[1] == "modem_message" then if e[1] == "modem_message" then
-- type, to, from, port, dist, magic[6], data[7] -- type, receiver, sender, port, dist, magic, data
if ((e[2] == tunnel.address) or (e[4] == 4957)) then if (e[2] == inboundModem.address) or (e[2] == outboundModem.address) then
if e[6] == "copper" then local incoming = e[2] == outboundModem.address
if type(e[7]) == "string" then if (e[4] == 4957) or (incoming and (outboundModemAdr == nil)) then
local hops, nfrom, nto, data = cdlib.decode(e[7]) if (e[6] == "copper") and e[7] then
if data then handlePacket(incoming, e[7])
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
end end
end end
end end

5
ports Normal file
View File

@ -0,0 +1,5 @@
0: Ping
1: 'IoT' Protocol: Discovery
2: Chat (Client to Server)
3: Chat (Server to Client)
4: 'IoT' Protocol: Main