Project: the fail-safe controller

A command program that authenticates, validates every command, and is tested against a plan.

F6.7Robust programsGCSE25 min

Do this lesson in the simulator

A robot in a warehouse takes commands from operators all day. Some are mistyped, some come from people who should not be giving commands, and some would drive it into a wall. In this project you build a command controller for BugBot that survives all of that: it authenticates the operator, validates every command, refuses unsafe ones, and is tested against a plan.

The brief

The controller asks for a username and password, allowing three attempts. Once logged in, it repeatedly asks Command? and obeys valid commands:

  • forward <cm>, back <cm>, left <cm> and right <cm>, with a distance from 1 to 50;
  • beep;
  • quit, which prints bye and ends the program.

Anything else, including a missing or out-of-range distance, prints invalid command and beeps low, and the controller asks again. It never crashes, whatever is typed.

Decompose it

fail-safe controller
├── login()                  up to three attempts; returns the username or None
├── parse(command)           checks a command; returns (word, distance) or None
└── main loop                ask, parse, obey or refuse, until quit

Validation lives in parse, so every command goes through the same checks. The main loop only acts on commands that parse has approved.

The fail-safe controller: log in, then ask for a command, check it with parse, and obey it, refuse it or stop, asking again every time until quitlogin()three attemptsokNone: nocommandsCommand?askparsecheck itquitprint bye, stoprefuseinvalid command,low beepbeepobeydrive that farthen ask again
Every command goes through parse, and only what parse approves is obeyed. Whatever is typed, the controller either obeys, refuses or stops on quit, and then asks again, so there is no way for a bad command to crash it.

Step 1: validate one command

Write and test parse before anything else. It must handle every possible string without crashing:

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

MOVES = ["forward", "back", "left", "right"]

def parse(command):
    """Return (word, distance) for a valid command, (word, 0) for beep or quit, or None."""
    parts = command.strip().lower().split()
    if len(parts) == 1 and parts[0] in ["beep", "quit"]:
        return (parts[0], 0)
    if len(parts) != 2 or parts[0] not in MOVES:
        return None
    if not parts[1].isdigit() or not 1 <= int(parts[1]) <= 50:
        return None
    return (parts[0], int(parts[1]))

tests = [("forward 20", ("forward", 20)), ("beep", ("beep", 0)), ("fly 10", None),
         ("right 500", None), ("left", None), ("", None), ("BACK 5", ("back", 5))]
for data, expected in tests:
    print(repr(data), "pass" if parse(data) == expected else "FAIL")

Run this in the simulator

The test list is the test plan, turned into code: normal commands, invalid values, and erroneous input like an empty string. It has no boundary tests yet: add tests for 1, 50, 0 and 51 before you move on.

The checks inside parse in order, and where each of the lesson's test commands stopstidy upstrip, lower,split on spacesone word:beep or quit?two words,a move first?a numberfrom 1 to 50?valid moveword anddistancenoyesyescommandyes(word, 0)noNonenoNonethe tests stop here:"forward 20""BACK 5""beep""fly 10""left""""right 500"
Each check only lets a command on if it passed the one before, so by the time int() is used the second word is known to be digits. Every test in the list stops where it should: three at the check for two words, and "BACK 5" gets through because it is made lower case first.

Step 2: log in

login() is the function from lesson F6.2. Copy it, and test it with a wrong password followed by a right one.

Step 3: the main loop

The loop asks for a command, calls parse, and then either obeys or refuses:

while True:
    result = parse(input("Command? "))
    if result is None:
        print("invalid command")
        tone(200, 0.3)
    elif result[0] == "quit":
        print("bye")
        break
    ...

Fill in the rest: beep, and the four moves.

Test plan for the whole program

Before running the finished controller, write the expected result of each test:

Test Typed Kind Expected
1 a wrong password, then the right one authentication access denied, then welcome
2 forward 20 normal drives 20 cm
3 fly 10 invalid invalid command, low beep
4 right 500 invalid invalid command
5 an empty line erroneous invalid command, no crash
6 quit normal bye, program ends

The task runs almost exactly this plan.

Task: the fail-safe controller

Build the controller from the brief with operators = {"ada": "robot42"}. Print access denied for a wrong login, welcome <name> for a right one, invalid command for every command it refuses, and bye on quit. The task types: ada and oops, then ada and robot42, then the commands forward 20, fly 10, right 500, an empty line, right 15, beep and quit.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

operators = {"ada": "robot42"}
MOVES = ["forward", "back", "left", "right"]

Challenges

  1. Refuse a forward command that would take the robot closer than 10 cm to a wall, using distance().
  2. Keep a log of every command in a list, with whether it was obeyed, and print it at quit.
  3. Log out automatically after five invalid commands in a row, and ask for the login again.