Skip to content

Replace PySimpleGUI with Tkinter for Python GUI #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Nov 25, 2024
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6daeedb
python: Use tkinter build GUI instead of PySympleGUI
moon-jam Oct 24, 2024
be455f7
python: Fix weird btn color in different OS
moon-jam Nov 6, 2024
c958eaa
python: Split UI into tabs
JohnAZoidberg Nov 18, 2024
c4b52f0
python: Remove styling
JohnAZoidberg Nov 18, 2024
7f3e67c
python: Move device control to the bottom
JohnAZoidberg Nov 18, 2024
4333b1b
python: Add ledris game
JohnAZoidberg Nov 22, 2024
3d5fca9
python/ledris: Draw on led matrix
JohnAZoidberg Nov 22, 2024
f379c85
python/ledris: Allow to be imported
JohnAZoidberg Nov 22, 2024
4219c2a
python: Add ledris in tkinter GUI
JohnAZoidberg Nov 22, 2024
e24c3c9
python: Migrate snake to pygame
JohnAZoidberg Nov 22, 2024
5ddd382
python: Add game of life to tkinter gui
JohnAZoidberg Nov 22, 2024
b028239
gh-actions: Update actions to v4
JohnAZoidberg Nov 25, 2024
766b235
python: Update windows bundle to Python 3.12
JohnAZoidberg Nov 25, 2024
d97884d
python: Move requirements.txt to subfolder
JohnAZoidberg Nov 25, 2024
7fb179f
python: Implement firmware update
JohnAZoidberg Nov 25, 2024
5440e25
python: Rearrange game of life buttons in a grid
JohnAZoidberg Nov 25, 2024
1b17be5
python: Install pygame
JohnAZoidberg Nov 25, 2024
89186a5
python: Fix pyinstaller
JohnAZoidberg Nov 25, 2024
a6c0258
python: fix popup function changed when removing pysimplegui
JohnAZoidberg Nov 25, 2024
5d4128c
python: readd keyget snake
JohnAZoidberg Nov 25, 2024
ca29198
python: Add GUI screenshot
JohnAZoidberg Nov 25, 2024
cce7898
python: Add links to online info
JohnAZoidberg Nov 25, 2024
ca30109
python: Don't import pygame if not needed
JohnAZoidberg Nov 25, 2024
c3d7359
python: Move sleep/wake buttons to advanced tab
JohnAZoidberg Nov 25, 2024
fd0b85c
python: Launch GUI by default
JohnAZoidberg Nov 25, 2024
13d3d02
python: Add windows app icon
JohnAZoidberg Nov 25, 2024
b5b8739
python: Add executable icon
JohnAZoidberg Nov 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/firmware.yml
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ jobs:
name: Building
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
@@ -66,7 +66,7 @@ jobs:
cargo make --cwd ledmatrix bin

- name: Upload ledmatrix files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ledmatrix_fw_${{github.sha}}
path: |
@@ -79,15 +79,15 @@ jobs:
target/thumbv6m-none-eabi/release/ledmatrix_evt.uf2

- name: Upload b1display files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: b1display_fw_${{github.sha}}
path: |
target/thumbv6m-none-eabi/release/b1display.bin
target/thumbv6m-none-eabi/release/b1display.uf2

- name: Upload c1minimal files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: c1minimal_fw_${{github.sha}}
path: |
@@ -98,7 +98,7 @@ jobs:
name: Linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
@@ -130,7 +130,7 @@ jobs:
name: Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
36 changes: 22 additions & 14 deletions .github/workflows/software.yml
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ jobs:
# name: Cross-Build for FreeBSD
# runs-on: 'ubuntu-22.04'
# steps:
# - uses: actions/checkout@v3
# - uses: actions/checkout@v4

# - name: Setup Rust toolchain
# run: rustup show
@@ -41,7 +41,7 @@ jobs:
# run: cross build --target=x86_64-unknown-freebsd

# - name: Upload FreeBSD App
# uses: actions/upload-artifact@v3
# uses: actions/upload-artifact@v4
# with:
# name: qmk_hid_freebsd
# path: target/x86_64-unknown-freebsd/debug/qmk_hid
@@ -50,7 +50,7 @@ jobs:
name: Build Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install dependencies
run: |
@@ -69,7 +69,7 @@ jobs:
run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline'

- name: Upload Linux tool
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: inputmodule-control
path: target/x86_64-unknown-linux-gnu/release/inputmodule-control
@@ -78,7 +78,7 @@ jobs:
name: Build Windows
runs-on: windows-2022
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
@@ -92,7 +92,7 @@ jobs:
run: cargo make --cwd inputmodule-control run -- --help | grep 'RAW HID and VIA commandline'

