├── .gitignore ├── README.md ├── images └── elevator.jpg └── lifty.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lifty - The Elevator 2 | 3 | Author: David Beazley (https://www.dabeaz.com) 4 | Source: https://github.com/dabeaz/lifty 5 | 6 | Copyright (C) 2025 7 | 8 | Graydon decides that he's going to go visit his friend Fletcher. He 9 | arrives at his apartment building and gets in the elevator. However, 10 | instead of a typical building directory, the following puzzler is 11 | posted: 12 | 13 | > Baker, Cooper, Fletcher, Miller, and Smith live here. They each 14 | > live on a different floor. Baker does not live on the top 15 | > floor. Cooper does not live on the bottom floor. Fletcher does not 16 | > live on either the top or the bottom floor. Miller lives on a higher 17 | > floor than does Cooper. Smith does not live on a floor adjacent to 18 | > Fletcher. Fletcher does not live on a floor adjacent to Cooper. 19 | 20 | "Ah, hell, what kind of [madness](https://sarabander.github.io/sicp/html/4_002e3.xhtml#g_t4_002e3_002e2) is this?" he mumbles. After turning over 21 | all of the solutions in his mind before arriving at the correct floor, 22 | he pushes the elevator button only to find that the elevator software has 23 | crashed. 24 | 25 | "It's [ridiculous](https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language/)," he thought, "that we computer people can't even 26 | make an *elevator* that works without crashing!" Indeed. How hard 27 | could it be? An elevator goes up and down. Doors open and close. 28 | Maybe it's kind of like a glorified toaster. 29 | 30 | ## Overview 31 | 32 | An elevator is an object from everyday experience that most people 33 | would claim to understand. However, elevators are surprisingly 34 | devious. Thus, an elevator makes a great problem domain for all sorts 35 | of computer science problems involving algorithms, design, testing, 36 | systems, formal verification, software engineering, and more. 37 | 38 | What you'll find here is a simulator of elevator hardware with none of 39 | the brains of elevator behavior. It's intended use is as an 40 | educational tool. Thus, think of it as an empty canvas for exploring 41 | various kinds of programming projects involving elevators (see the 42 | "Ideas" section at the end for possible projects). 43 | 44 | The simulator models an elevator system consisting of a single car in 45 | a five-floor building. It's nothing fancy--the image below gives you 46 | a visual reference for the setup. 47 | 48 | ![](images/elevator.jpg) 49 | 50 | The hardware has the following features: 51 | 52 | * A single elevator car. 53 | * A motor that makes the car go up and down. 54 | * A door that can open and close. 55 | * A button panel inside the car. 56 | * Up buttons on floors 1-4. 57 | * Down buttons on floors 2-5. 58 | * Indicator lights on each floor that can show a direction. 59 | 60 | Certain elevator features such door open/close buttons, an emergency key, 61 | and other things are ommitted in the interest of simplicity. 62 | 63 | You can control the elevator by typing commands in the terminal or by 64 | sending network messages. The simulator can also report real-time 65 | events to a separate control program via the network. 66 | 67 | ## The Simulator 68 | 69 | The simulator is implemented in a file `lifty.rs`. This is a 70 | single-file Rust project with no dependencies. Compile it to create 71 | the `lifty` program: 72 | 73 | ``` 74 | bash % rustc lifty.rs 75 | ``` 76 | 77 | If you run it, you'll get output like this: 78 | 79 | ``` 80 | bash % ./lifty 81 | Welcome! I'm Lifty--a simulated elevator in a 5-floor building. 82 | 83 | I'm just hardware, but I have buttons (type below and hit return): 84 | 85 | Pn - Floor n button on panel inside car 86 | Un - Up button on floor n 87 | Dn - Down button on floor n 88 | 89 | If something goes wrong, I'll crash and you'll have to call 90 | maintenance to restart the elevator control program. 91 | 92 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : 93 | ``` 94 | 95 | The elevator is stopped on the first floor with the door closed. 96 | You can type commands after the `:`. Try pressing a few buttons by 97 | typing "P2", "U3", and "D5" separately. The output should change to 98 | the following: 99 | 100 | ``` 101 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : P2 102 | [ FLOOR 1 | CLOSED -- | P:-2--- | U:----- | D:----- ] : U3 103 | [ FLOOR 1 | CLOSED -- | P:-2--- | U:--3-- | D:----- ] : D5 104 | [ FLOOR 1 | CLOSED -- | P:-2--- | U:--3-- | D:----5 ] : 105 | ``` 106 | 107 | Here, the hardware has recorded some button presses. However, there 108 | are no brains built into the simulator. Literally nothing 109 | happens. You can instruct the hardware to do things though. Try 110 | opening the door by typing "DO". You'll see the elevator status show 111 | "OPENING" for a few seconds before changing to "OPEN". The output 112 | will look like this: 113 | 114 | ``` 115 | [ FLOOR 1 | CLOSED -- | P:-2--- | U:--3-- | D:----5 ] : DO 116 | [ FLOOR 1 | OPENING -- | P:-2--- | U:--3-- | D:----5 ] : 117 | [ FLOOR 1 | OPEN -- | P:-2--- | U:--3-- | D:----5 ] : 118 | ``` 119 | 120 | Try closing the door by typing "DC". You should see the elevator 121 | status show "CLOSING" for a few seconds before finally changing to 122 | "CLOSED". Although the output is basic, the elevator is dynamic, 123 | has internal timing, and will change its state automatically (updating 124 | the output as needed). It might look weird to see all of 125 | the state changes printed in order, but you'll appreciate this 126 | feature when it comes to explaining what's happening or for 127 | debugging. 128 | 129 | Try typing "MU" to have the elevator start moving up. You 130 | should see the floor slowly increment about every 3-4 seconds. 131 | Eventually the elevator will crash with a message like this: 132 | 133 | ``` 134 | [ FLOOR 1 | CLOSED -- | P:-2--- | U:--3-- | D:----5 ] : MU 135 | [ FLOOR 1 | UP -- | P:-2--- | U:--3-- | D:----5 ] : 136 | [ FLOOR 2 | UP -- | P:-2--- | U:--3-- | D:----5 ] : 137 | [ FLOOR 3 | UP -- | P:-2--- | U:--3-- | D:----5 ] : 138 | [ FLOOR 4 | UP -- | P:-2--- | U:--3-- | D:----5 ] : 139 | [ FLOOR 5 | UP -- | P:-2--- | U:--3-- | D:----5 ] : 140 | CRASH! : Hit the roof! 141 | [ FLOOR 5 | CRASH -- | P:-2--- | U:--3-- | D:----5 ] : 142 | ``` 143 | 144 | Again, the hardware is dumb. The elevator did not stop at any floor 145 | and just slammed into the top of the building. Once crashed, the 146 | elevator stays crashed and won't respond to any future commands until 147 | you issue a reset (R). 148 | 149 | Giving the elevator bad combinations of commands will also cause it 150 | to crash. For example, telling it to open the doors and then move. 151 | 152 | ``` 153 | [ FLOOR 5 | CRASH -- | P:-2--- | U:--3-- | D:----5 ] : R 154 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : DO 155 | [ FLOOR 1 | OPENING -- | P:----- | U:----- | D:----- ] : 156 | [ FLOOR 1 | OPEN -- | P:----- | U:----- | D:----- ] : MU 157 | 158 | CRASH! : motor command received while doors open 159 | [ FLOOR 1 | CRASH -- | P:----- | U:----- | D:----- ] : 160 | ``` 161 | 162 | The simulator is pretty sensitive to timing related issues 163 | and various modal errors. For example, if you tried to 164 | open the doors twice: 165 | 166 | ``` 167 | [ FLOOR 5 | CRASH -- | P:-2--- | U:--3-- | D:----5 ] : R 168 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : DO 169 | [ FLOOR 1 | OPENING -- | P:----- | U:----- | D:----- ] : 170 | [ FLOOR 1 | OPEN -- | P:----- | U:----- | D:----- ] : DO 171 | 172 | CRASH! : door already open 173 | [ FLOOR 1 | CRASH -- | P:----- | U:----- | D:----- ] : 174 | ``` 175 | 176 | ## Remote Access 177 | 178 | Although the above example involved typing commands at the terminal, 179 | the simulator can also receive commands over the network using a UDP 180 | socket. Start the `lifty` program in a separate terminal window. 181 | Now, go to a different terminal and start Python (which provides a 182 | good environment for interactive experimentation). Try the following 183 | example: 184 | 185 | ```python 186 | >>> from socket import * 187 | >>> sock = socket(AF_INET, SOCK_DGRAM) 188 | >>> sock.bind(('localhost', 11000)) 189 | >>> sock.sendto(b'R', ('localhost', 10000)) 190 | 1 191 | >>> sock.sendto(b'DO', ('localhost', 10000)) 192 | 2 193 | >>> 194 | ``` 195 | 196 | The Lifty program should be showing an elevator with the door open like 197 | this: 198 | 199 | ``` 200 | [ FLOOR 1 | CRASH -- | P:----- | U:----- | D:----- ] : recv: R 201 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : recv: DO 202 | [ FLOOR 1 | OPENING -- | P:----- | U:----- | D:----- ] : 203 | [ FLOOR 1 | OPEN -- | P:----- | U:----- | D:----- ] : 204 | ``` 205 | 206 | Any command that can be typed at the prompt can also be sent to Lifty 207 | via a socket. All commands received in this way will be printed, but 208 | prefaced by a "recv:" to indicate that they were received over the 209 | network. 210 | 211 | ## Events 212 | 213 | Lifty also generates certain events that get sent to a control program. 214 | In the same Python example above, type the following to receive an 215 | event: 216 | 217 | ```python 218 | >>> sock.recvfrom(100) 219 | (b'O1', ('127.0.0.1', 50903)) 220 | >>> 221 | ``` 222 | 223 | You'll immediately see an "O1" event. This is something the simulator 224 | sent when the doors finished opening. Now, type this in Python: 225 | 226 | ```python 227 | >>> sock.recvfrom(100) 228 | ... blocked (waiting) 229 | ``` 230 | 231 | Python will be blocked waiting for a message. Go the Lifty prompt and type "DC" 232 | to close the doors. Lifty will briefly display "CLOSING" before shifting to "CLOSED" 233 | like this: 234 | 235 | ``` 236 | [ FLOOR 1 | OPEN -- | P:----- | U:----- | D:----- ] : DC 237 | [ FLOOR 1 | CLOSING -- | P:----- | U:----- | D:----- ] : 238 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : 239 | ``` 240 | 241 | The Python program should have now returned with a message like this: 242 | 243 | ```python 244 | >>> sock.recvfrom(100) 245 | (b'C1', ('127.0.0.1', 50903)) 246 | >>> 247 | ``` 248 | 249 | The "C1" event means that the doors just closed on floor 1. Put the 250 | Python program into a loop like this: 251 | 252 | ```python 253 | >>> while True: 254 | ... print(sock.recvfrom(100)) 255 | ... 256 | ``` 257 | 258 | Now, type "MU" into the Lifty program to turn the motor on. You should 259 | see the following appear in the Python terminal before the elevator 260 | crashes into the roof: 261 | 262 | ```python 263 | >>> while True: 264 | ... print(sock.recvfrom(100)) 265 | ... 266 | (b'A2', ('127.0.0.1', 50903)) 267 | (b'A3', ('127.0.0.1', 50903)) 268 | (b'A4', ('127.0.0.1', 50903)) 269 | (b'A5', ('127.0.0.1', 50903)) 270 | ``` 271 | 272 | These are events indicating that the elevator is approaching a new 273 | floor. Again, the elevator is pretty dumb. It will keep moving 274 | unless you tell it to stop. Here's a modified Python example 275 | that illustrates stopping on the 4th floor: 276 | 277 | ```python 278 | >>> while True: 279 | ... msg, _ = sock.recvfrom(100) 280 | ... if msg == b'A4': 281 | ... sock.sendto(b'S', ('localhost', 10000)) 282 | ... break 283 | ... 284 | ``` 285 | 286 | With that running, go to the Lifty window and type "R" followed by "MU". You 287 | should see the elevator slowly start moving up and stopping as soon as it 288 | reaches the 4th floor. The final state will look like this: 289 | 290 | ``` 291 | [ FLOOR 5 | CRASH -- | P:----- | U:----- | D:----- ] : R 292 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : MU 293 | [ FLOOR 1 | UP -- | P:----- | U:----- | D:----- ] : 294 | [ FLOOR 2 | UP -- | P:----- | U:----- | D:----- ] : 295 | [ FLOOR 3 | UP -- | P:----- | U:----- | D:----- ] : recv: S 296 | [ FLOOR 3 | STOPPING -- | P:----- | U:----- | D:----- ] : 297 | [ FLOOR 4 | CLOSED -- | P:----- | U:----- | D:----- ] : 298 | ``` 299 | 300 | The "STOPPING" state means the elevator has been instructed to stop 301 | at the next floor and is in the process of doing so. 302 | The "CLOSED" state indicates that the elevator has fully stopped. 303 | If you want it to open the doors, a "DO" command 304 | must be sent. You can do that from Python if you want: 305 | 306 | ```python 307 | >>> sock.sendto(b'DO', ('localhost', 10000)) 308 | 2 309 | >>> 310 | ``` 311 | 312 | This is the basic idea of Lifty. It's the simulated hardware of an 313 | elevator, but none of the brains of an elevator. It will try to 314 | show you everything that is happening though. 315 | 316 | ## Commands 317 | 318 | Here is a complete list of commands that Lifty understands. These 319 | may be typed at the prompt or sent to Lifty via a socket. 320 | 321 | ``` 322 | Pn - Press panel button n 323 | Un - Press up button on floor n 324 | Dn - Press down button on floor n 325 | MU - Start moving up 326 | MD - Start moving down 327 | S - Stop moving (will generate Sn event when stopped) 328 | DO - Open door (will generate On event when done) 329 | DC - Close the door (will generate Cn event when done) 330 | CPn - Clear panel button n 331 | CUn - Clear up button n 332 | CDn - Clear down button n 333 | IUn - Set indicator light on floor n to "up" 334 | IDn - Set indicator light on floor n to "down" 335 | CIn - Clear the indicator light on floor n 336 | R - Reset 337 | ``` 338 | 339 | I'd encourage you to play around with the hardware by typing various commands 340 | at the Lifty prompt. You should see the elevator respond in various ways. 341 | You'll probably make it crash quite a bit too--remember to type "R" to 342 | reset it after this happens. 343 | 344 | ## Events 345 | 346 | Lifty generates the following events that are sent via a UDP socket to 347 | a separate control program assumed to be listening on port 11000. 348 | 349 | ``` 350 | Pn - Panel button for floor n was pressed 351 | Un - Up button on floor n was pressed 352 | Dn - Down button floor n was pressed 353 | An - Approaching floor n (in motion) 354 | Sn - Stopped at floor n (safe to open doors) 355 | Cn - Door closed on floor n (now safe to move) 356 | On - Door opened on floor n (door fully open) 357 | ``` 358 | 359 | If there is a control program running, it would make decisions about what 360 | to do next based on these events. Emphasis: The simulator itself has 361 | no smarts built into it other than some basic defense to avoid cutting 362 | passengers in half. 363 | 364 | ## Small Details 365 | 366 | Elevators in the real world do a lot of things with illuminated 367 | buttons and lights. The simulator mimics this behavior but requires 368 | explicit control. For example, if a button gets pressed, it will 369 | light up and stay lit up until it is explicitly cleared by a command. 370 | Try this: 371 | 372 | ``` 373 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : P3 374 | [ FLOOR 1 | CLOSED -- | P:--3-- | U:----- | D:----- ] : CP3 375 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : 376 | ``` 377 | 378 | The "P3" presses a button on the panel. The "CP3" command clears 379 | the associated light. It is assumed that "CP3" would be sent by 380 | whatever control software is running. For example, when an elevator 381 | stops on floor 3, the control software would clear the button light. 382 | Again, lights are never cleared on their own by the simulator. 383 | 384 | Direction indicator lights are a helpful user interface for riders 385 | that is easy to overlook. Close your eyes and visualize your use of 386 | an elevator. Yes, when the elevator arrives at your floor, there is 387 | usually some kind of light that displays the direction of travel. 388 | The simulator has this, but it must be explicitly controlled. 389 | Here is an example of setting a direction indicator 390 | light. The "IU1" command illuminates the "Up" arrow on floor 1 (shown by 391 | "^^"). The "CI1" command turns the indicator light off. 392 | 393 | ``` 394 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : IU1 395 | [ FLOOR 1 | CLOSED ^^ | P:----- | U:----- | D:----- ] : CI1 396 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : 397 | ``` 398 | 399 | Indicator lights are finicky. Only one such light can be illuminated 400 | at a time in the entire building and not all floors have the same 401 | lights. For example, if you try to turn on the "Down" arrow on floor 402 | 1, you'll crash the simulator: 403 | 404 | ``` 405 | [ FLOOR 1 | CLOSED -- | P:----- | U:----- | D:----- ] : ID1 406 | 407 | CRASH! : No down indicator light on bottom floor 408 | [ FLOOR 1 | CRASH -- | P:----- | U:----- | D:----- ] : 409 | ``` 410 | 411 | Buttons and direction indicator lights don't have any impact on the 412 | operation of the simulator other than generating an event when 413 | a button gets pressed. For example, the elevator is *NOT* going to 414 | stop just because a button is illuminated---remember that the 415 | simulator is dumb. 416 | 417 | If you're giving some kind of class project, buttons and lights can be 418 | a great source of pedantic point deductions. "Why did I get a B?" 419 | "Because you didn't turn off the up button light upon car arrival." 420 | "I hate you." You get the idea. 421 | 422 | ## Ideas 423 | 424 | Now that you've seen the simulator, what might you do with it? Here are some 425 | possible ideas: 426 | 427 | ### Projects Involving the Use of the Simulator 428 | 429 | * **An evil coding project involving elevators.** If an elevator isn't 430 | so hard to code, then do it! Moreover, prove to everyone else that 431 | a) your code corresponds to how an elevator actually works in 432 | reality and b) your code will never crash the elevator, deadlock, or 433 | exhibit other strange behavior. This is a project that I give in 434 | some of my courses such as [Advanced Programming with 435 | Python](https://dabeaz.com/advprog.html) or [Rusty 436 | Elevator](https://dabeaz.com/rusty_elevator.html). It's fun. 437 | 438 | * **An evil vibe coding project.** "Here's some hardware for an 439 | elevator--go write the software for controlling it." No further guidance 440 | of any kind is given. Now sit back with a wry smile 441 | as you watch the resulting process of self-discovery unfold. 442 | 443 | * **An evil job interview question.** Kind of like the vibe coding idea, but 444 | only give applicants an hour to work on it to see how 10x they are. 445 | Make sure you record the camera to see all of the sweating. 446 | 447 | * **An evil object-oriented design project.** Apply various OO design 448 | principles to the problem of controlling an elevator. Is this an 449 | opportunity to practice dependency injection? Or apply the state 450 | machine pattern? Or to accidentally unleash a zombie hoard by 451 | mispronouncing one of the words while uttering 452 | "model-view-control?" Yeah, maybe. 453 | 454 | * **An evil formal modeling project.** Use the features of the simulator 455 | as the basis for formally specifying an elevator control algorithm 456 | using a tool like TLA+. Once you've convinced yourself that the 457 | algorithm is correct, code it in your favorite language and watch 458 | the elevator work on the first try. If not, you get an F. 459 | 460 | * **An evil elevator demonstration tool.** Maybe you're trying to 461 | explain some kind of puzzler problem involving elevators. Your 462 | description involving the ending of the Blues Brothers fell flat, so 463 | you've decided that a live demo would be better. Fire up Lifty and say 464 | "now visualize yourself riding this elevator in the Building of the 465 | Imagination--as perfectly embodied by this text-only program running 466 | in the terminal." 467 | 468 | ### Projects Involving the Simulator Program Itself 469 | 470 | * **Adaptation into adjacent evil.** Could you modify this code into 471 | something like a simulation of a hard drive? Yeah, maybe. That 472 | seems like a good way to make students of an operating systems 473 | course cry. John Azariah, clearly an evil person himself, has 474 | suggested to me that an elevator might not be too unlike a Turing 475 | machine. 476 | 477 | * **An evil code porting project.** The simulator is written in Rust. 478 | Give students a project to simply port it to C++ or Python. Emphasis 479 | on the word "simply." 480 | 481 | * **An evil refactoring project.** Refactor the simulator so that it 482 | corresponds to an eight car elevator system in a 20 story building. 483 | "Claude? Claude?! Is this mic on?" 484 | 485 | * **An evil Rust refactoring project.** Refactor the simulator 486 | so that's based on a different Rust programming approach such as 487 | asynchronous I/O. 488 | 489 | * **An evil code review project.** Carefully review the code in 490 | the simulator and think of ways that it be improved and/or 491 | simplified without giving up any functionality. Can I think of 492 | some possible improvements? Yes, I can. But, can you? 493 | 494 | * **An evil physics project.** Imagine an elevator in a 100 story 495 | building where the elevator now accelerates up to about 30mph. Is 496 | it going to stop instantly when you press a button? No, it's not. 497 | There are going to be forces. And those forces say that you 498 | should probably model the elevator hoist using a differential 499 | equation instead of a simple "Up", "Down", "Off" modal selection. 500 | 501 | * **A probably not-so-evil example of event-driven Rust programming.** 502 | I wouldn't claim to be any kind of Rust expert, but the simulator 503 | involves a variety of Rust programming features including structs, 504 | enums, `Option<>`, `Result<>`, I/O, channels, threads, sockets, and 505 | timers. That's probably at least two or three Stack Overflow 506 | questions answered all in one place. 507 | 508 | ## Feedback 509 | 510 | This software is provided "as is." However, if you've found a bug or 511 | have ideas for improvement, feel free to submit a bug report or PR. 512 | Be advised that I do *NOT* intend to make this an official open source 513 | package or something that could be installed via a package manager. 514 | It's purely for education purposes and my own amusement. 515 | 516 | P.S. If you like stuff like this, come take a [course](https://dabeaz.com/courses.html). 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | -------------------------------------------------------------------------------- /images/elevator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabeaz/lifty/6d8fc03fa7835eb2d2b092faad01d2e9954daa1e/images/elevator.jpg -------------------------------------------------------------------------------- /lifty.rs: -------------------------------------------------------------------------------- 1 | /* 2 | lifty.rs 3 | 4 | Author: David Beazley (https://www.dabeaz.com) 5 | Source: https://github.com/dabeaz/lifty 6 | 7 | Copyright (C) 2025 8 | All Rights Reserved 9 | 10 | This code may be freely copied, modified, and used for EDUCATIONAL 11 | PURPOSES ONLY provided that the above attribution, URLs, and copyright 12 | notice are preserved in all copies. 13 | ----------------------------------------------------------------------------- 14 | 15 | Hi, I'm a Lifty, a hardware simulator for a basic 5-floor elevator 16 | system with a single elevator car. I have the following hardware 17 | features: 18 | 19 | - A motor that makes the car go up and down. 20 | - A door that can open and close. 21 | - A panel of 5 buttons inside the elevator car. 22 | - Up request buttons on floors 1-4. 23 | - Down request buttons on floors 2-5. 24 | - A direction indicator light on each floor. 25 | 26 | Residents of the building interact with me by pressing buttons. 27 | This is done by typing the following commands at the keyboard: 28 | 29 | Pn - Press button for floor n in the elevator car 30 | Un - Press up button on floor n 31 | Dn - Press down button on floor n 32 | 33 | Sadly, I don't have any brains of my own to know what to do 34 | when a button is pressed. However, I can interact with a 35 | separate control program via UDP sockets. 36 | 37 | Resident -> [ Lifty ] <--------> [ Control ] 38 | buttons UDP 39 | 40 | I will send the following event messages to the controller: 41 | 42 | Pn - Panel button for floor n was pressed 43 | Un - Up button on floor n was pressed 44 | Dn - Down button floor n was pressed 45 | An - Approaching floor n (still in motion) 46 | Sn - Stopped at floor n (safe to open door) 47 | On - Door open on floor n (doors have fully opened) 48 | Cn - Door closed on floor n (now safe to move) 49 | 50 | I understand the following commands from the controller 51 | 52 | MU - Start moving up 53 | MD - Start moving down 54 | S - Stop at the next floor (generates Sn event when stopped) 55 | DO - Open door (will generate On event when done) 56 | DC - Close the door (will generate Cn event when done) 57 | CPn - Clear panel button n 58 | CUn - Clear up button n 59 | CDn - Clear down button n 60 | IUn - Set indicator light on floor n to "up" 61 | IDn - Set indicator light on floor n to "down" 62 | CIn - Clear the indicator light on floor n 63 | R - Reset 64 | 65 | Although I don't have any brains, I am programmed with some 66 | some basic protection features and am prone to crashing 67 | if I'm given bad instructions. If I crash, I'll enter a 68 | permanent crashed state that can only be reset by rebooting 69 | the control software and having it send a reset (R) command. 70 | 71 | Your challenge, should you choose to accept it--write a control 72 | program that runs the elevator algorithm and prove that (a) it works 73 | like an actual elevator and (b) it will never cause the elevator to 74 | crash. Good luck! 75 | 76 | */ 77 | 78 | // This is a single-file Rust program with no dependencies. 79 | // Compile using `rustc lifty.rs`. 80 | 81 | // Network ports for myself and the control program. 82 | const MY_ADDRESS: &str = "127.0.0.1:10000"; 83 | const CONTROL_ADDRESS: &str = "127.0.0.1:11000"; 84 | 85 | // Internal timing 86 | const TICKS_PER_FLOOR: usize = 40; 87 | const TICKS_FOR_DOOR: usize = 20; 88 | const APPROACH_TICKS: usize = 10; 89 | const TICK_INTERVAL: u64 = 100; 90 | 91 | // Hoist motor status 92 | #[derive(Debug, Clone, PartialEq)] 93 | enum Motor { 94 | Up, 95 | Down, 96 | Off, 97 | } 98 | 99 | // Door status 100 | #[derive(Debug, Clone, PartialEq)] 101 | enum Door { 102 | Opening, 103 | Open, 104 | Closing, 105 | Closed, 106 | } 107 | 108 | // Direction indicators 109 | #[derive(Debug, Clone, PartialEq)] 110 | enum Indicator { 111 | Up, 112 | Down, 113 | Off, 114 | } 115 | 116 | #[derive(Debug)] 117 | struct Elevator { 118 | pub floor: usize, 119 | pub panel_buttons: [bool; 5], // Buttons in the car 120 | pub up_buttons: [bool; 5], // Up buttons in the building 121 | pub down_buttons: [bool; 5], // Down buttons in the building 122 | pub indicator: Indicator, // Indicator light status 123 | pub indicator_floor: usize, 124 | pub clock: usize, 125 | pub motor: Motor, 126 | pub door: Door, 127 | pub stopping: bool, 128 | pub crashed: bool, 129 | } 130 | 131 | impl Elevator { 132 | fn new() -> Elevator { 133 | Elevator { 134 | floor: 1, 135 | panel_buttons: [false, false, false, false, false], 136 | up_buttons: [false, false, false, false, false], 137 | down_buttons: [false, false, false, false, false], 138 | indicator: Indicator::Off, 139 | indicator_floor: 1, 140 | clock: 0, 141 | motor: Motor::Off, 142 | door: Door::Closed, 143 | stopping: false, 144 | crashed: false, 145 | } 146 | } 147 | 148 | fn reset(&mut self) { 149 | self.floor = 1; 150 | self.panel_buttons = [false, false, false, false, false]; 151 | self.up_buttons = [false, false, false, false, false]; 152 | self.down_buttons = [false, false, false, false, false]; 153 | self.indicator = Indicator::Off; 154 | self.indicator_floor = 1; 155 | self.clock = 0; 156 | self.motor = Motor::Off; 157 | self.door = Door::Closed; 158 | self.stopping = false; 159 | self.crashed = false; 160 | } 161 | 162 | fn crash(&mut self, reason: &str) { 163 | println!("\nCRASH! : {reason}"); 164 | self.crashed = true; 165 | } 166 | 167 | fn as_string(&self) -> String { 168 | let mut ps = String::from("P:"); 169 | for (n, floor) in self.panel_buttons.iter().enumerate() { 170 | if *floor { 171 | ps.push(char::from_u32(49 + n as u32).unwrap()); 172 | } else { 173 | ps.push('-'); 174 | } 175 | } 176 | let mut us = String::from("U:"); 177 | for (n, floor) in self.up_buttons.iter().enumerate() { 178 | if *floor { 179 | us.push(char::from_u32(49 + n as u32).unwrap()); 180 | } else { 181 | us.push('-'); 182 | } 183 | } 184 | let mut ds = String::from("D:"); 185 | for (n, floor) in self.down_buttons.iter().enumerate() { 186 | if *floor { 187 | ds.push(char::from_u32(49 + n as u32).unwrap()); 188 | } else { 189 | ds.push('-'); 190 | } 191 | } 192 | let indicator = if self.indicator_floor == self.floor { 193 | match self.indicator { 194 | Indicator::Up => "^^", 195 | Indicator::Down => "vv", 196 | Indicator::Off => "--", 197 | } 198 | } else { 199 | "--" 200 | }; 201 | let status = if self.crashed { 202 | "CRASH" 203 | } else if self.stopping && self.clock >= (TICKS_PER_FLOOR - APPROACH_TICKS) { 204 | "STOPPING" 205 | } else if self.motor == Motor::Up { 206 | "UP" 207 | } else if self.motor == Motor::Down { 208 | "DOWN" 209 | } else if self.door == Door::Opening { 210 | "OPENING" 211 | } else if self.door == Door::Open { 212 | "OPEN" 213 | } else if self.door == Door::Closing { 214 | "CLOSING" 215 | } else if self.door == Door::Closed { 216 | "CLOSED" 217 | } else { 218 | panic!("Can't determine status") 219 | }; 220 | format!( 221 | "[ FLOOR {} | {status:8} {indicator} | {ps} | {us} | {ds} ]", 222 | self.floor 223 | ) 224 | } 225 | 226 | fn set_panel_button(&mut self, floor: usize) { 227 | self.panel_buttons[floor - 1] = true; 228 | } 229 | 230 | fn clear_panel_button(&mut self, floor: usize) { 231 | if !self.panel_buttons[floor - 1] { 232 | self.crash("panel button not previously set"); 233 | } else { 234 | self.panel_buttons[floor - 1] = false; 235 | } 236 | } 237 | 238 | fn set_up_button(&mut self, floor: usize) { 239 | self.up_buttons[floor - 1] = true; 240 | } 241 | 242 | fn clear_up_button(&mut self, floor: usize) { 243 | if !self.up_buttons[floor - 1] { 244 | self.crash("up button not previously set"); 245 | } else { 246 | self.up_buttons[floor - 1] = false; 247 | } 248 | } 249 | 250 | fn set_down_button(&mut self, floor: usize) { 251 | self.down_buttons[floor - 1] = true; 252 | } 253 | 254 | fn clear_down_button(&mut self, floor: usize) { 255 | if !self.down_buttons[floor - 1] { 256 | self.crash("down button not previously set"); 257 | } else { 258 | self.down_buttons[floor - 1] = false; 259 | } 260 | } 261 | 262 | fn set_indicator(&mut self, floor: usize, status: Indicator) { 263 | if self.indicator != Indicator::Off && status != Indicator::Off { 264 | self.crash("direction indicator already illuminated"); 265 | } else if self.indicator == Indicator::Off && status == Indicator::Off { 266 | self.crash("direction indicator already off"); 267 | } else { 268 | self.indicator = status; 269 | self.indicator_floor = floor; 270 | } 271 | } 272 | 273 | fn set_motor(&mut self, status: Motor) { 274 | if self.door != Door::Closed { 275 | self.crash("motor command received while doors open"); 276 | return; 277 | } 278 | if self.motor == Motor::Up && status == Motor::Down { 279 | self.crash("violent direction switch (up->down)"); 280 | return; 281 | } 282 | if self.motor == Motor::Down && status == Motor::Up { 283 | self.crash("violent direction switch (down->up)"); 284 | return; 285 | } 286 | if self.motor != status { 287 | self.motor = status; 288 | self.clock = 0; 289 | } else if status == Motor::Up { 290 | self.crash("already moving up"); 291 | } else if status == Motor::Down { 292 | self.crash("already moving down"); 293 | } 294 | } 295 | 296 | fn set_door(&mut self, status: Door) { 297 | if self.motor != Motor::Off { 298 | self.crash("door command received while moving"); 299 | return; 300 | } 301 | if self.door == Door::Closing && status != Door::Closed { 302 | self.crash("door command received while closing"); 303 | return; 304 | } 305 | if self.door == Door::Opening && status != Door::Open { 306 | self.crash("door command received while opening"); 307 | return; 308 | } 309 | if self.door == Door::Open && status == Door::Opening { 310 | self.crash("door already open"); 311 | return; 312 | } 313 | if self.door == Door::Closed && status == Door::Closing { 314 | self.crash("door already closed"); 315 | return; 316 | } 317 | self.door = status; 318 | self.clock = 0; 319 | } 320 | 321 | fn handle_command(&mut self, cmd: &str) -> Option { 322 | if cmd == "R" { 323 | self.reset(); 324 | return None; 325 | } 326 | if self.crashed { 327 | return None; 328 | } 329 | match cmd { 330 | // Button presses 331 | "P1" | "P2" | "P3" | "P4" | "P5" => { 332 | self.set_panel_button(cmd[1..].parse().unwrap()); 333 | Some(cmd.to_string()) 334 | } 335 | "U1" | "U2" | "U3" | "U4" => { 336 | self.set_up_button(cmd[1..].parse().unwrap()); 337 | Some(cmd.to_string()) 338 | } 339 | "U5" | "CU5" => { 340 | self.crash("No up button on top floor"); 341 | None 342 | } 343 | "D2" | "D3" | "D4" | "D5" => { 344 | self.set_down_button(cmd[1..].parse().unwrap()); 345 | Some(cmd.to_string()) 346 | } 347 | "D1" | "CD1" => { 348 | self.crash("No down button on bottom floor"); 349 | None 350 | } 351 | // Clear buttons 352 | "CP1" | "CP2" | "CP3" | "CP4" | "CP5" => { 353 | self.clear_panel_button(cmd[2..].parse().unwrap()); 354 | None 355 | } 356 | "CU1" | "CU2" | "CU3" | "CU4" => { 357 | self.clear_up_button(cmd[2..].parse().unwrap()); 358 | None 359 | } 360 | "CD2" | "CD3" | "CD4" | "CD5" => { 361 | self.clear_down_button(cmd[2..].parse().unwrap()); 362 | None 363 | } 364 | // Direction indicator lights 365 | "IU1" | "IU2" | "IU3" | "IU4" => { 366 | self.set_indicator(cmd[2..].parse().unwrap(), Indicator::Up); 367 | None 368 | } 369 | "IU5" => { 370 | self.crash("No up indicator light on top floor"); 371 | None 372 | } 373 | "ID2" | "ID3" | "ID4" | "ID5" => { 374 | self.set_indicator(cmd[2..].parse().unwrap(), Indicator::Down); 375 | None 376 | } 377 | "ID1" => { 378 | self.crash("No down indicator light on bottom floor"); 379 | None 380 | } 381 | "CI1" | "CI2" | "CI3" | "CI4" | "CI5" => { 382 | self.set_indicator(cmd[2..].parse().unwrap(), Indicator::Off); 383 | None 384 | } 385 | // Motor (from control) 386 | "MU" => { 387 | self.set_motor(Motor::Up); 388 | None 389 | } 390 | "MD" => { 391 | self.set_motor(Motor::Down); 392 | None 393 | } 394 | "S" => { 395 | if self.stopping { 396 | self.crash("Already made a request to stop"); 397 | } else if self.motor != Motor::Off { 398 | // If we can safely stop we will. 399 | if self.clock <= TICKS_PER_FLOOR - APPROACH_TICKS { 400 | self.stopping = true; 401 | } 402 | } else { 403 | self.crash("Request to stop, but not moving"); 404 | } 405 | None 406 | } 407 | // Door commands (from control) 408 | "DO" => { 409 | self.set_door(Door::Opening); 410 | None 411 | } 412 | "DC" => { 413 | self.set_door(Door::Closing); 414 | None 415 | } 416 | // Clock 417 | "T" => self.handle_tick(), 418 | _ => { 419 | self.crash("Unrecognized command"); 420 | None 421 | } 422 | } 423 | } 424 | 425 | fn handle_tick(&mut self) -> Option { 426 | self.clock += 1; 427 | if self.motor == Motor::Up { 428 | if self.floor >= 5 { 429 | self.crash("Hit the roof!"); 430 | } else if self.clock == (TICKS_PER_FLOOR - APPROACH_TICKS) { 431 | return Some(format!("A{}", self.floor + 1)); 432 | } else if self.clock >= TICKS_PER_FLOOR { 433 | self.floor += 1; 434 | self.clock = 0; 435 | if self.stopping { 436 | self.set_motor(Motor::Off); 437 | self.stopping = false; 438 | return Some(format!("S{}", self.floor)); 439 | } 440 | } 441 | } else if self.motor == Motor::Down { 442 | if self.floor <= 1 { 443 | self.crash("Hit the ground!"); 444 | } else if self.clock == (TICKS_PER_FLOOR - APPROACH_TICKS) { 445 | return Some(format!("A{}", self.floor - 1)); 446 | } else if self.clock >= TICKS_PER_FLOOR { 447 | self.floor -= 1; 448 | self.clock = 0; 449 | if self.stopping { 450 | self.set_motor(Motor::Off); 451 | self.stopping = false; 452 | return Some(format!("S{}", self.floor)); 453 | } 454 | } 455 | } else if self.door == Door::Closing { 456 | if self.clock > TICKS_FOR_DOOR { 457 | self.set_door(Door::Closed); 458 | return Some(format!("C{}", self.floor)); 459 | } 460 | } else if self.door == Door::Opening { 461 | if self.clock > TICKS_FOR_DOOR { 462 | self.set_door(Door::Open); 463 | return Some(format!("O{}", self.floor)); 464 | } 465 | } 466 | None 467 | } 468 | } 469 | 470 | // Runtime environment for the simulator 471 | 472 | use std::io; 473 | use std::io::Write; 474 | use std::net::UdpSocket; 475 | use std::sync::mpsc; 476 | use std::sync::mpsc::{Receiver, Sender}; 477 | use std::{thread, time}; 478 | 479 | enum Command { 480 | UserInput(String), 481 | Internal(String), 482 | } 483 | 484 | fn read_stdin(tx: Sender) -> ! { 485 | loop { 486 | let mut buffer = String::new(); 487 | io::stdin().read_line(&mut buffer).unwrap(); 488 | let cmd = Command::UserInput(buffer.trim().to_uppercase()); 489 | tx.send(cmd).unwrap(); 490 | } 491 | } 492 | 493 | fn generate_clock_ticks(tx: Sender) -> ! { 494 | loop { 495 | thread::sleep(time::Duration::from_millis(TICK_INTERVAL)); 496 | tx.send(Command::Internal(String::from("T"))).unwrap(); 497 | } 498 | } 499 | 500 | fn read_socket(address: &str, tx: Sender) -> ! { 501 | let socket = UdpSocket::bind(address).unwrap(); 502 | loop { 503 | let mut buf = [0; 2000]; 504 | match socket.recv_from(&mut buf) { 505 | Ok((n, _)) => { 506 | let cmds = String::from_utf8((&buf[0..n]).to_vec()).unwrap(); 507 | for cmd in cmds.lines() { 508 | tx.send(Command::Internal(cmd.to_string())).unwrap(); 509 | } 510 | } 511 | Err(e) => panic!("IO Error: {}", e), 512 | } 513 | } 514 | } 515 | 516 | fn spawn_threads() -> Receiver { 517 | let (tx, rx) = mpsc::channel::(); 518 | let itx = tx.clone(); 519 | thread::spawn(move || read_stdin(itx)); 520 | let ttx = tx.clone(); 521 | thread::spawn(move || generate_clock_ticks(ttx)); 522 | thread::spawn(move || read_socket(MY_ADDRESS, tx)); 523 | rx 524 | } 525 | 526 | fn main() { 527 | let mut elev = Elevator::new(); 528 | let command_channel = spawn_threads(); 529 | let mut last = String::new(); 530 | let out_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); 531 | 532 | println!("Welcome! I'm Lifty--a simulated elevator in a 5-floor building.\n"); 533 | println!("I'm just hardware, but I have buttons (type below and hit return):\n"); 534 | println!(" Pn - Floor n button on panel inside car"); 535 | println!(" Un - Up button on floor n"); 536 | println!(" Dn - Down button on floor n\n"); 537 | println!("If something goes wrong, I'll crash and you'll have to call"); 538 | println!("maintenance to restart the elevator control program.\n"); 539 | 540 | let mut print_newline = false; 541 | loop { 542 | let es = elev.as_string(); 543 | if es != last { 544 | if print_newline { 545 | print!("\n"); 546 | } 547 | print!("{} : ", es); 548 | std::io::stdout().flush().unwrap(); 549 | last = es; 550 | } 551 | match command_channel.recv() { 552 | Ok(recvcmd) => { 553 | let cmd = match recvcmd { 554 | Command::UserInput(cmd) => { 555 | print_newline = false; 556 | last = String::from(""); 557 | cmd 558 | } 559 | Command::Internal(cmd) => { 560 | if cmd != "T" { 561 | print_newline = false; 562 | println!("recv: {cmd}"); 563 | last = String::from(""); 564 | } else { 565 | print_newline = true; 566 | } 567 | cmd 568 | } 569 | }; 570 | if let Some(outcmd) = elev.handle_command(&cmd) { 571 | out_socket 572 | .send_to(outcmd.as_bytes(), CONTROL_ADDRESS) 573 | .expect("couldn't send data"); 574 | } 575 | } 576 | Err(e) => { 577 | println!("{:?}", e); 578 | break; 579 | } 580 | }; 581 | } 582 | } 583 | --------------------------------------------------------------------------------