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