Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Copyright (c) 2017 angryoptimist
- -- (email: "copyright" at domain "angryoptimist.info" or "angryoptimist.net")
- --
- -- Permission is hereby granted, free of charge, to any person obtaining
- -- a copy of this software and associated documentation files (the
- -- "Software"), to deal in the Software without restriction, including
- -- without limitation the rights to use, copy, modify, merge, publish,
- -- distribute, sublicense, and/or sell copies of the Software, and to
- -- permit persons to whom the Software is furnished to do so, subject to
- -- the following conditions:
- --
- -- The above copyright notice and this permission notice shall be included
- -- in all copies or substantial portions of the Software.
- --
- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- -- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- -- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- -- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- --
- PROTO_DISCOVER=1
- PROTO_DISCOVER_ACK=2
- PROTO_UPDATE=3
- UPDATE_STATE=1
- UPDATE_SEND=2
- UPDATE_CHANGE=3
- UPDATE_QUIT=4
- UPDATE_REDISCOVER=5
- DISCOVERY_TIMEOUT=10
- --##############
- -- Shared stuff
- --##############
- function member_of(some_table, possible_member)
- for k,v in pairs(some_table) do
- if possible_member == v then
- return true
- end
- end
- return false
- end
- function trim(some_string)
- return some_string:gsub("^%s+",""):gsub("%s+$","")
- end
- function filter_proto_recv(proto, timeout)
- local addr = nil
- local raw = nil
- local msg = nil
- local extra = nil
- while true do
- if timeout ~= nil then
- addr, raw, extra = rednet.receive(timeout)
- else
- addr, raw, extra = rednet.receive()
- end
- if addr == nil then
- return nil
- else
- msg = deserialize_packet(addr, raw)
- end
- if msg["proto"] == proto then
- return msg
- end
- end
- end
- function deserialize_packet(addr, raw)
- local msg = textutils.unserialize(raw)
- msg["addr"] = addr
- return msg
- end
- -- Initialization functions
- function load_settings(settings, filepath)
- if not fs.exists(filepath) then
- return false
- end
- local h = fs.open(filepath, "r")
- local serialized = h.readAll()
- h.close()
- local settings_in = textutils.unserialize(serialized)
- for key,val in pairs(settings_in) do
- settings[key] = val
- end
- return true
- end
- function save_settings(settings, filepath)
- if fs.exists(filepath) then
- return false
- end
- local serialized = textutils.serialize(settings)
- local h = fs.open(filepath, "w")
- h.write(serialized)
- h.close()
- return true
- end
- --##############
- -- Server stuff
- --##############
- Server = { _settings_path="/server.dat" }
- function Server:new(modem_side, expected_floors)
- o = {}
- setmetatable(o, self)
- self.__index = self
- o.state={ elevator={ floors={}, cart=nil } }
- o.layout={ num=0, maxwidth=0, seq={}, map={} }
- o.settings={}
- o:_initialize_settings(modem_side, expected_floors)
- return o
- end
- function Server:_initialize()
- rednet.open(self.settings["modem"])
- print("Discovering floors...")
- self:discover()
- print(string.format("Found %s floors.", self.layout["num"]))
- self:_initialize_state()
- self:blast_state()
- end
- function Server:_initialize_state()
- self.state["elevator"] = { floors={}, cart=nil }
- self.state["elevator"]["cart"] = self.layout["seq"][1]
- for n,floor in ipairs(self.layout.seq) do
- if floor == self.state.elevator.cart then
- self.state.elevator.floors[floor] = true
- else
- self.state.elevator.floors[floor] = false
- end
- end
- end
- function Server:_initialize_settings(modem_side, expected_floors)
- if modem_side ~= nil then
- assert(expected_floors ~= nil)
- self.settings["modem"] = modem_side
- self.settings["floors"] = expected_floors
- save_settings(self.settings, self._settings_path)
- else
- assert(fs.exists(self._settings_path))
- load_settings(self.settings, self._settings_path)
- end
- end
- function Server:_discover_client()
- local msg = filter_proto_recv(PROTO_DISCOVER_ACK, DISCOVERY_TIMEOUT)
- if msg == nil then
- return nil
- end
- local client_addr = msg["addr"]
- local floor = msg["floor"]
- local order = msg["order"]
- if member_of(self.layout["seq"], floor) then
- print(string.format("Floor '%s' already exists "..
- "(reg: %s/req: %s)",
- floor, layout["map"][floor], client_addr))
- return nil
- end
- self.layout["map"][floor] = client_addr
- table.insert(self.layout["seq"], floor)
- self.layout["num"] = self.layout["num"] + 1
- self.layout["maxwidth"] = math.max(self.layout["maxwidth"],
- string.len(string.format("%s", floor)))
- return client_addr, floor, order
- end
- function Server:discover(tries)
- self.layout["num"] = 0
- self.layout["maxwidth"] = 0
- self.layout["seq"] = {}
- self.layout["map"] = {}
- local fail = 0
- local max_fails = tries or 3
- local addr, floor, seq = nil, nil, nil
- local seq_map = {}
- local packet={ proto=PROTO_DISCOVER }
- print("Pinging clients...")
- rednet.broadcast(textutils.serialize(packet))
- print("Listing for replies...")
- while (self.layout["num"] == 0 or fail < max_fails) and
- (self.layout["num"] < self.settings["floors"]) do
- addr, floor, seq = self:_discover_client()
- if addr == nil then
- if self.layout["num"] > 0 then
- fail = fail + 1
- print(string.format("Timeout. %d retries remaining.",
- max_fails - fail))
- end
- print("No client responses recieved yet...")
- rednet.broadcast(textutils.serialize(packet))
- else
- seq_map[floor] = seq
- print(string.format("Found floor '%s' at '%s'",
- floor, addr))
- end
- end
- -- Sort in reverse order, since we'll list in that order
- table.sort(self.layout["seq"],
- function(x,y) return seq_map[x]>seq_map[y] end)
- end
- function Server:blast_state()
- local packet = { proto=PROTO_UPDATE,
- update=UPDATE_STATE,
- state=self.state.elevator,
- order=self.layout["seq"],
- maxwidth=self.layout.maxwidth,
- num=self.layout.num }
- print("Broadcasting state packet...")
- local serialized = textutils.serialize(packet)
- --rednet.broadcast(serialized)
- for floor, addr in pairs(self.layout.map) do
- rednet.send(addr, serialized)
- end
- end
- function Server:send_changes(changes)
- local packet = { proto=PROTO_UPDATE,
- update=UPDATE_CHANGE,
- changes=changes }
- rednet.broadcast(textutils.serialize(packet))
- for floor, state in pairs(changes) do
- print(string.format("Set floor '%s' to %s", floor, tostring(state)))
- self.state.elevator.floors[floor] = state
- end
- end
- function Server:move_cart(to_floor)
- print("Move cart to "..to_floor)
- local current=self.state.elevator.cart
- if to_floor == current then
- print("Cart already at "..to_floor)
- return
- end
- local changes = {}
- changes[to_floor] = true
- changes[current] = false
- self:send_changes(changes)
- self.state.elevator.cart=to_floor
- end
- function Server:run()
- self:_initialize()
- while true do
- msg = filter_proto_recv(PROTO_UPDATE)
- if msg.update == UPDATE_SEND then
- print(string.format("Recieved request to send cart to floor '%s'...",
- msg.floor))
- self:move_cart(msg.floor)
- print("Requested changes bounced to clients.")
- elseif msg.update == UPDATE_QUIT then
- print("Recieved request to quit.")
- rednet.broadcast(textutils.serialize(msg))
- print("Told clients to quit.")
- break
- else
- print(string.format("Got unrecongized update type '%s'", msg.update))
- end
- end
- print("Exiting.")
- end
- --##############
- -- Client stuff
- --##############
- Client = { _settings_path="/client.dat",
- screen_bg=colors.black,
- text_bg=colors.lightGray,
- text_fg=colors.black,
- sel_bg=colors.blue,
- sel_fg=colors.white }
- function Client:new(order, floor, modem_side, redstone_side, monitor_side)
- o = {}
- setmetatable(o, self)
- self.__index = self
- o.state={ floors={}, active=true, server=nil }
- o.layout={ num=0, maxwidth=0, columns=0, maxheight=0,
- width=0, height=0, lmargin=0, rmargin=0,
- draw = {}, order = {} }
- o.settings={}
- o:_initialize_settings(order, floor, modem_side, redstone_side, monitor_side)
- return o
- end
- function Client:_initialize()
- rednet.open(self.settings["modem"])
- print("Discovering server...")
- self:discover()
- end
- function Client:_initialize_state(packet)
- self.state.floors = packet.state.floors
- self.layout.maxwidth = packet.maxwidth
- self.layout.num = packet.num
- self.layout.order = packet.order
- self.monitor = peripheral.wrap(self.settings["monitor"])
- self:_recalculate_layout()
- end
- function Client:_initialize_settings(order, floor,
- modem_side, redstone_side,
- monitor_side)
- if order ~= nil then
- assert(floor ~= nil and modem_side ~= nil and
- redstone_side ~= nil and monitor_side ~= nil)
- self.settings["order"] = order
- self.settings["floor"] = floor
- self.settings["modem"] = modem_side
- self.settings["redstone"] = redstone_side
- self.settings["monitor"] = monitor_side
- save_settings(self.settings, self._settings_path)
- else
- assert(fs.exists(self._settings_path))
- load_settings(self.settings, self._settings_path)
- end
- end
- function Client:discover()
- print("Listening for server...")
- local msg = filter_proto_recv(PROTO_DISCOVER)
- self.state.server = msg["addr"]
- print(string.format("Server found, addr:%s", msg["addr"]))
- local packet = { proto = PROTO_DISCOVER_ACK,
- floor = self.settings.floor,
- order = self.settings.order }
- rednet.send(msg["addr"], textutils.serialize(packet))
- print("Identified self to server.")
- print(string.format("Found server at addr:%s", self.state.server))
- print("Awaiting state packet...")
- local msg = filter_proto_recv(PROTO_UPDATE)
- print("Got state packet.")
- self:_initialize_state(msg)
- print(string.format("Recieved state from server."))
- redstone.setOutput(self.settings.redstone, self.state.active)
- end
- function Client:_recalculate_layout()
- self.monitor = peripheral.wrap(self.settings["monitor"])
- -- Initialize values
- self.layout.lmargin, self.layout.rmargin = 0, 0
- self.layout.width, self.layout.height = self.monitor.getSize()
- -- Calculate number of columns
- if (self.layout.num > self.layout.height) then
- self.layout.columns = math.ceil((self.layout.width + 1)/(self.layout.maxwidth+1))
- else
- self.layout.columns = 1
- end
- self.layout.maxheight=math.ceil(self.layout.num/self.layout.columns)
- -- Not implementing scrollbars (yet)--freak out if stuff doesn't fit
- assert(self.layout.maxheight <= self.layout.height)
- -- Now to figure out margins (if any)...
- local used_width = ((self.layout.columns * (self.layout.maxwidth)) +
- (self.layout.columns - 1))
- self.layout.lmargin=math.ceil((self.layout.width%used_width)/2)
- self.layout.rmargin=(self.layout.width%used_width)-self.layout.lmargin
- -- Now to build the arrangement of columns
- self.layout.draw = {}
- local column = nil
- local fmt_str = "%"..self.layout.maxwidth.."s"
- local floor = nil
- for i=1,self.layout.num,self.layout.maxheight do
- column = {}
- for j=i,i+self.layout.maxheight-1 do
- floor = self.layout.order[j]
- if self.state.floors[floor] ~= nil then
- table.insert(column, string.format(fmt_str, floor))
- else
- break
- end
- end
- table.insert(self.layout.draw, column)
- end
- end
- function Client:draw()
- self.monitor.setBackgroundColor(self.screen_bg)
- self.monitor.clear()
- self:redraw()
- end
- function Client:fulldraw()
- self:_recalculate_layout()
- self:draw()
- end
- function Client:redraw()
- local index = nil
- local floor = nil
- local x,y = 0,0
- local offset = nil
- for i,column in ipairs(self.layout.draw) do
- x = 1 + self.layout.lmargin + (i-1)*(self.layout.maxwidth+1) + (i-1)
- offset = (i-1)*self.layout.maxheight
- for j,label in ipairs(column) do
- y = j
- index = offset + j
- floor = self.layout.order[index]
- if self.state.floors[floor] then
- self.monitor.setBackgroundColor(self.sel_bg)
- self.monitor.setTextColor(self.sel_fg)
- else
- self.monitor.setBackgroundColor(self.text_bg)
- self.monitor.setTextColor(self.text_fg)
- end
- self.monitor.setCursorPos(x,y)
- self.monitor.write(label)
- end
- end
- end
- function Client:_index_from_point(x,y)
- -- because it's the harder part, first we find the column
- local divisor = self.layout.maxwidth+1
- local column = math.ceil((x-self.layout.lmargin+1)/divisor)
- local bound = math.floor((x-self.layout.lmargin+self.layout.maxwidth)/divisor)
- if column ~= bound or column > self.layout.columns then
- return nil
- end
- -- row's easier
- local row = y
- -- now getting the index into the floor state array is relatively simple
- local index = (column - 1)*self.layout.maxheight + row
- if index > self.layout.num then
- return nil
- else
- return index
- end
- end
- function Client:change(floor, state)
- if state then
- print("Change: "..floor.." to true")
- else
- print("Change: "..floor.." to false")
- end
- assert(self.state.floors[floor] ~= nil)
- self.state.floors[floor] = state
- if floor == self.settings["floor"] then
- self.state.active = state
- redstone.setOutput(self.settings.redstone, state)
- end
- end
- function Client:_dispatch_monitor_resize()
- self:fulldraw()
- return true
- end
- function Client:_dispatch_monitor_touch(x, y)
- local index = self:_index_from_point(x,y)
- if index == nil then
- -- the monitor touch doesn't correspond to any floor "button"
- return true
- end
- local floor = self.layout.order[index]
- assert(floor ~= nil)
- local packet = { proto=PROTO_UPDATE,
- update=UPDATE_SEND,
- floor=floor }
- rednet.send(self.state.server, textutils.serialize(packet))
- return true
- end
- function Client:_dispatch_rednet_message(sender, rawmsg, extra)
- local msg = deserialize_packet(addr, rawmsg)
- if msg["proto"] == PROTO_UPDATE then
- local update = msg["update"]
- if update == UPDATE_CHANGE then
- for floor, state in pairs(msg.changes) do
- self:change(floor, state)
- end
- self:redraw()
- elseif update == UPDATE_STATE then
- self:_initialize_state(msg)
- self:fulldraw()
- elseif update == UPDATE_QUIT then
- return false
- elseif update == REDISCOVER then
- self:discover()
- end
- end
- return true
- end
- function Client:_dispatch_event(event, param1, param2, param3, param4, param5)
- if event == "rednet_message" then
- return self:_dispatch_rednet_message(param1, param2, param3)
- elseif event == "monitor_touch" and param1 == self.settings["monitor"] then
- return self:_dispatch_monitor_touch(param2, param3)
- elseif event == "monitor_resize" and param1 == self.settings["monitor"] then
- return self:_dispatch_monitor_resize()
- else
- return true
- end
- end
- function Client:run()
- self:_initialize()
- print("Doing initial draw.")
- self:draw()
- print("Initial draw complete.")
- local event, p1, p2, p3, p4, p5
- local success = nil
- while true do
- event, p1, p2, p3, p4, p5 = os.pullEvent()
- assert(self._dispatch_event ~= nil)
- success = self:_dispatch_event(event, p1, p2, p3, p4, p5)
- if not success then
- break
- end
- end
- print("Exiting.")
- end
- -- ##########################
- function client_prompt()
- print("== CLIENT INSTALL ==")
- local order, floor, modem_side = nil, nil, nil
- local monitor_side, redstone_side = nil, nil
- local answer = nil
- while true do
- print("What is the numerical order of this floor?")
- order = trim(read())
- print("What is the floor name? (Must be unique, probably should be short.)")
- floor = trim(read())
- print("What side is the modem connected to?")
- modem_side = trim(read())
- print("What side is the monitor connected to?")
- monitor_side = trim(read())
- print("What side in the redstone output on?")
- redstone_side = trim(read())
- print("\nHere are your answers:")
- print(string.format("ORDER: %d\n"..
- "FLOOR: \"%s\"\n"..
- "MODEM: \"%s\"\n"..
- "MONITOR: \"%s\"\n"..
- "REDSTONE: \"%s\"\n",
- order, floor, modem_side,
- monitor_side, redstone_side))
- print("Is that correct? (y/N)")
- answer = trim(read())
- if answer == "y" or answer == "Y" then
- break
- else
- print("You did not answer \"y\" or \"Y\"--asking questions again.")
- end
- end
- return tonumber(order), floor, modem_side, redstone_side, monitor_side
- end
- function server_prompt()
- print("== SERVER INSTALL ==")
- local modem_side, answer = nil, nil
- while true do
- print("What side is the modem connected to?")
- modem_side = trim(read())
- print("How many floors are expected?")
- expected_floors = trim(read())
- print("\nHere is what you answered:")
- print(string.format("MODEM: \"%s\"\n"..
- "FLOORS: \"%d\"\n",
- modem_side,
- expected_floors))
- print("Is that correct? (y/N)")
- answer = trim(read())
- if answer == "y" or answer == "Y" then
- return modem_side, tonumber(expected_floors)
- else
- print("You did not answer \"y\" or \"Y\"--asking questions again.")
- end
- end
- end
- function make_autostart(mode)
- local path = shell.getRunningProgram()
- local autostart_file = io.open("/startup", "w")
- autostart_file:write("shell.run(\""..path.."\", \""..mode.."\")")
- autostart_file:flush()
- autostart_file:close()
- end
- function install_client()
- local ord, flr, mod, red, mon = client_prompt()
- client = Client:new(ord, flr, mod, red, mon)
- print("Created client object and saved settings.")
- make_autostart("client")
- print("Autostart script created. Starting client.")
- client:run()
- end
- function install_server()
- local modem, floors = server_prompt()
- server = Server:new(modem, floors)
- print("Create server object and saved settings.")
- make_autostart("server")
- print("Autostart script created. Starting server.")
- server:run()
- end
- function main(args)
- local action = args[1]
- if action == "install" then
- if args[2] == "server" then
- print("Installing server...")
- install_server()
- elseif args[2] == "client" then
- print("Installing client...")
- install_client()
- else
- print("Can't install \""..args[2].."\"")
- return
- end
- elseif action == "server" then
- local server = Server:new()
- server:run()
- elseif action == "client" then
- local client = Client:new()
- client:run()
- else
- print("Unrecognized action \""..action.."\"")
- end
- end
- main({...})
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement