Fotobox2/Fotobox.py

356 lines
13 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", QR_TIMEOUT, 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):
if self._qr_timer.complete():
print("QR TIMEOUT")
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)
self._qr_timer.tick(dt)
def _draw(self):
screen.fill((0, 0, 0))
if self._state == State.IDLE:
if self._event is not None and self._event.gallery:
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)
else:
screen.blit(FOTOBOX_TITLE_SURFACE, FOTOBOX_TITLE_RECT_CENTER)
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.IDLE:
self._qr_timer.cancel()
elif 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)
self._qr_timer.restart()
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())