adding severance terminal code
main file - lumon.py subclasses - palette.py, data_bin.py and data.py images - lumon-logo and lumon-logo-small mdr.service - systemd file to run on boot
This commit is contained in:
parent
06d7ef1381
commit
9f557683eb
7 changed files with 1816 additions and 0 deletions
300
Raspberry_Pi_Severance_MDR_Terminal/data.py
Normal file
300
Raspberry_Pi_Severance_MDR_Terminal/data.py
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import math
|
||||
import random
|
||||
import tkinter as tk
|
||||
import time
|
||||
from palette import Palette
|
||||
|
||||
class DataNumber:
|
||||
active_bin = None
|
||||
|
||||
@classmethod
|
||||
def reset_active_bin(cls):
|
||||
"""Reset the class-level active bin tracker"""
|
||||
cls.active_bin = None
|
||||
|
||||
def __init__(self, x: int, y: int, canvas: tk.Canvas, base_size: int = 35, palette=Palette):
|
||||
"""
|
||||
Initialize a data number for macrodata refinement
|
||||
"""
|
||||
self.num = random.randint(0, 9)
|
||||
self.home_x = x
|
||||
self.home_y = y
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.mouse_offset_x = 0
|
||||
self.mouse_offset_y = 0
|
||||
self.palette = palette
|
||||
self.color = self.palette.FG
|
||||
self.alpha = 255
|
||||
self.base_size = base_size
|
||||
self.size = base_size
|
||||
self.refined = False
|
||||
self.bin_it = False
|
||||
self.bin = None
|
||||
self.bin_pause_time = 2
|
||||
self.bin_pause = self.bin_pause_time
|
||||
self.canvas = canvas
|
||||
self.text_id = self.canvas.create_text(
|
||||
self.x, self.y,
|
||||
text=str(self.num),
|
||||
font=('Courier', self.size),
|
||||
fill=self.color,
|
||||
anchor='center'
|
||||
)
|
||||
self.needs_refinement = False
|
||||
self.wiggle_offset_x = 0
|
||||
self.wiggle_offset_y = 0
|
||||
|
||||
def refine(self, bin_obj=None, bins_list=None):
|
||||
"""
|
||||
Mark this number for refinement and assign it to a bin.
|
||||
"""
|
||||
if bin_obj is not None:
|
||||
if bin_obj.is_full():
|
||||
return False
|
||||
target_bin = bin_obj
|
||||
elif bins_list is not None:
|
||||
target_bin = self.get_non_full_bin_for_position(bins_list)
|
||||
if target_bin is None:
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Either bin_obj or bins_list must be provided")
|
||||
self.bin_it = True
|
||||
if DataNumber.active_bin is None:
|
||||
DataNumber.active_bin = target_bin
|
||||
self.bin = target_bin
|
||||
else:
|
||||
if DataNumber.active_bin.is_full():
|
||||
DataNumber.active_bin = target_bin
|
||||
self.bin = DataNumber.active_bin
|
||||
return True
|
||||
|
||||
def get_non_full_bin_for_position(self, bins_list):
|
||||
"""
|
||||
Determine which available bin should open based on the position of this number.
|
||||
"""
|
||||
non_full_bins = [bin_obj for bin_obj in bins_list if not bin_obj.is_full()]
|
||||
if not non_full_bins:
|
||||
return None
|
||||
screen_width = self.canvas.winfo_width()
|
||||
original_bin_index = self.get_bin_index_for_position(screen_width, len(bins_list))
|
||||
closest_bin = None
|
||||
min_distance = float('inf')
|
||||
for bin_obj in non_full_bins:
|
||||
distance = abs(bin_obj.i - original_bin_index)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_bin = bin_obj
|
||||
return closest_bin
|
||||
|
||||
def get_bin_index_for_position(self, screen_width, num_bins):
|
||||
"""
|
||||
Get the bin index that corresponds to this number's position
|
||||
"""
|
||||
bin_width = screen_width / num_bins
|
||||
bin_index = int(self.x / bin_width)
|
||||
bin_index = max(0, min(bin_index, num_bins - 1))
|
||||
return bin_index
|
||||
|
||||
def go_bin(self):
|
||||
"""Move toward the bin for refinement"""
|
||||
if self.bin:
|
||||
self.bin.open()
|
||||
if self.bin_pause <= 0:
|
||||
dx = self.bin.x - self.x
|
||||
dy = self.bin.y - self.y
|
||||
distance = math.sqrt(dx*dx + dy*dy)
|
||||
if distance < 20:
|
||||
self.alpha = int(255 * (distance / 20))
|
||||
if distance < 3:
|
||||
self.wiggle_offset_x = 0
|
||||
self.wiggle_offset_y = 0
|
||||
self.mouse_offset_x = 0
|
||||
self.mouse_offset_y = 0
|
||||
self.bin.add_number()
|
||||
self.reset()
|
||||
return
|
||||
easing = max(0.03, min(0.1, 5.0 / distance))
|
||||
self.x += dx * easing
|
||||
self.y += dy * easing
|
||||
fade_start_distance = self.distance(self.home_x, self.home_y,
|
||||
self.bin.x, self.bin.y) * 0.4
|
||||
current_distance = self.distance(self.x, self.y, self.bin.x, self.bin.y)
|
||||
if distance >= 20:
|
||||
self.alpha = self.map_value(current_distance, fade_start_distance, 20, 255, 55)
|
||||
self.update_display()
|
||||
if hasattr(self.bin, 'level_elements'):
|
||||
for element_id in self.bin.level_elements.values():
|
||||
self.canvas.tag_raise(self.text_id, element_id)
|
||||
self.bin.last_refined_time = int(time.time() * 1000)
|
||||
else:
|
||||
self.bin_pause -= 1
|
||||
if self.bin_pause > 0:
|
||||
pulse_size = self.base_size * (1.0 + 0.5 *
|
||||
(1.0 - (self.bin_pause / self.bin_pause_time)))
|
||||
self.set_size(pulse_size)
|
||||
if hasattr(self.bin, 'level_elements'):
|
||||
for element_id in self.bin.level_elements.values():
|
||||
self.canvas.tag_raise(self.text_id, element_id)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the number after being binned."""
|
||||
self.num = random.randint(0, 9)
|
||||
self.x = self.home_x
|
||||
self.y = self.home_y
|
||||
self.wiggle_offset_x = 0
|
||||
self.wiggle_offset_y = 0
|
||||
self.mouse_offset_x = 0
|
||||
self.mouse_offset_y = 0
|
||||
self.refined = False
|
||||
self.bin_it = False
|
||||
self.bin = None
|
||||
self.color = self.palette.FG
|
||||
self.alpha = 255
|
||||
self.bin_pause = self.bin_pause_time
|
||||
self.update_display()
|
||||
still_active = False
|
||||
if not still_active and DataNumber.active_bin is not None:
|
||||
DataNumber.active_bin = None
|
||||
|
||||
def go_home(self):
|
||||
"""Move the number back to its home position with easing."""
|
||||
self.x = self.lerp(self.x, self.home_x, 0.1)
|
||||
self.y = self.lerp(self.y, self.home_y, 0.1)
|
||||
self.size = self.lerp(self.size, self.base_size, 0.1)
|
||||
self.update_display()
|
||||
|
||||
def set_size(self, sz):
|
||||
"""Set the size of the number."""
|
||||
self.size = sz
|
||||
self.update_display()
|
||||
|
||||
def turn(self, new_color):
|
||||
"""Change the color of the number."""
|
||||
self.color = new_color
|
||||
self.update_display()
|
||||
|
||||
def inside(self, x1, y1, x2, y2):
|
||||
"""Check if this number is inside the given rectangle."""
|
||||
return (
|
||||
self.x > min(x1, x2) and
|
||||
self.x < max(x1, x2) and
|
||||
self.y > min(y1, y2) and
|
||||
self.y < max(y1, y2)
|
||||
)
|
||||
|
||||
def show(self):
|
||||
"""Update the display of this number."""
|
||||
self.update_display()
|
||||
|
||||
def update_display(self):
|
||||
"""Update the text display with current properties and improved alpha handling"""
|
||||
if self.bin_it:
|
||||
digit_size = self.lerp(self.size, self.size * 2.5,
|
||||
self.map_value(self.bin_pause, self.bin_pause_time, 0, 0, 1))
|
||||
else:
|
||||
digit_size = self.size
|
||||
font = ('Courier', int(digit_size))
|
||||
clamped_alpha = max(0, min(255, self.alpha))
|
||||
if clamped_alpha == 0:
|
||||
self.canvas.itemconfig(self.text_id, state='hidden')
|
||||
return
|
||||
else:
|
||||
self.canvas.itemconfig(self.text_id, state='normal')
|
||||
if clamped_alpha < 255:
|
||||
bg_color = self.palette.BG
|
||||
fg_color = self.color
|
||||
alpha_ratio = clamped_alpha / 255.0
|
||||
if alpha_ratio < 0.05:
|
||||
display_color = self.blend_colors(bg_color, fg_color, 0.05)
|
||||
else:
|
||||
display_color = self.blend_colors(bg_color, fg_color, alpha_ratio)
|
||||
else:
|
||||
display_color = self.color
|
||||
self.canvas.itemconfig(self.text_id,
|
||||
text=str(self.num),
|
||||
font=font,
|
||||
fill=display_color)
|
||||
if not hasattr(self, 'wiggle_offset_x'):
|
||||
self.wiggle_offset_x = 0
|
||||
if not hasattr(self, 'wiggle_offset_y'):
|
||||
self.wiggle_offset_y = 0
|
||||
if not hasattr(self, 'mouse_offset_x'):
|
||||
self.mouse_offset_x = 0
|
||||
if not hasattr(self, 'mouse_offset_y'):
|
||||
self.mouse_offset_y = 0
|
||||
smooth_wiggle_x = round(self.wiggle_offset_x * 10) / 10
|
||||
smooth_wiggle_y = round(self.wiggle_offset_y * 10) / 10
|
||||
smooth_mouse_x = round(self.mouse_offset_x * 10) / 10
|
||||
smooth_mouse_y = round(self.mouse_offset_y * 10) / 10
|
||||
display_x = self.x + smooth_wiggle_x + smooth_mouse_x
|
||||
display_y = self.y + smooth_wiggle_y + smooth_mouse_y
|
||||
self.canvas.coords(self.text_id, display_x, display_y)
|
||||
|
||||
def resize(self, new_x, new_y):
|
||||
"""Update the home position when the window is resized."""
|
||||
self.home_x = new_x
|
||||
self.home_y = new_y
|
||||
|
||||
def show_wiggle(self, proximity_factor=0):
|
||||
"""Make the number threatening"""
|
||||
if self.needs_refinement and not self.bin_it:
|
||||
original_x, original_y = self.x, self.y
|
||||
smooth_x = round(self.wiggle_offset_x * 10) / 10
|
||||
smooth_y = round(self.wiggle_offset_y * 10) / 10
|
||||
self.x += smooth_x
|
||||
self.y += smooth_y
|
||||
original_color = self.color
|
||||
base_pulse = 0.7
|
||||
wave1 = math.sin(time.time() * 0.9) * 0.15
|
||||
wave2 = math.sin(time.time() * 1.8) * 0.05
|
||||
highlight_intensity = base_pulse + wave1 + wave2 + (proximity_factor * 0.2)
|
||||
highlight_intensity = max(0.6, min(1.0, highlight_intensity))
|
||||
if highlight_intensity > 0.82:
|
||||
self.color = self.palette.SELECT
|
||||
else:
|
||||
blend_amount = (highlight_intensity - 0.6) / 0.22
|
||||
self.color = self.blend_colors(self.palette.FG, self.palette.SELECT, blend_amount)
|
||||
self.update_display()
|
||||
self.x, self.y = original_x, original_y
|
||||
self.color = original_color
|
||||
|
||||
@staticmethod
|
||||
def lerp(start, end, amt):
|
||||
"""Linear interpolation between start and end by amt."""
|
||||
return start + (end - start) * amt
|
||||
|
||||
@staticmethod
|
||||
def map_value(value, start1, stop1, start2, stop2):
|
||||
"""Re-maps a number from one range to another."""
|
||||
if stop1 == start1:
|
||||
return start2
|
||||
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
|
||||
|
||||
@staticmethod
|
||||
def distance(x1, y1, x2, y2):
|
||||
"""Calculate distance between two points."""
|
||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
|
||||
@staticmethod
|
||||
def hex_to_rgb(hex_color):
|
||||
"""Convert hex color to RGB values."""
|
||||
# Strip the # if it exists
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_hex(rgb):
|
||||
"""Convert RGB tuple to hex color."""
|
||||
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
|
||||
|
||||
@staticmethod
|
||||
def blend_colors(color1, color2, ratio):
|
||||
"""Blend two colors based on ratio (0-1)."""
|
||||
c1 = DataNumber.hex_to_rgb(color1)
|
||||
c2 = DataNumber.hex_to_rgb(color2)
|
||||
blended = tuple(int(c1[i] + (c2[i] - c1[i]) * ratio) for i in range(3))
|
||||
return DataNumber.rgb_to_hex(blended)
|
||||
417
Raspberry_Pi_Severance_MDR_Terminal/data_bin.py
Normal file
417
Raspberry_Pi_Severance_MDR_Terminal/data_bin.py
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk, ImageFont, ImageDraw
|
||||
|
||||
# pylint: disable=broad-exception-caught,too-many-locals
|
||||
|
||||
class Bin:
|
||||
KEYS = ['WO', 'FC', 'DR', 'MA']
|
||||
MAX_LID_ANGLE = 45
|
||||
CLOSED_LID_ANGLE = 180
|
||||
MAX_SHOW_TIME = 1500 # milliseconds
|
||||
LID_OPEN_CLOSE_TIME = 750 # milliseconds
|
||||
|
||||
def __init__(self, width, index, goal, canvas, levels=None, palette=None):
|
||||
self.w = width
|
||||
self.i = index
|
||||
self.x = index * width + width * 0.5
|
||||
self.canvas = canvas
|
||||
self.buffer = 60
|
||||
self.y = canvas.winfo_height() - 50
|
||||
self.palette = palette
|
||||
self.fg_color = self.palette.FG
|
||||
self.bg_color = self.palette.BG
|
||||
self.goal = goal
|
||||
self.level_goal = self.goal / 4
|
||||
self.level_h = self.buffer * 1.7
|
||||
self.levels_y_offset = self.level_h
|
||||
self.last_refined_time = self.get_millis()
|
||||
if levels is None:
|
||||
self.levels = {
|
||||
'WO': 0,
|
||||
'FC': 0,
|
||||
'DR': 0,
|
||||
'MA': 0
|
||||
}
|
||||
else:
|
||||
self.levels = levels
|
||||
self.count = sum(self.levels.values())
|
||||
|
||||
self.show_levels = False
|
||||
self.closing_animation = False
|
||||
self.opening_animation = False
|
||||
self.lid_angle = self.CLOSED_LID_ANGLE
|
||||
self.show_time = 0
|
||||
self.animation_start_time = 0
|
||||
self.animation_progress = 0
|
||||
|
||||
self.visual_elements = {}
|
||||
self.level_elements = {}
|
||||
self.progress_bar_elements = {}
|
||||
|
||||
self.create_visual_elements()
|
||||
|
||||
def create_outlined_text(self, text, font_size=22, stroke_width=4):
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/msttcorefonts/arial.ttf", font_size)
|
||||
dummy_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||
dummy_draw = ImageDraw.Draw(dummy_img)
|
||||
bbox = dummy_draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
padding = 12
|
||||
width = text_width + padding * 2 + stroke_width * 2
|
||||
height = text_height + padding * 2 + stroke_width * 2
|
||||
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
fill_color = self.bg_color
|
||||
stroke_color = self.fg_color
|
||||
position = (padding + stroke_width, padding+stroke_width)
|
||||
draw.text(position, text, font=font, fill=fill_color,
|
||||
stroke_width=stroke_width, stroke_fill=stroke_color)
|
||||
photo = ImageTk.PhotoImage(img)
|
||||
return photo
|
||||
|
||||
def create_visual_elements(self):
|
||||
for key in list(self.visual_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.visual_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
for key in list(self.level_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.level_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
for key in list(self.progress_bar_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.progress_bar_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
self.visual_elements = {}
|
||||
self.level_elements = {}
|
||||
self.progress_bar_elements = {}
|
||||
rw = self.w
|
||||
popup_width = rw
|
||||
popup_height = self.buffer * 3
|
||||
base_y = self.y - self.buffer/4
|
||||
popup_y = base_y - popup_height/2
|
||||
self.visual_elements['main'] = self.canvas.create_rectangle(
|
||||
self.x - rw/2, self.y - self.buffer/4,
|
||||
self.x + rw/2, self.y + self.buffer/4,
|
||||
outline=self.fg_color, fill=self.bg_color, width=1
|
||||
)
|
||||
self.visual_elements['label'] = self.canvas.create_text(
|
||||
self.x, self.y,
|
||||
text=f"{self.i:02d}",
|
||||
font=('Arial', 16),
|
||||
fill=self.fg_color
|
||||
)
|
||||
self.level_elements['popup_bg'] = self.canvas.create_rectangle(
|
||||
self.x - popup_width/2, popup_y - popup_height/2,
|
||||
self.x + popup_width/2, popup_y + popup_height/2,
|
||||
outline="", fill=self.bg_color,
|
||||
state='hidden'
|
||||
)
|
||||
self.level_elements['container'] = self.canvas.create_rectangle(
|
||||
self.x - popup_width/2, popup_y - popup_height/2,
|
||||
self.x + popup_width/2, popup_y + popup_height/2,
|
||||
outline=self.fg_color, fill="", width=1,
|
||||
state='hidden'
|
||||
)
|
||||
bar_height = popup_height * 0.15
|
||||
for i, key in enumerate(self.KEYS):
|
||||
level_y = popup_y - popup_height * 0.3 + i * (popup_height * 0.6 / 3)
|
||||
self.level_elements[f'{key}_label'] = self.canvas.create_text(
|
||||
self.x - popup_width * 0.4, level_y,
|
||||
text=key,
|
||||
font=('Courier', 14),
|
||||
fill=self.fg_color,
|
||||
anchor='w',
|
||||
state='hidden'
|
||||
)
|
||||
self.level_elements[f'{key}_outline'] = self.canvas.create_rectangle(
|
||||
self.x - popup_width * 0.2, level_y - bar_height/2,
|
||||
self.x + popup_width * 0.4, level_y + bar_height/2,
|
||||
outline=self.fg_color, fill="",
|
||||
state='hidden'
|
||||
)
|
||||
self.level_elements[f'{key}_progress'] = self.canvas.create_rectangle(
|
||||
self.x - popup_width * 0.2, level_y - bar_height/2,
|
||||
self.x - popup_width * 0.2, level_y + bar_height/2,
|
||||
outline="", fill=self.fg_color,
|
||||
state='hidden'
|
||||
)
|
||||
left_edge_x = self.x - rw/2
|
||||
right_edge_x = self.x + rw/2
|
||||
top_edge_y = self.y - self.buffer/4
|
||||
self.level_elements['left_lid'] = self.canvas.create_line(
|
||||
left_edge_x, top_edge_y,
|
||||
self.x, top_edge_y,
|
||||
fill=self.fg_color, width=1,
|
||||
state='normal'
|
||||
)
|
||||
self.level_elements['right_lid'] = self.canvas.create_line(
|
||||
self.x, top_edge_y,
|
||||
right_edge_x, top_edge_y,
|
||||
fill=self.fg_color, width=1,
|
||||
state='normal'
|
||||
)
|
||||
progress_bar_y = self.y + self.buffer/4 + 2
|
||||
progress_bar_height = self.buffer/2
|
||||
self.progress_bar_elements['outline'] = self.canvas.create_rectangle(
|
||||
self.x - rw/2, progress_bar_y,
|
||||
self.x + rw/2, progress_bar_y + progress_bar_height,
|
||||
outline=self.fg_color, fill=self.bg_color, width=1
|
||||
)
|
||||
self.progress_bar_elements['fill'] = self.canvas.create_rectangle(
|
||||
self.x - rw/2 + 1, progress_bar_y + 1,
|
||||
self.x - rw/2 + 1, progress_bar_y + progress_bar_height - 1,
|
||||
outline="", fill=self.fg_color
|
||||
)
|
||||
percentage_text = "0%"
|
||||
outlined_img = self.create_outlined_text(percentage_text, font_size=14, stroke_width=1)
|
||||
left_edge = self.x - rw/2
|
||||
text_padding = 14
|
||||
self.progress_bar_elements['text'] = self.canvas.create_image(
|
||||
left_edge + text_padding, progress_bar_y + progress_bar_height/2,
|
||||
image=outlined_img,
|
||||
anchor=tk.CENTER
|
||||
)
|
||||
self.fix_z_order()
|
||||
self.update_progress_bar()
|
||||
|
||||
def fix_z_order(self):
|
||||
self.canvas.tag_raise(self.level_elements['popup_bg'])
|
||||
self.canvas.tag_raise(self.level_elements['container'])
|
||||
for key in self.KEYS:
|
||||
if f'{key}_label' in self.level_elements:
|
||||
self.canvas.tag_raise(self.level_elements[f'{key}_label'])
|
||||
if f'{key}_outline' in self.level_elements:
|
||||
self.canvas.tag_raise(self.level_elements[f'{key}_outline'])
|
||||
if f'{key}_progress' in self.level_elements:
|
||||
self.canvas.tag_raise(self.level_elements[f'{key}_progress'])
|
||||
self.canvas.tag_raise(self.visual_elements['main'])
|
||||
self.canvas.tag_raise(self.visual_elements['label'])
|
||||
self.canvas.tag_raise(self.level_elements['left_lid'])
|
||||
self.canvas.tag_raise(self.level_elements['right_lid'])
|
||||
self.canvas.tag_raise(self.progress_bar_elements['outline'])
|
||||
self.canvas.tag_raise(self.progress_bar_elements['fill'])
|
||||
self.canvas.tag_raise(self.progress_bar_elements['text'])
|
||||
|
||||
def is_full(self):
|
||||
total_levels = sum(self.levels.values())
|
||||
return total_levels >= self.goal
|
||||
|
||||
def add_number(self):
|
||||
if self.is_full():
|
||||
return False
|
||||
options = [key for key in self.KEYS if self.levels[key] < self.level_goal]
|
||||
if options:
|
||||
key = random.choice(options)
|
||||
self.levels[key] += 1
|
||||
self.open()
|
||||
self.last_refined_time = self.get_millis()
|
||||
self.update_display()
|
||||
self.update_progress_bar()
|
||||
self.fix_z_order()
|
||||
return True
|
||||
return False
|
||||
|
||||
def open(self):
|
||||
if not self.show_levels and not self.opening_animation:
|
||||
self.animation_start_time = self.get_millis()
|
||||
self.opening_animation = True
|
||||
self.closing_animation = False
|
||||
self.show_levels = True
|
||||
|
||||
def update(self):
|
||||
current_time = self.get_millis()
|
||||
if self.opening_animation:
|
||||
elapsed = current_time - self.animation_start_time
|
||||
if elapsed >= self.LID_OPEN_CLOSE_TIME:
|
||||
self.opening_animation = False
|
||||
self.animation_progress = 1.0
|
||||
else:
|
||||
progress = elapsed / self.LID_OPEN_CLOSE_TIME
|
||||
self.animation_progress = 1 - (1 - progress) * (1 - progress)
|
||||
self.update_display()
|
||||
self.fix_z_order()
|
||||
elif self.show_levels and not self.closing_animation:
|
||||
if current_time - self.last_refined_time > self.MAX_SHOW_TIME:
|
||||
self.closing_animation = True
|
||||
self.animation_start_time = current_time
|
||||
elif self.closing_animation:
|
||||
elapsed = current_time - self.animation_start_time
|
||||
if elapsed >= self.LID_OPEN_CLOSE_TIME:
|
||||
self.closing_animation = False
|
||||
self.show_levels = False
|
||||
self.animation_progress = 0.0
|
||||
else:
|
||||
progress = elapsed / self.LID_OPEN_CLOSE_TIME
|
||||
self.animation_progress = 1.0 - (progress * progress)
|
||||
self.update_display()
|
||||
self.fix_z_order()
|
||||
self.update_progress_bar()
|
||||
|
||||
def update_progress_bar(self):
|
||||
total_levels = sum(self.levels.values())
|
||||
completion_percentage = (total_levels / self.goal) * 100 if self.goal > 0 else 0
|
||||
rw = self.w
|
||||
progress_bar_y = self.y + self.buffer/4 + 2
|
||||
progress_bar_height = self.buffer/2
|
||||
fill_width = (rw * completion_percentage) / 100
|
||||
if completion_percentage == 0:
|
||||
self.canvas.coords(
|
||||
self.progress_bar_elements['fill'],
|
||||
self.x - rw/2 + 1, progress_bar_y + 1,
|
||||
self.x - rw/2 + 1, progress_bar_y + progress_bar_height - 1
|
||||
)
|
||||
else:
|
||||
self.canvas.coords(
|
||||
self.progress_bar_elements['fill'],
|
||||
self.x - rw/2 + 1, progress_bar_y + 1,
|
||||
self.x - rw/2 + max(1, fill_width), progress_bar_y + progress_bar_height - 1
|
||||
)
|
||||
percentage_text = f"{int(completion_percentage)}%"
|
||||
outlined_img = self.create_outlined_text(
|
||||
percentage_text,
|
||||
font_size=14,
|
||||
stroke_width=1
|
||||
)
|
||||
left_edge = self.x - rw/2
|
||||
text_padding = 30 if completion_percentage >= 100 else 24
|
||||
self.canvas.itemconfig(self.progress_bar_elements['text'], image=outlined_img)
|
||||
self.canvas.coords(
|
||||
self.progress_bar_elements['text'],
|
||||
left_edge + text_padding, progress_bar_y + progress_bar_height/2
|
||||
)
|
||||
|
||||
def update_display(self):
|
||||
self.count = sum(self.levels.values())
|
||||
self.count = min(max(self.count, 0), self.goal)
|
||||
rw = self.w
|
||||
popup_width = rw
|
||||
popup_height = self.buffer * 3
|
||||
base_y = self.y - self.buffer/4
|
||||
popup_y_closed = base_y
|
||||
popup_y_open = base_y - popup_height/2
|
||||
current_popup_y = self.map_value(
|
||||
self.animation_progress,
|
||||
0, 1,
|
||||
popup_y_closed, popup_y_open
|
||||
)
|
||||
self.canvas.coords(
|
||||
self.level_elements['popup_bg'],
|
||||
self.x - popup_width/2, current_popup_y - popup_height/2,
|
||||
self.x + popup_width/2, current_popup_y + popup_height/2
|
||||
)
|
||||
self.canvas.coords(
|
||||
self.level_elements['container'],
|
||||
self.x - popup_width/2, current_popup_y - popup_height/2,
|
||||
self.x + popup_width/2, current_popup_y + popup_height/2
|
||||
)
|
||||
left_edge_x = self.x - rw/2
|
||||
right_edge_x = self.x + rw/2
|
||||
top_edge_y = base_y
|
||||
max_lid_angle = 120
|
||||
current_angle = self.animation_progress * max_lid_angle
|
||||
angle_rad = math.radians(current_angle)
|
||||
|
||||
left_lid_end_x = left_edge_x + (rw/2) * math.cos(angle_rad)
|
||||
left_lid_end_y = top_edge_y - (rw/2) * math.sin(angle_rad)
|
||||
right_lid_end_x = right_edge_x - (rw/2) * math.cos(angle_rad)
|
||||
right_lid_end_y = top_edge_y - (rw/2) * math.sin(angle_rad)
|
||||
self.canvas.coords(
|
||||
self.level_elements['left_lid'],
|
||||
left_edge_x, top_edge_y,
|
||||
left_lid_end_x, left_lid_end_y
|
||||
)
|
||||
self.canvas.coords(
|
||||
self.level_elements['right_lid'],
|
||||
right_edge_x, top_edge_y,
|
||||
right_lid_end_x, right_lid_end_y
|
||||
)
|
||||
visibility_threshold = 0.05
|
||||
state = 'normal' if self.animation_progress > visibility_threshold else 'hidden'
|
||||
|
||||
self.canvas.itemconfig(self.level_elements['popup_bg'], state=state)
|
||||
self.canvas.itemconfig(self.level_elements['container'], state=state)
|
||||
self.canvas.itemconfig(self.level_elements['left_lid'], state='normal')
|
||||
self.canvas.itemconfig(self.level_elements['right_lid'], state='normal')
|
||||
self.update_level_displays(current_popup_y, popup_height, popup_width)
|
||||
if state == 'hidden':
|
||||
for key in self.KEYS:
|
||||
if f'{key}_label' in self.level_elements:
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_label'], state='hidden')
|
||||
if f'{key}_outline' in self.level_elements:
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_outline'], state='hidden')
|
||||
if f'{key}_progress' in self.level_elements:
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_progress'], state='hidden')
|
||||
|
||||
def update_level_displays(self, popup_y, popup_height, popup_width):
|
||||
bar_height = popup_height * 0.15
|
||||
state = 'normal' if self.animation_progress > 0.05 else 'hidden'
|
||||
bar_width = popup_width * 0.6
|
||||
for i, key in enumerate(self.KEYS):
|
||||
level_y = popup_y - popup_height * 0.3 + i * (popup_height * 0.6 / 3)
|
||||
self.canvas.coords(
|
||||
self.level_elements[f'{key}_label'],
|
||||
self.x - popup_width * 0.4, level_y
|
||||
)
|
||||
self.canvas.coords(
|
||||
self.level_elements[f'{key}_outline'],
|
||||
self.x - popup_width * 0.2, level_y - bar_height/2,
|
||||
self.x + popup_width * 0.4, level_y + bar_height/2
|
||||
)
|
||||
progress_width = ((bar_width * self.levels[key])
|
||||
/ self.level_goal if self.level_goal > 0 else 0)
|
||||
self.canvas.coords(
|
||||
self.level_elements[f'{key}_progress'],
|
||||
self.x - popup_width * 0.2, level_y - bar_height/2,
|
||||
self.x - popup_width * 0.2 + progress_width, level_y + bar_height/2
|
||||
)
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_label'], state=state)
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_outline'], state=state)
|
||||
self.canvas.itemconfig(self.level_elements[f'{key}_progress'], state=state)
|
||||
|
||||
def resize(self, new_w):
|
||||
self.w = new_w
|
||||
self.x = self.i * new_w + new_w * 0.5
|
||||
self.y = self.canvas.winfo_height() - 50
|
||||
for key in list(self.visual_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.visual_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for key in list(self.level_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.level_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for key in list(self.progress_bar_elements.keys()):
|
||||
try:
|
||||
self.canvas.delete(self.progress_bar_elements[key])
|
||||
except Exception:
|
||||
pass
|
||||
self.visual_elements = {}
|
||||
self.level_elements = {}
|
||||
self.progress_bar_elements = {}
|
||||
self.create_visual_elements()
|
||||
self.update_display()
|
||||
|
||||
@staticmethod
|
||||
def get_millis():
|
||||
return int(time.time() * 1000)
|
||||
|
||||
@staticmethod
|
||||
def map_value(value, start1, stop1, start2, stop2):
|
||||
if stop1 == start1:
|
||||
return start2
|
||||
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1))
|
||||
BIN
Raspberry_Pi_Severance_MDR_Terminal/lumon-logo-small.png
Normal file
BIN
Raspberry_Pi_Severance_MDR_Terminal/lumon-logo-small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Raspberry_Pi_Severance_MDR_Terminal/lumon-logo.png
Normal file
BIN
Raspberry_Pi_Severance_MDR_Terminal/lumon-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
1072
Raspberry_Pi_Severance_MDR_Terminal/lumon.py
Normal file
1072
Raspberry_Pi_Severance_MDR_Terminal/lumon.py
Normal file
File diff suppressed because it is too large
Load diff
18
Raspberry_Pi_Severance_MDR_Terminal/mdr.service
Normal file
18
Raspberry_Pi_Severance_MDR_Terminal/mdr.service
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Unit]
|
||||
Description=Macrodata Refinement
|
||||
After=multi-user.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
PAMName=login
|
||||
User=pi-lumon
|
||||
Group=pi-lumon
|
||||
WorkingDirectory=/home/pi-lumon
|
||||
Environment=DISPLAY=:0
|
||||
Environment=XAUTHORITY=/home/pi-lumon/.Xauthority
|
||||
ExecStart=/home/pi-lumon/lumon/bin/python /home/pi-lumon/lumon.py
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
Raspberry_Pi_Severance_MDR_Terminal/palette.py
Normal file
9
Raspberry_Pi_Severance_MDR_Terminal/palette.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
class Palette:
|
||||
"""Color palette based on the Severance MDR terminal"""
|
||||
def __init__(self):
|
||||
self.BG = '#010A13' # Dark blue-black background
|
||||
self.FG = '#ABFFE9' # Light cyan text
|
||||
self.SELECT = '#ABFFE9' # Selection color
|
||||
Loading…
Reference in a new issue