Added elevator-controller.
This commit is contained in:
parent
cf10db1b7f
commit
80bb1986b2
|
@ -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
|
|
@ -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)
|
Loading…
Reference in New Issue