Modular Computers the manual GitHub

Redstone Card

A redstone card puts out a redstone signal on any face of the block it is installed in, and reads the signal on each face.

Facts

| | | |—|—| | Type | redstone | | Address | An expansion slot path such as exp:0, or a hub’s | | Interrupt | Raised while a change to a face waits | | Power | Nothing at rest; each change of an output costs a tick of a running CPU | | Info | kind (redstone) |

It works where it is installed: in a case it sets and reads redstone at the case, in an expansion hub at the hub. A block may hold several cards. On each face it puts out the strongest signal of any of its cards. Without power it puts out nothing.

Faces

All six, by face name: north, south, east, west, up, down, or front, back, left, right relative to the block’s front. Answers always use the world’s names. Each face has two numbers:

So a second card beside one that puts out 15 reads level = 15, powered = 0.

In mcos

/dev/redstone0 (redstone1 for the next card) is text, a line a face, in this order:

north 7 7
south 0 0
east 0 0
west 9 0
up 0 0
down 0 0

Each line is face level powered. The state is taken at the first read after the file is opened or sought back to its start, so a read from the start always gives the faces as they are then.

A line written, face powered, sets that face as soon as the line is written (flushed), as a sysfs attribute does: north 11. Relative names work too (front 15). A line that is not a face and a number fails the write with EINVAL, and so does an unknown face (no such face).

The file is always readable (poll’s IN) and writable (OUT). A change to any face since the file was last sought to its start shows as PRI, and stays until the file is sought there again (f:seek("set", 0), sys.lseek(fd, 0, "set")), as a GPIO’s value file does. tail -f shows the faces again each time they change.

cat /dev/redstone0
echo "north 15" > /dev/redstone0
tail -f /dev/redstone0

The file’s ioctls are the card’s methods, by name, in any open mode: sys.ioctl(fd, "set", {north = {powered = 15}}). A method that fails answers nil, its message and EINVAL; a card that is gone, ENODEV. setLabel labels the card, as for any device (see The sys library).

The file’s PRI comes from the card’s events, and polling loses none: the events a poll found are the next ones pull answers, and once a program has pulled them all, PRI is off until the next change (as reading the file from its start turns it off). So a program can wait with sys.poll and then take every change with pull until it answers nil.

Methods

get([sides]) → table

Every face as {level, powered}, by world name: {north = {level = 7, powered = 7}, ...}. With a table, only the faces it names, as keys or as a list of names, relative names too ({"front"} answers the face that is the block’s front, by its world name). An unknown face fails with no such face.

local west = sys.ioctl(fd, "get", {"west"}).west.level

set(sides) → table

Takes a table in the same shape and sets each face it names: {north = {powered = 11}}. powered is the signal to put out; it is clamped to 0 through 15 and must be an integer (north: powered: integer expected). level is what is detected, so it is ignored, and a face the table leaves out stays as it was. Returns every face, as get(). The arguments are all checked before anything changes, so a table with one bad face changes nothing. An unknown face fails with no such face; a face that is not a table, with north: table expected. Each output that changes costs a tick of a running CPU’s power.

sys.ioctl(fd, "set", {north = {powered = 15}, up = {powered = 0}})

pull() → event or nil

The oldest change waiting, as an event table (see Events), or nil when none waits. Each is taken once.

local event = sys.ioctl(fd, "pull")  -- {name = "redstone", side = "west", level = 9, powered = 0}

Events

Any change to a face’s state raises an event, from wherever it came: a signal coming in, this card’s own set, or another card in the block. Its fields are the face as it is now:

Field Value
name "redstone"
side the face, by world name
level the signal there, 0 to 15
powered this card’s output there

At most 32 wait. When more come, the oldest is dropped and the next event carries lost, the number dropped. The events are kept across a save, and dropped when no program runs on the computer.

Examples

A program drives the card by writing lines to its file and reading it back, or through its methods as ioctls. Run these with lua name.lua, from a computer with a redstone card and something on its faces (add & to keep the shell). Each is a complete program. For why they loop without yielding, see Writing programs.

Set an output

local f = io.open("/dev/redstone0", "w")
f:write("north 15\n")  -- "face strength": 0 to 15
f:close()

A line written takes effect when it is flushed (f:flush(), or closing the file). From a shell, echo "north 15" > /dev/redstone0 does the same.

Read an input

local f = io.open("/dev/redstone0")
local text = f:read("a")  -- one line a face: "west 9 0"
local level = tonumber(text:match("west (%d+)"))
print("west is " .. level)

Each line is face level powered: the signal on that face, and how much of it is this card’s own.

-- Blink whatever is on the redstone card's north face: on half a second, off half a second, forever.
--
-- There is no event loop and nothing to yield: this is a plain loop. sys.sleep waits (the
-- computer does other work meanwhile) and the loop goes on until you stop it with Ctrl+C.
local sys = require("sys")

