Merge branch 'master' into RobbeWeMissYou

This commit is contained in:
redfast00 2020-08-26 20:02:14 +02:00 committed by GitHub
commit 086ae0e3fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 845 additions and 6 deletions

View file

@ -63,3 +63,7 @@ Some things we had to consider:
- payload is 8 bytes per packet
- packets can be delayed or not received on every node, so detection of this and retransmission might be needed: if the bomb interactor solves a module and the packet that communicates this with the bomb does not get delivered to the controller, the bomb will still go off, even if all modules have been solved
- we can't send an infinite amount of packets; the higher our bitrate is, the shorter our wires need to be
## Development setup
We use [this](https://github.com/autowp/arduino-mcp2515/) library for CAN communications. See [this](https://github.com/autowp/arduino-mcp2515/#software-usage) header for 3 simple steps on how to use it in the arduino IDE

View file

@ -0,0 +1,3 @@
[*.py]
indent_style = space
indent_size = 4

138
debugging_tool/.gitignore vendored Normal file
View file

@ -0,0 +1,138 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

View file

@ -0,0 +1,7 @@
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pyserial==3.4
Werkzeug==1.0.1

108
debugging_tool/server.py Normal file
View file

@ -0,0 +1,108 @@
from threading import Thread
from flask import Flask, jsonify, send_file
from time import sleep
from dataclasses import dataclass
from datetime import datetime
import serial
app = Flask(__name__)
shared_message_log = []
@dataclass
class Message:
payload: bytes
received_from: int
received_at: datetime
internal_id: int
def readable_time(self):
return self.received_at.strftime('%H:%M:%S')
def priority_bit(self):
return (self.received_from >> 10) & 0b1
def sender_type(self):
return (self.received_from >> 8) & 0b11
def sender_id(self):
return (self.received_from >> 0) & 0b1111_1111
def human_readable_type(self):
return ['controller', 'puzzle', 'needy', 'RESERVED TYPE'][self.sender_type()]
def _parse_state_update(self):
timeleft = self.payload[1] << 0x18 | self.payload[2] << 0x10 | self.payload[3] << 0x08 | self.payload[4]
strikes = self.payload[5]
max_strikes = self.payload[6]
return f'{timeleft/1000:3.2f} {strikes:02}/{max_strikes:02}'
def parse_message(self):
sender_type = self.sender_type()
message_type = self.payload[0]
if sender_type == 0b00: # controller
if message_type == 0:
return "ACK"
elif message_type == 1:
return "HELLO"
elif message_type == 2:
return "START " + self._parse_state_update()
elif message_type == 3:
return "STATE " + self._parse_state_update()
elif message_type == 4:
return "SOLVED " + self._parse_state_update()
elif message_type == 5:
return "TIMEOUT " + self._parse_state_update()
elif message_type == 6:
return "STRIKEOUT " + self._parse_state_update()
elif sender_type == 0b01: # puzzle
if message_type == 0:
return "REGISTER"
elif message_type == 1:
return f"STRIKE {self.payload[5]}"
elif message_type == 2:
return f"SOLVED"
else:
return f"PARSE ERROR {self.received_from:011b} {self.payload.hex(' ')}"
def serialize(self):
return {
'time': self.readable_time(),
'parsed': self.parse_message(),
'pretty_raw_sender_id': f'{self.priority_bit():01b} {self.sender_type():02b} {self.sender_id():08b}',
'raw_message': f"{self.payload.hex(' ')}",
'human_readable_type': self.human_readable_type(),
'sender_id': self.sender_id(),
'internal_id': self.internal_id
}
def serial_reader(messagelog):
with serial.Serial('/dev/ttyACM0', 115200, timeout=10) as ser:
while True:
line = ser.readline()
print(line.decode('ascii'))
if line.startswith(b"message"):
line = line.decode('ascii')
line = line.strip()
parts = line.split(' ')
sender = int(parts[1])
message = bytes(int(p) for p in parts[2:])
received = Message(message, sender, datetime.now(), len(messagelog))
messagelog.append(received.serialize())
print(len(messagelog))
@app.route('/')
def index():
return send_file('static/index.html')
@app.route('/api.json')
def api():
return jsonify(shared_message_log)
if __name__ == '__main__':
thread = Thread(target=serial_reader, args=(shared_message_log, ))
thread.start()
app.run(debug=True, host='0.0.0.0')

View file

@ -0,0 +1,29 @@
#include <mcp2515.h>
MCP2515 mcp2515(10);
void setup() {
Serial.begin(115200);
mcp2515.reset();
mcp2515.setBitrate(CAN_50KBPS);
mcp2515.setNormalMode();
for (int i = 0; i < 5; i++) {
Serial.println("begin");
delay(1000);
}
}
void loop() {
char message[9];
message[8] = '\0';
struct can_frame receive_frame;
if (mcp2515.readMessage(&receive_frame) == MCP2515::ERROR_OK) {
Serial.print("message ");
Serial.print(receive_frame.can_id, DEC);
for (int i = 0; i < 8; i++) {
Serial.print(" ");
Serial.print(receive_frame.data[i], DEC);
}
Serial.print("\n");
}
}

View file

@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CAN debugger</title>
<style>
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.message {
display: flex;
animation: fadein 1s;
}
.message > * {
margin-top: 0;
margin-bottom: 0;
padding: 0 1.5ch 0 1.5ch;
}
.hide_details .message .pretty_raw_sender_id {
display: none;
}
.hide_details .message .raw_message {
display: none;
}
.human_readable_type {
order: -5;
}
.time {
order: 5;
}
.sender_id {
order: 1;
}
.parsed {
order: 2;
flex: 1;
background: lightgreen;
}
.pretty_raw_sender_id, .raw_message {
font-family: monospace, monospace;
}
.pretty_raw_sender_id {
order: 9998;
}
.raw_message {
order: 9999;
}
</style>
</head>
<body>
<div>
<input type="checkbox" id="show_raw" name="show_raw" checked onchange="updateShow()">
<label for="show_raw">Show raw address and payload</label>
<input type="checkbox" id="pause" name="pause">
<label for="pause">Pause</label>
</div>
<div id="messages">
</div>
<script src="static/script.js"></script>
</body>
</html>

View file

@ -0,0 +1,75 @@
maxseen = 0;
function updateShow() {
if (document.getElementById('show_raw').checked) {
document.getElementById('messages').classList = '';
} else {
document.getElementById('messages').classList = 'hide_details';
}
}
function updateMessages() {
fetch('/api.json')
.then(
function(response) {
if (response.status !== 200) {
console.log('FAIL: ' + response.status);
return;
}
response.json().then(function(data) {
console.log(data);
if (data.length > maxseen) {
var messageContainer = document.getElementById('messages');
for (let i = maxseen; i < data.length; i++) {
var current = data[i];
var time = document.createElement("p");
time.innerHTML = current['time'];
time.className = 'time';
var parsed = document.createElement("p");
parsed.innerHTML = current['parsed'];
parsed.className = 'parsed';
var sender_id = document.createElement("p");
sender_id.innerHTML = current['sender_id'];
sender_id.className = 'sender_id';
var pretty_raw_sender_id = document.createElement("p");
pretty_raw_sender_id.innerHTML = current['pretty_raw_sender_id'];
pretty_raw_sender_id.className = 'pretty_raw_sender_id';
var raw_message = document.createElement("p");
raw_message.innerHTML = current['raw_message'];
raw_message.className = 'raw_message';
var human_readable_type = document.createElement("p");
human_readable_type.innerHTML = current['human_readable_type'];
human_readable_type.className = 'human_readable_type';
var newNode = document.createElement("div");
newNode.className = "message";
newNode.append(time, parsed, sender_id, pretty_raw_sender_id, raw_message, human_readable_type);
messageContainer.prepend(newNode)
}
maxseen = data.length;
}
});
}
)
}
window.onload = function() {
updateShow()
console.log("loaded");
updateMessages();
setInterval(function() {
if (document.getElementById('pause').checked) {
return;
}
updateMessages()
}, 1000);
};

11
docs/hardware/README.md Normal file
View file

@ -0,0 +1,11 @@
# Hardware documentation
![Breadboard with Arduino Nano and CAN module](./basic_module.png)
## Parts of basic module
- 2x 330 ohm resistor
- RGB common cathode LED
- MCP2515 CAN module
- Arduino nano (clone)

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View file

@ -18,7 +18,7 @@ b bb bbbbbbbb
| | ↓
| ↓ module-ID: 2⁸=256
↓ type: 2²=4
priority bit (so that each type can send priority messages if need be)
priority bit (so that each type can send priority messages if need be, 0 = high priority, 1 = low priority)
type:
- 0 module-ID 0: controller, >0: info
@ -30,7 +30,7 @@ Payload:
[ B B B B B B B B ]
-
type (per module type)
type of the message (per module type)
- - - - - - - - - - - - - - - - - - -
@ -79,15 +79,15 @@ Types for info:
Types for modules:
- 0 hanlo
- 0 register
[ X B B B B B B B ]
--------------
reserved
- 1 strike
[ X B B B B B B B ]
--------------
reserved
↓ -----↓------
#strikes reserved
- 2 solved (not for needy modules)
[ X B B B B B B B ]

View file

@ -1,3 +1,23 @@
# Wireguide
This is a parametric model of a tool to put the individual wires of an ethernet-cable in the correct position to easily attach an IDC-connector (vampire tap)
The original plan of connecting the CAN-modules was to use an ethernet cable:
we would strip the outer layer of the cable where we want to connect a module,
then put on an IDC connector. We would then use one twisted pair for CAN data, and
the other pairs for 12v. We wanted to use an ethernetcable because we have a lot of
spare ethernetcable that's too good to throw away, but too long to be practical.
The CAN specifications also demand a twisted cable.
So in theory, this was a great solution. Unfortunately, theory and practice are
the same in theory, but not in pracice: connecting the IDC connectors proved to be
very time-intensive (first stripping the outer cable, then partially untwisting the strands
and putting them in the correct order) and error-prone (the slightest issue in alignment
will cause two strands to become electrically connected).
To partially fix this, we designed a small tool that would be able to more easily keep
the strands lined up before pressing the IDC-connector together. This also proved
to be rather hard, so we gave up and just ordered some automotive twisted wire that is
made for CAN traffic.
The term you'll need when trying to buy this twisted wire is `canbus wire`.
`wireguide.scad` is a parametric model of a tool to put the individual wires of an ethernet-cable in the correct position to easily attach an IDC-connector (vampire tap)

View file

@ -0,0 +1,98 @@
#include <mcp2515.h>
#include "pitches.h"
MCP2515 mcp2515(10);
uint16_t id = 0b01100000011;
// type: 3 (other)
// module-id: 3
int soundpin = 9;
void setup() {
// Serial.begin(9600);
Serial.begin(115200);
mcp2515.reset();
mcp2515.setBitrate(CAN_50KBPS);
mcp2515.setNormalMode();
Serial.println("begin");
// play_sound();
}
void loop() {
char message[9];
message[8] = '\0';
// Send text typed in the console over the can network
struct can_frame send_frame;
int read = Serial.readBytesUntil('\n', send_frame.data, 8);
if (read > 0) {
send_frame.can_id = id;
send_frame.can_dlc = read;
mcp2515.sendMessage(&send_frame);
Serial.println("sent");
}
// Read from the can bus
struct can_frame receive_frame;
if (mcp2515.readMessage(&receive_frame) == MCP2515::ERROR_OK) {
Serial.print("[prio-bit: ");
Serial.print(receive_frame.can_id >> 10 & 0b00000001);
Serial.print("\tmodule-type: ");
Serial.print(receive_frame.can_id >> 8 & 0b00000011);
Serial.print("\tmodule-id: ");
Serial.print(receive_frame.can_id & 0b11111111);
Serial.print("]\tLength: ");
Serial.print(receive_frame.can_dlc, HEX); // print DLC
Serial.print("\t Body: ");
// First byte has the type of the message
// print per byte in hex
for (int i = 0; i < receive_frame.can_dlc; i++) { // print the data
Serial.print(receive_frame.data[i], HEX);
Serial.print(" ");
}
Serial.println("");
// print all
//memcpy(message, receive_frame.data, 8);
//Serial.println(message);
}
}
// notes in the melody:
int melody[] = {
NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4
};
// note durations: 4 = quarter note, 8 = eighth note, etc.:
int noteDurations[] = {
4, 8, 8, 4, 4, 4, 4, 4
};
void play_melody() {
// iterate over the notes of the melody:
for (int thisNote = 0; thisNote < 8; thisNote++) {
// to calculate the note duration, take one second divided by the note type.
//e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
int noteDuration = 1000 / noteDurations[thisNote];
tone(soundpin, melody[thisNote], noteDuration);
// to distinguish the notes, set a minimum time between them.
// the note's duration + 30% seems to work well:
int pauseBetweenNotes = noteDuration * 1.30;
delay(pauseBetweenNotes);
// stop the tone playing:
noTone(soundpin);
}
}
void play_sound() {
}

View file

@ -0,0 +1,93 @@
/*************************************************
* Public Constants
*************************************************/
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978

179
src/module/module.ino Normal file
View file

@ -0,0 +1,179 @@
#include <CAN.h>
#include <ezButton.h>
#define HANDLE_MESSAGE(type) case type: handle_##type(); break;
#define PUZZLE_M_HANLO 0
#define PUZZLE_M_STRIKE 1
#define PUZZLE_M_SOLVED 2
enum gamestate {
PENDING, RUNNING, SOLVED, TIMEOUT, STRIKEOUT
};
enum controller_m_type {
ACK, HELLO, GAME_START, GAME_STATE, GAME_SOLVED, GAME_TIMEOUT, GAME_STRIKEOUT
};
typedef struct state {
gamestate game_state;
uint32_t time;
uint8_t cur_strikes;
uint8_t max_strikes;
} state_s;
state_s state = { .game_state = PENDING, .time = (uint32_t) -1 };
uint16_t id = 0b00100000010;
ezButton green_button(5); // create ezButton object that attach to pin 5;
ezButton red_button(7); // create ezButton object that attach to pin 7;
/* Send Hanlo to controller */
void hanlo() {
Serial.println("-> HANLO");
CAN.beginPacket(id);
CAN.write(PUZZLE_M_HANLO); /* Message Type */
CAN.endPacket();
}
/* Send strike to controller */
void strike() {
Serial.println("-> STRIKE");
CAN.beginPacket(id);
CAN.write(PUZZLE_M_STRIKE); /* Message Type */
CAN.endPacket();
}
/* Send solve to controller */
void solved() {
Serial.println("-> SOLVED");
CAN.beginPacket(id);
CAN.write(PUZZLE_M_SOLVED); /* Message Type */
CAN.endPacket();
}
/* Handle Ack message from controller */
void handle_ACK() {
Serial.println("<- ACK");
}
/* Handle Hello message from controller */
void handle_HELLO() {
Serial.println("<- HELLO");
hanlo();
}
/* Handle Game start message from controller */
void handle_GAME_START() {
Serial.println("<- GAME_START");
state.game_state = RUNNING;
handle_GAME_STATE();
}
/* Handle State message from controller */
void handle_GAME_STATE() {
Serial.println("<- GAME_STATE");
state.time =
((uint32_t) CAN.read() << 24) |
((uint32_t) CAN.read() << 16) |
((uint32_t) CAN.read() << 8) |
CAN.read();
state.cur_strikes = CAN.read();
state.max_strikes = CAN.read();
Serial.print("TIME: ");
Serial.println(state.time);
Serial.print("CUR_STRIKES: ");
Serial.println(state.cur_strikes);
Serial.print("MAX_STRIKES: ");
Serial.println(state.max_strikes);
}
/* Handle Solved message from controller */
void handle_GAME_SOLVED() {
Serial.println("<- GAME_SOLVED");
state.game_state = SOLVED;
}
/* Handle Timeout message from controller */
void handle_GAME_TIMEOUT() {
Serial.println("<- GAME_TIMEOUT");
state.game_state = TIMEOUT;
}
/* Handle Strikeout message from controller */
void handle_GAME_STRIKEOUT() {
Serial.println("<- GAME_STRIKEOUT");
state.game_state = STRIKEOUT;
}
void doCAN() {
if (CAN.parsePacket()) {
Serial.println("--- GOT_CAN ---");
/* only react to messages from the controller */
if (CAN.filter(0b00000000000, 0b01100000000)) {
Serial.println("--- HANDLING ---");
if (CAN.peek() != -1) {
uint8_t message_type = CAN.read();
Serial.print("MESSAGE_TYPE: ");
Serial.println(message_type, HEX);
switch(message_type) {
HANDLE_MESSAGE(ACK)
HANDLE_MESSAGE(HELLO)
HANDLE_MESSAGE(GAME_START)
HANDLE_MESSAGE(GAME_STATE)
HANDLE_MESSAGE(GAME_SOLVED)
HANDLE_MESSAGE(GAME_TIMEOUT)
HANDLE_MESSAGE(GAME_STRIKEOUT)
default:
Serial.println("--- NO_HANDLER_FOUND ---");
}
}
} else {
Serial.println("--- IGNORING ---");
}
}
}
void setup() {
/* Init Serial */
Serial.begin(9600);
Serial.println("--- INITIATING ---");
Serial.print("ID: ");
Serial.println(id, HEX);
/* Init Can pins */
// CAN.setPins(cs,irq);
/* Set SPI Frequency */
// CAN.setSPIFrequency(frequency);
if (!CAN.begin(50E3)) {
Serial.println("--- FAILED_TO_INIT_CAN ---");
while(1) {};
}
red_button.setDebounceTime(50);
green_button.setDebounceTime(50);
Serial.println("--- INITIALIZED ---");
}
void loop() {
red_button.loop();
green_button.loop();
/* Handle CAN messages */
doCAN();
if (green_button.isPressed()) {
solved();
}
if (red_button.isPressed()) {
strike();
}
// TODO
}