├── .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 |
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"
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"
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"
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"
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"
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 |
12 |
--------------------------------------------------------------------------------