Advertisement
Guest User

elevator.lua

a guest
Jan 19th, 2017
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.48 KB | None | 0 0
  1. -- Copyright (c) 2017 angryoptimist
  2. -- (email: "copyright" at domain "angryoptimist.info" or "angryoptimist.net")
  3. --
  4. -- Permission is hereby granted, free of charge, to any person obtaining
  5. -- a copy of this software and associated documentation files (the
  6. -- "Software"), to deal in the Software without restriction, including
  7. -- without limitation the rights to use, copy, modify, merge, publish,
  8. -- distribute, sublicense, and/or sell copies of the Software, and to
  9. -- permit persons to whom the Software is furnished to do so, subject to
  10. -- the following conditions:
  11. --
  12. -- The above copyright notice and this permission notice shall be included
  13. -- in all copies or substantial portions of the Software.
  14. --
  15. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  18. -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  19. -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  20. -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  21. -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. --
  23.  
  24. PROTO_DISCOVER=1
  25. PROTO_DISCOVER_ACK=2
  26. PROTO_UPDATE=3
  27.  
  28. UPDATE_STATE=1
  29. UPDATE_SEND=2
  30. UPDATE_CHANGE=3
  31. UPDATE_QUIT=4
  32. UPDATE_REDISCOVER=5
  33.  
  34. DISCOVERY_TIMEOUT=10
  35.  
  36. --##############
  37. -- Shared stuff
  38. --##############
  39.  
  40.  
  41. function member_of(some_table, possible_member)
  42.   for k,v in pairs(some_table) do
  43.     if possible_member == v then
  44.       return true
  45.     end
  46.   end
  47.   return false
  48. end
  49.  
  50.  
  51. function trim(some_string)
  52.   return some_string:gsub("^%s+",""):gsub("%s+$","")
  53. end
  54.  
  55.  
  56. function filter_proto_recv(proto, timeout)
  57.   local addr = nil
  58.   local raw = nil
  59.   local msg = nil
  60.   local extra = nil
  61.   while true do
  62.     if timeout ~= nil then
  63.       addr, raw, extra = rednet.receive(timeout)
  64.     else
  65.       addr, raw, extra = rednet.receive()
  66.     end
  67.     if addr == nil then
  68.       return nil
  69.     else
  70.       msg = deserialize_packet(addr, raw)
  71.     end
  72.     if msg["proto"] == proto then
  73.       return msg
  74.     end
  75.   end
  76. end
  77.  
  78.  
  79. function deserialize_packet(addr, raw)
  80.   local msg = textutils.unserialize(raw)
  81.   msg["addr"] = addr
  82.   return msg
  83. end
  84.  
  85.  
  86. -- Initialization functions
  87.  
  88. function load_settings(settings, filepath)
  89.   if not fs.exists(filepath) then
  90.     return false
  91.   end
  92.   local h = fs.open(filepath, "r")
  93.   local serialized = h.readAll()
  94.   h.close()
  95.   local settings_in = textutils.unserialize(serialized)
  96.   for key,val in pairs(settings_in) do
  97.     settings[key] = val
  98.   end
  99.   return true
  100. end
  101.  
  102.  
  103. function save_settings(settings, filepath)
  104.   if fs.exists(filepath) then
  105.     return false
  106.   end
  107.   local serialized = textutils.serialize(settings)
  108.   local h = fs.open(filepath, "w")
  109.   h.write(serialized)
  110.   h.close()
  111.   return true
  112. end
  113.  
  114.  
  115. --##############
  116. -- Server stuff
  117. --##############
  118.  
  119. Server = { _settings_path="/server.dat" }
  120.  
  121. function Server:new(modem_side, expected_floors)
  122.   o = {}
  123.   setmetatable(o, self)
  124.   self.__index = self
  125.   o.state={ elevator={ floors={}, cart=nil } }
  126.   o.layout={ num=0, maxwidth=0, seq={}, map={} }
  127.   o.settings={}
  128.   o:_initialize_settings(modem_side, expected_floors)
  129.   return o
  130. end
  131.  
  132. function Server:_initialize()
  133.   rednet.open(self.settings["modem"])
  134.   print("Discovering floors...")
  135.   self:discover()
  136.   print(string.format("Found %s floors.", self.layout["num"]))
  137.   self:_initialize_state()
  138.   self:blast_state()
  139. end
  140.  
  141. function Server:_initialize_state()
  142.   self.state["elevator"] = { floors={}, cart=nil }
  143.   self.state["elevator"]["cart"] = self.layout["seq"][1]
  144.   for n,floor in ipairs(self.layout.seq) do
  145.     if floor == self.state.elevator.cart then
  146.       self.state.elevator.floors[floor] = true
  147.     else
  148.       self.state.elevator.floors[floor] = false
  149.     end
  150.   end
  151. end
  152.  
  153. function Server:_initialize_settings(modem_side, expected_floors)
  154.   if modem_side ~= nil then
  155.     assert(expected_floors ~= nil)
  156.     self.settings["modem"] = modem_side
  157.     self.settings["floors"] = expected_floors
  158.     save_settings(self.settings, self._settings_path)
  159.   else
  160.     assert(fs.exists(self._settings_path))
  161.     load_settings(self.settings, self._settings_path)
  162.   end
  163. end
  164.  
  165. function Server:_discover_client()
  166.   local msg = filter_proto_recv(PROTO_DISCOVER_ACK, DISCOVERY_TIMEOUT)
  167.   if msg == nil then
  168.     return nil
  169.   end
  170.   local client_addr = msg["addr"]
  171.   local floor = msg["floor"]
  172.   local order = msg["order"]
  173.   if member_of(self.layout["seq"], floor) then
  174.     print(string.format("Floor '%s' already exists "..
  175.                         "(reg: %s/req: %s)",
  176.                         floor, layout["map"][floor], client_addr))
  177.     return nil
  178.   end
  179.   self.layout["map"][floor] = client_addr
  180.   table.insert(self.layout["seq"], floor)
  181.   self.layout["num"] = self.layout["num"] + 1
  182.   self.layout["maxwidth"] = math.max(self.layout["maxwidth"],
  183.                                      string.len(string.format("%s", floor)))
  184.   return client_addr, floor, order
  185. end
  186.  
  187. function Server:discover(tries)
  188.   self.layout["num"] = 0
  189.   self.layout["maxwidth"] = 0
  190.   self.layout["seq"] = {}
  191.   self.layout["map"] = {}
  192.   local fail = 0
  193.   local max_fails = tries or 3
  194.   local addr, floor, seq = nil, nil, nil
  195.   local seq_map = {}
  196.   local packet={ proto=PROTO_DISCOVER }
  197.   print("Pinging clients...")
  198.   rednet.broadcast(textutils.serialize(packet))
  199.   print("Listing for replies...")
  200.   while (self.layout["num"] == 0 or fail < max_fails) and
  201.         (self.layout["num"] < self.settings["floors"]) do
  202.     addr, floor, seq = self:_discover_client()
  203.     if addr == nil then
  204.       if self.layout["num"] > 0 then
  205.         fail = fail + 1
  206.         print(string.format("Timeout.  %d retries remaining.",
  207.                             max_fails - fail))
  208.       end
  209.       print("No client responses recieved yet...")
  210.       rednet.broadcast(textutils.serialize(packet))
  211.     else
  212.       seq_map[floor] = seq
  213.       print(string.format("Found floor '%s' at '%s'",
  214.                           floor, addr))
  215.     end
  216.   end
  217.   -- Sort in reverse order, since we'll list in that order
  218.   table.sort(self.layout["seq"],
  219.              function(x,y) return seq_map[x]>seq_map[y] end)
  220. end
  221.  
  222. function Server:blast_state()
  223.   local packet = { proto=PROTO_UPDATE,
  224.                    update=UPDATE_STATE,
  225.                    state=self.state.elevator,
  226.                    order=self.layout["seq"],
  227.                    maxwidth=self.layout.maxwidth,
  228.                    num=self.layout.num }
  229.   print("Broadcasting state packet...")
  230.   local serialized = textutils.serialize(packet)
  231.   --rednet.broadcast(serialized)
  232.   for floor, addr in pairs(self.layout.map) do
  233.     rednet.send(addr, serialized)
  234.   end
  235. end
  236.  
  237. function Server:send_changes(changes)
  238.   local packet = { proto=PROTO_UPDATE,
  239.                    update=UPDATE_CHANGE,
  240.                    changes=changes }
  241.   rednet.broadcast(textutils.serialize(packet))
  242.   for floor, state in pairs(changes) do
  243.     print(string.format("Set floor '%s' to %s", floor, tostring(state)))
  244.     self.state.elevator.floors[floor] = state
  245.   end
  246. end
  247.  
  248. function Server:move_cart(to_floor)
  249.   print("Move cart to "..to_floor)
  250.   local current=self.state.elevator.cart
  251.   if to_floor == current then
  252.     print("Cart already at "..to_floor)
  253.     return
  254.   end
  255.   local changes = {}
  256.   changes[to_floor] = true
  257.   changes[current] = false
  258.   self:send_changes(changes)
  259.   self.state.elevator.cart=to_floor
  260. end
  261.  
  262. function Server:run()
  263.   self:_initialize()
  264.   while true do
  265.     msg = filter_proto_recv(PROTO_UPDATE)
  266.     if msg.update == UPDATE_SEND then
  267.       print(string.format("Recieved request to send cart to floor '%s'...",
  268.                           msg.floor))
  269.       self:move_cart(msg.floor)
  270.       print("Requested changes bounced to clients.")
  271.     elseif msg.update == UPDATE_QUIT then
  272.       print("Recieved request to quit.")
  273.       rednet.broadcast(textutils.serialize(msg))
  274.       print("Told clients to quit.")
  275.       break
  276.     else
  277.       print(string.format("Got unrecongized update type '%s'", msg.update))
  278.     end
  279.   end
  280.   print("Exiting.")
  281. end
  282.  
  283.  
  284. --##############
  285. -- Client stuff
  286. --##############
  287.  
  288. Client = { _settings_path="/client.dat",
  289.            screen_bg=colors.black,
  290.            text_bg=colors.lightGray,
  291.            text_fg=colors.black,
  292.            sel_bg=colors.blue,
  293.            sel_fg=colors.white }
  294.  
  295. function Client:new(order, floor, modem_side, redstone_side, monitor_side)
  296.   o = {}
  297.   setmetatable(o, self)
  298.   self.__index = self
  299.   o.state={ floors={}, active=true, server=nil }
  300.   o.layout={ num=0, maxwidth=0, columns=0, maxheight=0,
  301.              width=0, height=0, lmargin=0, rmargin=0,
  302.              draw = {}, order = {} }
  303.   o.settings={}
  304.   o:_initialize_settings(order, floor, modem_side, redstone_side, monitor_side)
  305.   return o
  306. end
  307.  
  308. function Client:_initialize()
  309.   rednet.open(self.settings["modem"])
  310.   print("Discovering server...")
  311.   self:discover()
  312. end
  313.  
  314. function Client:_initialize_state(packet)
  315.   self.state.floors = packet.state.floors
  316.   self.layout.maxwidth = packet.maxwidth
  317.   self.layout.num = packet.num
  318.   self.layout.order = packet.order
  319.   self.monitor = peripheral.wrap(self.settings["monitor"])
  320.   self:_recalculate_layout()
  321. end
  322.  
  323. function Client:_initialize_settings(order, floor,
  324.                                      modem_side, redstone_side,
  325.                                      monitor_side)
  326.   if order ~= nil then
  327.     assert(floor ~= nil and modem_side ~= nil and
  328.            redstone_side ~= nil and monitor_side ~= nil)
  329.     self.settings["order"] = order
  330.     self.settings["floor"] = floor
  331.     self.settings["modem"] = modem_side
  332.     self.settings["redstone"] = redstone_side
  333.     self.settings["monitor"] = monitor_side
  334.     save_settings(self.settings, self._settings_path)
  335.   else
  336.     assert(fs.exists(self._settings_path))
  337.     load_settings(self.settings, self._settings_path)
  338.   end
  339. end
  340.  
  341. function Client:discover()
  342.   print("Listening for server...")
  343.   local msg = filter_proto_recv(PROTO_DISCOVER)
  344.   self.state.server = msg["addr"]
  345.   print(string.format("Server found, addr:%s", msg["addr"]))
  346.   local packet = { proto = PROTO_DISCOVER_ACK,
  347.                    floor = self.settings.floor,
  348.                    order = self.settings.order }
  349.   rednet.send(msg["addr"], textutils.serialize(packet))
  350.   print("Identified self to server.")
  351.   print(string.format("Found server at addr:%s", self.state.server))
  352.   print("Awaiting state packet...")
  353.   local msg = filter_proto_recv(PROTO_UPDATE)
  354.   print("Got state packet.")
  355.   self:_initialize_state(msg)
  356.   print(string.format("Recieved state from server."))
  357.   redstone.setOutput(self.settings.redstone, self.state.active)
  358. end
  359.  
  360. function Client:_recalculate_layout()
  361.   self.monitor = peripheral.wrap(self.settings["monitor"])
  362.   -- Initialize values
  363.   self.layout.lmargin, self.layout.rmargin = 0, 0
  364.   self.layout.width, self.layout.height = self.monitor.getSize()
  365.   -- Calculate number of columns
  366.   if (self.layout.num > self.layout.height) then
  367.     self.layout.columns = math.ceil((self.layout.width + 1)/(self.layout.maxwidth+1))
  368.   else
  369.     self.layout.columns = 1
  370.   end
  371.   self.layout.maxheight=math.ceil(self.layout.num/self.layout.columns)
  372.   -- Not implementing scrollbars (yet)--freak out if stuff doesn't fit
  373.   assert(self.layout.maxheight <= self.layout.height)
  374.   -- Now to figure out margins (if any)...
  375.   local used_width = ((self.layout.columns * (self.layout.maxwidth)) +
  376.                       (self.layout.columns - 1))
  377.   self.layout.lmargin=math.ceil((self.layout.width%used_width)/2)
  378.   self.layout.rmargin=(self.layout.width%used_width)-self.layout.lmargin
  379.   -- Now to build the arrangement of columns
  380.   self.layout.draw = {}
  381.   local column = nil
  382.   local fmt_str = "%"..self.layout.maxwidth.."s"
  383.   local floor = nil
  384.   for i=1,self.layout.num,self.layout.maxheight do
  385.     column = {}
  386.     for j=i,i+self.layout.maxheight-1 do
  387.       floor = self.layout.order[j]
  388.       if self.state.floors[floor] ~= nil then
  389.         table.insert(column, string.format(fmt_str, floor))
  390.       else
  391.         break
  392.       end
  393.     end
  394.     table.insert(self.layout.draw, column)
  395.   end
  396. end
  397.  
  398. function Client:draw()
  399.   self.monitor.setBackgroundColor(self.screen_bg)
  400.   self.monitor.clear()
  401.   self:redraw()
  402. end
  403.  
  404. function Client:fulldraw()
  405.   self:_recalculate_layout()
  406.   self:draw()
  407. end
  408.  
  409. function Client:redraw()
  410.   local index = nil
  411.   local floor = nil
  412.   local x,y = 0,0
  413.   local offset = nil
  414.   for i,column in ipairs(self.layout.draw) do
  415.     x = 1 + self.layout.lmargin + (i-1)*(self.layout.maxwidth+1) + (i-1)
  416.     offset = (i-1)*self.layout.maxheight
  417.     for j,label in ipairs(column) do
  418.       y = j
  419.       index = offset + j
  420.       floor = self.layout.order[index]
  421.       if self.state.floors[floor] then
  422.         self.monitor.setBackgroundColor(self.sel_bg)
  423.         self.monitor.setTextColor(self.sel_fg)
  424.       else
  425.         self.monitor.setBackgroundColor(self.text_bg)
  426.         self.monitor.setTextColor(self.text_fg)
  427.       end
  428.       self.monitor.setCursorPos(x,y)
  429.       self.monitor.write(label)
  430.     end
  431.   end
  432. end
  433.  
  434. function Client:_index_from_point(x,y)
  435.   -- because it's the harder part, first we find the column
  436.   local divisor = self.layout.maxwidth+1
  437.   local column = math.ceil((x-self.layout.lmargin+1)/divisor)
  438.   local bound = math.floor((x-self.layout.lmargin+self.layout.maxwidth)/divisor)
  439.   if column ~= bound or column > self.layout.columns then
  440.     return nil
  441.   end
  442.   -- row's easier
  443.   local row = y
  444.   -- now getting the index into the floor state array is relatively simple
  445.   local index = (column - 1)*self.layout.maxheight + row
  446.   if index > self.layout.num then
  447.     return nil
  448.   else
  449.     return index
  450.   end
  451. end
  452.  
  453. function Client:change(floor, state)
  454.   if state then
  455.     print("Change: "..floor.." to true")
  456.   else
  457.     print("Change: "..floor.." to false")
  458.   end
  459.   assert(self.state.floors[floor] ~= nil)
  460.   self.state.floors[floor] = state
  461.   if floor == self.settings["floor"] then
  462.     self.state.active = state
  463.     redstone.setOutput(self.settings.redstone, state)
  464.   end
  465. end
  466.  
  467. function Client:_dispatch_monitor_resize()
  468.   self:fulldraw()
  469.   return true
  470. end
  471.  
  472. function Client:_dispatch_monitor_touch(x, y)
  473.   local index = self:_index_from_point(x,y)
  474.   if index == nil then
  475.     -- the monitor touch doesn't correspond to any floor "button"
  476.     return true
  477.   end
  478.   local floor = self.layout.order[index]
  479.   assert(floor ~= nil)
  480.   local packet = { proto=PROTO_UPDATE,
  481.                    update=UPDATE_SEND,
  482.                    floor=floor }
  483.   rednet.send(self.state.server, textutils.serialize(packet))
  484.   return true
  485. end
  486.  
  487. function Client:_dispatch_rednet_message(sender, rawmsg, extra)
  488.   local msg = deserialize_packet(addr, rawmsg)
  489.   if msg["proto"] == PROTO_UPDATE then
  490.     local update = msg["update"]
  491.     if update == UPDATE_CHANGE then
  492.       for floor, state in pairs(msg.changes) do
  493.         self:change(floor, state)
  494.       end
  495.       self:redraw()
  496.     elseif update == UPDATE_STATE then
  497.       self:_initialize_state(msg)
  498.       self:fulldraw()
  499.     elseif update == UPDATE_QUIT then
  500.       return false
  501.     elseif update == REDISCOVER then
  502.       self:discover()
  503.     end
  504.   end
  505.   return true
  506. end
  507.  
  508. function Client:_dispatch_event(event, param1, param2, param3, param4, param5)
  509.   if event == "rednet_message" then
  510.     return self:_dispatch_rednet_message(param1, param2, param3)
  511.   elseif event == "monitor_touch" and param1 == self.settings["monitor"] then
  512.     return self:_dispatch_monitor_touch(param2, param3)
  513.   elseif event == "monitor_resize" and param1 == self.settings["monitor"] then
  514.     return self:_dispatch_monitor_resize()
  515.   else
  516.     return true
  517.   end
  518. end
  519.  
  520. function Client:run()
  521.   self:_initialize()
  522.   print("Doing initial draw.")
  523.   self:draw()
  524.   print("Initial draw complete.")
  525.   local event, p1, p2, p3, p4, p5
  526.   local success = nil
  527.   while true do
  528.     event, p1, p2, p3, p4, p5 = os.pullEvent()
  529.     assert(self._dispatch_event ~= nil)
  530.     success = self:_dispatch_event(event, p1, p2, p3, p4, p5)
  531.     if not success then
  532.       break
  533.     end
  534.   end
  535.   print("Exiting.")
  536. end
  537.  
  538. -- ##########################
  539.  
  540. function client_prompt()
  541.   print("== CLIENT INSTALL ==")
  542.   local order, floor, modem_side = nil, nil, nil
  543.   local monitor_side, redstone_side = nil, nil
  544.   local answer = nil
  545.   while true do
  546.     print("What is the numerical order of this floor?")
  547.     order = trim(read())
  548.     print("What is the floor name? (Must be unique, probably should be short.)")
  549.     floor = trim(read())
  550.     print("What side is the modem connected to?")
  551.     modem_side = trim(read())
  552.     print("What side is the monitor connected to?")
  553.     monitor_side = trim(read())
  554.     print("What side in the redstone output on?")
  555.     redstone_side = trim(read())
  556.     print("\nHere are your answers:")
  557.     print(string.format("ORDER: %d\n"..
  558.                         "FLOOR: \"%s\"\n"..
  559.                         "MODEM: \"%s\"\n"..
  560.                         "MONITOR: \"%s\"\n"..
  561.                         "REDSTONE: \"%s\"\n",
  562.                         order, floor, modem_side,
  563.                         monitor_side, redstone_side))
  564.     print("Is that correct? (y/N)")
  565.     answer = trim(read())
  566.     if answer == "y" or answer == "Y" then
  567.       break
  568.     else
  569.       print("You did not answer \"y\" or \"Y\"--asking questions again.")
  570.     end
  571.   end
  572.   return tonumber(order), floor, modem_side, redstone_side, monitor_side
  573. end
  574.  
  575.  
  576. function server_prompt()
  577.   print("== SERVER INSTALL ==")
  578.   local modem_side, answer = nil, nil
  579.   while true do
  580.     print("What side is the modem connected to?")
  581.     modem_side = trim(read())
  582.     print("How many floors are expected?")
  583.     expected_floors = trim(read())
  584.     print("\nHere is what you answered:")
  585.     print(string.format("MODEM: \"%s\"\n"..
  586.                         "FLOORS: \"%d\"\n",
  587.                         modem_side,
  588.                         expected_floors))
  589.     print("Is that correct? (y/N)")
  590.     answer = trim(read())
  591.     if answer == "y" or answer == "Y" then
  592.       return modem_side, tonumber(expected_floors)
  593.     else
  594.       print("You did not answer \"y\" or \"Y\"--asking questions again.")
  595.     end
  596.   end
  597. end
  598.  
  599.  
  600. function make_autostart(mode)
  601.   local path = shell.getRunningProgram()
  602.   local autostart_file = io.open("/startup", "w")
  603.   autostart_file:write("shell.run(\""..path.."\", \""..mode.."\")")
  604.   autostart_file:flush()
  605.   autostart_file:close()
  606. end
  607.  
  608.  
  609. function install_client()
  610.   local ord, flr, mod, red, mon = client_prompt()
  611.   client = Client:new(ord, flr, mod, red, mon)
  612.   print("Created client object and saved settings.")
  613.   make_autostart("client")
  614.   print("Autostart script created.  Starting client.")
  615.   client:run()
  616. end
  617.  
  618.  
  619. function install_server()
  620.   local modem, floors = server_prompt()
  621.   server = Server:new(modem, floors)
  622.   print("Create server object and saved settings.")
  623.   make_autostart("server")
  624.   print("Autostart script created.  Starting server.")
  625.   server:run()
  626. end
  627.  
  628.  
  629. function main(args)
  630.   local action = args[1]
  631.   if action == "install" then
  632.     if args[2] == "server" then
  633.       print("Installing server...")
  634.       install_server()
  635.     elseif args[2] == "client" then
  636.       print("Installing client...")
  637.       install_client()
  638.     else
  639.       print("Can't install \""..args[2].."\"")
  640.       return
  641.     end
  642.   elseif action == "server" then
  643.     local server = Server:new()
  644.     server:run()
  645.   elseif action == "client" then
  646.     local client = Client:new()
  647.     client:run()
  648.   else
  649.     print("Unrecognized action \""..action.."\"")
  650.   end
  651. end
  652.  
  653.  
  654. main({...})
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement