Week 15 - Fabio Coelho Ribeiro

Prior knowledge

I've already made a bit of interface design in the past mainly with Visual Studio.



Hero shot



Web Serial API

The Web Serial API allows to send and receive data with a serial device using JavaScript.

It allows for example, to use a website to communicate to a microcontroller connected to a computer via USB.

I'll create an interface using the Web Serial API and buttons to control some outputs on a microcontroller directly from my computer.

The interface is composed of :

  1. Two buttons to connect and disconnect from a serial port ;

  2. One button to toggle the value of a LED on a PCB ;

  3. One button and an input box to type and modify the angle of a servomotor on a PCB.

Here's the interface (you can interact with it but be sure to send the code in the microcontroller first, LED on GPIO 3 and servomotor on GPIO0) :

Not connected

LED control :

Servomotor control :

This is the JavaScript code to interact with the buttons and the input box of the interface :

 
                                // Variables
                                let port;               // Serial device connection
                                let writer;             // Data to send to the device
                                let ledState = false;   // Keeps track of the LED status
                                let infoText = "";      // Empty string for display

                                // Displays status messages for the serial port connection
                                const statusLine1 = document.getElementById("statusLine1");
                                const statusLine2 = document.getElementById("statusLine2");

                                // For the "Connect" button
                                document.getElementById("connect").addEventListener("click", async () => {
                                    try {
                                        // Selection of serial port
                                        port = await navigator.serial.requestPort();

                                        // Open the serial connection
                                        await port.open({ baudRate: 115200 });

                                        writer = port.writable.getWriter();

                                        // Updates status messages for first line and sets color to green
                                        statusLine1.textContent = "Connected to :";
                                        statusLine1.style.color = "green";

                                        // Retrieves USB data about the device
                                        const info = port.getInfo();

                                        // Checks vendor and product IDs
                                        if (info.usbVendorId && info.usbProductId) {
                                            infoText = `VID: ${info.usbVendorId}, PID: ${info.usbProductId}`;
                                        } else if (info.usbVendorId) {
                                            infoText = `VID: ${info.usbVendorId}`;
                                        } else {
                                            infoText = "Unknown device";
                                        }

                                        // Updates status messages for second line and sets color to green
                                        statusLine2.textContent = infoText;
                                        statusLine2.style.color = "green";

                                        // Prints message in browser console
                                        console.log("Connected!");
                                    }
                                    // Error handling
                                    catch (err) {
                                        console.error(err);

                                        // Resets UI on failure
                                        statusLine1.textContent = "Not connected";
                                        statusLine2.textContent = "";
                                        statusLine1.style.color = "red";
                                    }
                                });

                                // For the "Disconnect" button
                                document.getElementById("disconnect").addEventListener("click", async () => {
                                    try {
                                        // Closes writer
                                        if (writer) {
                                            await writer.close();
                                            writer.releaseLock();
                                            writer = null;
                                        }

                                        // Closes serial connection
                                        if (port) {
                                            await port.close();
                                            port = null;
                                        }

                                        // Resets UI
                                        statusLine1.textContent = "Not connected";
                                        statusLine2.textContent = "";
                                        statusLine1.style.color = "red";
                                        ledState = false;
                                        document.getElementById("sendLED").textContent = "Turn LED ON";

                                        // Prints message in browser console
                                        console.log("Disconnected");
                                    }
                                    // Error handling
                                    catch (err) {
                                        console.error("Disconnect error:", err);
                                    }
                                });

                                // For the "Turn LED ON/OFF" button
                                document.getElementById("sendLED").addEventListener("click", async () => {
                                    // Checks port connection
                                    if (!writer) {
                                        alert("Not connected");
                                        return;
                                    }

                                    // Button reference
                                    const btn = document.getElementById("sendLED");

                                    // If the LED is OFF
                                    if (!ledState) {
                                        // Converts string "LED ON\n" into bytes
                                        const data = new TextEncoder().encode("LED ON\n");
                                        // Sends data
                                        await writer.write(data);

                                        // Updates UI and state
                                        btn.textContent = "Turn LED OFF";
                                        ledState = true;
                                    }
                                    // If the LED is ON 
                                    else {
                                        // Converts string "LED OFF\n" into bytes
                                        const data = new TextEncoder().encode("LED OFF\n");
                                        // Sends data
                                        await writer.write(data);

                                        // Updates UI and state
                                        btn.textContent = "Turn LED ON";
                                        ledState = false;
                                    }
                                });

                                // For the "Send" button
                                document.getElementById("send").addEventListener("click", async () => {
                                    // Checks port connection
                                    if (!writer) {
                                        alert("Not connected");
                                        return;
                                    }

                                    // Reads input from the text field
                                    const text = document.getElementById("angleValue").value;

                                    // Converts input into bytes
                                    const data = new TextEncoder().encode(text + "\n");

                                    // Send data
                                    await writer.write(data);

                                    // Clears text after sending
                                    document.getElementById("angleValue").value = "";
                                });
                            

