Modular Computers the manual GitHub

Network Card

A network card sends frames over the cables to another card’s address, or to every card it reaches. It has the shape of a real NIC.

Facts

| | | |—|—| | Type | network | | Address | An expansion slot path such as exp:0 | | Interrupt | Raised while frames wait; also a wake line | | Power | 2 W at rest; a send costs a tenth of a tick of a running CPU | | Frame size | At most 8192 bytes | | Info | kind (network), wireless (false) |

The wireless cards, which send through the air, are on WLAN Cards; both kinds have the same methods. A wired card hears only wired cards, and a WLAN card only WLAN cards. Nothing routes between the two networks.

Addresses

A card’s address is its id (a MAC address burned into the card: its item’s serial number), without the id: prefix. send takes it with or without the prefix. A frame goes to the card with that address, or to all cards in reach (a broadcast).

A wired frame reaches the cards the cables reach from the card’s block, through hubs and screens and across Bluetooth links, ending at a computer. A cable is a shared fabric: two cases on one cable are two hosts on it. Two cards in one block are on the same wire, 0 meters apart.

Nothing is dropped

A frame sent to one card is taken, or the sender hears why not at once. The card never tries again; that is software’s job (mcos’s TCP does).

A broadcast is best effort: it says how many cards took it.

In mcos

mcos sends and receives through sockets (UDP and TCP), with Linux’s names for interfaces (eth0, wlan0). Ports belong to the kernel, in the frame’s data: the card knows only addresses. See Networking. A card’s address is shown by lsdev -i.

The card’s file is /dev/network0 (network1 for the next card, wired or not). It is its traffic, as tcpdump shows a card’s: a line for each frame in or out. Each line is the direction, the other card’s address (or broadcast), how far it is in meters (- for a broadcast sent), the frame’s bytes, and how it went:

in 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 5 taken
out 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 8 taken
out 9a1b2c3d-0000-4000-8000-000000000001 - 12 unreachable
in 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 9 broadcast
out broadcast - 4 2 took it

An outcome is taken, busy or unreachable for a frame to one card, broadcast for a broadcast that came in, and N took it for one sent. The card keeps only its last 8 lines, so a reader sees what’s current.

local sys = require("sys")
local card = assert(sys.open("/dev/network0"))
print(sys.ioctl(card, "address"))  -- this card's address
sys.ioctl(card, "setWake", "letmein")

A method’s failure is nil, its message and EINVAL; a card that has gone is ENODEV. The kernel’s own ioctl setLabel sets the card’s label (see /dev).

Once a program has used a socket, the kernel takes every frame the card gets, for its sockets, so the receive method finds nothing: receive through sockets. Sending by send and broadcast works alongside them.

Methods

address() → string

The card’s address: its id, which other cards send to.

send(address, data) → true, or false and why

Sends a frame of at most 8192 bytes to the card with that address (with or without id:). Gives true when it was taken, else false and "busy" or "unreachable". A longer frame fails with frame too long. Sending costs power. The card never tries again itself.

broadcast(data) → count

Sends a frame to every card in reach and gives how many took it. A longer frame than 8192 bytes fails with frame too long.

receive() → frame or nil

The oldest frame that came, or nil. A frame is a table:

Field Value
from the sender’s address
data the bytes
distance meters; a wired frame’s is the length of cable it went along
broadcast true, only when it was a broadcast

pull() → entry or nil

The traffic log’s oldest entry, or nil: what the card’s file reads as a line.

Field Value
direction "in" or "out"
address the other card’s, or "broadcast"
distance meters; missing for a broadcast sent
bytes the frame’s size
outcome "taken", "busy", "unreachable", "broadcast", or "N took it"

getWake() → string or nil

The wake message, or nil for none.

setWake([message]) → old message or nil

Sets the message that turns the computer on while it is off. Nil or an empty string clears it. At most 256 bytes (wake message too long). Returns the old message.

getStrength(), setStrength(blocks)

A WLAN card’s signal strength (see WLAN Cards). On a wired card both fail with a wired card has no signal strength.

Events

