Change interface to clickable (38c3)
This commit is contained in:
parent
0b7aa02f44
commit
bc9a7c56e9
16 changed files with 1099 additions and 522 deletions
|
@ -13,7 +13,7 @@ from loguru import logger
|
|||
|
||||
from . import (
|
||||
detector as _detector,
|
||||
game_bt,
|
||||
game_common,
|
||||
bluetooth,
|
||||
)
|
||||
|
||||
|
@ -33,10 +33,10 @@ def game(mac):
|
|||
logger.debug('pygame.init()')
|
||||
pygame.init()
|
||||
pygame.display.set_caption('tuxgo')
|
||||
pygame.mouse.set_visible(False)
|
||||
screen = pygame.display.set_mode(flags=pygame.FULLSCREEN)
|
||||
bot = bluetooth.BluetoothBot(mac)
|
||||
game = game_bt.Game(screen, bot)
|
||||
game.run()
|
||||
game_common.Game(screen, bot).run()
|
||||
|
||||
|
||||
@cli.command('debug-image')
|
||||
|
|
BIN
tuxgo/assets/fonts/Iosevka-Regular.ttf
Normal file
BIN
tuxgo/assets/fonts/Iosevka-Regular.ttf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/Pilowlava-Regular.otf
Normal file
BIN
tuxgo/assets/fonts/Pilowlava-Regular.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceGrotesk-Bold.otf
Normal file
BIN
tuxgo/assets/fonts/SpaceGrotesk-Bold.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceGrotesk-Light.otf
Normal file
BIN
tuxgo/assets/fonts/SpaceGrotesk-Light.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceGrotesk-Medium.otf
Normal file
BIN
tuxgo/assets/fonts/SpaceGrotesk-Medium.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceGrotesk-Regular.otf
Normal file
BIN
tuxgo/assets/fonts/SpaceGrotesk-Regular.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceGrotesk-SemiBold.otf
Normal file
BIN
tuxgo/assets/fonts/SpaceGrotesk-SemiBold.otf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceMono-Bold.ttf
Normal file
BIN
tuxgo/assets/fonts/SpaceMono-Bold.ttf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceMono-BoldItalic.ttf
Normal file
BIN
tuxgo/assets/fonts/SpaceMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceMono-Regular.ttf
Normal file
BIN
tuxgo/assets/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
tuxgo/assets/fonts/SpaceMono-RegularItalic.ttf
Normal file
BIN
tuxgo/assets/fonts/SpaceMono-RegularItalic.ttf
Normal file
Binary file not shown.
92
tuxgo/camera.py
Normal file
92
tuxgo/camera.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2023 Wojtek Porczyk <woju@hackerspace.pl>
|
||||
|
||||
import numpy as np
|
||||
import picamera2 # pylint: disable=import-error
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def get_char_for_pixel(pixel):
|
||||
s = sum(pixel)
|
||||
if s > 614:
|
||||
return '\u2588'
|
||||
if s > 460:
|
||||
return '\u2593'
|
||||
if s > 307:
|
||||
return '\u2592'
|
||||
if s > 153:
|
||||
return '\u2591'
|
||||
return ' '
|
||||
|
||||
|
||||
def print_frame_to_console(frame):
|
||||
for row in frame:
|
||||
print(''.join(get_char_for_pixel(cell) for cell in row))
|
||||
print()
|
||||
|
||||
|
||||
class Camera:
|
||||
def __init__(self, size):
|
||||
self._exposure = 1.0
|
||||
|
||||
size = tuple(map(int, size))
|
||||
self.camera = picamera2.Picamera2()
|
||||
self.camera.preview_configuration.update(
|
||||
{
|
||||
'main': {
|
||||
'format': 'BGR888',
|
||||
'size': size,
|
||||
},
|
||||
'lores': None,
|
||||
'raw': None,
|
||||
'display': None,
|
||||
'encode': None,
|
||||
'controls': {
|
||||
'ExposureValue': self._exposure,
|
||||
},
|
||||
}
|
||||
)
|
||||
self.camera.configure('preview')
|
||||
|
||||
# ExposureValue vanishes from controls for some reason
|
||||
@property
|
||||
def exposure(self):
|
||||
return self._exposure
|
||||
|
||||
@exposure.setter
|
||||
def exposure(self, value):
|
||||
self._exposure = max(-8, min(8, value))
|
||||
self.camera.controls.ExposureValue = self._exposure
|
||||
|
||||
def __enter__(self):
|
||||
self.camera.start()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||
try:
|
||||
self.camera.stop()
|
||||
except SystemError: # raised on double stop
|
||||
pass
|
||||
|
||||
def resize(self, size):
|
||||
logger.debug(f'resize({size=})')
|
||||
|
||||
# this is in picamera2.py:
|
||||
# if size[0] % 2 or size[1] % 2:
|
||||
# raise RuntimeError("width and height should be even")
|
||||
x, y = map(int, size)
|
||||
x -= x & 1
|
||||
y -= y & 1
|
||||
self.camera.preview_configuration.update({'size': (x, y)})
|
||||
self.camera.switch_mode('preview')
|
||||
|
||||
def capture_frame(self, rotate=0):
|
||||
'''returns (array, metadata)
|
||||
|
||||
array has shape (height, width, 3)
|
||||
'''
|
||||
request = self.camera.capture_request()
|
||||
frame = request.make_array('main')
|
||||
metadata = picamera2.Metadata(request.get_metadata())
|
||||
request.release()
|
||||
return np.rot90(frame, rotate), metadata
|
499
tuxgo/game_bt.py
499
tuxgo/game_bt.py
|
@ -1,499 +0,0 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2023 Wojtek Porczyk <woju@hackerspace.pl>
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
import funcparserlib.parser
|
||||
import numpy as np
|
||||
import picamera2
|
||||
import pygame
|
||||
import pygame.camera
|
||||
import pygame.display
|
||||
import pygame.event
|
||||
import pygame.surfarray
|
||||
import pygame.time
|
||||
|
||||
from . import (
|
||||
assets,
|
||||
badges,
|
||||
camera,
|
||||
detector as _detector,
|
||||
game_base,
|
||||
parser,
|
||||
utils,
|
||||
)
|
||||
|
||||
from .theme import Colour
|
||||
|
||||
|
||||
|
||||
PYGAME_EVENT_UPDATE_CPU_LOAD = pygame.event.custom_type()
|
||||
|
||||
|
||||
class RobotComponent(game_base.Component):
|
||||
def add_widgets(self):
|
||||
if self.game.bot.connected:
|
||||
self.game.widgets.label('CONNECTED',
|
||||
bottom=2, left=0, bg=Colour.GREEN2)
|
||||
self.game.widgets.key('WSAD', 'Move',
|
||||
bottom=1, grid_left=0)
|
||||
self.game.widgets.key('E', 'Sonar',
|
||||
bottom=1, grid_left=1)
|
||||
|
||||
if self.game.bot.last_range is not None:
|
||||
self.game.widgets.label(f'RANGE: {self.game.bot.last_range:3d} cm',
|
||||
bottom=2, left=280)
|
||||
elif self.game.bot.loop is not None:
|
||||
self.game.widgets.label('CONNECTING',
|
||||
bottom=2, left=0)
|
||||
else:
|
||||
self.game.widgets.label('DISCONNECTED',
|
||||
bottom=2, left=0, bg=Colour.RED)
|
||||
self.game.widgets.key('R', 'Reconnect',
|
||||
bottom=1, grid_left=0)
|
||||
|
||||
def handle_event(self, event):
|
||||
if event.type != pygame.KEYDOWN:
|
||||
return False
|
||||
|
||||
if self.game.bot.connected:
|
||||
if event.key == pygame.K_w:
|
||||
self.game.bot.forward_nowait()
|
||||
return True
|
||||
if event.key == pygame.K_s:
|
||||
self.game.bot.backward_nowait()
|
||||
return True
|
||||
if event.key == pygame.K_a:
|
||||
self.game.bot.turn_left_nowait()
|
||||
return True
|
||||
if event.key == pygame.K_d:
|
||||
self.game.bot.turn_right_nowait()
|
||||
return True
|
||||
if event.key == pygame.K_e:
|
||||
self.game.bot.sonar_nowait()
|
||||
return True
|
||||
|
||||
elif self.game.bot.loop is None:
|
||||
if event.key == pygame.K_r:
|
||||
self.game.bot.connect()
|
||||
return True
|
||||
|
||||
class BadgesComponent(Component):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
self.enabled = not all(badge is None for badge in self.game.badges)
|
||||
|
||||
def add_widgets(self):
|
||||
if self.enabled:
|
||||
self.game.widgets.key('B', 'Badges', bottom=1, grid_left=3)
|
||||
|
||||
def handle_event(self, event):
|
||||
if (self.enabled
|
||||
and event.type == pygame.KEYDOWN
|
||||
and event.key == pygame.K_b):
|
||||
self.game.mode = self.game.mode_badges
|
||||
return True
|
||||
|
||||
class ClockComponent(Component):
|
||||
def add_widgets(self):
|
||||
self.game.widgets.label(f'{datetime.datetime.now():%H:%M}',
|
||||
top=0, right=0)
|
||||
|
||||
class CPUComponent(Component):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
self.cpuload = utils.CPULoad()
|
||||
self.cpuload.update()
|
||||
pygame.time.set_timer(PYGAME_EVENT_UPDATE_CPU_LOAD, 1000)
|
||||
|
||||
def add_widgets(self):
|
||||
self.game.widgets.label(self.cpuload.format_all_short(), top=1, left=0)
|
||||
|
||||
def handle_event(self, event):
|
||||
if event.type == PYGAME_EVENT_UPDATE_CPU_LOAD:
|
||||
self.cpuload.update()
|
||||
return True
|
||||
|
||||
class PWMComponent(Component):
|
||||
def __init__(self, *args, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
self.pwm = utils.PWM()
|
||||
self.pwm.period = 1000000 # ns -> 1 kHz
|
||||
self.pwm.duty_cycle = 0
|
||||
self.pwm.enable = 1
|
||||
|
||||
def add_widgets(self):
|
||||
self.game.widgets.key('[]', 'Lamp', bottom=0, grid_left=2)
|
||||
|
||||
def handle_event(self, event):
|
||||
if event.type != pygame.KEYDOWN:
|
||||
return
|
||||
|
||||
if event.key == pygame.K_LEFTBRACKET:
|
||||
self.pwm.duty_cycle = max(0, self.pwm.duty_cycle - 100000)
|
||||
return True
|
||||
if event.key == pygame.K_RIGHTBRACKET:
|
||||
self.pwm.duty_cycle = min(1000000, self.pwm.duty_cycle + 100000)
|
||||
return True
|
||||
|
||||
class RFKillComponent(Component):
|
||||
def add_widgets(self):
|
||||
if not utils.get_rfkill_state('phy0'):
|
||||
self.game.widgets.label('WIFI NOT RFKILL\'D', bottom=3, left=0, bg=Colour.RED)
|
||||
|
||||
class Game:
|
||||
def __init__(self, screen, bot):
|
||||
self.screen = screen
|
||||
self.screen_width, self.screen_height = self.screen.get_size()
|
||||
self.font_vcr_60 = pygame.font.Font(assets / 'fonts/VCROCDFaux.ttf', 60)
|
||||
self.widgets = game_base.Widgets(self.screen,
|
||||
grid_left=(80, 280, 450, 580),
|
||||
grid_right=(),
|
||||
)
|
||||
self.clock = pygame.time.Clock()
|
||||
|
||||
self.mode = self.mode_normal
|
||||
|
||||
self.bot = bot
|
||||
|
||||
self.camera = camera.Camera(
|
||||
(self.screen_width * 1.5, self.screen_height * 1.5))
|
||||
self.detector = _detector.Detector()
|
||||
self.frame = None
|
||||
self.metadata = None
|
||||
|
||||
self.badges = list(badges.Badge.load_from_assets())
|
||||
self.current_badge = None
|
||||
|
||||
self.comp_clock = ClockComponent(self)
|
||||
self.comp_robot = RobotComponent(self)
|
||||
self.comp_badges = BadgesComponent(self)
|
||||
self.comp_cpu = CPUComponent(self)
|
||||
self.comp_pwm = PWMComponent(self)
|
||||
self.comp_rfkill = RFKillComponent(self)
|
||||
|
||||
|
||||
def run(self):
|
||||
with self.camera:
|
||||
try:
|
||||
while True:
|
||||
self.mode()
|
||||
pygame.display.flip()
|
||||
self.clock.tick(50)
|
||||
|
||||
finally:
|
||||
self.comp_pwm.pwm.enable = 0
|
||||
|
||||
def capture_frame(self):
|
||||
self.frame, self.metadata = self.camera.capture_frame()
|
||||
|
||||
#
|
||||
# COMPONENTS AND EVENT HANDLING
|
||||
#
|
||||
|
||||
def handle_exit(self, event):
|
||||
if (event.type == pygame.QUIT
|
||||
or (event.type == pygame.KEYDOWN and event.key == pygame.K_q)):
|
||||
if self.bot.loop is not None:
|
||||
self.bot.disconnect()
|
||||
sys.exit()
|
||||
|
||||
def handle_event(self, components, event):
|
||||
self.handle_exit(event)
|
||||
|
||||
# CPULoad is a bit special, we need to always catch the timer event,
|
||||
# no matter which mode we're in
|
||||
if self.comp_cpu.handle_event(event):
|
||||
return
|
||||
|
||||
for component in components:
|
||||
if component.handle_event(event):
|
||||
break
|
||||
|
||||
def add_widgets(self, components):
|
||||
for component in components:
|
||||
component.add_widgets()
|
||||
|
||||
#
|
||||
# SCREEN MODES
|
||||
#
|
||||
|
||||
def mode_normal(self):
|
||||
components = (
|
||||
self.comp_clock,
|
||||
self.comp_robot,
|
||||
self.comp_badges,
|
||||
self.comp_cpu,
|
||||
self.comp_pwm,
|
||||
self.comp_rfkill,
|
||||
)
|
||||
|
||||
self.capture_frame()
|
||||
display_frame = self.frame.copy()
|
||||
|
||||
detections = self.detector.detect_markers(self.frame)
|
||||
analyser = _detector.Analyser(detections)
|
||||
for detection in detections:
|
||||
detection.debug_draw(display_frame)
|
||||
try:
|
||||
_programme = list(analyser.get_programme(debug_frame=display_frame))
|
||||
except _detector.BoardError:
|
||||
error = True
|
||||
else:
|
||||
error = False
|
||||
|
||||
# surface = pygame.surfarray.make_surface(frame.T)
|
||||
|
||||
# XXX WTF this math, TODO get understanding
|
||||
surface = pygame.surfarray.make_surface(np.flipud(np.rot90(display_frame)))
|
||||
surface = pygame.transform.scale(surface, self.screen.get_size())
|
||||
frame_width, frame_height = surface.get_size()
|
||||
# logger.debug(f'{surface.get_size()=}')
|
||||
|
||||
self.screen.blit(surface, (
|
||||
(self.screen_width - frame_width) / 2,
|
||||
(self.screen_height - frame_height) / 2))
|
||||
|
||||
self.widgets.label(f'EXP: {self.metadata.ExposureTime:5} us (1/{1000000/self.metadata.ExposureTime:3.0f} s)',
|
||||
top=2, left=0)
|
||||
self.widgets.label(f'GAIN:'
|
||||
f' A{self.metadata.AnalogueGain:.1f}'
|
||||
f' D{self.metadata.DigitalGain:.1f}'
|
||||
f' R{self.metadata.ColourGains[0]:.1f}'
|
||||
f' B{self.metadata.ColourGains[1]:.1f}',
|
||||
top=3, left=0)
|
||||
self.widgets.label(f'BLUR: {self.detector.blur} px',
|
||||
top=1, right=0)
|
||||
self.widgets.label(f'FPS: {self.clock.get_fps():4.1f} Hz',
|
||||
top=0, left=0)
|
||||
|
||||
# self.widgets.label(f'SHUTTER: ',
|
||||
# top=2, left=0)
|
||||
self.widgets.label(f'ILLUM: {self.metadata.Lux:4.0f} lux',
|
||||
top=3, right=0)
|
||||
self.widgets.label(f'PWM: {self.comp_pwm.pwm.percent:3.0f} %',
|
||||
top=2, right=0)
|
||||
|
||||
self.widgets.label(f'TEMP: {self.metadata.SensorTemperature:2.0f} °C',
|
||||
top=0, right=100)
|
||||
|
||||
|
||||
|
||||
if error:
|
||||
self.widgets.label('ERROR', bottom=2, right=0, bg=Colour.RED)
|
||||
else:
|
||||
self.widgets.label('OK', bottom=2, right=0)
|
||||
|
||||
self.widgets.key('SPC', 'Snapshot', bottom=0, grid_left=0)
|
||||
|
||||
# self.widgets.key('C', 'Cam. sett.', bottom=1, left=600)
|
||||
# self.widgets.key('I', 'Load file', bottom=0, left=600)
|
||||
self.widgets.key('+-', 'Blur', bottom=1, grid_left=2)
|
||||
|
||||
self.widgets.key('Q', 'Quit', bottom=0, grid_left=3)
|
||||
|
||||
self.add_widgets(components)
|
||||
# for i, item in enumerate(self.metadata.__dict__.items()):
|
||||
# k, v = item
|
||||
# self.widgets.label(f'{k}: {v}', left=0, top=i + 3)
|
||||
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(components, event)
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_SPACE:
|
||||
self.mode = self.mode_snapshot
|
||||
if event.key == pygame.K_c:
|
||||
self.mode = self.mode_calibrate
|
||||
|
||||
if event.key == pygame.K_MINUS:
|
||||
self.detector.blur = max(1, self.detector.blur - 1)
|
||||
if event.key in (pygame.K_PLUS, pygame.K_EQUALS):
|
||||
self.detector.blur += 1
|
||||
|
||||
|
||||
def mode_calibrate(self):
|
||||
components = (
|
||||
self.comp_clock,
|
||||
)
|
||||
display_frame = self.frame.copy()
|
||||
|
||||
detections = camera.detect_calib_board(self.frame)
|
||||
|
||||
# XXX WTF this math, TODO get understanding
|
||||
surface = pygame.surfarray.make_surface(np.flipud(np.rot90(display_frame)))
|
||||
surface = pygame.transform.scale(surface, self.screen.get_size())
|
||||
frame_width, frame_height = surface.get_size()
|
||||
|
||||
self.screen.blit(surface, (
|
||||
(self.screen_width - frame_width) / 2,
|
||||
(self.screen_height - frame_height) / 2))
|
||||
|
||||
self.add_widgets(components)
|
||||
self.widgets.key('C', 'Calibrate', bottom=0, grid_left=0)
|
||||
self.widgets.key('Q', 'Quit', bottom=0, grid_left=3)
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(components, event)
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self.mode = self.mode_normal
|
||||
|
||||
if event.key == pygame.K_c:
|
||||
self.capture_frame()
|
||||
|
||||
def mode_snapshot(self):
|
||||
components = (
|
||||
self.comp_clock,
|
||||
self.comp_robot,
|
||||
self.comp_badges,
|
||||
self.comp_pwm,
|
||||
self.comp_rfkill,
|
||||
)
|
||||
|
||||
display_frame = self.frame.copy()
|
||||
detections = self.detector.detect_markers(self.frame)
|
||||
analyser = _detector.Analyser(detections)
|
||||
for detection in detections:
|
||||
detection.debug_draw(display_frame)
|
||||
try:
|
||||
programme = list(analyser.get_programme(debug_frame=display_frame))
|
||||
parsed = parser.parse(programme)
|
||||
|
||||
except (_detector.BoardError, funcparserlib.parser.NoParseError):
|
||||
error = True
|
||||
else:
|
||||
error = False
|
||||
|
||||
# surface = pygame.surfarray.make_surface(frame.T)
|
||||
|
||||
# XXX WTF this math, TODO get understanding
|
||||
surface = pygame.surfarray.make_surface(np.flipud(np.rot90(display_frame)))
|
||||
surface = pygame.transform.scale(surface, self.screen.get_size())
|
||||
frame_width, frame_height = surface.get_size()
|
||||
# logger.debug(f'{surface.get_size()=}')
|
||||
|
||||
self.screen.blit(surface, (
|
||||
(self.screen_width - frame_width) / 2,
|
||||
(self.screen_height - frame_height) / 2))
|
||||
|
||||
self.add_widgets(components)
|
||||
self.widgets.key('SPC', 'Snapshot', bottom=0, grid_left=0)
|
||||
self.widgets.key('Q', 'Quit', bottom=0, grid_left=3)
|
||||
|
||||
if error:
|
||||
self.widgets.label('ERROR', bottom=2, right=0, bg=Colour.RED)
|
||||
else:
|
||||
for i, line in enumerate(programme, 1):
|
||||
line = ' '.join(token.value.text for token in line)
|
||||
self.widgets.label(f'{i:2d} {line}', top=i, left=0)
|
||||
self.widgets.label('OK', bottom=2, right=0)
|
||||
|
||||
if self.bot.connected:
|
||||
if self.bot.current_task is not None:
|
||||
self.widgets.key('X', 'Stop', bottom=0, grid_left=1)
|
||||
elif not error:
|
||||
self.widgets.key('R', 'Run', bottom=0, grid_left=1)
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(components, event)
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self.mode = self.mode_normal
|
||||
|
||||
if event.key == pygame.K_SPACE:
|
||||
self.capture_frame()
|
||||
|
||||
if self.bot.connected:
|
||||
if self.bot.current_task is not None:
|
||||
if event.key == pygame.K_x:
|
||||
self.bot.stop_programme()
|
||||
elif not error:
|
||||
if event.key == pygame.K_r:
|
||||
self.bot.execute_programme(parsed)
|
||||
|
||||
def mode_badges(self):
|
||||
self.screen.fill((0, 0, 0))
|
||||
|
||||
image_size = 256
|
||||
assert self.screen_height == 1280
|
||||
assert self.screen_width == 720
|
||||
|
||||
padding = (self.screen_width - 2 * image_size) / 3
|
||||
|
||||
for i, badge in enumerate(self.badges):
|
||||
if badge is None:
|
||||
continue
|
||||
|
||||
x = padding + (image_size + padding) * (i // 5)
|
||||
y = image_size * (i % 5)
|
||||
|
||||
image = pygame.transform.scale(
|
||||
badge.image_menu or badge.image, (image_size, image_size))
|
||||
|
||||
label = self.font_vcr_60.render(
|
||||
str((i + 1) % 10), True, (0, 0, 0), (255, 255, 255))
|
||||
|
||||
self.screen.blit(image, (x, y))
|
||||
self.screen.blit(label, (x - label.get_width() - 16, y))
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event((), event)
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
self.mode = self.mode_normal
|
||||
continue
|
||||
|
||||
for i in range(min(len(self.badges), 10)):
|
||||
if event.key == getattr(pygame, f'K_{(i + 1) % 10}'):
|
||||
if self.badges[i] is not None:
|
||||
self.mode = self.mode_badge
|
||||
self.current_badge = self.badges[i]
|
||||
|
||||
def mode_badge(self):
|
||||
assert self.current_badge
|
||||
|
||||
padding = 48
|
||||
|
||||
self.screen.fill((0, 0, 0))
|
||||
|
||||
qrcode_size = (self.screen_height
|
||||
- 3 * padding - self.current_badge.image.get_height())
|
||||
qrcode = pygame.transform.scale(
|
||||
self.current_badge.qrcode, (qrcode_size, qrcode_size))
|
||||
|
||||
self.screen.blit(qrcode,
|
||||
((self.screen_width - qrcode_size) / 2, padding))
|
||||
self.screen.blit(self.current_badge.image, (
|
||||
(self.screen_width - self.current_badge.image.get_width()) / 2,
|
||||
(self.screen_height - self.current_badge.image.get_height() - padding)
|
||||
))
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event((), event)
|
||||
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key in (pygame.K_ESCAPE, pygame.K_b):
|
||||
self.mode = self.mode_badges
|
||||
self.current_badge = None
|
||||
continue
|
||||
|
||||
for i in range(min(len(self.badges), 10)):
|
||||
if event.key == getattr(pygame, f'K_{(i + 1) % 10}'):
|
||||
if self.badges[i] is not None:
|
||||
self.current_badge = self.badges[i]
|
||||
|
||||
def mode_camera_settings(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def mode_load_file(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# vim: tw=80 ts=4 sts=4 sw=4 et
|
956
tuxgo/game_common.py
Normal file
956
tuxgo/game_common.py
Normal file
|
@ -0,0 +1,956 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# SPDX-FileCopyrightText: 2023-2024 Wojtek Porczyk <woju@hackerspace.pl>
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import cv2
|
||||
import funcparserlib.parser
|
||||
import pygame
|
||||
|
||||
from . import (
|
||||
assets,
|
||||
badges,
|
||||
camera,
|
||||
detector as _detector,
|
||||
parser,
|
||||
utils,
|
||||
)
|
||||
from .theme import Colour
|
||||
|
||||
PYGAME_EVENT_UPDATE_CPU_LOAD = pygame.event.custom_type()
|
||||
|
||||
|
||||
class Layout:
|
||||
layout_width = 6
|
||||
layout_height = 2
|
||||
|
||||
def __init__(self, screen):
|
||||
self.screen = screen
|
||||
self.screen_width, self.screen_height = self.screen.get_size()
|
||||
assert self.screen_width >= 720
|
||||
self.button_size = self.screen_width // self.layout_width
|
||||
|
||||
self.font = pygame.font.Font(
|
||||
assets / 'fonts/SpaceGrotesk-Regular.otf', 36
|
||||
)
|
||||
self.font_mono = pygame.font.Font(
|
||||
assets / 'fonts/Iosevka-Regular.ttf', 24
|
||||
)
|
||||
self.font_key = pygame.font.Font(
|
||||
assets / 'fonts/Pilowlava-Regular.otf', 56
|
||||
)
|
||||
|
||||
def place_button(
|
||||
self, *, left=None, right=None, top=None, bottom=None, width=1, height=1
|
||||
):
|
||||
assert (left is not None) ^ (right is not None)
|
||||
assert (top is not None) ^ (bottom is not None)
|
||||
|
||||
if left is not None:
|
||||
x = left * self.button_size
|
||||
if right is not None:
|
||||
x = self.screen_width - (left + width) * self.button_size
|
||||
if top is not None:
|
||||
y = top * self.button_size
|
||||
if bottom is not None:
|
||||
y = self.screen_height - (bottom + height) * self.button_size
|
||||
|
||||
return {
|
||||
'left': x,
|
||||
'top': y,
|
||||
'width': width * self.button_size,
|
||||
'height': height * self.button_size,
|
||||
}
|
||||
|
||||
|
||||
class Control:
|
||||
def __init__(self, game):
|
||||
self.game = game
|
||||
|
||||
def handle_event(self, event):
|
||||
pass
|
||||
|
||||
def get_blits(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Status(Control):
|
||||
def get_blits(self):
|
||||
surface = self.game.layout.font_mono.render(
|
||||
f' FPS: {self.game.clock.get_fps():4.1f} Hz '
|
||||
f' {datetime.datetime.now():%H:%M} ',
|
||||
True,
|
||||
Colour.ACCENT3,
|
||||
Colour.BACKGROUND,
|
||||
)
|
||||
yield surface, (self.game.layout.screen_width - surface.get_width(), 0)
|
||||
|
||||
|
||||
class Sonar(Control):
|
||||
def get_blits(self):
|
||||
if self.game.bot.last_range is None:
|
||||
return
|
||||
|
||||
surface = self.game.layout.font_mono.render(
|
||||
(
|
||||
' \N{INFINITY} '
|
||||
if self.game.bot.last_range == float('inf')
|
||||
else f' {self.game.bot.last_range:3} cm '
|
||||
),
|
||||
True,
|
||||
Colour.ACCENT2,
|
||||
Colour.BACKGROUND,
|
||||
)
|
||||
|
||||
yield surface, (
|
||||
int(self.game.layout.button_size * 3.5 - surface.get_width() / 2),
|
||||
self.game.layout.screen_height
|
||||
- self.game.layout.button_size * 2
|
||||
- surface.get_height()
|
||||
- 5,
|
||||
)
|
||||
|
||||
|
||||
class Debug(Control):
|
||||
def label(
|
||||
self,
|
||||
text,
|
||||
*,
|
||||
left,
|
||||
top,
|
||||
color=Colour.ACCENT3,
|
||||
background=Colour.BACKGROUND,
|
||||
):
|
||||
surface = self.game.layout.font_mono.render(
|
||||
text, True, color, background
|
||||
)
|
||||
return surface, (left, top * surface.get_height())
|
||||
|
||||
def get_blits(self):
|
||||
yield self.label(
|
||||
f'EXP:'
|
||||
f' {self.game.metadata.ExposureTime:5} us'
|
||||
f' (1/{1000000/self.game.metadata.ExposureTime:3.0f} s)',
|
||||
top=2,
|
||||
left=0,
|
||||
)
|
||||
yield self.label(
|
||||
f'GAIN:'
|
||||
f' A{self.game.metadata.AnalogueGain:.1f}'
|
||||
f' D{self.game.metadata.DigitalGain:.1f}'
|
||||
f' R{self.game.metadata.ColourGains[0]:.1f}'
|
||||
f' B{self.game.metadata.ColourGains[1]:.1f}',
|
||||
top=3,
|
||||
left=0,
|
||||
)
|
||||
yield self.label(
|
||||
f'ILLUM: {self.game.metadata.Lux:4.0f} lux', top=3, left=500
|
||||
)
|
||||
|
||||
yield self.label(
|
||||
f'TEMP: {self.game.metadata.SensorTemperature:2.0f} °C',
|
||||
top=0,
|
||||
left=100,
|
||||
)
|
||||
|
||||
height, width, _ = self.game.frame.shape
|
||||
|
||||
yield self.label(f'W: {width} × H: {height}', top=1, left=0)
|
||||
|
||||
|
||||
class RectControl(Control):
|
||||
def __init__(self, *args, left, top, width, height, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
self.rect = pygame.Rect(left, top, width, height)
|
||||
self.uv = self.rect.move(-self.rect.x, -self.rect.y)
|
||||
|
||||
def get_blits(self):
|
||||
surface = pygame.Surface(self.rect.size, pygame.SRCALPHA)
|
||||
self.draw(surface)
|
||||
yield surface, self.rect.topleft
|
||||
|
||||
def draw(self, surface):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class BaseButton(RectControl):
|
||||
key = None
|
||||
|
||||
def __init__(self, *args, key=None, on_press=None, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
|
||||
if key is not None:
|
||||
self.key = key
|
||||
if on_press is not None:
|
||||
self.on_press = on_press
|
||||
|
||||
def on_press(self, pos): # pylint: disable=method-hidden
|
||||
raise NotImplementedError()
|
||||
|
||||
def handle_event(self, event):
|
||||
if (
|
||||
event.type == pygame.MOUSEBUTTONDOWN
|
||||
and event.button == 1
|
||||
and self.rect.collidepoint(event.pos)
|
||||
) or (event.type == pygame.KEYDOWN and event.key == self.key):
|
||||
return self.on_press(event.pos)
|
||||
return None
|
||||
|
||||
|
||||
class Button(BaseButton):
|
||||
# pylint: disable=abstract-method
|
||||
label = None
|
||||
|
||||
def __init__(self, *args, on_press=None, label=None, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
|
||||
if label is not None:
|
||||
self.label = label
|
||||
|
||||
def get_blits(self):
|
||||
# for the interest of borders, we need to draw a bit bigger rectangle
|
||||
rect = pygame.Rect(
|
||||
self.rect.x - 1,
|
||||
self.rect.y - 1,
|
||||
self.rect.width + 2,
|
||||
self.rect.height + 2,
|
||||
)
|
||||
|
||||
if self.rect.left == 0:
|
||||
rect.x = 0
|
||||
rect.width -= 1
|
||||
if self.rect.right == self.game.screen_width:
|
||||
rect.width -= 1
|
||||
if self.rect.bottom == self.game.screen_height:
|
||||
rect.height -= 1
|
||||
|
||||
surface = pygame.Surface(rect.size)
|
||||
self.draw(surface)
|
||||
yield surface, rect.topleft
|
||||
|
||||
def draw_label(self, surface):
|
||||
fontsurf = self.game.layout.font.render(
|
||||
self.label, True, Colour.PRIMARY
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(
|
||||
fontsurf,
|
||||
(
|
||||
(self.rect.width - fontrect.width) // 2,
|
||||
(self.rect.height - fontrect.height) // 2,
|
||||
),
|
||||
)
|
||||
|
||||
def draw_key(self, surface):
|
||||
fontsurf = self.game.layout.font_key.render(
|
||||
self.key, True, Colour.ACCENT5, Colour.BACKGROUND
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(
|
||||
fontsurf,
|
||||
(
|
||||
(self.rect.width - fontrect.width) // 2,
|
||||
(self.rect.height - fontrect.height) // 2,
|
||||
),
|
||||
)
|
||||
|
||||
def draw(self, surface):
|
||||
surface.fill(Colour.BACKGROUND)
|
||||
pygame.draw.rect(surface, Colour.ACCENT3, surface.get_rect(), width=2)
|
||||
|
||||
if self.key is not None:
|
||||
self.draw_key(surface)
|
||||
|
||||
self.draw_label(surface)
|
||||
|
||||
|
||||
class UpButton(BaseButton):
|
||||
# pylint: disable=abstract-method
|
||||
def draw(self, surface):
|
||||
surface.fill(Colour.BACKGROUND)
|
||||
pygame.draw.lines(
|
||||
surface,
|
||||
Colour.ACCENT3,
|
||||
False,
|
||||
(
|
||||
(self.rect.width * 0.2, self.rect.height - 1),
|
||||
(0, self.rect.height - 1),
|
||||
(0, 0),
|
||||
(self.rect.width - 2, 0),
|
||||
(self.rect.width - 2, self.rect.height - 1),
|
||||
(self.rect.width * 0.8, self.rect.height - 1),
|
||||
),
|
||||
width=2,
|
||||
)
|
||||
|
||||
fontsurf = self.game.layout.font.render(
|
||||
'+', True, Colour.ACCENT1, Colour.BACKGROUND
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(fontsurf, ((self.rect.width - fontrect.width) // 2, 2))
|
||||
|
||||
|
||||
class DownButton(BaseButton):
|
||||
# pylint: disable=abstract-method
|
||||
def draw(self, surface):
|
||||
surface.fill(Colour.BACKGROUND)
|
||||
pygame.draw.lines(
|
||||
surface,
|
||||
Colour.ACCENT3,
|
||||
False,
|
||||
(
|
||||
(self.rect.width * 0.2, 0),
|
||||
(0, 0),
|
||||
(0, self.rect.height - 2),
|
||||
(self.rect.width - 2, self.rect.height - 2),
|
||||
(self.rect.width - 2, 0),
|
||||
(self.rect.width * 0.8, 0),
|
||||
),
|
||||
width=2,
|
||||
)
|
||||
|
||||
fontsurf = self.game.layout.font.render(
|
||||
'-', True, Colour.ACCENT1, Colour.BACKGROUND
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(
|
||||
fontsurf,
|
||||
(
|
||||
(self.rect.width - fontrect.width) // 2,
|
||||
self.rect.height - fontrect.height - 2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Rocker(RectControl):
|
||||
label = None
|
||||
|
||||
def __init__(self, *args, key_up=None, key_down=None, **kwds):
|
||||
super().__init__(*args, **kwds)
|
||||
self.up = UpButton(
|
||||
self.game,
|
||||
left=self.rect.left,
|
||||
top=self.rect.top,
|
||||
width=self.rect.width,
|
||||
height=self.rect.centery - self.rect.top,
|
||||
key=key_up,
|
||||
on_press=self.on_press_up,
|
||||
)
|
||||
self.down = DownButton(
|
||||
self.game,
|
||||
left=self.rect.left,
|
||||
top=self.rect.centery,
|
||||
width=self.rect.width,
|
||||
height=self.rect.height - self.up.rect.height,
|
||||
key=key_down,
|
||||
on_press=self.on_press_down,
|
||||
)
|
||||
|
||||
def draw(self, surface):
|
||||
fontsurf = self.game.layout.font.render(
|
||||
self.label, True, Colour.PRIMARY, Colour.BACKGROUND
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(
|
||||
fontsurf,
|
||||
(
|
||||
self.uv.centerx - fontrect.width // 2,
|
||||
self.uv.centery - fontrect.height - 2,
|
||||
),
|
||||
)
|
||||
|
||||
fontsurf = self.game.layout.font_mono.render(
|
||||
self.get_value(), True, Colour.ACCENT2, Colour.BACKGROUND
|
||||
)
|
||||
fontrect = fontsurf.get_rect()
|
||||
surface.blit(
|
||||
fontsurf,
|
||||
(self.uv.centerx - fontrect.width // 2, self.uv.centery + 2),
|
||||
)
|
||||
|
||||
def get_value(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_blits(self):
|
||||
yield from self.up.get_blits()
|
||||
yield from self.down.get_blits()
|
||||
yield from super().get_blits()
|
||||
|
||||
def handle_event(self, event):
|
||||
return self.up.handle_event(event) or self.down.handle_event(event)
|
||||
|
||||
def on_press_up(self, pos):
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_press_down(self, pos):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class PWMRocker(Rocker):
|
||||
label = 'PWM'
|
||||
|
||||
def get_value(self):
|
||||
return f'{self.game.pwm.percent:3.0f} %'
|
||||
|
||||
def on_press_up(self, _pos):
|
||||
self.game.pwm.duty_cycle = min(
|
||||
1000000, self.game.pwm.duty_cycle + 100000
|
||||
)
|
||||
return True
|
||||
|
||||
def on_press_down(self, _pos):
|
||||
self.game.pwm.duty_cycle = max(0, self.game.pwm.duty_cycle - 100000)
|
||||
return True
|
||||
|
||||
|
||||
class BlurRocker(Rocker):
|
||||
label = 'BLUR'
|
||||
|
||||
def get_value(self):
|
||||
return f'{self.game.detector.blur} px'
|
||||
|
||||
def on_press_up(self, _pos):
|
||||
self.game.detector.blur += 1
|
||||
return True
|
||||
|
||||
def on_press_down(self, _pos):
|
||||
self.game.detector.blur = max(1, self.game.detector.blur - 1)
|
||||
return True
|
||||
|
||||
|
||||
class ExposureRocker(Rocker):
|
||||
label = 'EXPO'
|
||||
|
||||
def get_value(self):
|
||||
return (
|
||||
'±0.0 EV'
|
||||
if abs(self.game.camera.exposure) < 1e-5
|
||||
else (
|
||||
f'{self.game.camera.exposure:+3.1f}'
|
||||
' E\N{LATIN SUBSCRIPT SMALL LETTER V}'
|
||||
)
|
||||
)
|
||||
|
||||
def on_press_up(self, _pos):
|
||||
self.game.camera.exposure += 1 / 3
|
||||
return True
|
||||
|
||||
def on_press_down(self, _pos):
|
||||
self.game.camera.exposure -= 1 / 3
|
||||
return True
|
||||
|
||||
|
||||
class ResolutionRocker(Rocker):
|
||||
label = 'RES'
|
||||
|
||||
def get_value(self):
|
||||
return f'× {self.game.frame.shape[0] / self.game.screen_height:3.1f}'
|
||||
|
||||
def on_press_up(self, _pos):
|
||||
self.game.resolution_index += 1
|
||||
self.game.camera.resize(
|
||||
(
|
||||
self.game.screen_width * renard(self.game.resolution_index),
|
||||
self.game.screen_height * renard(self.game.resolution_index),
|
||||
)
|
||||
)
|
||||
|
||||
def on_press_down(self, _pos):
|
||||
self.game.resolution_index -= 1
|
||||
self.game.camera.resize(
|
||||
(
|
||||
self.game.screen_width * renard(self.game.resolution_index),
|
||||
self.game.screen_height * renard(self.game.resolution_index),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# def on_press_up(self, _pos):
|
||||
# ratio = self.game.frame.shape[0] / self.game.screen_height
|
||||
# ratio += 0.25
|
||||
# self.game.camera.resize(
|
||||
# (self.game.screen_width * ratio, self.game.screen_height * ratio))
|
||||
# return True
|
||||
|
||||
# def on_press_down(self, _pos):
|
||||
# ratio = self.game.frame.shape[0] / self.game.screen_height
|
||||
# ratio -= 0.25
|
||||
# self.game.camera.resize(
|
||||
# (self.game.screen_width * ratio, self.game.screen_height * ratio))
|
||||
# return True
|
||||
|
||||
|
||||
class ButtonBack(Button):
|
||||
label = 'BACK'
|
||||
|
||||
def on_press(self, _pos):
|
||||
self.game.mode = self.game.mode_normal
|
||||
return True
|
||||
|
||||
|
||||
class ButtonCallibrate(Button):
|
||||
label = 'ADJ'
|
||||
|
||||
def on_press(self, _pos):
|
||||
self.game.mode = self.game.mode_callibrate
|
||||
return True
|
||||
|
||||
|
||||
class ButtonQuit(Button):
|
||||
label = 'QUIT'
|
||||
key = 'Q'
|
||||
|
||||
def on_press(self, _pos):
|
||||
sys.exit()
|
||||
|
||||
|
||||
class ButtonConnect(Button):
|
||||
key = 'R'
|
||||
|
||||
def draw_label(self, surface):
|
||||
if self.game.bot.connected:
|
||||
self.label = 'CONNECTED'
|
||||
elif self.game.bot.loop is not None:
|
||||
self.label = 'CONNECTING'
|
||||
else:
|
||||
self.label = 'CONNECT'
|
||||
|
||||
return super().draw_label(surface)
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.loop is None:
|
||||
self.game.bot.connect()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonSonar(Button):
|
||||
label = 'SONAR'
|
||||
key = 'E'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.connected:
|
||||
self.game.bot.sonar_nowait()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonForward(Button):
|
||||
label = '↑'
|
||||
key = 'W'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.connected:
|
||||
self.game.bot.forward_nowait()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonBackward(Button):
|
||||
label = '↓'
|
||||
key = 'S'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.connected:
|
||||
self.game.bot.backward_nowait()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonTurnLeft(Button):
|
||||
label = '←'
|
||||
key = 'A'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.connected:
|
||||
self.game.bot.turn_left_nowait()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonTurnRight(Button):
|
||||
label = '→'
|
||||
key = 'D'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.bot.connected:
|
||||
self.game.bot.turn_right_nowait()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonCapture(Button):
|
||||
label = 'CAPTURE'
|
||||
key = 'SPACE'
|
||||
|
||||
def on_press(self, _pos):
|
||||
if self.game.mode == self.game.mode_snapshot:
|
||||
self.game.capture_frame()
|
||||
else:
|
||||
self.game.mode = self.game.mode_snapshot
|
||||
return True
|
||||
|
||||
|
||||
class ButtonRun(Button):
|
||||
label = 'RUN'
|
||||
key = 'R'
|
||||
|
||||
def on_press(self, _pos):
|
||||
self.game.bot.execute_programme(self.game.current_ast)
|
||||
return True
|
||||
|
||||
|
||||
class ButtonStop(Button):
|
||||
label = 'STOP'
|
||||
key = 'X'
|
||||
|
||||
def on_press(self, _pos):
|
||||
self.game.bot.stop_programme()
|
||||
return True
|
||||
|
||||
|
||||
class ButtonBadges(Button):
|
||||
label = 'B'
|
||||
key = 'B'
|
||||
|
||||
def on_press(self, _pos):
|
||||
# TODO check how many badges are there, and then B switches to either
|
||||
# mode_badges or just mode_badge with first one picked
|
||||
self.game.current_badge = self.game.badges[0]
|
||||
self.game.mode = self.game.mode_badge
|
||||
|
||||
|
||||
# fmt: off
|
||||
R20 = (
|
||||
1.00, 1.12, 1.25, 1.40,
|
||||
1.60, 1.80, 2.00, 2.24,
|
||||
2.50, 2.80, 3.15, 3.55,
|
||||
4.00, 4.50, 5.00, 5.60,
|
||||
6.30, 7.10, 8.00, 9.00,
|
||||
)
|
||||
R10 = R20[::2]
|
||||
R5 = R10[::2]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def renard(index, series=R10):
|
||||
magnitude, number = divmod(index, len(series))
|
||||
return series[number] * 10**magnitude
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, screen, bot):
|
||||
self.screen = screen
|
||||
self.screen_width, self.screen_height = self.screen.get_size()
|
||||
self.layout = Layout(self.screen)
|
||||
|
||||
self.clock = pygame.time.Clock()
|
||||
|
||||
self.mode = self.mode_normal
|
||||
|
||||
self.bot = bot
|
||||
|
||||
self.pwm = utils.PWM()
|
||||
self.pwm.period = 1000000 # ns -> 1 kHz
|
||||
self.pwm.duty_cycle = 0
|
||||
self.pwm.enable = 1
|
||||
|
||||
self.cpuload = utils.CPULoad()
|
||||
self.cpuload.update()
|
||||
pygame.time.set_timer(PYGAME_EVENT_UPDATE_CPU_LOAD, 1000)
|
||||
|
||||
self.resolution_index = 2
|
||||
self.camera = camera.Camera(
|
||||
(
|
||||
self.screen_width * renard(self.resolution_index),
|
||||
self.screen_height * renard(self.resolution_index),
|
||||
)
|
||||
)
|
||||
self.detector = _detector.Detector()
|
||||
self.frame = None
|
||||
self.metadata = None
|
||||
|
||||
self.current_programme = None
|
||||
self.current_ast = None
|
||||
|
||||
self.badges = list(badges.Badge.load_from_assets())
|
||||
self.current_badge = None
|
||||
|
||||
self.error = False
|
||||
|
||||
self.ctrl_pwm = PWMRocker(
|
||||
self, **self.layout.place_button(left=1, bottom=0, height=2)
|
||||
)
|
||||
self.ctrl_blur = BlurRocker(
|
||||
self, **self.layout.place_button(left=2, bottom=0, height=2)
|
||||
)
|
||||
self.ctrl_expo = ExposureRocker(
|
||||
self, **self.layout.place_button(left=3, bottom=0, height=2)
|
||||
)
|
||||
self.ctrl_res = ResolutionRocker(
|
||||
self, **self.layout.place_button(left=4, bottom=0, height=2)
|
||||
)
|
||||
|
||||
self.ctrl_button_back = ButtonBack(
|
||||
self, **self.layout.place_button(left=0, bottom=0)
|
||||
)
|
||||
|
||||
self.ctrl_button_callibrate = ButtonCallibrate(
|
||||
self, **self.layout.place_button(left=0, bottom=1)
|
||||
)
|
||||
self.ctrl_button_quit = ButtonQuit(
|
||||
self, **self.layout.place_button(left=0, bottom=0)
|
||||
)
|
||||
self.ctrl_button_capture = ButtonCapture(
|
||||
self, **self.layout.place_button(left=4, bottom=0, width=2)
|
||||
)
|
||||
self.ctrl_button_badges = ButtonBadges(
|
||||
self, **self.layout.place_button(left=1, bottom=1)
|
||||
)
|
||||
|
||||
self.ctrl_button_connect = ButtonConnect(
|
||||
self, **self.layout.place_button(left=1, bottom=0, width=3)
|
||||
)
|
||||
self.ctrl_button_turn_left = ButtonTurnLeft(
|
||||
self, **self.layout.place_button(left=1, bottom=0)
|
||||
)
|
||||
self.ctrl_button_forward = ButtonForward(
|
||||
self, **self.layout.place_button(left=2, bottom=1)
|
||||
)
|
||||
self.ctrl_button_backward = ButtonBackward(
|
||||
self, **self.layout.place_button(left=2, bottom=0)
|
||||
)
|
||||
self.ctrl_button_turn_right = ButtonTurnRight(
|
||||
self, **self.layout.place_button(left=3, bottom=0)
|
||||
)
|
||||
self.ctrl_button_sonar = ButtonSonar(
|
||||
self, **self.layout.place_button(left=3, bottom=1)
|
||||
)
|
||||
|
||||
self.ctrl_button_run = ButtonRun(
|
||||
self, **self.layout.place_button(left=4, bottom=1, width=2)
|
||||
)
|
||||
self.ctrl_button_stop = ButtonStop(
|
||||
self,
|
||||
**self.layout.place_button(left=0, bottom=0, width=6, height=2),
|
||||
)
|
||||
|
||||
self.ctrl_status = Status(self)
|
||||
self.ctrl_debug = Debug(self)
|
||||
self.ctrl_sonar = Sonar(self)
|
||||
|
||||
def run(self):
|
||||
with self.camera:
|
||||
try:
|
||||
while True:
|
||||
self.mode()
|
||||
pygame.display.flip()
|
||||
self.clock.tick(50)
|
||||
|
||||
except Exception as exc:
|
||||
# TODO make this configurable
|
||||
if self.frame is not None:
|
||||
now = f'{datetime.datetime.now():%Y%m%d-%H%M%S}'
|
||||
cv2.imwrite(f'badframe-{now}.png', self.frame)
|
||||
with open(
|
||||
f'badframe-{now}.txt', 'w', encoding='utf-8'
|
||||
) as file:
|
||||
traceback.print_exception(exc, file=file)
|
||||
raise
|
||||
|
||||
finally:
|
||||
if self.bot.loop is not None:
|
||||
self.bot.disconnect()
|
||||
self.pwm.enable = 0
|
||||
|
||||
def capture_frame(self):
|
||||
# NOTE about frames, arrays and shapes:
|
||||
# picamera's make_array returns array of shape (height, width, 3)
|
||||
# array[0][0] is top left, but the camera is mounted "upside down" in
|
||||
# the box I'm using, therefore rotate 180°
|
||||
self.frame, self.metadata = self.camera.capture_frame(rotate=2)
|
||||
|
||||
def handle_event(self, ui, event):
|
||||
if event.type == pygame.QUIT or (
|
||||
event.type == pygame.KEYDOWN and event.key == pygame.K_q
|
||||
):
|
||||
sys.exit()
|
||||
|
||||
if event.type == PYGAME_EVENT_UPDATE_CPU_LOAD:
|
||||
self.cpuload.update()
|
||||
return True
|
||||
|
||||
return any(ctrl.handle_event(event) for ctrl in ui)
|
||||
|
||||
def draw_ui(self, ui):
|
||||
self.screen.blits(
|
||||
list(itertools.chain.from_iterable(ctrl.get_blits() for ctrl in ui))
|
||||
)
|
||||
|
||||
def compile_display(self):
|
||||
display_frame = self.frame.copy()
|
||||
|
||||
detections = self.detector.detect_markers(self.frame)
|
||||
analyser = _detector.Analyser(detections)
|
||||
for detection in detections:
|
||||
detection.debug_draw(display_frame)
|
||||
try:
|
||||
self.current_programme = list(
|
||||
analyser.get_programme(debug_frame=display_frame)
|
||||
)
|
||||
self.current_ast = parser.parse(self.current_programme)
|
||||
except (_detector.BoardError, funcparserlib.parser.NoParseError):
|
||||
self.current_programme = None
|
||||
self.current_ast = None
|
||||
self.error = True
|
||||
else:
|
||||
self.error = False
|
||||
|
||||
# pygame's surface needs shape (width, height, 3), hence transpose
|
||||
surface = pygame.pixelcopy.make_surface(
|
||||
display_frame.transpose(1, 0, 2)
|
||||
)
|
||||
surface = pygame.transform.scale(surface, self.screen.get_size())
|
||||
frame_width, frame_height = surface.get_size()
|
||||
|
||||
self.screen.blit(
|
||||
surface,
|
||||
(
|
||||
(self.screen_width - frame_width) / 2,
|
||||
(self.screen_height - frame_height) / 2,
|
||||
),
|
||||
)
|
||||
|
||||
def mode_normal(self):
|
||||
ui = (
|
||||
self.ctrl_status,
|
||||
self.ctrl_button_callibrate,
|
||||
self.ctrl_button_quit,
|
||||
self.ctrl_button_badges,
|
||||
self.ctrl_button_capture,
|
||||
) + (
|
||||
(
|
||||
self.ctrl_button_forward,
|
||||
self.ctrl_button_backward,
|
||||
self.ctrl_button_turn_left,
|
||||
self.ctrl_button_turn_right,
|
||||
self.ctrl_button_sonar,
|
||||
self.ctrl_sonar,
|
||||
)
|
||||
if self.bot.connected
|
||||
else (self.ctrl_button_connect,)
|
||||
)
|
||||
|
||||
self.capture_frame()
|
||||
self.compile_display()
|
||||
|
||||
self.draw_ui(ui)
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(ui, event)
|
||||
|
||||
def mode_snapshot(self):
|
||||
ui = [
|
||||
self.ctrl_status,
|
||||
]
|
||||
|
||||
if self.bot.connected:
|
||||
ui.append(self.ctrl_sonar)
|
||||
|
||||
if self.bot.current_task is not None:
|
||||
ui.append(self.ctrl_button_stop)
|
||||
else:
|
||||
ui.extend(
|
||||
(
|
||||
self.ctrl_button_forward,
|
||||
self.ctrl_button_backward,
|
||||
self.ctrl_button_turn_left,
|
||||
self.ctrl_button_turn_right,
|
||||
self.ctrl_button_sonar,
|
||||
self.ctrl_button_capture,
|
||||
self.ctrl_button_badges,
|
||||
self.ctrl_button_back,
|
||||
)
|
||||
)
|
||||
if self.current_programme:
|
||||
ui.append(self.ctrl_button_run)
|
||||
else:
|
||||
ui.extend(
|
||||
(
|
||||
self.ctrl_button_connect,
|
||||
self.ctrl_button_capture,
|
||||
self.ctrl_button_badges,
|
||||
self.ctrl_button_back,
|
||||
)
|
||||
)
|
||||
|
||||
self.compile_display()
|
||||
|
||||
if self.current_programme is not None:
|
||||
for i, line in enumerate(self.current_programme, 1):
|
||||
line = ' '.join(token.value.text for token in line)
|
||||
surface = self.layout.font_mono.render(
|
||||
f'{i:2d} {line}', True, Colour.HIGHLIGHT, Colour.BACKGROUND
|
||||
)
|
||||
self.screen.blit(surface, (10, surface.get_height() * i))
|
||||
|
||||
self.draw_ui(ui)
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(ui, event)
|
||||
|
||||
def mode_callibrate(self):
|
||||
ui = (
|
||||
self.ctrl_status,
|
||||
self.ctrl_debug,
|
||||
self.ctrl_button_back,
|
||||
self.ctrl_pwm,
|
||||
self.ctrl_blur,
|
||||
self.ctrl_expo,
|
||||
self.ctrl_res,
|
||||
)
|
||||
|
||||
self.capture_frame()
|
||||
self.compile_display()
|
||||
|
||||
self.draw_ui(ui)
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(ui, event)
|
||||
|
||||
def mode_badge(self):
|
||||
ui = (
|
||||
self.ctrl_button_back,
|
||||
) # fmt: skip
|
||||
|
||||
assert self.current_badge is not None
|
||||
|
||||
padding = 48
|
||||
|
||||
self.screen.fill((0, 0, 0))
|
||||
|
||||
qrcode_size = (
|
||||
self.screen_height
|
||||
- 3 * padding
|
||||
- self.current_badge.image.get_height()
|
||||
)
|
||||
qrcode = pygame.transform.scale(
|
||||
self.current_badge.qrcode, (qrcode_size, qrcode_size)
|
||||
)
|
||||
|
||||
self.screen.blit(
|
||||
qrcode, ((self.screen_width - qrcode_size) / 2, padding)
|
||||
)
|
||||
self.screen.blit(
|
||||
self.current_badge.image,
|
||||
(
|
||||
(self.screen_width - self.current_badge.image.get_width()) / 2,
|
||||
(
|
||||
self.screen_height
|
||||
- self.current_badge.image.get_height()
|
||||
- padding
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.draw_ui(ui)
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.handle_event(ui, event)
|
||||
|
||||
|
||||
# vim: tw=80 ts=4 sts=4 sw=4 et
|
|
@ -4,25 +4,53 @@
|
|||
import enum
|
||||
|
||||
|
||||
# 37c3 colours
|
||||
class Colour(tuple, enum.Enum):
|
||||
class Colour38c3(tuple, enum.Enum):
|
||||
# fmt: off
|
||||
BLACK = ( 0, 0, 0) #000000
|
||||
WHITE = (255, 255, 255) #ffffff
|
||||
GREY1 = (217, 217, 217) #d9d9d9
|
||||
GREY2 = (170, 170, 170) #aaaaaa
|
||||
GREY3 = (122, 122, 122) #7a7a7a
|
||||
GREY4 = ( 32, 32, 32) #202020
|
||||
BLUE = ( 45, 66, 255) #2d42ff
|
||||
BLUE2 = ( 11, 21, 117) #0b1575
|
||||
RED = (222, 64, 64) #de4040
|
||||
RED2 = ( 86, 16, 16) #561010
|
||||
GREEN = (121, 255, 94) #79ff5e
|
||||
GREEN2 = ( 43, 141, 24) #2b8d18
|
||||
CYAN = ( 41, 255, 255) #29ffff
|
||||
CYAN2 = ( 0, 107, 107) #006b6b
|
||||
MAGENTA = (222, 55, 255) #de37ff
|
||||
MAGENTA2 = (102, 0, 122) #66007a
|
||||
YELLOW = (246, 246, 117) #f6f675
|
||||
YELLOW2 = (117, 177, 1) #757501
|
||||
CORAL_RED = (255, 80, 83) #FF5053
|
||||
PEARL_WHITE = (254, 242, 255) #FEF2FF
|
||||
AMETHYST = (106, 95, 219) #6A5FDB
|
||||
PERIWINKLE = (178, 170, 255) #B2AAFF
|
||||
DARK_PURPLE = ( 38, 26, 102) #261A66
|
||||
AUBERGINE = ( 41, 17, 76) #29114C
|
||||
SUPER_DARK_PINK = ( 15, 0, 10) #0F000A
|
||||
DARK_AUBERGINE = ( 31, 11, 47) #190B2F
|
||||
|
||||
PRIMARY = CORAL_RED
|
||||
HIGHLIGHT = PEARL_WHITE
|
||||
ACCENT1 = AMETHYST
|
||||
ACCENT2 = PERIWINKLE
|
||||
ACCENT3 = DARK_PURPLE
|
||||
ACCENT4 = AUBERGINE
|
||||
ACCENT5 = DARK_AUBERGINE
|
||||
BACKGROUND = SUPER_DARK_PINK
|
||||
|
||||
BLACK = ( 0, 0, 0) #000000
|
||||
WHITE = (255, 255, 255) #ffffff
|
||||
# fmt: on
|
||||
|
||||
|
||||
# 37c3 colours
|
||||
class Colour37c3(tuple, enum.Enum):
|
||||
# fmt: off
|
||||
BLACK = ( 0, 0, 0) #000000
|
||||
WHITE = (255, 255, 255) #ffffff
|
||||
GREY1 = (217, 217, 217) #d9d9d9
|
||||
GREY2 = (170, 170, 170) #aaaaaa
|
||||
GREY3 = (122, 122, 122) #7a7a7a
|
||||
GREY4 = ( 32, 32, 32) #202020
|
||||
BLUE = ( 45, 66, 255) #2d42ff
|
||||
BLUE2 = ( 11, 21, 117) #0b1575
|
||||
RED = (222, 64, 64) #de4040
|
||||
RED2 = ( 86, 16, 16) #561010
|
||||
GREEN = (121, 255, 94) #79ff5e
|
||||
GREEN2 = ( 43, 141, 24) #2b8d18
|
||||
CYAN = ( 41, 255, 255) #29ffff
|
||||
CYAN2 = ( 0, 107, 107) #006b6b
|
||||
MAGENTA = (222, 55, 255) #de37ff
|
||||
MAGENTA2 = (102, 0, 122) #66007a
|
||||
YELLOW = (246, 246, 117) #f6f675
|
||||
YELLOW2 = (117, 177, 1) #757501
|
||||
# fmt: on
|
||||
|
||||
|
||||
Colour = Colour38c3
|
||||
|
|
Loading…
Add table
Reference in a new issue