commit
a6fc3a0980
@ -0,0 +1,71 @@
|
|||||||
|
int PIN_MEATBAG_IN = 2;
|
||||||
|
int PIN_MEATBAG_ACTIVATE = 3;
|
||||||
|
|
||||||
|
// MSG 0 and 1 are reserved for the low/high signals
|
||||||
|
// sent when meatbags are transcribing.
|
||||||
|
int MSG_MEATBAG_DEACTIVATE = 2;
|
||||||
|
int MSG_MEATBAG_ACTIVATE = 3;
|
||||||
|
// Heartbeats every 0.5, 1.5, 2.5, etc... of cycle time. Heartbeats are when we tell the GUI to draw the rise/fall.
|
||||||
|
// Probes every 1, 2, 3, etc... of cycle time. Probes are when we tell the server what the bit was for that cycle.
|
||||||
|
int MSG_EVENT_HEARTBEAT = 4;
|
||||||
|
int MSG_EVENT_PROBE = 5;
|
||||||
|
|
||||||
|
int MEATBAG_BAUD = 4;
|
||||||
|
int CYCLE_TIMESPAN = 1000 / MEATBAG_BAUD;
|
||||||
|
int EVENT_TIMESPAN = CYCLE_TIMESPAN / 2;
|
||||||
|
|
||||||
|
int isMeatbagTranscribing = 0;
|
||||||
|
unsigned long previousTimestamp = 0;
|
||||||
|
unsigned long previousEvent = MSG_EVENT_PROBE;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(9600);
|
||||||
|
pinMode(PIN_MEATBAG_IN, INPUT);
|
||||||
|
pinMode(PIN_MEATBAG_ACTIVATE, INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It could be difficult to sync the timing of reading the input from the switch and
|
||||||
|
// displaying the signal on a graph. How do we get this loop and the Python/Matplotlib
|
||||||
|
// loop to be as in-sync as possible? And how can we sample in such a way that isn't
|
||||||
|
// vulnerable to slight mistakes?
|
||||||
|
//
|
||||||
|
// I think the key is going to be to probe the switch in the middle of the signal
|
||||||
|
// So, this loop will be the metronome. We'll send a "beat" message over Serial every
|
||||||
|
// 250, 500, 750, 1000 milliseconds (for example). And we'll send a "value" message
|
||||||
|
// every 125, 375, 625, 875 milliseconds.
|
||||||
|
void loop() {
|
||||||
|
unsigned long currentTimestamp = millis();
|
||||||
|
int timeElapsed = currentTimestamp - previousTimestamp;
|
||||||
|
|
||||||
|
// Send the heartbeat every 125, 375, 625, etc...
|
||||||
|
if (timeElapsed > EVENT_TIMESPAN
|
||||||
|
&& previousEvent == MSG_EVENT_PROBE)
|
||||||
|
{
|
||||||
|
previousTimestamp = currentTimestamp;
|
||||||
|
previousEvent = MSG_EVENT_HEARTBEAT;
|
||||||
|
Serial.print(MSG_EVENT_HEARTBEAT);
|
||||||
|
} else
|
||||||
|
|
||||||
|
// Send the signal on every 250, 500, 750, etc...
|
||||||
|
if (timeElapsed > EVENT_TIMESPAN
|
||||||
|
&& previousEvent == MSG_EVENT_HEARTBEAT
|
||||||
|
&& isMeatbagTranscribing == 1
|
||||||
|
&& timeElapsed > EVENT_TIMESPAN)
|
||||||
|
{
|
||||||
|
previousTimestamp = currentTimestamp;
|
||||||
|
previousEvent = MSG_EVENT_PROBE;
|
||||||
|
int value = digitalRead(PIN_MEATBAG_IN);
|
||||||
|
Serial.print(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meatbags have to eat and sleep. We don't want to drop the signal
|
||||||
|
// during their moments of weakness. When meat fails, metal resumes control.
|
||||||
|
int activate = digitalRead(PIN_MEATBAG_ACTIVATE);
|
||||||
|
if (isMeatbagTranscribing == 1 && activate == LOW) {
|
||||||
|
isMeatbagTranscribing = 0;
|
||||||
|
Serial.print(MSG_MEATBAG_DEACTIVATE);
|
||||||
|
} else if (isMeatbagTranscribing == 0 && activate == HIGH) {
|
||||||
|
isMeatbagTranscribing = 1;
|
||||||
|
Serial.print(MSG_MEATBAG_ACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,192 @@
|
|||||||
|
"""Scratchpad for Defcon's Hardware Hacking Village's Rube Goldberg Machine.
|
||||||
|
|
||||||
|
For local testing you can use `socat` to spin up two virtual RS-232 devices.
|
||||||
|
socat -d -d pty,rawer,echo=0,link=/tmp/ttyV0,b9600 pty,rawer,echo=0,link=/tmp/ttyV1,b9600
|
||||||
|
|
||||||
|
The human signal will come from a switch connected to an Arduino's GPIO pin. The
|
||||||
|
Arduino will capture the signal at 4 baud and send the bit over its serial/USB
|
||||||
|
connection to the computer at 9600 baud.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import queue
|
||||||
|
import random
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import serial
|
||||||
|
|
||||||
|
# This in queue will contain bytes from DCE. It's the very start of our chain.
|
||||||
|
dce_inq = queue.Queue()
|
||||||
|
# The out queue will contain bits that we can pull at a very low baud rate and
|
||||||
|
# display to the meat bag.
|
||||||
|
dce_outq = queue.Queue()
|
||||||
|
|
||||||
|
dte_outq = queue.Queue()
|
||||||
|
|
||||||
|
|
||||||
|
class TransformerQueue:
|
||||||
|
"""Transforms values from an input queue and puts the transformation in an output queue.
|
||||||
|
|
||||||
|
`transform` needs to be a function that returns a list, even if you're
|
||||||
|
transforming a single value to a single value.
|
||||||
|
|
||||||
|
This class is useful as both a buffer to throttle the incoming messages
|
||||||
|
and a way to convert bytes to 1's and 0's so that we can display those 1's
|
||||||
|
and 0's on a graph.
|
||||||
|
|
||||||
|
If we want to terminate the manual work that we're doing of transcribing
|
||||||
|
the 1's and 0's, then we still have the backing queue of messages that
|
||||||
|
we can pass along by automated digital means, so we don't lose any
|
||||||
|
messages and thus break the Rube Goldberg machine.
|
||||||
|
"""
|
||||||
|
def __init__(self, q1, q2, transform):
|
||||||
|
self.q1 = q1
|
||||||
|
self.q2 = q2
|
||||||
|
self.transform = transform
|
||||||
|
|
||||||
|
def empty(self):
|
||||||
|
return self.q2.empty() and self.q1.empty()
|
||||||
|
|
||||||
|
def put(self, *args, **kwargs):
|
||||||
|
return self.q1.put(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
if self.empty():
|
||||||
|
raise queue.Empty()
|
||||||
|
if self.q2.empty():
|
||||||
|
for v in self.transform(self.q1.get(*args, **kwargs)):
|
||||||
|
self.q2.put(v)
|
||||||
|
return self.q2.get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def byte_to_bits(byte):
|
||||||
|
"""Convert a byte to a list of 8 1's and 0's."""
|
||||||
|
assert byte <= 255, "Can't send more than 8 bits in a single RS-232 message."
|
||||||
|
out = []
|
||||||
|
for _ in range(8):
|
||||||
|
out.append(byte & 0x01)
|
||||||
|
byte >>= 1
|
||||||
|
return reversed(out)
|
||||||
|
|
||||||
|
|
||||||
|
dce_transformer = TransformerQueue(dce_inq, dce_outq, byte_to_bits)
|
||||||
|
|
||||||
|
|
||||||
|
# This in queue will contain the bits that the meat bag signaled.
|
||||||
|
# We'll probably receive each bit by way of an Arduino talking to us over RS-232
|
||||||
|
# where each RS-232 byte will contain a single human-entered bit.
|
||||||
|
human_signal_inq = queue.Queue() # Bits
|
||||||
|
# This out queue will be the bytes we get when we combine those bits.
|
||||||
|
# (Maybe we could just write the byte directly to serial. But I'm going with
|
||||||
|
# this for now.)
|
||||||
|
human_signal_outq = queue.Queue() # Bytes
|
||||||
|
|
||||||
|
|
||||||
|
# Pretend we're getting input from a DCE terminal.
|
||||||
|
# For testing purposes.
|
||||||
|
# Create a device like this with:
|
||||||
|
# socat -d -d pty,rawer,echo=0,link=/tmp/ttyV0,b9600 pty,rawer,echo=0,link=/tmp/ttyV1,b9600
|
||||||
|
with serial.Serial("/dev/pts/6", 9600) as dce_out:
|
||||||
|
dce_out.write(b"Hello, world!")
|
||||||
|
|
||||||
|
|
||||||
|
def dce_inq_listen():
|
||||||
|
"""
|
||||||
|
Read one byte at a time from DCE and put in a queue.
|
||||||
|
|
||||||
|
The queue is a "transformer" queue that will transform
|
||||||
|
the byte values into a sequence of 0's and 1's.
|
||||||
|
"""
|
||||||
|
with serial.Serial("/dev/pts/7", 9600) as dce_in:
|
||||||
|
while True:
|
||||||
|
dce_transformer.put(ord(dce_in.read()))
|
||||||
|
|
||||||
|
|
||||||
|
def speak():
|
||||||
|
"""Background thread to populate the in queue."""
|
||||||
|
t = threading.Thread(target=dce_inq_listen)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def bits_to_byte(bits):
|
||||||
|
"""Convert list of 8 1's and 0's to a single int value."""
|
||||||
|
out = 0
|
||||||
|
for bit in bits:
|
||||||
|
out = (out << 1) | bit
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def display_dce():
|
||||||
|
"""
|
||||||
|
Display a graph of the input signal, slow enough for a human to transcribe.
|
||||||
|
|
||||||
|
This is probably the most finicky part of the process right now. It's not a very
|
||||||
|
clean display. The framerate isn't great. It's a bit choppy.
|
||||||
|
"""
|
||||||
|
SAMPLE_RATE = 500 # In milliseconds
|
||||||
|
FRAME_RATE = 60
|
||||||
|
FRAME_SIZE = 1000
|
||||||
|
SLEEP = 1 / FRAME_RATE
|
||||||
|
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
|
||||||
|
ax.set_xlim(0, 1000, auto=False)
|
||||||
|
ax.spines["left"].set_position("center")
|
||||||
|
ax.spines["bottom"].set_position("center")
|
||||||
|
ax.spines["right"].set_color("none")
|
||||||
|
ax.spines["top"].set_color("none")
|
||||||
|
|
||||||
|
points = []
|
||||||
|
|
||||||
|
prev = time.time_ns()
|
||||||
|
|
||||||
|
plt.ion()
|
||||||
|
plt.show()
|
||||||
|
while True:
|
||||||
|
# Advance all points
|
||||||
|
for point in points:
|
||||||
|
point[0] += 1/80 * FRAME_SIZE
|
||||||
|
|
||||||
|
# If we've passed 250ms, take a sample.
|
||||||
|
if time.time_ns() - prev > SAMPLE_RATE * 1e6:
|
||||||
|
prev += SAMPLE_RATE * 1e6
|
||||||
|
new_sample = [0, dce_transformer.get()]
|
||||||
|
if points and new_sample[1] != points[-1][1]:
|
||||||
|
points.append([points[-1][0], new_sample[1]]) # prev point's x, new sample's y.
|
||||||
|
points.append(new_sample)
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for point in points:
|
||||||
|
if point[0] < FRAME_SIZE:
|
||||||
|
filtered.append(point)
|
||||||
|
|
||||||
|
points = filtered
|
||||||
|
|
||||||
|
lines = ax.get_lines()
|
||||||
|
for line in lines:
|
||||||
|
line.remove()
|
||||||
|
|
||||||
|
ax.plot(*zip(*points), color="black")
|
||||||
|
plt.draw()
|
||||||
|
plt.pause(SLEEP)
|
||||||
|
|
||||||
|
|
||||||
|
def sample_signal():
|
||||||
|
"""Get a signal from the human-managed switch."""
|
||||||
|
# The signal will come from an Arduino or something.
|
||||||
|
# This fakes it until that's setup.
|
||||||
|
return [0, random.randint(0, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def human_signal_listen():
|
||||||
|
"""Read one byte (which represents 1 bit) at a time from human input."""
|
||||||
|
bits = [0] * 8
|
||||||
|
i = 0
|
||||||
|
with serial.Serial("/dev/pts/11", 9600) as human_signal_in:
|
||||||
|
while True:
|
||||||
|
bits[i] = human_signal_in.put(human_signal_inq.read())
|
||||||
|
i += 1
|
||||||
|
if i == 8:
|
||||||
|
i = 0
|
||||||
|
human_signal_outq.put()
|
Loading…
Reference in New Issue