add a menu with bt pairing
This commit is contained in:
@@ -21,4 +21,6 @@ class DISPLAY_MODES(Enum):
|
|||||||
RECIPE_SELECTION = 3
|
RECIPE_SELECTION = 3
|
||||||
EDIT_RECIPE = 4
|
EDIT_RECIPE = 4
|
||||||
DO_RECIPE = 5
|
DO_RECIPE = 5
|
||||||
EDIT_STEP = 6
|
EDIT_STEP = 6
|
||||||
|
MENU = 7
|
||||||
|
BLUETOOTH_PAIRING = 10
|
||||||
80
frontend/views/bluetooth_pairing.py
Normal file
80
frontend/views/bluetooth_pairing.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from typing import Callable, Optional, Tuple
|
||||||
|
|
||||||
|
from PIL import ImageDraw, Image
|
||||||
|
|
||||||
|
from .base import View
|
||||||
|
from .button_interface import ButtonInterface
|
||||||
|
from .draw_utils import draw_bluetooth_icon
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothPairingView(View, ButtonInterface):
|
||||||
|
"""Shows a 'waiting for connection' screen while BT pairing is active.
|
||||||
|
|
||||||
|
``start_command`` is called once when the view is first rendered, to
|
||||||
|
trigger pairing mode on the hardware side.
|
||||||
|
``stop_command`` is called when the user cancels (left long press).
|
||||||
|
``on_connected(device_name)`` is called by external code when a device
|
||||||
|
has paired; it transitions back by calling ``deactivate_command``.
|
||||||
|
``deactivate_command`` returns to the previous screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent, im_size, center,
|
||||||
|
start_command: Optional[Callable] = None,
|
||||||
|
stop_command: Optional[Callable] = None,
|
||||||
|
on_connected: Optional[Callable[[str], None]] = None,
|
||||||
|
deactivate_command: Optional[Callable] = None):
|
||||||
|
self.start_command = start_command
|
||||||
|
self.stop_command = stop_command
|
||||||
|
self.on_connected = on_connected
|
||||||
|
self.deactivate_command = deactivate_command
|
||||||
|
self._frame = 0 # used for the waiting animation
|
||||||
|
self._started = False
|
||||||
|
super().__init__(parent, im_size, center)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def update_weight(self, weight: float) -> Image.Image:
|
||||||
|
if not self._started:
|
||||||
|
self._started = True
|
||||||
|
if self.start_command:
|
||||||
|
self.start_command()
|
||||||
|
|
||||||
|
im = self.bkg_im.copy()
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
cx = self.size[0] // 2
|
||||||
|
|
||||||
|
# Large BT icon centred
|
||||||
|
draw_bluetooth_icon(draw, (cx, 52), size=20)
|
||||||
|
|
||||||
|
draw.text((cx - 28, 80), "Pairing", fill='black')
|
||||||
|
|
||||||
|
# Three animated dots
|
||||||
|
self._frame = (self._frame + 1) % 6
|
||||||
|
for i in range(3):
|
||||||
|
r = 3 if i < self._frame // 2 + 1 else 1
|
||||||
|
draw.circle((cx - 8 + i * 8, 100), r, fill='black')
|
||||||
|
|
||||||
|
return im
|
||||||
|
|
||||||
|
def notify_connected(self, device_name: str = ''):
|
||||||
|
"""Call this from the outside when a BT device has connected."""
|
||||||
|
if self.on_connected:
|
||||||
|
self.on_connected(device_name)
|
||||||
|
self._cancel()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _cancel(self):
|
||||||
|
if self.stop_command:
|
||||||
|
self.stop_command()
|
||||||
|
if self.deactivate_command:
|
||||||
|
self.deactivate_command()
|
||||||
|
|
||||||
|
def left_long_press(self):
|
||||||
|
self._cancel()
|
||||||
|
|
||||||
|
def has_button(self) -> Tuple[bool, bool, bool, bool]:
|
||||||
|
return False, True, False, False
|
||||||
|
|
||||||
|
def render_left_long_press(self, draw, x, y):
|
||||||
|
draw.text((x, y - 5), 'Cancel', fill='black')
|
||||||
@@ -34,4 +34,8 @@ class ButtonInterface:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def render_both_long_press(self, draw, x, y):
|
def render_both_long_press(self, draw, x, y):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def render_status_icons(self, draw):
|
||||||
|
"""Called once per frame to render persistent status icons (e.g. BT indicator)."""
|
||||||
pass
|
pass
|
||||||
@@ -68,6 +68,8 @@ class ButtonsManager(View):
|
|||||||
|
|
||||||
self.current_view.render_both_long_press(draw, self.size[0] // 2, self.size[1] - 10)
|
self.current_view.render_both_long_press(draw, self.size[0] // 2, self.size[1] - 10)
|
||||||
|
|
||||||
|
self.current_view.render_status_icons(draw)
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
############ BUTTON ACTIONS ###########
|
############ BUTTON ACTIONS ###########
|
||||||
|
|||||||
@@ -10,3 +10,17 @@ def draw_long_press(draw, position):
|
|||||||
x, y = position
|
x, y = position
|
||||||
for i in range(0, 5, 2):
|
for i in range(0, 5, 2):
|
||||||
draw.circle((x + i, y), 2, fill='black')
|
draw.circle((x + i, y), 2, fill='black')
|
||||||
|
|
||||||
|
def draw_bluetooth_icon(draw, position, size=6, color='black'):
|
||||||
|
"""Draw a minimal Bluetooth icon (stylised B with two pointed flanges)."""
|
||||||
|
x, y = position
|
||||||
|
h = size # half-height of the centre line
|
||||||
|
w = size // 2 # half-width of the diamond cross
|
||||||
|
# Vertical spine
|
||||||
|
draw.line([(x, y - h), (x, y + h)], fill=color, width=1)
|
||||||
|
# Upper arm: spine top -> right tip -> spine midpoint
|
||||||
|
draw.line([(x, y - h), (x + w, y - h // 2)], fill=color, width=1)
|
||||||
|
draw.line([(x + w, y - h // 2), (x, y)], fill=color, width=1)
|
||||||
|
# Lower arm: spine midpoint -> right tip -> spine bottom
|
||||||
|
draw.line([(x, y), (x + w, y + h // 2)], fill=color, width=1)
|
||||||
|
draw.line([(x + w, y + h // 2), (x, y + h)], fill=color, width=1)
|
||||||
|
|||||||
@@ -5,18 +5,23 @@ from tkinter import Frame, Canvas, ttk, PhotoImage
|
|||||||
from PIL import ImageChops
|
from PIL import ImageChops
|
||||||
|
|
||||||
from ..config import DISPLAY_TYPES, DISPLAY_MODES
|
from ..config import DISPLAY_TYPES, DISPLAY_MODES
|
||||||
from .draw_utils import draw_clock
|
from .draw_utils import draw_clock, draw_bluetooth_icon
|
||||||
from . import NumberView, CircleView, TimerView, TextView
|
from . import NumberView, CircleView, TimerView, TextView
|
||||||
|
|
||||||
from .button_interface import ButtonInterface
|
from .button_interface import ButtonInterface
|
||||||
from .buttons_manager import ButtonsManager
|
from .buttons_manager import ButtonsManager
|
||||||
|
|
||||||
from .recipes import RecipeSelection, RecipeManager, EditRecipe, EditStep, StepType, Step
|
from .recipes import RecipeSelection, RecipeManager, EditRecipe, EditStep, StepType, Step
|
||||||
|
from .menu import MenuView
|
||||||
|
from .bluetooth_pairing import BluetoothPairingView
|
||||||
|
|
||||||
class MainView(tk.Frame, ButtonInterface):
|
class MainView(tk.Frame, ButtonInterface):
|
||||||
def __init__(self, parent,
|
def __init__(self, parent,
|
||||||
tare_command=None,
|
tare_command=None,
|
||||||
calibrate_command=None,
|
calibrate_command=None,
|
||||||
|
bt_connected_getter=None,
|
||||||
|
bt_start_pairing_command=None,
|
||||||
|
bt_stop_pairing_command=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
super().__init__(parent, **kwargs)
|
super().__init__(parent, **kwargs)
|
||||||
self.curr_mode = DISPLAY_MODES.MAIN
|
self.curr_mode = DISPLAY_MODES.MAIN
|
||||||
@@ -24,6 +29,9 @@ class MainView(tk.Frame, ButtonInterface):
|
|||||||
self.timer_view = None # Timer view is always active
|
self.timer_view = None # Timer view is always active
|
||||||
self.tare_command = tare_command
|
self.tare_command = tare_command
|
||||||
self.calibrate_command = calibrate_command
|
self.calibrate_command = calibrate_command
|
||||||
|
self.bt_connected_getter = bt_connected_getter
|
||||||
|
self.bt_start_pairing_command = bt_start_pairing_command
|
||||||
|
self.bt_stop_pairing_command = bt_stop_pairing_command
|
||||||
|
|
||||||
self.actions = Frame(self)
|
self.actions = Frame(self)
|
||||||
self.actions.pack()
|
self.actions.pack()
|
||||||
@@ -75,6 +83,10 @@ class MainView(tk.Frame, ButtonInterface):
|
|||||||
return
|
return
|
||||||
self.enter_recipe_selection()
|
self.enter_recipe_selection()
|
||||||
|
|
||||||
|
def both_long_press(self):
|
||||||
|
if self.curr_mode == DISPLAY_MODES.MAIN:
|
||||||
|
self.enter_menu()
|
||||||
|
|
||||||
|
|
||||||
def render_left_press(self, draw, x, y):
|
def render_left_press(self, draw, x, y):
|
||||||
if self.curr_mode == DISPLAY_MODES.MAIN:
|
if self.curr_mode == DISPLAY_MODES.MAIN:
|
||||||
@@ -106,6 +118,32 @@ class MainView(tk.Frame, ButtonInterface):
|
|||||||
if self.curr_mode == DISPLAY_MODES.MAIN:
|
if self.curr_mode == DISPLAY_MODES.MAIN:
|
||||||
draw.text((x, y - 5), "R", fill='black')
|
draw.text((x, y - 5), "R", fill='black')
|
||||||
|
|
||||||
|
def render_both_long_press(self, draw, x, y):
|
||||||
|
if self.curr_mode == DISPLAY_MODES.MAIN:
|
||||||
|
draw.text((x - 12, y - 5), "Menu", fill='black')
|
||||||
|
|
||||||
|
def render_status_icons(self, draw):
|
||||||
|
if self.bt_connected_getter and self.bt_connected_getter():
|
||||||
|
draw_bluetooth_icon(draw, (self.im_size[0] - 8, 8), size=5)
|
||||||
|
|
||||||
|
|
||||||
|
def enter_menu(self):
|
||||||
|
self.curr_mode = DISPLAY_MODES.MENU
|
||||||
|
self.buttons.current_view = MenuView(self,
|
||||||
|
self.im_size, self.center,
|
||||||
|
bluetooth_pair_command=self.start_bluetooth_pairing,
|
||||||
|
deactivate_command=self.enter_main_mode)
|
||||||
|
self.refresh(0.0)
|
||||||
|
|
||||||
|
def start_bluetooth_pairing(self):
|
||||||
|
self.curr_mode = DISPLAY_MODES.BLUETOOTH_PAIRING
|
||||||
|
self.buttons.current_view = BluetoothPairingView(
|
||||||
|
self, self.im_size, self.center,
|
||||||
|
start_command=self.bt_start_pairing_command,
|
||||||
|
stop_command=self.bt_stop_pairing_command,
|
||||||
|
deactivate_command=self.enter_menu,
|
||||||
|
)
|
||||||
|
self.refresh(0.0)
|
||||||
|
|
||||||
def enter_main_mode(self):
|
def enter_main_mode(self):
|
||||||
self.curr_mode = DISPLAY_MODES.MAIN
|
self.curr_mode = DISPLAY_MODES.MAIN
|
||||||
|
|||||||
61
frontend/views/menu.py
Normal file
61
frontend/views/menu.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from .base import View
|
||||||
|
from .button_interface import ButtonInterface
|
||||||
|
from .list_select import ListItem, CarouselView
|
||||||
|
|
||||||
|
from PIL import ImageDraw, Image
|
||||||
|
|
||||||
|
|
||||||
|
def _make_text_item(text):
|
||||||
|
return ListItem(lambda draw, pos, fill, t=text, **kw: draw.text(pos, str(t), fill=fill, **kw))
|
||||||
|
|
||||||
|
|
||||||
|
class MenuView(View, ButtonInterface):
|
||||||
|
|
||||||
|
def __init__(self, parent, im_size, center,
|
||||||
|
bluetooth_pair_command=None,
|
||||||
|
deactivate_command=None):
|
||||||
|
self.deactivate_command = deactivate_command
|
||||||
|
self.bluetooth_pair_command = bluetooth_pair_command
|
||||||
|
self.item_list = CarouselView(render_height=124, large_font_size=20)
|
||||||
|
self._actions = [
|
||||||
|
("BT Pair", self.bluetooth_pair_command),
|
||||||
|
]
|
||||||
|
self.item_list.items = [_make_text_item(label) for label, _ in self._actions]
|
||||||
|
super().__init__(parent, im_size, center)
|
||||||
|
|
||||||
|
def update_weight(self, weight: float) -> Image.Image:
|
||||||
|
im = self.bkg_im.copy()
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
self.item_list.render(draw, y_start=10)
|
||||||
|
return im
|
||||||
|
|
||||||
|
def left_press(self):
|
||||||
|
self.item_list.select_previous()
|
||||||
|
|
||||||
|
def left_long_press(self):
|
||||||
|
self.deactivate_command()
|
||||||
|
|
||||||
|
def right_press(self):
|
||||||
|
self.item_list.select_next()
|
||||||
|
|
||||||
|
def right_long_press(self):
|
||||||
|
_, action = self._actions[self.item_list.selected_index]
|
||||||
|
if action:
|
||||||
|
action()
|
||||||
|
|
||||||
|
def has_button(self) -> Tuple[bool, bool, bool, bool]:
|
||||||
|
return True, True, True, True
|
||||||
|
|
||||||
|
def render_left_press(self, draw, x, y):
|
||||||
|
draw.regular_polygon((x, y + 2, 5), 3, fill='black')
|
||||||
|
|
||||||
|
def render_left_long_press(self, draw, x, y):
|
||||||
|
draw.text((x, y - 5), 'Back', fill='black')
|
||||||
|
|
||||||
|
def render_right_press(self, draw, x, y):
|
||||||
|
draw.regular_polygon((x, y + 4, 5), 3, fill='black', rotation=180)
|
||||||
|
|
||||||
|
def render_right_long_press(self, draw, x, y):
|
||||||
|
draw.text((x - 30, y - 5), 'Select', fill='black')
|
||||||
Reference in New Issue
Block a user