├── .gitignore ├── .gitmodules ├── README.md ├── bus_docu ├── bus_connector.md └── protocol.md ├── master ├── gamemaster │ ├── Bus.py │ ├── Sound.py │ ├── TestBus.py │ ├── gamemaster.py │ ├── parse_errorcode_from_cpp.py │ ├── server.py │ ├── sounds │ │ ├── beep_end.wav │ │ └── beep_short.wav │ ├── webserver.html │ └── webserver.py ├── gui │ ├── master_gui.py │ ├── master_gui_main_window.py │ └── test_server.py └── lib │ ├── client.py │ ├── connection.py │ └── server.py ├── modules ├── a │ ├── Makefile │ └── a.ino ├── b │ ├── Makefile │ ├── autowires.cpp.in │ ├── autowires.h │ ├── b.ino │ ├── common.py │ ├── create_cheatsheet.py │ ├── create_figurelist.py │ ├── create_figures.py │ ├── create_flowchoice.py │ ├── create_gatelist.py │ ├── create_layout.py │ ├── create_logic_examples.py │ ├── create_lookupcode.py │ ├── create_lookuptable.py │ ├── create_panel.py │ ├── manual.tex │ ├── module_solver.py │ └── pin_definitions.h ├── common │ ├── chde.tex │ ├── cliparts │ │ ├── led_off.pdf │ │ ├── led_on.pdf │ │ ├── switch_down.pdf │ │ └── switch_up.pdf │ ├── common_preamble.tex │ ├── de.tex │ ├── en.tex │ ├── layout_generation │ │ └── layout.py │ ├── manual │ │ ├── Makefile │ │ └── manual.tex │ └── manual_preamble.tex ├── d │ ├── Makefile │ ├── config.py │ ├── create_cppfile.py │ ├── create_layout.py │ ├── create_lookuptable.py │ ├── create_manual_description.py │ ├── create_manual_table.py │ ├── create_panel.py │ ├── d.ino.in │ ├── manual.tex │ ├── module_solver.py │ └── pin_definitions.h ├── default_makefile ├── doc │ └── statemachine.dot ├── libraries │ ├── BUMMSlave │ │ ├── BUMMSlave.cpp │ │ ├── BUMMSlave.h │ │ ├── examples │ │ │ └── BUMMExample │ │ │ │ └── BUMMExample.pde │ │ └── keywords.txt │ └── debounce │ │ ├── debounce.cpp │ │ └── debounce.h ├── mastercontrol │ ├── Makefile │ ├── create_panel.py │ └── mastercontrol.ino └── serial_display │ ├── Makefile │ ├── create_panel.py │ └── serial_display.ino └── parts ├── laser_config.py ├── pcb_holder └── pcb_holder.scad ├── top_plate.py └── top_plate_with_all_holes.svg /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pdf 3 | *.aux 4 | *.log 5 | *.pyc 6 | *autogen* 7 | build-uno 8 | __pycache__ 9 | *.gcode 10 | *.svg 11 | *.stl 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "modules/libraries/Adafruit_GFX"] 2 | path = modules/libraries/Adafruit_GFX 3 | url = https://github.com/adafruit/Adafruit-GFX-Library.git 4 | [submodule "modules/libraries/Adafruit_LEDBackpack"] 5 | path = modules/libraries/Adafruit_LEDBackpack 6 | url = https://github.com/adafruit/Adafruit_LED_Backpack.git 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BrickUsingMultipleModules 2 | 3 | More info about gameplay and pictures on https://hackerspace-ffm.de/wiki/index.php?title=BrickUsingMultipleModules 4 | 5 | ## Build dependencies 6 | Use `git clone --recursive` to get the Adafruit libraries necessary for some modules. 7 | ### Modules 8 | * make 9 | * pdflatex 10 | * arduino-mk 11 | * python3 12 | * PyGraphViz (Gates module) 13 | 14 | ### Gamemaster 15 | * python2 16 | * pygame 17 | 18 | ### GUI 19 | * python3 20 | * tkinter for python3 21 | 22 | ## Build commands 23 | ### Program an arduino 24 | `make upload` in module folder 25 | 26 | ### Manual 27 | `make manual` in module folder or `make manual_en.pdf` if you want to skip the german manuals. 28 | 29 | ### Frontpanel 30 | `make panel` in module folder 31 | 32 | ## Gamemaster 33 | `./gamemaster.py --help` in `master/gamemaster` to get info about command line usage. 34 | 35 | ## GUI 36 | `./master_gui.py HOSTNAME` in `master/gui` to connect to gamemaster running on `HOSTNAME`. 37 | -------------------------------------------------------------------------------- /bus_docu/bus_connector.md: -------------------------------------------------------------------------------- 1 | 10-pin connector 2 | * 1-4: GND 3 | * 5-6: +5V 4 | * 7-8: +12V 5 | * 9: Bus + 6 | * 10: Bus - 7 | -------------------------------------------------------------------------------- /bus_docu/protocol.md: -------------------------------------------------------------------------------- 1 | # Protocol 2 | 3 | ## Message anatomy 4 | 5 | ### Gamemaster -> module 6 | * First byte: target ID (module) 7 | * starting with "a" 8 | * "_" is broadcast 9 | * Second byte: Command 10 | * Following bytes: command parameters 11 | * Ended with "\n" 12 | 13 | ### Module response 14 | * First byte: "=" (response) 15 | * Return values from slave to master 16 | 17 | ### a: Module exists? 18 | 19 | Parameter: none 20 | Return: 21 | * 1 byte module-specific revision number 22 | * 1 byte number of random bytes requested on gamestart 23 | 24 | ### b: Module init 25 | 26 | Parameter 27 | * 1 byte mode: 28 | * bit 0: is enabled? 29 | * 5 bytes serial number, each byte an ascii value for a digit/letter 30 | * first two digits are decimal digits. Higher number repesents a higher difficulty and modules should choose difficulty accordingly 31 | * the following two digits are decimal digits which just carry some entropy 32 | * the last digit should be an uppercase letter 33 | * example: "2410H" would be difficulty 24 34 | * N bytes random number (according to module description) 35 | 36 | Result: 37 | * Reset logic 38 | * Reset hardware 39 | * Reset failure counter 40 | 41 | ### c: Game start (broadcast) 42 | Enables displays and countdown 43 | 44 | ### d: Status poll 45 | 46 | Parameter: none 47 | Return 48 | * 1 byte success state (1 if defused, 0 if not defused yet, all other codes are errorcodes) 49 | * 1 byte failure counter (gets incremented each failure) OR error code (if success state is > 1) 50 | 51 | ### e: Game status broadcast 52 | 53 | Parameter 54 | * 2 byte: remaining seconds 55 | * 1 byte: life counter 56 | 57 | ### f: Game end 58 | 59 | Parameter 60 | * 1 byte: game result (0: defused, 1: timeout, 2: too many failures) 61 | -------------------------------------------------------------------------------- /master/gamemaster/Bus.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import random 3 | 4 | 5 | BROADCAST_ADDRESS = "_" 6 | CONTROL_MODULE = "!" 7 | RESPONSE_ADDRESS = "=" 8 | 9 | MODULE_EXISTS = "a" 10 | MODULE_INIT = "b" 11 | GAME_START = "c" 12 | STATUS_POLL = "d" 13 | STATUS_BROADCAST = "e" 14 | GAME_END = "f" 15 | 16 | class BusException(RuntimeError): 17 | def __init__(self, text): 18 | self.text = text 19 | def __str__(self): 20 | return "BusException: " + self.text 21 | 22 | def retryOnBusException(func): 23 | def f(*args, **kwargs): 24 | while True: 25 | try: 26 | return func(*args, **kwargs) 27 | except BusException as e: 28 | print("Caught BusException: {}".format(e)) 29 | print("retrying...") 30 | return f 31 | 32 | class Bus(object): 33 | ## 34 | # usage: Bus(serial_device) for a preexisting serial device or Bus(name_of_serial_device, baudrate) to create a new one 35 | def __init__(self, device, baudrate=None, debug=False): 36 | if baudrate is None: 37 | self.serial = device 38 | else: 39 | self.serial = serial.Serial(device, baudrate, timeout=0.1) 40 | self.debug = debug 41 | 42 | def _write(self, buf): 43 | if self.debug: 44 | print(">"+buf) 45 | self.serial.write(buf+"\n") 46 | self.serial.flush() 47 | 48 | def _readline(self): 49 | result = self.serial.readline() 50 | if len(result) > 0: 51 | if result[-1] == "\n": 52 | result = result[:-1] 53 | if len(result) > 0: 54 | if result[0] != RESPONSE_ADDRESS: # expected return code 55 | raise BusException("got response with wrong target ID: '{}' (expected '{}')".format(result[0], RESPONSE_ADDRESS)) 56 | result = result[1:] 57 | if self.debug: 58 | print("<"+result) 59 | return result 60 | 61 | ## Drain input buffer (read input until none is left) 62 | def drain(self): 63 | while(True): 64 | if len(self.serial.readline()) == 0: 65 | break 66 | 67 | def _request(self, buf): 68 | self._write(buf) 69 | return self._readline() 70 | 71 | @staticmethod 72 | def _check_module_id(module_id): 73 | assert len(module_id) == 1 74 | 75 | 76 | ## converts int to hex value 77 | # \param value The integer value to convert 78 | # \param length number of bytes to use. Value has to fit into length bytes 79 | @staticmethod 80 | def _to_hex(value, length): 81 | assert isinstance(value, int) 82 | result = hex(value)[2:].upper() 83 | assert len(result) <= 2*length # value would violate protocol specification 84 | return "0" * (2*length-len(result)) + result # pad with zeroes 85 | 86 | ## Parses and spilts hex string 87 | # \param value the value to parse 88 | # \param parameters list of tuples with parameter name and length 89 | @staticmethod 90 | def _pick_from_hex(value, parameters): 91 | assert isinstance(value, str) 92 | assert isinstance(parameters, list) 93 | for p in parameters: 94 | assert isinstance(p, tuple) 95 | assert len(p) == 2 96 | assert isinstance(p[0], str) 97 | assert isinstance(p[1], int) 98 | 99 | expected_len = sum((p[1] for p in parameters)) 100 | if expected_len*2 != len(value): 101 | raise BusException("'{}' cannot be parsed into {} bytes!".format(value, expected_len)) 102 | 103 | pos = 0 104 | result = {} 105 | for p in parameters: 106 | result[p[0]] = int(value[pos : pos+2*p[1]], base=16) 107 | pos += 2*p[1] 108 | return result 109 | 110 | ## checks whether the module with the given id is connected to the bus 111 | # \param module_id single-character module id 112 | # \returns dict(revision, num_random) if present, None otherwise 113 | def check_for_module(self, module_id): 114 | Bus._check_module_id(module_id) 115 | 116 | result = self._request(module_id + MODULE_EXISTS) 117 | if len(result) == 0: 118 | return None 119 | return Bus._pick_from_hex(result, [("revision", 1), ("num_random",1)]) 120 | 121 | ## Initialises module for new game 122 | # \param module_id single-character module id 123 | # \param enabled boolean value which determines whether this module will take part in this game 124 | # \param serial_number 5-char string of bumm serial number 125 | # \param num_random number of random bytes or list of 1-byte integers 126 | def init_module(self, module_id, enabled, serial_number, num_random): 127 | Bus._check_module_id(module_id) 128 | assert enabled in [True, False] 129 | assert isinstance(serial_number, str) 130 | assert len(serial_number) == 5 131 | assert "1" <= serial_number[0] <= "9" 132 | assert "0" <= serial_number[1] <= "9" 133 | assert "0" <= serial_number[2] <= "9" 134 | assert "0" <= serial_number[3] <= "9" 135 | assert "A" <= serial_number[4] <= "Z" 136 | 137 | if isinstance(num_random, int): 138 | random_number = (random.randrange(0, 256) for i in range(num_random)) 139 | elif isinstance(num_random, list): 140 | for i in num_random: 141 | assert isinstance(i, int) 142 | assert 0 <= i < 256 143 | random_number = num_random 144 | else: 145 | raise AssertionError("num_random neither of type int or list") 146 | 147 | mode = 1 if enabled else 0 148 | random_number = "".join(Bus._to_hex(i, 1) for i in random_number) 149 | serial_number_encoded = "".join(Bus._to_hex(ord(s), 1) for s in serial_number) 150 | 151 | self._write(module_id + MODULE_INIT + Bus._to_hex(mode, 1) + serial_number_encoded + random_number) 152 | 153 | ## Actually start the game 154 | def start_game(self, module_id): 155 | Bus._check_module_id(module_id) 156 | self._write(module_id + GAME_START) 157 | 158 | ## Poll module status 159 | # \param module_id single-character module id 160 | # \returns tuple (success (boolean), number of failures) 161 | @retryOnBusException 162 | def poll_status(self, module_id): 163 | Bus._check_module_id(module_id) 164 | response = self._request(module_id + STATUS_POLL) 165 | result = Bus._pick_from_hex(response, [ 166 | ("success", 1), 167 | ("failures", 1) 168 | ]) 169 | 170 | return (result["success"], result["failures"]) 171 | 172 | ## Broadcast the current game state 173 | # \param remaining_seconds of the countdown 174 | # \param num_lifes number of lifes left 175 | def broadcast_status(self, remaining_seconds, num_lifes): 176 | self._write(BROADCAST_ADDRESS + STATUS_BROADCAST + Bus._to_hex(remaining_seconds, 2) + Bus._to_hex(num_lifes, 1)) 177 | 178 | ## Game finished (bomb exploded or was successfully defused) 179 | # \param result 0 if defused, 1 if countdown reached, 2 if too many failures 180 | def end_game(self, result): 181 | assert result in [0,1,2,3] 182 | self._write(BROADCAST_ADDRESS + GAME_END + Bus._to_hex(result, 1)) 183 | -------------------------------------------------------------------------------- /master/gamemaster/Sound.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import pygame 4 | 5 | 6 | 7 | sound_folder = "sounds/" 8 | 9 | class SoundManager(object): 10 | MUSIC_CHANNEL = 0 11 | instance = None 12 | 13 | def __init__(self, soundfiles): 14 | if SoundManager.instance is not None: 15 | raise Exception("Sound manager is a singleton instance. an instance is already defined!") 16 | SoundManager.instance = self 17 | pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=64) 18 | pygame.mixer.set_num_channels(2) 19 | pygame.mixer.set_reserved(1) # number of dedicated channels (e.g. music) 20 | self.musicChannel = pygame.mixer.Channel(SoundManager.MUSIC_CHANNEL) 21 | 22 | self.files = {k: pygame.mixer.Sound(sound_folder+filename) for k, filename in soundfiles.iteritems()} 23 | 24 | def __getitem__(self, key): 25 | return self.files[key] 26 | 27 | @staticmethod 28 | def StopAll(): 29 | pygame.mixer.stop() 30 | 31 | -------------------------------------------------------------------------------- /master/gamemaster/TestBus.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from Bus import Bus, BusException, RESPONSE_ADDRESS 3 | 4 | class FakeSerial(object): 5 | def __init__(self, responses=None): 6 | self.responses = {} if responses is None else responses 7 | self.sendbuf = "" 8 | self.recvbuf = "" 9 | 10 | def write(self, content): 11 | content = content.decode() 12 | self.sendbuf += content 13 | assert content[-1] == "\n", "'{}'".format(content) 14 | content = content[:-1] 15 | if content in self.responses: 16 | self.recvbuf += RESPONSE_ADDRESS + self.responses[content]+"\n" 17 | 18 | def readline(self): 19 | return bytes(self.recvbuf, "utf8") 20 | 21 | def flush(self): 22 | pass 23 | 24 | def create_bus(bufcontent): 25 | return Bus(FakeSerial(bufcontent)) 26 | 27 | class TestBus(unittest.TestCase): 28 | def test_to_hex(self): 29 | self.assertEqual(Bus._to_hex(16, 1), "10") 30 | self.assertEqual(Bus._to_hex(16, 2), "0010") 31 | self.assertRaises(AssertionError, Bus._to_hex, 500, 1) 32 | self.assertRaises(AssertionError, Bus._to_hex, "a", 1) 33 | def test_from_hex(self): 34 | self.assertRaises(AssertionError, Bus._pick_from_hex, 1, []) 35 | self.assertRaises(BusException, Bus._pick_from_hex, "", [("x",1)]) 36 | self.assertRaises(BusException, Bus._pick_from_hex, "10", []) 37 | 38 | self.assertEqual(Bus._pick_from_hex("10", [("x", 1)]), {"x":16}) 39 | self.assertEqual(Bus._pick_from_hex("100010", [("x", 1), ("y", 2)]), {"x":16, "y":16}) 40 | 41 | def test_module_existence(self): 42 | self.assertEqual(create_bus({}).check_for_module("a"), None) 43 | self.assertEqual(create_bus({"aa":"0302"}).check_for_module("a"), {"revision":3, "num_random":2}) 44 | self.assertRaises(BusException, create_bus({"aa":"3"}).check_for_module, "a") 45 | 46 | def test_module_init_0(self): 47 | serial = FakeSerial() 48 | bus = Bus(serial) 49 | bus.init_module("a", False, "1000A", 1) 50 | self.assertEqual(serial.sendbuf[:14], "ab003130303041") 51 | self.assertEqual(len(serial.sendbuf), 17) 52 | 53 | def test_module_init_1(self): 54 | serial = FakeSerial() 55 | bus = Bus(serial) 56 | bus.init_module("a", True, "1000A", 1) 57 | self.assertEqual(serial.sendbuf[:14], "ab013130303041") 58 | self.assertEqual(len(serial.sendbuf), 17) 59 | 60 | def test_module_init_2(self): 61 | serial = FakeSerial() 62 | bus = Bus(serial) 63 | self.assertRaises(AssertionError, bus.init_module, "a", True, 16, 1) 64 | self.assertRaises(AssertionError, bus.init_module, "a", True, "1000", 1) 65 | self.assertRaises(AssertionError, bus.init_module, "a", True, "10000", 1) 66 | self.assertRaises(AssertionError, bus.init_module, "a", True, "100A0", 1) 67 | 68 | def test_game_start(self): 69 | serial = FakeSerial() 70 | bus = Bus(serial) 71 | bus.start_game("a") 72 | self.assertEqual(serial.sendbuf, "ac\n") 73 | 74 | def test_status_poll(self): 75 | self.assertEqual(create_bus({"ad":"0000"}).poll_status("a"), (False, 0)) 76 | self.assertEqual(create_bus({"ad":"0100"}).poll_status("a"), (True, 0)) 77 | self.assertEqual(create_bus({"ad":"0001"}).poll_status("a"), (False, 1)) 78 | 79 | def test_status_broadcast(self): 80 | serial = FakeSerial() 81 | bus = Bus(serial) 82 | bus.broadcast_status(10, 1) 83 | self.assertEqual(serial.sendbuf, "_e000A01\n") 84 | 85 | def test_game_end(self): 86 | serial = FakeSerial() 87 | bus = Bus(serial) 88 | bus.end_game(1) 89 | self.assertEqual(serial.sendbuf, "_f01\n") 90 | -------------------------------------------------------------------------------- /master/gamemaster/gamemaster.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | import argparse 3 | import random 4 | import time 5 | import sys 6 | 7 | import Bus 8 | from Sound import SoundManager 9 | sys.path.append("../lib") 10 | from server import Server 11 | from parse_errorcode_from_cpp import parse_errorcode_from_cpp 12 | 13 | from webserver import WebServer 14 | 15 | BAUDRATE = 19200 16 | 17 | # parse arguments 18 | parser = argparse.ArgumentParser(description="BUMM Gamemaster") 19 | parser.add_argument("serial_device", type=str, help="Serial device used for bus communication") 20 | parser.add_argument("--disable", metavar="module", type=str, default="", help="Disable (don't use) the specified modules (unseparated list e.g. 'ac')") 21 | parser.add_argument("--ignore-master-control", action="store_true", help="Don't communicate explicitly with mastercontrol. If connected, game updates may still be displayed because of broadcast.") 22 | parser.add_argument("--bus-debug", action="store_true", help="print bus communication") 23 | 24 | subparsers = parser.add_subparsers(title="modes", description="Pick one of the following modes how to control game flow", dest="mode") 25 | 26 | parser_standalone = subparsers.add_parser("standalone", help="directly start a game without control module or gui") 27 | 28 | parser_standalone.add_argument("num_modules", type=int, help="Number of active modules") 29 | parser_standalone.add_argument("difficulty", type=int, help="Difficulty value from 10-99") 30 | parser_standalone.add_argument("time", type=int, help="Number of seconds for the countdown timer") 31 | parser_standalone.add_argument("max_errors", type=int, default=0, help="Maximum errors allowed") 32 | 33 | parser_gui = subparsers.add_parser("gui", help="start server and wait for connection") 34 | 35 | parser_webgui = subparsers.add_parser("webgui", help="start webserver with gui") 36 | 37 | 38 | 39 | def linspace(start, stop, step): 40 | return list(i*step for i in range( int(start/step), int(stop/step) ) ) 41 | 42 | def make_beep_times(fulltime): 43 | result = ( linspace(0, 5, 0.2) + linspace(5, 10, 0.5) + linspace(10, 20, 1) + linspace(20, fulltime, 5) )[::-1] 44 | return result 45 | 46 | def check_argument_validity(args): 47 | assert 10 <= args.difficulty <= 99 48 | assert args.time > 0 49 | assert args.max_errors >= 0 50 | 51 | def check_existing_modules(bus): 52 | result = {} 53 | print("found the following modules (enumeration may take a while!)") 54 | print("---+-----------") 55 | print(" | revision ") 56 | print("---+-----------") 57 | for m in "abcdefgh":#ijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789": 58 | description = bus.check_for_module(m) 59 | if description is not None: 60 | print(" {} | {}".format(m, description["revision"])) 61 | result[m] = description 62 | return result 63 | 64 | class Gamemaster(): 65 | def __init__(self, args): 66 | self.args = args 67 | 68 | self.bus = Bus.Bus(args.serial_device, BAUDRATE, debug=args.bus_debug) 69 | self.bus.drain() 70 | 71 | self.sound = SoundManager({"beep":"beep_short.wav", "beep_end": "beep_end.wav", "error": "error.wav", "defused": "bomb_defused.wav", "explosion": "explosion_02.wav"}) 72 | 73 | # check for mastercontrol module 74 | if not args.ignore_master_control: 75 | control_description = self.bus.check_for_module(Bus.CONTROL_MODULE) 76 | if control_description is None: 77 | raise Exception("no mastercontrol module found!") 78 | self.bus.init_module(Bus.CONTROL_MODULE, True, "1000A", control_description["num_random"]) 79 | 80 | # check other modules 81 | self.modules = check_existing_modules(self.bus) 82 | for m in args.disable: 83 | try: 84 | self.modules.pop(m) 85 | except KeyError: 86 | print("disable module {} which was not enumerated. typo?".format(m)) 87 | 88 | if args.mode == "gui": 89 | self.server = Server("", self.modules) 90 | 91 | if args.mode == "webgui": 92 | self.server = WebServer(("0.0.0.0", 8080), module_descriptions=self.modules) 93 | 94 | def get_game_info(self): 95 | if self.args.mode == "standalone": 96 | print("waiting 5s for game start...") 97 | time.sleep(5) 98 | return self.get_game_info_standalone() 99 | 100 | elif self.args.mode in ("gui", "webgui"): 101 | # wait for "start" command 102 | while(True): 103 | if self.server._CheckForMessage(): 104 | msg = self.server._PopMessage() 105 | if "start" in msg: 106 | d=msg["start"] 107 | return (d["seconds"], d["maxerrors"], d["serial"], d["modules"]) 108 | else: 109 | print("waiting for start command...") 110 | time.sleep(0.5) 111 | else: 112 | raise NotImplementedError() 113 | 114 | def get_game_info_standalone(self): 115 | serial_number = str(self.args.difficulty) + str(random.randrange(0, 99)).zfill(2) + chr(0x41+random.randrange(0, 26)) 116 | used_modules = random.sample(self.modules, self.args.num_modules) 117 | 118 | module_states = {m: {"enabled":m in used_modules, "random":[random.randrange(0,256) for i in range(self.modules[m]["num_random"])]} for m in self.modules} 119 | 120 | return (self.args.time, self.args.max_errors, serial_number, module_states) 121 | 122 | def should_abort(self): 123 | if self.args.mode in ("gui", "webgui"): 124 | if self.server._CheckForMessage(): 125 | msg = self.server._PopMessage() 126 | if "abort" in msg: 127 | return True 128 | return False 129 | else: 130 | return False 131 | 132 | def explode(self): 133 | self.sound["explosion"].play() 134 | print("BOOOM!") 135 | self.cleanup_after_game() 136 | 137 | def win(self): 138 | self.sound["defused"].play() 139 | print("BOMB HAS BEEN DEFUSED!") 140 | self.cleanup_after_game() 141 | 142 | def cleanup_after_game(self): 143 | if self.args.mode == "standalone": 144 | print("game over, shutting down") 145 | sys.exit(0) 146 | 147 | def start_game(self): 148 | game_info = self.get_game_info() 149 | print(game_info) 150 | seconds, max_errors, serial_number, module_states = game_info 151 | # init all normal modules 152 | for m, state in module_states.iteritems(): 153 | self.bus.init_module(m, state["enabled"], serial_number, state["random"]) 154 | 155 | # wait for start switch on control panel 156 | """ 157 | if not args.ignore_control_module: 158 | bus.start_game(Bus.CONTROL_MODULE) 159 | while(True): 160 | continue_waiting, _ = bus.poll_status(Bus.CONTROL_MODULE) 161 | if not continue_waiting: 162 | break 163 | """ 164 | 165 | num_active_modules = sum(state["enabled"] for state in module_states.values()) 166 | 167 | 168 | for m in module_states: 169 | self.bus.start_game(m) 170 | explosion_time = time.time() + seconds 171 | beeptimes = make_beep_times(seconds) 172 | next_beep_index = 0 173 | 174 | last_time_left = seconds 175 | last_num_lifes = max_errors 176 | 177 | while True: 178 | state = {"modules": {}} 179 | 180 | num_lifes = max_errors 181 | defused = 0 182 | for m in module_states: 183 | success, module_failures = self.bus.poll_status(m) 184 | state["modules"][m] = {"state": success, "failures": module_failures} 185 | if success in [0,1]: 186 | num_lifes -= module_failures 187 | if success: 188 | print("defused module. success = {}".format(success)) 189 | defused += 1 190 | else: 191 | if self.args.mode == "standalone": 192 | raise Exception("state error in module {}: {} ({})".format(m, parse_errorcode_from_cpp("../../modules/libraries/BUMMSlave/BUMMSlave.cpp", success), module_failures)) 193 | 194 | time_left = int(explosion_time - time.time()) 195 | 196 | # SOUNDS 197 | # make countdown sounds 198 | if ( explosion_time - time.time() ) < self.sound["beep_end"].get_length(): 199 | self.sound["beep_end"].play() 200 | elif ( explosion_time - time.time() ) < beeptimes[next_beep_index]: 201 | next_beep_index += 1 202 | self.sound["beep"].play() 203 | 204 | if last_num_lifes != num_lifes and num_lifes >= 0: 205 | self.sound["error"].play() 206 | 207 | 208 | # BROADCAST STATE 209 | 210 | state["seconds"] = time_left 211 | state["lifes"] = num_lifes if num_lifes >= 0 else 0 212 | 213 | if last_time_left != time_left or last_num_lifes != num_lifes: 214 | print(time_left) 215 | self.bus.broadcast_status(time_left, num_lifes) 216 | last_time_left = time_left 217 | last_num_lifes = num_lifes 218 | 219 | if self.args.mode in ("gui", "webgui"): 220 | self.server.send_game_update(state) 221 | 222 | # GAME END CONDITIONS 223 | # countdown over? 224 | if time_left <= 0: 225 | print("no time left") 226 | if self.args.mode in ("gui", "webgui"): 227 | self.server.send_game_end(reason=1) 228 | self.bus.end_game(1) 229 | time.sleep(1) 230 | self.explode() 231 | break 232 | 233 | # too many failures? 234 | if num_lifes < 0: 235 | print("too many failures!") 236 | if self.args.mode in ("gui", "webgui"): 237 | self.server.send_game_end(reason=2) 238 | self.bus.end_game(2) 239 | self.explode() 240 | break 241 | 242 | # defused? 243 | if defused == num_active_modules: 244 | if self.args.mode in ("gui", "webgui"): 245 | self.server.send_game_end(reason=0) 246 | self.bus.end_game(0) 247 | self.win() 248 | break 249 | 250 | if self.should_abort(): 251 | if self.args.mode in ("gui", "webgui"): 252 | self.server.send_game_end(reason=3) 253 | self.bus.end_game(3) 254 | self.cleanup_after_game() 255 | break 256 | 257 | 258 | def main(): 259 | main_args = parser.parse_args() 260 | print(main_args.disable) 261 | gamemaster = Gamemaster(main_args) 262 | while(True): 263 | gamemaster.start_game() 264 | 265 | 266 | if __name__=="__main__": 267 | main() 268 | 269 | -------------------------------------------------------------------------------- /master/gamemaster/parse_errorcode_from_cpp.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | regex = re.compile(r"^#define ERROR_(\w+) (\w+)$") 4 | 5 | def parse_errorcode_from_cpp(filename, code): 6 | result = None 7 | with open(filename, "r") as f: 8 | for line in f: 9 | match = regex.match(line) 10 | if match: 11 | if int(match.group(2), 0) == code: 12 | if result is None: 13 | result = match.group(1) 14 | else: 15 | raise Exception("ambiguous errorcode ({}) could be either {} or {}".format(code, result, match.group(1))) 16 | return "UNKNOWN ERROR CODE ({})".format(code) if result is None else result 17 | -------------------------------------------------------------------------------- /master/gamemaster/server.py: -------------------------------------------------------------------------------- 1 | ../lib/server.py -------------------------------------------------------------------------------- /master/gamemaster/sounds/beep_end.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackffm/BrickUsingMultipleModules/5bb6fb3457101d1a45464fa8fd972393179a839a/master/gamemaster/sounds/beep_end.wav -------------------------------------------------------------------------------- /master/gamemaster/sounds/beep_short.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackffm/BrickUsingMultipleModules/5bb6fb3457101d1a45464fa8fd972393179a839a/master/gamemaster/sounds/beep_short.wav -------------------------------------------------------------------------------- /master/gamemaster/webserver.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | BUMM remote control 4 | 5 | 6 | 7 | 8 | 9 | 98 | 99 | 100 |
BUMM remote control
101 |
102 |
103 |
104 |
105 | 106 |
107 | 108 |
109 | ID 110 | enable 111 | state 112 | failures 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | start 125 | 126 |
127 | 128 | 129 | -------------------------------------------------------------------------------- /master/gamemaster/webserver.py: -------------------------------------------------------------------------------- 1 | import SimpleHTTPServer 2 | import SocketServer 3 | import logging 4 | import cgi 5 | import random 6 | import sys 7 | import json 8 | 9 | PORT = 8080 10 | 11 | game_instance = None 12 | 13 | class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 14 | def make_headers(self): 15 | self.send_response(200) 16 | self.send_header('Content-type', 'text/html') 17 | self.end_headers() 18 | 19 | def serve_main_page(self, post): 20 | with open("webserver.html", "r") as f: 21 | self.wfile.write(f.read() % 22 | (", ".join('"{}"'.format(m) for m in self.server.module_descriptions)) 23 | ) 24 | 25 | def do_GET(self): 26 | self.make_headers() 27 | if self.path == "/status": 28 | state = self.server.state 29 | if state is None: 30 | result = {"seconds": -1, "lifes": -1} 31 | result["modules"] = {m: {"failures": 0, "state": 5} for m in self.server.module_descriptions} 32 | else: 33 | result = { 34 | "seconds": state["seconds"], 35 | "lifes": state["lifes"], 36 | } 37 | result["modules"] = {m: {"failures": state["modules"][m]["failures"], "state": state["modules"][m]["state"]} for m in self.server.module_descriptions} 38 | self.wfile.write(json.dumps(result)) 39 | else: 40 | self.serve_main_page({}) 41 | 42 | def do_POST(self): 43 | self.make_headers() 44 | form = cgi.FieldStorage( 45 | fp=self.rfile, 46 | headers=self.headers, 47 | environ={'REQUEST_METHOD':'POST', 48 | 'CONTENT_TYPE':self.headers['Content-Type'], 49 | }) 50 | params = {item.name: item.value for item in form.list} 51 | 52 | try: 53 | handler = { 54 | "/start": self.handle_start, 55 | "/abort": self.handle_abort, 56 | }[self.path] 57 | except KeyError: 58 | print("unknown path: {}".format(self.path)) 59 | return 60 | 61 | handler(params) 62 | 63 | #self.serve_main_page(d) 64 | 65 | def handle_start(self, params): 66 | md = self.server.module_descriptions 67 | self.server.last_message = {"start":{ 68 | "seconds": int(params["seconds"]), 69 | "maxerrors": int(params["maxerrors"]), 70 | "serial": params["serial"], 71 | "modules": {m: {"enabled": ("enable_"+m) in params and params["enable_"+m]=="on", "random":[random.randrange(0,256) for i in range(md[m]["num_random"])]} for m in md} 72 | }} 73 | self.wfile.write("") 74 | 75 | def handle_abort(self, params): 76 | self.server.last_message = {"abort": {}} 77 | 78 | 79 | def handle_post(self, d): 80 | for name, value in d.iteritems(): 81 | print("{}={}".format(name, value)) 82 | 83 | md = self.server.module_descriptions 84 | 85 | try: 86 | if d["btn"] == "start": 87 | print("sending start") 88 | self.server.last_message = {"start":{ 89 | "seconds": int(d["new_countdown"]), 90 | "maxerrors": int(d["new_maxerrors"]), 91 | "serial": d["serial"], 92 | "modules": {m: {"enabled": ("enable_"+m) in d and d["enable_"+m]=="on", "random":[random.randrange(0,256) for i in range(md[m]["num_random"])]} for m in md} 93 | }} 94 | elif d["btn"] == "abort": 95 | print("sending abort") 96 | self.server.last_message = {"abort":{ 97 | }} 98 | except Exception as e: 99 | print("error:") 100 | print(e) 101 | 102 | class WebServer(SocketServer.TCPServer): 103 | allow_reuse_address = True 104 | 105 | 106 | def __init__(self, interface, module_descriptions): 107 | SocketServer.TCPServer.__init__(self, interface, ServerHandler) 108 | self.module_descriptions = module_descriptions 109 | self.state = None 110 | self.last_message = None 111 | self.timeout = 0.01 112 | 113 | def send_game_update(self, state): 114 | self.state = state 115 | 116 | def send_game_end(self, reason): 117 | self.state = None 118 | 119 | def _PopMessage(self): 120 | lm = self.last_message 121 | self.last_message = None 122 | return lm 123 | 124 | def _CheckForMessage(self): 125 | self.handle_request() 126 | return self.last_message is not None 127 | 128 | if __name__ == "__main__": 129 | httpd = WebServer(("0.0.0.0", PORT), ServerHandler) 130 | httpd.state = {"countdown": "0", "lives": "1"} 131 | 132 | while True: 133 | httpd.handle_request() 134 | -------------------------------------------------------------------------------- /master/gui/master_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | import argparse 4 | import tkinter as tk 5 | from master_gui_main_window import Application 6 | 7 | sys.path.append("../lib") 8 | from client import Client 9 | 10 | parser = argparse.ArgumentParser(description="BUMM master gui") 11 | parser.add_argument("hostname", type=str, help="hostname of gamemaster") 12 | parser.add_argument("--expected-revisions", type=argparse.FileType("r"), nargs="?", default=None, help="filename of module_id=revision pairs of expected revisions (this file should match the printed out manual)") 13 | args = parser.parse_args() 14 | 15 | connection = Client(args.hostname) 16 | 17 | if args.expected_revisions is None: 18 | expected_revisions = None 19 | else: 20 | expected_revisions = {line.split("=")[0]: line.split("=")[1] for line in args.expected_revisions} 21 | 22 | root = tk.Tk() 23 | root.wm_title("BUMM Master control UI") 24 | root.grid_columnconfigure(0, weight=1) 25 | app = Application(connection, master=root, expected_revisions=expected_revisions) 26 | app.mainloop() 27 | -------------------------------------------------------------------------------- /master/gui/master_gui_main_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk, messagebox 3 | import time 4 | import random 5 | import subprocess 6 | import sys 7 | 8 | sys.path.append("../gamemaster") 9 | from parse_errorcode_from_cpp import parse_errorcode_from_cpp 10 | 11 | COL_ID = 0 12 | COL_REVISION = 1 13 | COL_NEXTSTATE = 2 14 | COL_RANDOMNUMBER = 3 15 | COL_STATE = 4 16 | COL_FAILURES = 5 17 | COL_DESCRIPTION = 6 18 | COL_SOLVE = 7 19 | 20 | """ 21 | center area (table) 22 | module id 23 | module revision (assumed from config and real, if different) 24 | module state (inactive, armed, disarmed, error) 25 | next state (on, off, random) 26 | failure counter 27 | description from module_solver 28 | button for more info (hints and solution) 29 | """ 30 | 31 | WIDTH_SHORT = 10 32 | table_default_style = dict(width=WIDTH_SHORT, relief=tk.RIDGE, anchor=tk.CENTER) 33 | current_display_default_style = dict(font="monospace 80", justify=tk.RIGHT, width=5) 34 | next_info_default_entry_style = dict( width=5, justify=tk.LEFT ) 35 | 36 | class Application(ttk.Frame): 37 | def __init__(self, connection, expected_revisions, master=None): 38 | ttk.Frame.__init__(self, master) 39 | 40 | self.lifes_at_start = 0 41 | 42 | self.connection = connection 43 | self.disable_while_running = [] 44 | self.is_running = False 45 | 46 | self.grid(sticky=tk.N+tk.S+tk.E+tk.W) 47 | self.module_controls = {} 48 | self.createWidgets() 49 | 50 | 51 | print("waiting for message") 52 | while not connection._CheckForMessage(): 53 | time.sleep(0.01) 54 | 55 | print("message found") 56 | hardware_description = connection._PopMessage()["capabilities"] 57 | 58 | self.module_descriptions = hardware_description["modules"] 59 | 60 | self.num_modules.set(len(self.module_descriptions)) 61 | 62 | for i, module_id in enumerate(sorted(self.module_descriptions.keys())): 63 | self.add_module_line(i+1, module_id, self.module_descriptions[module_id], expected_revisions) 64 | 65 | self.randomize() 66 | 67 | self.poll_network() 68 | 69 | def createWidgets(self): 70 | self.grid_columnconfigure(0, weight=1) 71 | 72 | # ************** 73 | # current display 74 | # ************** 75 | self.current_display = ttk.Frame(self) 76 | self.current_display.grid(column=0, row=1) 77 | 78 | # current countdown 79 | self.current_countdown = tk.StringVar() 80 | current_countdown = tk.Label(self.current_display, textvariable=self.current_countdown, foreground="red", **current_display_default_style) 81 | current_countdown.grid(row=0, column=1, sticky=tk.N+tk.S+tk.E+tk.W) 82 | 83 | # current lifes 84 | self.current_lifes = tk.StringVar(self.lifes_at_start) 85 | current_lifes = tk.Label(self.current_display, textvariable=self.current_lifes, foreground="green" , **current_display_default_style) 86 | current_lifes.grid(row=0, column=4, sticky=tk.N+tk.S+tk.E+tk.W) 87 | 88 | # start button 89 | self.btn_start = ttk.Button(self.current_display) 90 | self.btn_start["text"] = "Start" 91 | self.btn_start["command"] = self.start_new_game 92 | self.btn_start["state"] = tk.DISABLED 93 | self.btn_start.grid(row=1, column=2) 94 | 95 | # abort button 96 | self.btn_abort = ttk.Button(self.current_display) 97 | self.btn_abort["text"] = "Abort" 98 | self.btn_abort["command"] = self.abort_game 99 | self.btn_abort.grid(row=1, column=3) 100 | 101 | # error display 102 | self.error_display = ttk.Label(self.current_display, text="", justify=tk.CENTER, foreground="red") 103 | self.error_display.grid(row=2, column=1, columnspan=4) 104 | 105 | 106 | # ************** 107 | # next game info 108 | # ************** 109 | self.next_game_info = ttk.Frame(self) 110 | self.next_game_info.grid(column=0, row=2) 111 | 112 | # fill space on sides 113 | self.next_game_info.grid_columnconfigure(0, weight=1) 114 | ttk.Frame(self.next_game_info).grid(row=0, column=0) 115 | self.next_game_info.grid_columnconfigure(3, weight=1) 116 | ttk.Frame(self.next_game_info).grid(row=0, column=3) 117 | 118 | # seconds 119 | ttk.Label(self.next_game_info, text="seconds", justify=tk.RIGHT).grid(row=1, column=1) 120 | self.seconds = tk.StringVar(value="300") 121 | seconds = tk.Entry(self.next_game_info, textvariable=self.seconds, **next_info_default_entry_style) 122 | seconds.grid(row=1, column=2, sticky=tk.N+tk.S+tk.W) 123 | self.make_red_on_invalid_change(self.seconds, seconds, self.parse_seconds) 124 | self.seconds.trace("w", lambda *args: self.disable_widgets_if_necessary()) 125 | 126 | # maxerrors 127 | ttk.Label(self.next_game_info, text="max errors allowed", justify=tk.RIGHT).grid(row=2, column=1) 128 | self.maxerrors = tk.StringVar(value="0") 129 | maxerrors = tk.Entry(self.next_game_info, textvariable=self.maxerrors, **next_info_default_entry_style) 130 | maxerrors.grid(row=2, column=2, sticky=tk.N+tk.S+tk.W) 131 | self.make_red_on_invalid_change(self.maxerrors, maxerrors, self.parse_maxerrors) 132 | self.maxerrors.trace("w", lambda *args: self.disable_widgets_if_necessary()) 133 | 134 | 135 | 136 | # serial 137 | ttk.Label(self.next_game_info, text="serial number", justify=tk.RIGHT).grid(row=3, column=1) 138 | self.serial = tk.StringVar() 139 | serial = tk.Entry(self.next_game_info, textvariable=self.serial, **next_info_default_entry_style) 140 | serial.grid(row=3, column=2 141 | , sticky=tk.N+tk.S+tk.W 142 | ) 143 | self.make_red_on_invalid_change(self.serial, serial, self.parse_serial) 144 | self.serial.trace("w", lambda *args: self.disable_widgets_if_necessary()) 145 | self.disable_while_running.append(serial) 146 | 147 | # number of modules 148 | ttk.Label(self.next_game_info, text="number of modules", justify=tk.RIGHT).grid(row=4, column=1) 149 | self.num_modules = tk.StringVar(value="1") 150 | num_modules = tk.Entry(self.next_game_info, textvariable=self.num_modules, **next_info_default_entry_style) 151 | num_modules.grid(row=4, column=2 152 | , sticky=tk.N+tk.S+tk.W 153 | ) 154 | self.make_red_on_invalid_change(self.num_modules, num_modules, self.parse_num_modules) 155 | self.num_modules.trace("w", lambda *args: self.disable_widgets_if_necessary()) 156 | self.disable_while_running.append(num_modules) 157 | 158 | 159 | # randomize button 160 | self.btn_randomize = ttk.Button(self.next_game_info, text="randomize", command=self.randomize) 161 | self.btn_randomize.grid(row=5, column=1, columnspan=2) 162 | self.disable_while_running.append(self.btn_randomize) 163 | 164 | 165 | 166 | 167 | # table 168 | self.table = ttk.Frame(self) 169 | self.table.grid(column=0, row=3, sticky=tk.N+tk.S+tk.E+tk.W) 170 | self.table.grid_columnconfigure(COL_DESCRIPTION, weight=1) 171 | 172 | ttk.Label(self.table, text="ID", **table_default_style).grid(row=0, column=COL_ID, sticky=tk.N+tk.S+tk.E+tk.W) 173 | ttk.Label(self.table, text="REV", **table_default_style).grid(row=0, column=COL_REVISION, sticky=tk.N+tk.S+tk.E+tk.W) 174 | ttk.Label(self.table, text="NEXT", **table_default_style).grid(row=0, column=COL_NEXTSTATE, sticky=tk.N+tk.S+tk.E+tk.W) 175 | ttk.Label(self.table, text="RANDOM", **table_default_style).grid(row=0, column=COL_RANDOMNUMBER, sticky=tk.N+tk.S+tk.E+tk.W) 176 | ttk.Label(self.table, text="STATE", **table_default_style).grid(row=0, column=COL_STATE, sticky=tk.N+tk.S+tk.E+tk.W) 177 | ttk.Label(self.table, text="FAIL", **table_default_style).grid(row=0, column=COL_FAILURES, sticky=tk.N+tk.S+tk.E+tk.W) 178 | ttk.Label(self.table, text="DESCRIPTION", **table_default_style).grid(row=0, column=COL_DESCRIPTION, sticky=tk.N+tk.S+tk.E+tk.W) 179 | #ttk.Label(self.table, text="", **table_default_style).grid(row=0, column=COL_SOLVE) 180 | 181 | def add_module_line(self, row, module_id, module_desc, expected_revisions): 182 | self.module_controls[module_id] = {} 183 | controls = self.module_controls[module_id] 184 | module_id_widget = ttk.Label(self.table, text=module_id, **table_default_style) 185 | module_id_widget.grid(row=row, column=COL_ID, sticky=tk.N+tk.S+tk.E+tk.W) 186 | 187 | # revision 188 | revision = module_desc["revision"] 189 | if expected_revisions is None: 190 | rev_text = "{}*".format(revision) 191 | color = None 192 | else: 193 | try: 194 | if revision == int(expected_revisions[module_id]): 195 | rev_text = revision 196 | color = None 197 | else: 198 | rev_text = "{} / {}".format(revision, expected_revisions[module_id]) 199 | color = "red" 200 | except KeyError: 201 | rev_text = "{} / ?".format(revision) 202 | color = "red" 203 | 204 | module_revision = ttk.Label(self.table, text=rev_text, background=color, **table_default_style) 205 | module_revision.grid(row=row, column=COL_REVISION, sticky=tk.N+tk.S+tk.E+tk.W) 206 | 207 | # nextstate 208 | module_nextstate = ttk.Combobox(self.table, values=["on", "random", "off"], state="readonly") 209 | module_nextstate.grid(row=row, column=COL_NEXTSTATE, sticky=tk.N+tk.S+tk.E+tk.W) 210 | module_nextstate.current(1) 211 | controls["nextstate"] = module_nextstate 212 | 213 | # random value 214 | random_value = tk.StringVar() 215 | module_random = tk.Entry(self.table, textvariable=random_value, disabledforeground="black") 216 | module_random.grid(row=row, column=COL_RANDOMNUMBER, sticky=tk.N+tk.S+tk.E+tk.W) 217 | controls["random"] = module_random 218 | controls["random_value"] = random_value 219 | self.make_red_on_invalid_change(random_value, module_random, lambda module_id=module_id: self.parse_randomness_string(module_id)) 220 | random_value.trace("w", lambda *args: self.disable_widgets_if_necessary()) 221 | self.disable_while_running.append(module_random) 222 | 223 | # state 224 | module_state = ttk.Label(self.table, text="?", **table_default_style) 225 | module_state.grid(row=row, column=COL_STATE, sticky=tk.N+tk.S+tk.E+tk.W) 226 | controls["state"] = module_state 227 | 228 | # failure counter 229 | module_fail = ttk.Label(self.table, text="?", **table_default_style) 230 | module_fail.grid(row=row, column=COL_FAILURES, sticky=tk.N+tk.S+tk.E+tk.W) 231 | controls["fail"] = module_fail 232 | 233 | # ui description 234 | module_description = ttk.Label(self.table, text="?", relief=tk.RIDGE) 235 | module_description.grid(row=row, column=COL_DESCRIPTION, sticky=tk.N+tk.S+tk.E+tk.W) 236 | controls["desc"] = module_description 237 | 238 | # solve button 239 | module_solve = ttk.Button(self.table, text="solve", command=lambda module_id=module_id: self.solve(module_id), width=WIDTH_SHORT) 240 | module_solve.grid(row=row, column=COL_SOLVE) 241 | 242 | def make_red_on_invalid_change(self, variable, widget, callback): 243 | def red_if_invalid(widget, validator_callback): 244 | try: 245 | validator_callback() 246 | except ValueError as e: 247 | widget["background"] = "red" 248 | else: 249 | widget["background"] = "white" 250 | variable.trace("w", lambda *args, widget=widget, callback=callback: red_if_invalid(widget, callback)) 251 | 252 | 253 | def parse_seconds(self): 254 | result = int(self.seconds.get()) 255 | if result <= 20: 256 | raise ValueError("seconds has to be >20") 257 | return result 258 | 259 | def parse_maxerrors(self): 260 | result = int(self.maxerrors.get()) 261 | if result < 0: 262 | raise ValueError("maxerrors has to be >=0") 263 | return result 264 | 265 | def parse_num_modules(self): 266 | result = int(self.num_modules.get()) 267 | if result <= 0: 268 | raise ValueError("num_modules has to be >0") 269 | 270 | forced_modules, random_modules = self.get_forced_and_random_modules() 271 | if not len(forced_modules) <= result <= len(forced_modules)+len(random_modules): 272 | raise ValueError("num_modules has to be in [{}, {}]".format(len(forced_modules), len(forced_modules)+len(random_modules))) 273 | 274 | return result 275 | 276 | def parse_serial(self): 277 | if len(self.serial.get()) != 5: 278 | raise ValueError("serial number must have 5 digits") 279 | if not 1000 <= int(self.serial.get()[0:4]) <= 9999: 280 | raise ValueError("numeric part of serial number invalid") 281 | if not "A" <= self.serial.get()[4] <= "Z": 282 | raise ValueError("alpha part of serial number invalid") 283 | return self.serial.get() 284 | 285 | def parse_randomness_string(self, module_id): 286 | string = self.module_controls[module_id]["random_value"].get() 287 | integers = [int(x) for x in string.split()] 288 | for i in integers: 289 | if not 0 <= i < 256: 290 | raise ValueError("all numbers in string should be uint8_t integers") 291 | if len(integers) != self.module_descriptions[module_id]["num_random"]: 292 | raise ValueError("module {} requires {} random numbers ({} given)".format(module_id, self.module_descriptions[module_id]["num_random"], len(integers))) 293 | return integers 294 | 295 | def check_all_validities(self): 296 | try: 297 | self.gather_new_game_dict() 298 | 299 | except ValueError: 300 | return False 301 | else: 302 | return True 303 | 304 | def disable_widgets_if_necessary(self): 305 | for widget in self.disable_while_running: 306 | widget["state"] = tk.DISABLED if self.is_running else tk.NORMAL 307 | 308 | try: 309 | self.gather_new_game_dict() 310 | valid = True 311 | self.error_display["text"] = "" 312 | except Exception as e: 313 | valid = False 314 | self.error_display["text"] = str(e) 315 | 316 | self.btn_start["state"] = tk.DISABLED if self.is_running or not valid else tk.NORMAL 317 | #self.btn_abort["state"] = tk.NORMAL if self.is_running else tk.DISABLED 318 | 319 | 320 | def poll_network(self): 321 | if self.connection._CheckForMessage(): 322 | msg = self.connection._PopMessage() 323 | 324 | if "state" in msg: 325 | self.handle_update_modules(msg["state"]) 326 | elif "end" in msg: 327 | self.handle_game_end(msg["end"]) 328 | else: 329 | print("unknown message: {}".format(msg)) 330 | 331 | self.after(10, self.poll_network) 332 | 333 | def handle_update_modules(self, d): 334 | self.is_running = True 335 | for module_id, module_state in d["modules"].items(): 336 | self.update_module(module_id, module_state) 337 | self.current_countdown.set(d["seconds"]) 338 | self.current_lifes.set(d["lifes"]) 339 | self.disable_widgets_if_necessary() 340 | 341 | def handle_game_end(self, d): 342 | self.is_running = False 343 | self.disable_widgets_if_necessary() 344 | # TODO: add to highscore/statistics 345 | 346 | def update_module(self, module_id, module_state): 347 | controls = self.module_controls[module_id] 348 | 349 | has_error = False 350 | 351 | if module_state["state"] == 0: # armed 352 | controls["state"]["background"] = "red" 353 | controls["state"]["text"] = "ARMED" 354 | elif module_state["state"] == 1: # defused 355 | controls["state"]["background"] = "green" 356 | controls["state"]["text"] = "DEFUSED" 357 | else: 358 | controls["state"]["background"] = "orange" 359 | controls["state"]["text"] = "ERROR" 360 | controls["desc"]["text"] = parse_errorcode_from_cpp("../../modules/libraries/BUMMSlave/BUMMSlave.cpp", module_state["state"]) 361 | has_error = True 362 | 363 | if not has_error: 364 | controls["fail"]["text"] = module_state["failures"] 365 | 366 | def solve(self, module_id): 367 | try: 368 | text = self.get_solver_info(module_id, "hints") + "\n\n" + self.get_solver_info(module_id, "solution") 369 | except: 370 | text = "NOT AVAILABLE" 371 | messagebox.showinfo("hints and solution for module {}".format(module_id), text) 372 | 373 | def get_forced_and_random_modules(self): 374 | used_modules = [module_id for module_id, controls in self.module_controls.items() if controls["nextstate"].current() == 0] 375 | random_modules = [module_id for module_id, controls in self.module_controls.items() if controls["nextstate"].current() == 1] 376 | return used_modules, random_modules 377 | 378 | def gather_new_game_dict(self): 379 | num_modules = self.parse_num_modules() 380 | forced_modules, random_modules = self.get_forced_and_random_modules() 381 | 382 | used_modules = forced_modules + random.sample(random_modules, num_modules-len(forced_modules)) 383 | 384 | result = { 385 | "seconds": self.parse_seconds(), 386 | "maxerrors": self.parse_maxerrors(), 387 | "serial": self.parse_serial(), 388 | "modules": 389 | { 390 | module_id: { 391 | "enabled": module_id in used_modules, 392 | "random": self.parse_randomness_string(module_id) 393 | } 394 | for module_id in self.module_descriptions 395 | } 396 | } 397 | 398 | return result 399 | 400 | 401 | def start_new_game(self): 402 | game_dict = self.gather_new_game_dict() 403 | print(game_dict) 404 | self.connection.send_start(game_dict) 405 | self.is_running = True 406 | 407 | # describe-display 408 | for module_id in self.module_descriptions: 409 | try: 410 | ui_description = self.get_solver_info(module_id, "describe") 411 | except: 412 | ui_description = "NOT AVAILABLE" 413 | self.module_controls[module_id]["desc"]["text"] = ui_description 414 | 415 | self.disable_widgets_if_necessary() 416 | 417 | def abort_game(self): 418 | self.connection.send_abort() 419 | 420 | self.is_running = False 421 | self.disable_widgets_if_necessary() 422 | 423 | def randomize(self): 424 | try: 425 | difficulty = int(self.serial.get()[0:2]) 426 | if difficulty < 10: 427 | raise Exception() 428 | except: 429 | difficulty = random.randint(10, 99) 430 | new_serial = str(difficulty) + str(random.randrange(0, 99)).zfill(2) + chr(0x41+random.randrange(0,26)) 431 | self.serial.set(new_serial) 432 | 433 | for module_id, controls in self.module_controls.items(): 434 | num_random = self.module_descriptions[module_id]["num_random"] 435 | controls["random_value"].set(" ".join(str(random.randrange(0, 256)) for i in range(num_random))) 436 | 437 | def get_solver_info(self, module_id, mode): 438 | cwd = "../../modules/{}/".format(module_id) 439 | return (subprocess.run([cwd+"module_solver.py", mode, self.serial.get()] + [str(i) for i in self.parse_randomness_string(module_id)], cwd=cwd, stdout=subprocess.PIPE).stdout[:-1]).decode("utf-8") 440 | 441 | -------------------------------------------------------------------------------- /master/gui/test_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import time 3 | import sys 4 | import random 5 | 6 | sys.path.append("../lib") 7 | from server import Server 8 | 9 | if __name__ == "__main__": 10 | server = Server("",{ 11 | "a": {"revision": 3, "num_random": 0}, 12 | "b": {"revision": 1, "num_random": 1}, 13 | "c": {"revision": 2, "num_random": 1} 14 | }) 15 | state = {"seconds":49, "modules": 16 | { 17 | "a": {"state": 0, "failures": 0}, 18 | "b": {"state": 0, "failures": 0}, 19 | "c": {"state": 2, "failures": 0} 20 | }} 21 | 22 | running = False 23 | 24 | while(True): 25 | if server._CheckForMessage(): 26 | msg = server._PopMessage() 27 | if "start" in msg: 28 | running = True 29 | state["seconds"] = msg["start"]["seconds"] 30 | elif "abort" in msg: 31 | running = False 32 | server.send_game_end(reason=3) 33 | else: 34 | print(msg) 35 | time.sleep(1.0) 36 | 37 | state["seconds"] -= 1 38 | if random.randrange(20) == 0: 39 | state["modules"]["b"]["state"] = 1 40 | 41 | if running: 42 | server.send_game_update(state) 43 | 44 | if state["seconds"] <= 0: 45 | server.send_game_end(reason=1) 46 | running = False 47 | 48 | -------------------------------------------------------------------------------- /master/lib/client.py: -------------------------------------------------------------------------------- 1 | from connection import Connection 2 | 3 | class Client(Connection): 4 | def __init__(self, targetHostname): 5 | Connection.__init__(self, targetHostname) 6 | 7 | def send_abort(self): 8 | self._SendJson({"abort": None}) 9 | 10 | def send_start(self, info): 11 | self._SendJson({"start": info}) 12 | -------------------------------------------------------------------------------- /master/lib/connection.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | import sys 4 | 5 | PORT_NUMBER = 5000 6 | 7 | def byteify(val): 8 | if isinstance(val, dict): 9 | return {byteify(key): byteify(value) for key, value in (val.items() if sys.version_info.major == 3 else val.iteritems())} 10 | elif isinstance(val, list): 11 | return [byteify(element) for element in val] 12 | elif sys.version_info.major == 2 and isinstance(val, unicode): 13 | return val.encode('utf-8') 14 | else: 15 | return val 16 | 17 | class Connection(object): 18 | ## Time to try to establish a new connection 19 | CONNECTION_TIMEOUT = 0.1 20 | 21 | ## Initialises state and sends player information to display. 22 | # \param targetHostname IP of server to connect to. Use empty string to use server-mode 23 | def __init__(self, targetHostname): 24 | self.state = "init" 25 | 26 | self.buffer = "" 27 | 28 | self.server = None 29 | self.connection = None 30 | self.targetHostname = targetHostname 31 | self.port = PORT_NUMBER 32 | 33 | #self._TryConnect() 34 | 35 | def _CleanupConnections(self): 36 | print("disconnect!") 37 | self.server = None 38 | self.connection = None 39 | 40 | ## Try to connect to remote host. 41 | # If connection succeeds, internal state is updated accordingly. 42 | def _TryConnect(self): 43 | # server mode 44 | if self.targetHostname is "": 45 | # if server is not already running, start it 46 | if self.server is None: 47 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 48 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # avoid port block on server failure 49 | print("server mode: waiting for connection...") 50 | try: 51 | s.bind(("", self.port)) 52 | except socket.error: # address already in use? 53 | if self.port == PORT_NUMBER+10: 54 | self.port = PORT_NUMBER 55 | else: 56 | self.port += 1 57 | s.listen(1) 58 | s.settimeout(Connection.CONNECTION_TIMEOUT) 59 | self.server = s 60 | 61 | # check for new connections on server 62 | try: 63 | conn, addr = self.server.accept() 64 | print("connection from {}".format(addr)) 65 | self.connection = conn 66 | self.connection.setblocking(0) # non-blocking mode 67 | self._OnConnect() 68 | except socket.timeout: 69 | print("timeout - using port {}".format(self.port)) 70 | 71 | # client mode 72 | else: 73 | print("connecting to {}...".format(self.targetHostname)) 74 | for port_increment in range(10): 75 | try: 76 | s = socket.create_connection((self.targetHostname, PORT_NUMBER+port_increment), Connection.CONNECTION_TIMEOUT) 77 | self.connection = s 78 | print("connected") 79 | self.connection.setblocking(0) # non-blocking mode 80 | self._OnConnect() 81 | break 82 | except socket.timeout: 83 | print("timeout") 84 | except ConnectionRefusedError: 85 | print("refused - using port {}".format(PORT_NUMBER+port_increment)) 86 | 87 | ## Reads buffer until empty or \0 88 | # \returns True if new message complete available, else False 89 | # use popMessage to retrieve the message 90 | def _CheckForMessage(self): 91 | if self.connection is None: 92 | self._TryConnect() 93 | return False 94 | if len(self.buffer) > 0 and self.buffer[-1] == "\0": # already a message in the buffer 95 | return True 96 | try: 97 | while(True): 98 | #print(self.connection.gettimeout()) 99 | buf = self.connection.recv(1) 100 | if len(buf) == 0: # this will only occur on disconnects. if no data is available, a socket.error will be raised and handled below! 101 | self._CleanupConnections() 102 | return False 103 | self.buffer += buf.decode("utf-8") if sys.version_info.major == 3 else buf 104 | if len(self.buffer) > 0 and self.buffer[-1] == "\0": 105 | return True 106 | except socket.error: # no data available 107 | return False 108 | 109 | ## Return currently buffered message and clear the buffer 110 | def _PopMessage(self): 111 | j = byteify(json.loads(self.buffer[:-1])) 112 | self.buffer = "" 113 | print("got message: {}".format(j)) 114 | return j 115 | 116 | ## Internal helper function 117 | def _SendJson(self, data): 118 | if self.connection is None: 119 | self._TryConnect() 120 | else: 121 | j = json.dumps(data)+"\0" 122 | print(j) 123 | try: 124 | self.connection.send(j.encode("utf-8") if sys.version_info.major == 3 else j) 125 | print("sent message: {}".format(j)) 126 | except socket.error: 127 | self._CleanupConnections() 128 | 129 | ## Check whether connection is active 130 | # \returns True on connection failure, False otherwise 131 | def IsInErrorState(self): 132 | return self.connection is None 133 | 134 | def _OnConnect(self): 135 | pass 136 | -------------------------------------------------------------------------------- /master/lib/server.py: -------------------------------------------------------------------------------- 1 | from connection import Connection 2 | 3 | class Server(Connection): 4 | def __init__(self, targetHostname, module_descriptions): 5 | Connection.__init__(self, targetHostname) 6 | self.module_descriptions = module_descriptions 7 | 8 | def _OnConnect(self): 9 | self._SendJson({"capabilities": 10 | { 11 | "modules": self.module_descriptions 12 | }}) 13 | 14 | def send_game_update(self, state): 15 | self._SendJson({"state": state}) 16 | 17 | def send_game_end(self, reason): 18 | self._SendJson({"end": {"reason": reason}}) 19 | 20 | -------------------------------------------------------------------------------- /modules/a/Makefile: -------------------------------------------------------------------------------- 1 | ../default_makefile -------------------------------------------------------------------------------- /modules/a/a.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | BUMMSlave bs('a',0,0,2,3); 4 | 5 | #define PIN_BTN_DISARM 5 6 | #define PIN_BTN_FAIL 4 7 | 8 | void setup() 9 | { 10 | pinMode(PIN_BTN_DISARM, INPUT_PULLUP); 11 | pinMode(PIN_BTN_FAIL, INPUT_PULLUP); 12 | 13 | bs.begin(); 14 | } 15 | 16 | void loop() 17 | { 18 | bs.loop(); 19 | if(bs.isModuleEnabled()) 20 | { 21 | if(digitalRead(PIN_BTN_DISARM) == LOW) 22 | bs.disarm(); 23 | if(digitalRead(PIN_BTN_FAIL) == LOW) 24 | bs.disarmFailed(); 25 | } 26 | } 27 | 28 | void onModuleInit() 29 | { 30 | } 31 | 32 | void onGameStart() 33 | { 34 | } 35 | 36 | void onGameStatusUpdate() 37 | { 38 | } 39 | 40 | void onGameEnd(uint8_t result) 41 | { 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /modules/b/Makefile: -------------------------------------------------------------------------------- 1 | export USER_LIB_PATH=../libraries 2 | 3 | export ISP_PROG=avrisp2 4 | export ISP_PORT=usb 5 | 6 | 7 | #include /usr/share/arduino/Arduino.mk 8 | 9 | all: wires_autogen.cpp 10 | @$(MAKE) -f ../default_makefile all 11 | 12 | %: force 13 | @$(MAKE) -f ../default_makefile $@ 14 | 15 | force: ; 16 | 17 | .PHONY: panel manual 18 | 19 | panel: panel_autogen.svg 20 | 21 | manual: manual_en.pdf manual_de.pdf manual_chde.pdf 22 | 23 | manual_%.pdf: manual_figures manual_figures_autogen.tex manual.tex layout_autogen.tex flowchoice_autogen.tex 24 | pdflatex -jobname manual_$* "\input{../common/$*.tex}\input{manual.tex}" 25 | pdflatex -jobname manual_$* "\input{../common/$*.tex}\input{manual.tex}" 26 | 27 | wires_autogen.cpp: autowires.cpp.in lookuptable_autogen.npy 28 | python3 create_lookupcode.py 29 | 30 | cheatsheet_autogen.html: lookuptable_autogen.npy 31 | python3 create_cheatsheet.py 32 | 33 | manual_figures_autogen.tex: common.py create_figurelist.py logic_examples 34 | python3 create_figurelist.py 35 | 36 | logic_examples: create_logic_examples.py 37 | python3 create_logic_examples.py 38 | 39 | manual_figures: gates_autogen.dat 40 | python3 create_figures.py 41 | 42 | gates_autogen.dat: common.py 43 | python3 create_gatelist.py 44 | 45 | wires_autogen.cpp: autowires.cpp.in lookuptable_autogen.npy 46 | python3 create_lookupcode.py 47 | 48 | lookuptable_autogen.npy: gates_autogen.dat 49 | python3 create_lookuptable.py 50 | 51 | panel_autogen.svg: common.py ../../parts/laser_config.py ../../parts/top_plate.py 52 | python3 create_panel.py 53 | 54 | layout_autogen.tex: ../common/layout_generation/layout.py create_layout.py common.py 55 | python3 create_layout.py 56 | 57 | flowchoice_autogen.tex: create_flowchoice.py common.py 58 | python3 create_flowchoice.py 59 | -------------------------------------------------------------------------------- /modules/b/autowires.cpp.in: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include "autowires.h" 3 | #include "pin_definitions.h" 4 | 5 | uint8_t invert_leds(char* serial_number) 6 | {{ 7 | {invert_leds} 8 | return 0; 9 | }} 10 | 11 | uint8_t invert_switches(char* serial_number) 12 | {{ 13 | {invert_switches} 14 | return 0; 15 | }} 16 | 17 | uint8_t lookuptable[{num_tables}][{num_combinations}]= 18 | {{ 19 | {lookuptable} 20 | }}; 21 | 22 | uint8_t input_valid(uint8_t table_number, uint8_t random_value, char* serial_number) 23 | {{ 24 | uint8_t input_value = 0; 25 | {pininput} 26 | return lookuptable[table_number][random_value & {random_value_bitmask}] == input_value; 27 | }} 28 | 29 | void autowires_setup() 30 | {{ 31 | {setup} 32 | }} 33 | 34 | void autowires_setdisplay(uint8_t random_value, char* serial_number) 35 | {{ 36 | {setdisplay} 37 | }}; 38 | -------------------------------------------------------------------------------- /modules/b/autowires.h: -------------------------------------------------------------------------------- 1 | uint8_t input_valid(uint8_t table_number, uint8_t random_value, char* serial_number); 2 | void autowires_setup(); 3 | void autowires_setdisplay(uint8_t random_value, char* serial_number); 4 | -------------------------------------------------------------------------------- /modules/b/b.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "pin_definitions.h" 5 | #include "autowires.h" 6 | 7 | BUMMSlave bs('b',0,1,PIN_STATUS_RED,PIN_STATUS_GREEN); 8 | 9 | void setup() 10 | { 11 | autowires_setup(); 12 | bs.begin(); 13 | } 14 | 15 | uint8_t table_number_from_serial_number(char serialNumber[]) 16 | { 17 | if(serialNumber[0] < '2') 18 | return 0; 19 | else if(serialNumber[0] < '4') 20 | return 1; 21 | else if(serialNumber[0] < '6') 22 | return 2; 23 | else 24 | return 3; 25 | } 26 | 27 | void loop() 28 | { 29 | bs.loop(); 30 | if(bs.isArmed()) 31 | { 32 | if(get_debounced_flank(PIN_RUN) == LOW) // using closer as button! 33 | { 34 | if(input_valid(table_number_from_serial_number(bs.serialNumber), bs.randomSeeds[0], bs.serialNumber)) 35 | { 36 | bs.disarm(); 37 | } 38 | else 39 | { 40 | bs.disarmFailed(); 41 | } 42 | } 43 | } 44 | } 45 | 46 | void onModuleInit() 47 | { 48 | } 49 | 50 | void onGameStart() 51 | { 52 | autowires_setdisplay(bs.randomSeeds[0], bs.serialNumber); 53 | } 54 | 55 | void onGameStatusUpdate() 56 | { 57 | } 58 | 59 | void onGameEnd(uint8_t result) 60 | { 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /modules/b/common.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | GATE_FILE = "gates_autogen.dat" 4 | TABLE_FILE = "lookuptable_autogen.npy" 5 | 6 | class Gate(object): 7 | running_number = 0 8 | all_gates = [] 9 | def __init__(self): 10 | self.number = Gate.running_number 11 | Gate.running_number += 1 12 | Gate.all_gates.append(self) 13 | self.label = None 14 | 15 | self.children = False 16 | 17 | self.node_kwargs = {"fontname":"Courier"} 18 | 19 | def __str__(self): 20 | return str(self.number) 21 | 22 | def put_to_graph(self, graph): 23 | graph.add_node(self, label=self.fname if self.label is None else self.label, **self.node_kwargs) 24 | for inp in self.inputs: 25 | graph.add_edge(inp, self) 26 | 27 | def get_value(self, inputs): 28 | return self.func(*(parent.get_value(inputs) for parent in self.inputs)) 29 | 30 | class Nor(Gate): 31 | def __init__(self, in1, in2): 32 | Gate.__init__(self) 33 | self.fname = "nor" 34 | self.inputs = [in1, in2] 35 | 36 | def func(self, a, b): 37 | return not (a or b) 38 | 39 | class Nand(Gate): 40 | def __init__(self, in1, in2): 41 | Gate.__init__(self) 42 | self.fname = "nand" 43 | self.inputs = [in1, in2] 44 | 45 | def func(self, a, b): 46 | return not (a and b) 47 | 48 | class Out(Gate): 49 | def __init__(self, label, in1): 50 | Gate.__init__(self) 51 | self.fname = "out" 52 | self.inputs = [in1] 53 | 54 | self.label = label 55 | 56 | # self.node_kwargs["color"] = "green" 57 | 58 | def func(self, a): 59 | return a 60 | 61 | class In(Gate): 62 | def __init__(self, label): 63 | Gate.__init__(self) 64 | self.fname = "in" 65 | self.inputs = [] 66 | 67 | self.label = label 68 | 69 | # self.node_kwargs["color"] = "red" 70 | 71 | def get_value(self, inputs): 72 | return inputs[self.label] 73 | 74 | def func(self): 75 | return None 76 | 77 | # IMPORTANT: remember to change serialnumber->lookuptable-index in b.ino AND in module_solver.py! 78 | configs = [ 79 | { 80 | "num_gates":5, 81 | "stretch_factor":2.5, 82 | "title":"10xxx-19xxx", 83 | "gate_types":[Nor], 84 | "random_seed":0, 85 | "show_root_rule": True, 86 | "shuffle_rules_again": False 87 | }, 88 | { 89 | "num_gates":25, 90 | "stretch_factor":2.5, 91 | "title":"20xxx-39xxx", 92 | "gate_types":[Nor], 93 | "random_seed":3, 94 | "show_root_rule": True, 95 | "shuffle_rules_again": False 96 | }, 97 | { 98 | "num_gates":40, 99 | "stretch_factor":2.0, 100 | "title":"40xxx-59xxx", 101 | "gate_types":[Nand], 102 | "random_seed":1, 103 | "show_root_rule": True, 104 | "shuffle_rules_again": True 105 | }, 106 | { 107 | "num_gates":70, 108 | "stretch_factor":3.5, 109 | "title":"60xxx and later", 110 | "gate_types":[Nor], 111 | "random_seed":17, 112 | "show_root_rule": False, 113 | "shuffle_rules_again": True 114 | } 115 | ] 116 | 117 | random.seed(0) 118 | switch_inverts = sorted(random.sample( [str(i)+str(j) for i in range(0,10) for j in range(0,10)] , 30)) # number of inverted switches has to be a multiple of 5! 119 | led_inverts = sorted(random.sample([chr(0x41+i) for i in range(26)], 5)) 120 | 121 | input_names = ["DRV", "SET", "AVM", "DEL", "XTR", "LVL", "VDS"] 122 | output_names = ["AUX", "SRV", "DXF", "RTS"] 123 | 124 | 125 | -------------------------------------------------------------------------------- /modules/b/create_cheatsheet.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from common import * 4 | 5 | def main(): 6 | num_inputs = len(input_names) 7 | num_outputs = len(output_names) 8 | tables = np.load(TABLE_FILE) 9 | # generate cheatsheet 10 | with open("cheatsheet_autogen.html","w") as f: 11 | f.write("""\nModule: Gates - Cheatsheet\n\n""") 12 | for i, table in enumerate(tables): 13 | f.write("""
\n""".format(100//len(tables), 100//len(tables)*i)) 14 | f.write("

{}

\n".format(configs[i]["title"])) 15 | f.write("""\n""") 16 | for i in input_names: 17 | f.write("".format(i)) 18 | for o in reversed(output_names): 19 | f.write("".format(o)) 20 | f.write("\n") 21 | for row in table: 22 | f.write("") 23 | for col in row: 24 | f.write("".format("X" if col else "O")) 25 | f.write("\n") 26 | f.write("
{}{}
{}
\n") 27 | f.write("
\n") 28 | f.write("\n") 29 | 30 | main() 31 | -------------------------------------------------------------------------------- /modules/b/create_figurelist.py: -------------------------------------------------------------------------------- 1 | from common import * 2 | 3 | # generate manual figures 4 | with open("manual_figures_autogen.tex", "w") as f: 5 | invert_switches_table = "" 6 | for row in range(len(switch_inverts)//5): 7 | for col in range(5): 8 | i = col+row*5 9 | invert_switches_table += "xx{}x".format(switch_inverts[i]) 10 | if col < 4: 11 | invert_switches_table += " &" 12 | else: 13 | invert_switches_table += " \\\\" 14 | 15 | f.write(r""" 16 | \begin{{table}}[b] 17 | \begin{{center}} 18 | \begin{{tabular}}{{ccccc}} 19 | {} 20 | \end{{tabular}} 21 | \end{{center}} 22 | \caption{{ 23 | \en{{List of serial numbers with inverted switch directions (down=FALSE)}} 24 | \de{{Liste der Seriennummern mit invertierten Schaltern (unten=FALSE)}} 25 | \chde{{Liste der Seriennummern von invertierten Schalter (unten = FALSE)}} 26 | }} 27 | \label{{tab:b_switch_inversion}} 28 | \end{{table}} 29 | """.format( invert_switches_table )) 30 | 31 | f.write(r"\newcommand{{\ledInversionNumbers}}{{{}}}".format(", ".join("xxxx"+l for l in led_inverts[:-1]) + r" \en{or}\de{oder}\chde{oder} xxxx" + led_inverts[-1])) 32 | f.write("\n\\clearpage\n\n") 33 | 34 | for gate_name in ("nor", "nand"): 35 | f.write(r""" 36 | \begin{figure} 37 | \begin{center} 38 | """) 39 | for in1, in2 in ((False, False), (False, True), (True, True)): 40 | f.write(r"""\includegraphics[width=0.3\textwidth]{{logic_example_autogen_{}_{}_{}}}""".format(gate_name, in1, in2)) 41 | f.write(r""" 42 | \caption{{ 43 | \en{{Example for all possible {0} combinations}} 44 | \de{{Beispiel für alle möglichen Fälle für {0} Gatter}} 45 | \chde{{Unter allen möglichen Umständen {0} Türzargen}} 46 | }} 47 | \end{{center}} 48 | \end{{figure}} 49 | """.format(gate_name)) 50 | 51 | for i, config in enumerate(configs): 52 | f.write(r""" 53 | \begin{{figure}} 54 | \begin{{center}} 55 | \includegraphics[scale=0.4]{{wires_autogen_{i}}} 56 | \caption{{ 57 | \en{{Internal wiring for series {title}}} 58 | \de{{Interne Verdrahtung der Serie {title}}} 59 | \chde{{Eine Reihe von internen Verdrahtungs {title}}} 60 | }} 61 | \end{{center}} 62 | \end{{figure}}""".format(i=i, title=config["title"])) 63 | 64 | 65 | -------------------------------------------------------------------------------- /modules/b/create_figures.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import pygraphviz as pgv 3 | from common import * 4 | 5 | def main(): 6 | with open(GATE_FILE, "rb") as f: 7 | gates_per_config = pickle.load(f) 8 | 9 | for config_number, config in enumerate(configs): 10 | gates = gates_per_config[config_number] 11 | g = pgv.AGraph(directed=True) 12 | 13 | for gate in gates: 14 | gate.put_to_graph(g) 15 | 16 | g.layout(prog='dot') 17 | g.draw("wires_autogen_{}.pdf".format(config_number)) 18 | 19 | main() 20 | -------------------------------------------------------------------------------- /modules/b/create_flowchoice.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | from common import * 4 | 5 | random.seed(0) 6 | 7 | class Rule(object): 8 | def __eq__(self, other): 9 | return self.make_str() == other.make_str() 10 | 11 | class DecisionRule(Rule): 12 | def __init__(self, condition, false_result, true_result): 13 | assert isinstance(condition, str) 14 | assert isinstance(false_result, Rule) 15 | assert isinstance(true_result, Rule) 16 | self.condition = condition 17 | self.false_result = false_result 18 | self.true_result = true_result 19 | 20 | def make_str(self): 21 | return "{}?({}):({})".format(self.condition, self.true_result.make_str(), self.false_result.make_str()) 22 | 23 | def make_tex(self, all_rules): 24 | return r"\en{{If {0} is TRUE, obey rule {1}, else obey rule {2}}}\de{{Wenn {0} TRUE ist, befolge Regel {1}, ansonsten befolge Regel {2}}}\chde{{Wenn {0} wahr ist, folgen die Regeln {1}, {2}, sonst die Regeln}}".format( 25 | self.condition.upper(), 26 | get_number(all_rules, self.true_result), 27 | get_number(all_rules, self.false_result) 28 | ) 29 | 30 | class MultiRule(Rule): 31 | def __init__(self, subrules): 32 | assert isinstance(subrules, list) 33 | for r in subrules: 34 | assert isinstance(r, Rule) 35 | self.subrules = subrules 36 | 37 | def make_str(self): 38 | return "[{}]".format(",".join(r.make_str() for r in self.subrules)) 39 | 40 | def make_tex(self, all_rules): 41 | return r"\en{{Obey the rules {0} and {1}}}\de{{Befolge Regel {0} und {1}}}\chde{{In Übereinstimmung mit den Regeln {0} und {1}}}".format(", ".join(str(get_number(all_rules, r)) for r in self.subrules[:-1]), get_number(all_rules, self.subrules[-1])) 42 | 43 | class LeafRule(Rule): 44 | def __init__(self, switchresult): 45 | assert isinstance(switchresult, str) 46 | assert switchresult.upper() in output_names 47 | self.switchresult = switchresult 48 | 49 | def make_str(self): 50 | return self.switchresult 51 | 52 | def make_tex(self, all_rules): 53 | return r"\en{{Set {0} to {1}}}\de{{Schalte {0} auf {1}}}\chde{{Schalten {0} auf {1}}}".format(self.switchresult.upper(), "TRUE" if self.switchresult.isupper() else "FALSE") 54 | 55 | def get_number(all_rules, rule): 56 | for i, r in enumerate(all_rules): 57 | if r == rule: 58 | return i+1 59 | raise Exception("rule {} not found!".format(rule.make_str())) 60 | 61 | 62 | def add_rule_if_new(rules, rule): 63 | assert isinstance(rule, Rule) 64 | for r in rules: 65 | if r == rule: 66 | return r 67 | rules.append(rule) 68 | return rule 69 | 70 | def make_leaf_rule(all_rules, tableline): 71 | switch_states = tableline[len(input_names):] 72 | return add_rule_if_new(all_rules, MultiRule( 73 | [ 74 | add_rule_if_new(all_rules, LeafRule(name.upper() if switch_states[-i-1] else name.lower())) 75 | for i, name in enumerate(output_names) 76 | ])) 77 | 78 | def make_decision_rule(all_rules, condition, false_result, true_result): 79 | false_result = add_rule_if_new(all_rules, false_result) 80 | true_result = add_rule_if_new(all_rules, true_result) 81 | return add_rule_if_new(all_rules, DecisionRule(condition, false_result, true_result)) 82 | 83 | def make_recursive_rule(all_rules, table, depth): 84 | assert table.shape[0] == 2**(len(input_names)-depth) 85 | if table.shape[0] == 2: 86 | false_result = make_leaf_rule(all_rules, table[0]) 87 | true_result = make_leaf_rule(all_rules, table[1]) 88 | if false_result == true_result: 89 | return false_result 90 | return make_decision_rule(all_rules, input_names[0], false_result, true_result) 91 | else: 92 | false_result = make_recursive_rule(all_rules, table[:table.shape[0]//2], depth+1) 93 | true_result = make_recursive_rule(all_rules, table[table.shape[0]//2:], depth+1) 94 | if false_result == true_result: 95 | return false_result 96 | return make_decision_rule(all_rules, input_names[len(input_names)-1-depth], false_result, true_result) 97 | 98 | def generate_text(all_rules): 99 | result = "" 100 | for i, rule in enumerate(all_rules): 101 | result += r"\item[\en{{Rule}}\de{{Regel}}\chde{{Regel}} {0}]: ".format(i+1) + rule.make_tex(all_rules) + "\n" 102 | return result 103 | 104 | def main(): 105 | tables = np.load(TABLE_FILE) 106 | text = "" 107 | root_rule_table = "" 108 | 109 | for level, table in enumerate(tables): 110 | rules = [] 111 | root_rule = make_recursive_rule(rules, table, 0) 112 | random.shuffle(rules) 113 | root_rule_number = get_number(rules, root_rule) 114 | 115 | if configs[level]["show_root_rule"]: 116 | root_rule_table += "{} & {} \\\\\n".format(configs[level]["title"], root_rule_number) 117 | 118 | text += "\n"+r"\section*{{\en{{Series {0}}}\de{{Serien {0}}}\chde{{Serien {0}}}}}".format(configs[level]["title"])+"\n\n" 119 | text += r"\begin{description}"+"\n" 120 | ruletext = generate_text(rules) 121 | if configs[level]["shuffle_rules_again"]: 122 | textlist = ruletext.split("\n") 123 | random.shuffle(textlist) 124 | ruletext = "\n".join(textlist) 125 | text += ruletext 126 | text += r"\end{description}"+"\n" 127 | 128 | with open("flowchoice_autogen.tex", "w") as f: 129 | f.write(r"\begin{table}\begin{center}\begin{tabular}{|l|l|}\hline"+"\n") 130 | f.write(r"\en{Series}\de{Seriennummer}\chde{Serien} & \en{rule}\de{Regel}\chde{Regel}\\\hline"+"\n") 131 | f.write(root_rule_table) 132 | f.write(r"\hline\end{tabular}\caption{\en{Most important Rules}\de{Wichtigste Regeln}\chde{Die Hauptregel}}\label{tab:root_rules}\end{center}\end{table}"+"\n") 133 | f.write(text) 134 | 135 | main() 136 | -------------------------------------------------------------------------------- /modules/b/create_gatelist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import pickle 3 | from common import * 4 | 5 | def main(): 6 | num_inputs = len(input_names) 7 | num_outputs = len(output_names) 8 | 9 | gates_per_config = [] 10 | 11 | for config_number, config in enumerate(configs): 12 | random.seed(config["random_seed"]) 13 | 14 | 15 | num_gates = config["num_gates"] 16 | stretch_factor = config["stretch_factor"] 17 | 18 | inputs = [] 19 | intermediate_gates = [] 20 | outputs = [] 21 | gates = [] 22 | 23 | for i in range(num_inputs): 24 | inputs.append(In(input_names[i])) 25 | gates += inputs 26 | 27 | for i in range(num_gates): 28 | gate_type = random.choice(config["gate_types"]) 29 | gate = gate_type(*random.sample(gates[-int(num_inputs*stretch_factor):], 2)) 30 | intermediate_gates.append(gate) 31 | gates.append(gate) 32 | 33 | 34 | for i in range(num_outputs): 35 | outputs.append(Out(output_names[i], intermediate_gates[-(i+1)])) 36 | gates += outputs 37 | 38 | # prune graph 39 | continue_pruning = True 40 | while(continue_pruning): 41 | continue_pruning = False 42 | for gate in gates: 43 | gate.has_children = gate in outputs 44 | 45 | for gate in gates: 46 | for parent in gate.inputs: 47 | parent.has_children = True 48 | rest = [gate for gate in gates if gate.has_children] 49 | if len(rest) < len(gates): 50 | gates[:] = rest 51 | continue_pruning = True 52 | 53 | gates_per_config.append(gates) 54 | 55 | with open(GATE_FILE, "wb") as f: 56 | pickle.dump(gates_per_config, f) 57 | 58 | main() 59 | -------------------------------------------------------------------------------- /modules/b/create_layout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | from common import * 4 | import sys 5 | sys.path.append("../../parts") 6 | sys.path.append("../common/layout_generation") 7 | import top_plate 8 | import layout 9 | 10 | with open("layout_autogen.tex","w") as f: 11 | names = ["SET", "DEL", "RTS", "AUX", "AVM", "XTR", "DRV", "DXF", "VDS", "SRV", "LVL", "RUN"] 12 | 13 | num_cols = 4 14 | num_rows = 3 15 | 16 | h_padding = 15.0 17 | v_padding = 30.0 18 | 19 | h_distance = (100.0 - 2*h_padding)/(num_cols-1) 20 | v_distance = (100.0 - 2*v_padding)/(num_rows-1) 21 | 22 | f.write(layout.make_plate()) 23 | 24 | for i, name in enumerate(names): 25 | x_i = i // num_rows 26 | y_i = i % num_rows 27 | x = x_i * h_distance + h_padding 28 | y = y_i * v_distance + v_padding 29 | 30 | if name in output_names: 31 | f.write(layout.make_clipart((x,y), "switch_up")) 32 | else: 33 | f.write(layout.make_clipart((x,y), "led_off")) 34 | 35 | f.write(layout.make_label((x, y), name, "left" if x_i in (0,1) else "right", y-v_distance*{0:0, 1:-0.5, 2:+0.5, 3:0}[x_i])) 36 | 37 | -------------------------------------------------------------------------------- /modules/b/create_logic_examples.py: -------------------------------------------------------------------------------- 1 | import pygraphviz as pgv 2 | 3 | node_kwargs = {"fontname":"Courier"} 4 | params = { 5 | False : dict(color="red", fontname="Courier", style="dashed"), 6 | True : dict(color="green", fontname="Courier") 7 | } 8 | print(params[True]) 9 | 10 | def main(): 11 | for gate_name, gate_f in ( 12 | ("nor", lambda a, b: not (a or b)), 13 | ("nand", lambda a, b: not (a and b)) 14 | ): 15 | for in1, in2 in ((False, False), (False, True), (True, True)): 16 | result = gate_f(in1, in2) 17 | g = pgv.AGraph(directed=True) 18 | 19 | g.add_node("in1", label=str(in1).upper(), **params[in1]) 20 | g.add_node("in2", label=str(in2).upper(), **params[in2]) 21 | g.add_node("gate", label="{}\n{}".format(gate_name, str(result).upper()), **params[result]) 22 | g.add_node("result", style="invis") 23 | 24 | g.add_edge("in1", "gate", **params[in1]) 25 | g.add_edge("in2", "gate", **params[in2]) 26 | g.add_edge("gate", "result", **params[result]) 27 | 28 | g.layout(prog='dot') 29 | g.draw("logic_example_autogen_{}_{}_{}.pdf".format(gate_name, in1, in2)) 30 | 31 | main() 32 | -------------------------------------------------------------------------------- /modules/b/create_lookupcode.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from common import * 3 | 4 | template_pininput = """ 5 | if(digitalRead(PIN_{name}) == HIGH) 6 | input_value |= (1<<{bit}); 7 | """ 8 | 9 | def main(): 10 | num_inputs = len(input_names) 11 | num_outputs = len(output_names) 12 | tables = np.load(TABLE_FILE) 13 | 14 | pininput = "\n".join([template_pininput.format(name=name, bit=i) for i, name in enumerate(output_names)]) 15 | pininput += "\n\tif(invert_switches(serial_number))\n\t\tinput_value ^= {};\n".format(2**num_outputs-1) 16 | 17 | lookuptable = ",\n".join([ 18 | "// {}\n{{ ".format(configs[i]["title"])+np.array2string(np.sum(np.power(2, np.arange(num_outputs))[None, ::-1] * tables[i, :, num_inputs:], axis=1), separator=", ")[1:-1]+"}" # remove [] on the outside 19 | for i in range(len(configs))]) 20 | 21 | setup = "\n".join(["""\tpinMode(PIN_{name}, OUTPUT);""".format(name=name) for name in input_names]) 22 | setup += "\n\n" 23 | setup += "\n".join(["""\tpinMode(PIN_{name}, INPUT_PULLUP);""".format(name=name) for name in output_names+["RUN"]]) 24 | 25 | setdisplay = "\tif(invert_leds(serial_number))\n\t\trandom_value ^= 0xFF;\n\n" 26 | setdisplay += "\n".join(["""\tdigitalWrite(PIN_{name}, random_value & (1<<{bit}) ? HIGH : LOW);""".format(name=name, bit=i) for i, name in enumerate(input_names)]) 27 | 28 | invert_leds_string = "\n".join(("""\tif(serial_number[4]=='{}') return 1;""".format(l) for l in led_inverts)) 29 | 30 | invert_switches_string = "\n".join(("""\tif( (serial_number[2]=='{}') && (serial_number[3]=='{}') ) return 1;""".format(s[0], s[1]) for s in switch_inverts)) 31 | 32 | random_value_bitmask = 2**num_inputs-1 33 | 34 | with open("autowires.cpp.in", "r") as f: 35 | template = f.read() 36 | 37 | with open("wires_autogen.cpp","w") as f: 38 | f.write("// This file has been generated automatically! Do not modify!\n") 39 | f.write(template.format( 40 | lookuptable=lookuptable, 41 | pininput=pininput, 42 | setup=setup, 43 | setdisplay=setdisplay, 44 | invert_switches = invert_switches_string, 45 | invert_leds = invert_leds_string, 46 | random_value_bitmask = random_value_bitmask, 47 | num_tables=len(tables), 48 | num_combinations=len(tables[0]))) 49 | 50 | main() 51 | -------------------------------------------------------------------------------- /modules/b/create_lookuptable.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import numpy as np 3 | 4 | from common import * 5 | 6 | def boolean_table(num_inputs, outputs): 7 | table = np.empty((2**num_inputs, num_inputs+len(outputs),), dtype=np.bool) 8 | mesh = np.array(np.meshgrid(*[[False, True] for i in range(num_inputs)], indexing="ij")) 9 | mesh.shape = num_inputs, 2**num_inputs 10 | mesh = mesh.T[:,::-1] 11 | 12 | for i in range(2**num_inputs): 13 | table[i, :num_inputs] = mesh[i] 14 | input_values = {input_names[-k-1]:table[i, k] for k in range(num_inputs)} 15 | for j, o in enumerate(outputs): 16 | table[i, -j-1] = o.get_value(input_values) 17 | return table 18 | 19 | 20 | def main(): 21 | num_inputs = len(input_names) 22 | num_outputs = len(output_names) 23 | tables = np.empty((len(configs), 2**num_inputs, num_inputs+num_outputs,), dtype=np.bool) 24 | 25 | with open(GATE_FILE, "rb") as f: 26 | gates_per_config = pickle.load(f) 27 | for config_number, config in enumerate(configs): 28 | gates = gates_per_config[config_number] 29 | outputs = gates[-len(output_names):] 30 | 31 | # generate lookup table 32 | table = boolean_table(len(input_names), outputs) 33 | tables[config_number,] = table 34 | 35 | for i in range(num_outputs): 36 | print("output {}: {} probability to be True".format(i, np.sum( table[:, num_inputs+i]) / (2**num_inputs) )) 37 | 38 | np.save(TABLE_FILE, tables) 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /modules/b/create_panel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | from common import * 4 | sys.path.append("../../parts") 5 | import top_plate 6 | import laser_config 7 | 8 | # generate panel svg 9 | with open("panel_autogen.svg", "w") as f: 10 | cut = laser_config.styles["cut"] 11 | engrave = laser_config.styles["engrave"] 12 | 13 | circle = """\n""" 14 | svg_start = """\n\n""".format( 15 | width=top_plate.plate_width, height=top_plate.plate_width, id="panel", units="mm") 16 | svg_end = "\n" 17 | text = """{}\n""" 18 | 19 | names = input_names+output_names 20 | random.shuffle(names) 21 | 22 | f.write(svg_start) 23 | 24 | f.write(top_plate.make_top_plate("lt rt lb rb")) 25 | f.write(circle.format(top_plate.plate_width/2, -top_plate.hole_distance, laser_config.radius_led5, style=cut)) 26 | 27 | num_cols = 4 28 | num_rows = 3 29 | 30 | h_padding = 15.0 31 | v_padding = 30.0 32 | 33 | text_offset = - 6.0 34 | 35 | h_distance = (100.0 - 2*h_padding)/(num_cols-1) 36 | v_distance = (100.0 - 2*v_padding)/(num_rows-1) 37 | 38 | for i, name in enumerate(names): 39 | x = i // num_rows * h_distance + h_padding 40 | y = i % num_rows * v_distance + v_padding 41 | radius = laser_config.radius_led5 if name in input_names else laser_config.radius_switch 42 | 43 | f.write(circle.format(x, y, radius, style=cut)) 44 | f.write(text.format(x, y+text_offset, name, style=engrave)) 45 | 46 | x = (num_cols-1) * h_distance + h_padding 47 | y = (num_rows-1) * v_distance + v_padding 48 | 49 | f.write(circle.format(x, y, laser_config.radius_button_small, style=cut)) 50 | f.write(text.format(x, y+text_offset, "RUN", style=engrave)) 51 | 52 | 53 | 54 | f.write(svg_end) 55 | 56 | -------------------------------------------------------------------------------- /modules/b/manual.tex: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper]{report} 2 | 3 | % begin config 4 | \author{hephaisto} 5 | \newcommand{\moduleTitle}{Gates} 6 | \newcommand{\moduleRevision}{1} 7 | \newcommand{\moduleId}{b} 8 | % end config 9 | 10 | \title{\moduleTitle} 11 | \input{../common/manual_preamble} 12 | \usepackage{tikz} 13 | \usepackage{array} 14 | 15 | \begin{document} 16 | 17 | % begin content 18 | 19 | \begin{figure}[t] 20 | \begin{center} 21 | \begin{tikzpicture}[scale=0.03,y=-1cm] 22 | \input{layout_autogen} 23 | \end{tikzpicture} 24 | \end{center} 25 | \caption{Panel layout} 26 | \label{fig:b_layout} 27 | \end{figure} 28 | 29 | \begin{table} 30 | \centering 31 | \begin{tabular}{|cm{1cm}m{1cm}|}\hline 32 | % Signal & LED & Switch \\\hline 33 | TRUE & \includegraphics{../common/cliparts/led_on} & \includegraphics{../common/cliparts/switch_down} \\ 34 | FALSE & \includegraphics{../common/cliparts/led_off} & \includegraphics{../common/cliparts/switch_up} \\\hline 35 | \end{tabular} 36 | \caption{ 37 | \en{Logical states corresponding to input and output elements in default configuration} 38 | \de{Logischer status der Eingabe- und Ausgabeelemente} 39 | \chde{Der logische Zustand der Ein- und Ausgabeeinheit} 40 | } 41 | \label{tab:b_io_logic} 42 | \end{table} 43 | 44 | % BEGIN EXPLANATION 1 (NAND/NOR gates) 45 | 46 | \input{manual_figures_autogen} 47 | 48 | \section*{\en{How to defuse}\de{Entschärfungsanleitung}\chde{Entschärfungsanleitung}} 49 | \en{ 50 | The Gates-module is an accumulation of multiple logical gates connected to other gates and named signal lines. 51 | Each gate has two inputs (binary gate), which can have values of TRUE or FALSE. 52 | Each gate drives all outgoing signal lines to the output value according to the inputs and table \ref{tab:b_truthtable}. 53 | Some named signal lines are connected to LEDs, enabling the user to see the current state of the line. 54 | To defuse this module, set all switches so that they have the value of the corresponding signal line, and press RUN down. 55 | 56 | This module is also available with inverted LED values (choose \ledInversionNumbers ) and inverted switch directions (table \ref{tab:b_switch_inversion}). 57 | } 58 | \de{ 59 | Das ``Gates'' Modul ist eine Ansammlung von logischen Gattern, die untereinander und mit Signalleitungen verbunden sind. 60 | Jedes Gatter hat zwei Eingabeleitungen, die einen Wert von TRUE (wahr) oder FALSE (falsch) annehmen können, und legt an allen Ausgabeleitungen einen Wert entsprechend Tabelle \ref{tab:b_truthtable} an. 61 | Manche Signalleitungen sind mit LEDs verbunden, sodass man an deren Status den Wert der Leitung erkennen kann. 62 | Um dieses Modul zu entschärfen müssen alle Schalter in die Stellung gebracht werden, die dem Wert der gleichbenannten Signalleitung entspricht, gefolgt von einem Druck auf den RUN Knopf. 63 | Dieses Modul ist auch mit invertierten LED Anzeigen (Seriennummern \ledInversionNumbers ) und invertierten Schalter-Richtungen verfügbar (siehe Tabelle \ref{tab:b_switch_inversion}). 64 | } 65 | \chde{ 66 | In ``Gates'' Logikgatter -Modul, mit einander, und sie sind mit der Signalleitung Sammlung verbunden. 67 | Jedes Tor hat zwei Eingangsleitungen , TRUE (true) oder falsch (false) Wert akzeptiert werden kann, und gilt für alle Ausgangsleitungen entsprechend der Wertetabelle \ref{tab:b_truthtable}. 68 | Ein Teil der Signalleitung an die LED-Leuchten angeschlossen, so dass Sie den Wert des Rohres, um ihre Position erkennen kann. 69 | Die Notwendigkeit, alle des Schaltmoduls lösen wird in die Position gebracht, um die Signalleitung gleichbenannten Wert, drücken Sie die Taste RUN entspricht. 70 | Das Modul auch invertierte LED-Anzeige (Seriennummer \ledInversionNumbers) und das invertierte Schaltrichtung (siehe Tabelle \ref{tab:b_switch_inversion}). 71 | } 72 | 73 | \begin{table} 74 | \centering 75 | \begin{tabular}{|cc|cc|}\hline 76 | IN 1 & IN 2 & nor & nand \\\hline 77 | FALSE & FALSE & TRUE & TRUE \\ 78 | FALSE & TRUE & FALSE & TRUE \\ 79 | TRUE & FALSE & FALSE & TRUE \\ 80 | TRUE & TRUE & FALSE & FALSE \\\hline 81 | \end{tabular} 82 | \caption{ 83 | \en{Truth table for NAND and NOR gates for different input values} 84 | \de{Wahrheitstabelle für NAND und NOR Gatter für verschiedene Eingabewerte} 85 | \chde{Für NAND- und NOR-Gatter für die verschiedenen Eingangswerte Wahrheitstabelle} 86 | } 87 | \label{tab:b_truthtable} 88 | \end{table} 89 | 90 | % END EXPLANATION 1 (NAND/NOR gates) 91 | 92 | \clearpage 93 | 94 | % BEGIN EXPLANATION 2 (rules) 95 | \section*{\en{How to defuse}\de{Entschärfungsanleitung}\chde{Entschärfungsanleitung}} 96 | \en{This module can be defused following some simple rules listed below. Be careful, not all rules are equally important and you will have to ignore some! Start with the one rule that is not referenced by others (table \ref{tab:root_rules}). Press the RUN button down when all necessary rules are followed.} 97 | \de{Dieses Modul kann entschärft werden, indem nachfolgende simple Regeln befolgt werden. Nicht alle Regeln sind wichtig, viele müssen ignoriert werden! Starte mit der einen Regel, die von keiner anderen erwähnt wird (siehe Tabelle \ref{tab:root_rules}). Drücke den RUN-Knopf herunter, wenn alle relevanten Regeln erfüllt sind.} 98 | \chde{Das Modul kann durch die folgenden einfachen Regeln erhalten werden, zur Einhaltung erleichtert werden. Nicht alle Regeln sind wichtig, viele müssen ignoriert werden! Beginnen Sie mit einem anderen Referenz (siehe Tabelle \ref{tab:root_rules}) Regeln. Wenn Sie die RUN-Taste drücken, werden alle einschlägigen Vorschriften erfüllt sind.} 99 | 100 | \en{This module is also available with inverted LED values (choose \ledInversionNumbers ) and inverted switch directions (table \ref{tab:b_switch_inversion}).} 101 | \de{Dieses Modul ist auch mit invertierten LED Anzeigen (Seriennummern \ledInversionNumbers ) und invertierten Schalter-Richtungen verfügbar (siehe Tabelle \ref{tab:b_switch_inversion}).} 102 | \chde{Das Modul auch invertierte LED-Anzeige (Seriennummer \ledInversionNumbers) und das invertierte Schaltrichtung (siehe Tabelle \ref{tab:b_switch_inversion}).} 103 | \input{flowchoice_autogen} 104 | % end EXPLANATION 2 (rules) 105 | 106 | % end content 107 | 108 | \end{document} 109 | -------------------------------------------------------------------------------- /modules/b/module_solver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import common 5 | import numpy as np 6 | 7 | def describe(serial, randomness): 8 | invert_leds = serial[4] in common.led_inverts 9 | 10 | result = ", ".join("{} : {}".format(name, "on" if (((randomness & 1< y else -1) 40 | 41 | kink_point_offset = max(math.fabs(label_y - y), connector_distance) 42 | coordinates = ( 43 | (label_x, label_y), # label point 44 | (x - kink_point_offset*leftrightfactor, y+kink_point_offset*topdownfactor), # kink point 45 | (x - connector_distance*(1.0 if label_y == y else sqhalf)*leftrightfactor, y+connector_distance*sqhalf*topdownfactor) # "arrowhead" 46 | ) 47 | result = "" 48 | result += r"\draw [ultra thin] {};".format(" -- ".join("({},{})".format(*c) for c in coordinates)) 49 | result += r"\node [{}] at ({},{}) {{{}}};".format(label_horizontal, coordinates[0][0], coordinates[0][1], label) 50 | return result 51 | 52 | -------------------------------------------------------------------------------- /modules/common/manual/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: manual 2 | 3 | manual: manual_en.pdf manual_de.pdf manual_chde.pdf 4 | 5 | manual_%.pdf: manual.tex 6 | pdflatex -jobname manual_$* "\input{../$*.tex}\input{manual.tex}" 7 | pdflatex -jobname manual_$* "\input{../$*.tex}\input{manual.tex}" 8 | 9 | -------------------------------------------------------------------------------- /modules/common/manual/manual.tex: -------------------------------------------------------------------------------- 1 | \documentclass[a4paper]{report} 2 | 3 | \author{hephaisto} 4 | 5 | \title{Intro} 6 | \input{../common_preamble} 7 | \rhead{\en{Introduction}\de{Einführung}\chde{Beginn}} 8 | 9 | \begin{document} 10 | 11 | \en{ 12 | Thank you for purchasing a custom made BUMM. 13 | Please remember to connect your own external effector, which will be activated when the countdown reaches 0:00. 14 | We suggest using C4 as an effector for the default configuration. 15 | For other effectors, please do not hesitate to contact our sales department for different options. 16 | } 17 | \de{ 18 | Vielen Dank, dass Sie sich für den Kauf eines individuell gefertigten BUMM entschieden haben. 19 | Bitte beachten Sie, dass jedes BUMM mit einem eigenen externen Effektgeber verbunden werden muss. 20 | Dieser wird aktiviert, sobald der Countdown 0:00 erreicht. 21 | Wir empfehlen C4 als Standardeffektgeber. 22 | Für andere Effektgeber kontaktieren Sie bitte unsere Verkaufsabteilung mit ihren genauen Spezifikationen. 23 | } 24 | \chde{ 25 | Vielen Dank, dass Sie sich für individuelle BUMM zu erwerben. 26 | Bitte beachten Sie, dass jeder Ausleger muss auf seine eigene externe Agenten verbunden werden. 27 | Wenn der Countdown 0:00 erreicht wird diese aktiviert. 28 | Wir empfehlen C4 als Standardmittel. 29 | Für andere Effekte wenden Sie sich bitte an unsere Verkaufsabteilung mit ihren Spezifikationen. 30 | } 31 | 32 | \en{ 33 | Each BUMM is uniquely assembled using different control modules as per customer specification. 34 | Each control module can be activated independently and has to be defused separately, if the situation requires a restart. 35 | Defusing a single module changes the status indicator LED from red to green. 36 | You do not have to activate all modules, but it is highly suggested you do so to achieve maximum security. 37 | } 38 | \de{ 39 | Jedes BUMM wird entsprechend der Kundenspezifikation aus einzelnen Modulen gefertigt. 40 | Jedes dieser Module kann eigenständig aktiviert werden und muss separat entschärft werden, falls der Countdown deaktivert werden soll. 41 | Entschärfen eines Moduls ändert die Zustandsleuchtanzeige des Moduls von rot auf grün. 42 | Sie müssen nicht alle Module aktivieren, wir empfehlen dies jedoch aus Sicherheitsgründen. 43 | } 44 | \chde{ 45 | Jede BUMM werden entsprechend den Spezifikationen der einzelnen Kunden Modul. 46 | Jedes Modul kann unabhängig aktiviert werden, müssen einzeln angehoben werden, wenn der Countdown deaktiviert wird. 47 | Modul-Modul, von rot auf grün Statusanzeige entschärfen. 48 | Sie müssen nicht alle Module zu aktivieren, empfehlen wir Ihnen, so aus Gründen der Sicherheit zu tun. 49 | } 50 | 51 | \warning \en{Do not move the BUMM until all status indicators are green or off! 52 | Moving the BUMM with armed modules may result in immediate activation of the effector! 53 | } 54 | \de{ 55 | Bewegen sie das BUMM nicht, solange nicht alle Modul-Statusanzeigen grün oder aus sind! 56 | Bewegungen mit aktivierten Modulen kann zur sofortigen Aktivierung des Effekts führen! 57 | } 58 | \chde{ 59 | Bewegen Sie es nicht BUMM bis die Statusanzeige für alle Module ein- oder ausgeschaltet ist grün! 60 | Effect Aktion Aktivierungsmodul kann zur sofortigen Aktivierung führen! 61 | } 62 | 63 | \paragraph{} 64 | \en{ 65 | During setup, BUMM can be configured with different countdown settings and a manipulation detector (MD). 66 | The MD submodule can activate the connected BUMM effector if an unauthorized manipulation of the modules is detected. 67 | It can be configured with different sensitivities: 68 | Low sensitivities will only trigger the manipulation alert if multiple mistakes are made during defusal. 69 | As long as at least one of the MD health indicators (above the countdown timer) is lit, the MD is in safe mode. 70 | If all indicator LEDs are off, any mishandling of the modules will immediately activate the MD alert and the connected effector. 71 | } 72 | \de{ 73 | Während der Konfiguration können verschiedene Countdown-Zeiten sowie ein Manipulationsdetektor (MD) eingestellt werden. 74 | Der MD kann den angeschlossenen Effekt aktivieren, wenn eine unauthorisierte Manipulation der Module erkannt wird. 75 | Der MD kann verschieden empfindlich eingestellt werden: 76 | Mit niedriger Empfindlichkeit wird der MD erst aktiviert, wenn mehrere Fehler während der Entschärfung passieren. 77 | Solange mindestens eine der MD-Sicherheitsleuchten (oberhalb des Countdowns) leuchtet, ist der MD sicher. 78 | Erst wenn alle Leuchten aus sind, führt die nächste Fehlbedienung eines Moduls zu einer Aktivierung des MD und des Effektors. 79 | } 80 | \chde{ 81 | Obwohl die Konfiguration verschiedener Countdown-Zeit und Manipulationserkennung (MD) eingestellt werden. 82 | Wenn das Detektionsmodul MD unbefugte Manipulation der Effekt aktivieren kann angeschlossen ist. 83 | MD können unterschiedliche Empfindlichkeit eingestellt: 84 | Mit geringer Empfindlichkeit MD nur, wenn treten mehrere Fehler gleichzeitig aktiviert entschärfen. 85 | Solange die MD- Sicherheitslampe (Countdown oder mehr) mindestens eine leuchtet, ist MD sicher. 86 | Erst wenn alle Lichter aus sind, ein Modul in einer falschen Bedienung Aktivierung führt MD und Effekte. 87 | } 88 | 89 | \en{ 90 | For service requests, please have your serial number ready. 91 | You can find the serial number of your BUMM visible on the outside of the BUMM. 92 | The serial number has four decimal digits followed by a single uppercase letter (e.g. 1234A). 93 | } 94 | \de{ 95 | Wenn Sie Fragen an unseren Kundendienst haben, halten Sie bitte die Seriennummer des Gerätes griffbereit. 96 | Sie finden diese auf der Außenseite des BUMM. 97 | Die Seriennummer hat vier Nummernstellen, gefolgt von einem einzelnen Großbuchstaben (z.B. 1234A). 98 | } 99 | \chde{ 100 | Wenn Sie unseren Kundendienst sind Fragen haben, wenden Sie sich bitte die Seriennummer des Gerätes haben. 101 | Sie können diese auf der Außenseite BUMM finden. 102 | Es gibt vierstellige Seriennummer der Lage, mit einem Großbuchstaben gefolgt (zB 1234A). 103 | } 104 | 105 | \end{document} 106 | -------------------------------------------------------------------------------- /modules/common/manual_preamble.tex: -------------------------------------------------------------------------------- 1 | \input{../common/common_preamble} 2 | 3 | \rhead{ {\moduleTitle} module (Rev. \moduleRevision)} 4 | -------------------------------------------------------------------------------- /modules/d/Makefile: -------------------------------------------------------------------------------- 1 | export USER_LIB_PATH=../libraries 2 | 3 | export ISP_PROG=avrisp2 4 | export ISP_PORT=usb 5 | 6 | #include /usr/share/arduino/Arduino.mk 7 | 8 | all: d_autogen.ino 9 | @$(MAKE) -f ../default_makefile all 10 | 11 | %: force 12 | @$(MAKE) -f ../default_makefile $@ 13 | 14 | force: ; 15 | 16 | .PHONY: panel manual 17 | 18 | 19 | lookuptable_autogen.dat: config.py create_lookuptable.py 20 | python3 create_lookuptable.py 21 | 22 | manual_tables_autogen.tex: config.py lookuptable_autogen.dat create_manual_table.py 23 | python3 create_manual_table.py 24 | 25 | manual_description_autogen.tex: config.py create_manual_description.py 26 | python3 create_manual_description.py 27 | 28 | d_autogen.ino: config.py lookuptable_autogen.dat create_cppfile.py d.ino.in 29 | python3 create_cppfile.py 30 | 31 | panel: panel_autogen.svg 32 | 33 | 34 | panel_autogen.svg: create_panel.py 35 | python3 create_panel.py 36 | 37 | layout_autogen.tex: ../common/layout_generation/layout.py create_layout.py 38 | python3 create_layout.py 39 | 40 | manual: manual_en.pdf manual_de.pdf manual_chde.pdf 41 | 42 | manual_%.pdf: manual.tex manual_description_autogen.tex manual_tables_autogen.tex layout_autogen.tex 43 | pdflatex -jobname manual_$* "\input{../common/$*.tex}\input{manual.tex}" 44 | pdflatex -jobname manual_$* "\input{../common/$*.tex}\input{manual.tex}" 45 | -------------------------------------------------------------------------------- /modules/d/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | definitions: 3 | 0: green (false) 4 | 1: red (true) 5 | """ 6 | 7 | parameters = [ 8 | { 9 | "random_seed": 0, 10 | "difficulty_interval": (10, 20), 11 | "blink_speed": 1000, # milliseconds 12 | "num_blinks": 3, 13 | "num_rounds": 2, 14 | "show_ordered_in_manual": True, 15 | "button_modes": "GR" 16 | }, 17 | { 18 | "random_seed": 0, 19 | "difficulty_interval": (20, 30), 20 | "blink_speed": 1000, # milliseconds 21 | "num_blinks": 5, 22 | "num_rounds": 2, 23 | "show_ordered_in_manual": True, 24 | "button_modes": "GREV" 25 | }, 26 | { 27 | "random_seed": 0, 28 | "difficulty_interval": (30, 50), 29 | "blink_speed": 500, # milliseconds 30 | "num_blinks": 6, 31 | "num_rounds": 3, 32 | "show_ordered_in_manual": True, 33 | "button_modes": "GREV" 34 | }, 35 | { 36 | "random_seed": 0, 37 | "difficulty_interval": (50, 80), 38 | "blink_speed": 500, # milliseconds 39 | "num_blinks": 7, 40 | "num_rounds": 5, 41 | "show_ordered_in_manual": True, 42 | "button_modes": "GREV" 43 | }, 44 | { 45 | "random_seed": 0, 46 | "difficulty_interval": (80, 100), 47 | "blink_speed": 300, # milliseconds 48 | "num_blinks": 8, 49 | "num_rounds": 5, 50 | "show_ordered_in_manual": False, 51 | "button_modes": "GREV" 52 | } 53 | ] 54 | 55 | V_mode_letters = "AEIOU" 56 | 57 | button_modes = { 58 | "G": { 59 | "description": "\en{press the green button}\de{den grünen Knopf drücken}\chde{drücken grün}", 60 | "solver": "False", 61 | "code": "false" 62 | }, 63 | "R": { 64 | "description": "\en{press the red button}\de{den roten Knopf drücken}\chde{drücken rot}", 65 | "solver": "True", 66 | "code": "true" 67 | }, 68 | "E": { 69 | "description": "\en{if the module modifier number is even, press the red button, else press the green button}\de{wenn die Modulseriennummer gerade ist, den roten Knopf drücken, sonst den grünen}\chde{Wenn das Modul Seriennummer gerade nur auf den roten Knopf drücken, sonst grün}", 70 | "solver": "(int(serial[3]) % 2 == 0)", 71 | "code": """ !((bs.serialNumber[3]-'0') & 1) """ 72 | }, 73 | "V": { 74 | "description": "\en{{if the serial number ends with a {0}, press the red button, else press the green button}}\de{{wenn die Seriennummer mit {0} endet, den roten Knopf drücken, sonst den grünen}}\chde{{Wenn nach dem Ende der {0}, den roten Knopf drücken, oder die Seriennummer des grünen}}".format(", ".join(V_mode_letters[:-1]) + " or " + V_mode_letters[-1]), 75 | "solver": "serial[4] in ['{}']".format("','".join(V_mode_letters)), 76 | "code": " ( " + " || ".join(" (bs.serialNumber[4] == '{}') ".format(letter) for letter in V_mode_letters) + " ) " 77 | } 78 | # ,"O": { 79 | # "description": "if this is the first, third, fifth, ... command, press the red button, else press the green button", 80 | # "solver": "command_number % 2 == 0", # (we start with 0, normal people with 1) 81 | # "code": 82 | # } 83 | } 84 | -------------------------------------------------------------------------------- /modules/d/create_cppfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import pickle 3 | import config 4 | 5 | with open("lookuptable_autogen.dat", "rb") as f: 6 | lookuptable = pickle.load(f) 7 | 8 | with open("d.ino.in", "r") as f: 9 | template = f.read() 10 | 11 | max_rounds = max(p["num_rounds"] for p in config.parameters) 12 | 13 | #num_tables = len(config.parameters) 14 | 15 | #masks = ", ".join(str(2**p["num_blinks"]-1) for p in config.parameters) 16 | 17 | lookuptables = "\n".join("const uint8_t PROGMEM lookuptable_{}[] = {{ '{}' }};".format(i, "', '".join(table)) for i, table in enumerate(lookuptable)) 18 | 19 | init_patterns = "\n".join(""" 20 | if( ( {} <= difficulty ) && (difficulty < {}) ) 21 | {{ 22 | current_pattern_length = {}; 23 | num_iterations = {}; 24 | ICR1 = {}; 25 | OCR1A = {}; 26 | }} 27 | """.format(p["difficulty_interval"][0], p["difficulty_interval"][1], p["num_blinks"], p["num_rounds"], 16*p["blink_speed"], int(16*p["blink_speed"]*0.2)) for i, p in enumerate(config.parameters)) 28 | 29 | difficulty_chooser = "\n".join(""" 30 | if( ( {} <= difficulty ) && (difficulty < {}) ) 31 | {{ 32 | return pgm_read_byte(lookuptable_{}+number); 33 | }} 34 | """.format(p["difficulty_interval"][0], p["difficulty_interval"][1], i) for i, p in enumerate(config.parameters)) 35 | 36 | check_commands = "\n".join(""" 37 | case '{}': 38 | {{ 39 | return {}; 40 | }}break; 41 | """.format(key, value["code"]) for key, value in config.button_modes.items()) 42 | 43 | result = template % (max_rounds, lookuptables, init_patterns, difficulty_chooser, check_commands) 44 | 45 | with open("d_autogen.ino", "w") as f: 46 | f.write("// This file has been generated automatically! Do not modify!\n") 47 | f.write(result) 48 | -------------------------------------------------------------------------------- /modules/d/create_layout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import math 3 | import sys 4 | sys.path.append("../../parts") 5 | sys.path.append("../common/layout_generation") 6 | import top_plate 7 | import layout 8 | 9 | with open("layout_autogen.tex","w") as f: 10 | f.write(layout.make_plate()) 11 | 12 | objects = ( 13 | ("\en{green control led}\de{grüne Nachrichtenleuchte}\chde{leuchten grün}", top_plate.plate_width*1/4, top_plate.plate_height*1/3, top_plate.plate_height*4/9.0, "led_off"), 14 | ("\en{red control led}\de{rote Nachrichtenleuchte}\chde{leuchten rot}", top_plate.plate_width*3/4, top_plate.plate_height*1/3, top_plate.plate_height*3/9.0, "led_off"), 15 | ("\en{green input button}\de{grüner Eingabeknopf}\chde{grün Knopf}", top_plate.plate_width*1/4, top_plate.plate_height*2/3, top_plate.plate_height*5/9.0, "led_off"), 16 | ("\en{red input button}\de{roter Eingabeknopf}\chde{rot Knopf}", top_plate.plate_width*3/4, top_plate.plate_height*2/3, top_plate.plate_height*6/9.0, "led_off"), 17 | ) 18 | 19 | for label, x, y, label_y, clipart in objects: 20 | f.write(layout.make_clipart((x, y), clipart)) 21 | f.write(layout.make_label((x, y), label, label_y=label_y)) 22 | 23 | -------------------------------------------------------------------------------- /modules/d/create_lookuptable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import random 3 | import pickle 4 | import config 5 | 6 | result = [] 7 | for p in config.parameters: 8 | random.seed(p["random_seed"]) 9 | count = 2 ** p["num_blinks"] 10 | result.append([random.choice(p["button_modes"]) for i in range(count)]) 11 | 12 | with open("lookuptable_autogen.dat", "wb") as f: 13 | pickle.dump(result, f) 14 | -------------------------------------------------------------------------------- /modules/d/create_manual_description.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import config 3 | 4 | with open("manual_description_autogen.tex", "w") as f: 5 | for command, mode in config.button_modes.items(): 6 | f.write(r"{} & {} \\".format(command, mode["description"])+"\n") 7 | -------------------------------------------------------------------------------- /modules/d/create_manual_table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import pickle 3 | import random 4 | import config 5 | 6 | random.seed(0) 7 | 8 | with open("lookuptable_autogen.dat", "rb") as f: 9 | lookuptable = pickle.load(f) 10 | 11 | 12 | table_columns = 8 13 | 14 | with open("manual_tables_autogen.tex", "w") as f: 15 | for i_set, params in enumerate(config.parameters): 16 | f.write(r"\begin{table}\begin{center}\begin{tabular}{|cccccccc|}\hline"+"\n") 17 | strings = [] 18 | for i, result in enumerate(lookuptable[i_set]): 19 | blink_string = "".join("r" if i & (1<\n""" 13 | svg_start = """\n\n""".format( 14 | width=top_plate.plate_width, height=top_plate.plate_width, id="panel", units="mm") 15 | svg_end = "\n" 16 | text = """{}\n""" 17 | 18 | f.write(svg_start) 19 | 20 | f.write(top_plate.make_top_plate("lt rt lb rb")) 21 | 22 | f.write(circle.format(top_plate.plate_width/2, -top_plate.hole_distance, laser_config.radius_led5, style=cut)) 23 | 24 | available_width = (top_plate.plate_width + 2*top_plate.hole_distance) 25 | distance_x = available_width / 2.0 26 | distance_y = available_width / 3.0 27 | 28 | f.write(circle.format( (top_plate.plate_width+distance_x)/2 , (top_plate.plate_height-distance_y)/2, laser_config.radius_led5, style=cut)) 29 | f.write(circle.format( (top_plate.plate_width-distance_x)/2 , (top_plate.plate_height-distance_y)/2, laser_config.radius_led5, style=cut)) 30 | 31 | f.write(circle.format( (top_plate.plate_width-distance_x)/2 , (top_plate.plate_height+distance_y)/2, laser_config.radius_button_small, style=cut)) 32 | f.write(circle.format( (top_plate.plate_width+distance_x)/2 , (top_plate.plate_height+distance_y)/2, laser_config.radius_button_small, style=cut)) 33 | 34 | f.write(svg_end) 35 | 36 | -------------------------------------------------------------------------------- /modules/d/d.ino.in: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "pin_definitions.h" 4 | #include "debounce.h" 5 | 6 | #define PAUSE_LENGTH 2 7 | #define MAX_ROUNDS %s 8 | 9 | #define REVISION 0 10 | 11 | BUMMSlave bs('d', REVISION, MAX_ROUNDS, PIN_STATUS_RED, PIN_STATUS_GREEN); 12 | 13 | %s 14 | 15 | volatile uint8_t current_iteration = 0; 16 | volatile uint8_t current_pattern; 17 | volatile uint8_t current_pattern_length; 18 | volatile uint8_t current_pattern_i; 19 | volatile uint8_t num_iterations; 20 | 21 | void onModuleInit() 22 | { 23 | uint8_t difficulty = 10*(bs.serialNumber[0]-'0') + (bs.serialNumber[1]-'0'); 24 | current_iteration = 0; 25 | current_pattern = bs.randomSeeds[0]; 26 | current_pattern_i = 0; 27 | 28 | %s 29 | } 30 | 31 | void setup() 32 | { 33 | // clear timer on ICR1 34 | TCCR1A &= ~(0x03); 35 | TCCR1B |= (1< current_pattern_length + PAUSE_LENGTH) 84 | current_pattern_i = 0; 85 | if(current_pattern_i < current_pattern_length) 86 | { 87 | uint8_t pattern_mask = (1<<(current_pattern_length-1-current_pattern_i)); 88 | if(current_pattern & pattern_mask) 89 | PIN_LED_RED_PORT |= (1<= num_iterations) 100 | return true; 101 | current_pattern = bs.randomSeeds[current_iteration]; 102 | return false; 103 | } 104 | uint8_t get_lookuptable_from_serial(uint8_t number) 105 | { 106 | uint8_t difficulty = 10*(bs.serialNumber[0]-'0') + (bs.serialNumber[1]-'0'); 107 | %s 108 | } 109 | 110 | char get_command_code() 111 | { 112 | uint8_t mask = (1< poweroff; 10 | poweroff -> "wait for 'a'" [ label = "setup" ]; 11 | "wait for 'a'" -> disabled [label= "module exists? (master)"]; 12 | disabled -> initialized [label="moduleInit (master)"]; 13 | initialized -> armed [label="gameStart (master)"]; 14 | armed -> disarmed [label="disarm (module logic)"]; 15 | armed -> disabled [label="gameEnd (master)"]; 16 | disarmed -> disabled [label="gameEnd (master)"]; 17 | disarmed -> armed [label="rearm (module logic)"]; 18 | } 19 | -------------------------------------------------------------------------------- /modules/libraries/BUMMSlave/BUMMSlave.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define BROADCAST_ID '_' 4 | #define MESSAGE_DELIMITER '\n' 5 | #define RESPONSE_ID '=' 6 | 7 | #define TARGET_BYTE 0 8 | #define COMMAND_BYTE 1 9 | #define PARAMETER_START 2 10 | #define EXPECT_LENGTH(count) if(_BytesReceived != (2*(count)+PARAMETER_START) ) {setErrorStatus(ERROR_MESSAGE_LENGTH, count); return;} 11 | 12 | #define CMD_MODULE_EXISTS 'a' 13 | #define CMD_MODULE_INIT 'b' 14 | #define CMD_GAME_START 'c' 15 | #define CMD_STATUS_POLL 'd' 16 | #define CMD_STATUS_BROADCAST 'e' 17 | #define CMD_GAME_END 'f' 18 | 19 | // error codes 20 | #define RETURNCODE_ARMED 0x00 21 | #define RETURNCODE_DEFUSED 0x01 22 | #define ERROR_MESSAGE_LENGTH 0x02 23 | #define ERROR_INITIALISED_WHEN_IN_WRONG_STATE 0x03 24 | #define ERROR_GAME_START_WHEN_IN_WRONG_STATE 0x04 25 | #define ERROR_STATUS_POLL_WHEN_IN_WRONG_STATE 0x05 26 | #define ERROR_DISARM_WHEN_IN_WRONG_STATE 0x06 27 | #define ERROR_REARM_WHEN_IN_WRONG_STATE 0x07 28 | #define ERROR_BAD_COMMAND_CODE 0x08 29 | 30 | void onModuleInit(); 31 | void onGameStart(); 32 | void onGameStatusUpdate(); 33 | void onGameEnd(uint8_t status); 34 | 35 | void setSerialOutputEnabled() 36 | { 37 | UCSR0B |= (1<= '0') && (hex <= '9')) 50 | { 51 | ret = hex - '0'; 52 | } 53 | else if((hex >= 'a') && (hex <= 'f')) 54 | { 55 | ret = hex - 'a' + 10; 56 | } 57 | else if((hex >= 'A') && (hex <= 'F')) 58 | { 59 | ret = hex - 'A' + 10; 60 | } 61 | return ret; 62 | } 63 | 64 | void sendHexByte(uint8_t value) 65 | { 66 | if(value<0x10) 67 | Serial.print("0"); 68 | Serial.print(value, HEX); 69 | } 70 | 71 | 72 | BUMMSlave::BUMMSlave(char moduleID, char revisionNumber, uint8_t numRandomSeeds, uint8_t digitalPin_LEDRed, uint8_t digitalPin_LEDGreen) 73 | { 74 | _moduleID = moduleID; 75 | _revisionNumber = revisionNumber; 76 | _numRandomSeeds = numRandomSeeds; 77 | // TODO initialise randomSeeds 78 | _digitalPin_LEDRed = digitalPin_LEDRed; 79 | _digitalPin_LEDGreen = digitalPin_LEDGreen; 80 | _BytesReceived = 0; 81 | _errorCode = 0x00; 82 | 83 | _moduleStatus = MODULE_STATUS_DISABLED; 84 | pinMode(_digitalPin_LEDRed, OUTPUT); 85 | pinMode(_digitalPin_LEDGreen, OUTPUT); 86 | 87 | setLEDs(); 88 | } 89 | void BUMMSlave::begin() 90 | { 91 | Serial.begin(19200); 92 | UCSR0B &= ~(1< 5 | 6 | #define MODULE_STATUS_DISABLED 0 7 | #define MODULE_STATUS_INITIALIZED 1 8 | #define MODULE_STATUS_ARMED 2 9 | #define MODULE_STATUS_DEFUSED 3 10 | #define MODULE_STATUS_ERROR 4 11 | 12 | #define SERIAL_NUMBER_LENGTH 5 13 | #define RANDOM_NUMBER_MAX_SIZE 16 14 | 15 | #define GAME_END_DEFUSED 0 16 | #define GAME_END_TIME 1 17 | #define GAME_END_FAILURES 2 18 | #define GAME_END_ABORT 3 19 | 20 | void setSerialOutputDisabled(); 21 | 22 | class BUMMSlave 23 | { 24 | private: 25 | // Allmost any character is allowed. 26 | // Folowing character is reserved -> _,\n,= 27 | // Don't use!!! 28 | char _moduleID; 29 | char _revisionNumber; 30 | uint8_t _numRandomSeeds; 31 | uint8_t _digitalPin_LEDRed; 32 | uint8_t _digitalPin_LEDGreen; 33 | 34 | uint8_t _moduleStatus; 35 | uint8_t _errorCode; 36 | uint8_t _errorCodeMore; 37 | uint8_t _failCount; 38 | 39 | uint8_t _receiveBuffer[32]; 40 | uint8_t _BytesReceived; 41 | 42 | void setLEDs(); 43 | void setErrorStatus(uint8_t errorCode, uint8_t errorCodeMore=0); 44 | 45 | // bus parsing functions 46 | void receive(); 47 | void parseMessage(); 48 | uint8_t getBufferByte(uint8_t number); 49 | uint16_t getTwoBufferBytes(uint8_t number_of_first_byte); 50 | void parseModuleExists(); 51 | void parseModuleInit(); 52 | void parseGameStart(); 53 | void parseStatusPoll(); 54 | void parseStatusBroadcast(); 55 | void parseGameEnd(); 56 | public: 57 | uint8_t randomSeeds[RANDOM_NUMBER_MAX_SIZE]; 58 | uint16_t currentCountDown; 59 | uint8_t globalLifeCount; 60 | char serialNumber[SERIAL_NUMBER_LENGTH]; 61 | 62 | // constructor 63 | BUMMSlave(char moduleID, char revisionNumber, uint8_t numRandomSeeds, uint8_t digitalPin_LEDRed, uint8_t digitalPin_LEDGreen); 64 | void begin(); 65 | 66 | // ------------------------------------ 67 | // module state properties 68 | 69 | /// setters module arm state 70 | void rearm(); // call this method to rearm a this module during game 71 | void disarm(); // call this method to diasarm the module, for example if the user has solved the module riddle. 72 | void disarmFailed(); // call this method to log a failure. The failure counter will increased. 73 | 74 | 75 | // ------------------------------------ 76 | // BUMM global settings getters 77 | bool isModuleEnabled(); 78 | bool isArmed(); 79 | bool isDisarmed(); 80 | 81 | void loop(); 82 | }; 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /modules/libraries/BUMMSlave/examples/BUMMExample/BUMMExample.pde: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | BUMMSlave bs('b','a',2,1,2,3); 4 | 5 | 6 | void setup() 7 | { 8 | } 9 | 10 | void loop() 11 | { 12 | bs.loop(); 13 | } 14 | 15 | void onModuleInit() 16 | { 17 | // this function are called if the gamemaster do a init call 18 | } 19 | 20 | void onGameStart() 21 | { 22 | // 23 | } 24 | 25 | void onGameStatusUpdate() 26 | { 27 | 28 | } 29 | 30 | void onGameEnd() 31 | { 32 | 33 | } -------------------------------------------------------------------------------- /modules/libraries/BUMMSlave/keywords.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackffm/BrickUsingMultipleModules/5bb6fb3457101d1a45464fa8fd972393179a839a/modules/libraries/BUMMSlave/keywords.txt -------------------------------------------------------------------------------- /modules/libraries/debounce/debounce.cpp: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include "debounce.h" 3 | 4 | #define NUM_PINS 13 5 | 6 | uint8_t buffer[NUM_PINS] = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; // 0b01 at end 7 | uint8_t last_state[NUM_PINS] = {UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE, UNSURE}; 8 | 9 | uint8_t get_debounced_state(uint8_t pinNumber) 10 | { 11 | if(pinNumber >= NUM_PINS) 12 | return UNSURE; 13 | buffer[pinNumber] <<= 1; 14 | buffer[pinNumber] |= digitalRead(pinNumber); 15 | 16 | if(buffer[pinNumber] == 0x00) 17 | return LOW; 18 | if(buffer[pinNumber] == 0xFF) 19 | return HIGH; 20 | return UNSURE; 21 | } 22 | 23 | uint8_t get_debounced_flank(uint8_t pinNumber) 24 | { 25 | uint8_t current_state = get_debounced_state(pinNumber); 26 | if(current_state == UNSURE) 27 | return UNSURE; 28 | if(current_state != last_state[pinNumber]) 29 | { 30 | last_state[pinNumber] = current_state; 31 | return current_state; 32 | } 33 | return UNSURE; 34 | } 35 | -------------------------------------------------------------------------------- /modules/libraries/debounce/debounce.h: -------------------------------------------------------------------------------- 1 | #define UNSURE 0x2 2 | uint8_t get_debounced_state(uint8_t pinNumber); 3 | uint8_t get_debounced_flank(uint8_t pinNumber); 4 | -------------------------------------------------------------------------------- /modules/mastercontrol/Makefile: -------------------------------------------------------------------------------- 1 | export USER_LIB_PATH=../libraries 2 | #export AVRDUDE_ARD_BAUDRATE=14400 3 | #export AVRDUDE_ARD_BAUDRATE=19200 4 | #export AVRDUDE_ARD_BAUDRATE=9600 5 | #export AVRDUDE_ARD_BAUDRATE=28800 6 | #export AVRDUDE_ARD_BAUDRATE=38400 7 | export AVRDUDE_ARD_BAUDRATE=57600 8 | #export AVRDUDE_ARD_BAUDRATE=115200 9 | #export AVRDUDE_ARD_PROGRAMMER=avrisp2 10 | export ISP_PROG=avrisp2 11 | export ISP_PORT = usb 12 | 13 | all: mastercontrol.ino 14 | @$(MAKE) -f ../default_makefile all 15 | 16 | %: force 17 | @$(MAKE) -f ../default_makefile $@ 18 | 19 | force: ; 20 | 21 | .PHONY: panel 22 | 23 | panel: panel_autogen.svg 24 | 25 | panel_autogen.svg: create_panel.py 26 | python3 create_panel.py 27 | 28 | -------------------------------------------------------------------------------- /modules/mastercontrol/create_panel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | sys.path.append("../../parts") 4 | import top_plate 5 | import laser_config 6 | 7 | # generate panel svg 8 | with open("panel_autogen.svg", "w") as f: 9 | cut = laser_config.styles["cut"] 10 | engrave = laser_config.styles["engrave"] 11 | 12 | circle = """\n""" 13 | rect = """\n""" 14 | svg_start = """\n\n""".format( 15 | width=top_plate.plate_width, height=top_plate.plate_width, id="panel", units="mm") 16 | svg_end = "\n" 17 | text = """{}\n""" 18 | 19 | f.write(svg_start) 20 | 21 | f.write(top_plate.make_top_plate("lt rt lb rb")) 22 | 23 | #f.write(circle.format(top_plate.plate_width/2, -top_plate.hole_distance, laser_config.radius_led5, style=cut)) 24 | 25 | available_width = (top_plate.plate_width + 2*top_plate.hole_distance) 26 | 27 | distance_x = 10.0 28 | life_led_y = 30.0 29 | 30 | timer_width = 50.3 31 | timer_height = 19.0 32 | timer_y_center = top_plate.plate_height/2 33 | 34 | for i in range(-3, 4): 35 | f.write(circle.format( (top_plate.plate_width)/2 + i*distance_x , life_led_y, laser_config.radius_led5, style=cut)) 36 | 37 | f.write(rect.format(h=timer_height, w=timer_width, y=timer_y_center-timer_height/2, x=top_plate.plate_width/2-timer_width/2, style=cut)) 38 | 39 | f.write(svg_end) 40 | 41 | with open("panel_autogen_recttest.svg", "w") as f: 42 | f.write(svg_start) 43 | f.write(rect.format(h=timer_height, w=timer_width, y=timer_y_center-timer_height/2, x=top_plate.plate_width/2-timer_width/2, style=cut)) 44 | f.write(svg_end) 45 | -------------------------------------------------------------------------------- /modules/mastercontrol/mastercontrol.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | BUMMSlave bs('!',0,0,2,3); 7 | Adafruit_7segment display = Adafruit_7segment(); 8 | 9 | //#define PIN_LOCK A3 10 | 11 | const uint8_t LED_MASK_PB = (1<<2) | (1<<3) | (1<<4) | (1<<5); 12 | const uint8_t LED_MASK_PC = (1<<3) - 1; 13 | 14 | void setLifes(uint8_t lifes) 15 | { 16 | PORTB &= ~(LED_MASK_PB); 17 | PORTC &= ~(LED_MASK_PC); 18 | switch(lifes) // fallthrough 19 | { 20 | case 7: PORTB |= (1<<2); 21 | case 6: PORTB |= (1<<3); 22 | case 5: PORTB |= (1<<4); 23 | case 4: PORTB |= (1<<5); 24 | case 3: PORTC |= (1<<0); 25 | case 2: PORTC |= (1<<1); 26 | case 1: PORTC |= (1<<2); 27 | case 0: ; 28 | default: ; 29 | } 30 | } 31 | 32 | void setup() 33 | { 34 | //pinMode(PIN_LOCK, INPUT_PULLUP); 35 | DDRB |= LED_MASK_PB; 36 | DDRC |= LED_MASK_PC; 37 | 38 | display.begin(0x70); 39 | display.printError(); // "----" 40 | display.writeDisplay(); 41 | bs.begin(); 42 | } 43 | void loop() 44 | { 45 | bs.loop(); 46 | //if( digitalRead(PIN_LOCK) == LOW ) 47 | // bs.rearm(); 48 | } 49 | 50 | void onModuleInit() 51 | { 52 | display.printError(); // "----" 53 | display.writeDisplay(); 54 | } 55 | 56 | void onGameStart() 57 | { 58 | bs.disarm(); 59 | } 60 | 61 | void onGameStatusUpdate() 62 | { 63 | uint16_t total_seconds = bs.currentCountDown; 64 | uint8_t minutes = total_seconds / 60; 65 | uint8_t seconds = total_seconds % 60; 66 | 67 | // leading zeroes 68 | display.writeDigitNum(0, minutes / 10); 69 | display.writeDigitNum(1, minutes % 10); 70 | display.writeDigitNum(3, seconds / 10); 71 | display.writeDigitNum(4, seconds % 10); 72 | display.drawColon(total_seconds&0x01); 73 | display.writeDisplay(); 74 | 75 | setLifes(bs.globalLifeCount); 76 | } 77 | 78 | void onGameEnd(uint8_t status) 79 | { 80 | switch(status) 81 | { 82 | case GAME_END_DEFUSED: 83 | // do nothing 84 | break; 85 | case GAME_END_TIME: 86 | for(uint8_t i=0; i<10;i++) 87 | { 88 | display.writeDigitNum(0, 0); 89 | display.writeDigitNum(1, 0); 90 | display.writeDigitNum(3, 0); 91 | display.writeDigitNum(4, 0); 92 | display.writeDisplay(); 93 | _delay_ms(500); 94 | 95 | display.printError(); 96 | display.writeDisplay(); 97 | _delay_ms(500); 98 | } 99 | break; 100 | case GAME_END_FAILURES: 101 | for(uint8_t i=0; i<10;i++) 102 | { 103 | setLifes(0); 104 | _delay_ms(500); 105 | setLifes(7); 106 | _delay_ms(500); 107 | } 108 | break; 109 | case GAME_END_ABORT: 110 | display.printError(); 111 | display.writeDisplay(); 112 | break; 113 | } 114 | // if(status != 0) 115 | // { 116 | // display.printError(); 117 | // display.writeDisplay(); 118 | // } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /modules/serial_display/Makefile: -------------------------------------------------------------------------------- 1 | export USER_LIB_PATH=../libraries 2 | #export AVRDUDE_ARD_BAUDRATE=14400 3 | #export AVRDUDE_ARD_BAUDRATE=19200 4 | #export AVRDUDE_ARD_BAUDRATE=9600 5 | #export AVRDUDE_ARD_BAUDRATE=28800 6 | #export AVRDUDE_ARD_BAUDRATE=38400 7 | export AVRDUDE_ARD_BAUDRATE=57600 8 | #export AVRDUDE_ARD_BAUDRATE=115200 9 | #export AVRDUDE_ARD_PROGRAMMER=avrisp2 10 | export ISP_PROG=avrisp2 11 | export ISP_PORT = usb 12 | 13 | all: serial_display.ino 14 | @$(MAKE) -f ../default_makefile all 15 | 16 | %: force 17 | @$(MAKE) -f ../default_makefile $@ 18 | 19 | force: ; 20 | 21 | panel_autogen.svg: create_panel.py ../../parts/laser_config.py ../../parts/top_plate.py 22 | python3 create_panel.py 23 | -------------------------------------------------------------------------------- /modules/serial_display/create_panel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | sys.path.append("../../parts") 4 | import top_plate 5 | import laser_config 6 | 7 | # generate panel svg 8 | with open("panel_autogen.svg", "w") as f: 9 | cut = laser_config.styles["cut"] 10 | engrave = laser_config.styles["engrave"] 11 | 12 | svg_start = """\n\n""".format( 13 | width=top_plate.plate_width, height=top_plate.plate_width, id="panel", units="mm") 14 | svg_end = "\n" 15 | rect = """\n""" 16 | text = """{}\n""" 17 | f.write(svg_start) 18 | 19 | display_width = 72.0 20 | display_height = 32.0 21 | display_y_center = top_plate.plate_width/2 22 | 23 | f.write(top_plate.make_top_plate("lt rt lb rb")) 24 | f.write(rect.format(h=display_height, w=display_width, y=display_y_center-display_height/2, x=top_plate.plate_width/2-display_width/2, style=cut)) 25 | 26 | f.write(svg_end) 27 | 28 | -------------------------------------------------------------------------------- /modules/serial_display/serial_display.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | LiquidCrystal lcd(12, 11, 5, 4, 3, 2); 6 | #define MESSAGE_DELIMITER '\n' 7 | #define PARAMETER_START 2 8 | #define COMMAND_BYTE 1 9 | #define CMD_MODULE_INIT 'b' 10 | #define SERIAL_NUMBER_LENGTH 5 11 | uint8_t getNibbleFromHex(const char hex) 12 | { 13 | uint8_t ret = 0; 14 | if((hex >= '0') && (hex <= '9')) 15 | { 16 | ret = hex - '0'; 17 | } 18 | else if((hex >= 'a') && (hex <= 'f')) 19 | { 20 | ret = hex - 'a' + 10; 21 | } 22 | else if((hex >= 'A') && (hex <= 'F')) 23 | { 24 | ret = hex - 'A' + 10; 25 | } 26 | return ret; 27 | } 28 | 29 | uint8_t getBufferByte(String buf, uint8_t number) 30 | { 31 | uint8_t ret = 0; 32 | ret = getNibbleFromHex(buf[PARAMETER_START+2*number]); 33 | ret <<= 4; 34 | ret |= getNibbleFromHex(buf[PARAMETER_START+2*number+1]); 35 | return ret; 36 | } 37 | void setup() 38 | { 39 | lcd.begin(16, 2); 40 | Serial.begin(19200); 41 | UCSR0B &= ~(1<\n""".format(styles["cut"], position[0], position[1], radius_m4) 39 | p = plate_padding 40 | c = corner_cut 41 | points = [ 42 | (-p, -p+c), 43 | (-p+c, -p), 44 | (plate_width+p-c, -p), 45 | (plate_width+p, -p+c), 46 | (plate_width+p, plate_height+p-c), 47 | (plate_width+p-c, plate_height+p), 48 | (-p+c, plate_height+p), 49 | (-p, plate_height+p-c), 50 | ] 51 | svg_code += """""".format(styles["cut"], "{} {}".format(*points[0]), " ".join(("{} {}".format(*p) for p in points[1:]))) 52 | return svg_code 53 | 54 | if __name__ == "__main__": 55 | import sys 56 | try: 57 | target = sys.argv[1] 58 | position_flags = sys.argv[2] 59 | except IndexError: 60 | sys.exit('USAGE example: {} target.svg "lt rt"'.format(sys.argv[0])) 61 | 62 | svg_start = """\n\n""".format(width=plate_width, height=plate_height, id="panel", units="mm") 63 | svg_end = "\n" 64 | content = make_top_plate(position_flags) 65 | 66 | with open(target, "w") as f: 67 | f.write(svg_start) 68 | f.write(content) 69 | f.write(svg_end) 70 | -------------------------------------------------------------------------------- /parts/top_plate_with_all_holes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------