Merge pull request #1 from ZeusWPI/RobbeWeMissYou

Robbe we miss you
This commit is contained in:
redfast00 2020-08-26 20:02:31 +02:00 committed by GitHub
commit 247c4800bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 748 additions and 49 deletions

View file

@ -1,7 +1,15 @@
# Ontmijnen van een Bom vereist Uitstekende Samenwerking
OBUS is a real-life version of the multiplayer game "Keep Talking And Nobody Explodes"
OBUS is a real-life version of the multiplayer game "Keep Talking And Nobody Explodes".
# Get started writing a module
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`
4. TODO
# Background
## Game
The game is played by at least two players. The goal is to defuse a bomb,
@ -10,10 +18,10 @@ timer runs out.
There are two roles:
- Experts: this person can read the manual about how to defuse the bomb, but cannot see nor interact with the bomb
- Expert: this person can read the manual about how to defuse the bomb, but cannot see nor interact with the bomb
- Defuser: this person can see and interact with the bomb, but cannot read the manual
These two roles can communicate with each other. To succesfully defuse the bomb, they must
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.
@ -24,7 +32,7 @@ it should be easy to 1) make new modules and 2) attach them to a bomb. To do thi
a protocol, both in hardware and in software.
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 succesfully disarmed, and to have multiple modules that have one or more challenges on them
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
@ -48,9 +56,9 @@ The payload of a CAN packet is 8 bytes long, this should be enough.
## Software
We need to decide on a protocol to communicate between the bomb controller and the modules (and possibly also between modules?).
We needed to decide on a protocol to communicate between the bomb controller and the modules (and possibly also between modules?).
Some things we'll need to consider:
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

View file

@ -21,8 +21,8 @@ b bb bbbbbbbb
priority bit (so that each type can send priority messages if need be, 0 = high priority, 1 = low priority)
type:
- 0 controller; module ID is ignored
- 1 puzzle
- 0 module-ID 0: controller, >0: info
- 1 puzzel
- 2 needy
- 3 reserved for future use
@ -68,6 +68,15 @@ Types for controller:
- - - - - - - - - - - - - - - - - - -
Types for info:
These are managed by MOANA: the Modules of OBUS Authority for Numbers Assignment.
You can reach MOANA in the ~obus Mattermost channel.
(none yet)
- - - - - - - - - - - - - - - - - - -
Types for modules:
- 0 register

29
lib/module.cpp Normal file
View file

@ -0,0 +1,29 @@
#include "obus_can.h"
#include "module.h"
struct module this_module;
uint8_t strike_count;
void obusmodule_setup(uint8_t type, uint8_t id) {
this_module.type = type;
this_module.id = id;
obuscan_init();
strike_count = 0;
}
void obusmodule_loop() {
}
void obusmodule_strike() {
strike_count++;
obuscan_send_m_strike(this_module, strike_count);
}
void obusmodule_solve() {
obuscan_send_m_solved(this_module);
}

7
lib/module.hpp Normal file
View file

@ -0,0 +1,7 @@
void obusmodule_setup(uint8_t type, uint8_t id);
void obusmodule_loop();
void obusmodule_strike();
void obusmodule_solve();

180
lib/obus_can.cpp Normal file
View file

@ -0,0 +1,180 @@
#include <mcp2515.h>
#include "obus_can.h"
MCP2515 mcp2515(10);
bool obuscan_is_init = false;
uint16_t _encode_can_id(struct module mod, bool priority) {
assert(mod.type <= 0b11);
/* b bb bbbbbbbb
* type module ID
* priority bit
*/
return \
((uint16_t) (priority ? CAN_DOMINANT : CAN_RECESSIVE) << 10) | \
((uint16_t) mod.type << 8) | \
(uint16_t) mod.id;
}
void _decode_can_id(uint16_t can_id, struct module *mod, bool *priority) {
*priority = ((can_id >> 10) & 1) == CAN_DOMINANT;
mod->type = (can_id >> 8) & 0b11;
mod->id = can_id & 0b11111111;
assert(mod->type <= 0b11);
}
uint8_t obuscan_payload_type(uint8_t module_type, uint8_t msg_type) {
if (module_type == OBUS_TYPE_CONTROLLER) {
switch (msg_type) {
case OBUS_MSGTYPE_C_ACK:
case OBUS_MSGTYPE_C_HELLO:
return OBUS_PAYLDTYPE_EMPTY;
case OBUS_MSGTYPE_C_GAMESTART:
case OBUS_MSGTYPE_C_STATE:
case OBUS_MSGTYPE_C_SOLVED:
case OBUS_MSGTYPE_C_TIMEOUT:
case OBUS_MSGTYPE_C_STRIKEOUT:
return OBUS_PAYLDTYPE_GAMESTATUS;
default:
return false;
break;
}
// Module messages
} else {
switch (msg_type) {
case OBUS_MSGTYPE_M_STRIKE:
return OBUS_PAYLDTYPE_COUNT;
case OBUS_MSGTYPE_M_HELLO:
case OBUS_MSGTYPE_M_SOLVED:
return OBUS_PAYLDTYPE_EMPTY;
default:
return -1;
break;
}
}
}
void obuscan_init() {
obuscan_is_init = true;
mcp2515.reset();
mcp2515.setBitrate(CAN_50KBPS);
mcp2515.setNormalMode();
}
bool obuscan_receive(struct obus_message *msg) {
if (!obuscan_is_init) {
Serial.println(F("E Call obuscan_init first"));
return false;
}
struct can_frame receive_frame;
memset(&receive_frame.data, 0, CAN_MAX_DLEN);
MCP2515::ERROR status = mcp2515.readMessage(&receive_frame);
if (status != MCP2515::ERROR_OK) {
return false;
}
// Always at least OBUS message type required
if (receive_frame.can_dlc < 1) {
Serial.println(F("W Received illegal msg: payload <1"));
return false;
}
uint8_t msg_type = receive_frame.data[0];
struct module from;
bool priority;
_decode_can_id(receive_frame.can_id, &from, &priority);
// Controller messages
switch (obuscan_payload_type(from.type, msg_type)) {
case OBUS_PAYLDTYPE_EMPTY:
break;
case OBUS_PAYLDTYPE_GAMESTATUS:
if (receive_frame.can_dlc < 7) {
Serial.println(F("W Received illegal gamestatus msg: payload <7"));
return false;
}
msg->gamestatus.time_left =
((uint32_t) receive_frame.data[1] << 0x18) |
((uint32_t) receive_frame.data[2] << 0x10) |
((uint32_t) receive_frame.data[3] << 0x08) |
((uint32_t) receive_frame.data[4]);
msg->gamestatus.strikes = receive_frame.data[5];
msg->gamestatus.max_strikes = receive_frame.data[6];
break;
case OBUS_PAYLDTYPE_COUNT:
msg->count = receive_frame.data[1];
break;
default:
Serial.println(F("W Couldn't determine payload type"));
return false;
}
msg->from = from;
msg->priority = priority;
msg->msg_type = msg_type;
return true;
}
void obuscan_send(struct obus_message *msg) {
if (!obuscan_is_init) {
Serial.println(F("E Call obuscan_init first"));
return;
}
struct can_frame send_frame;
memset(&send_frame.data, 0, CAN_MAX_DLEN);
uint8_t length = 1;
send_frame.data[0] = msg->msg_type;
switch (obuscan_payload_type(msg->from.type, msg->msg_type)) {
case OBUS_PAYLDTYPE_EMPTY:
break;
case OBUS_PAYLDTYPE_GAMESTATUS:
send_frame.data[1] = (uint8_t) ((msg->gamestatus.time_left & 0xFF000000) >> 0x18);
send_frame.data[2] = (uint8_t) ((msg->gamestatus.time_left & 0x00FF0000) >> 0x10);
send_frame.data[3] = (uint8_t) ((msg->gamestatus.time_left & 0x0000FF00) >> 0x08);
send_frame.data[4] = (uint8_t) (msg->gamestatus.time_left & 0x000000FF);
send_frame.data[5] = msg->gamestatus.strikes;
send_frame.data[6] = msg->gamestatus.max_strikes;
length = 7;
break;
case OBUS_PAYLDTYPE_COUNT:
send_frame.data[1] = msg->count;
length = 2;
break;
default:
Serial.println(F("Unknown payload type"));
return;
}
send_frame.can_id = _encode_can_id(msg->from, msg->priority);
send_frame.can_dlc = length;
mcp2515.sendMessage(&send_frame);
}

226
lib/obus_can.h Normal file
View file

@ -0,0 +1,226 @@
#ifndef OBUS_CAN_H
#define OBUS_CAN_H
#include <assert.h>
#define CAN_DOMINANT 0
#define CAN_RECESSIVE 1
#define OBUS_CONTROLLER_ID 0x000
#define OBUS_MSG_LENGTH 8 // Max 8 to fit in a CAN message
#define OBUS_TYPE_CONTROLLER 0
#define OBUS_TYPE_PUZZLE 1
#define OBUS_TYPE_NEEDY 2
#define OBUS_MSGTYPE_C_ACK 0
#define OBUS_MSGTYPE_C_HELLO 1
#define OBUS_MSGTYPE_C_GAMESTART 2
#define OBUS_MSGTYPE_C_STATE 3
#define OBUS_MSGTYPE_C_SOLVED 4
#define OBUS_MSGTYPE_C_TIMEOUT 5
#define OBUS_MSGTYPE_C_STRIKEOUT 6
#define OBUS_MSGTYPE_M_HELLO 0
#define OBUS_MSGTYPE_M_STRIKE 1
#define OBUS_MSGTYPE_M_SOLVED 2
#define OBUS_PAYLDTYPE_EMPTY 0
#define OBUS_PAYLDTYPE_GAMESTATUS 1
#define OBUS_PAYLDTYPE_COUNT 2
struct module {
uint8_t type;
uint8_t id;
};
struct payld_empty {};
struct payld_gamestatus {
uint32_t time_left;
uint8_t strikes;
uint8_t max_strikes;
};
struct obus_message {
struct module from;
bool priority;
uint8_t msg_type;
union {
struct payld_empty empty;
struct payld_gamestatus gamestatus;
uint8_t count;
};
};
/**
* Determine payload type for message
*
* @param module_type One of OBUS_TYPE_*
* @param module_type One of OBUS_MSGTYPE_*
*
* @return One of OBUS_PAYLDTYPE_*
*/
uint8_t obuscan_payload_type(uint8_t module_type, uint8_t message_type);
/**
* Initialize the CAN controller for OBUS messaging
*/
void obuscan_init();
/**
* Receive a message
*
* @param msg Pointer to memory where the received message will be wriitten
* @return true if a message was received, false otherwise
*/
bool obuscan_receive(struct obus_message *msg);
/**
* Lowlevel interface to send a message, you may want to use one of the helpers, like
* obuscan_send_m_strike
*
* @param msg Pointer to a message to send
*/
void obuscan_send(struct obus_message *msg);
/**
* For internal use only
*
* Send an OBUS message
*/
inline struct obus_message _obuscan_msg(struct module from, bool priority, uint8_t msg_type) {
struct obus_message msg;
msg.from = from;
msg.priority = priority;
msg.msg_type = msg_type;
return msg;
}
/**
* For internal use only
*
* Send a controller OBUS message with a gamestatus payload
*/
inline void _obuscan_send_payld_gamestatus(
struct module from, bool priority, uint8_t msg_type,
uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
struct obus_message msg = _obuscan_msg(from, priority, msg_type);
msg.gamestatus.time_left = time_left;
msg.gamestatus.strikes = strikes;
msg.gamestatus.max_strikes = max_strikes;
obuscan_send(&msg);
}
/**
* Send a controller "ACK" OBUS message
*/
inline void obuscan_send_c_ack(struct module from) {
assert(from.type == OBUS_TYPE_CONTROLLER);
struct obus_message msg = _obuscan_msg(from, false, OBUS_MSGTYPE_C_ACK);
obuscan_send(&msg);
}
/**
* Send a controller "hello" OBUS message
*/
inline void obuscan_send_c_hello(struct module from) {
assert(from.type == OBUS_TYPE_CONTROLLER);
struct obus_message msg = _obuscan_msg(from, false, OBUS_MSGTYPE_C_HELLO);
obuscan_send(&msg);
}
/**
* Send a controller "game start" OBUS message
*/
inline void obuscan_send_c_gamestart(
struct module from, uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
assert(from.type == OBUS_TYPE_CONTROLLER);
_obuscan_send_payld_gamestatus(
from, false, OBUS_MSGTYPE_C_GAMESTART, time_left, strikes, max_strikes);
}
/**
* Send a controller "state" OBUS message
*/
inline void obuscan_send_c_state(
struct module from, uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
assert(from.type == OBUS_TYPE_CONTROLLER);
_obuscan_send_payld_gamestatus(
from, false, OBUS_MSGTYPE_C_STATE, time_left, strikes, max_strikes);
}
/**
* Send a controller "solved" OBUS message
*/
inline void obuscan_send_c_solved(
struct module from, uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
assert(from.type == OBUS_TYPE_CONTROLLER);
_obuscan_send_payld_gamestatus(
from, false, OBUS_MSGTYPE_C_SOLVED, time_left, strikes, max_strikes);
}
/**
* Send a controller "timeout" OBUS message
*/
inline void obuscan_send_c_timeout(
struct module from, uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
assert(from.type == OBUS_TYPE_CONTROLLER);
_obuscan_send_payld_gamestatus(
from, false, OBUS_MSGTYPE_C_TIMEOUT, time_left, strikes, max_strikes);
}
/**
* Send a controller "strikeout" OBUS message
*/
inline void obuscan_send_c_strikeout(
struct module from, uint32_t time_left, uint8_t strikes, uint8_t max_strikes) {
assert(from.type == OBUS_TYPE_CONTROLLER);
_obuscan_send_payld_gamestatus(
from, false, OBUS_MSGTYPE_C_STRIKEOUT, time_left, strikes, max_strikes);
}
/**
* Send a module "hello" OBUS message
*/
inline void obuscan_send_m_hello(struct module from) {
assert(from.type != OBUS_TYPE_CONTROLLER);
struct obus_message msg = _obuscan_msg(from, false, OBUS_MSGTYPE_M_HELLO);
obuscan_send(&msg);
}
/**
* Send a module "strike" OBUS message
*/
inline void obuscan_send_m_strike(struct module from, uint8_t count) {
assert(from.type != OBUS_TYPE_CONTROLLER);
struct obus_message msg = _obuscan_msg(from, false, OBUS_MSGTYPE_M_STRIKE);
msg.count = count;
obuscan_send(&msg);
}
/**
* Send a module "solved" OBUS message
*/
inline void obuscan_send_m_solved(struct module from) {
assert(from.type != OBUS_TYPE_CONTROLLER);
struct obus_message msg = _obuscan_msg(from, false, OBUS_MSGTYPE_M_STRIKE);
obuscan_send(&msg);
}
#endif /* end of include guard: OBUS_CAN_H */

View file

@ -1,68 +1,213 @@
#include <mcp2515.h>
#include <obus_can.h>
#define STATE_INACTIVE 0
#define STATE_HELLO 1
#define STATE_GAME 2
#define OBUS_CONTROLLER_ID 0x000
#define OBUS_MAX_STRIKES 3 // Number of strikes allowed until game over
#define OBUS_GAME_DURATION 10 // Duration of the game in seconds
#define OBUS_TYPE_CONTROLLER 0
#define OBUS_TYPE_PUZZLE 1
#define OBUS_TYPE_NEEDY 2
#define OBUS_MAX_MODULES 16
#define OBUS_DISC_DURATION 5 // Duration of discovery round in seconds
#define OBUS_UPDATE_INTERVAL 500 // Number of milliseconds between game updates
#define OBUS_MSG_LENGTH 8 // Max 8 to fit in a CAN message
#define OBUS_GAME_DURATION_MS ((uint32_t) OBUS_GAME_DURATION*1000)
#define OBUS_DISC_DURATION_MS ((uint32_t) OBUS_DISC_DURATION*1000)
#define OBUS_MSGTYPE_C_ACK 0
#define OBUS_MSGTYPE_C_HELLO 1
#define OBUS_MSGTYPE_C_GAMESTART 2
#define OBUS_MSGTYPE_C_STATE 3
#define OBUS_MSGTYPE_C_SOLVED 4
#define OBUS_MSGTYPE_C_TIMEOUT 5
#define OBUS_MSGTYPE_C_STRIKEOUT 6
#define DIVIDE_CEIL(dividend, divisor) ((dividend + (divisor - 1)) / divisor)
#define MAX_AMOUNT_PUZZLES 256 // The ID of a puzzle is uint8
#define OBUS_MSGTYPE_M_HELLO 0
#define OBUS_MSGTYPE_M_STRIKE 1
#define OBUS_MSGTYPE_M_SOLVED 2
#define CAN_DOMINANT 0
#define CAN_RECESSIVE 1
MCP2515 mcp2515(10);
uint8_t state = STATE_INACTIVE;
struct module connected_modules_ids[OBUS_MAX_MODULES];
uint8_t nr_connected_modules;
uint8_t strikes;
// Bitvector for checking if game is solved or not
// 32 bits per uint32 bitvector field
#define N_UNSOLVED_PUZZLES DIVIDE_CEIL(MAX_AMOUNT_PUZZLES, 32)
uint32_t unsolved_puzzles[N_UNSOLVED_PUZZLES];
// Timers
uint32_t hello_round_start;
uint32_t game_start;
uint32_t last_update;
struct module this_module = {
.type = OBUS_TYPE_CONTROLLER,
.id = OBUS_CONTROLLER_ID
};
void setup() {
Serial.begin(9600);
mcp2515.reset();
mcp2515.setBitrate(CAN_50KBPS);
mcp2515.setNormalMode();
Serial.println("begin");
obuscan_init();
state = STATE_INACTIVE;
}
uint16_t make_id(uint8_t id, bool priority, uint8_t type) {
assert(type <= 0x11);
/* b bb bbbbbbbb
* type module ID
* priority bit
*/
return \
((uint16_t) (priority ? CAN_DOMINANT : CAN_RECESSIVE) << 10) | \
((uint16_t) type << 8) | \
(uint16_t) id;
bool check_solved() {
for (uint8_t i = 0; i < N_UNSOLVED_PUZZLES; i++) {
if (unsolved_puzzles[i] != 0) {
return false;
}
}
return true;
}
void add_module_to_bit_vector(uint8_t module_id) {
uint8_t byte_index = module_id >> 3;
uint8_t bit_index = module_id & 0x07;
unsolved_puzzles[byte_index] |= 0x1 << bit_index;
}
void solve_module_in_bit_vector(uint8_t module_id) {
uint8_t byte_index = module_id >> 3;
uint8_t bit_index = module_id & 0x07;
unsolved_puzzles[byte_index] &= ~(0x1 << bit_index);
}
void start_hello() {
state = STATE_HELLO;
hello_round_start = millis();
nr_connected_modules = 0;
char message[OBUS_MSG_LENGTH + 1];
message[OBUS_MSG_LENGTH] = '\0';
send_frame.can_id = make_id(OBUS_CONTROLLER_ID, false, OBUS_TYPE_CONTROLLER);
send_frame.can_dlc = OBUS_MSG_LENGTH;
mcp2515.sendMessage(&send_frame);
Serial.println("sent");
// Zero bit vectors
for (uint8_t i = 0; i < N_UNSOLVED_PUZZLES; i++) {
unsolved_puzzles[i] = 0;
}
obuscan_send_c_hello(this_module);
Serial.println(F(" Start of discovery round"));
}
uint16_t full_module_id(struct module mod) {
return \
((uint16_t) mod.type << 8) | \
(uint16_t) mod.id;
}
void receive_hello() {
struct obus_message msg;
uint32_t current_time = millis();
if (obuscan_receive(&msg)) {
if (msg.msg_type == OBUS_MSGTYPE_M_HELLO) {
Serial.print(" Registered module ");
Serial.println(full_module_id(msg.from));
if (nr_connected_modules < OBUS_MAX_MODULES) {
connected_modules_ids[nr_connected_modules] = msg.from;
nr_connected_modules++;
if (msg.from.type == OBUS_TYPE_PUZZLE) {
add_module_to_bit_vector(full_module_id(msg.from));
}
}
obuscan_send_c_ack(this_module);
Serial.println(" ACK");
}
} else if (current_time - hello_round_start > OBUS_DISC_DURATION_MS) {
Serial.println(" End of discovery round");
initialize_game();
}
}
void initialize_game() {
strikes = 0;
game_start = millis();
last_update = game_start;
state = STATE_GAME;
Serial.println(" Game started");
obuscan_send_c_gamestart(this_module, OBUS_GAME_DURATION_MS, strikes, OBUS_MAX_STRIKES);
}
void receive_module_update() {
struct obus_message msg;
if (obuscan_receive(&msg)) {
switch (msg.msg_type) {
case OBUS_MSGTYPE_M_STRIKE:
// TODO check idempotency ID
strikes++;
break;
case OBUS_MSGTYPE_M_SOLVED:
solve_module_in_bit_vector(full_module_id(msg.from));
break;
default:
Serial.print(F("W Ignoring msg "));
Serial.println(msg.msg_type);
break;
}
}
}
void game_loop() {
uint32_t current_time = millis();
uint32_t time_elapsed = current_time - game_start;
uint32_t time_left =
OBUS_GAME_DURATION_MS < time_elapsed ? 0 : OBUS_GAME_DURATION_MS - time_elapsed;
// We cannot check for '<= 0' in an uint type so we check the terms prior to subtraction
receive_module_update();
if (check_solved()) {
Serial.println(" Game solved");
obuscan_send_c_solved(this_module, time_left, strikes, OBUS_MAX_STRIKES);
state = STATE_INACTIVE;
return;
}
if (time_left == 0) {
Serial.println(" Time's up");
obuscan_send_c_timeout(this_module, time_left, strikes, OBUS_MAX_STRIKES);
state = STATE_INACTIVE;
return;
}
if (strikes >= OBUS_MAX_STRIKES) {
Serial.println(" Strikeout");
obuscan_send_c_strikeout(this_module, time_left, strikes, OBUS_MAX_STRIKES);
state = STATE_INACTIVE;
return;
}
if (last_update + OBUS_UPDATE_INTERVAL <= current_time) {
obuscan_send_c_state(this_module, time_left, strikes, OBUS_MAX_STRIKES);
last_update = current_time;
}
}
void loop() {
switch (state) {
case STATE_INACTIVE:
start_hello();
break;
case STATE_HELLO:
receive_hello();
break;
case STATE_GAME:
game_loop();
break;
}
}

57
src/new_module.sh Executable file
View file

@ -0,0 +1,57 @@
#/bin/bash
# "Bash strict mode", see http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -euo pipefail
IFS=$'\n\t'
# Go to the current working directory so things work if people are in a different one and e.g. use ../src/new_module.sh
cd "$(dirname "$0")"
# Make sure the template dir exists so we don't let people enter details unnecessarily
if [ ! -d ./template_module ]; then
echo "template_module doesn't exist" >&2
exit 1
fi
# Ask for module name
read -p "Name of module (e.g. Oil gauge): " module_name
if [[ $module_name == *%* ]]; then
echo "Module name must not contain %" >&2
exit 1
fi
# Determine a "clean" module name: lowercase, no spaces
module="${module_name,,}"
module="${module// /_}"
module="${module//\'/}"
# Make sure `modules` directory exists and target directory doesn't
mkdir -p modules
module_dir="modules/$module"
if [[ -e "$module_dir" ]]; then
echo "$module_dir already exists" >&2
exit 1
fi
# Ask for author name
read -p "How would you like to be credited? Your name: " author
if [[ $author == *%* ]]; then
echo "Author name must not contain %" >&2
exit 1
fi
# Copy the template directory
cp -r -T template_module "$module_dir"
cd "$module_dir"
# Fill in the blanks in the template
sed -i "
s/{YEAR}/$(date +%Y)/
s%{AUTHOR}%$author%
s%{MODULE_NAME}%$module_name%
s%{MODULE}%$module%
" $(find -type f)
# Arduino IDE requires .ino sketches to have the same name as their directory
mv main.ino "$module.ino"
echo "The basic structure for your module is now ready in $module_dir"

View file

@ -0,0 +1,19 @@
## {MODULE_NAME}
Write the defusing guide for your module here. For inspiration, look at the documentation of
existing modules.
Use drawings! Put them in the same directory as this file and include them like this:
![](filename.pdf)
Use tables! Write them like this:
| Symbol | Action to take |
|------------------|----------------|
| ![](symbol1.pdf) | Do nothing |
| ![](symbol2.pdf) | Press the red button |
| ![](symbol3.pdf) | Press the yellow button |
### Credits
Module developed by {AUTHOR}.

View file

@ -0,0 +1,19 @@
// (c) {YEAR}, {AUTHOR}
// See the LICENSE file for conditions for copying
#include <obus_module.h>
void setup() {
Serial.begin(9600);
// Choose one
// Puzzle: a module that must be solved
obusmodule_init(OBUS_TYPE_PUZZLE, /* Retrieve ID from MOANA */);
// Needy: a module that periodically requires an action not to get strikes
// obusmodule_init(OBUS_TYPE_NEEDY, /* Retrieve ID from MOANA */);
}
void loop() {
obusmodule_loop();
}