Skip to content
Snippets Groups Projects
keychain-utility.py 17.4 KiB
Newer Older
from tkinter import ttk, filedialog, scrolledtext, messagebox
from PIL import Image, ImageTk, ImageEnhance, ImageOps
import subprocess
import os

class UNLKeychainProgrammerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("UNL Keychain Utility")
        self.root.geometry("800x600")

        # Create a notebook (tabbed interface)
        self.notebook = ttk.Notebook(root)
        self.notebook.pack(fill=tk.BOTH, expand=True)

        # Page 1: UNL Keychain Programmer
        self.page1 = ttk.Frame(self.notebook)
        self.notebook.add(self.page1, text="Keychain Programmer")

        # Terminal output window
        self.terminal_output = scrolledtext.ScrolledText(self.page1, wrap=tk.WORD, width=80, height=20)
        self.terminal_output.pack(padx=10, pady=10)
        
        # Add initial text to the terminal output window
        self.terminal_output.insert(tk.END, "> Welcome to the UNL Keychain Utility application v1.0.\n")
        self.terminal_output.insert(tk.END, "> To begin programming your keychain, select the '.bin' file from the 'build' folder.\n")
        self.terminal_output.insert(tk.END, "> Create custom images using the Bitmap Playground tab.\n\n")        

        # Select File button
        self.select_file_button = ttk.Button(self.page1, text="Select File", command=self.select_file)
        self.select_file_button.pack(pady=5)

        # Flash Firmware button
        self.flash_firmware_button = ttk.Button(self.page1, text="Flash Firmware", command=self.flash_firmware)
        self.flash_firmware_button.pack(pady=5)
        
         # Button to toggle options visibility
        self.toggle_options_button = ttk.Button(self.page1, text="Options", command=self.toggle_options)
        self.toggle_options_button.pack(pady=5)

        # Frame to hold checkboxes (initially hidden)
        self.options_frame = ttk.Frame(self.page1)
        self.options_visible = False

        # Checkboxes for options
        self.options_vars = {}
        for option in ["-u", "-l", "-e", "-o", "-G", "-R"]:
            var = tk.BooleanVar()
            self.options_vars[option] = var
            checkbox = ttk.Checkbutton(self.options_frame, text=option, variable=var)
            checkbox.pack(anchor=tk.W)

        # Page 2: Image to Bitmap Converter
        self.page2 = ttk.Frame(self.notebook)
        self.notebook.add(self.page2, text="Bitmap Playground")

        # Upload Image button
        self.upload_image_button = ttk.Button(self.page2, text="Upload Image", command=self.upload_image)
        self.upload_image_button.pack(pady=10)

        # Brightness label
        self.brightness_label = ttk.Label(self.page2, text="Brightness:")
        self.brightness_label.pack(pady=5)

        # Brightness slider with labels
        self.brightness_frame = ttk.Frame(self.page2)
        self.brightness_frame.pack(pady=5)

        self.brightness_label_left = ttk.Label(self.brightness_frame, text="0%")
        self.brightness_label_left.pack(side=tk.LEFT, padx=5)

        self.brightness_slider = ttk.Scale(self.brightness_frame, from_=0, to=1, value=1, orient=tk.HORIZONTAL)
        self.brightness_slider.pack(side=tk.LEFT, fill=tk.X, expand=True)

        self.brightness_label_right = ttk.Label(self.brightness_frame, text="100%")
        self.brightness_label_right.pack(side=tk.LEFT, padx=5)

        # Mono Mode button
        self.mono_mode_var = tk.BooleanVar()
        self.mono_mode_button = ttk.Checkbutton(self.page2, text="Mono Mode", variable=self.mono_mode_var, command=self.toggle_mono_mode)
        self.mono_mode_button.pack(pady=5)

        # Generate Image button
        self.generate_image_button = ttk.Button(self.page2, text="Generate Bitmap", command=self.generate_image)
        self.generate_image_button.pack(pady=10)

        # Image preview window
        self.image_preview_label = ttk.Label(self.page2)
        self.image_preview_label.pack(pady=10)

        # C Array output text box
        self.c_array_text = scrolledtext.ScrolledText(self.page2, wrap=tk.WORD, width=100, height=13)
        self.c_array_text.pack(padx=10, pady=10)

        # Add a "Copy to Clipboard" button
        self.copy_button = ttk.Button(self.page2, text="Copy Code to Clipboard", command=self.copy_to_clipboard)
        self.copy_button.pack(pady=10)

        # Page 3: GIF to Bitmap Converter
        self.page3 = ttk.Frame(self.notebook)
        self.notebook.add(self.page3, text="GIF Playground")

        # Upload GIF button
        self.upload_gif_button = ttk.Button(self.page3, text="Upload GIF", command=self.upload_gif)
        self.upload_gif_button.pack(pady=10)

        # Label to display the number of frames
        self.frame_count_label = ttk.Label(self.page3, text="Original number of frames: 0")
        self.frame_count_label.pack(pady=5)

        # Label to display the slider value (frames to output)
        self.frame_slider_value_label = ttk.Label(self.page3, text="Frames to output: 0")
        self.frame_slider_value_label.pack(pady=5)

        # Slider for number of frames
        self.frame_slider_label = ttk.Label(self.page3)
        #self.frame_slider_label.pack(pady=5)

        self.frame_slider = ttk.Scale(self.page3, from_=1, to=100, orient=tk.HORIZONTAL)
        self.frame_slider.pack(pady=5)

        # Bind the slider to update the label
        self.frame_slider.config(command=self.update_slider_value_label)

        # Checkbox for retaining aspect ratio
        self.aspect_ratio_var = tk.BooleanVar(value=True)
        self.aspect_ratio_checkbox = ttk.Checkbutton(self.page3, text="Retain Aspect Ratio", variable=self.aspect_ratio_var)
        self.aspect_ratio_checkbox.pack(pady=5)

        # Save GIF button
        self.save_gif_button = ttk.Button(self.page3, text="Generate GIF Frame Code", command=self.save_gif)
        self.save_gif_button.pack(pady=10)

        # GIF preview window
        self.gif_preview_label = ttk.Label(self.page3)
        self.gif_preview_label.pack(pady=10)
        # C Array output text box
        self.gif_array_text = scrolledtext.ScrolledText(self.page3, wrap=tk.WORD, width=100, height=13)
        self.gif_array_text.pack(padx=10, pady=10)

        # Add a "Copy to Clipboard" button
        self.copy_text = ttk.Button(self.page3, text="Copy Code to Clipboard", command=self.copy_to_clipboard)
        self.copy_text.pack(pady=10)

        # Variable to track the preview loop
        self.after_id = None

    def toggle_options(self):
        """Toggle the visibility of the options frame."""
        if self.options_visible:
            self.options_frame.pack_forget()
        else:
            self.options_frame.pack(pady=5, fill=tk.X)
        self.options_visible = not self.options_visible

    def select_file(self):
        file_path = filedialog.askopenfilename(filetypes=[("Binary files", "*.bin")])
        if file_path:
            self.terminal_output.insert(tk.END, f"Selected file: {file_path}\n")
            self.selected_file = file_path
        else:
            self.terminal_output.insert(tk.END, "No file selected.\n")

    def flash_firmware(self):
        if hasattr(self, 'selected_file'):
            selected_options = [option for option, var in self.options_vars.items() if var.get()]
            command = f"puyaisp {' '.join(selected_options)} -f {self.selected_file}"
            
            # Display the command being executed
            self.terminal_output.insert(tk.END, f"Executing command: {command}\n")
            
            try:
                result = subprocess.run(command, shell=True, capture_output=True, text=True)
                self.terminal_output.insert(tk.END, result.stdout)
                self.terminal_output.insert(tk.END, result.stderr)
            except Exception as e:
                self.terminal_output.insert(tk.END, f"Error: {e}\n")
        else:
            self.terminal_output.insert(tk.END, "No file selected. Please select a file first.\n")

    def upload_image(self):
        file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.png *.bmp")])
        if file_path:
            self.image_path = file_path
            self.show_image_preview(file_path)
        else:
            self.image_preview_label.config(text="No image selected.")

    def toggle_mono_mode(self):
        """Enable or disable the brightness slider based on mono mode."""
        if self.mono_mode_var.get():
            self.brightness_slider.config(state=tk.DISABLED)  # Disable the slider
        else:
            self.brightness_slider.config(state=tk.NORMAL)  # Enable the slider

    def generate_image(self):
        if hasattr(self, 'image_path'):
            brightness = self.brightness_slider.get() if not self.mono_mode_var.get() else None
            use_second_method = self.mono_mode_var.get()

            if use_second_method:
                pixels = self.process_image_mono(self.image_path)
            else:
                pixels = self.process_image_regular(self.image_path, brightness)

            c_arr = self.generate_c_array(pixels, "const uint8_t custom_frame[1024] = {")
            self.c_array_text.delete(1.0, tk.END)
            self.c_array_text.insert(tk.END, c_arr)

            # Show the processed image preview
            base_filename = os.path.basename(self.image_path)
            processed_image_path = f'monochrome_{base_filename}'
            self.show_image_preview(processed_image_path)
        else:
            self.c_array_text.delete(1.0, tk.END)
            self.c_array_text.insert(tk.END, "No image selected. Please upload an image first.")

    def copy_to_clipboard(self):
        """Copy the content of the ScrolledText widget to the clipboard."""
        text = self.c_array_text.get("1.0", tk.END).strip()
        self.root.clipboard_clear()
        self.root.clipboard_append(text)

    def process_image_regular(self, filename, brightness=1.0):
        img = Image.open(filename).convert('L')
        enhancer = ImageEnhance.Brightness(img)
        img = enhancer.enhance(brightness)
        img = img.resize((128, 64))
        img.save('greyscale3.png')

        mono = img.convert('1')
        mono = mono.resize((128, 64))

        pixels = list(mono.getdata())
        white = pixels.count(255)
        black = pixels.count(0)

        if white > black:
            pixels = [not elem for elem in pixels]

        mono = Image.new(mono.mode, mono.size)
        mono.putdata(pixels)

        base_filename = os.path.basename(filename)
        output_filename = f'monochrome_{base_filename}'
        mono.save(output_filename)

        return pixels

    def process_image_mono(self, filename):
        img = Image.open(filename).convert('L')
        img = img.resize((128, 64))

        threshold = 128
        mono = img.point(lambda p: 255 if p > threshold else 0, '1')

        pixels = list(mono.getdata())
        white = pixels.count(255)
        black = pixels.count(0)

        if white > black:
            pixels = [0 if elem == 255 else 255 for elem in pixels]

        mono = Image.new('1', (128, 64))
        mono.putdata(pixels)

        base_filename = os.path.basename(filename)
        output_filename = f'monochrome_{base_filename}'
        mono.save(output_filename)

        return pixels

    def generate_c_array(self, pixels, c_arr_str):
        c_arr = c_arr_str
        q = 0
        for i in range(len(pixels)):
            j = i % 8
            if j == 0 and i != 0:
                c_arr += f"0x{q:02x},"
                q = 0
        start_index = c_arr.find("{") + 1
        end_index = c_arr.find("}")
        hex_values_str = c_arr[start_index:end_index]
        hex_values_list = hex_values_str.split(",")
        reg_bitmap = [int(x.strip(), 16) for x in hex_values_list]

        paged_bitmap = []
        for page in range(8):
            for segment in range(16):
                buff = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
                for col in range(8):
                    b = reg_bitmap[1023 - (page * 128 + segment + col * 16)]
                    for e in range(8):
                        buff[e] = buff[e] | ((b & 0x01) << col)
                        b = b >> 1
                paged_bitmap.extend(buff)
        
        hex_strings = [f"0x{byte:02x}" for byte in paged_bitmap]
        chunks = [hex_strings[i:i + 16] for i in range(0, len(hex_strings), 16)]
        rows = [", ".join(chunk) for chunk in chunks]
        return c_arr_str + "\n    " + ",\n    ".join(rows) + "};"

    def show_image_preview(self, image_path):
        image = Image.open(image_path)
        image = image.resize((128, 64))
        photo = ImageTk.PhotoImage(image)
        self.image_preview_label.config(image=photo)
        self.image_preview_label.image = photo

    def upload_gif(self):
        file_path = filedialog.askopenfilename(filetypes=[("GIF files", "*.gif")])
        if file_path:
            self.gif_path = file_path
            self.show_gif_preview(file_path)
        else:
            self.gif_preview_label.config(text="No GIF selected.")

    def show_gif_preview(self, gif_path):
        if self.after_id:
            self.root.after_cancel(self.after_id)  # Stop the previous loop
        gif = Image.open(gif_path)
        self.gif_frames = []
        self.frame_durations = []
        for frame in range(gif.n_frames):
            gif.seek(frame)
            self.gif_frames.append(gif.copy())
            self.frame_durations.append(gif.info['duration'])
        self.current_frame = 0
        self.frame_count_label.config(text=f"Number of frames: {len(self.gif_frames)}")  # Update frame count
        self.frame_slider.config(to=len(self.gif_frames))  # Update slider max value
        self.frame_slider.set(int(len(self.gif_frames) * 0.75))  # Set default to 75% of total frames
        self.update_slider_value_label(self.frame_slider.get())  # Update the slider value label
        self.update_gif_preview()

    def update_slider_value_label(self, value):
        """Update the label to show the current slider value."""
        self.frame_slider_value_label.config(text=f"Frames to output: {int(float(value))}")

    def update_gif_preview(self):
        if self.gif_frames:
            frame_image = ImageTk.PhotoImage(self.gif_frames[self.current_frame])
            self.gif_preview_label.config(image=frame_image)
            self.gif_preview_label.image = frame_image
            self.current_frame = (self.current_frame + 1) % len(self.gif_frames)
            self.after_id = self.root.after(self.frame_durations[self.current_frame], self.update_gif_preview)

    def save_gif(self):
        if hasattr(self, 'gif_path'):
            num_frames = int(self.frame_slider.get())  # Get the slider value
            retain_aspect_ratio = self.aspect_ratio_var.get()
            output_path = os.path.join(os.getcwd(), "output.gif")
            self.convert_gif_to_bw_gif(self.gif_path, output_path, num_frames, retain_aspect_ratio)
            self.show_gif_preview(output_path)
        else:
            messagebox.showerror("Error", "No valid GIF file uploaded.")

    def convert_gif_to_bw_gif(self, gif_path, output_path, num_frames, retain_aspect_ratio):
        try:
            gif = Image.open(gif_path)
            frames = []
            durations = []
            total_frames = gif.n_frames
            step = total_frames / num_frames

            for i in range(num_frames):
                frame_index = int(i * step)
                gif.seek(frame_index)

                if retain_aspect_ratio:
                    resized_frame = self.resize_with_aspect_ratio(gif, (128, 64))
                else:
                    resized_frame = gif.resize((128, 64))

                bw_frame = resized_frame.convert("1")
                frames.append(bw_frame)
                durations.append(gif.info['duration'])

            frames[0].save(
                output_path,
                save_all=True,
                append_images=frames[1:],
                loop=0,
                duration=durations,
                disposal=2
            )

            self.gif_array_text.delete(1.0, tk.END)
            for idx, frame in enumerate(frames):
                pixels = list(frame.getdata())
                c_arr = self.generate_c_array(pixels, f"const uint8_t gif{idx}[1024] = {{")
                self.gif_array_text.insert(tk.END, c_arr + "\n\n")

            #messagebox.showinfo("Success", f"GIF converted to 128x64 black-and-white GIF and saved as {output_path}")
            return output_path
        except Exception as e:
            messagebox.showerror("Error", f"An error occurred: {e}")
            return None

    def resize_with_aspect_ratio(self, frame, target_size):
        original_width, original_height = frame.size
        target_width, target_height = target_size
        aspect_ratio = original_width / original_height

        if aspect_ratio > (target_width / target_height):
            new_width = target_width
            new_height = int(target_width / aspect_ratio)
        else:
            new_height = target_height
            new_width = int(target_height * aspect_ratio)

        resized_frame = frame.resize((new_width, new_height))
        padded_frame = Image.new("1", target_size, 0)
        padded_frame.paste(resized_frame, (
            (target_width - new_width) // 2,
            (target_height - new_height) // 2
        ))
        return padded_frame

if __name__ == "__main__":
    root = tk.Tk()
    app = UNLKeychainProgrammerApp(root)
    root.mainloop()