Before connecting to the microcontroller, I need to insert a code in it to control the outputs depending on the data received from the interface.

 
                                ###########################
                                ### Code in MicroPython ###
                                ###########################
                                
                                # Modules of the XIAO-RP2040 for GPIOs control and PWM generation
                                from machine import Pin, PWM

                                # Set pin 3 as LED output
                                led = Pin(3, Pin.OUT)

                                # Set pin 0 as servo output
                                servo = PWM(Pin(0))
                                # Set servo frequency
                                servo.freq(50)

                                # Function that converts an angle (0-180) to a duty cycle
                                def set_angle(angle):
                                    # Safety if values are under 0 or above 180
                                    if angle < 0:
                                        angle = 0
                                    if angle > 180:
                                        angle = 180

                                    # Servo pulse width range : ~500µs to ~2500µs
                                    min_us = 500
                                    max_us = 2500

                                    # Angle to pulse width conversion
                                    pulse_us = min_us + (max_us - min_us) * angle / 180

                                    # Pulse width to PWM duty conversion
                                    # RP2040 PWM : 16-bit (0-65535), 20ms period = 20000µs
                                    duty = int(pulse_us * 65535 / 20000)

                                    # Send PWM signal
                                    servo.duty_u16(duty)

                                # Infinite loop
                                while True:
                                    # Variable that reads the shell's data
                                    shell = input()
                                    
                                    # If the shell reads "LED ON"
                                    if shell == "LED ON":
                                        # Turns LED ON
                                        led.value(1)

                                    # If the shell reads "LED OFF"
                                    elif shell == "LED OFF":
                                        # Turns LED OFF
                                        led.value(0)
                                        
                                    # If the shell reads a value between 0 and 180
                                    elif 0 <= int(shell) <= 180:
                                        # Sets servomotor angle
                                        set_angle(int(shell))
                            

The LED is connected to the GPIO 3 and the servomotor to the GPIO 0 of the XIAO-RP2040, if you want to change this, you must change the GPIOs in the MicroPython code.



Windows Forms App

I used Visual Studio to make a Windows Forms App to make an app to control a servomotor with my mouse.

It allows for example, to use a website to communicate to a microcontroller connected to a computer via USB.

The interface is composed of :

  1. Two buttons to refresh and connect or disconnect from a serial port ;

  2. A drawing that can be interacted with to control the servomotor :

  3. One button to close the application ;

Here's the final result :