- name: Upload Windows App
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: inputmodule-control.exe
path: target/x86_64-pc-windows-msvc/release/inputmodule-control.exe
@@ -103,22 +103,30 @@ jobs:
name: Build GUI
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Download releases to bundle
run: |
mkdir releases
mkdir releases\0.2.0
Invoke-WebRequest -Uri https://github.com/FrameworkComputer/inputmodule-rs/releases/download/v0.2.0/ledmatrix.uf2 -OutFile releases\0.2.0\ledmatrix.uf2

# To run locally, need to make sure to include the pywin32 DLL
# pyinstaller --onefile, --name "python/inputmodule/cli.py", --windowed, --add-data "releases;releases" --icon=res\framework_startmenuicon.ico --path C:\users\skype\appdata\local\packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\localcache\local-packages\Python312\site-packages\pywin32_system32 --add-data 'res;res' -p python/inputmodule python/inputmodule/cli.py
- name: Create Executable
uses: Martin005/pyinstaller-action@main
uses: JohnAZoidberg/pyinstaller-action@dont-clean
with:
python_ver: '3.11'
python_ver: '3.12'
spec: python/inputmodule/cli.py #'src/build.spec'
requirements: 'requirements.txt'
upload_exe_with_name: 'ledmatrixgui'
options: --onefile, --windowed, --add-data 'res;res'
requirements: 'python/requirements.txt'
upload_exe_with_name: 'ledmatrixgui.exe'
options: --onefile, --name "ledmatrixgui", --windowed, --add-data "releases;releases" --icon=res/framework_startmenuicon.ico --add-data 'res;res' -p python/inputmodule

