346 lines
12 KiB
Python
346 lines
12 KiB
Python
import os.path
|
|
import time
|
|
import traceback
|
|
|
|
from typing import List
|
|
from PIL import Image
|
|
|
|
from escpos.escpos import Escpos
|
|
from escpos.printer import Usb
|
|
|
|
from Event import Event, load_event
|
|
from Photo import Photo
|
|
from config import *
|
|
|
|
from enum import Enum
|
|
|
|
from config import read, file_increase, iso, now
|
|
from timer import Timer
|
|
from libcamera import Transform
|
|
|
|
import RPi.GPIO as GPIO
|
|
|
|
GPIO_LED_BORDER = 7
|
|
GPIO_SPOT_RIGHT = 35
|
|
GPIO_SPOT_LEFT = 37
|
|
GPIO_BUTTON_LED = 36
|
|
GPIO_BUTTON = 38
|
|
|
|
|
|
# GPIO helpers
|
|
|
|
spot_mode = 0
|
|
|
|
def led_border(state):
|
|
global GPIO_LED_BORDER
|
|
GPIO.output(GPIO_LED_BORDER, state)
|
|
|
|
|
|
def set_spot_mode(new_mode):
|
|
global spot_mode
|
|
spot_mode = new_mode % 4
|
|
GPIO.output(GPIO_SPOT_LEFT, not (spot_mode & 1))
|
|
GPIO.output(GPIO_SPOT_RIGHT, not (spot_mode & 2))
|
|
|
|
|
|
def next_spot_mode():
|
|
global spot_mode
|
|
set_spot_mode(spot_mode + 1)
|
|
|
|
|
|
# user interface
|
|
|
|
def button_press(gpio_id):
|
|
next_spot_mode()
|
|
|
|
|
|
# INIT GPIO
|
|
GPIO.setmode(GPIO.BOARD)
|
|
GPIO.setwarnings(False)
|
|
|
|
GPIO.setup(GPIO_SPOT_RIGHT, GPIO.OUT, initial=GPIO.HIGH)
|
|
GPIO.setup(GPIO_SPOT_LEFT, GPIO.OUT, initial=GPIO.HIGH)
|
|
GPIO.setup(GPIO_LED_BORDER, GPIO.OUT, initial=GPIO.LOW)
|
|
GPIO.setup(GPIO_BUTTON_LED, GPIO.OUT, initial=GPIO.LOW)
|
|
|
|
GPIO.setup(GPIO_BUTTON, GPIO.IN)
|
|
GPIO.add_event_detect(GPIO_BUTTON, GPIO.FALLING, button_press, bouncetime=300)
|
|
|
|
led_border(False)
|
|
time.sleep(0.02)
|
|
led_border(True)
|
|
time.sleep(0.02)
|
|
led_border(False)
|
|
time.sleep(0.02)
|
|
led_border(True)
|
|
time.sleep(0.02)
|
|
led_border(False)
|
|
time.sleep(0.02)
|
|
led_border(True)
|
|
time.sleep(0.02)
|
|
led_border(False)
|
|
|
|
class State(Enum):
|
|
IDLE = 0
|
|
COUNTDOWN = 1
|
|
SHOOTING = 2
|
|
PREPARE = 3
|
|
CHOOSE = 4
|
|
QR = 5
|
|
|
|
res = (1024,768)
|
|
|
|
class Fotobox:
|
|
|
|
def __init__(self, camera):
|
|
camera.vflip = False
|
|
camera.hflip = True
|
|
camera.resolution = (2592, 1944)
|
|
camera.rotation = 270
|
|
|
|
camera.preview_configuration.main.size = res
|
|
camera.preview_configuration.main.format = 'BGR888'
|
|
camera.configure("preview")
|
|
|
|
self._capture_config = camera.create_still_configuration()
|
|
camera.still_configuration.main.size = (2592, 1944)
|
|
|
|
camera.start()
|
|
|
|
self._camera = camera
|
|
self._running: bool = True
|
|
self._state: State = State.IDLE
|
|
self._photos: List[Photo] = []
|
|
self._choice: Photo | None = None
|
|
self._countdown_timer: Timer = Timer("countdown", 0, 1, COUNTDOWN_COUNT + 1, self._countdown_callback)
|
|
self._shooting_timer: Timer = Timer("shooting", 0, SHOOTING_INTERVAL, SHOOTING_COUNT, self._shooting_callback)
|
|
self._qr_timer: Timer = Timer("qr", 0, QR_TIMEOUT, 1, self._qr_callback)
|
|
|
|
self._fotobox_uuid = read("./data/fotobox.uuid")
|
|
print("Starting fotobox: F-%s" % self._fotobox_uuid)
|
|
|
|
self._runtime_datetime = now()
|
|
self._runtime_number = file_increase("./data/runtime.number")
|
|
print("Starting runtime: R%04d-%s" % (self._runtime_number, iso(self._runtime_datetime)))
|
|
|
|
self._event: Event | None = load_event()
|
|
self._idle_url_without_protocol = "%s/e/%s" % (DOMAIN, self._event.code) if self._event is not None else DOMAIN
|
|
self._idle_url_with_protocol = "http%s://%s/" % ('s' if HTTPS else '', self._idle_url_without_protocol)
|
|
self._idle_qr = qr_create(self._idle_url_with_protocol)
|
|
self._idle_qr_rect = self._idle_qr.get_rect(center=SCREEN_RECT.center)
|
|
self._idle_url = FONT_URL.render(self._idle_url_without_protocol, True, (255, 255, 255))
|
|
self._idle_url_rect = self._idle_url.get_rect(centerx=SCREEN_RECT.centerx, bottom=HEIGHT - BORDER)
|
|
|
|
self._shooting_number = 0
|
|
self._shooting_datetime = None
|
|
|
|
self._printer: Escpos | None = None
|
|
|
|
def _countdown_callback(self):
|
|
if self._countdown_timer.complete():
|
|
self._set_state(State.SHOOTING)
|
|
else:
|
|
print("COUNTDOWN: %d" % self._countdown_timer.rest())
|
|
|
|
def _shooting_callback(self):
|
|
print("SHOT: %d/%d" % (self._shooting_timer.current(), SHOOTING_COUNT))
|
|
|
|
screen.fill((255, 255, 255))
|
|
pygame.display.flip()
|
|
if not DEBUG:
|
|
time.sleep(0.5)
|
|
|
|
frame = self._camera.switch_mode_and_capture_array(self._capture_config, "main")
|
|
image = Image.fromarray(frame)
|
|
self._photos.append(Photo(self._shooting_timer.current(), image))
|
|
|
|
screen.fill((0, 0, 0))
|
|
pygame.display.flip()
|
|
|
|
if self._shooting_timer.complete():
|
|
self._set_state(State.PREPARE)
|
|
elif not DEBUG:
|
|
start = time.time()
|
|
while time.time() - start < 1:
|
|
self._draw_live_frame()
|
|
pygame.display.flip()
|
|
|
|
def _qr_callback(self):
|
|
self._set_state(State.IDLE)
|
|
|
|
def run(self):
|
|
if DEBUG:
|
|
self._set_state(State.SHOOTING)
|
|
while self._running:
|
|
self._events()
|
|
self._timer()
|
|
self._draw()
|
|
|
|
def _events(self):
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
self._running = False
|
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
if self._state == State.IDLE:
|
|
self._set_state(State.COUNTDOWN)
|
|
elif self._state == State.CHOOSE:
|
|
if CANCEL_RECT.collidepoint(event.pos):
|
|
print("CANCEL")
|
|
self._set_state(State.IDLE)
|
|
elif SAVE_RECT.collidepoint(event.pos) and self._choice is not None:
|
|
print("SAVE")
|
|
self._set_state(State.QR)
|
|
else:
|
|
for photo in self._photos:
|
|
if photo.thumb_rect.collidepoint(event.pos):
|
|
self._choice = photo
|
|
print("CHOSE: %d" % self._choice.number)
|
|
elif self._state == State.QR:
|
|
if DONE_RECT.collidepoint(event.pos):
|
|
print("DONE")
|
|
self._set_state(State.IDLE)
|
|
elif self._printer is not None and not self._printed and PRINT_RECT.collidepoint(event.pos):
|
|
print("PRINT")
|
|
try:
|
|
self._printer.set(align='center', font='a', width=1, height=1)
|
|
self._printer.text("Fotobox\n")
|
|
url = "http%s://%s" % ('s' if HTTPS else '', DOMAIN)
|
|
if self._event is not None:
|
|
self._printer.text(self._event.title + "\n")
|
|
self._printer.text(self._event.date.strftime("%d.%m.%Y") + "\n")
|
|
if self._event.gallery:
|
|
self._printer.text(self._event.url_without_protocol + "\n")
|
|
self._printer.text("Passwort: " + self._event.password + "\n")
|
|
self._printer.text("Dein Foto findest du hier:\n")
|
|
self._printer.qr(self._choice.urlWithProtocol, center=True, size=5)
|
|
self._printer.text(url + "\n")
|
|
self._printer.text("/p/" + self._choice.code + "\n")
|
|
self._printer.cut()
|
|
self._printed = True
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
def _timer(self):
|
|
dt: float = clock.tick(20) / 1000
|
|
self._shooting_timer.tick(dt)
|
|
self._countdown_timer.tick(dt)
|
|
|
|
def _draw(self):
|
|
screen.fill((0, 0, 0))
|
|
if self._state == State.IDLE:
|
|
screen.blit(FOTOBOX_TITLE_SURFACE, FOTOBOX_TITLE_RECT)
|
|
screen.blit(self._idle_qr, self._idle_qr_rect)
|
|
screen.blit(self._idle_url, self._idle_url_rect)
|
|
elif self._state == State.COUNTDOWN:
|
|
self._draw_live_frame()
|
|
if self._countdown_timer.rest() == 3:
|
|
screen.blit(FOTOBOX_COUNTDOWN3_SURFACE, FOTOBOX_COUNTDOWN3_RECT)
|
|
elif self._countdown_timer.rest() == 2:
|
|
screen.blit(FOTOBOX_COUNTDOWN2_SURFACE, FOTOBOX_COUNTDOWN2_RECT)
|
|
elif self._countdown_timer.rest() == 1:
|
|
screen.blit(FOTOBOX_COUNTDOWN1_SURFACE, FOTOBOX_COUNTDOWN1_RECT)
|
|
elif self._state == State.CHOOSE:
|
|
screen.blit(CHOOSE_SURFACE, CHOOSE_RECT)
|
|
screen.blit(CANCEL_SURFACE, CANCEL_RECT)
|
|
if self._choice is not None:
|
|
screen.blit(SAVE_SURFACE, SAVE_RECT)
|
|
for photo in self._photos:
|
|
screen.blit(photo.thumb_surface, photo.thumb_rect)
|
|
if self._choice == photo:
|
|
rect = pygame.Rect(
|
|
photo.thumb_rect.x - CHOICE_BORDER,
|
|
photo.thumb_rect.y - CHOICE_BORDER,
|
|
photo.thumb_rect.width + 2 * CHOICE_BORDER,
|
|
photo.thumb_rect.height + 2 * CHOICE_BORDER
|
|
)
|
|
pygame.draw.rect(screen, (0, 255, 0), rect, width=CHOICE_BORDER)
|
|
elif self._state == State.QR:
|
|
screen.blit(self._qr, self._qr_rect)
|
|
screen.blit(self._choice.chosen_surface, self._choice.chosen_rect)
|
|
if self._printer is not None and not self._printed:
|
|
screen.blit(PRINT_SURFACE, PRINT_RECT)
|
|
screen.blit(DONE_SURFACE, DONE_RECT)
|
|
pygame.display.flip()
|
|
|
|
def _draw_live_frame(self):
|
|
array = self._camera.capture_array()
|
|
img = pygame.image.frombuffer(array.data, res, 'RGB')
|
|
screen.blit(img, (0, 0))
|
|
|
|
def _set_state(self, new_state: State):
|
|
print(new_state)
|
|
self._state = new_state
|
|
if new_state == State.COUNTDOWN:
|
|
led_border(True)
|
|
self._countdown_timer.restart()
|
|
elif new_state == State.SHOOTING:
|
|
self._photos = []
|
|
self._shooting_number += 1
|
|
self._shooting_datetime = now()
|
|
print("Starting shooting: S%04d-%s" % (self._shooting_number, iso(self._shooting_datetime)))
|
|
self._shooting_timer.restart()
|
|
elif new_state == State.PREPARE:
|
|
led_border(False)
|
|
screen.fill((0, 0, 0))
|
|
screen.blit(LOADING_SURFACE, LOADING_RECT)
|
|
pygame.display.flip()
|
|
for photo in self._photos:
|
|
photo.prepare(self._event)
|
|
self._set_state(State.CHOOSE)
|
|
elif new_state == State.CHOOSE:
|
|
if DEBUG:
|
|
self._choice = self._photos[0]
|
|
self._set_state(State.QR)
|
|
else:
|
|
self._choice = None
|
|
elif new_state == State.QR:
|
|
screen.fill((0, 0, 0))
|
|
screen.blit(LOADING_SURFACE, LOADING_RECT)
|
|
pygame.display.flip()
|
|
self._printed = False
|
|
try:
|
|
self._printer = Usb(0x0483, 0x5840, 0, interface=0, out_ep=0x04, in_ep=0x82)
|
|
self._printer.profile.media['width']['pixels'] = 512
|
|
print("Printer AVAILABLE")
|
|
except Exception as e:
|
|
print("No printer available")
|
|
traceback.print_exc()
|
|
self._printer = None
|
|
|
|
self._qr = qr_create(self._choice.urlWithProtocol)
|
|
self._qr_rect = self._qr.get_rect(left=2 * BORDER, centery=SCREEN_RECT.centery)
|
|
|
|
self.save(self._choice.photo_image, "", False)
|
|
self.save(self._choice.framed_image, "framed", True)
|
|
|
|
def save(self, img: Image, suffix: str, do_upload: bool):
|
|
filename = "F-%s---R%04d-%s---S%04d-%s---P%04d-%s---%s%s.jpg" % (
|
|
self._fotobox_uuid,
|
|
self._runtime_number,
|
|
iso(self._runtime_datetime),
|
|
self._shooting_number,
|
|
iso(self._shooting_datetime),
|
|
self._choice.number,
|
|
iso(self._choice.datetime),
|
|
self._choice.code,
|
|
"---" + suffix if suffix != "" else ""
|
|
)
|
|
path = Path("./data/photos/original/F-%s/R%04d-%s/%s" % (
|
|
self._fotobox_uuid,
|
|
self._runtime_number,
|
|
iso(self._runtime_datetime),
|
|
filename,
|
|
))
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
img.convert('RGB').save(path, format='JPEG', quality=95)
|
|
if do_upload:
|
|
upload = Path("./data/photos/upload/%s.jpg" % (self._choice.code,))
|
|
upload.parent.mkdir(parents=True, exist_ok=True)
|
|
os.chmod(upload.parent, 0o777)
|
|
os.link(path, upload)
|
|
os.chmod(upload, 0o777)
|
|
print("Photo saved: %s" % path.absolute())
|
|
|
|
|