From 80bb1986b296a9db37f14551de62be6c7bfe8640 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 30 Dec 2022 18:11:24 +0100 Subject: [PATCH] Added elevator-controller. --- elevator-controller.lua | 399 ++++++++++++++++++++++++++++++++++++++++ test.lua | 71 +++++++ 2 files changed, 470 insertions(+) create mode 100644 elevator-controller.lua create mode 100644 test.lua diff --git a/elevator-controller.lua b/elevator-controller.lua new file mode 100644 index 0000000..0a96d11 --- /dev/null +++ b/elevator-controller.lua @@ -0,0 +1,399 @@ +--[[ + Elevator Controller + +A script for an all-in-one elevator with floor selection, sounds, doors, and +more. +]] + +local SYSTEM_NAME = "Test Elevator" + +-- Floors, in order from bottom to top. +local FLOORS = { + { + label = "-1", + name = "Basement", + speaker = "speaker_2", + doorRedstone = "redstoneIntegrator_17", + contactRedstone = "redstoneIntegrator_18", + monitor = "monitor_4", + callMonitor = "monitor_5", + height = 25 + }, + { + label = "0", + name = "Ground Floor", + speaker = "speaker_1", + doorRedstone = "redstoneIntegrator_7", + contactRedstone = "redstoneIntegrator_6", + monitor = "monitor_1", + callMonitor = "monitor_3", + height = 58 + }, + { + label = "1", + name = "Mechanical Room", + speaker = "speaker_0", + doorRedstone = "redstoneIntegrator_4", + contactRedstone = "redstoneIntegrator_5", + monitor = "monitor_0", + callMonitor = "monitor_2", + height = 68 + } +} + +local FLOORS_BY_LABEL = {} +for _, floor in pairs(FLOORS) do + FLOORS_BY_LABEL[floor.label] = floor +end + +local FLOOR_LABELS_ORDERED = {} +for _, floor in pairs(FLOORS) do + table.insert(FLOOR_LABELS_ORDERED, floor.label) +end +table.sort(FLOOR_LABELS_ORDERED, function(lblA, lblB) return FLOORS_BY_LABEL[lblA].height < FLOORS_BY_LABEL[lblB].height end) + +local CONTROL_BASE_RPM = 16 +local CONTROL_MAX_RPM = 256 +local CONTROL_ANALOG_LEVEL_PER_RPM = 2 + +local CONTROL_DIRECTION_UP = true +local CONTROL_DIRECTION_DOWN = false +local CONTROL_REDSTONE = "redstoneIntegrator_13" + +local CURRENT_STATE = { + rpm = nil, + direction = nil +} + +local function openDoor(floor) + peripheral.call(floor.doorRedstone, "setOutput", "back", true) +end + +local function closeDoor(floor) + peripheral.call(floor.doorRedstone, "setOutput", "back", false) +end + +local function playChime(floor) + local speaker = peripheral.wrap(floor.speaker) + speaker.playNote("chime", 1, 18) + os.sleep(0.1) + speaker.playNote("chime", 1, 12) +end + +-- Converts an RPM speed to a blocks-per-second speed. +local function rpmToBps(rpm) + return (10 / 256) * rpm +end + +-- Sets the RPM of the elevator winch, and returns the true rpm that the system operates at. +local function setRpm(rpm) + if rpm == 0 then + peripheral.call(CONTROL_REDSTONE, "setOutput", "left", true) + return 0 + else + local analogPower = 0 + local trueRpm = 16 + while trueRpm < rpm do + analogPower = analogPower + CONTROL_ANALOG_LEVEL_PER_RPM + trueRpm = trueRpm * 2 + end + peripheral.call(CONTROL_REDSTONE, "setAnalogOutput", "right", analogPower) + peripheral.call(CONTROL_REDSTONE, "setOutput", "left", false) + return trueRpm + end +end + +-- Sets the speed of the elevator motion. +-- Positive numbers move the elevator up. +-- Zero sets the elevator as motionless. +-- Negative numbers move the elevator down. +-- The nearest possible RPM is used, via SPEEDS. +local function setSpeed(rpm) + if rpm == 0 then + if CURRENT_STATE.rpm ~= 0 then + CURRENT_STATE.rpm = setRpm(0) + -- print("Set RPM to " .. tostring(CURRENT_STATE.rpm)) + end + return + end + + if rpm > 0 then + peripheral.call(CONTROL_REDSTONE, "setOutput", "top", CONTROL_DIRECTION_UP) + CURRENT_STATE.direction = CONTROL_DIRECTION_UP + -- print("Set winch to UP") + elseif rpm < 0 then + peripheral.call(CONTROL_REDSTONE, "setOutput", "top", CONTROL_DIRECTION_DOWN) + CURRENT_STATE.direction = CONTROL_DIRECTION_DOWN + -- print("Set winch to DOWN") + end + + if math.abs(rpm) == CURRENT_STATE.rpm then return end + CURRENT_STATE.rpm = setRpm(math.abs(rpm)) + -- print("Set RPM to " .. tostring(CURRENT_STATE.rpm)) +end + +local function isFloorContactActive(floor) + return peripheral.call(floor.contactRedstone, "getInput", "back") +end + +-- Determines the label of the floor we're currently on. +-- We first check all known floors to see if the elevator is at one. +-- If that fails, the elevator is at an unknown position, so we move it as soon as possible to top. +local function determineCurrentFloorLabel() + for _, floor in pairs(FLOORS) do + local status = peripheral.call(floor.contactRedstone, "getInput", "back") + if status then return floor.label end + end + -- No floor found. Move the elevator to the top. + print("Elevator at unknown position, moving to top.") + local lastFloor = FLOORS[#FLOORS] + setSpeed(256) + local elapsedTime = 0 + while not isFloorContactActive(lastFloor) and elapsedTime < 10 do + os.sleep(1) + elapsedTime = elapsedTime + 1 + end + setSpeed(0) + if not isFloorContactActive(lastFloor) then + print("Timed out. Moving down until we hit the top floor.") + setSpeed(-1) + while not isFloorContactActive(lastFloor) do + -- Busy-wait until we hit the contact. + end + setSpeed(0) + end + return lastFloor.label +end + +-- Computes a series of keyframes describing the linear motion of the elevator. +local function computeLinearMotion(distance) + local preFrames = {} + local postFrames = {} + local intervalDuration = 0.25 + + local distanceToCover = distance + local rpmFactor = 1 + while rpmFactor * CONTROL_BASE_RPM < CONTROL_MAX_RPM do + --print("Need to cover " .. distanceToCover .. " more meters.") + local rpm = CONTROL_BASE_RPM * rpmFactor + local potentialDistanceCovered = 2 * intervalDuration * rpmToBps(rpm) + local nextRpmFactorDuration = (distanceToCover - potentialDistanceCovered) / rpmToBps(CONTROL_BASE_RPM * (rpmFactor + 1)) + --print("We'd cover " .. potentialDistanceCovered .. " by moving at " .. rpm .. " rpm for " .. intervalDuration .. " seconds twice.") + if potentialDistanceCovered <= distanceToCover and nextRpmFactorDuration >= 2 then + local frame = { + rpm = rpm, + duration = intervalDuration + } + table.insert(preFrames, frame) + table.insert(postFrames, 1, frame) + distanceToCover = distanceToCover - potentialDistanceCovered + rpmFactor = rpmFactor * 2 + elseif nextRpmFactorDuration < 2 then + break + end + end + + -- Cover the remaining distance with the next rpmFactor. + local finalRpm = CONTROL_BASE_RPM * rpmFactor + local finalDuration = distanceToCover / rpmToBps(finalRpm) + local finalFrame = { + rpm = finalRpm, + duration = finalDuration + } + local frames = {} + for _, frame in pairs(preFrames) do table.insert(frames, frame) end + table.insert(frames, finalFrame) + for _, frame in pairs(postFrames) do table.insert(frames, frame) end + return frames +end + +-- Moves the elevator from its current floor to the floor with the given label. +-- During this action, all user input is ignored. +local function goToFloor(floorLabel) + print("Going to floor " .. floorLabel) + local currentFloorLabel = determineCurrentFloorLabel() + if currentFloorLabel == floorLabel then return end + local currentFloor = FLOORS_BY_LABEL[currentFloorLabel] + local targetFloor = FLOORS_BY_LABEL[floorLabel] + local rpmDir = 1 + if targetFloor.height < currentFloor.height then + rpmDir = -1 + end + + local distance = math.abs(targetFloor.height - currentFloor.height) - 1 + local motionKeyframes = computeLinearMotion(distance) + playChime(currentFloor) + closeDoor(currentFloor) + for _, frame in pairs(motionKeyframes) do + local sleepTime = math.floor((frame.duration - 0.05) * 20) / 20 -- Make sure we round down to safely arrive before the detector. + if frame.rpm == CONTROL_MAX_RPM then + sleepTime = sleepTime - 0.05 -- For some reason at max RPM this is needed. + end + print("Running frame: rpm = " .. tostring(frame.rpm) .. ", dur = " .. tostring(sleepTime)) + setSpeed(rpmDir * frame.rpm) + os.sleep(sleepTime) + end + + -- On approach, slow down, wait for contact, then slowly align and stop. + setSpeed(rpmDir * 1) + print("Waiting for floor contact capture...") + local waited = false + while not isFloorContactActive(targetFloor) do + waited = true + end + print("Contact made.") + if waited then + print("Aligning...") + local alignmentDuration = 0.4 / rpmToBps(CONTROL_BASE_RPM) + os.sleep(alignmentDuration) + end + setSpeed(0) + print("Locked") + + playChime(targetFloor) + openDoor(targetFloor) +end + +local function initControls() + print("Initializing control system.") + setSpeed(0) + local currentFloorLabel = determineCurrentFloorLabel() + local currentFloor = FLOORS_BY_LABEL[currentFloorLabel] + for _, floor in pairs(FLOORS) do + openDoor(floor) + os.sleep(0.05) + closeDoor(floor) + end + openDoor(currentFloor) + print("Control system initialized.") +end + +--[[ + User Interface Section +]] + +local function drawText(monitor, x, y, text, fg, bg) + if fg ~= nil then + monitor.setTextColor(fg) + end + if bg ~= nil then + monitor.setBackgroundColor(bg) + end + monitor.setCursorPos(x, y) + monitor.write(text) +end + +local function drawTextCentered(monitor, x, y, text, fg, bg) + local w, h = monitor.getSize() + drawText(monitor, x - (string.len(text) / 2), y, text, fg, bg) +end + +local function clearLine(monitor, line, color) + monitor.setBackgroundColor(color) + monitor.setCursorPos(1, line) + monitor.clearLine() +end + +local function drawGui(floor, currentFloorLabel, destinationFloorLabel) + local monitor = peripheral.wrap(floor.monitor) + monitor.setTextScale(1) + monitor.setBackgroundColor(colors.black) + monitor.clear() + + local w, h = monitor.getSize() + clearLine(monitor, 1, colors.blue) + drawText(monitor, 1, 1, SYSTEM_NAME, colors.white, colors.blue) + + for i=1, #FLOOR_LABELS_ORDERED do + local label = FLOOR_LABELS_ORDERED[#FLOOR_LABELS_ORDERED - i + 1] + local floor = FLOORS_BY_LABEL[label] + local bg = colors.lightGray + if i % 2 == 0 then bg = colors.gray end + local line = i + 1 + clearLine(monitor, line, bg) + + local labelBg = bg + if label == currentFloorLabel and destinationFloorLabel == nil then + labelBg = colors.green + end + if label == destinationFloorLabel then + labelBg = colors.yellow + end + -- Format label with padding. + label = " " .. label + while string.len(label) < 3 do label = label .. " " end + drawText(monitor, 1, line, label, colors.white, labelBg) + + drawText(monitor, 4, line, floor.name, colors.white, bg) + end +end + +local function drawCallMonitorGui(floor, currentFloorLabel, destinationFloorLabel) + local monitor = peripheral.wrap(floor.callMonitor) + monitor.setTextScale(0.5) + monitor.setBackgroundColor(colors.white) + monitor.clear() + + local w, h = monitor.getSize() + if destinationFloorLabel == floor.label then + drawTextCentered(monitor, w/2, h/2, "Arriving", colors.green, colors.white) + elseif destinationFloorLabel ~= nil then + drawTextCentered(monitor, w/2, h/2, "In transit", colors.yellow, colors.white) + elseif floor.label == currentFloorLabel then + drawTextCentered(monitor, w/2, h/2, "Available", colors.green, colors.white) + else + drawTextCentered(monitor, w/2, h/2, "Call", colors.blue, colors.white) + end +end + +local function renderMonitors(currentFloorLabel, destinationFloorLabel) + for _, floor in pairs(FLOORS) do + drawGui(floor, currentFloorLabel, destinationFloorLabel) + drawCallMonitorGui(floor, currentFloorLabel, destinationFloorLabel) + end +end + +local function initUserInterface() + local currentFloorLabel = determineCurrentFloorLabel() + renderMonitors(currentFloorLabel, nil) +end + +local function listenForInput() + local event, peripheralId, x, y = os.pullEvent("monitor_touch") + for _, floor in pairs(FLOORS) do + if floor.monitor == peripheralId then + if y > 1 and y <= #FLOORS + 1 then + local floorIndex = #FLOOR_LABELS_ORDERED - (y - 1) + 1 + local label = FLOOR_LABELS_ORDERED[floorIndex] + print("y = " .. tostring(y) .. ", floorIndex = " .. floorIndex .. ", label = " .. label) + local currentFloorLabel = determineCurrentFloorLabel() + if label ~= currentFloorLabel then + renderMonitors(currentFloorLabel, label) + goToFloor(label) + renderMonitors(label, nil) + end + end + return + elseif floor.callMonitor == peripheralId then + local currentFloorLabel = determineCurrentFloorLabel() + if floor.label ~= currentFloorLabel then + renderMonitors(currentFloorLabel, floor.label) + goToFloor(floor.label) + renderMonitors(floor.label, nil) + end + return + end + end +end + +--[[ + Main Script Area. +]] + + + +initControls() +initUserInterface() +while true do + listenForInput() +end \ No newline at end of file diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..7a0a38e --- /dev/null +++ b/test.lua @@ -0,0 +1,71 @@ +local CONTROL_BASE_RPM = 16 +local CONTROL_MAX_RPM = 256 + +-- Converts an RPM speed to a blocks-per-second speed. +local function rpmToBps(rpm) + return (10 / 256) * rpm +end + +-- Computes a series of keyframes describing the linear motion of the elevator. +local function computeLinearMotion(distance) + local preFrames = {} + local postFrames = {} + local intervalDuration = 0.5 + + -- Linear motion calculation + local v1Dist = 2 * intervalDuration * rpmToBps(CONTROL_BASE_RPM) + local v2Dist = 2 * intervalDuration * rpmToBps(CONTROL_BASE_RPM * 2) + local v3Dist = 2 * intervalDuration * rpmToBps(CONTROL_BASE_RPM * 4) + local v4Dist = 2 * intervalDuration * rpmToBps(CONTROL_BASE_RPM * 8) + + local distanceToCover = distance + local rpmFactor = 1 + while rpmFactor * CONTROL_BASE_RPM < CONTROL_MAX_RPM do + print("Need to cover " .. distanceToCover .. " more meters.") + local rpm = CONTROL_BASE_RPM * rpmFactor + local potentialDistanceCovered = 2 * intervalDuration * rpmToBps(rpm) + local nextRpmFactorDuration = (distanceToCover - potentialDistanceCovered) / rpmToBps(CONTROL_BASE_RPM * (rpmFactor + 1)) + print("We'd cover " .. potentialDistanceCovered .. " by moving at " .. rpm .. " rpm for " .. intervalDuration .. " seconds twice.") + if potentialDistanceCovered <= distanceToCover and nextRpmFactorDuration >= 2 then + local frame = { + rpm = rpm, + duration = intervalDuration + } + table.insert(preFrames, frame) + table.insert(postFrames, 1, frame) + distanceToCover = distanceToCover - potentialDistanceCovered + rpmFactor = rpmFactor * 2 + elseif nextRpmFactorDuration < 2 then + break + end + end + + -- Cover the remaining distance with the next rpmFactor. + local finalRpm = CONTROL_BASE_RPM * rpmFactor + local finalDuration = distanceToCover / rpmToBps(finalRpm) + local finalFrame = { + rpm = finalRpm, + duration = finalDuration + } + local frames = {} + for _, frame in pairs(preFrames) do table.insert(frames, frame) end + table.insert(frames, finalFrame) + for _, frame in pairs(postFrames) do table.insert(frames, frame) end + return frames +end + +local function printFrames(frames) + for _, frame in pairs(frames) do + print("Frame: rpm = " .. tostring(frame.rpm) .. ", duration = " .. tostring(frame.duration)) + end +end + +local frames = computeLinearMotion(5) +printFrames(frames) +local dist = 0 +for _, frame in pairs(frames) do + dist = dist + rpmToBps(frame.rpm) * frame.duration +end +print(dist) + +print(0.15 % 0.05 == 0) \ No newline at end of file