Here's the C# code :

 
                                // Libraries for drawings and forms
                                using System;
                                using System.Drawing;
                                using System.Windows.Forms;
                                using System.IO.Ports;  // Library for serial communication

                                namespace Servomotor_control
                                {
                                    public partial class Form1 : Form
                                    {
                                        // ========================================== //
                                        // ================= SERIAL ================= //
                                        // ========================================== //

                                        private SerialPort serialPort = new SerialPort();   // Creation of object serialPort
                                        private float lastSentAngle = -1;                   // Latest stored angle

                                        // Method to refresh the detected ports
                                        private void RefreshPorts()
                                        {
                                            comboBoxPorts.Items.Clear();    // Clears the current list

                                            string[] ports = SerialPort.GetPortNames(); // Detects every serial ports
                                            Array.Sort(ports);                          // Sorts the ports alphabetically and numerically

                                            comboBoxPorts.Items.AddRange(ports);    // Adds every port name in the dropdown list

                                            // If a port is available, automatically connect to it
                                            if (comboBoxPorts.Items.Count > 0)
                                                comboBoxPorts.SelectedIndex = 0;
                                        }

                                        // Method to send the angle value through the serial port
                                        private void SendAngle(float angle)
                                        {
                                            // Checks if the new angle is too similar to the last one
                                            if (Math.Abs(angle - lastSentAngle) < 1) return;

                                            // Updates the latest stored angle
                                            lastSentAngle = angle;

                                            // If the serial port is available, send stored angle value
                                            try
                                            {
                                                if (serialPort.IsOpen)
                                                {
                                                    serialPort.WriteLine(((int)angle).ToString());
                                                }
                                            }
                                            catch
                                            {
                                                // ignore disconnect errors
                                            }
                                        }

                                        // =========================================== //
                                        // ================= DRAWING ================= //
                                        // =========================================== //

                                        private float angleDeg = 90f;   // Angle value
                                        private float length = 100f;    // Length of line
                                        private bool dragging = false;  // User interaction detection

                                        private Rectangle rect = new Rectangle(200, 250, 100, 75);  // Creation of rectangle
                                        private PointF endPoint;                                    // Stores the current end of the rotating line

                                        // Drawings and forms
                                        protected override void OnPaint(PaintEventArgs e)
                                        {
                                            base.OnPaint(e);    // Default drawing behavior

                                            var g = e.Graphics;                                                 // Drawing surface
                                            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // Makes lines smoother

                                            // Fills rectangle blue with black border
                                            g.FillRectangle(Brushes.SkyBlue, rect);
                                            g.DrawRectangle(Pens.Black, rect);

                                            // Puts pivot at the top center of the rectangle for the line rotation
                                            PointF pivot = new PointF(
                                                rect.X + rect.Width / 2f,
                                                rect.Y
                                            );

                                            double rad = angleDeg * Math.PI / 180.0;    // Converts degrees to radians

                                            // Line endpoint calculation
                                            endPoint = new PointF(
                                                pivot.X + (float)(Math.Cos(rad) * length),
                                                pivot.Y - (float)(Math.Sin(rad) * length)
                                            );

                                            // Draws a line
                                            g.DrawLine(Pens.Black, pivot, endPoint);

                                            // Draws a line a draggable red circle (endpoint)
                                            int r = 8;
                                            g.FillEllipse(Brushes.Red, endPoint.X - r, endPoint.Y - r, r * 2, r * 2);

                                            // Draws a line a black circle at pivot point
                                            g.FillEllipse(Brushes.Black, pivot.X - 3, pivot.Y - 3, 6, 6);

                                            // Angle value text
                                            string text = $"Angle: {angleDeg:0}°";
                                            SizeF textSize = g.MeasureString(text, this.Font);

                                            // Center text inside the rectangle
                                            PointF textPos = new PointF(
                                                rect.X + (rect.Width - textSize.Width) / 2f,
                                                rect.Y + (rect.Height - textSize.Height) / 2f
                                            );

                                            // Draws angle value text
                                            g.DrawString(text, this.Font, Brushes.Black, textPos);
                                        }

                                        // Checks if user clicked near the red endpoint
                                        private void OnMouseDown(object sender, MouseEventArgs e)
                                        {
                                            if (Distance(e.Location, endPoint) < 20)
                                                dragging = true;
                                        }

                                        // If the red endpoint is dragged
                                        private void OnMouseMove(object sender, MouseEventArgs e)
                                        {
                                            if (!dragging) return;

                                            // Recalculates pivot
                                            PointF pivot = new PointF(
                                                rect.X + rect.Width / 2f,
                                                rect.Y
                                            );
                                            float dx = e.X - pivot.X;
                                            float dy = pivot.Y - e.Y;

                                            // Converts mouse position into an angle
                                            double rad = Math.Atan2(dy, dx);
                                            double deg = rad * 180.0 / Math.PI;

                                            // Only accept angles in the top half of the rectangle between 0° and 180° and sends it to serial port
                                            if (deg >= 0 && deg <= 180)
                                            {
                                                angleDeg = (float)deg;
                                                SendAngle(angleDeg);
                                                Invalidate();
                                            }
                                        }

                                        // Checks how close the mouse click is to the draggable endpoint
                                        private float Distance(Point a, PointF b)
                                        {
                                            float dx = a.X - b.X;
                                            float dy = a.Y - b.Y;
                                            return (float)Math.Sqrt(dx * dx + dy * dy);
                                        }

                                        // =========================================== //
                                        // ================= APP ===================== //
                                        // =========================================== //

                                        public Form1()
                                        {
                                            InitializeComponent();  // Initialization of application

                                            DoubleBuffered = true;  // Enables double buffering (drawing happens off-screen first, then displayed)

                                            // Controls mouse buttons interaction
                                            MouseDown += OnMouseDown;
                                            MouseMove += OnMouseMove;
                                            MouseUp += (s, e) => dragging = false;
                                        }

                                        // Method to close the application
                                        private void btnClose_Click(object sender, EventArgs e)
                                        {
                                            Close();    // Closes the application500
                                        }

                                        // Method to refresh the detected ports
                                        private void btnRefresh_Click(object sender, EventArgs e)
                                        {
                                            RefreshPorts(); // Calls method
                                        }

                                        // Method to connect and disconnect to a port
                                        private void btnConnect_Click(object sender, EventArgs e)
                                        {
                                            // Function to disconnect
                                            if (serialPort.IsOpen)
                                            {
                                                serialPort.Close();
                                                btnConnect.Text = "Connect";
                                                return;
                                            }

                                            try
                                            {
                                                // Function if no ports are selected
                                                if (comboBoxPorts.SelectedItem == null)
                                                {
                                                    MessageBox.Show("Please select a COM port.", "Warning !");  // A message box appears
                                                    return;
                                                }

                                                // Serial port configuration
                                                serialPort.PortName = comboBoxPorts.SelectedItem.ToString();
                                                serialPort.BaudRate = 115200;
                                                serialPort.NewLine = "\n";
                                                serialPort.Open();
                                                btnConnect.Text = "Disconnect";
                                            }
                                            catch (Exception ex)
                                            {
                                                MessageBox.Show(ex.Message);    // A message box appears
                                            }
                                        }

                                        // Method to disconnect the port when closing the application
                                        protected override void OnFormClosing(FormClosingEventArgs e)
                                        {
                                            if (serialPort.IsOpen)
                                                serialPort.Close();

                                            base.OnFormClosing(e);
                                        }

                                        // Method when the application is launched
                                        private void Form1_Load(object sender, EventArgs e)
                                        {
                                            RefreshPorts(); // Calls method
                                        }

                                        private void Form1_Shown(object sender, EventArgs e)
                                        {
                                            this.ActiveControl = null;
                                        }
                                    }
                                }
                            

Before connecting to the microcontroller, I need to insert a code in it to control the output depending on the data received from the interface.

 
                                ###########################
                                ### Code in MicroPython ###
                                ###########################

                                # Modules of the XIAO-RP2040 for GPIOs control and PWM generation
                                from machine import Pin, PWM

                                # Set pin 0 as servo output
                                servo = PWM(Pin(0))
                                # Set servo frequency
                                servo.freq(50)

                                # Function that converts an angle (0-180) to a duty cycle
                                def set_angle(angle):
                                    # Safety if values are under 0 or above 180
                                    if angle < 0:
                                        angle = 0
                                    if angle > 180:
                                        angle = 180

                                    # Servo pulse width range : ~500µs to ~2500µs
                                    min_us = 500
                                    max_us = 2500

                                    # Angle to pulse width conversion
                                    pulse_us = min_us + (max_us - min_us) * angle / 180

                                    # Pulse width to PWM duty conversion
                                    # RP2040 PWM : 16-bit (0-65535), 20ms period = 20000µs
                                    duty = int(pulse_us * 65535 / 20000)

                                    # Send PWM signal
                                    servo.duty_u16(duty)

                                # Infinite loop
                                while True:
                                    # Variable that reads the shell's data
                                    shell = input()
                                        
                                    # If the shell reads a value between 0 and 180
                                    if 0 <= int(shell) <= 180:
                                        # Sets servomotor angle
                                        set_angle(int(shell))
                            

The servomotor to the GPIO 0 of the XIAO-RP2040, if you want to change this, you must change the GPIO in the MicroPython code.



Useful file(s) (Click to download)

  1. Web Serial API MicroPython code (Thonny)
  2. Windows Forms App MicroPython code (Thonny)
  3. Windows Forms App C# code (Thonny)


ChatGPT prompts