local FACE = "north"  -- any face: north, south, east, west, up, down (or front, back, left, right)
local HALF = 0.5      -- seconds on, and seconds off

-- A line written to the card's file is "face strength" and takes effect at once.
local card = io.open("/dev/redstone0", "w")

local function set(strength)
  card:write(FACE, " ", strength, "\n")
  card:flush()
end

while true do
  set(15)
  sys.sleep(HALF)
  set(0)
  sys.sleep(HALF)
end

Download redstone-blink.lua

Follow an input

-- Copy the signal coming in on the west face out of the east face, and keep doing it.
--
-- The card's file says "face level powered" for each face: the level of the signal there, and how
-- much of it is this card's own. sys.poll waits (it blocks, it does not yield) until the card
-- reports a change, so the program uses no time while nothing happens.
local sys = require("sys")

local FROM = "west"
local TO = "east"

local reader = io.open("/dev/redstone0")
local writer = io.open("/dev/redstone0", "w")
local watch = {[sys.fileno(reader)] = {PRI = true}}  -- PRI: the device's state changed

-- Read the whole file from its start, and pick out one face's level.
local function level(face)
  reader:seek("set", 0)
  return tonumber(reader:read("a"):match(face .. " (%d+)")) or 0
end

local last = -1
while true do
  local now = level(FROM)
  if now ~= last then
    writer:write(TO, " ", now, "\n")
    writer:flush()
    last = now
  end
  sys.poll(watch)  -- block until the card changes
end

Download redstone-mirror.lua

sys.poll blocks until the card reports a change (PRI), so the program uses no time while the signal is steady. The redstone tutorial walks through both programs.

A door that closes by itself

-- A door that opens when a button on the west face is pressed, and closes by itself once nothing
-- has changed for a few seconds (a pressure plate or a button held keeps it open).
--
-- The door (an iron door, a piston) is on the north face. It uses the card's methods, through its
-- file's ioctls: get to read the button, set to power the door. sys.poll waits for the card to
-- change (PRI), and with a time limit for the door to stay open. A redstone file's PRI stays on
-- until the file is sought back to its start, so the program seeks after each wake.
local sys = require("sys")

local BUTTON = "west"
local DOOR = "north"
local HOLD = 3 -- seconds the door stays open after the last change

local card = assert(sys.open("/dev/redstone0"))
local watch = {[card] = {PRI = true}}

-- Waits until the card changes, or 'seconds' pass: whether it changed.
local function changed(seconds)
  local ready = sys.poll(watch, seconds)
  sys.lseek(card, 0, "set") -- (PRI again only for a newer change)
  return ready[card] ~= nil
end

local function pressed()
  return sys.ioctl(card, "get", {BUTTON})[BUTTON].level > 0
end

local function door(strength)
  sys.ioctl(card, "set", {[DOOR] = {powered = strength}})
end

door(0)
while true do
  if pressed() then
    door(15)
    while changed(HOLD) do -- (each change, the button let go too, starts the wait again)
    end
    door(0)
  end
  changed() -- wait for the next press
end

Download redstone-door.lua

The door’s own change (its face powered) wakes poll too; that only starts the wait again.

Count pulses

-- Count the pulses that come in on the west face (a button, a hopper clock, a daylight sensor) and
-- log how many came each minute, a line a minute: "Day 3 08:12:00 4".
--
-- It uses the card's methods, through its file's ioctls: get for a face's level now, and pull for
-- each change the card kept since the last look. The card keeps its changes (32 at most) while the
-- program sleeps, so sys.sleep is the whole wait and no pulse between looks is missed. A change it
-- had to drop shows as "lost" on the next one.
local sys = require("sys")

local FACE = "west"            -- where the pulses come in
local PERIOD = 60              -- seconds between counts
local LOG = "/home/pulses.log" -- a line a count, appended

local card = assert(sys.open("/dev/redstone0"))

-- Every change the card kept, oldest first (pull answers nil when there are none left).
local function changes()
  local list = {}
  local event = sys.ioctl(card, "pull")
  while event do
    list[#list + 1] = event
    event = sys.ioctl(card, "pull")
  end
  return list
end

changes() -- (what changed before the program started is not counted)
local was = sys.ioctl(card, "get", {FACE})[FACE].level

while true do
  sys.sleep(PERIOD)
  local pulses, lost = 0, 0
  for _, event in ipairs(changes()) do
    lost = lost + (event.lost or 0)
    if event.side == FACE then
      if event.level > 0 and was == 0 then
        pulses = pulses + 1 -- off to on: a pulse
      end
      was = event.level
    end
  end
  local log = assert(io.open(LOG, "a"))
  log:write(os.date("%c"), " ", pulses, lost > 0 and " (some changes lost)" or "", "\n")
  log:close() -- (a disk's file shows what's written when it's closed)
end

Download redstone-counter.lua

The card keeps at most 32 changes: a pulse is two, so with more than 16 pulses a period some are lost (the log says so). Shorten PERIOD for a faster clock.