Merge pull request #18 from ZeusWPI/getting-started

Make it easier for new people to get started
This commit is contained in:
redfast00 2021-02-01 18:08:18 +01:00 committed by GitHub
commit 2a48261c7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 75 deletions

View file

@ -2,17 +2,26 @@
You see an armed time bomb but don't know how to disarm it. Your friends found a manual to defuse the bomb and you have them on call. This is the premise of the game OBUS, a hardware project by [Zeus WPI](https://zeus.ugent.be). Inspired by the amazing software game [Keep Talking and Nobody Explodes](https://www.keeptalkinggame.com/).
# Get started writing a module
## Get started writing a module
These are the instructions for building your own OBUS module with an Arduino Nano v3 and the custom PCB. If
you're using other hardware, you might need to do some things differently.
0. Read the "[Getting started guide](docs/GETTING_STARTED.md)" to get a general idea of how OBUS works.
1. [Install](https://www.arduino.cc/en/Guide/#install-the-arduino-desktop-ide) the Arduino IDE.
2. Clone this repository with Git in a permanent location on your drive.
3. Symlink the library: `ln -s /ABSOLUTE/PATH/TO/REPO/lib /PATH/TO/Arduino/libraries/obus`
(on most Linux distro's, this the Arduino folder is in `$HOME/Arduino`)
4. Follow [these steps](https://github.com/autowp/arduino-mcp2515/#software-usage) to install the CAN library
5. Execute `./src/new_module.sh` to create a new module
5. Execute `./src/new_module.sh` to create a new module. When asked for a type, you'll probably want to choose 'puzzle'.
6. Edit the newly generated .ino file, either in the Arduino IDE or in your own editor.
7. In the Arduino IDE, select the correct board (Arduino Nano) and processor (ATmega328P (Old Bootloader)). After that,
flash your code to the Arduino and test it out.
# Background
## Game
TODO insert picture here
## Background
### Game
The game is played by at least two players. The goal is to defuse a bomb,
this is accomplished by defusing every module on that bomb before the bomb
@ -25,49 +34,15 @@ There are two roles:
These two roles can communicate with each other. To successfully defuse the bomb, they must
communicate efficiently and clearly. If a mistake is made, the team gets a strike.
If they get too many strikes, the bomb explodes, even if the timer hasn't run out yet.
If they get too many strikes or the timer runs out, the bomb explodes.
## Implementation details
### Implementation goals
Now we want to implement this game in hardware. As in the computer version, we want this game to be modular:
it should be easy to 1) make new modules and 2) attach them to a bomb. To do this, we need to settle on
a protocol, both in hardware and in software.
- It should be easy to add new modules, both hardware- and software-wise
- It should be easy to build your own module and get it working with the rest of the game
- Every run of the game should be different
- It should be possible for multiple people at the same time to develop a new module
The idea is to have one bomb controller that keeps track of the timer, the amount of strikes and of whether
the bomb has been successfully disarmed, and to have multiple modules that have one or more challenges on them
that need to be solved.
### Hardware
The bomb controller and modules have to be able to communicate with each other. Ideally, we would like a hardware bus where it is easy to add more modules. We would also like to minimise the amount of wires that are needed.
Multiple protocols were considered:
- I2C: very standard, most microcontrollers have this built in; unfortunately, limited in the amount of nodes that can connect to the same network (255), limited in distance between nodes (about 1 meter), and the bus needs 4 wires (GND, VCC and two data lines)
- SPI: needs even more wires, and requires a separate wire per module
- Serial: this is not a bus architecture, so a lot of wires will need to be used
But eventually, CAN was picked. CAN is widely used in vehicles and has several desired properties:
- We only need two wires (CAN uses a differential pair)
- Distance can be up to 500m
- There is built-in packet collision avoidance and per-node priorities
- CAN modules are very cheap
- There are existing Arduino libraries for the CAN module we'll be using
The payload of a CAN packet is 8 bytes long, this should be enough.
## Software
We needed to decide on a protocol to communicate between the bomb controller and the modules (and possibly also between modules?).
Some things we had to consider:
- payload is 8 bytes per packet
- packets can be delayed or not received on every node, so detection of this and retransmission might be needed: if the bomb interactor solves a module and the packet that communicates this with the bomb does not get delivered to the controller, the bomb will still go off, even if all modules have been solved
- we can't send an infinite amount of packets; the higher our bitrate is, the shorter our wires need to be
## Development setup
In the Arduino IDE, select the correct board (Arduino Nano) and processor (ATmega328P (Old Bootloader)).
We use [this](https://github.com/autowp/arduino-mcp2515/) library for CAN communications. See [this](https://github.com/autowp/arduino-mcp2515/#software-usage) header for 3 simple steps on how to use it in the arduino IDE

123
docs/GETTING_STARTED.md Normal file
View file

@ -0,0 +1,123 @@
# Getting started
This is a guide for writing your own puzzle module. We'll first start by
describing an OBUS game, then describe the minimum things you need to implement
to get a working puzzle module. We'll then finish by explaining some nice-to-haves
(like using info modules or game state in a puzzle module).
## OBUS game
### Parts of the game
The goal of OBUS is to defuse a bomb by communicating clearly. There are (at least) two players: the defuser and the expert.
The defuser can see and interact with the bomb, the expert can read the manual that describes how to defuse the bomb.
You win the game by defusing the bomb, you lose if the bomb explodes. The bomb is defused when all puzzle modules are
solved; the bomb explodes if the time runs out or too much mistakes ("strikes") have been made.
The bomb consists of a controller and multiple modules: the controller shows the time left, the amount of mistakes made
while defusing the bomb (strikes) and the amount of strikes left. It's responsible for starting games, enumerating modules,
keeping track of game state, ...
The modules on the other hand are the parts of the bomb that can be interacted with by the defuser. There are several kinds of
modules: puzzle modules, needy modules and info modules.
Puzzle modules are the most common kind of modules. The bomb is defused as soon as all puzzle modules are solved.
Puzzle modules are generally solved by having the defuser observe some kind of state of the game, then having them
communicate that information to the expert. The expert then uses this information and the manual to communicate a set of
instructions to the defuser. If the instructions are correct and the defuser executes them correctly, the module is solved.
If one of the two players makes an error and the defuser executes an incorrect action, the module generates a "strike".
An example of a puzzle module is an RGB led with a red and a green button under it. The defuser looks at the module and
sees that the RGB led is colored blue. They then tell that to the expert, who then looks up this module in the manual.
The manual instructs to press the green button if the color starts with the letter "b" or "o". The expert then asks the
defuser to press the green button. When the defuser does this, the module is solved.
There are also needy modules. These can't be "solved", but they can generate strikes: they require periodic action
from the defuser to prevent them from generating a strike.
An example of a needy module is a buzzer with a red button. If the buzzer goes off, the defuser has to press the button within 5 seconds
or a strike is generated. Needy modules can also have the expert look something up in the manual (if you do this, make sure
to balance the time needed by the defuser against the time between the signal that action is needed and the strike).
Finally, there are also info modules. These don't serve a purpose on their own, they are only useful in combination with
puzzle or needy modules. They provide extra information to those modules. These modules were added to the game
to make it possible to make puzzle/needy modules with less hardware: puzzle modules generally consist of two "parts":
a part that shows information to the defuser that needs to be communicated to the expert and a part that the defuser
can interact with to solve the module. With info modules, this first part can be moved to a dedicated module that can
be shared between multiple puzzle modules.
An example of an info module is an LCD display that displays a serial number. There can then be a puzzle module with
just two buttons on it. When the defuser is solving that module, the expert reads in the manual "press the second button
if the last digit of the serial number is even, otherwise press the first". The expert then asks for the serial number
and after that has the defuser press the correct button, solving the puzzle module.
### Hardware
In our OBUS implementation, every module has its own microcontroller and CAN module. CAN is a hardware protocol
that allows the modules to communicate with the controller (and each other). To write your own module, you don't
have to know how CAN works, this is all abstracted away with the OBUS framework. The only thing you need to do
is to set the type (puzzle/needy/info) of the module and it's ID. The combination type/ID needs to be unique across
the OBUS game, so in order to avoid collisions, you can register your module in TODO LINK MOANA and get an ID.
In addition to a CAN module, every module also has an RGB LED. This LED is used to show both the
state of the module and to indicate if the module has an error.
If the module is solved, the LED is green, if a module generates a strike, the module blinks red. There are also several blinking
patterns for errors. In order to save pins on the microcontroller for implementing the puzzle, only the red and green
parts of the RGB led are connected. You CANNOT use the RGB LED for the puzzle itself: this would be confusing for the player
and when debugging a game.
### A sample game
This is a description of a sample game with only one module: the `puzzle_testmodule_buttons`. When reading this part, it's useful to have the `puzzle_testmodule_buttons.ino` file next to you as well.
We'll start this story from the start: the puzzle module boots up.
It calls the `obus_module::setup` function to register it's module type and ID. It then keeps calling the `obus_module::loopPuzzle` in a loop.
It's important that the `loopPuzzle` function is executed very frequently without delays: if this doesn't happen enough, important CAN messages can get dropped.
Then after a while, a button gets pressed on the controller and the controller starts preparing to start the game. It first asks all info modules to broadcast their information. After some time, it then asks all puzzle/needy module to register themselves. The controller then confirms that that module will be active in the next round. After some time, the controller broadcasts that the game has started and starts counting down.
This broadcasts is received on the puzzle module and results in the `callback_game_start` function getting called the next time the
`obus::loopPuzzle` is called. The `callback_game_start` function is responsible for setting up the module for a new game. Here, we
randomly turn the blue LED on or off, and enable the main loop to start checking button presses.
After the `callback_game_start` function returns, the microcontroller stays inside the `loop` function, and keeps executing the `obus_module::loopPuzzle`
frequently. If the correct button is pressed, it calls the `obus_module::solve` function and turns off input checking. Turning off imput checking is
important: that way it's impossible for the puzzle module to generate strikes after it has been solved. If an incorrect button is pressed, it calls the
`obus_module::strike` function. That function will send a strike to the controller.
When `obus_module::solve` is called, the module sends an "I'm solved" CAN packet to the controller. The controller then sees that all modules have been solved,
and broadcasts a "solved" packet to every module. The next time the `obus_module::loopPuzzle` function gets called, the
`callback_game_stop` will be called. This function is responsible for tearing down the puzzle state. This should have the
same effect as just resetting the microcontroller, so if your state is too hard to clean up, you can just reboot the microcontroller.
## What you need to implement for your own puzzle module
- The setup code, initializing your microcontroller and setting the type and id of the module with `obus_module::setup`
- The main loop code. This should call the `loopPuzzle` function frequently so that all CAN packets can be handled.
If you are using calls to `delay()`, try to replace them with a timer (a variable that keeps track of when something should happen). That way the loop can continue executing, without being stuck in the `delay()` function.
- A call to the `obus_module::solve` function
- A description for the expert of how to defuse the module in `doc/index.md` of your module folder
- The `callback_game_start`, `callback_game_stop` and `callback_info` functions. These can be empty.
## More advanced puzzle modules
### Receiving game updates
It's possible to use the current state of the game in your module: the amount of
strikes (in microseconds), amount of allowed strikes and time left regularly get
broadcast to all modules. That way, you can spice up your puzzle, for example by
making the defuser press a button when the timer has a `1` in it, or by
having the instructions in the manual vary based on the amount of strikes.
TODO how will we do this?
### Using info modules in your puzzle/needy modules
Using info modules is a great way to reduce the amount of hardware components needed in a puzzle:
you can then still have puzzles that change every game, without having to add components that show information
to the defuser. Info modules broadcast their information to all modules in the phase before the game starts.
Every module can listen to these info messages with the `callback_info` callback. This callback will get
the ID of the info module and 7 bytes, as specified by the specific module. The `callback_info` function is
responsible for filtering out the info messages the module is interested in, and saving that info for the upcoming game.
For example, the serial number module has ID 1 and sends in its message 7 random character, chosen randomly from numbers
and the uppercase letters.

View file

@ -1,6 +1,6 @@
## Testmodule buttons
Don't press the red button. Press the green button to solve the module.
If the blue LED is lit, press the green button, otherwise press the red button.
### Credits
Module developed by redfast00.

View file

@ -1,17 +1,27 @@
// (c) 2020, redfast00
// See the LICENSE file for conditions for copying
// A red button connected to pin 5
// A green button connected to pin 6
// A blue led (with 330 ohm resistor) connected to pin 9
#include <obus_module.h>
#include <ezButton.h>
#define BLUE_LED 9
ezButton red_button(5);
ezButton green_button(6);
bool blue_state = false;
bool checking_input = false;
void setup() {
Serial.begin(115200);
obus_module::setup(OBUS_TYPE_PUZZLE, OBUS_PUZZLE_ID_DEVELOPMENT);
red_button.setDebounceTime(100);
green_button.setDebounceTime(100);
pinMode(BLUE_LED, OUTPUT);
}
obus_can::message message;
@ -23,22 +33,37 @@ void loop() {
red_button.loop();
green_button.loop();
if (red_button.getCount() > 0) {
red_button.resetCount();
obus_module::strike();
}
if (checking_input) {
if (red_button.getCount() > 0) {
if (blue_state) {
obus_module::strike();
} else {
obus_module::solve();
checking_input = blue_state = false;
}
}
if (green_button.getCount() > 0) {
green_button.resetCount();
obus_module::solve();
if (green_button.getCount() > 0) {
if (blue_state) {
obus_module::solve();
checking_input = blue_state = false;
} else {
obus_module::strike();
}
}
}
red_button.resetCount();
green_button.resetCount();
digitalWrite(BLUE_LED, blue_state);
}
void callback_game_start(uint8_t puzzle_modules_connected) {
// Intentionally emtpy
(void)puzzle_modules_connected;
(void)puzzle_modules_connected;
blue_state = random(0, 2);
checking_input = true;
}
void callback_game_stop() {
// Intentionally empty
blue_state = checking_input = false;
}

View file

@ -39,7 +39,19 @@ read author
cp -r -- template_module "$module_dir"
cd -- "$module_dir"
# Disallow % in fields that will be used in %-delimited ed substitution
# Fill in the blanks in the template
# `sed -i` is not portable so we create something like it ourselves
sed_inplace="`mktemp`"
cleanup_sed() { rm -f -- "$sed_inplace"; }
trap cleanup_sed EXIT
print '
filename="$1"
shift 1
tmpfile="`mktemp`"
sed "$@" -- "$filename" > "$tmpfile"
mv -- "$tmpfile" "$filename"
' > "$sed_inplace"
chmod 0500 -- "$sed_inplace" # Make executable
assert_no_percent() {
case "$1" in
*"%"*) println "$2 must not contain %" >&2; exit 1 ;;
@ -48,25 +60,11 @@ assert_no_percent() {
assert_no_percent "$author" "Author name"
assert_no_percent "$module_name" "Module name"
assert_no_percent "$module" "Module path name"
# Fill in the blanks in the template
# `sed -i` is not portable so we create something like it ourselves
reced() {
for file in "$1"/*; do
if [ -f "$file" ]; then
ed "$file" <<HERE
%s/{YEAR}/$(date +%Y)/
%s%{AUTHOR}%$author%
%s%{MODULE_NAME}%$module_name%
%s%{MODULE}%$module%
wq
HERE
elif [ -d "$file" ]; then
reced "$file"
fi
done
}
reced .
find . -type f -exec "$sed_inplace" '{}' -e "
s/{YEAR}/$(date +%Y)/
s%{AUTHOR}%$author%
s%{MODULE_NAME}%$module_name%
s%{MODULE}%$module%" \;
# Arduino IDE requires .ino sketches to have the same name as their directory
mv -- main.ino "$module.ino"