diff --git a/daq/chart-observe.py b/daq/chart-observe.py index 8984bd3..02403c2 100755 --- a/daq/chart-observe.py +++ b/daq/chart-observe.py @@ -59,8 +59,8 @@ def start(): #the variables taken from the entry inputs sUser = customtkinter.CTkEntry.get(user_name) - sLocation = customtkinter.CTkEntry.get(location_name) - trial = customtkinter.CTkEntry.get(time_name) + sLongitude = customtkinter.CTkEntry.get(longitude_name) #getting the longitude from the entry + sLatitude = customtkinter.CTkEntry.get(latitude_name) #getting the latitude from the entry date_name.configure(state=tkinter.NORMAL) #checking if date was empty so that it knows to use the input from entry or the one from the system time and date @@ -68,14 +68,8 @@ def start(): date = customtkinter.CTkEntry.get(date_name) time = customtkinter.CTkEntry.get(curr_time) - #checking for empty trials - if not trial: - trial = '1' - - tDay = combobox.get() #make sure the location does not have spaces or slashes that many people accidentally do - location = sLocation.replace(" ", "-") date = date.replace("/", ".") user = sUser.replace("_", ".") @@ -86,8 +80,9 @@ def start(): date_y_m_d = year+"."+month+"."+day - #changed the date to be the correct formal - directory = user+"_"+location+"_"+date_y_m_d+"_"+trial+"_"+time.replace(":", ".")+"_"+tDay + # New format without location and with latitude and longitude + # I used f strings here for clarity + directory = f"{user}_lon{sLongitude}_lat{sLatitude}_{date_y_m_d}_{time.replace(':', '.')}_{tDay}" print(directory) if(len(month) == 1): @@ -311,239 +306,311 @@ def mode(): customtkinter.set_appearance_mode("Light") - +# below is the layout in the GUI mode_switch = customtkinter.CTkSwitch(master=app, text="Dark Mode", command=mode, onvalue="on", offvalue="off") -mode_switch.pack(padx=20, pady=10) -mode_switch.place(relx=0.1, rely=.03, anchor=tkinter.CENTER) - -#this is the start of the gui design where everything is layed out -label = customtkinter.CTkLabel(master=app, - text="Initial Frequency:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=8) - -label.place(relx=0.1, rely=0.1, anchor=tkinter.W) - -freq_i_in = customtkinter.CTkEntry(master=app, - placeholder_text= default_freq_i, - width=80, - height=25, - border_width=2, - corner_radius=10 - ) - -freq_i_in.place(relx=0.3, rely=0.1, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Final Frequency:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.1, rely=0.2, anchor=tkinter.W) - -freq_f_in = customtkinter.CTkEntry(master=app, - placeholder_text= default_freq_f, - width=80, - height=25, - border_width=2, - corner_radius=10 - ) - -freq_f_in.place(relx=0.3, rely=0.2, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Integration Time:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.1, rely=0.3, anchor=tkinter.W) - -int_time_in = customtkinter.CTkEntry(master=app, - placeholder_text=default_int_time, - width=80, - height=25, - border_width=2, - corner_radius=10 - ) - -int_time_in.place(relx=0.3, rely=0.3, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Number of Integrations:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.07, rely=0.4, anchor=tkinter.W) - -nint_in = customtkinter.CTkEntry(master=app, - placeholder_text=default_nint, - width=80, - height=25, - border_width=2, - corner_radius=10 - ) - -nint_in.place(relx=0.34, rely=0.4, anchor=tkinter.W) - -default_parameters_switch = customtkinter.CTkSwitch(master=app, text="Use Default Parameters", command=default_parameters, onvalue="on", offvalue="off") -default_parameters_switch.pack(padx=20, pady=10) -default_parameters_switch.place(relx=0.25, rely=.5, anchor=tkinter.CENTER) - -biasT_switch = customtkinter.CTkSwitch(master=app, text="Enable Bias-T", command=biasT_switch, onvalue="on", offvalue="off") -biasT_switch.pack(padx=20, pady=10) -biasT_switch.place(relx=0.25, rely=.57, anchor=tkinter.CENTER) - -description = customtkinter.CTkEntry(master=app, - placeholder_text="Describe what you are looking at.", - width=310, - height=30, - border_width=2, - corner_radius=10 - ) - -description.place(relx=0.05, rely=0.67, anchor=tkinter.W) - - -#below is the right side layout in the GUI -start_button = customtkinter.CTkButton(master=app, text="Start", command=start) -start_button.place(relx=0.5, rely=.8, anchor=tkinter.CENTER) -start_button.configure(state=tkinter.NORMAL) - - -stop_button = customtkinter.CTkButton(master=app, text="Stop", command=stop) -stop_button.place(relx=0.5, rely=.9, anchor=tkinter.CENTER) -stop_button.configure(state=tkinter.DISABLED) -#start with stop disabled so you cannot click stop before start - -jupyter_button = customtkinter.CTkButton(master=app, text="Open Jupyter Hub to Upload", command=open_jupyter) -jupyter_button.place(relx=0.8, rely=.8, anchor=tkinter.CENTER) -jupyter_button.configure(state=tkinter.NORMAL) - -local_jupyter_button = customtkinter.CTkButton(master=app, text="Open LOCAL Jupyter Notebook", command=open_local_jupyter) -local_jupyter_button.place(relx=0.8, rely=.9, anchor=tkinter.CENTER) -local_jupyter_button.configure(state=tkinter.NORMAL) - -label = customtkinter.CTkLabel(master=app, - text="Username:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.5, rely=0.1, anchor=tkinter.W) - -user_name = customtkinter.CTkEntry(master=app, - placeholder_text="Enter Here", - width=210, - height=25, - border_width=2, - corner_radius=10 - ) - -user_name.place(relx=0.7, rely=0.1, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Location:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.5, rely=0.2, anchor=tkinter.W) - -location_name = customtkinter.CTkEntry(master=app, - placeholder_text="Enter Here", - width=210, - height=25, - border_width=2, - corner_radius=10 - ) - -location_name.place(relx=0.7, rely=0.2, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Trial:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius= 5 - ) - -label.place(relx=0.5, rely=0.3, anchor=tkinter.W) - -time_name = customtkinter.CTkEntry(master=app, - placeholder_text="00", - width=120, - height=25, - border_width=2, - corner_radius=10 - ) - -time_name.place(relx=0.7, rely=0.3, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Date:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.5, rely=0.4, anchor=tkinter.W) - -date_name = customtkinter.CTkEntry(master=app, - placeholder_text="MM.DD.YYYY", - width=150, - height=25, - border_width=2, - corner_radius=10 - ) - -date_name.place(relx=0.7, rely=0.4, anchor=tkinter.W) - -label = customtkinter.CTkLabel(master=app, - text="Time:", - width=100, - height=25, - fg_color=("white", "gray"), - corner_radius=5 - ) - -label.place(relx=0.5, rely=0.5, anchor=tkinter.W) - -curr_time = customtkinter.CTkEntry(master=app, - placeholder_text="00:00", - width=70, - height=25, - border_width=2, - corner_radius=10 - ) - -curr_time.place(relx=0.7, rely=0.5, anchor=tkinter.W) - -combobox = customtkinter.CTkComboBox(master=app, - values=["am", "pm"], - height = 25, width = 55, variable = "") -combobox.pack(padx=5, pady=5) -combobox.set("am") # set initial value -combobox.place(relx=0.81, rely=0.5, anchor=tkinter.W) +mode_switch.pack(padx=20, pady=(10, 0), anchor="w") + +# Scrollable Frame for all widgets +scroll_frame = customtkinter.CTkScrollableFrame(master=app, width=760, height=440) +scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + +# Configure flexible grid layout +for i in range(12): + scroll_frame.rowconfigure(i, weight=1) +for j in range(2): + scroll_frame.columnconfigure(j, weight=1) + +# Store references to all fields for responsive layout +responsive_widgets = [] + +# === Input Fields (Adaptive Grid) === + +# Row 0 — Username +label_user = customtkinter.CTkLabel(scroll_frame, text="Username:") +label_user.grid(row=0, column=0, sticky="w", padx=10, pady=10) +user_name = customtkinter.CTkEntry(scroll_frame, placeholder_text="Enter Here", width=180) +user_name.grid(row=0, column=1, sticky="w", padx=10, pady=10) + +# Row 0 (right side) — Longitude +label_long = customtkinter.CTkLabel(scroll_frame, text="Longitude:") +label_long.grid(row=0, column=2, sticky="w", padx=10, pady=10) +longitude_name = customtkinter.CTkEntry(scroll_frame, placeholder_text="e.g., -91.64", width=120) +longitude_name.grid(row=0, column=3, sticky="w", padx=10, pady=10) + +# Row 1 — Latitude +label_lat = customtkinter.CTkLabel(scroll_frame, text="Latitude:") +label_lat.grid(row=1, column=0, sticky="w", padx=10, pady=10) +latitude_name = customtkinter.CTkEntry(scroll_frame, placeholder_text="e.g., 44.05", width=120) +latitude_name.grid(row=1, column=1, sticky="w", padx=10, pady=10) + +# Row 1 (right side) — Date +label_date = customtkinter.CTkLabel(scroll_frame, text="Date:") +label_date.grid(row=1, column=2, sticky="w", padx=10, pady=10) +date_name = customtkinter.CTkEntry(scroll_frame, placeholder_text="MM.DD.YYYY", width=120) +date_name.grid(row=1, column=3, sticky="w", padx=10, pady=10) + +# Row 2 — Time + AM/PM combo +label_time = customtkinter.CTkLabel(scroll_frame, text="Time:") +label_time.grid(row=2, column=0, sticky="w", padx=10, pady=10) +time_frame = customtkinter.CTkFrame(scroll_frame) +time_frame.grid(row=2, column=1, sticky="w", padx=10, pady=10) +curr_time = customtkinter.CTkEntry(time_frame, placeholder_text="00:00", width=80) +curr_time.grid(row=0, column=0, sticky="w", padx=(0, 5)) +combobox = customtkinter.CTkComboBox(time_frame, values=["am", "pm"], width=60) +combobox.grid(row=0, column=1) +combobox.set("am") + +# Row 2 (right side) — Initial Frequency +label_freq_i = customtkinter.CTkLabel(scroll_frame, text="Initial Frequency (MHz):") +label_freq_i.grid(row=2, column=2, sticky="w", padx=10, pady=10) +freq_i_in = customtkinter.CTkEntry(scroll_frame, placeholder_text="1415", width=100) +freq_i_in.grid(row=2, column=3, sticky="w", padx=10, pady=10) + +# Row 3 — Final Frequency +label_freq_f = customtkinter.CTkLabel(scroll_frame, text="Final Frequency (MHz):") +label_freq_f.grid(row=3, column=0, sticky="w", padx=10, pady=10) +freq_f_in = customtkinter.CTkEntry(scroll_frame, placeholder_text="1425", width=100) +freq_f_in.grid(row=3, column=1, sticky="w", padx=10, pady=10) + +# Row 3 (right side) — Integration Time +label_int_time = customtkinter.CTkLabel(scroll_frame, text="Integration Time (s):") +label_int_time.grid(row=3, column=2, sticky="w", padx=10, pady=10) +int_time_in = customtkinter.CTkEntry(scroll_frame, placeholder_text="5", width=100) +int_time_in.grid(row=3, column=3, sticky="w", padx=10, pady=10) + +# Row 4 — Number of Integrations +label_nint = customtkinter.CTkLabel(scroll_frame, text="Number of Integrations:") +label_nint.grid(row=4, column=0, sticky="w", padx=10, pady=10) +nint_in = customtkinter.CTkEntry(scroll_frame, placeholder_text="10", width=100) +nint_in.grid(row=4, column=1, sticky="w", padx=10, pady=10) + +# Row 4 (right side) — Description +label_desc = customtkinter.CTkLabel(scroll_frame, text="Description:") +label_desc.grid(row=4, column=2, sticky="w", padx=10, pady=10) +description = customtkinter.CTkEntry(scroll_frame, placeholder_text="Describe observation", width=280) +description.grid(row=4, column=3, sticky="w", padx=10, pady=10) + +# Fix minimum label widths so text never disappears +MIN_LABEL_WIDTH = 150 + +all_labels = [ + label_user, label_long, label_lat, label_date, label_time, + label_freq_i, label_freq_f, label_int_time, label_nint, label_desc +] + +for lbl in all_labels: + lbl.configure(width=MIN_LABEL_WIDTH) + + +# Row 5 — Switches +switch_frame = customtkinter.CTkFrame(scroll_frame) +switch_frame.grid(row=5, column=0, columnspan=4, pady=10, sticky="w") + +default_parameters_switch = customtkinter.CTkSwitch( + switch_frame, + text="Use Default Parameters", + command=default_parameters, + onvalue="on", + offvalue="off" +) + +biasT_switch = customtkinter.CTkSwitch( + switch_frame, + text="Enable Bias-T", + command=biasT_switch, + onvalue="on", + offvalue="off" +) + +system_date_time_switch = customtkinter.CTkSwitch( + switch_frame, + text="Use System Date and Time", + command=current_date_time, + onvalue="on", + offvalue="off" +) + +default_parameters_switch.grid(row=0, column=0, padx=10) +biasT_switch.grid(row=0, column=1, padx=10) +system_date_time_switch.grid(row=0, column=2, padx=10) + + +# Row 6 — Buttons +button_frame = customtkinter.CTkFrame(scroll_frame) +button_frame.grid(row=6, column=0, columnspan=4, pady=(15, 5), sticky="w") +start_button = customtkinter.CTkButton(button_frame, text="Start", command=start) +stop_button = customtkinter.CTkButton(button_frame, text="Stop", command=stop) +jupyter_button = customtkinter.CTkButton(button_frame, text="Open Jupyter Hub", command=open_jupyter) +local_jupyter_button = customtkinter.CTkButton(button_frame, text="Open Local Jupyter", command=open_local_jupyter) +start_button.grid(row=0, column=0, padx=10) +stop_button.grid(row=0, column=1, padx=10) +jupyter_button.grid(row=0, column=2, padx=10) +local_jupyter_button.grid(row=0, column=3, padx=10) + + +# Enhanced hint label +# It will only shows for active entry +active_hint_label = None # global tracker + +# Enhanced responsive hint labels +active_hint_label = None +HINT_MODE = "below" # this will dynamically switch based on window width + +def add_hint_label(entry_widget, hint_text): + + global active_hint_label + hint_label = customtkinter.CTkLabel( + master=scroll_frame, + text=hint_text, + text_color="gray50", + font=("Arial", 9) + ) + hint_label.place_forget() + + def show_hint(event): + global active_hint_label, HINT_MODE + + # Hide previous hint + if active_hint_label and active_hint_label != hint_label: + active_hint_label.place_forget() + + # Get widget coords relative to scroll_frame + entry_x = entry_widget.winfo_x() + entry_y = entry_widget.winfo_y() + + if HINT_MODE == "side": + # Side placement + hint_label.place( + x=entry_x + entry_x/2, + y=entry_y + ) + else: + # Below placement + hint_label.place( + x=entry_x, + y=entry_y + entry_widget.winfo_height()/2 + ) + + active_hint_label = hint_label + + def hide_hint(event): + hint_label.place_forget() + + entry_widget.bind("", show_hint) + entry_widget.bind("", hide_hint) + + #here are the two switches at the bottom of each side. You can view the location with relx and rely -system_date_time_switch = customtkinter.CTkSwitch(master=app, text="Use System Date and Time", command=current_date_time, onvalue="on", offvalue="off") -system_date_time_switch.pack(padx=20, pady=10) -system_date_time_switch.place(relx=0.7, rely=.6, anchor=tkinter.CENTER) +# system_date_time_switch = customtkinter.CTkSwitch(master=app, text="Use System Date and Time", command=current_date_time, onvalue="on", offvalue="off") +# system_date_time_switch.pack(padx=20, pady=10) +# system_date_time_switch.place(relx=0.7, rely=.6, anchor=tkinter.CENTER) + +# Add hint labels for all entries with appropriate positions +# Position can be "below" or "beside" as needed +add_hint_label(user_name, "Your WSU username or observer name") +add_hint_label(longitude_name, "Longitude in decimal degrees (East/West)") +add_hint_label(latitude_name, "Latitude in decimal degrees (North/South)") +add_hint_label(date_name, "Format: MM.DD.YYYY") +add_hint_label(curr_time, "Format: HH:MM with am/pm toggle") +add_hint_label(freq_i_in, "Start frequency in MHz") +add_hint_label(freq_f_in, "End frequency in MHz") +add_hint_label(int_time_in, "Integration time in seconds") +add_hint_label(nint_in, "Number of integrations") +add_hint_label(description, "Short description of observation") + +# Make window responsive +# Responsive layout: switch between 2-column and 1-column when resizing +def on_resize(event): + width = app.winfo_width() + + # Threshold for two-column layout +def on_resize(event): + width = app.winfo_width() + + # Making the hint global to adjust its position + global HINT_MODE + if width < 900: + HINT_MODE = "side" + else: + HINT_MODE = "below" + if width < 900: + + # Reflow input fields (already working) + label_user.grid_configure(row=0, column=0) + user_name.grid_configure(row=0, column=1) + + label_long.grid_configure(row=1, column=0) + longitude_name.grid_configure(row=1, column=1) + + label_lat.grid_configure(row=2, column=0) + latitude_name.grid_configure(row=2, column=1) + + label_date.grid_configure(row=3, column=0) + date_name.grid_configure(row=3, column=1) + + label_time.grid_configure(row=4, column=0) + time_frame.grid_configure(row=4, column=1) + + label_freq_i.grid_configure(row=5, column=0) + freq_i_in.grid_configure(row=5, column=1) + + label_freq_f.grid_configure(row=6, column=0) + freq_f_in.grid_configure(row=6, column=1) + + label_int_time.grid_configure(row=7, column=0) + int_time_in.grid_configure(row=7, column=1) + + label_nint.grid_configure(row=8, column=0) + nint_in.grid_configure(row=8, column=1) + + label_desc.grid_configure(row=9, column=0) + description.grid_configure(row=9, column=1) + + switch_frame.grid_configure(row=10, column=0, columnspan=2, sticky="w", padx=10, pady=(20, 10)) + + button_frame.grid_configure(row=11, column=0, columnspan=2, sticky="w", padx=10, pady=(10, 20)) + else: + + label_user.grid_configure(row=0, column=0) + user_name.grid_configure(row=0, column=1) + + label_long.grid_configure(row=0, column=2) + longitude_name.grid_configure(row=0, column=3) + + label_lat.grid_configure(row=1, column=0) + latitude_name.grid_configure(row=1, column=1) + + label_date.grid_configure(row=1, column=2) + date_name.grid_configure(row=1, column=3) + + label_time.grid_configure(row=2, column=0) + time_frame.grid_configure(row=2, column=1) + + label_freq_i.grid_configure(row=2, column=2) + freq_i_in.grid_configure(row=2, column=3) + + label_freq_f.grid_configure(row=3, column=0) + freq_f_in.grid_configure(row=3, column=1) + + label_int_time.grid_configure(row=3, column=2) + int_time_in.grid_configure(row=3, column=3) + + label_nint.grid_configure(row=4, column=0) + nint_in.grid_configure(row=4, column=1) + + label_desc.grid_configure(row=4, column=2) + description.grid_configure(row=4, column=3) + + switch_frame.grid_configure(row=5, column=0, columnspan=4, sticky="w", padx=10, pady=10) + + button_frame.grid_configure(row=6, column=0, columnspan=4, sticky="w", padx=10, pady=10) + +# Bind the handler +app.bind("", on_resize) + +app.rowconfigure(0, weight=1) +app.columnconfigure(0, weight=1) + app.mainloop() diff --git a/daq/chart_observe_flet.py b/daq/chart_observe_flet.py new file mode 100644 index 0000000..6ca4b50 --- /dev/null +++ b/daq/chart_observe_flet.py @@ -0,0 +1,502 @@ +#!/usr/bin/python3 +import os +import subprocess +import datetime +import shutil +import threading +import webbrowser +import time as time_mod + +import flet as ft # Flet for GUI to become modern and responsive + +# Defaults (same as our old Tk app) +DEFAULT_FREQ_I = "1415" +DEFAULT_FREQ_F = "1425" +DEFAULT_INT_TIME = "5" +DEFAULT_NINT = "10" + + + +# Backend helpers (preserve behavior) +def _normalize_date(date_str: str) -> str: + # Your original code replaces "/" with "." and expects MM.DD.YYYY + return (date_str or "").replace("/", ".") + + +def _normalize_user(user: str) -> str: + # Your original code replaced "_" with "." + return (user or "").replace("_", ".") + + +def _ensure_data_dir() -> str: + home = os.path.expanduser("~") + data_dir = os.path.join(home, "data") + if not os.path.isdir(data_dir): + os.mkdir(data_dir, mode=0o1777) + print(f"Directory '{data_dir}' is built!") + else: + print("directory data already exists") + return data_dir + + +def _make_observation_dir_name(user: str, lon: str, lat: str, date_str: str, time_str: str, ampm: str) -> str: + # Matches our existing naming pattern (no "location", no "trial") + # directory = f"{user}_lon{lon}_lat{lat}_{year}.{month}.{day}_{time.replace(':','.')}_{ampm}" + month, day, year = date_str.split(".") + date_y_m_d = f"{year}.{month}.{day}" + return f"{user}_lon{lon}_lat{lat}_{date_y_m_d}_{time_str.replace(':', '.')}_{ampm}" + + +def _apply_system_date_time(date_str: str, time_str: str, ampm: str) -> None: + """ + Preserve your behavior: + - Convert date MM.DD.YYYY and time HH:MM with am/pm into sudo date -s "YYYY-MM-DDTHH:MM:SS" + - Add seconds ":00" + """ + month, day, year = date_str.split(".") + if len(month) == 1: + month = "0" + month + if len(day) == 1: + day = "0" + day + if len(year) == 2: + year = "20" + year # Assuming 21st century for 2-digit years + + hour, minute = time_str.split(":") + if ampm == "pm" and hour != "12": + hour = str(int(hour) + 12) + if len(hour) == 1: + hour = "0" + hour + + # Add seconds + hhmmss = f"{hour}:{minute}:00" + cmd = f'sudo date -s "{year}-{month}-{day}T{hhmmss}"' + os.system(cmd) + + +# Flet App +def main(page: ft.Page): + page.title = "CHART Data Collection" + page.theme_mode = ft.ThemeMode.LIGHT + page.padding = 16 + + # State + state = { + "biasT": False, + "proc": None, + "data_directory": None, + "directory": None, + "stop_requested": False, + } + + # UI Controls + # Inputs (structure matches our old app) + user_tf = ft.TextField( + label="Username", + hint_text="Enter Here", + helper_text="Your WSU username or observer name", + width=320, + ) + lon_tf = ft.TextField( + label="Longitude", + hint_text="e.g., -91.64", + helper_text="Longitude in decimal degrees (East/West)", + width=220, + ) + lat_tf = ft.TextField( + label="Latitude", + hint_text="e.g., 44.05", + helper_text="Latitude in decimal degrees (North/South)", + width=220, + ) + + date_tf = ft.TextField( + label="Date", + hint_text="MM.DD.YYYY", + helper_text="Format: MM.DD.YYYY", + width=220, + ) + time_tf = ft.TextField( + label="Time", + hint_text="HH:MM", + helper_text="Format: HH:MM (use am/pm selector)", + width=140, + ) + ampm_dd = ft.Dropdown( + label="AM/PM", + options=[ft.dropdown.Option("am"), ft.dropdown.Option("pm")], + value="am", + width=120, + ) + + freq_i_tf = ft.TextField( + label="Initial Frequency (MHz)", + hint_text=DEFAULT_FREQ_I, + helper_text="Start frequency in MHz", + width=220, + ) + freq_f_tf = ft.TextField( + label="Final Frequency (MHz)", + hint_text=DEFAULT_FREQ_F, + helper_text="End frequency in MHz", + width=220, + ) + int_time_tf = ft.TextField( + label="Integration Time (s)", + hint_text=DEFAULT_INT_TIME, + helper_text="Integration time in seconds", + width=220, + ) + nint_tf = ft.TextField( + label="Number of Integrations", + hint_text=DEFAULT_NINT, + helper_text="Number of integrations", + width=240, + ) + + desc_tf = ft.TextField( + label="Description", + hint_text="Describe observation", + helper_text="Short description of observation", + width=520, + ) + + # Switches (keep same behaviors) + use_defaults_sw = ft.Switch(label="Use Default Parameters", value=False) + biasT_sw = ft.Switch(label="Enable Bias-T", value=False) + use_system_dt_sw = ft.Switch(label="Use System Date and Time", value=False) + dark_mode_sw = ft.Switch(label="Dark Mode", value=False) + + # Status / logging + status_text = ft.Text(value="Ready.", selectable=True) + snack = ft.SnackBar(content=ft.Text("")) + + page.snack_bar = snack + + # Buttons + start_btn = ft.ElevatedButton(text="Start") + stop_btn = ft.ElevatedButton(text="Stop", disabled=True) + open_jupyter_btn = ft.OutlinedButton(text="Open documentation") + + # Helpers + def _toast(msg: str): + page.snack_bar.content = ft.Text(msg) + page.snack_bar.open = True + page.update() + + def _set_status(msg: str): + status_text.value = msg + page.update() + + def _set_running(running: bool): + start_btn.disabled = running + stop_btn.disabled = not running + page.update() + + def _refresh_system_datetime_fields(): + now = datetime.datetime.now() + date_entry = f"{now.month}.{now.day}.{now.year}" + hour = now.hour + ampm = "am" + if hour >= 12: + ampm = "pm" + if hour > 12: + hour -= 12 + minute = f"{now.minute:02d}" + time_entry = f"{hour}:{minute}" + + # Fill hints/values similarly to your placeholder approach + date_tf.value = date_entry + time_tf.value = time_entry + ampm_dd.value = ampm + + # Disable/enable fields to match the switch behavior + locked = use_system_dt_sw.value is True + date_tf.disabled = locked + time_tf.disabled = locked + ampm_dd.disabled = locked + + page.update() + + # If system datetime is enabled, keep updating every 10 seconds + def _system_dt_loop(): + while True: + time_mod.sleep(10) #this can be increased or decreased + if use_system_dt_sw.value: + _refresh_system_datetime_fields() + + threading.Thread(target=_system_dt_loop, daemon=True).start() + + def _create_zip_watcher(): + """ + Here the Tk version calls create_zip() periodically and if proc ends successfully: + - writes description.txt + - zips the directory + - stops + """ + while True: + time_mod.sleep(10) + proc = state.get("proc") + if not proc: + continue + # If process ended with code 0 + if proc.poll() is not None and proc.poll() == 0: + try: + data_dir = state["data_directory"] + directory = state["directory"] + if not data_dir or not directory: + continue + + _set_status("Creating description.txt and zip archive...") + + desc = desc_tf.value or "" + with open(os.path.join(data_dir, directory, "description.txt"), "w") as f: + f.write(desc) + + shutil.make_archive(os.path.join(data_dir, directory), "zip", data_dir, directory) + _set_status("Zip created. Data collection halted.") + # Auto-stop behavior matches your original + _do_stop(silent=True) + except Exception as e: + _toast(f"Zip creation error: {e}") + + threading.Thread(target=_create_zip_watcher, daemon=True).start() + + # Actions + def _do_stop(silent: bool = False): + proc = state.get("proc") + if proc: + try: + proc.terminate() + except Exception: + pass + state["proc"] = None + _set_running(False) + if not silent: + _set_status("Data collection halted!") + + def _do_start(e=None): + # Gather inputs + user = _normalize_user(user_tf.value.strip() if user_tf.value else "") + lon = (lon_tf.value or "").strip() + lat = (lat_tf.value or "").strip() + + # Use datetime depending on switch + if use_system_dt_sw.value: + # Ensure latest values are present + _refresh_system_datetime_fields() + + date_str = _normalize_date(date_tf.value.strip() if date_tf.value else "") + time_str = (time_tf.value or "").strip() + ampm = (ampm_dd.value or "am").strip() + + # Validate minimum required + if not user: + _toast("Username is required.") + return + if not lon or not lat: + _toast("Longitude and Latitude are required.") + return + if not date_str or not time_str: + _toast("Date and Time are required.") + return + + # Handle defaults switch + freq_i = (freq_i_tf.value or "").strip() + freq_f = (freq_f_tf.value or "").strip() + itime = (int_time_tf.value or "").strip() + nint = (nint_tf.value or "").strip() + + if use_defaults_sw.value: + freq_i, freq_f, itime, nint = DEFAULT_FREQ_I, DEFAULT_FREQ_F, DEFAULT_INT_TIME, DEFAULT_NINT + freq_i_tf.value = freq_i + freq_f_tf.value = freq_f + int_time_tf.value = itime + nint_tf.value = nint + + # Disable fields when using defaults (like the old Tk behavior) + freq_i_tf.disabled = True + freq_f_tf.disabled = True + int_time_tf.disabled = True + nint_tf.disabled = True + else: + # Enable fields + freq_i_tf.disabled = False + freq_f_tf.disabled = False + int_time_tf.disabled = False + nint_tf.disabled = False + + # Fill missing with defaults + if not freq_i: + freq_i = DEFAULT_FREQ_I + if not freq_f: + freq_f = DEFAULT_FREQ_F + if not itime: + itime = DEFAULT_INT_TIME + if not nint: + nint = DEFAULT_NINT + + page.update() + + # Apply system date/time to OS (same as the old script) + try: + _apply_system_date_time(date_str, time_str, ampm) + except Exception as ex: + _toast(f"Date/time apply failed: {ex}") + return + + # Create data directory and observation directory + data_dir = _ensure_data_dir() + directory = _make_observation_dir_name(user, lon, lat, date_str, time_str, ampm) + main_dir = os.path.join(data_dir, directory) + + if os.path.isdir(main_dir): + _toast("File already exists. Change the time before clicking Start.") + return + + os.mkdir(main_dir, mode=0o1777) + state["data_directory"] = data_dir + state["directory"] = directory + + use_directory = os.path.join(data_dir, directory) + print("directory being used:", use_directory) + + # Build command (Tried to have the exact behavior as the old one. But not sure everything is same) + biasT = bool(biasT_sw.value) + state["biasT"] = biasT + + if biasT: + cmd = [ + "freq_and_time_scan.py", + f"--freq_i={freq_i}", + f"--freq_f={freq_f}", + f"--int_time={itime}", + f"--nint={nint}", + f"--data_dir={use_directory}", + "--biasT=True", + ] + else: + cmd = [ + "freq_and_time_scan.py", + f"--freq_i={freq_i}", + f"--freq_f={freq_f}", + f"--int_time={itime}", + f"--nint={nint}", + f"--data_dir={use_directory}", + ] + + try: + proc = subprocess.Popen(cmd) + state["proc"] = proc + except Exception as ex: + _toast(f"Failed to start scan: {ex}") + return + + _set_running(True) + _set_status(f"Running. Output directory: {directory}") + + def _toggle_defaults(e): + # If turning on defaults, disable and fill. else enable + if use_defaults_sw.value: + freq_i_tf.value = DEFAULT_FREQ_I + freq_f_tf.value = DEFAULT_FREQ_F + int_time_tf.value = DEFAULT_INT_TIME + nint_tf.value = DEFAULT_NINT + freq_i_tf.disabled = True + freq_f_tf.disabled = True + int_time_tf.disabled = True + nint_tf.disabled = True + else: + freq_i_tf.disabled = False + freq_f_tf.disabled = False + int_time_tf.disabled = False + nint_tf.disabled = False + page.update() + + def _toggle_system_dt(e): + _refresh_system_datetime_fields() + + def _toggle_dark_mode(e): + page.theme_mode = ft.ThemeMode.DARK if dark_mode_sw.value else ft.ThemeMode.LIGHT + page.update() + + def _open_jupyter(e): + webbrowser.open_new("https://adampbeardsley.github.io/research.html#chart") # It is going to the documentation of the project + # We can change it to the Jupyter Hub link if needed + + # Wire handlers + start_btn.on_click = _do_start + stop_btn.on_click = lambda e: _do_stop() + open_jupyter_btn.on_click = _open_jupyter + use_defaults_sw.on_change = _toggle_defaults + use_system_dt_sw.on_change = _toggle_system_dt + dark_mode_sw.on_change = _toggle_dark_mode + + # Initial state of system datetime fields + _refresh_system_datetime_fields() + + # Responsive Layout + # ResponsiveRow wraps controls naturally (no manual resize code required). This part was my favorite. + # We keep the structure similar to our existing GUI: observer info + time/date + parameters + description + switches + buttons. + form = ft.ResponsiveRow( + columns=12, + spacing=12, + run_spacing=12, + controls=[ + ft.Container(user_tf, col={"sm": 12, "md": 6, "lg": 6}), + ft.Container(lon_tf, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(lat_tf, col={"sm": 12, "md": 3, "lg": 3}), + + ft.Container(date_tf, col={"sm": 12, "md": 4, "lg": 4}), + ft.Container(time_tf, col={"sm": 6, "md": 4, "lg": 4}), + ft.Container(ampm_dd, col={"sm": 6, "md": 4, "lg": 4}), + + ft.Container(freq_i_tf, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(freq_f_tf, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(int_time_tf, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(nint_tf, col={"sm": 12, "md": 3, "lg": 3}), + + ft.Container(desc_tf, col={"sm": 12, "md": 12, "lg": 12}), + ], + ) + + switches = ft.ResponsiveRow( + columns=12, + spacing=12, + controls=[ + ft.Container(dark_mode_sw, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(use_defaults_sw, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(biasT_sw, col={"sm": 12, "md": 3, "lg": 3}), + ft.Container(use_system_dt_sw, col={"sm": 12, "md": 3, "lg": 3}), + ], + ) + + buttons = ft.ResponsiveRow( + columns=12, + spacing=12, + controls=[ + ft.Container(start_btn, col={"sm": 6, "md": 3, "lg": 2}), + ft.Container(stop_btn, col={"sm": 6, "md": 3, "lg": 2}), + ft.Container(open_jupyter_btn, col={"sm": 12, "md": 6, "lg": 8}), + ], + ) + + # Scrollable page content + page.add( + ft.Column( + expand=True, + scroll=ft.ScrollMode.AUTO, + controls=[ + ft.Text("CHART Data Collection", size=20, weight=ft.FontWeight.BOLD), + form, + switches, + buttons, + ft.Divider(), + ft.Text("Status", weight=ft.FontWeight.BOLD), + status_text, + ], + ) + ) + + +if __name__ == "__main__": + ft.app(target=main)