skipzero
4/17/2015 - 1:21 AM

bootstrap.lua

--
-- Control turbines of a active cooled Big Reactor (http://big-reactors.com/).
--
-- Author: kla_sch
--
-- History:
--     v0.1, 2014-12-29:
--         - first version
--
--     v0.2, 2015-01-02:
--         - Big Reactor since 0.3.4A: Feature to disengage coil to
--           startup faster (see config value useDisengageCoils).
--         - minor bugfixes
--         - some code improvments (thanks to wieselkatze)
--
-- Save as "startup"


--
-- Constant values (configuration)
-- ===============================


-- Maximum loop time of controller (default: 0.5s)
local loopT=0.5

-- Display loop time, of controller has been switched off (default: 1s). 
local displayT=1

-- Modem channel to listen for status requests. If it is set to 0,
-- the remote status requests are disabled.
-- The sender sould simply send "BR_turbine_get_state" to this channel. The
-- turbine replies with status informations to the replay channel.
local stateRequestChannel = 32768 -- Broadcast channel.

--
-- Enable remote control.
-- Set to "true" if you want to enable this feature.
--
local remoteControl = false

--
-- Use disengaged coil for faster speedup. (Big Reactor >= 0.3.4A).
-- Set this to "false" if you want to disable this feature.
--
local useDisengageCoils = true


--
-- Internal values:
-- ================

-- File to save last known controller state:
local saveFilename = "turbine_crtl.save" 

local Kp, Kd, Ki, errSum, errOld -- global PID controller values
local tSpeed -- target speed
local turbine -- wraped turbine
local maxFRate -- maximum float rate of turbine
local floatRateChanged = false -- flag: true on float rate change


--
-- Find the first connected big turbine and return the wraped handle.
--
-- If no turbine was found this function terminate the program.
--
-- Return:
--     Handle of first connected turbine found.
--
local function getTurbineHandle()
   local pList = peripheral.getNames()
   local i, name
   for i, name in pairs(pList) do
      if peripheral.getType(name) == "BigReactors-Turbine" then
         return peripheral.wrap(name)
      end
   end

   error("No turbine connected: Exit program")
end


--
-- Search for any modem and open it to recieve modem requests.
--
local function searchAndOpenModems()
   if stateRequestChannel <= 0 then
      return -- feature disabled
   end

   local pList = peripheral.getNames()
   local i, name
   for i, name in pairs(pList) do
      if peripheral.getType(name) == "modem" then
         peripheral.call(name, "open", stateRequestChannel)
         peripheral.call(name, "open", os.getComputerID())
      end
   end
end


--
-- Saves current controller state
--
local function saveControllerState()
   local tmpFilename = saveFilename .. ".tmp"
   
   fs.delete(tmpFilename)
   local sFile = fs.open(tmpFilename, "w")
   if sFile == nil then
      error("cannot open status file for writing.")
   end

   sFile.writeLine("V0.1")
   sFile.writeLine(tostring(tSpeed))
   sFile.writeLine(tostring(loopT))
   sFile.writeLine(tostring(Kp))
   sFile.writeLine(tostring(errSum))
   sFile.writeLine(tostring(errOld))
   sFile.writeLine("EOF")
   sFile.close()

   fs.delete(saveFilename)
   fs.move(tmpFilename, saveFilename)
end


--
-- Initialize basic PID controller values
--
local function initControllerValues()
   local Tn = loopT
   local Tv = loopT * 10

   Ki = Kp / Tn
   Kd = Kp * Tv
end


--
-- Read number from file
--
-- Parameters:
--     sFile - opened file to read from
--
-- Return
--     the number of nil, if an error has occurred 
--
local function readNumber(sFile)
   local s = sFile.readLine()
   if s == nil then
      return nil
   end
   return tonumber(s)
end

--
-- Restore last known controller state
--
-- Returns:
--     true, if the last saved state has successfully readed.
local function restoreControllerState()
   local tmpFilename = saveFilename .. ".tmp"
   local sFile = fs.open(saveFilename, "r")
   if sFile == nil and fs.exists(tmpFilename) then
      fs.move(tmpFilename, saveFilename)
      sFile = fs.open(saveFilename)
   end

   if sFile == nil then
      return false -- cannot read any file
   end

   local version = sFile.readLine()
   if version == nil then
      sFile.close()
      return false -- empty file
   end
   if version ~= "V0.1" then
      sFile.close()
      return false -- unknown version
   end

   local tSpeedNum = readNumber(sFile)
   if tSpeedNum == nil then
      sFile.close()
      return false -- cannot read target speed
   end

   local loopTNum = readNumber(sFile)
   if loopTNum == nil then
      sFile.close()
      return false -- cannot read loop speed
   end

   local KpNum = readNumber(sFile)
   if KpNum == nil then
      sFile.close()
      return false -- cannot read Kp
   end

   local errSumNum = readNumber(sFile)
   if errSumNum == nil then
      sFile.close()
      return false -- cannot read error sum
   end

   local errOldNum = readNumber(sFile)
   if errOldNum == nil then
      sFile.close()
      return false -- cannot read last error
   end

   local eofStr = sFile.readLine()
   if eofStr == nil or eofStr ~= "EOF" then
      sFile.close()
      return false -- EOF marker not found. File corrupted?
   end

   sFile.close()

   -- Restore saved values
   tSpeed = tSpeedNum
   loopT = loopTNum
   Kp = KpNum
   errSum = errSumNum
   errOld = errOldNum

   initControllerValues()

   if tSpeed == 0 then
      turbine.setActive(false)
   else
      turbine.setActive(true)
   end

   return true
end


--
-- Write text with colors, if possible (advance monitor)
--
-- Parameters:
--     mon   - handle of monitor
--     color - text color
--     text  - text to write
--
local function writeColor(mon, color, text)
   if mon.isColor() then
      mon.setTextColor(color)
   end
   mon.write(text)
   if mon.isColor() then
      mon.setTextColor(colors.white)
   end
end


--
-- Scale the monitor text size to needed size of output text.
--
-- This function try to scale the monitor text size, so that it is enoth for
-- "optHeight" lines with "optWidth" characters. If it is not possible
-- (text scale is not supported or the connected monitor is too small),
-- it also accept "minHeight" lines and "minWidth" characters.
--
-- Parameters:
--     mon        - handle of monitor.
--     minWidth   - Minimum number of columns needed.
--     optWidth   - Optimal number of columns desired.
--     minHeight  - Minimum number of rows needed.
--     optHeight  - Optimal number of rows desired.
--
-- Return:
--     Size of monitor after scaling: width, height.
--     If the monitor is too small, it returns nul,nil.
--
local function scaleMonitor(mon, minWidth, optWidth, minHeight, optHeight)
   if mon.setTextScale ~= nil then
       mon.setTextScale(1)
   end

   local width, height = mon.getSize()

   if mon.setTextScale == nil then
      -- Scale not available
      if width < minWidth or height < minHeight then
         return nil, nil -- too small
      else
         return width, height
      end
   end

   if width < optWidth or height < optHeight then
      -- too small: try to scale down.
      mon.setTextScale(0.5)

      width, height = mon.getSize()
      if width < minWidth or height < minHeight then
         return nil, nil -- still too small
      end
   else
      -- Huge monitors? Try to scale up, if possible (max scale=5).
      local scale = math.min(width / optWidth, height / optHeight, 5)
      scale = math.floor(scale * 2) / 2 -- multiple of 0.5

      if scale > 1 then
         mon.setTextScale(scale)
         width, height = mon.getSize()
      end
   end

   return width, height
end


-- Display turbine status to a monitor
--
-- Parameters:
--     mon     - Wraped handle of monitor
--     turbine - Wraped handle of turbine.
--     tSpeed  - Target speed.
--
local function displayStateOnMonitor(mon, turbine, tSpeed)

   -- scale it, if possible.
   local width, height = scaleMonitor(mon, 15, 16, 5, 5)
   
   if width == nil or height == nil then
      return -- Montitor is too small
   end

   mon.clear()

   mon.setCursorPos(1,1)
   mon.write("Turbine: ")
   if tSpeed == 0 then
      writeColor(mon, colors.red, "off")
   else
      writeColor(mon, colors.green, string.format("%d", tSpeed))
      if width > 16 then
         mon.write(" RPM")
      end
   end

   mon.setCursorPos(1,3)
   local speed = math.floor(turbine.getRotorSpeed()*10+0.5)/10
   mon.write("Speed: ")
   if (speed == tSpeed) then
      writeColor(mon, colors.green, speed)
   else
      writeColor(mon, colors.orange, speed)
   end
   if width > 16 then
       mon.write(" RPM")
   end

   local maxFlow = turbine.getFluidFlowRateMax()
   local actFlow = turbine.getFluidFlowRate()
   if width >= 16 then
      -- bigger monitor
       mon.setCursorPos(1,4)
       mon.write("MFlow: " .. string.format("%d", maxFlow) .. " mB/t")

       mon.setCursorPos(1,5)
       mon.write("AFlow: ")
       if actFlow < maxFlow then
          writeColor(mon, colors.red, string.format("%d", actFlow))
       else
          writeColor(mon, colors.green, string.format("%d", actFlow))
       end
       mon.write(" mB/t")
   else
      -- 1x1 monitor
       mon.setCursorPos(1,4)
       mon.write("Flow (act/max):")
       mon.setCursorPos(1,5)
       mon.write("(")

       if actFlow < maxFlow then
          writeColor(mon, colors.red, string.format("%d",actFlow))
       else
          writeColor(mon, colors.green, string.format("%d",actFlow))
       end

       mon.write("/")
       mon.write(string.format("%d", maxFlow))
       mon.write(" mB/t)")
   end
   
end


-- Display turbine status to any connected monitor and also to console.
--
-- Parameters:
--     turbine - Wraped handle of turbine.
--     tSpeed  - Target speed.
--
function displayState(turbine, tSpeed)
  displayStateOnMonitor(term, turbine, tSpeed)
  term.setCursorPos(1,7)
  term.write("* Keys: [o]ff, [m]edium (900), [f]ast (1800)")
  term.setCursorPos(1,8)

   local pList = peripheral.getNames()
   local i, name
   for i, name in pairs(pList) do
      if peripheral.getType(name) == "monitor" then
         -- found monitor as peripheral
         displayStateOnMonitor(peripheral.wrap(name), turbine, tSpeed)
      end
   end   
end

--
-- Test the speedup time of the turbine.
--
-- Parameters:
--     turbine - Wraped handle of turbine.
--     loopT   - Loop timer.
--     tSpeed  - Target speed
local function testSpeedup(turbine, loopT, tSpeed)
   turbine.setFluidFlowRateMax(maxFRate)

   if turbine.setInductorEngaged then
      -- always engage coil
      turbine.setInductorEngaged(true)
   end
   
   local KpSum=0
   local nKp=0
   local oldSpeed=-1

   for i=0,5 do
      displayState(turbine, tSpeed)
      speed = turbine.getRotorSpeed()
      if oldSpeed >= 0 then
         KpSum = KpSum + (speed-oldSpeed)
         nKp = nKp + 1
      end
      oldSpeed = speed
      sleep(loopT)
   end

   if KpSum * loopT / nKp > 5 then
      -- Too fast: increase loop speed
      loopT = 5 * nKp / KpSum
      return 5, loopT
   else
      return (KpSum / nKp), loopT
   end
end


--
-- Main program
--

sleep(2) -- wait for 2s

-- wrap turbine
turbine = getTurbineHandle()

searchAndOpenModems() -- search and open any modem.

if restoreControllerState() == false then
   -- restore of old values failed.
   tSpeed = 0
   Kp=0
end


maxFRate = turbine.getFluidFlowRateMaxMax()
while true do
   displayState(turbine, tSpeed)

   if tSpeed ~= 0 then
      -- calculate PID controller
      local speed = turbine.getRotorSpeed()

      err = tSpeed - speed
      local errSumOld = errSum
      errSum = errSum + err

      if useDisengageCoils
         and floatRateChanged
         and err > 0
         and turbine.setInductorEngaged
      then
         -- Turbine startup: disengage coils
         turbine.setInductorEngaged(false)
      end

      if turbine.setInductorEngaged and err < 0 then
         -- speed too fast: engage coils
         turbine.setInductorEngaged(true)
      end

      local p = Kp * err
      local i = Ki * loopT * errSum
      local d = Kd * (err - errOld) * loopT

      if i < 0 or i > maxFRate then
         errSum=errSumOld -- error too heavy => reset to old value.
         i = Ki * loopT * errSum
      end

      local fRate = p + i + d
      errOld = err


      -- cut extreme flow rates.
      if fRate < 0 then
         fRate = 0
      elseif fRate > maxFRate then
         fRate = maxFRate
      end

      turbine.setFluidFlowRateMax(fRate)

      tID = os.startTimer(loopT) -- Wait for loopT secounds.
   else
      -- Turbine switched off:
      tID = os.startTimer(displayT) -- Wait for displayT secounds.
   end

   saveControllerState()
   floatRateChanged = false
   repeat
      -- Event loop
      local evt, p1, p2, p3, p4, p5 = os.pullEvent()
      if evt == "char" then
         -- character typed
         local oldTSpeed = tSpeed
         if p1 == "o" then -- off
            tSpeed = 0
            turbine.setActive(false)
            saveControllerState()
         elseif p1 == "m" then -- medium speed = 900 RPM
            turbine.setActive(true)
            tSpeed = 900
            floatRateChanged=true
         elseif p1 == "f" then -- fast speed = 1800 RPM
            turbine.setActive(true)
            tSpeed = 1800
            floatRateChanged=true
         end

         if turbine.setInductorEngaged then
            -- engage coil by default
            turbine.setInductorEngaged(true)
         end


         if (p1 == "m" or p1 == "f") and tSpeed ~= oldTSpeed then
            -- Initialize PID controller values
            if Kp == 0 then
               Kp, loopT = testSpeedup(turbine, loopT, tSpeed)
            end

            initControllerValues()

            errSum = 0
            errOld = 0

            saveControllerState()
         end
      elseif evt == "modem_message"
         and stateRequestChannel >= 0
         and stateRequestChannel == p2
         and tostring(p4) == "BR_turbine_get_state"
      then
         -- Send status informations
         local cLabel = os.getComputerLabel()
         if cLabel == nil then
            -- No label: use coputer number (ID) as tio create a name
            cLabel = "ComputerID:" .. tostring(os.getComputerID())
         end

         if turbine.getInductorEngaged ~= nil then
            inductorEngaged = turbine.getInductorEngaged()
         else
            inductorEngaged = nil
         end

         -- State structure
         local rState = {
            version = "Turbine V0.1", -- Version of data structure
            label = cLabel,
            computerId = os.getComputerID(),
            remoteControl = remoteControl,
            active = turbine.getActive(),
            tSpeed = tSpeed,
            energyStored = turbine.getEnergyStored(),
            rotorSpeed = turbine.getRotorSpeed(),
            inputAmount = turbine.getInputAmount(),
            inputType = turbine.getInputType(),
            outputAmount = turbine.getOutputAmount(),
            outputType = turbine.getOutputType(),
            fluidAmountMax = turbine.getFluidAmountMax(),
            fluidFlowRate = turbine.getFluidFlowRate(),
            fluidFlowRateMax = turbine.getFluidFlowRateMax(),
            fluidFlowRateMaxMax = turbine.getFluidFlowRateMaxMax(),
            energyProducedLastTick = turbine.getEnergyProducedLastTick(),
            inductorEngaged = inductorEngaged
         }

         peripheral.call(p1, "transmit", p3, stateRequestChannel,
                         textutils.serialize(rState))
      elseif evt == "modem_message"
         and remoteControl
         and p2 == os.getComputerID()
         and string.sub(tostring(p4), 1, 21) == "BR_turbine_set_speed:"
      then
         -- Remote Request: Speed change
         local oldTSpeed = tSpeed
         tSpeed = tonumber(string.sub(tostring(p4), 22))
         if (tSpeed == 0) then
            turbine.setActive(false)
            saveControllerState()            
         else
            turbine.setActive(true)
            floatRateChanged=true
         end

         if tSpeed ~= 0 and tSpeed ~= oldTSpeed then
            -- Initialize PID controller values
            if Kp == 0 then
               Kp = testSpeedup(turbine, loopT, tSpeed)
            end

            initControllerValues()

            errSum = 0
            errOld = 0

            saveControllerState()
         end

         if turbine.setInductorEngaged then
            -- engage coil by default
            turbine.setInductorEngaged(true)
         end
      elseif evt == "peripheral"
         and peripheral.getType(p1) == "modem"
         and stateRequestChannel >= 0
      then
         -- new modem connected
         peripheral.call(p1, "open", stateRequestChannel)
         peripheral.call(p1, "open", os.getComputerID())
      end

      -- exit loop on timer event or float rate change
   until (evt == "timer" and p1 == tID) or floatRateChanged
end

--
-- EOF
--
fs = {
  _STORAGE = {},
  _reset = function() fs._STORAGE = {} end,
  _write = function(path, content) fs._STORAGE[path] = content end,
  open = function(path, mode)
    local contents = ""
    return {
      write = function(content)
        assert(mode == "w", "Attempting to write mode of " .. mode)
        contents = contents .. content
      end,
      readAll = function()
        assert(mode == "r", "Attempting to read mode of " .. mode)
        return fs._STORAGE[path]
      end,
      close = function() fs._STORAGE[path] = contents end
    }
  end,
  exists = function(path) return fs._STORAGE[path] end,
  makeDir = function(path) fs._STORAGE[path] = {} end,
  combine = function(first, last) return first .. "/" .. last end,
  delete = function(path)
    for fullpath, contents in pairs(fs._STORAGE) do
      if string.find(fullpath, "^" .. path) then
        fs._STORAGE[fullpath] = nil
      end
    end
  end,
  move = function(from, to)
    for fullpath, contents in pairs(fs._STORAGE) do
      if string.find(fullpath, "^" .. from) then
        local newpath = to .. string.gsub(fullpath, "^" .. from, "")
        assert(not fs._STORAGE[newpath], newpath .. " already exists")
        fs._STORAGE[newpath] = fs._STORAGE[fullpath]
        fs._STORAGE[fullpath] = nil
      end
    end
  end
}
fs._read = fs.exists
fs._at = fs.exists

http = {
  _STORAGE = {},
  _reset = function() fs._STORAGE = {} end,
  _register = function(url, contents)
    http._STORAGE[url] = contents
  end,
  get = function(url)
    if not http._STORAGE[url] then error("Unknown url " .. url) end
    return {
      readAll = function() return http._STORAGE[url] end
    }
  end
}

shell = {
  _CURRENT = "/",
  setDir = function(path) shell._CURRENT = path end
}
require "mockcc"

require "gfs"

local function setup()
  fs._reset()
  http._reset()
end

local function testInstall()
  setup()
  http._register("https://api.github.com/gists/1", [=[
{
  "files": {
    "uno.lua": {
      "filename": "uno.lua",
      "content": "hi"
    },
    "dos.lua": {
      "filename": "dos.lua",
      "content": "bye"
    },
    "skip": {
      "filename": "skip",
      "content": "no"
    }
  }
}
]=])

  gfs.install("1", "one")

  assert(fs._at("/gists/one/id.txt"), "Missing the id file")
  assert(fs._read("/gists/one/id.txt") == "1", "The id file is incorrect: " .. fs._read("/gists/one/id.txt"))
  assert(fs._at("/gists/one/uno"), "Missing the first file")
  assert(fs._read("/gists/one/uno") == "hi", "The first file is incorrect: " .. fs._read("/gists/one/uno"))
  assert(fs._at("/gists/one/dos"), "Missing the second file")
  assert(fs._read("/gists/one/dos") == "bye", "The second file is incorrect: " .. fs._read("/gists/one/dos"))
  assert(not fs._at("/gists/one/skip"), "Did not skip non-Lua file")
end
testInstall()

local function testRefresh()
  setup()
  fs._write("/gists/two/id.txt", "2")
  fs._write("/gists/two/existing", "nice")
  fs._write("/gists/two/gone", "ouch")
  http._register("https://api.github.com/gists/2", [=[
{
  "files": {
    "new.lua": {
      "content": "sweet"
    },
    "existing.lua": {
      "content": "even better"
    }
  }
}
]=])

  gfs.refresh("two")

  assert(fs._at("/gists/two/id.txt"), "Missing the id file")
  assert(fs._read("/gists/two/id.txt") == "2", "The id file is incorrect: " .. fs._read("/gists/two/id.txt"))
  assert(fs._at("/gists/two/new"), "Missing the new file")
  assert(fs._read("/gists/two/new") == "sweet", "The new file is incorrect: " .. fs._read("/gists/two/new"))
  assert(fs._at("/gists/two/existing"), "Missing the existing file")
  assert(fs._read("/gists/two/existing") == "even better", "The existing file is incorrect: " .. fs._read("/gists/two/existing"))
  assert(not fs._at("/gists/two/gone"), "Did not remove the old file")
end
testRefresh()

local function testGo()
  setup()

  gfs.go("one")

  assert(shell._CURRENT == "/gists/one", "Incorrect current directory " .. shell._CURRENT)
end
testGo()

print("Passed")
--[[
Gist File System

Takes a Gist ID and loads all Lua files into a named directory.

Usage: gfs install <gist id> <name>
       gfs refresh <name>
--]]

gfs = {}

local path = "/gists"

function gfs.setup()
  if not fs.exists(path) then
    fs.makeDir(path)
  end
end

function gfs.install(id, name, installTo)
  if not id or not name then
    gfs.usage()
  end

  gfs.setup()

  local installPath = installTo or fs.combine(path, name)

  assert(not fs.exists(installPath), "A gist with that name is already installed")

  local body = http.get("https://api.github.com/gists/" .. id).readAll()
  local gist = json.decode(body)

  assert(gist.files, "File info not found")

  fs.makeDir(installPath)

  local handle = fs.open(fs.combine(installPath, "id.txt"), "w")
  handle.write(id)
  handle.close()

  for name, details in pairs(gist.files) do
    if string.find(name, "%.lua$") then
      local newName = string.gsub(name, "%.lua$", "")
      handle = fs.open(fs.combine(installPath, newName), "w")
      handle.write(details.content)
      handle.close()
    end
  end
end

function gfs.refresh(name)
  if not name then
    gfs.usage()
  end

  local installPath = fs.combine(path, name)
  local handle = fs.open(fs.combine(installPath, "id.txt"), "r")
  assert(handle, "Cannot open file " .. fs.combine(installPath, "id.txt"))
  local id = handle.readAll()
  assert(id, "Cannot read id.txt")

  local tmpPath = fs.combine(path, "_tmp")
  gfs.install(id, name, tmpPath)
  fs.delete(installPath)
  fs.move(tmpPath, installPath)
end

function gfs.go(name)
  if not name then
    gfs.usage()
  end

  shell.setDir(fs.combine(path, name))
end

local function build_json()
  -- Source: http://hg.prosody.im/trunk/file/tip/util/json.lua
  -- License: http://hg.prosody.im/trunk/file/tip/COPYING

  local t_insert = table.insert;
  local s_char = string.char;
  local tonumber = tonumber;
  local ipairs = ipairs;
  local error = error;
  local newproxy = newproxy;
  local print = print;

  local json = {};

  local null = newproxy and newproxy(true) or {};
  json.null = null;

  local escapes = {
    ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b",
    ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"};
  local unescapes = {
    ["\""] = "\"", ["\\"] = "\\", ["/"] = "/",
    b = "\b", f = "\f", n = "\n", r = "\r", t = "\t"};
  for i=0,31 do
    local ch = s_char(i);
    if not escapes[ch] then escapes[ch] = ("\\u%.4X"):format(i); end
  end

  function json.decode(json)
    json = json.." "; -- appending a space ensures valid json wouldn't touch EOF
    local pos = 1;
    local current = {};
    local stack = {};
    local ch, peek;
    local function next()
      ch = json:sub(pos, pos);
      if ch == "" then error("Unexpected EOF"); end
      pos = pos+1;
      peek = json:sub(pos, pos);
      return ch;
    end

    local function skipwhitespace()
      while ch and (ch == "\r" or ch == "\n" or ch == "\t" or ch == " ") do
        next();
      end
    end
    local function skiplinecomment()
      repeat next(); until not(ch) or ch == "\r" or ch == "\n";
      skipwhitespace();
    end
    local function skipstarcomment()
      next(); next(); -- skip '/', '*'
      while peek and ch ~= "*" and peek ~= "/" do next(); end
      if not peek then error("eof in star comment") end
      next(); next(); -- skip '*', '/'
      skipwhitespace();
    end
    local function skipstuff()
      while true do
        skipwhitespace();
        if ch == "/" and peek == "*" then
          skipstarcomment();
        elseif ch == "/" and peek == "/" then
          skiplinecomment();
        else
          return;
        end
      end
    end

    local readvalue;
    local function readarray()
      local t = {};
      next(); -- skip '['
      skipstuff();
      if ch == "]" then next(); return t; end
      t_insert(t, readvalue());
      while true do
        skipstuff();
        if ch == "]" then next(); return t; end
        if not ch then error("eof while reading array");
        elseif ch == "," then next();
        elseif ch then error("unexpected character in array, comma expected"); end
        if not ch then error("eof while reading array"); end
        t_insert(t, readvalue());
      end
    end

    local function checkandskip(c)
      local x = ch or "eof";
      if x ~= c then error("unexpected "..x..", '"..c.."' expected"); end
      next();
    end
    local function readliteral(lit, val)
      for c in lit:gmatch(".") do
        checkandskip(c);
      end
      return val;
    end
    local function readstring()
      local s = "";
      checkandskip("\"");
      while ch do
        while ch and ch ~= "\\" and ch ~= "\"" do
          s = s..ch; next();
        end
        if ch == "\\" then
          next();
          if unescapes[ch] then
            s = s..unescapes[ch];
            next();
          elseif ch == "u" then
            local seq = "";
            for i=1,4 do
              next();
              if not ch then error("unexpected eof in string"); end
              if not ch:match("[0-9a-fA-F]") then error("invalid unicode escape sequence in string"); end
              seq = seq..ch;
            end
            s = s..s.char(tonumber(seq, 16)); -- FIXME do proper utf-8
            next();
          else error("invalid escape sequence in string"); end
        end
        if ch == "\"" then
          next();
          return s;
        end
      end
      error("eof while reading string");
    end
    local function readnumber()
      local s = "";
      if ch == "-" then
        s = s..ch; next();
        if not ch:match("[0-9]") then error("number format error"); end
      end
      if ch == "0" then
        s = s..ch; next();
        if ch:match("[0-9]") then error("number format error"); end
      else
        while ch and ch:match("[0-9]") do
          s = s..ch; next();
        end
      end
      if ch == "." then
        s = s..ch; next();
        if not ch:match("[0-9]") then error("number format error"); end
        while ch and ch:match("[0-9]") do
          s = s..ch; next();
        end
        if ch == "e" or ch == "E" then
          s = s..ch; next();
          if ch == "+" or ch == "-" then
            s = s..ch; next();
            if not ch:match("[0-9]") then error("number format error"); end
            while ch and ch:match("[0-9]") do
              s = s..ch; next();
            end
          end
        end
      end
      return tonumber(s);
    end
    local function readmember(t)
      skipstuff();
      local k = readstring();
      skipstuff();
      checkandskip(":");
      t[k] = readvalue();
    end
    local function fixobject(obj)
      local __array = obj.__array;
      if __array then
        obj.__array = nil;
        for i,v in ipairs(__array) do
          t_insert(obj, v);
        end
      end
      local __hash = obj.__hash;
      if __hash then
        obj.__hash = nil;
        local k;
        for i,v in ipairs(__hash) do
          if k ~= nil then
            obj[k] = v; k = nil;
          else
            k = v;
          end
        end
      end
      return obj;
    end
    local function readobject()
      local t = {};
      next(); -- skip '{'
      skipstuff();
      if ch == "}" then next(); return t; end
      if not ch then error("eof while reading object"); end
      readmember(t);
      while true do
        skipstuff();
        if ch == "}" then next(); return fixobject(t); end
        if not ch then error("eof while reading object");
        elseif ch == "," then next();
        elseif ch then error("unexpected character in object, comma expected"); end
        if not ch then error("eof while reading object"); end
        readmember(t);
      end
    end

    function readvalue()
      skipstuff();
      while ch do
        if ch == "{" then
          return readobject();
        elseif ch == "[" then
          return readarray();
        elseif ch == "\"" then
          return readstring();
        elseif ch:match("[%-0-9%.]") then
          return readnumber();
        elseif ch == "n" then
          return readliteral("null", null);
        elseif ch == "t" then
          return readliteral("true", true);
        elseif ch == "f" then
          return readliteral("false", false);
        else
          error("invalid character at value start: "..ch);
        end
      end
      error("eof while reading value");
    end
    next();
    return readvalue();
  end

  return json;
end

json = build_json()

function gfs.usage()
  print([=[
Usage: gfs install <gist id> <name>
       gfs refresh <name>
       gfs go <name>
]=])
  error("Command not understood.")
end

if not debug or string.find(debug.getinfo(1).source, arg[0] .. "$") then
  local arg = {...}
  if gfs[arg[1]] then
    gfs[arg[1]](arg[2], arg[3])
  else
    gfs.usage()
  end
end
require "mockcc"

http._register("https://gist.github.com/zerosquadron/d36b66f4a99a54797402/raw/gfs.lua", "gfs body")

require "bootstrap"

assert(fs._at("/gfs"), "gfs program does not exist")
assert(fs._at("/gfs") == "gfs body", "gfs program has the wrong contents")

print "Passed"
local url = "https://gist.github.com/zerosquadron/d36b66f4a99a54797402/raw/gfs.lua"
local body = http.get(url).readAll()

local handle = fs.open("/gfs", "w")
handle.write(body)
handle.close()