Each frame that arrives is an event, taken with receive, and raises the card’s interrupt; it stays raised while frames wait. At most 32 wait, which is the card’s ring; a frame that finds it full is refused as "busy", not dropped. Frames wait only while the card has power and a program runs on its computer; they are dropped when it stops. Each frame in or out also adds a line to the traffic log (pull).

Wake-on-LAN. With a wake message set, a card listens while its computer is off. A frame holding the message anywhere in its data presses the computer’s power button, so a UDP datagram carrying it does. The card takes such a frame (the sender hears true) whether it holds the message or not; without a message set, an off computer’s card is "unreachable". The setting is kept across power off and saves. This is the mod’s own wake message, not a real magic packet.

Examples

Each is a complete program: copy it to a computer with a network card and run it with lua. For why they loop without yielding, see Writing programs. Two computers on one cable can try the socket ones; nc is a handy other end.

Log the traffic

Through the card’s file: each line it reads goes into a log, with the time.

-- Keep a log of everything a network card sends and takes, each frame timed, in a file.
--
-- The card's file, /dev/network0, is its traffic: a line for each frame in or out, as
-- "in 5f0c2a9e-... 3.0m 17 taken" (the direction, the other card or "broadcast", how far, the
-- frame's bytes, and how it went). A read waits until a frame comes or goes, so the program uses
-- no time between frames. Each line is added to the log, and the log closed again, so what's
-- logged is on the disk at once (a file's writes reach the disk when it's closed).
--
-- usage: lua network-traffic.lua [log [card]]
local sys = require("sys")

local LOG = arg[1] or "/home/traffic.log"
local CARD = arg[2] or "/dev/network0"
local READ_SIZE = 512 -- the most one read takes (a line is far shorter)

local traffic = assert(sys.open(CARD))
local pending = "" -- what has been read of a line not yet ended

while true do
  local data = assert(sys.read(traffic, READ_SIZE)) -- waits for a frame
  pending = pending .. data
  for line in pending:gmatch("([^\n]*)\n") do
    local log = assert(io.open(LOG, "a"))
    log:write(string.format("%10.2f %s\n", sys.time(), line))
    log:close()
  end
  pending = pending:match("[^\n]*$")
end

Download network-traffic.lua

A chat over UDP

Broadcasts what’s typed, shows what others send. Run it on each computer: lua network-chat.lua alice.

-- A chat between every computer on the network: what you type goes to all of them, and what they
-- send shows here, with who sent it.
--
-- Each line typed is a UDP datagram broadcast to PORT ("name: text"), and each datagram that comes
-- in on PORT is shown. A broadcast comes back to this computer's own sockets too, so lines from its
-- own cards (their addresses, from the socket's SIOCGIFCONF ioctl) are skipped. sys.poll waits for
-- whichever comes first, the keyboard or the network, so the program uses no time while nobody
-- talks. It ends at the end of its input (Ctrl+D at the start of a line).
--
-- usage: lua network-chat.lua [name]
local sys = require("sys")

local PORT = 4000 -- every computer in the chat listens and sends here
local READ_SIZE = 4096 -- the most one read of the keyboard takes

local name = arg[1] or sys.gethostname()
local sock = assert(sys.socket()) -- UDP
assert(sys.bind(sock, PORT))

local own = {} -- this computer's cards' addresses
for _, interface in ipairs(sys.ioctl(sock, "SIOCGIFCONF")) do
  own[interface.address] = true
end

local typed = "" -- keys read, up to a line's end
local watch = {[0] = {IN = true}, [sock] = {IN = true}}
while true do
  local ready = sys.poll(watch) -- blocks until a line is typed or a datagram comes
  if ready[sock] then
    local data, from = sys.recvfrom(sock)
    if not own[from] then
      io.write(data)
      io.flush()
    end
  end
  if ready[0] then
    local keys = sys.read(0, READ_SIZE)
    if not keys or keys == "" then
      break -- the end of the input
    end
    typed = typed .. keys
    for line in typed:gmatch("([^\n]*)\n") do
      local ok, why = sys.sendto(sock, name .. ": " .. line .. "\n", "broadcast", PORT)
      if not ok then
        io.stderr:write("chat: not sent: ", why, "\n")
      end
    end
    typed = typed:match("[^\n]*$")
  end
end
sys.close(sock)

Download network-chat.lua

A TCP echo server

Serves any number of clients at once with one sys.poll. Start it with lua network-echo.lua &, then from another computer:

echo hello | nc <the server card's address> 7
-- An echo server over TCP: everything a client sends comes back to it, for as many clients as
-- connect at once. Try it from another computer with nc: echo hello | nc <this card's id> 7
--
-- The listening socket is ready (IN) when a connection waits to be accepted, and a connection when
-- it has data or its client is done sending (a read gives "" then: the connection is closed).
-- sys.poll waits on all of them at once, so the server uses no time while nobody talks.
--
-- usage: lua network-echo.lua [port]
local sys = require("sys")

local PORT = tonumber(arg[1]) or 7 -- echo's own port
local READ_SIZE = 4096 -- the most one read of a connection takes

local server = assert(sys.socket("stream"))
assert(sys.bind(server, PORT))
assert(sys.listen(server))

-- Writes all of 'data' (a write takes what the connection has room for, and waits when it has none).
local function send(fd, data)
  while #data > 0 do
    local count = sys.write(fd, data)
    if not count then
      return false -- the client went away
    end
    data = data:sub(count + 1)
  end
  return true
end

local clients = {} -- each connection's descriptor, to its client's address
while true do
  local watch = {[server] = {IN = true}}
  for fd in pairs(clients) do
    watch[fd] = {IN = true}
  end
  local ready = sys.poll(watch)
  for fd in pairs(ready) do
    if fd == server then
      local conn, address = sys.accept(server)
      if conn then
        clients[conn] = address
      end
    else
      local data = sys.read(fd, READ_SIZE)
      if not (data and data ~= "" and send(fd, data)) then
        sys.close(fd) -- its end, or it's gone
        clients[fd] = nil
      end
    end
  end
end

Download network-echo.lua

Wake-on-LAN

The card’s setWake and getWake methods as ioctls, and a datagram to wake another computer. On the computer to be woken, lua network-wol.lua enable letmein, then poweroff; from another, lua network-wol.lua <its card's address> letmein.

-- Wake-on-LAN: get this computer ready to be turned on over the network, or turn another one on.
--
--   lua network-wol.lua enable MESSAGE    from now on, a frame holding MESSAGE turns this one on
--   lua network-wol.lua disable           it doesn't any more
--   lua network-wol.lua status            the message set, if any
--   lua network-wol.lua ADDRESS MESSAGE   turn on the computer whose card is at ADDRESS
--
-- The wake message is the card's own setting, set and read with its methods setWake and getWake,
-- which are ioctls on its file. A card with one listens while its computer is off, and a frame
-- holding the message anywhere in its data presses the power button, so a UDP datagram carrying it
-- does. The sender hears whether the card took it (EHOSTUNREACH: no card there, or it isn't
-- listening).
local sys = require("sys")

local CARD = "/dev/network0"
local PORT = 9 -- discard's port, where real wake-on-LAN packets go: no program answers there

local command, message = arg[1], arg[2]

local function fail(...)
  io.stderr:write("wol: ", ...)
  io.stderr:write("\n")
  os.exit(1)
end

if command == "enable" or command == "disable" or command == "status" then
  local card = assert(sys.open(CARD))
  if command == "status" then
    print(sys.ioctl(card, "getWake") or "(none)")
  else
    if command == "enable" and not message then
      fail("enable needs a message")
    end
    local old, why = sys.ioctl(card, "setWake", command == "enable" and message or nil)
    if old == nil and why then
      fail(why) -- (a message over 256 bytes)
    end
    print("was: " .. tostring(old or "(none)"))
  end
  sys.close(card)
elseif command and message then
  local sock = assert(sys.socket())
  local sent, why = sys.sendto(sock, message, command, PORT)
  if not sent then
    fail(command, ": ", why)
  end
  print("sent to " .. command)
else
  fail("usage: wol enable MESSAGE | disable | status | ADDRESS MESSAGE")
end

Download network-wol.lua