package-python:
name: Package Python
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- run: |
cd python
@@ -131,7 +139,7 @@ jobs:
name: Lints
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install dependencies
run: |
8 changes: 4 additions & 4 deletions .github/workflows/traditional-cargo.yml
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ jobs:
name: Build firmware
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
@@ -41,7 +41,7 @@ jobs:
name: Build Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install dependencies
run: |
@@ -61,7 +61,7 @@ jobs:
name: Build Windows
runs-on: windows-2022
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup Rust toolchain
run: rustup show
@@ -78,7 +78,7 @@ jobs:
name: Lint and format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Install dependencies
run: |
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -35,10 +35,14 @@ To build your own application see the: [API command documentation](commands.md)
Or use our `inputmodule-control` app, which you can download from the latest
[GH Actions](https://github.com/FrameworkComputer/inputmodule-rs/actions) run or
the [release page](https://github.com/FrameworkComputer/inputmodule-rs/releases).
Optionally there are is also a [Python script](python/README.md).

For device specific commands, see their individual documentation pages.

### GUI and Python
There are also a python library and GUI tool. See their [README](python/README.md).

![](res/ledmatrixgui-home.png)

###### Permissions on Linux
To ensure that the input module's port is accessible, install the `udev` rule and trigger a reload:

19 changes: 8 additions & 11 deletions python/inputmodule/cli.py
Original file line number Diff line number Diff line change
@@ -14,11 +14,11 @@
brightness,
get_brightness,
CommandVals,
bootloader,
bootloader_jump,
GameOfLifeStartParam,
GameControlVal,
)
from inputmodule.gui.games import (
from inputmodule.games import (
snake,
snake_embedded,
pong_embedded,
@@ -62,10 +62,6 @@
RGB_COLORS,
)

# Optional dependencies:
# from PIL import Image
# import PySimpleGUI as sg


def main_cli():
parser = argparse.ArgumentParser()
@@ -237,7 +233,7 @@ def main_cli():

if not ports:
print("No device found")
gui.popup(args.gui, "No device found")
gui.popup("No device found", gui=args.gui)
sys.exit(1)
elif args.serial_dev is not None:
filtered_devs = [
@@ -250,10 +246,10 @@ def main_cli():
dev = ports[0]
elif len(ports) >= 1 and not args.gui:
gui.popup(
args.gui,
"More than 1 compatibles devices found. Please choose from the commandline with --serial-dev COMX.\nConnected ports:\n- {}".format(
"\n- ".join([port.device for port in ports])
),
gui=args.gui,
)
print(
"More than 1 compatible device found. Please choose with --serial-dev ..."
@@ -268,11 +264,11 @@ def main_cli():

if not args.gui and dev is None:
print("No device selected")
gui.popup(args.gui, "No device selected")
gui.popup("No device selected", gui=args.gui)
sys.exit(1)

if args.bootloader:
bootloader(dev)
bootloader_jump(dev)
elif args.sleep is not None:
send_command(dev, CommandVals.Sleep, [args.sleep])
elif args.is_sleeping:
@@ -394,6 +390,7 @@ def find_devs():
def print_devs(ports):
for port in ports:
print(f"{port.device}")
print(f" {port.name}")
print(f" VID: 0x{port.vid:04X}")
print(f" PID: 0x{port.pid:04X}")
print(f" SN: {port.serial_number}")
@@ -407,4 +404,4 @@ def main_gui():


if __name__ == "__main__":
main_cli()
main_gui()
80 changes: 80 additions & 0 deletions python/inputmodule/firmware_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import time

from inputmodule.inputmodule import bootloader_jump
from inputmodule import uf2conv

def dev_to_str(dev):
return dev.name

def flash_firmware(dev, fw_path):
print(f"Flashing {fw_path} onto {dev_to_str(dev)}")

# First jump to bootloader
drives = uf2conv.list_drives()
if not drives:
print("Jump to bootloader")
bootloader_jump(dev)

timeout = 10 # 5s
while not drives:
if timeout == 0:
print("Failed to find device in bootloader")
# TODO: Handle return value
return False
# Wait for it to appear
time.sleep(0.5)
timeout -= 1
drives = uf2conv.get_drives()


if len(drives) == 0:
print("No drive to deploy.")
return False

# Firmware is pretty small, can just fit it all into memory
with open(fw_path, 'rb') as f:
fw_buf = f.read()

for d in drives:
print("Flashing {} ({})".format(d, uf2conv.board_id(d)))
uf2conv.write_file(d + "/NEW.UF2", fw_buf)

print("Flashing finished")

# Example return value
# {
# '0.1.7': {
# 'ansi': 'framework_ansi_default_v0.1.7.uf2',
# 'gridpad': 'framework_gridpad_default_v0.1.7.uf2'
# },
# '0.1.8': {
# 'ansi': 'framework_ansi_default.uf2',
# 'gridpad': 'framework_gridpad_default.uf2',
# }
# }
def find_releases(res_path, filename_format):
from os import listdir
from os.path import isfile, join
import re

releases = {}
try:
versions = listdir(os.path.join(res_path, "releases"))
except FileNotFoundError:
return releases

for version in versions:
path = join(res_path, "releases", version)
releases[version] = {}
for filename in listdir(path):
if not isfile(join(path, filename)):
continue
type_search = re.search(filename_format, filename)
if not type_search:
print(f"Filename '{filename}' not matching patten!")
sys.exit(1)
continue
fw_type = type_search.group(1)
releases[version][fw_type] = os.path.join(res_path, "releases", version, filename)
return releases
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ def opposite_direction(direction):
return direction



def snake_keyscan():
global direction
global body
551 changes: 314 additions & 237 deletions python/inputmodule/gui/__init__.py

Large diffs are not rendered by default.

Empty file.
254 changes: 254 additions & 0 deletions python/inputmodule/gui/pygames/ledris.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# Run like
# python3 ledris.py

import pygame
import random
import time

from inputmodule import cli
from inputmodule.gui.ledmatrix import show_string
from inputmodule.inputmodule import ledmatrix

# Set the screen width and height for a 34 x 9 block Ledris game
block_width = 20
block_height = 20
cols = 9
rows = 34

width = cols * block_width
height = rows * block_height

# Colors
black = (0, 0, 0)
white = (255, 255, 255)

# Ledrimino shapes
shapes = [
[[1, 1, 1, 1]], # I shape
[[1, 1], [1, 1]], # O shape
[[0, 1, 0], [1, 1, 1]], # T shape
[[1, 1, 0], [0, 1, 1]], # S shape
[[0, 1, 1], [1, 1, 0]], # Z shape
[[1, 1, 1], [1, 0, 0]], # L shape
[[1, 1, 1], [0, 0, 1]] # J shape
]

# Function to get the current board state
def get_board_state(board, current_shape, current_pos):
temp_board = [row[:] for row in board]
off_x, off_y = current_pos
for y, row in enumerate(current_shape):
for x, cell in enumerate(row):
if cell:
if 0 <= off_y + y < rows and 0 <= off_x + x < cols:
temp_board[off_y + y][off_x + x] = 1
return temp_board

def draw_ledmatrix(board, devices):
for dev in devices:
matrix = [[0 for _ in range(34)] for _ in range(9)]
for y in range(rows):
for x in range(cols):
matrix[x][y] = board[y][x]
ledmatrix.render_matrix(dev, matrix)
#vals = [0 for _ in range(39)]
#send_command(dev, CommandVals.Draw, vals)

# Function to check if the position is valid
def check_collision(board, shape, offset):
off_x, off_y = offset
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if cell:
if x + off_x < 0 or x + off_x >= cols or y + off_y >= rows:
return True
if y + off_y >= 0 and board[y + off_y][x + off_x]:
return True
return False

# Function to merge the shape into the board
def merge_shape(board, shape, offset):
off_x, off_y = offset
for y, row in enumerate(shape):
for x, cell in enumerate(row):
if cell:
if 0 <= off_y + y < rows and 0 <= off_x + x < cols:
board[off_y + y][off_x + x] = 1

# Function to clear complete rows
def clear_rows(board):
new_board = [row for row in board if any(cell == 0 for cell in row)]
cleared_rows = rows - len(new_board)
while len(new_board) < rows:
new_board.insert(0, [0 for _ in range(cols)])
return new_board, cleared_rows

# Function to display the score using blocks
def display_score(board, score):
score_str = str(score)
start_x = cols - len(score_str) * 4
for i, digit in enumerate(score_str):
if digit.isdigit():
digit = int(digit)
for y in range(5):
for x in range(3):
if digit_blocks[digit][y][x]:
if y < rows and start_x + i * 4 + x < cols:
board[y][start_x + i * 4 + x] = 1

# Digit blocks for representing score
# Each number is represented in a 5x3 block matrix
digit_blocks = [
[[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0
[[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1
[[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2
[[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3
[[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4
[[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5
[[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6
[[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7
[[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8
[[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9
]


class Ledris:
# Function to draw a grid
def draw_grid(self):
for y in range(rows):
for x in range(cols):
rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height)
pygame.draw.rect(self.screen, black, rect, 1)

# Function to draw the game based on the board state
def draw_board(self, board, devices):
draw_ledmatrix(board, devices)
self.screen.fill(white)
for y in range(rows):
for x in range(cols):
if board[y][x]:
rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height)
pygame.draw.rect(self.screen, black, rect)
self.draw_grid()
pygame.display.update()

# Main game function
def gameLoop(self, devices):
board = [[0 for _ in range(cols)] for _ in range(rows)]
current_shape = random.choice(shapes)
current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display
game_over = False
fall_time = 0
fall_speed = 500 # Falling speed in milliseconds
score = 0

while not game_over:
# Adjust falling speed based on score
fall_speed = max(100, 500 - (score * 10))

# Draw the current board state
board_state = get_board_state(board, current_shape, current_pos)
display_score(board_state, score)
self.draw_board(board_state, devices)

# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
game_over = True

if event.type == pygame.KEYDOWN:
if event.key in [pygame.K_LEFT, pygame.K_h]:
new_pos = [current_pos[0] - 1, current_pos[1]]
if not check_collision(board, current_shape, new_pos):
current_pos = new_pos
elif event.key in [pygame.K_RIGHT, pygame.K_l]:
new_pos = [current_pos[0] + 1, current_pos[1]]
if not check_collision(board, current_shape, new_pos):
current_pos = new_pos
elif event.key in [pygame.K_DOWN, pygame.K_j]:
new_pos = [current_pos[0], current_pos[1] + 1]
if not check_collision(board, current_shape, new_pos):
current_pos = new_pos
elif event.key in [pygame.K_UP, pygame.K_k]:
rotated_shape = list(zip(*current_shape[::-1]))
if not check_collision(board, rotated_shape, current_pos):
current_shape = rotated_shape
elif event.key == pygame.K_SPACE: # Hard drop
while not check_collision(board, current_shape, [current_pos[0], current_pos[1] + 1]):
current_pos[1] += 1

# Automatic falling
fall_time += self.clock.get_time()
if fall_time >= fall_speed:
fall_time = 0
new_pos = [current_pos[0], current_pos[1] + 1]
if not check_collision(board, current_shape, new_pos):
current_pos = new_pos
else:
merge_shape(board, current_shape, current_pos)
board, cleared_rows = clear_rows(board)
score += cleared_rows # Increase score by one for each row cleared
current_shape = random.choice(shapes)
current_pos = [cols // 2 - len(current_shape[0]) // 2, 5] # Start below the score display
if check_collision(board, current_shape, current_pos):
game_over = True

self.clock.tick(30)

# Flash the screen twice before waiting for restart
for _ in range(2):
for dev in devices:
ledmatrix.percentage(dev, 0)
self.screen.fill(black)
pygame.display.update()
time.sleep(0.3)

for dev in devices:
ledmatrix.percentage(dev, 100)
self.screen.fill(white)
pygame.display.update()
time.sleep(0.3)

# Display final score and wait for restart without clearing the screen
board_state = get_board_state(board, current_shape, current_pos)
display_score(board_state, score)
self.draw_board(board_state, devices)

waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.QUIT:
waiting = False
game_over = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
waiting = False
if event.key == pygame.K_r:
board = [[0 for _ in range(cols)] for _ in range(rows)]
gameLoop()

pygame.quit()
quit()

def __init__(self):
# Initialize pygame
pygame.init()

# Create the screen
self.screen = pygame.display.set_mode((width, height))

# Clock to control the speed of the game
self.clock = pygame.time.Clock()

def main_devices(devices):
ledris = Ledris()
ledris.gameLoop(devices)

def main():
devices = cli.find_devs()

ledris = Ledris()
ledris.gameLoop(devices)

if __name__ == "__main__":
main()
254 changes: 254 additions & 0 deletions python/inputmodule/gui/pygames/snake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import pygame
import random
import time

from inputmodule import cli
from inputmodule.inputmodule import ledmatrix

# Set the screen width and height for a 34 x 9 block game
block_width = 20
block_height = 20
COLS = 9
ROWS = 34

WIDTH = COLS * block_width
HEIGHT = ROWS * block_height

# Colors
black = (0, 0, 0)
white = (255, 255, 255)

def opposite_direction(direction):
if direction == pygame.K_RIGHT:
return pygame.K_LEFT
elif direction == pygame.K_LEFT:
return pygame.K_RIGHT
elif direction == pygame.K_UP:
return pygame.K_DOWN
elif direction == pygame.K_DOWN:
return pygame.K_UP
return direction

# Function to get the current board state
def get_board_state(board):
temp_board = [row[:] for row in board]
#off_x, off_y = current_pos
#for y, row in enumerate(current_shape):
# for x, cell in enumerate(row):
# if cell:
# if 0 <= off_y + y < ROWS and 0 <= off_x + x < COLS:
# temp_board[off_y + y][off_x + x] = 1
return temp_board

def draw_ledmatrix(board, devices):
for dev in devices:
matrix = [[0 for _ in range(34)] for _ in range(9)]
for y in range(ROWS):
for x in range(COLS):
matrix[x][y] = board[y][x]
ledmatrix.render_matrix(dev, matrix)
#vals = [0 for _ in range(39)]
#send_command(dev, CommandVals.Draw, vals)

# Function to display the score using blocks
def display_score(board, score):
return
score_str = str(score)
start_x = COLS - len(score_str) * 4
for i, digit in enumerate(score_str):
if digit.isdigit():
digit = int(digit)
for y in range(5):
for x in range(3):
if digit_blocks[digit][y][x]:
if y < ROWS and start_x + i * 4 + x < COLS:
board[y][start_x + i * 4 + x] = 1

# Digit blocks for representing score
# Each number is represented in a 5x3 block matrix
digit_blocks = [
[[1, 1, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1], [1, 1, 1]], # 0
[[0, 1, 0], [1, 1, 0], [0, 1, 0], [0, 1, 0], [1, 1, 1]], # 1
[[1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 0, 0], [1, 1, 1]], # 2
[[1, 1, 1], [0, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 3
[[1, 0, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [0, 0, 1]], # 4
[[1, 1, 1], [1, 0, 0], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 5
[[1, 1, 1], [1, 0, 0], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 6
[[1, 1, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1], [0, 0, 1]], # 7
[[1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1]], # 8
[[1, 1, 1], [1, 0, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1]], # 9
]


class Snake:
# Function to draw a grid
def draw_grid(self):
for y in range(ROWS):
for x in range(COLS):
rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height)
pygame.draw.rect(self.screen, black, rect, 1)

# Function to draw the game based on the board state
def draw_board(self, board, devices):
draw_ledmatrix(board, devices)
self.screen.fill(white)
for y in range(ROWS):
for x in range(COLS):
if board[y][x]:
rect = pygame.Rect(x * block_width, y * block_height, block_width, block_height)
pygame.draw.rect(self.screen, black, rect)
self.draw_grid()
pygame.display.update()

# Main game function
def gameLoop(self, devices):
board = [[0 for _ in range(COLS)] for _ in range(ROWS)]

game_over = False
body = []
score = 0
head = (0, 0)
direction = pygame.K_DOWN
food = (0, 0)
while food == head:
food = (random.randint(0, COLS - 1), random.randint(0, ROWS - 1))
move_time = 0

# Setting
# Wrap and let the snake come out the other side
WRAP = False
MOVE_PERIOD = 200

while not game_over:
# Draw the current board state
board_state = get_board_state(board)
display_score(board_state, score)
self.draw_board(board_state, devices)

# Event handling
for event in pygame.event.get():
if event.type == pygame.QUIT:
game_over = True

if event.type == pygame.KEYDOWN:
if event.key == opposite_direction(direction) and body:
continue
if event.key in [pygame.K_LEFT, pygame.K_h]:
direction = pygame.K_LEFT
elif event.key in [pygame.K_RIGHT, pygame.K_l]:
direction = pygame.K_RIGHT
elif event.key in [pygame.K_DOWN, pygame.K_j]:
direction = pygame.K_DOWN
elif event.key in [pygame.K_UP, pygame.K_k]:
direction = pygame.K_UP

move_time += self.clock.get_time()
if move_time >= MOVE_PERIOD:
move_time = 0

# Update position
(x, y) = head
oldhead = head
if direction == pygame.K_LEFT:
head = (x - 1, y)
elif direction == pygame.K_RIGHT:
head = (x + 1, y)
elif direction == pygame.K_DOWN:
head = (x, y + 1)
elif direction == pygame.K_UP:
head = (x, y - 1)

# Detect edge condition
(x, y) = head
if head in body:
game_over = True
elif x >= COLS or x < 0 or y >= ROWS or y < 0:
if WRAP:
if x >= COLS:
x = 0
elif x < 0:
x = COLS - 1
elif y >= ROWS:
y = 0
elif y < 0:
y = ROWS - 1
head = (x, y)
else:
game_over = True
elif head == food:
body.insert(0, oldhead)
while food == head:
food = (random.randint(0, COLS - 1),
random.randint(0, ROWS - 1))
elif body:
body.pop()
body.insert(0, oldhead)

# Draw on screen
if not game_over:
board = [[0 for _ in range(COLS)] for _ in range(ROWS)]
board[y][x] = 1
board[food[1]][food[0]] = 1
for bodypart in body:
(x, y) = bodypart
board[y][x] = 1

self.clock.tick(30)

# Flash the screen twice before waiting for restart
for _ in range(2):
for dev in devices:
ledmatrix.percentage(dev, 0)
self.screen.fill(black)
pygame.display.update()
time.sleep(0.3)

for dev in devices:
ledmatrix.percentage(dev, 100)
self.screen.fill(white)
pygame.display.update()
time.sleep(0.3)

# Display final score and wait for restart without clearing the screen
board_state = get_board_state(board)
display_score(board_state, score)
self.draw_board(board_state, devices)

waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.QUIT:
waiting = False
game_over = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_q:
waiting = False
if event.key == pygame.K_r:
board = [[0 for _ in range(COLS)] for _ in range(ROWS)]
gameLoop()

pygame.quit()
quit()

def __init__(self):
# Initialize pygame
pygame.init()

# Create the screen
self.screen = pygame.display.set_mode((WIDTH, HEIGHT))

# Clock to control the speed of the game
self.clock = pygame.time.Clock()

def main_devices(devices):
snake = Snake()
snake.gameLoop(devices)

def main():
devices = cli.find_devs()

snake = Snake()
snake.gameLoop(devices)

if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion python/inputmodule/inputmodule/__init__.py
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ class GameControlVal(IntEnum):
RESPONSE_SIZE = 32


def bootloader(dev):
def bootloader_jump(dev):
"""Reboot into the bootloader to flash new firmware"""
send_command(dev, CommandVals.BootloaderReset, [0x00])

397 changes: 397 additions & 0 deletions python/inputmodule/uf2conv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
#!/usr/bin/env python3

# SPDX-License-Identifier: MIT
# Copyright (c) Microsoft Corporation and others
# Taken from: https://github.com/microsoft/uf2/blob/master/utils/uf2conv.py
# And modified, some changes already upstreamed

# yapf: disable
import sys
import struct
import subprocess
import re
import os
import os.path
import argparse
import json

# Don't even need -b. hex has this embedded
# > ./util/uf2conv.py .build/framework_ansi_default.hex -o ansi.uf2 -b 0x10000000 -f rp2040 --convert --blocks-reserved 1
# Converted to 222 blocks
# Converted to uf2, output size: 113664, start address: 0x10000000
# Wrote 113664 bytes to ansi.uf2
# # 113664 / 512 = 222
#
# > ./util/uf2conv.py serial.bin -o serial.uf2 -b 0x100ff000 -f rp2040 --convert --blocks-offset 222
# Converted to 1 blocks
# Converted to uf2, output size: 512, start address: 0x100ff000
# Wrote 512 bytes to serial.uf2



UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
UF2_MAGIC_END = 0x0AB16F30 # Ditto

INFO_FILE = "/INFO_UF2.TXT"

appstartaddr = 0x2000
familyid = 0x0


def is_uf2(buf):
w = struct.unpack("<II", buf[0:8])
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1

def is_hex(buf):
try:
w = buf[0:30].decode("utf-8")
except UnicodeDecodeError:
return False
if w[0] == ':' and re.match(b"^[:0-9a-fA-F\r\n]+$", buf):
return True
return False

def convert_from_uf2(buf):
global appstartaddr
global familyid
numblocks = len(buf) // 512
curraddr = None
currfamilyid = None
families_found = {}
prev_flag = None
all_flags_same = True
outp = []
for blockno in range(numblocks):
ptr = blockno * 512
block = buf[ptr:ptr + 512]
hd = struct.unpack(b"<IIIIIIII", block[0:32])
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
print("Skipping block at " + ptr + "; bad magic")
continue
if hd[2] & 1:
# NO-flash flag set; skip block
continue
datalen = hd[4]
if datalen > 476:
assert False, "Invalid UF2 data size at " + ptr
newaddr = hd[3]
if (hd[2] & 0x2000) and (currfamilyid == None):
currfamilyid = hd[7]
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
currfamilyid = hd[7]
curraddr = newaddr
if familyid == 0x0 or familyid == hd[7]:
appstartaddr = newaddr
print(f" flags: 0x{hd[2]:02x}")
print(f" addr: 0x{hd[3]:02x}")
print(f" len: {hd[4]}")
print(f" block no: {hd[5]}")
print(f" blocks: {hd[6]}")
print(f" size/famid: {hd[7]}")
print()
padding = newaddr - curraddr
if padding < 0:
assert False, "Block out of order at " + ptr
if padding > 10*1024*1024:
assert False, "More than 10M of padding needed at " + ptr
if padding % 4 != 0:
assert False, "Non-word padding size at " + ptr
while padding > 0:
padding -= 4
outp.append(b"\x00\x00\x00\x00")
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
outp.append(block[32 : 32 + datalen])
curraddr = newaddr + datalen
if hd[2] & 0x2000:
if hd[7] in families_found.keys():
if families_found[hd[7]] > newaddr:
families_found[hd[7]] = newaddr
else:
families_found[hd[7]] = newaddr
if prev_flag == None:
prev_flag = hd[2]
if prev_flag != hd[2]:
all_flags_same = False
if blockno == (numblocks - 1):
print("--- UF2 File Header Info ---")
families = load_families()
for family_hex in families_found.keys():
family_short_name = ""
for name, value in families.items():
if value == family_hex:
family_short_name = name
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
if all_flags_same:
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
else:
print("Flags were not all the same")
print("----------------------------")
if len(families_found) > 1 and familyid == 0x0:
outp = []
appstartaddr = 0x0
return b"".join(outp)

def convert_to_carray(file_content):
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
for i in range(len(file_content)):
if i % 16 == 0:
outp += "\n"
outp += "0x%02x, " % file_content[i]
outp += "\n};\n"
return bytes(outp, "utf-8")

def convert_to_uf2(file_content, blocks_reserved=0, blocks_offset=0):
global familyid
datapadding = b""
while len(datapadding) < 512 - 256 - 32 - 4:
datapadding += b"\x00\x00\x00\x00"
numblocks = (len(file_content) + 255) // 256
outp = []
for blockno in range(numblocks):
ptr = 256 * blockno
chunk = file_content[ptr:ptr + 256]
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack(b"<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, ptr + appstartaddr, 256, blockno + blocks_offset, blocks_offset + blocks_reserved + numblocks, familyid)
while len(chunk) < 256:
chunk += b"\x00"
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
assert len(block) == 512
outp.append(block)
print(f"Converted to {numblocks} blocks")
return b"".join(outp)

class Block:
def __init__(self, addr):
self.addr = addr
self.bytes = bytearray(256)

def encode(self, blockno, numblocks, blocks_reserved=0, blocks_offset=0):
global familyid
flags = 0x0
if familyid:
flags |= 0x2000
hd = struct.pack("<IIIIIIII",
UF2_MAGIC_START0, UF2_MAGIC_START1,
flags, self.addr, 256, blockno + blocks_offset, blocks_offset + blocks_reserved + numblocks, familyid)
hd += self.bytes[0:256]
while len(hd) < 512 - 4:
hd += b"\x00"
hd += struct.pack("<I", UF2_MAGIC_END)
return hd

def convert_from_hex_to_uf2(buf, blocks_reserved=0, blocks_offset=0):
global appstartaddr
appstartaddr = None
upper = 0
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
continue
i = 1
rec = []
while i < len(line) - 1:
rec.append(int(line[i:i+2], 16))
i += 2
tp = rec[3]
if tp == 4:
upper = ((rec[4] << 8) | rec[5]) << 16
elif tp == 2:
upper = ((rec[4] << 8) | rec[5]) << 4
elif tp == 1:
break
elif tp == 0:
addr = upper + ((rec[1] << 8) | rec[2])
if appstartaddr == None:
appstartaddr = addr
i = 4
while i < len(rec) - 1:
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
currblock = Block(addr & ~0xff)
blocks.append(currblock)
currblock.bytes[addr & 0xff] = rec[i]
addr += 1
i += 1
numblocks = len(blocks)
print(f"Converted to {numblocks} blocks")
resfile = b""
for i in range(0, numblocks):
resfile += blocks[i].encode(i, numblocks, blocks_reserved, blocks_offset)
return resfile

def to_str(b):
return b.decode("utf-8")

def get_drives():
drives = []
if sys.platform == "win32":
# TODO: Could also check "VolumeName" == "RPI-RP2"
# But it should be okay because we check for the INFO file that no other drive would have
r = subprocess.check_output(["wmic", "PATH", "Win32_LogicalDisk",
"get", "DeviceID,",
"FileSystem,", "DriveType"])
for line in to_str(r).split('\n'):
words = re.split(r'\s+', line)
if len(words) >= 3 and words[1] == "2" and words[2] == "FAT":
drives.append(words[0])
else:
rootpath = "/media"
if sys.platform == "darwin":
rootpath = "/Volumes"
elif sys.platform == "linux":
tmp = rootpath + "/" + os.environ["USER"]
if os.path.isdir(tmp):
rootpath = tmp
tmp = "/run" + rootpath + "/" + os.environ["USER"]
if os.path.isdir(tmp):
rootpath = tmp
for d in os.listdir(rootpath):
drives.append(os.path.join(rootpath, d))


def has_info(d):
try:
return os.path.isfile(d + INFO_FILE)
except:
return False

return list(filter(has_info, drives))


def board_id(path):
with open(path + INFO_FILE, mode='r') as file:
file_content = file.read()
return re.search("Board-ID: ([^\r\n]*)", file_content).group(1)


def list_drives():
for d in get_drives():
print(d, board_id(d))


def write_file(name, buf):
with open(name, "wb") as f:
f.write(buf)
print("Wrote %d bytes to %s" % (len(buf), name))


def load_families():
# The expectation is that the `uf2families.json` file is in the same
# directory as this script. Make a path that works using `__file__`
# which contains the full path to this script.
filename = "uf2families.json"
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
with open(pathname) as f:
raw_families = json.load(f)

families = {}
for family in raw_families:
families[family["short_name"]] = int(family["id"], 0)

return families


def main():
global appstartaddr, familyid
def error(msg):
print(msg, file=sys.stderr)
sys.exit(1)
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
help='input file (HEX, BIN or UF2)')
parser.add_argument('-b' , '--base', dest='base', type=str,
default="0x2000",
help='set base address of application for BIN format (default: 0x2000)')
parser.add_argument('-o' , '--output', metavar="FILE", dest='output', type=str,
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
parser.add_argument('-d' , '--device', dest="device_path",
help='select a device path to flash')
parser.add_argument('-l' , '--list', action='store_true',
help='list connected devices')
parser.add_argument('-c' , '--convert', action='store_true',
help='do not flash, just convert')
parser.add_argument('-D' , '--deploy', action='store_true',
help='just flash, do not convert')
parser.add_argument('-f' , '--family', dest='family', type=str,
default="0x0",
help='specify familyID - number or name (default: 0x0)')
parser.add_argument('--blocks-offset', dest='blocks_offset', type=str,
default="0x0",
help='TODO')
parser.add_argument('--blocks-reserved', dest='blocks_reserved', type=str,
default="0x0",
help='TODO')
parser.add_argument('-C' , '--carray', action='store_true',
help='convert binary file to a C array, not UF2')
parser.add_argument('-i', '--info', action='store_true',
help='display header information from UF2, do not convert')
args = parser.parse_args()
appstartaddr = int(args.base, 0)
blocks_offset = int(args.blocks_offset, 0)
blocks_reserved = int(args.blocks_reserved, 0)

families = load_families()

if args.family.upper() in families:
familyid = families[args.family.upper()]
else:
try:
familyid = int(args.family, 0)
except ValueError:
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))

if args.list:
list_drives()
else:
if not args.input:
error("Need input file")
with open(args.input, mode='rb') as f:
inpbuf = f.read()
from_uf2 = is_uf2(inpbuf)
ext = "uf2"
if args.deploy:
outbuf = inpbuf
elif from_uf2 and not args.info:
outbuf = convert_from_uf2(inpbuf)
ext = "bin"
elif from_uf2 and args.info:
outbuf = ""
convert_from_uf2(inpbuf)

elif is_hex(inpbuf):
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), blocks_reserved, blocks_offset)
elif args.carray:
outbuf = convert_to_carray(inpbuf)
ext = "h"
else:
outbuf = convert_to_uf2(inpbuf, blocks_reserved, blocks_offset)
if not args.deploy and not args.info:
print("Converted to %s, output size: %d, start address: 0x%x" %
(ext, len(outbuf), appstartaddr))
if args.convert or ext != "uf2":
drives = []
if args.output == None:
args.output = "flash." + ext
else:
drives = get_drives()

if args.output:
write_file(args.output, outbuf)
else:
if len(drives) == 0:
error("No drive to deploy.")
if outbuf:
for d in drives:
print("Flashing %s (%s)" % (d, board_id(d)))
write_file(d + "/NEW.UF2", outbuf)


if __name__ == "__main__":
main()
6 changes: 3 additions & 3 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -25,9 +25,10 @@ classifiers = [
]
dependencies = [
"pyserial",
# Optional for GUI
# Optional for CLI
"getkey",
"PySimpleGUI",
# Optional for GUI
"pygame",
# Optional for image operations
"Pillow",
]
@@ -36,7 +37,6 @@ dependencies = [
Issues = "https://github.com/FrameworkComputer/inputmodule-rs/issues"
Source = "https://github.com/FrameworkComputer/inputmodule-rs"

# TODO: Figure out how to add a runnable-script
[project.scripts]
ledmatrixctl = "inputmodule.cli:main_cli"

2 changes: 1 addition & 1 deletion requirements.txt → python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
get-key==1.60.0
Pillow==10.0.0
pyserial==3.5
PySimpleGUI==4.60.5
pygame==2.6.1
Binary file added res/framework_startmenuicon.ico
Binary file not shown.
Binary file added res/ledmatrixgui-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.