team-2/PlantDashboard/main_dashboard.py
2025-08-02 02:41:58 +02:00

484 lines
20 KiB
Python

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import json
import os
from datetime import datetime, date
from plant_model import PlantGrowthModel
from data_handler import DataHandler
from tkcalendar import DateEntry
class PlantGrowthDashboard:
def __init__(self, root):
self.root = root
self.root.title("WeGrow")
self.root.geometry("1000x800") # More square dimensions
self.root.configure(bg='#f0f0f0')
image = Image.open("public/transparentLogo.png")
# Convert to PhotoImage
icon = ImageTk.PhotoImage(image)
# Set as window icon
self.root.iconphoto(False, icon)
# Initialize components
self.plant_model = PlantGrowthModel()
self.data_handler = DataHandler()
# Variables - fixed plant type
self.current_plant = "tomato" # Fixed plant type
self.ambient_mode = tk.StringVar(value="controlled")
self.baseline_image_path = None
# Environmental parameters with defaults
self.default_params = {
'temperature': 22.0,
'humidity': 65.0,
'soil_acidity': 6.5,
'pressure': 1013.25,
'brightness': 30,
'nutrients': 75.0,
'water': 80.0,
'co2': 850
}
self.env_params = {
'temperature': tk.DoubleVar(value=self.default_params['temperature']),
'humidity': tk.DoubleVar(value=self.default_params['humidity']),
'soil_acidity': tk.DoubleVar(value=self.default_params['soil_acidity']),
'pressure': tk.DoubleVar(value=self.default_params['pressure']),
'brightness': tk.DoubleVar(value=self.default_params['brightness']),
'nutrients': tk.DoubleVar(value=self.default_params['nutrients']),
'water': tk.DoubleVar(value=self.default_params['water']),
'co2': tk.DoubleVar(value=self.default_params['co2'])
}
self.setup_ui()
def setup_ui(self):
# Main container with square layout
main_frame = ttk.Frame(self.root, padding="8")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights for square layout
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=2) # Center panel wider
main_frame.columnconfigure(2, weight=1)
main_frame.rowconfigure(1, weight=1)
# Title
title_label = ttk.Label(main_frame, text="🌱 Plant Growth Dashboard",
font=('Arial', 14, 'bold'))
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 10))
# Left panel - Controls
self.setup_control_panel(main_frame)
# Center panel - Plant Visualization
self.setup_visualization_panel(main_frame)
# Right panel - Results only (no system messages)
self.setup_results_panel(main_frame)
def setup_control_panel(self, parent):
control_frame = ttk.LabelFrame(parent, text="Environmental Controls", padding="6")
control_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 6))
# Ambient mode
ttk.Label(control_frame, text="Environment Mode:", font=('Arial', 9, 'bold')).grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=(0, 6))
mode_frame = ttk.Frame(control_frame)
mode_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 8))
ttk.Radiobutton(mode_frame, text="Controlled", variable=self.ambient_mode,
value="controlled", command=self.on_mode_change).pack(anchor=tk.W)
ttk.Radiobutton(mode_frame, text="Open", variable=self.ambient_mode,
value="open", command=self.on_mode_change).pack(anchor=tk.W)
# Baseline image
ttk.Button(control_frame, text="📷 Load Plant Image",
command=self.load_baseline_image).grid(row=2, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E))
ttk.Label(control_frame, text="Parameters:",
font=('Arial', 9, 'bold')).grid(row=3, column=0, columnspan=2, sticky=tk.W, pady=(0, 4))
param_labels = {
'temperature': '🌡️ Temp (°C)',
'humidity': '💧 Humidity (%)',
'soil_acidity': '🧪 (pH)',
'pressure': '🌬️ Pressure (Pa)',
'brightness': '☀️ Light (DLI)',
'nutrients': '🌿 Nutrients (%)',
'water': '💦 Water (%)',
'co2': '🫧 CO2 (ppm)'
}
# Define bounds for each parameter (min, max)
param_bounds = {
'temperature': (10, 40),
'humidity': (20, 90),
'soil_acidity': (4.0, 9.0),
'pressure': (950, 1100),
'brightness': (5, 50),
'nutrients': (0, 100),
'water': (10, 100),
'co2': (400, 1200)
}
row = 4
for param, label in param_labels.items():
# Compact parameter layout
param_frame = ttk.Frame(control_frame)
param_frame.grid(row=row, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=1)
ttk.Label(param_frame, text=label, width=11, font=('Arial', 8)).pack(side=tk.LEFT)
# Get bounds for this parameter
scale_min, scale_max = param_bounds.get(param, (0, 100))
scale = ttk.Scale(param_frame, from_=scale_min, to=scale_max,
variable=self.env_params[param], orient=tk.HORIZONTAL,
command=lambda x, p=param: self.on_param_change(p))
scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(4, 4))
# Value label
value_label = ttk.Label(param_frame, text=f"{self.env_params[param].get():.1f}", width=5, font=('Arial', 8))
value_label.pack(side=tk.RIGHT)
# Store reference for updates
setattr(self, f"{param}_value_label", value_label)
row += 1
# Set to Default button
ttk.Button(control_frame, text="🔄 Set to Default",
command=self.set_to_default).grid(row=row, column=0, columnspan=2, pady=(10, 0), sticky=(tk.W, tk.E))
row += 1
# ADD EMPTY SPACER
spacer = ttk.Label(control_frame, text="")
spacer.grid(row=row, column=0, columnspan=2, pady=10)
# Add this after the parameters section
ttk.Label(control_frame, text="Final date of growth:",
font=('Arial', 9, 'bold')).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=(10, 4))
row += 1
# Compact date entry with calendar popup
# To get the selected dat simply call self.date_entry.get_date()
self.date_entry = DateEntry(control_frame,
width=12,
background='darkblue',
foreground='white',
borderwidth=2,
font=('Arial', 8),
date_pattern='dd/mm/yyyy',
state='readonly', # Add this
cursor='hand2') # Add this
self.date_entry.grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=2)
row += 1
control_frame.columnconfigure(0, weight=1)
control_frame.columnconfigure(1, weight=1)
def setup_visualization_panel(self, parent):
viz_frame = ttk.LabelFrame(parent, text="Plant Visualization", padding="6")
viz_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=3)
# Notebook for different views
notebook = ttk.Notebook(viz_frame)
notebook.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Plant Before Growth tab
self.setup_before_growth_tab(notebook)
# Plant Evolution tab
self.setup_plant_evolution_tab(notebook)
viz_frame.columnconfigure(0, weight=1)
viz_frame.rowconfigure(0, weight=1)
def setup_before_growth_tab(self, notebook):
"""Tab showing the plant before growth starts"""
before_frame = ttk.Frame(notebook)
notebook.add(before_frame, text="🌱 Initial Plant")
# Create a frame for the initial plant display
display_frame = ttk.Frame(before_frame)
display_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
# Title for initial state
title_label = ttk.Label(display_frame, text="Plant Initial State",
font=('Arial', 11, 'bold'))
title_label.pack(pady=(0, 8))
# Frame for the plant image
self.initial_plant_frame = ttk.Frame(display_frame)
self.initial_plant_frame.pack(fill=tk.BOTH, expand=True)
# Initial plant image label
self.initial_plant_label = ttk.Label(self.initial_plant_frame,
text="Initial plant state will appear here",
font=('Arial', 9))
self.initial_plant_label.pack(expand=True)
# Plant info display
info_frame = ttk.LabelFrame(display_frame, text="Plant Information", padding="4")
info_frame.pack(fill=tk.X, pady=(8, 0))
self.plant_info_text = tk.Text(info_frame, height=8, width=35, wrap=tk.WORD,
font=('Arial', 8), bg="#000000")
self.plant_info_text.pack(fill=tk.BOTH, expand=True)
# Submit button
submit_frame = ttk.Frame(display_frame)
submit_frame.pack(fill=tk.X, pady=(8, 0))
ttk.Button(submit_frame, text="📤 Submit Plant Information & Photo",
command=self.submit_plant_data).pack(fill=tk.X)
def setup_plant_evolution_tab(self, notebook):
"""Evolution tab with single image display"""
evolution_frame = ttk.Frame(notebook)
notebook.add(evolution_frame, text="🌿 Growth Evolution")
# Create main container for image display
self.image_display_frame = ttk.Frame(evolution_frame)
self.image_display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Create label for image or fallback text
self.evolution_image_label = ttk.Label(
self.image_display_frame,
text="Plant state after the prediction will appear here",
font=('Arial', 12),
foreground='gray',
anchor='center'
)
self.evolution_image_label.pack(expand=True)
def update_evolution_image(self, filename=None):
"""Update the evolution tab with an image from file or show fallback text"""
if filename and os.path.exists(filename):
try:
# Load and display the image
from PIL import Image, ImageTk
# Open and resize image if needed
pil_image = Image.open(filename)
# Optional: resize to fit the display area
pil_image = pil_image.resize((400, 300), Image.Resampling.LANCZOS)
# Convert to PhotoImage for tkinter
photo_image = ImageTk.PhotoImage(pil_image)
# Display the image
self.evolution_image_label.config(image=photo_image, text="")
self.evolution_image_label.image = photo_image # Keep reference to prevent garbage collection
except Exception as e:
print(f"Error loading image {filename}: {e}")
# Show fallback text on error
self.evolution_image_label.config(
image="",
text="Plant state after the prediction will appear here"
)
else:
# Show fallback text when no filename or file doesn't exist
self.evolution_image_label.config(
image="",
text="Plant state after the prediction will appear here"
)
def setup_results_panel(self, parent):
results_frame = ttk.LabelFrame(parent, text="Growth Prediction", padding="6")
results_frame.grid(row=1, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(6, 0))
# Prediction results only (no system messages)
ttk.Label(results_frame, text="Forecast Results:",
font=('Arial', 9, 'bold')).grid(row=0, column=0, sticky=tk.W, pady=(0, 4))
self.results_text = tk.Text(results_frame, height=10, width=26, wrap=tk.WORD, font=('Arial', 8), state='disabled')
self.results_text.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
img = Image.open("public/TransparentFlower.png")
# Resize if needed
img = img.resize((180, 290), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(img)
static_image_label = ttk.Label(results_frame, image=photo)
static_image_label.image = photo # Keep reference
static_image_label.grid(row=2, column=0, pady=(6, 0))
results_frame.columnconfigure(0, weight=1)
results_frame.rowconfigure(1, weight=1)
def on_mode_change(self):
#KEY FOCUS IS TO IMPLEMENT THIS PART
"""Handle mode changes"""
current_mode = self.ambient_mode.get()
if current_mode == "controlled":
print("Switched to Controlled mode")
# Enable all parameter controls
# No need to call the meteo api
elif current_mode == "open":
print("Switched to Open mode")
# Disable most parameter controls (temp, humidity, light)
# Call the meteo api to retrieve all the parameters, set variable meteo_values to retrieve when submitiing all
# Inside the retrieving of all the data check if the mode is one, select which data to use
def on_param_change(self, param):
value = self.env_params[param].get()
# Update value label
value_label = getattr(self, f"{param}_value_label")
if param == 'soil_acidity':
value_label.config(text=f"{value:.1f}")
elif param in ['brightness', 'pressure', 'co2']:
value_label.config(text=f"{value:.0f}")
else:
value_label.config(text=f"{value:.1f}")
def set_to_default(self):
"""Reset all parameters to default values"""
for param_name, default_value in self.default_params.items():
self.env_params[param_name].set(default_value)
self.update_parameter_label(param_name, default_value)
self.ambient_mode.set("controlled")
self.date_entry.set_date(date.today())
def update_parameter_label(self, param, value):
"""Update the value label for a specific parameter"""
try:
# Get the value label for this parameter
value_label = getattr(self, f"{param}_value_label")
# Format the value based on parameter type
if param == 'soil_acidity':
value_label.config(text=f"{value:.1f}")
elif param in ['brightness', 'pressure', 'co2']:
value_label.config(text=f"{value:.0f}")
else:
value_label.config(text=f"{value:.1f}")
except AttributeError:
# Handle case where label doesn't exist
print(f"Warning: No label found for parameter {param}")
def load_baseline_image(self):
self.results_text.delete(1.0, tk.END)
self.set_to_default()
file_path = filedialog.askopenfilename(
title="Select baseline plant image",
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.gif")]
)
if file_path:
self.baseline_image_path = file_path
def submit_plant_data(self):
"""Submit plant information and photo"""
try:
# Get current parameters
params = {param: var.get() for param, var in self.env_params.items()}
params['plant_type'] = self.current_plant
params['ambient_mode'] = self.ambient_mode.get()
# Create submission data
submission_data = {
'timestamp': datetime.now().isoformat(),
'parameters': params,
'baseline_image_path': self.baseline_image_path,
'plant_info': self.plant_info_text.get(1.0, tk.END),
'start date': datetime.now().date().isoformat(),
'end_date': self.date_entry.get_date().isoformat()
}
#Remove plant_info_text
self.plant_info_text.delete(1.0, tk.END)
data_dir = "../data"
os.makedirs(data_dir, exist_ok=True)
current_date = datetime.now().strftime('%Y%m%d')
filename = f"{current_date}-{current_date}.txt"
filepath = os.path.join(data_dir, filename)
with open(filepath, 'w') as f:
json.dump(submission_data, f, indent=4)
# Here call the bot pipeline to store results on files in plant_data
# results are in the form of (text, image)
results = "come bot please"
text = getattr(results, 'text', None)
image_filename = getattr(results, 'image', None)
images_dir = "./plant_data"
os.makedirs(data_dir, exist_ok=True)
image_path = os.path.join(images_dir, image_filename)
self.updating_evolution_and_forecasts(text, image_path)
# Here update the informations in the last box from plant_data/texts
# Here update the informations in growth evolution from plant_data/images
#Calling a small advertment to notify the user that the image has been generated
messagebox.showinfo("Submission successful, go to growth evolution to see the results")
print(f"Submission data saved to: {filepath}")
except Exception as e:
messagebox.showerror("Submission Error", f"Error submitting data: {str(e)}")
def updating_evolution_and_forecasts(self, text, image_path):
self.results_text.config(state='normal') # Enable editing
self.results_text.delete(1.0, tk.END) # Clear existing content
if text != None:
self.results_text.insert(1.0, text) # Insert new text
self.results_text.config(state='disabled') # Disable editing again
self.update_evolution_image(image_path)
def update_initial_plant_display(self):
"""Update the initial plant state display"""
try:
initial_image = None
# Generate initial plant image (stage 0)
try:
if self.baseline_image_path != None and os.path.exists(self.baseline_image_path):
initial_image = Image.open(self.baseline_image_path)
except Exception as e:
print(f"Error loading image from {self.baseline_image_path}: {e}")
# Resize image to fit better in square layout
if initial_image != None:
initial_image = initial_image.resize((280, 210), Image.Resampling.LANCZOS)
# Convert to PhotoImage and display
photo = ImageTk.PhotoImage(initial_image)
self.initial_plant_label.configure(image=photo, text="")
self.initial_plant_label.image = photo # Keep reference
except Exception as e:
messagebox.showerror("Image Error", f"Could not generate initial plant image: {str(e)}")
def update_results_display(self, prediction):
self.results_text.delete(1.0, tk.END)
#Here update the results display after submitting the photo and the message with the parameters and receveing the output
results = ""
self.results_text.insert(1.0, results)
def main():
root = tk.Tk()
app = PlantGrowthDashboard(root)
root.mainloop()
if __name__ == "__main__":
main()