diff --git a/FSE-ticket.png b/FSE-ticket.png deleted file mode 100644 index 0d51a09..0000000 Binary files a/FSE-ticket.png and /dev/null differ diff --git a/gate.lua b/gate.lua deleted file mode 100644 index 20d2b45..0000000 --- a/gate.lua +++ /dev/null @@ -1,1288 +0,0 @@ -local DEFAULT_SERVER_BASE = "http://ticket.fse-media.group" -local DEFAULT_SERVER_PATH = "/api/tickets/check" -local GATE_OPEN_SECONDS = 2 -local VERSION = "v1.5.7" - -local CONFIG_PATH = "gate_config.json" - -local function readFile(path) - if not fs.exists(path) then return nil end - local f = fs.open(path, "r") - if not f then return nil end - local c = f.readAll() - f.close() - return c -end - -local function writeFile(path, content) - local f = fs.open(path, "w") - if not f then return false end - f.write(content) - f.close() - return true -end - -local function trim(s) - return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) -end - -local function splitCsv(s) - local out = {} - s = trim(s) - if #s == 0 then return out end - for part in s:gmatch("[^,/%s]+") do - local v = trim(part) - if #v > 0 then table.insert(out, v) end - end - return out -end - -local pack = table.pack or function(...) - return { n = select("#", ...), ... } -end - -local function loadConfig() - local def = { mode = "entry", station_codes = {} } - local raw = readFile(CONFIG_PATH) - if not raw or #raw == 0 then return def end - local ok, data = pcall(textutils.unserializeJSON, raw) - if not ok or type(data) ~= "table" then return def end - if type(data.mode) == "string" then def.mode = data.mode end - if type(data.station_codes) == "table" then def.station_codes = data.station_codes end - if type(data.side_modes) == "table" then def.side_modes = data.side_modes end - if type(data.server_url) == "string" then def.server_url = data.server_url end - if type(data.card_server_url) == "string" then def.card_server_url = data.card_server_url end - if type(data.station_code) == "string" then def.station_code = data.station_code end - if type(data.side_station_codes) == "table" then def.side_station_codes = data.side_station_codes end - return def -end - -local function stationSetFromList(list) - local set = {} - if type(list) ~= "table" then return set end - for _, v in ipairs(list) do - local c = trim(v) - if #c > 0 then - local parts = splitCsv(c) - if #parts == 0 then - set[c] = true - else - for _, p in ipairs(parts) do set[p] = true end - end - end - end - return set -end - -local monitor = peripheral.find("monitor") -local speaker = peripheral.find("speaker") -local inspection = peripheral.find("ticket_inspection_machine") - -local serverConnected = nil -local serverLastChangeTs = 0 - -local function setServerConnected(ok) - if serverConnected == ok then return end - serverConnected = ok - serverLastChangeTs = os.epoch("utc") -end - -local termDev = term -if monitor then - pcall(monitor.setTextScale, 0.5) - termDev = monitor -end - -local function clear() - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(colors.white) - termDev.clear() - termDev.setCursorPos(1, 1) -end - -local function centerText(y, text, color) - local w = termDev.getSize() - termDev.setTextColor(color or colors.white) - local x = math.max(1, math.floor((w - #text) / 2) + 1) - termDev.setCursorPos(x, y) - termDev.write(text) -end - -local function drawServerStatusIndicator(w) - if w < 2 then return end - local col = colors.yellow - if serverConnected == true then col = colors.lime - elseif serverConnected == false then col = colors.red end - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(col) - termDev.setCursorPos(w - 1, 1) - termDev.write("S") - termDev.setCursorPos(w, 1) - termDev.write("*") - termDev.setTextColor(colors.white) -end - -local function drawVersionIndicator(w) - local s = tostring(VERSION or "") - if #s == 0 then return end - if w < #s then return end - termDev.setBackgroundColor(colors.black) - termDev.setTextColor(colors.gray) - termDev.setCursorPos(1, 1) - termDev.write(s) - termDev.setTextColor(colors.white) -end - -local function draw(statusLine1, statusLine2, statusColor) - clear() - local w, h = termDev.getSize() - centerText(1, "GATE", colors.cyan) - drawVersionIndicator(w) - drawServerStatusIndicator(w) - if statusLine1 and #statusLine1 > 0 then - centerText(math.max(2, math.floor(h / 2)), statusLine1, statusColor or colors.white) - end - if statusLine2 and #statusLine2 > 0 then - centerText(math.min(h, math.max(3, math.floor(h / 2) + 1)), statusLine2, statusColor or colors.white) - end - termDev.setCursorPos(1, h) - termDev.setTextColor(colors.gray) - termDev.write(string.rep(" ", w)) -end - -local function pulseLeftRedstone(seconds) - seconds = tonumber(seconds) or 1 - if not redstone or type(redstone.setOutput) ~= "function" then return end - pcall(redstone.setOutput, "left", true) - os.sleep(seconds) - pcall(redstone.setOutput, "left", false) -end - -pcall(function() - if redstone and type(redstone.setOutput) == "function" then - redstone.setOutput("left", false) - end -end) - -local function readApiEndpointFile(path) - local s = trim(readFile(path) or "") - if #s == 0 then return nil end - return s -end - -local function resolveServerURL(cfg) - if type(cfg.server_url) == "string" and #trim(cfg.server_url) > 0 then - local u = trim(cfg.server_url) - u = u:gsub("/api/tickets/status%s*$", "/api/tickets/check") - return u - end - - local base = readApiEndpointFile("API_ENDPOINT_GATE.txt") or readApiEndpointFile("API_ENDPOINT.txt") - if base and base:match("/api$") then - base = base:sub(1, -5) - end - if base and #base > 0 then - return base .. DEFAULT_SERVER_PATH - end - return DEFAULT_SERVER_BASE .. DEFAULT_SERVER_PATH -end - -local function guessBaseFromStatusURL(url) - url = trim(url or "") - if #url == 0 then return DEFAULT_SERVER_BASE end - local b = url:gsub("/api/tickets/check.*$", "") - b = b:gsub("/api/.*$", "") - b = trim(b) - if #b == 0 then return DEFAULT_SERVER_BASE end - return b -end - -local function httpRequest(method, url, body, headers) - if not http then - setServerConnected(false) - return false, "HTTP API disabled" - end - headers = headers or {} - local okReq, err = pcall(function() - http.request({ - url = url, - method = method, - headers = headers, - body = body, - }) - end) - if not okReq then - setServerConnected(false) - return false, tostring(err) - end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - setServerConnected(true) - return true, data - end - setServerConnected(false) - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - setServerConnected(false) - return false, data - end - setServerConnected(false) - return false, tostring(err or "http_failure") - end - os.queueEvent(ev, p1, p2, p3) - os.sleep(0) - end -end - -local function postCheck(url, payload) - local okBody, body = pcall(textutils.serializeJSON, payload) - if not okBody then return false end - local ok, data = httpRequest("POST", url, body, { ["Content-Type"] = "application/json" }) - if not ok then return false, data end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false, data end - return true, parsed -end - -local function getJSON(url) - local ok, data = httpRequest("GET", url) - if not ok then return false, data end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false, data end - return true, parsed -end - -local function resolveCardServerURL(cfg, ticketCheckURL) - if type(cfg.card_server_url) == "string" and #trim(cfg.card_server_url) > 0 then - return trim(cfg.card_server_url) - end - local base = guessBaseFromStatusURL(ticketCheckURL) - return base:gsub("/+$", "") .. "/api/cards/check" -end - -local function resolveCardSyncBaseURL(cfg, ticketCheckURL, cardCheckURL) - if type(cfg.card_sync_url) == "string" and #trim(cfg.card_sync_url) > 0 then - local raw = trim(cfg.card_sync_url) - return raw:gsub("/+$", "") - end - local base = guessBaseFromStatusURL(cardCheckURL or ticketCheckURL) - return base:gsub("/+$", "") .. "/api/ic-cards" -end - -local function resolveFareQueryURL(ticketCheckURL) - local base = guessBaseFromStatusURL(ticketCheckURL) - return base:gsub("/+$", "") .. "/api/public/fares/query" -end - -local function urlEncodeComponent(value) - local s = tostring(value or "") - return (s:gsub("([^%w%-_%.~])", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - -local function toMoney(v) - local n = tonumber(v) - if n == nil then return nil end - return math.floor(n * 100 + 0.5) / 100 -end - -local stationNameToCode = {} - -local function normKey(s) - return trim(s):lower() -end - -local function refreshStationNameMap(serverBase) - serverBase = trim(serverBase or "") - if #serverBase == 0 then return false end - local url = serverBase:gsub("/+$", "") .. "/api/stations" - local ok, data = httpRequest("GET", url) - if not ok then return false end - local okJ, parsed = pcall(textutils.unserializeJSON, data or "") - if not okJ then return false end - if type(parsed) == "table" and type(parsed.stations) == "table" then - parsed = parsed.stations - end - if type(parsed) ~= "table" then return false end - - stationNameToCode = {} - for _, st in ipairs(parsed) do - if type(st) == "table" then - local code = trim(st.code) - if #code > 0 then - local en = trim(st.en_name or st.en) - if #en > 0 then stationNameToCode[normKey(en)] = code end - local cn = trim(st.name) - if #cn > 0 then stationNameToCode[normKey(cn)] = code end - end - end - end - return true -end - -local function inferStationCodeFromName(name) - local key = normKey(name or "") - if #key == 0 then return "" end - return stationNameToCode[key] or "" -end - -local function playDfpwm(path) - if not speaker then return end - if not fs.exists(path) then return end - local okD, dfpwm = pcall(require, "cc.audio.dfpwm") - if not okD or not dfpwm then return end - local h = fs.open(path, "rb") - if not h then return end - local decoder = dfpwm.make_decoder() - while true do - local chunk = h.read(16 * 1024) - if not chunk then break end - local buf = decoder(chunk) - while not speaker.playAudio(buf) do - os.pullEvent("speaker_audio_empty") - end - end - h.close() -end - -local function normalizeTicketId(v) - v = tostring(v or "") - v = v:gsub("^%s+", ""):gsub("%s+$", "") - v = v:gsub("%s+", "") - if #v == 0 then return nil end - local prefix, num = v:match("^([A-Za-z][A-Za-z])%-?([0-9]+)$") - if prefix and num then - prefix = prefix:upper() - if #num < 8 then - num = string.rep("0", 8 - #num) .. num - elseif #num > 8 then - num = num:sub(-8) - end - return prefix .. "-" .. num - end - return v:lower() -end - -local function normalizeIcCardId(v) - local s = tostring(v or "") - s = s:gsub("%s+", ""):upper() - if #s == 0 then return "" end - local num = s:match("^IC%-?([0-9]+)$") - if num then - if #num < 6 then - num = string.rep("0", 6 - #num) .. num - elseif #num > 6 then - num = num:sub(-6) - end - return "IC-" .. num - end - return s -end - -local function collectScanTables(scan, includeTicket) - local out = {} - local seen = {} - local function add(v) - if type(v) ~= "table" or seen[v] then return end - seen[v] = true - table.insert(out, v) - end - if type(scan) ~= "table" then return out end - add(scan) - add(scan.data) - add(scan.payload) - add(scan.card) - add(scan.ic_card) - add(scan.wallet) - add(scan.card_data) - add(scan.media_data) - if includeTicket then - add(scan.ticket) - add(scan.ticket_data) - end - return out -end - -local function firstNonEmptyFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return "" end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - local v = t[key] - if v ~= nil then - local s = trim(v) - if #s > 0 then return s end - end - end - end - return "" -end - -local function firstNumberFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return nil end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - local n = tonumber(t[key]) - if n ~= nil then return n end - end - end - return nil -end - -local function isTruthy(v) - if v == true then return true end - if type(v) == "number" then return v ~= 0 end - if type(v) == "string" then - local s = v:lower() - return s == "true" or s == "1" or s == "yes" - end - return false -end - -local function firstTruthyFromTables(tables, keys) - if type(tables) ~= "table" or type(keys) ~= "table" then return false end - for _, t in ipairs(tables) do - for _, key in ipairs(keys) do - if t[key] ~= nil and isTruthy(t[key]) then - return true - end - end - end - return false -end - -local function getTicketId(scan) - local tables = collectScanTables(scan, true) - if #tables == 0 then return nil end - local raw = firstNonEmptyFromTables(tables, { - "ticketId", "ticket_id", "id", "ticketNo", "ticket_no", "code" - }) - if #raw == 0 then return nil end - return normalizeTicketId(raw) -end - - - -local function getCardId(scan) - local tables = collectScanTables(scan, false) - if #tables == 0 then return "" end - local raw = firstNonEmptyFromTables(tables, { - "card_id", - "cardId", - "ic_card_id", - "icCardId", - "wallet_id", - "walletId", - "card_uid", - "cardUid", - "uid", - "uuid", - "serial", - "serial_number", - "serialNumber", - "nfc_uid", - "nfcUid", - "rfid_uid", - "rfidUid" - }) - if #raw == 0 then return "" end - return normalizeIcCardId(raw) -end - -local function getCardBalance(scan) - local tables = collectScanTables(scan, false) - return firstNumberFromTables(tables, { - "balance", - "stored_value", - "storedValue", - "wallet_balance", - "walletBalance", - "remaining_balance", - "remainingBalance", - "value", - "amount" - }) -end - -local function isICCardScan(scan) - local tables = collectScanTables(scan, false) - if #tables == 0 then return false end - if #getCardId(scan) > 0 then return true end - if getCardBalance(scan) ~= nil then return true end - if type(scan.card) == "table" or type(scan.ic_card) == "table" or type(scan.wallet) == "table" then - return true - end - local media = firstNonEmptyFromTables(tables, { - "media", - "media_type", - "mediaType", - "product_type", - "productType", - "ticket_type", - "ticketType", - "kind", - "type", - "category" - }):lower() - if media:find("card", 1, true) or media:find("wallet", 1, true) then return true end - if media:find("ic", 1, true) or media:find("nfc", 1, true) or media:find("rfid", 1, true) then return true end - return false -end - -local function getStartStation(scan) - local tables = collectScanTables(scan, true) - local id = firstNonEmptyFromTables(tables, { - "entry", - "start_station", - "startStation", - "start", - "start_station_id", - "start_station_code", - "from_station", - "from", - "startStationId", - "start_stationId", - "entry_station", - "entryStation" - }) - if #id > 0 then return id end - return inferStationCodeFromName(firstNonEmptyFromTables(tables, { - "start_name_en", "startNameEn", "start_name", "fromNameCnU", "fromNameCn", "entry_name", "entryName" - })) -end - -local function getTerminalStation(scan) - local tables = collectScanTables(scan, true) - local id = firstNonEmptyFromTables(tables, { - "exit", - "terminal_station", - "terminalStation", - "terminal", - "end_station", - "endStation", - "terminal_station_id", - "terminal_station_code", - "to_station", - "to", - "endStationId", - "end_stationId", - "exit_station", - "exitStation" - }) - if #id > 0 then return id end - return inferStationCodeFromName(firstNonEmptyFromTables(tables, { - "terminal_name_en", "terminalNameEn", "terminal_name", "toNameCnU", "toNameCn", "exit_name", "exitName" - })) -end - - -local function saveLastScan(scan) - if type(scan) ~= "table" then return end - local t = {} - for k, v in pairs(scan) do t[k] = v end - local startStation = getStartStation(t) - if #startStation > 0 and (t.start_station == nil or trim(t.start_station) == "") then - t.start_station = startStation - end - local terminalStation = getTerminalStation(t) - if #terminalStation > 0 and (t.terminal_station == nil or trim(t.terminal_station) == "") then - t.terminal_station = terminalStation - end - - local ok, s = pcall(textutils.serializeJSON, t) - if not ok or type(s) ~= "string" then - ok, s = pcall(textutils.serialize, t) - end - if ok and type(s) == "string" then - writeFile("last_scan.json", s) - end -end - -local cfg = loadConfig() -local stationSet = stationSetFromList(cfg.station_codes) -local serverURL = resolveServerURL(cfg) -local cardServerURL = resolveCardServerURL(cfg, serverURL) -local cardSyncBaseURL = resolveCardSyncBaseURL(cfg, serverURL, cardServerURL) -local fareQueryURL = resolveFareQueryURL(serverURL) -local mode = (trim(cfg.mode):lower() == "exit") and "exit" or "entry" - -local modeBySide = nil -if type(cfg.side_modes) == "table" then - local tmp = {} - for side, m in pairs(cfg.side_modes) do - if type(side) == "string" then - local s = trim(side):lower() - if #s > 0 then - tmp[s] = (trim(m):lower() == "exit") and "exit" or "entry" - end - end - end - if next(tmp) ~= nil then modeBySide = tmp end -end - -local sideStationCodeBySide = nil -if type(cfg.side_station_codes) == "table" then - local tmp = {} - for side, code in pairs(cfg.side_station_codes) do - if type(side) == "string" then - local s = trim(side):lower() - local c = trim(code) - if #s > 0 and #c > 0 then - tmp[s] = c - end - end - end - if next(tmp) ~= nil then sideStationCodeBySide = tmp end -end - -pcall(function() - refreshStationNameMap(guessBaseFromStatusURL(serverURL)) -end) - -if not inspection then - if modeBySide == nil then - draw("Missing peripheral:", "ticket_inspection_machine", colors.red) - error("ticket_inspection_machine not found") - end -end - -if next(stationSet) == nil then - draw("No station codes set.", "Run installer first.", colors.red) - error("No station codes configured") -end - -local stationListText = table.concat(cfg.station_codes, ",") -local function readyLine1() - if not modeBySide then - return "Ready (" .. mode:upper() .. ")" - end - local f = modeBySide.front and modeBySide.front:upper() or "-" - local b = modeBySide.back and modeBySide.back:upper() or "-" - return "Ready (BI) F:" .. f .. " B:" .. b -end - -draw(readyLine1(), "Station: " .. stationListText, colors.lime) - -local stationCodesPayload = {} -for k, _ in pairs(stationSet) do table.insert(stationCodesPayload, k) end -table.sort(stationCodesPayload) - -local function trimSide(s) - s = trim(s or ""):lower() - if #s == 0 then return nil end - return s -end - -local function defaultStationCode() - local direct = trim(cfg.station_code or "") - if #direct > 0 then return direct end - return trim(stationCodesPayload[1] or "") -end - -local function stationCodeForSide(side) - side = trimSide(side) - if side and type(sideStationCodeBySide) == "table" then - local v = trim(sideStationCodeBySide[side] or "") - if #v > 0 then return v end - end - return defaultStationCode() -end - -local function isInspectionPeripheral(p) - return type(p) == "table" and ( - type(p.getLastScanned) == "function" - or type(p.updateICCard) == "function" - or type(p.updateTicket) == "function" - or type(p.destroyTicket) == "function" - ) -end - -local function resolveInspection(side) - side = trimSide(side) - if side and peripheral and type(peripheral.wrap) == "function" then - local okW, p = pcall(peripheral.wrap, side) - if okW and isInspectionPeripheral(p) then - return p - end - return nil - end - if isInspectionPeripheral(inspection) then return inspection end - return nil -end - -local function validateBidirectional() - if not modeBySide then return true end - for side, _ in pairs(modeBySide) do - if not resolveInspection(side) then - draw("Missing peripheral:", "ticket_inspection_machine@" .. tostring(side), colors.red) - error("ticket_inspection_machine not found on side: " .. tostring(side)) - end - end - return true -end - -validateBidirectional() - -local function inferSideFromScan(scan) - if type(scan) ~= "table" then return nil end - return trimSide( - scan.side - or scan.source_side - or scan.reader_side - or scan.peripheral_side - or scan.peripheralSide - or scan.device_side - or scan.peripheral - or scan.source - or scan.reader - or scan.name - ) -end - -local function isSideName(s) - s = trimSide(s) - if not s then return false end - return s == "front" or s == "back" or s == "left" or s == "right" or s == "top" or s == "bottom" -end - -local function parseTicketScannedArgsPacked(ev) - local side = nil - local scan = nil - if type(ev) ~= "table" then return nil, nil end - local n = tonumber(ev.n) or #ev - for i = 2, n do - local v = ev[i] - if not scan and type(v) == "table" then - scan = v - elseif not side and type(v) == "string" and isSideName(v) then - side = trimSide(v) - end - end - if scan and not side then side = inferSideFromScan(scan) end - return scan, side -end - -local function actionForSide(side) - if not modeBySide then return mode end - side = trimSide(side) - if not side then return mode end - return modeBySide[side] or mode -end - -local function collectInspectionDevices(side, modeBySideRef) - local sideKnown = trimSide(side) ~= nil - local inspectionDevs = {} - local function addDev(dev) - if not dev then return end - table.insert(inspectionDevs, dev) - end - if sideKnown then - addDev(resolveInspection(side)) - elseif modeBySideRef then - for s, _ in pairs(modeBySideRef) do - addDev(resolveInspection(s)) - end - else - addDev(resolveInspection(side)) - end - return inspectionDevs, sideKnown -end - -local function collectInspectionBindings(side, modeBySideRef, fallbackInspection) - local out = {} - local seen = {} - local function addBinding(sideName, dev) - if not dev or seen[dev] then return end - seen[dev] = true - table.insert(out, { side = trimSide(sideName), dev = dev }) - end - - side = trimSide(side) - if side then - addBinding(side, resolveInspection(side)) - elseif modeBySideRef then - for s, _ in pairs(modeBySideRef) do - addBinding(s, resolveInspection(s)) - end - else - addBinding(nil, fallbackInspection or resolveInspection(nil)) - end - return out -end - -local function updateDeviceField(dev, key, value) - if type(dev) ~= "table" or type(dev.updateTicket) ~= "function" then return end - pcall(dev.updateTicket, key, value) -end - -local function updateICCardField(dev, key, value) - if type(dev) ~= "table" or type(dev.updateICCard) ~= "function" then return false end - local okCall, okRes, detail = pcall(dev.updateICCard, key, value) - if not okCall then return false, tostring(okRes) end - if okRes == false then return false, tostring(detail or "update_failed") end - return true -end - -local function updateICCardFields(dev, patch) - local allOk = true - local firstErr = nil - if type(patch) ~= "table" then return false end - for key, value in pairs(patch) do - local okField, errField = updateICCardField(dev, key, value) - if not okField then - allOk = false - if not firstErr then - firstErr = tostring(key) .. ": " .. tostring(errField or "update_failed") - end - end - end - return allOk, firstErr -end - -local function readLastScanned(dev) - if type(dev) ~= "table" or type(dev.getLastScanned) ~= "function" then return nil end - local ok, scan = pcall(dev.getLastScanned) - if not ok or type(scan) ~= "table" then return nil end - return scan -end - -local function getCardEntry(scan) - return firstNonEmptyFromTables(collectScanTables(scan, false), { - "entry", "entry_station", "entryStation", "start_station", "startStation" - }) -end - -local function getCardEntered(scan) - if #getCardEntry(scan) > 0 then return true end - return firstTruthyFromTables(collectScanTables(scan, false), { - "entered", "is_entered", "in_station", "inside_station" - }) -end - -local function getCardExited(scan) - if #getCardEntry(scan) > 0 then return false end - return firstTruthyFromTables(collectScanTables(scan, false), { - "exited", "is_exited", "out_station", "outside_station" - }) -end - -local function getCardOwnerName(scan) - return firstNonEmptyFromTables(collectScanTables(scan, false), { - "ownerName", "owner_name", "holder_name", "card_holder", "passenger" - }) -end - -local function queryFare(fromStation, toStation) - fromStation = trim(fromStation) - toStation = trim(toStation) - if #fromStation == 0 or #toStation == 0 then - return nil, "missing_station" - end - local url = fareQueryURL - .. "?from=" .. urlEncodeComponent(fromStation) - .. "&to=" .. urlEncodeComponent(toStation) - local ok, resp = getJSON(url) - if not ok or type(resp) ~= "table" then - return nil, "net_error" - end - local fare = tonumber( - resp.discounted_regular_fare - or resp.discounted_regular - or resp.regular_fare - or resp["优惠后常规票价"] - or resp["常规票价"] - or resp.regular - ) - if fare == nil then - return nil, tostring(resp.error or resp["错误"] or "fare_not_found") - end - return toMoney(fare), nil -end - -local function denyCard(reason, detail) - draw("DENIED", tostring(detail or reason or "deny"), colors.red) - playDfpwm("error.dfpwm") -end - -local function deductICCardBalance(dev, amount) - if type(dev) ~= "table" or type(dev.deductICCard) ~= "function" then - return false, "unsupported_method" - end - local okCall, okRes, detail = pcall(dev.deductICCard, amount) - if not okCall then return false, tostring(okRes) end - if okRes == true then return true, tonumber(detail) end - return false, tostring(detail or "deduct_failed") -end - -local function syncICCardState(cardId, payload) - local id = trim(cardId) - if #id == 0 then return false, "missing_card_id" end - local url = cardSyncBaseURL .. "/" .. urlEncodeComponent(id) .. "/sync" - payload = payload or {} - payload.card_id = id - return postCheck(url, payload) -end - -local function handleICCardScan(scan, side, scanDev) - saveLastScan(scan) - local inspectionDevs = scanDev and { scanDev } or select(1, collectInspectionDevices(side, modeBySide, inspection)) - local sideKnown = trimSide(side) ~= nil - local action = actionForSide(side) - if modeBySide and not sideKnown then - if getCardEntered(scan) and not getCardExited(scan) then - action = "exit" - else - action = "entry" - end - end - - local cardId = getCardId(scan) - if #cardId == 0 then - draw("Invalid card.", "Missing card_id.", colors.red) - playDfpwm("error.dfpwm") - return - end - - local balance = toMoney(getCardBalance(scan) or 0) or 0 - local entryStation = trim(getCardEntry(scan)) - local exitStation = stationCodeForSide(side) - local fare = 0 - local usedAction = action - - if #exitStation == 0 then - denyCard("missing_station", "Missing gate station") - return - end - - if usedAction == "entry" then - if getCardEntered(scan) and not getCardExited(scan) then - denyCard("already_entered", "Already entered") - return - end - local okWrite = true - local writeErr = nil - for _, dev in ipairs(inspectionDevs) do - local okPatch, errPatch = updateICCardFields(dev, { - entry = exitStation, - }) - okWrite = okPatch and okWrite - if not okPatch and not writeErr then writeErr = errPatch end - end - if not okWrite then - draw("WRITE ERROR", tostring(writeErr or "Failed to update card"), colors.red) - playDfpwm("error.dfpwm") - return - end - entryStation = exitStation - else - if not getCardEntered(scan) then - denyCard("not_entered", "Not entered") - return - end - if getCardExited(scan) then - denyCard("already_exited", "Already exited") - return - end - if #entryStation == 0 then - denyCard("missing_entry_station", "Missing entry station") - return - end - local fareValue, fareErr = queryFare(entryStation, exitStation) - if fareValue == nil then - if fareErr == "net_error" then - draw("NET ERROR", "Fare lookup failed", colors.red) - playDfpwm("error.dfpwm") - else - denyCard("fare_not_found", fareErr) - end - return - end - fare = fareValue - if balance < fare then - denyCard("insufficient_balance", "Fare: " .. tostring(fare) .. " Bal: " .. tostring(balance)) - return - end - local okWrite = true - local writeErr = nil - for _, dev in ipairs(inspectionDevs) do - local okDeduct, newBalanceOrErr = deductICCardBalance(dev, fare) - if okDeduct then - balance = toMoney(newBalanceOrErr or (balance - fare)) or 0 - local okClear, clearErr = updateICCardField(dev, "entry", nil) - local okFare, fareErr = updateICCardField(dev, "last_fare", fare) - if not okClear or not okFare then - okWrite = false - if not okClear and not writeErr then writeErr = "entry: " .. tostring(clearErr or "update_failed") end - if not okFare and not writeErr then writeErr = "last_fare: " .. tostring(fareErr or "update_failed") end - end - else - okWrite = false - if tostring(newBalanceOrErr) == "insufficient" then - denyCard("insufficient_balance", "-" .. tostring(fare) .. " Left: " .. tostring(balance)) - else - draw("WRITE ERROR", tostring(newBalanceOrErr or "deduct_failed"), colors.red) - playDfpwm("error.dfpwm") - end - break - end - end - if not okWrite then - if writeErr then - draw("WRITE ERROR", tostring(writeErr), colors.red) - playDfpwm("error.dfpwm") - end - return - end - end - - for _, dev in ipairs(inspectionDevs) do - if usedAction == "entry" then - updateDeviceField(dev, "entered", true) - updateDeviceField(dev, "exited", false) - if #entryStation > 0 then updateDeviceField(dev, "entry_station", entryStation) end - else - updateDeviceField(dev, "exited", true) - updateDeviceField(dev, "entered", false) - if #exitStation > 0 then updateDeviceField(dev, "exit_station", exitStation) end - updateDeviceField(dev, "last_fare", fare) - end - if balance ~= nil then updateDeviceField(dev, "balance", balance) end - end - - local syncTs = (os.epoch and os.epoch("utc")) or (os.time() * 1000) - local okSync, syncResp = syncICCardState(cardId, { - type = "check", - action = usedAction, - device = "gate", - ts = syncTs, - station_code = exitStation, - entry_station = entryStation, - exit_station = usedAction == "exit" and exitStation or "", - entered = (usedAction == "entry"), - exited = (usedAction == "exit"), - fare = fare, - last_fare = fare, - balance = balance, - result = "pass" - }) - if okSync and type(syncResp) == "table" and type(syncResp.card) == "table" then - balance = toMoney(syncResp.card.balance or balance) or balance - end - - local line2 = nil - if usedAction == "exit" then - line2 = "-" .. tostring(fare) .. " Left: " .. tostring(balance or "?") - else - line2 = "Left: " .. tostring(balance or "?") - end - draw("PASS", line2, colors.lime) - parallel.waitForAll( - function() pulseLeftRedstone(GATE_OPEN_SECONDS) end, - function() playDfpwm("pass.dfpwm") end - ) -end - -local function handleScan(scan, side, scanDev) - saveLastScan(scan) - local inspectionDevs = scanDev and { scanDev } or select(1, collectInspectionDevices(side, modeBySide, inspection)) - local sideKnown = trimSide(side) ~= nil - local action = actionForSide(side) - if modeBySide and not sideKnown then - if isTruthy(scan and scan.entered) and not isTruthy(scan and scan.exited) then - action = "exit" - else - action = "entry" - end - end - - local ticketId = getTicketId(scan) - if not ticketId then - draw("Invalid ticket.", "Missing ticketId.", colors.red) - playDfpwm("error.dfpwm") - return - end - - local hintTripsTotal = tonumber(scan.trips_total or scan.rides_total or scan.trips or scan.rides) - local hintTripsRemaining = tonumber(scan.trips_remaining or scan.rides_remaining) - - local function doCheck(act) - return postCheck(serverURL, { - ticket_id = ticketId, - action = act, - station_codes = stationCodesPayload, - station_code = stationCodeForSide(side), - device = "gate", - ts = os.epoch("utc"), - trips_total = hintTripsTotal, - trips_remaining = hintTripsRemaining, - }) - end - - local ok, resp = doCheck(action) - local usedAction = action - if ok and type(resp) == "table" and resp.result ~= "pass" and tostring(resp.reason) == "wrong_station" and modeBySide and not sideKnown then - local alt = (action == "entry") and "exit" or "entry" - local ok2, resp2 = doCheck(alt) - if ok2 and type(resp2) == "table" then - if resp2.result == "pass" then - ok, resp = ok2, resp2 - usedAction = alt - elseif tostring(resp2.reason) ~= "wrong_station" then - ok, resp = ok2, resp2 - usedAction = alt - end - end - end - - if not ok or type(resp) ~= "table" then - draw("NET ERROR", "Server check failed.", colors.red) - playDfpwm("error.dfpwm") - return - end - - if resp.result ~= "pass" then - draw("DENIED", tostring(resp.reason or "deny"), colors.red) - playDfpwm("error.dfpwm") - return - end - - pcall(function() - if #inspectionDevs == 0 then return end - local newRides = tonumber(resp.trips_remaining) - or tonumber(scan.trips_remaining or scan.rides_remaining) - or tonumber(scan.rides) - for _, dev in ipairs(inspectionDevs) do - if type(dev) == "table" and type(dev.updateTicket) == "function" then - if usedAction == "entry" then - dev.updateTicket("entered", true) - dev.updateTicket("exited", false) - if newRides ~= nil then dev.updateTicket("rides", newRides) end - else - dev.updateTicket("exited", true) - dev.updateTicket("entered", false) - if newRides ~= nil then dev.updateTicket("rides", newRides) end - end - end - end - end) - - local remaining = tonumber(resp.trips_remaining) - if usedAction == "exit" and isTruthy(resp.destroy_ticket) and remaining ~= nil and remaining <= 0 then - for _, dev in ipairs(inspectionDevs) do - if type(dev) == "table" and type(dev.destroyTicket) == "function" then - pcall(dev.destroyTicket) - end - end - end - - local msg = (usedAction == "exit") - and ("Rides left: " .. tostring(resp.trips_remaining or "")) - or "Welcome." - - draw("PASS", msg, colors.lime) - parallel.waitForAll( - function() pulseLeftRedstone(GATE_OPEN_SECONDS) end, - function() playDfpwm("pass.dfpwm") end - ) -end - -local recentScans = {} - -local function buildScanKey(scan, side, eventName) - side = trimSide(side) or "-" - if eventName == "ic_card_scanned" or isICCardScan(scan) then - return table.concat({ - side, - "ic", - getCardId(scan), - tostring(getCardBalance(scan) or ""), - getCardEntry(scan), - getCardOwnerName(scan), - }, "|") - end - return table.concat({ - side, - "ticket", - tostring(getTicketId(scan) or ""), - tostring(scan.timestamp or scan.order_datetime or ""), - tostring(scan.rides or scan.trips or ""), - tostring(scan.entered or ""), - tostring(scan.exited or ""), - }, "|") -end - -local function shouldProcessScan(scan, side, eventName) - local key = buildScanKey(scan, side, eventName) - local now = os.epoch("utc") - local prev = recentScans[key] - recentScans[key] = now - if prev and (now - prev) < 500 then - return false - end - return true -end - -local function matchesEventType(scan, eventName) - if eventName == "ic_card_scanned" then return isICCardScan(scan) end - if eventName == "ticket_scanned" then return not isICCardScan(scan) end - return true -end - -local function processInspectionEvent(eventName, ev) - local payloadScan, payloadSide = parseTicketScannedArgsPacked(ev) - local bindings = collectInspectionBindings(payloadSide, modeBySide, inspection) - local handled = false - - for _, binding in ipairs(bindings) do - local scan = readLastScanned(binding.dev) - if type(scan) == "table" and matchesEventType(scan, eventName) and shouldProcessScan(scan, binding.side, eventName) then - if eventName == "ic_card_scanned" or isICCardScan(scan) then - handleICCardScan(scan, binding.side, binding.dev) - else - handleScan(scan, binding.side, binding.dev) - end - handled = true - end - end - - if not handled and type(payloadScan) == "table" and shouldProcessScan(payloadScan, payloadSide, eventName) then - local payloadDev = nil - if payloadSide then - payloadDev = resolveInspection(payloadSide) - elseif #bindings == 1 then - payloadDev = bindings[1].dev - elseif not modeBySide then - payloadDev = inspection - end - if eventName == "ic_card_scanned" or isICCardScan(payloadScan) then - handleICCardScan(payloadScan, payloadSide, payloadDev) - else - handleScan(payloadScan, payloadSide, payloadDev) - end - end -end - -while true do - local ev = pack(os.pullEvent()) - if ev[1] == "ticket_scanned" or ev[1] == "ic_card_scanned" then - processInspectionEvent(ev[1], ev) - os.sleep(0.35) - draw(readyLine1(), "Station: " .. stationListText, colors.lime) - end -end diff --git a/install_refillmachine.lua b/install_refillmachine.lua deleted file mode 100644 index f7d909f..0000000 --- a/install_refillmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local errMsg = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - errMsg = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(errMsg or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Refill Machine Installer") -print("") -print("Downloading refill machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup", code, false) then - print("Write failed: startup") - return -end -atomicWrite("startup.lua", code, false) -atomicWrite("refillmachine.lua", code, false) - -print("") -print("Done.") -print("Reboot the computer to start the refill machine.") diff --git a/install_ticketmachine.lua b/install_ticketmachine.lua deleted file mode 100644 index 361f019..0000000 --- a/install_ticketmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/ticketmachine.lua?sign=UIiheDcpyzwKdovDZM3G-8IRZqApFgU2Kpnhe9k5ETQ=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(err or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Ticket Machine Installer") -print("") -print("Downloading ticket machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup", code, false) then - print("Write failed: startup") - return -end -atomicWrite("startup.lua", code, false) -if fs.exists("ticketmachine.lua") then atomicWrite("ticketmachine.lua", code, false) end - -print("") -print("Done.") -print("Reboot the computer to start the ticket machine.") diff --git a/installer_bi.lua b/installer_bi.lua deleted file mode 100644 index 5c44dfc..0000000 --- a/installer_bi.lua +++ /dev/null @@ -1,164 +0,0 @@ -local URL_ERROR = "http://cloud.fse-media.group/d/API/TicketMachine/error.dfpwm?sign=DvQ39wQDN6Cej4KPhAkM3JyXPM466koan6w9CckPxWU=:0" -local URL_PASS = "http://cloud.fse-media.group/d/API/TicketMachine/pass.dfpwm?sign=ZJfGDYnaxQU4JrGSh4NDzKZtjq3eMjYh9KHmMSAMKZA=:0" -local URL_GATE = "http://cloud.fse-media.group/d/API/TicketMachine/gate.lua?sign=OWy1wKkKhUhpnxXKeX6fRLePSg1XcaQgWOLvQbMuHRQ=:0" - -local CONFIG_PATH = "gate_config.json" - -local function trim(s) - return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) -end - -local function splitCsv(s) - local out = {} - s = trim(s) - if #s == 0 then return out end - for part in s:gmatch("[^,/%s]+") do - local v = trim(part) - if #v > 0 then table.insert(out, v) end - end - return out -end - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function prompt(label) - term.write(label) - return trim(read() or "") -end - -local function promptOptionalUrl(label) - local raw = prompt(label) - raw = trim(raw) - if #raw == 0 then return nil end - return raw -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local err = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - err = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(err or "http_failure") - end - end -end - -local function download(url, path, binary) - print("Downloading: " .. path) - local ok, data = httpGet(url) - if not ok then - print("Download failed: " .. tostring(data or "")) - return false - end - if not writeFile(path, data, binary) then - print("Write failed: " .. path) - return false - end - return true -end - -local function normalizeMode(raw) - return (trim(raw):lower() == "exit") and "exit" or "entry" -end - -term.clear() -term.setCursorPos(1, 1) -print("Bidirectional Ticket Gate Installer") -print("") - -local stationsRaw = prompt("Station codes (comma or slash): ") -local stationCodes = splitCsv(stationsRaw) -if #stationCodes == 0 then - print("No station codes provided.") - return -end - -print("") -print("Set mode for each side (front/back).") -local frontRaw = prompt("Front mode (entry/exit): ") -local backRaw = prompt("Back mode (entry/exit): ") -local frontMode = normalizeMode(frontRaw) -local backMode = normalizeMode(backRaw) -local frontStationCode = trim(prompt("Front station code (default first code): ")) -local backStationCode = trim(prompt("Back station code (default first code): ")) -if #frontStationCode == 0 then frontStationCode = stationCodes[1] end -if #backStationCode == 0 then backStationCode = stationCodes[1] end - -print("") -print("Optional server config for ticket / IC card checks.") -local ticketServerUrl = promptOptionalUrl("Ticket check URL (blank=auto): ") -local cardServerUrl = promptOptionalUrl("IC card check URL (blank=same server): ") - -local cfg = { - station_codes = stationCodes, - station_code = stationCodes[1], - side_modes = { - front = frontMode, - back = backMode, - }, - side_station_codes = { - front = frontStationCode, - back = backStationCode, - } -} -if ticketServerUrl then cfg.server_url = ticketServerUrl end -if cardServerUrl then cfg.card_server_url = cardServerUrl end -local okCfg, cfgJson = pcall(textutils.serializeJSON, cfg) -if not okCfg then - print("Config serialize failed.") - return -end -if not writeFile(CONFIG_PATH, cfgJson, false) then - print("Failed to write config.") - return -end - -if not download(URL_PASS, "pass.dfpwm", true) then return end -if not download(URL_ERROR, "error.dfpwm", true) then return end - -local okGate, gateCode = httpGet(URL_GATE) -if not okGate then - print("Download failed: startup") - return -end - -writeFile("startup.lua", gateCode, false) -writeFile("startup", gateCode, false) - -print("") -print("Done.") -print("This gate now supports tickets and IC cards.") -print("Attach ticket_inspection_machine on FRONT and BACK.") -print("Reboot the computer to start the gate.") diff --git a/update_refillmachine.lua b/update_refillmachine.lua deleted file mode 100644 index ff9cf20..0000000 --- a/update_refillmachine.lua +++ /dev/null @@ -1,81 +0,0 @@ -local URL_MACHINE_HTTPS = "https://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" -local URL_MACHINE_HTTP = "http://cloud.fse-media.group/d/API/TicketMachine/refillmachine.lua?sign=msZNBgD27qgGTwdeHlUxB25Q58386ZjDqorYurXiKqI=:0" - -local function writeFile(path, content, binary) - local mode = binary and "wb" or "w" - local f = fs.open(path, mode) - if not f then return false end - f.write(content) - f.close() - return true -end - -local function atomicWrite(path, content, binary) - local tmp = path .. ".new" - if fs.exists(tmp) then fs.delete(tmp) end - if not writeFile(tmp, content, binary) then return false end - if fs.exists(path) then fs.delete(path) end - fs.move(tmp, path) - return true -end - -local function httpGet(url) - if not http then return false, "HTTP API disabled" end - local okReq, err = pcall(function() - http.request({ url = url, method = "GET" }) - end) - if not okReq then return false, tostring(err) end - - while true do - local ev, p1, p2, p3 = os.pullEvent() - if ev == "http_success" and p1 == url then - local res = p2 - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return true, data - end - return false, "invalid http response" - end - if ev == "http_failure" and p1 == url then - local errMsg = p2 - local res = p3 - if type(p2) == "table" and type(p2.readAll) == "function" then - res = p2 - errMsg = p3 - end - if type(res) == "table" and type(res.readAll) == "function" then - local data = res.readAll() - res.close() - return false, data - end - return false, tostring(errMsg or "http_failure") - end - end -end - -term.clear() -term.setCursorPos(1, 1) -print("Refill Machine Updater") -print("") -print("Downloading refill machine program...") - -local ok, code = httpGet(URL_MACHINE_HTTPS) -if not ok then - ok, code = httpGet(URL_MACHINE_HTTP) -end -if not ok or type(code) ~= "string" or #code == 0 then - print("Download failed: " .. tostring(code or "")) - return -end - -if not atomicWrite("startup.lua", code, false) then - print("Write failed: startup.lua") - return -end -atomicWrite("startup", code, false) -atomicWrite("refillmachine.lua", code, false) - -print("") -print("Done.") -print("Reboot the computer to apply the update.")