Skip to content

Commit

Permalink
Add Tk-based simulator
Browse files Browse the repository at this point in the history
Inspired by the Scroll pHAT simulator - pimoroni/scroll-phat#87

Co-authored-by: Sebastian Brannstrom <teknolog@gmail.com>
  • Loading branch information
Gadgetoid and teknolog2000 committed Feb 9, 2022
1 parent 27a7dfc commit fae825c
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
13 changes: 13 additions & 0 deletions simulator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Blinkt! Simulator

A Tk based server to simulate Blinkt! on your Windows, Linux or macOS PC.

Works by hijacking the `blinkt` library and replacing it with a FIFO pipe to the Tk based simulator.

## Usage

Set the `PYTHONPATH` variable to the simulator directory and run an example. The fake `blinkt` will be loaded instead of the real one and output will launch in a new window:

```
PYTHONPATH=simulator python3 examples/rainbow.py
```
131 changes: 131 additions & 0 deletions simulator/blinkt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import sys
import subprocess
import pickle
import os
import atexit
import signal


__version__ = '0.1.2'


NUM_PIXELS = 8
BRIGHTNESS = 7

pixels = [[0, 0, 0, BRIGHTNESS]] * NUM_PIXELS

_process = None
_clear_on_exit = True


def _exit():
global _process, _clear_on_exit
if not _clear_on_exit:
sys.stdout.write("Ctrl+C to exit...\n")
sys.stdout.flush()
signal.pause()
if _process is not None:
_process.kill()


def set_brightness(brightness):
"""Set the brightness of all pixels.
:param brightness: Brightness: 0.0 to 1.0
"""
if brightness < 0 or brightness > 1:
raise ValueError('Brightness should be between 0.0 and 1.0')

for x in range(NUM_PIXELS):
pixels[x][3] = int(31.0 * brightness) & 0b11111


def clear():
"""Clear the pixel buffer."""
for x in range(NUM_PIXELS):
pixels[x][0:3] = [0, 0, 0]


def show():
"""Output the buffer to Blinkt!."""
global _process

if _process is None:
_process = subprocess.Popen(
[sys.executable, os.path.dirname(os.path.abspath(
__file__)) + '/blinkt_simulator.py'],
stdin=subprocess.PIPE)
atexit.register(_exit)

try:
pickle.dump(pixels, _process.stdin)
_process.stdin.flush()
except OSError:
sys.stderr.write('Lost connection to Blinkt! simulator\n')
sys.stderr.flush()
sys.exit(-1)


def set_all(r, g, b, brightness=None):
"""Set the RGB value and optionally brightness of all pixels.
If you don't supply a brightness value,
the last value set for each pixel be kept.
:param r: Amount of red: 0 to 255
:param g: Amount of green: 0 to 255
:param b: Amount of blue: 0 to 255
:param brightness: Brightness: 0.0 to 1.0 (default around 0.2)
"""
for x in range(NUM_PIXELS):
set_pixel(x, r, g, b, brightness)


def get_pixel(x):
"""Get the RGB and brightness value of a specific pixel.
:param x: The horizontal position of the pixel: 0 to 7
"""
r, g, b, brightness = pixels[x]
brightness /= 31.0

return r, g, b, round(brightness, 3)


def set_pixel(x, r, g, b, brightness=None):
"""Set the RGB value, and optionally brightness, of a single pixel.
If you don't supply a brightness value, the last value will be kept.
:param x: The horizontal position of the pixel: 0 to 7
:param r: Amount of red: 0 to 255
:param g: Amount of green: 0 to 255
:param b: Amount of blue: 0 to 255
:param brightness: Brightness: 0.0 to 1.0 (default around 0.2)
"""
if brightness is None:
brightness = pixels[x][3]
else:
brightness = int(31.0 * brightness) & 0b11111

pixels[x] = [int(r) & 0xff, int(g) & 0xff, int(b) & 0xff, brightness]


def set_clear_on_exit(value=True):
"""Set whether Blinkt! should be cleared upon exit.
By default Blinkt! will turn off the pixels on exit, but calling::
blinkt.set_clear_on_exit(False)
Will ensure that it does not.
:param value: True or False (default True)
"""
global _clear_on_exit
_clear_on_exit = value
115 changes: 115 additions & 0 deletions simulator/blinkt_simulator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import threading
import sys
import pickle
import tkinter as tk
import signal

NUM_PIXELS = 8
WINDOW_HEIGHT_PX = 80
WINDOW_WIDTH_PX = WINDOW_HEIGHT_PX * NUM_PIXELS

PIXEL_WIDTH = WINDOW_WIDTH_PX / NUM_PIXELS

DRAW_TIMEOUT_MS = 100


class TkPhatSimulator():
def __init__(self):
self.brightness = 70
self.do_run = True
self.data = [[0, 0, 0, 7] for _ in range(8)]

self.root = tk.Tk()
self.root.resizable(False, False)

self.root.bind('<Control-c>', lambda _: self.destroy())
self.root.bind("<Unmap>", lambda _: self.destroy())
self.root.protocol('WM_DELETE_WINDOW', self.destroy)

self.root.title('Blinkt! simulator')
self.root.geometry('{}x{}'.format(WINDOW_WIDTH_PX, WINDOW_HEIGHT_PX))
self.canvas = tk.Canvas(
self.root, width=WINDOW_WIDTH_PX, height=WINDOW_HEIGHT_PX)
self.canvas.config(highlightthickness=0)

def run(self):
try:
self.draw_pixels()
self.root.mainloop()
except Exception as e:
self.destroy()
raise e

def destroy(self):
self.do_run = False

def running(self):
return self.do_run

def draw_pixels(self):
if not self.running():
self.root.destroy()
return

self.canvas.delete(tk.ALL)
self.canvas.create_rectangle(
0, 0,
WINDOW_WIDTH_PX, WINDOW_HEIGHT_PX, width=0, fill='black')

for x in range(NUM_PIXELS):
pixel = self.data[x]
color = '#{0:02x}{1:02x}{2:02x}'.format(*pixel)
self.canvas.create_rectangle(
x * WINDOW_HEIGHT_PX, 0,
x * WINDOW_HEIGHT_PX + WINDOW_HEIGHT_PX,
WINDOW_HEIGHT_PX,
width=0, fill=color)

self.canvas.pack()

self.root.after(DRAW_TIMEOUT_MS, self.draw_pixels)

def set_data(self, data):
self.data = data


class ReadThread:
def __init__(self, simulator):
self.simulator = simulator
self.stdin_thread = threading.Thread(
target=self._read_stdin, daemon=True)

def start(self):
self.stdin_thread.start()

def join(self):
self.stdin_thread.join()

def _read_stdin(self):
while self.simulator.running():
try:
self._handle_update(pickle.load(sys.stdin.buffer))
except EOFError:
self.simulator.destroy()
except Exception as err:
self.simulator.destroy()
raise err

def _handle_update(self, buffer):
self.simulator.set_data(buffer)


def main():
print('Blinkt! simulator')

signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(0))

phat = TkPhatSimulator()
thread = ReadThread(phat)
thread.start()
phat.run()
thread.join()


if __name__ == "__main__":
main()
Binary file added simulator/simulator.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fae825c

Please sign in to comment.