Advertisement
HandieAndy

elevator-controller.lua

Dec 29th, 2022 (edited)
830
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.39 KB | None | 0 0
  1. --[[
  2.     Elevator Controller
  3.  
  4. A script for an all-in-one elevator with floor selection, sounds, doors, and
  5. more.
  6.  
  7. Requires `pcm-reader.lua` installed as to be required with `require("pcm")`
  8. ]]
  9.  
  10. local pcm = require("pcm")
  11.  
  12. -- Load floors from elevator settings.
  13. local FLOORS = {}
  14. local floorsFile = io.open("floors.tbl", "r") or error("Missing floors.tbl")
  15. FLOORS = textutils.unserialize(floorsFile:read("a"))
  16. floorsFile:close()
  17. table.sort(FLOORS, function(fA, fB) return fA.height < fB.height end)
  18.  
  19. local FLOORS_BY_LABEL = {}
  20. for _, floor in pairs(FLOORS) do
  21.     FLOORS_BY_LABEL[floor.label] = floor
  22. end
  23.  
  24. local FLOOR_LABELS_ORDERED = {}
  25. for _, floor in pairs(FLOORS) do
  26.     table.insert(FLOOR_LABELS_ORDERED, floor.label)
  27. end
  28. table.sort(FLOOR_LABELS_ORDERED, function(lblA, lblB) return FLOORS_BY_LABEL[lblA].height < FLOORS_BY_LABEL[lblB].height end)
  29.  
  30. local settingsFile = io.open("settings.tbl", "r") or error("Missing settings.tbl")
  31. local settings = textutils.unserialize(settingsFile:read("a"))
  32.  
  33. local SYSTEM_NAME = settings.systemName
  34.  
  35. local CONTROL_BASE_RPM = settings.control.baseRpm
  36. local CONTROL_MAX_RPM = settings.control.maxRpm
  37. local CONTROL_ANALOG_LEVEL_PER_RPM = settings.control.analogLevelPerRpm
  38.  
  39. local CONTROL_DIRECTION_UP = settings.control.directionUp
  40. local CONTROL_DIRECTION_DOWN = settings.control.directionDown
  41. local CONTROL_REDSTONE = settings.control.redstone
  42.  
  43. local CURRENT_STATE = {
  44.     rpm = nil,
  45.     direction = nil
  46. }
  47.  
  48. local STATS_FILE = "elevator-stats.tbl"
  49.  
  50. -- Records statistics about an elevator trip.
  51. local function recordTrip(fromFloorLabel, toFloorLabel)
  52.     local file = io.open(STATS_FILE, "r")
  53.     local stats = {}
  54.     if file ~= nil then
  55.         stats = textutils.unserialize(file:read("a"))
  56.         file:close()
  57.     end
  58.     if stats[fromFloorLabel] == nil then
  59.         local floor = FLOORS_BY_LABEL[fromFloorLabel]
  60.         stats[fromFloorLabel] = {
  61.             departures = 0,
  62.             arrivals = 0,
  63.             name = floor.name
  64.         }
  65.     end
  66.     if stats[toFloorLabel] == nil then
  67.         local floor = FLOORS_BY_LABEL[toFloorLabel]
  68.         stats[toFloorLabel] = {
  69.             departures = 0,
  70.             arrivals = 0,
  71.             name = floor.name
  72.         }
  73.     end
  74.     stats[fromFloorLabel].departures = stats[fromFloorLabel].departures + 1
  75.     stats[toFloorLabel].arrivals = stats[toFloorLabel].arrivals + 1
  76.     file = io.open(STATS_FILE, "w")
  77.     if file == nil then
  78.         print("Error: Couldn't save stats to " .. STATS_FILE)
  79.     else
  80.         file:write(textutils.serialize(stats))
  81.         file:close()
  82.     end
  83. end
  84.  
  85. local function openDoor(floor)
  86.     peripheral.call(floor.redstone, "setOutput", "left", true)
  87. end
  88.  
  89. local function closeDoor(floor)
  90.     peripheral.call(floor.redstone, "setOutput", "left", false)
  91. end
  92.  
  93. local function playChime(floor)
  94.     local speaker = peripheral.wrap(floor.speaker)
  95.     speaker.playNote("chime", 1, 18)
  96.     os.sleep(0.1)
  97.     speaker.playNote("chime", 1, 12)
  98. end
  99.  
  100. -- Converts an RPM speed to a blocks-per-second speed.
  101. local function rpmToBps(rpm)
  102.     return (10 / 256) * rpm
  103. end
  104.  
  105. -- Sets the RPM of the elevator winch, and returns the true rpm that the system operates at.
  106. local function setRpm(rpm)
  107.     if rpm == 0 then
  108.         peripheral.call(CONTROL_REDSTONE, "setOutput", "left", true)
  109.         return 0
  110.     else
  111.         local analogPower = 0
  112.         local trueRpm = 16
  113.         while trueRpm < rpm do
  114.             analogPower = analogPower + CONTROL_ANALOG_LEVEL_PER_RPM
  115.             trueRpm = trueRpm * 2
  116.         end
  117.         peripheral.call(CONTROL_REDSTONE, "setAnalogOutput", "right", analogPower)
  118.         peripheral.call(CONTROL_REDSTONE, "setOutput", "left", false)
  119.         return trueRpm
  120.     end
  121. end
  122.  
  123. -- Sets the speed of the elevator motion.
  124. -- Positive numbers move the elevator up.
  125. -- Zero sets the elevator as motionless.
  126. -- Negative numbers move the elevator down.
  127. -- The nearest possible RPM is used, via SPEEDS.
  128. local function setSpeed(rpm)
  129.     if rpm == 0 then
  130.         if CURRENT_STATE.rpm ~= 0 then
  131.             CURRENT_STATE.rpm = setRpm(0)
  132.             -- print("Set RPM to " .. tostring(CURRENT_STATE.rpm))
  133.         end
  134.         return
  135.     end
  136.    
  137.     if rpm > 0 then
  138.         peripheral.call(CONTROL_REDSTONE, "setOutput", "top", CONTROL_DIRECTION_UP)
  139.         CURRENT_STATE.direction = CONTROL_DIRECTION_UP
  140.         -- print("Set winch to UP")
  141.     elseif rpm < 0 then
  142.         peripheral.call(CONTROL_REDSTONE, "setOutput", "top", CONTROL_DIRECTION_DOWN)
  143.         CURRENT_STATE.direction = CONTROL_DIRECTION_DOWN
  144.         -- print("Set winch to DOWN")
  145.     end
  146.  
  147.     if math.abs(rpm) == CURRENT_STATE.rpm then return end
  148.     CURRENT_STATE.rpm = setRpm(math.abs(rpm))
  149.     -- print("Set RPM to " .. tostring(CURRENT_STATE.rpm))
  150. end
  151.  
  152. local function isFloorContactActive(floor)
  153.     return peripheral.call(floor.redstone, "getInput", "back")
  154. end
  155.  
  156. -- Determines the label of the floor we're currently on.
  157. -- We first check all known floors to see if the elevator is at one.
  158. -- If that fails, the elevator is at an unknown position, so we move it as soon as possible to top.
  159. local function determineCurrentFloorLabel()
  160.     for _, floor in pairs(FLOORS) do
  161.         local status = isFloorContactActive(floor)
  162.         if status then return floor.label end
  163.     end
  164.     -- No floor found. Move the elevator to the top.
  165.     print("Elevator at unknown position, moving to top.")
  166.     local lastFloor = FLOORS[#FLOORS]
  167.     setSpeed(256)
  168.     local elapsedTime = 0
  169.     while not isFloorContactActive(lastFloor) and elapsedTime < 10 do
  170.         os.sleep(1)
  171.         elapsedTime = elapsedTime + 1
  172.     end
  173.     setSpeed(0)
  174.     if not isFloorContactActive(lastFloor) then
  175.         print("Timed out. Moving down until we hit the top floor.")
  176.         setSpeed(-1)
  177.         while not isFloorContactActive(lastFloor) do
  178.             -- Busy-wait until we hit the contact.
  179.         end
  180.         setSpeed(0)
  181.     end
  182.     return lastFloor.label
  183. end
  184.  
  185. -- Computes a series of keyframes describing the linear motion of the elevator.
  186. local function computeLinearMotion(distance)
  187.     local preFrames = {}
  188.     local postFrames = {}
  189.     local intervalDuration = 0.25
  190.  
  191.     local distanceToCover = distance
  192.     local rpmFactor = 1
  193.     while rpmFactor * CONTROL_BASE_RPM < CONTROL_MAX_RPM do
  194.         --print("Need to cover " .. distanceToCover .. " more meters.")
  195.         local rpm = CONTROL_BASE_RPM * rpmFactor
  196.         local potentialDistanceCovered = 2 * intervalDuration * rpmToBps(rpm)
  197.         local nextRpmFactorDuration = (distanceToCover - potentialDistanceCovered) / rpmToBps(CONTROL_BASE_RPM * (rpmFactor + 1))
  198.         --print("We'd cover " .. potentialDistanceCovered .. " by moving at " .. rpm .. " rpm for " .. intervalDuration .. " seconds twice.")
  199.         if potentialDistanceCovered <= distanceToCover and nextRpmFactorDuration >= 2 then
  200.             local frame = {
  201.                 rpm = rpm,
  202.                 duration = intervalDuration
  203.             }
  204.             table.insert(preFrames, frame)
  205.             table.insert(postFrames, 1, frame)
  206.             distanceToCover = distanceToCover - potentialDistanceCovered
  207.             rpmFactor = rpmFactor * 2
  208.         elseif nextRpmFactorDuration < 2 then
  209.             break
  210.         end
  211.     end
  212.  
  213.     -- Cover the remaining distance with the next rpmFactor.
  214.     local finalRpm = CONTROL_BASE_RPM * rpmFactor
  215.     local finalDuration = distanceToCover / rpmToBps(finalRpm)
  216.     local finalFrame = {
  217.         rpm = finalRpm,
  218.         duration = finalDuration
  219.     }
  220.     local frames = {}
  221.     for _, frame in pairs(preFrames) do table.insert(frames, frame) end
  222.     table.insert(frames, finalFrame)
  223.     for _, frame in pairs(postFrames) do table.insert(frames, frame) end
  224.     return frames
  225. end
  226.  
  227. -- Moves the elevator from its current floor to the floor with the given label.
  228. -- During this action, all user input is ignored.
  229. local function goToFloor(floorLabel)
  230.     print("Going to floor " .. floorLabel)
  231.     local currentFloorLabel = determineCurrentFloorLabel()
  232.     if currentFloorLabel == floorLabel then return end
  233.     local currentFloor = FLOORS_BY_LABEL[currentFloorLabel]
  234.     local targetFloor = FLOORS_BY_LABEL[floorLabel]
  235.     local rpmDir = 1
  236.     if targetFloor.height < currentFloor.height then
  237.         rpmDir = -1
  238.     end
  239.  
  240.     local distance = math.abs(targetFloor.height - currentFloor.height) - 1
  241.     local motionKeyframes = computeLinearMotion(distance)
  242.     closeDoor(currentFloor)
  243.     local audioFile = "audio/going-up.pcm"
  244.     if rpmDir == -1 then audioFile = "audio/going-down.pcm" end
  245.     local speaker = peripheral.wrap(currentFloor.speaker)
  246.     pcm.playFile(speaker, audioFile)
  247.     for _, frame in pairs(motionKeyframes) do
  248.         local sleepTime = math.floor((frame.duration - 0.05) * 20) / 20 -- Make sure we round down to safely arrive before the detector.
  249.         if frame.rpm == CONTROL_MAX_RPM then
  250.             sleepTime = sleepTime - 0.05 -- For some reason at max RPM this is needed.
  251.         end
  252.         print("Running frame: rpm = " .. tostring(frame.rpm) .. ", dur = " .. tostring(sleepTime))
  253.         setSpeed(rpmDir * frame.rpm)
  254.         os.sleep(sleepTime)
  255.     end
  256.  
  257.     -- On approach, slow down, wait for contact, then slowly align and stop.
  258.     setSpeed(rpmDir * 1)
  259.     print("Waiting for floor contact capture...")
  260.     local waited = false
  261.     while not isFloorContactActive(targetFloor) do
  262.         waited = true
  263.     end
  264.     print("Contact made.")
  265.     if waited then
  266.         print("Aligning...")
  267.         local alignmentDuration = 0.4 / rpmToBps(CONTROL_BASE_RPM)
  268.         os.sleep(alignmentDuration)
  269.     end
  270.     setSpeed(0)
  271.     print("Locked")
  272.  
  273.     playChime(targetFloor)
  274.     openDoor(targetFloor)
  275.     recordTrip(currentFloorLabel, floorLabel)
  276. end
  277.  
  278. local function initControls()
  279.     print("Initializing control system.")
  280.     setSpeed(0)
  281.     local currentFloorLabel = determineCurrentFloorLabel()
  282.     local currentFloor = FLOORS_BY_LABEL[currentFloorLabel]
  283.     for _, floor in pairs(FLOORS) do
  284.         openDoor(floor)
  285.         os.sleep(0.05)
  286.         closeDoor(floor)
  287.     end
  288.     openDoor(currentFloor)
  289.     print("Control system initialized.")
  290. end
  291.  
  292. --[[
  293.     User Interface Section
  294. ]]
  295.  
  296. local function drawText(monitor, x, y, text, fg, bg)
  297.     if fg ~= nil then
  298.         monitor.setTextColor(fg)
  299.     end
  300.     if bg ~= nil then
  301.         monitor.setBackgroundColor(bg)
  302.     end
  303.     monitor.setCursorPos(x, y)
  304.     monitor.write(text)
  305. end
  306.  
  307. local function drawTextCentered(monitor, x, y, text, fg, bg)
  308.     local w, h = monitor.getSize()
  309.     drawText(monitor, x - (string.len(text) / 2), y, text, fg, bg)
  310. end
  311.  
  312. local function clearLine(monitor, line, color)
  313.     monitor.setBackgroundColor(color)
  314.     monitor.setCursorPos(1, line)
  315.     monitor.clearLine()
  316. end
  317.  
  318. local function drawGui(floor, currentFloorLabel, destinationFloorLabel)
  319.     local monitor = peripheral.wrap(floor.monitor)
  320.     monitor.setTextScale(1)
  321.     monitor.setBackgroundColor(colors.black)
  322.     monitor.clear()
  323.  
  324.     local w, h = monitor.getSize()
  325.     clearLine(monitor, 1, colors.blue)
  326.     drawText(monitor, 1, 1, SYSTEM_NAME, colors.white, colors.blue)
  327.  
  328.     for i=1, #FLOOR_LABELS_ORDERED do
  329.         local label = FLOOR_LABELS_ORDERED[#FLOOR_LABELS_ORDERED - i + 1]
  330.         local floor = FLOORS_BY_LABEL[label]
  331.         local bg = colors.lightGray
  332.         if i % 2 == 0 then bg = colors.gray end
  333.         local line = i + 1
  334.         clearLine(monitor, line, bg)
  335.  
  336.         local labelBg = bg
  337.         if label == currentFloorLabel and destinationFloorLabel == nil then
  338.             labelBg = colors.green
  339.         end
  340.         if label == destinationFloorLabel then
  341.             labelBg = colors.yellow
  342.         end
  343.         -- Format label with padding.
  344.         label = " " .. label
  345.         while string.len(label) < 3 do label = label .. " " end
  346.         drawText(monitor, 1, line, label, colors.white, labelBg)
  347.  
  348.         drawText(monitor, 4, line, floor.name, colors.white, bg)
  349.     end
  350. end
  351.  
  352. local function drawCallMonitorGui(floor, currentFloorLabel, destinationFloorLabel)
  353.     local monitor = peripheral.wrap(floor.callMonitor)
  354.     monitor.setTextScale(0.5)
  355.     monitor.setBackgroundColor(colors.white)
  356.     monitor.clear()
  357.  
  358.     local w, h = monitor.getSize()
  359.     if destinationFloorLabel == floor.label then
  360.         drawTextCentered(monitor, w/2, h/2, "Arriving", colors.green, colors.white)
  361.     elseif destinationFloorLabel ~= nil then
  362.         drawTextCentered(monitor, w/2, h/2, "In transit", colors.yellow, colors.white)
  363.     elseif floor.label == currentFloorLabel then
  364.         drawTextCentered(monitor, w/2, h/2, "Available", colors.green, colors.white)
  365.     else
  366.         drawTextCentered(monitor, w/2, h/2, "Call", colors.blue, colors.white)
  367.     end
  368. end
  369.  
  370. local function renderMonitors(currentFloorLabel, destinationFloorLabel)
  371.     for _, floor in pairs(FLOORS) do
  372.         drawGui(floor, currentFloorLabel, destinationFloorLabel)
  373.         drawCallMonitorGui(floor, currentFloorLabel, destinationFloorLabel)
  374.     end
  375. end
  376.  
  377. local function initUserInterface()
  378.     local currentFloorLabel = determineCurrentFloorLabel()
  379.     renderMonitors(currentFloorLabel, nil)
  380. end
  381.  
  382. local function listenForInput()
  383.     local event, peripheralId, x, y = os.pullEvent("monitor_touch")
  384.     for _, floor in pairs(FLOORS) do
  385.         if floor.monitor == peripheralId then
  386.             if y > 1 and y <= #FLOORS + 1 then
  387.                 local floorIndex = #FLOOR_LABELS_ORDERED - (y - 1) + 1
  388.                 local label = FLOOR_LABELS_ORDERED[floorIndex]
  389.                 print("y = " .. tostring(y) .. ", floorIndex = " .. floorIndex .. ", label = " .. label)
  390.                 local currentFloorLabel = determineCurrentFloorLabel()
  391.                 if label ~= currentFloorLabel then
  392.                     renderMonitors(currentFloorLabel, label)
  393.                     goToFloor(label)
  394.                     renderMonitors(label, nil)
  395.                 end
  396.             end
  397.             return
  398.         elseif floor.callMonitor == peripheralId then
  399.             local currentFloorLabel = determineCurrentFloorLabel()
  400.             if floor.label ~= currentFloorLabel then
  401.                 renderMonitors(currentFloorLabel, floor.label)
  402.                 goToFloor(floor.label)
  403.                 renderMonitors(floor.label, nil)
  404.             end
  405.             return
  406.         end
  407.     end
  408. end
  409.  
  410. --[[
  411.     Main Script Area.
  412. ]]
  413.  
  414. initControls()
  415. initUserInterface()
  416. while true do
  417.     listenForInput()
  418. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement