Ryan Hoffman's HTMAA Site

Week 12: Interface and Application Programming

Project Plan

I think this week could start pretty straight forward and become incredibly complex. I figured I'd start easy and get the important functionality working and then work on the other parts of the project that need to get done. If I have more time later, I will refine the GUI and add more functionality but I need to get the fundamental parts of the project working. I wanted to start by controlling the LEDs and then write a controller to be able to move the steppers. At the beginning of this week, I did not have the stepper control working yet over multiple nodes so I needed to get that working and in the meantime, I figured I could work on the LED controller. Once the stepper control works, I will write the stepper GUI too.

Execution

My control and communication already existed in python so I wanted to start by making the GUI in python. This led me to TKinter, a builtin python library. I started by watching a few videos about it:

I also found the documentation for CustomTKinter to be quite helpful. With these videos, I was able to get a grasp of how TKinter and CustomTKinter worked and began making my own GUI. The goal for the first one was to be able to simply control each nodes onboard LED.

I started by using all of the code that I orginally used to control the node LEDs, the serial communication test python file which can be found on last weeks page.
                    
    class GUI(ctk.CTk):
        def __init__(self, ser):
            super().__init__()
            self.title('LED Controller')
            self.geometry('500x500')
            self.ser = ser

            self.grid_columnconfigure(0, weight = 1)
            self.grid_columnconfigure(1, weight = 1)

            self.button_1 = ctk.CTkButton(self, text = 'Node 0 On', command = self.b1_on)    
            self.button_2 = ctk.CTkButton(self, text = 'Node 0 Off', command = self.b1_off)
            self.button_3 = ctk.CTkButton(self, text = 'Node 1 On', command = self.b2_on)    
            self.button_4 = ctk.CTkButton(self, text = 'Node 1 Off', command = self.b2_off)
            self.button_5 = ctk.CTkButton(self, text = 'Node 2 On', command = self.b3_on)    
            self.button_6 = ctk.CTkButton(self, text = 'Node 2 Off', command = self.b3_off)

            self.button_1.grid(row=0, column=0, padx=20, pady=20, sticky='ew')
            self.button_2.grid(row=0, column=1, padx=20, pady=20, sticky='ew')
            self.button_3.grid(row=1, column=0, padx=20, pady=20, sticky='ew')
            self.button_4.grid(row=1, column=1, padx=20, pady=20, sticky='ew')
            self.button_5.grid(row=2, column=0, padx=20, pady=20, sticky='ew')
            self.button_6.grid(row=2, column=1, padx=20, pady=20, sticky='ew')

        def b1_on(self):
            print('Node 0 On')
            packet = bytearray([ord('l'), 0, 0b1])
            self.ser.write(packet)
        
        def b1_off(self):
            print('Node 0 Off')
            packet = bytearray([ord('l'), 0, 0b0])
            self.ser.write(packet)

        def b2_on(self):
            print('Node 1 On')
            packet = bytearray([ord('l'), 1, 0b1])
            self.ser.write(packet)
        
        def b2_off(self):
            print('Node 1 Off')
            packet = bytearray([ord('l'), 1, 0b0])
            self.ser.write(packet)

        def b3_on(self):
            print('Node 2 On')
            packet = bytearray([ord('l'), 2, 0b1])
            self.ser.write(packet)
        
        def b3_off(self):
            print('Node 2 Off')
            packet = bytearray([ord('l'), 2, 0b0])
            self.ser.write(packet)
                    
                
For the GUI itself, I started by making a python class named GUI. In my __init__ function, there is a lot that happens. This is where the actual design of the GUI itself happens. I start by defining the size and name of the window that will appear with the title and geometry methods:
                    
        self.title('LED Controller')
        self.geometry('500x500') 
                    
                
I then add a two column grid in the window that I can place the buttons within:
                    
        self.grid_columnconfigure(0, weight = 1)
        self.grid_columnconfigure(1, weight = 1)
                    
                
I then go and create each individual button. The text is what is dispalyed on the button itself and the command is a pointer to the function that I want to execute when the button is pressed. Each button has a unique function that is defined within the class and I create after the initiatlization function.
                    
        self.button_1 = ctk.CTkButton(self, text = 'Node 0 On', command = self.b1_on)    
        self.button_2 = ctk.CTkButton(self, text = 'Node 0 Off', command = self.b1_off)
        self.button_3 = ctk.CTkButton(self, text = 'Node 1 On', command = self.b2_on)    
        self.button_4 = ctk.CTkButton(self, text = 'Node 1 Off', command = self.b2_off)
        self.button_5 = ctk.CTkButton(self, text = 'Node 2 On', command = self.b3_on)    
        self.button_6 = ctk.CTkButton(self, text = 'Node 2 Off', command = self.b3_off)
                    
                
With the buttons made, I then had to place them within the grid that I had created earlier using lines of code like this:
                    
        self.button_1.grid(row=0, column=0, padx=20, pady=20, sticky='ew')
        self.button_2.grid(row=0, column=1, padx=20, pady=20, sticky='ew')
        self.button_3.grid(row=1, column=0, padx=20, pady=20, sticky='ew')
        self.button_4.grid(row=1, column=1, padx=20, pady=20, sticky='ew')
        self.button_5.grid(row=2, column=0, padx=20, pady=20, sticky='ew')
        self.button_6.grid(row=2, column=1, padx=20, pady=20, sticky='ew')
                    
                
With that, the GUI itself has been made. As I mentioned eariler, I also had to define functions within the class to associate with each button press. The functions are almost identical. They print the action that is associated with the function to to the terminal, create a packet of information to be sent over serial to the boards with the specific address of the related board. It then sends that packet over serial.
                    
        def b1_on(self):
            print('Node 0 On')
            packet = bytearray([ord('l'), 0, 0b1])
            self.ser.write(packet)
                    
                
With all of that, the GUI itself was done, and this was the result:

GUI for LEDs
Here is a video of the GUI working with the LEDs:

By this point in the week I had gotten the serial communication working with stepper motors, see week 11 to see details on that system along with the code I used on the microcontrollers and the python for communication with my laptop. Similar to the LEDs, I am going to take the main functions within that python file and modify it to use them within a GUI.

                    
    class GUI(ctk.CTk):
        def __init__(self, ser):
            super().__init__()
            self.title('Node Controller')
            self.geometry('500x500')
            self.ser = ser

            print('Starting GUI')

            self.grid_columnconfigure(0, weight = 1)
            self.grid_columnconfigure(1, weight = 1)

            self.button_1 = ctk.CTkButton(self, text = 'Node 1 On', command = self.b1_on)    
            self.button_2 = ctk.CTkButton(self, text = 'Node 1 Off', command = self.b1_off)
            self.button_3 = ctk.CTkButton(self, text = 'Node 2 On', command = self.b2_on)    
            self.button_4 = ctk.CTkButton(self, text = 'Node 2 Off', command = self.b2_off)
            self.button_5 = ctk.CTkButton(self, text = 'Node 3 On', command = self.b3_on)    
            self.button_6 = ctk.CTkButton(self, text = 'Node 3 Off', command = self.b3_off)

            self.button_1.grid(row=0, column=0, padx=20, pady=20, sticky='ew')
            self.button_2.grid(row=0, column=1, padx=20, pady=20, sticky='ew')
            self.button_3.grid(row=1, column=0, padx=20, pady=20, sticky='ew')
            self.button_4.grid(row=1, column=1, padx=20, pady=20, sticky='ew')
            self.button_5.grid(row=2, column=0, padx=20, pady=20, sticky='ew')
            self.button_6.grid(row=2, column=1, padx=20, pady=20, sticky='ew')

            self.label2 = ctk.CTkLabel(self, text = 'Motor 1 Controls', fg_color = 'transparent')
            self.label3 = ctk.CTkLabel(self, text = 'Motor 2 Controls', fg_color = 'transparent')
            self.entry2 = ctk.CTkEntry(self, placeholder_text='num steps')
            self.entry3 = ctk.CTkEntry(self, placeholder_text='num steps')
            self.b2_move = ctk.CTkButton(self, text = 'Send Position to Node 2', command = self.n2_move)
            self.b3_move = ctk.CTkButton(self, text = 'Send Position to Node 3', command = self.n3_move)
            
            self.label2.grid(row=3, column=0, padx=20, pady=20, sticky='ew')
            self.entry2.grid(row=4, column=0, padx=20, pady=20, sticky='ew')
            self.b2_move.grid(row=4, column=1, padx=20, pady=20, sticky='ew')

            self.label3.grid(row=5, column=0, padx=20, pady=20, sticky='ew')
            self.entry3.grid(row=6, column=0, padx=20, pady=20, sticky='ew')
            self.b3_move.grid(row=6, column=1, padx=20, pady=20, sticky='ew')

        def b1_on(self):
            set_led(ser, 1, 1)
        
        def b1_off(self):
            set_led(ser, 1, 0)

        def b2_on(self):
            set_led(ser, 2, 1)
        
        def b2_off(self):
            set_led(ser, 2, 0)

        def b3_on(self):
            set_led(ser, 3, 1)
        
        def b3_off(self):
            set_led(ser, 3, 0)
        
        def n2_move(self):
            pos = self.entry2.get()
            int_pos = int(pos)
            move_motor(ser, 2, int_pos)

        def n3_move(self):
            pos = self.entry3.get()
            int_pos = int(pos)
            move_motor(ser, 3, int_pos)
                    
                
Here is what that GUI looks like:
GUI for steppers
There are alot of similarities in this GUI to the way the last one was made so I'll mostly be talking about the new widgets from CustomTKinter. It starts again by initializing the GUI object with a size, serial instance, title, another grid and a few buttons. The new widgets used were the label and the entry. The label acts as a text label. The entry enables the user to input a position and then send it to the stepper motors. Once I have the system working I will continue to add features but I want to get it all working first. Within the command functions defined below the initialization function, I use two more functions: set_led() and move_motor()
                    
    def set_led(ser, address_int, state): 
        """
        Command to control an individual LED

        Args:
            ser: serial object
            address_int: address of node with desired LED
            state: choose (0,1) to turn on or off

        Returns:
            None
        """
        addr_byte = struct.pack('B', address_int)
        led_byte = struct.pack('B', state)

        send_packet(ser, addr_byte, CMD_LED, led_byte)
                    
                
This takes the address that is passed in by the command buttons, turns it into a byte address. It also takes the state, 0 or 1, and turns that a byte representation like b'\x01' and then uses the send_packet() function to send the packet along the serial port. I will show the send packet command later in the page.
                    
    def move_motor(ser, address_int, steps):
        """
        Command to move an individual motor using its address and 
        the number of steps you want it to move.

        Args:
            ser: serial object
            addresss_int: address of node to speak to
            steps: an int, the location the stepper moves to

        Returns:
            None
        """

        addr_byte = struct.pack('B', address_int)
        step_bytes = struct.pack('>i', steps)

        send_packet(ser, addr_byte, CMD_MOVE, step_bytes)
        print(f'Move node {address_int} to {steps}')
                    
                
The move_motor() function works in a similar way to the set_led function. It takes in an address int and creates a byte representation of it b'\x02' for address 2. It then creates a 4 byte packet using the big endian form (it just means to create/order the bytes from largest to smallest the way humans read numbers, not smallest to largest). Say I entered 4,868 as the number of steps, the data packet that would be created to represent that integer would look like b'\x00\x00\x13\x04. It then again uses the send_packet() function to send the packet to the nodes.

The send_packet() function looks like this:
                    
    def send_packet(ser, target_addr_byte, cmd_byte, data_bytes):
        """
        Prepares and sends the data packet to the connected serial 
        port and to the nodes

        Args:
            ser: serial object
            target_addr_byte: the address of the node to talk to
            cmd_byte: the command to send the node
            data_bytes: the data to send after the command

        Returns:
            None
        """

        if len(data_bytes) < 4:
            padding = b'\x00'*(4-len(data_bytes))
            data_bytes = padding + data_bytes

        packet = START_MARKER + target_addr_byte + cmd_byte + data_bytes + END_MARKER

        ser.write(packet)
                    
                
This function takes in the serial connection thats been established, the target address (already in byte form), the desired command (in byte form), and then the data (in byte form) to be sent down the chain. It first starts by ensuring that the data packet is 4 bytes long, so in the case of the LED where we originally just had the byte b'\x01, it becomes, b'\x00\x00\x00\x01. It then adds the bytes together and assigns that to a variable named packet and then writes packet to the serial port.

Those are all the components of the system and how it all works. Here is a video showing it in action:

Thoughts and Reflection

This went pretty well! I'm pretty comfortable in python and so it was nice to implement a GUI using it. There are definitley some more features that I would like to add but I will add them in time. Like I said earlier, I want to actually get the system working before I move forward with the controller. I have all of the basic functionalities working that I would need to demonstrate the system, everything else would just be iceing on the cake. It would be cool to add a stop and start feature. I also want to add a homing command to the system. I think it would also be cool to implement a save and replay function so you can store multiple locations and have it execute a series of actions. It would also be awesome to implement an inverse kinematics solver so that it could do IK with whatever system someone puts together.