Have you ever wanted to control or configure an Arduino board using a user-friendly graphical interface? In this blog post, I will explain how to build a GUI with Electron (a popular framework for building cross-platform desktop applications) and Vue. With this application, you’ll be able to list Serial Devices and easily send data to an Arduino with a click of a button.

Preview

gui arduino interface built with electron

For your knowledge

The Electron framework lets you write cross-platform desktop applications using JavaScript, HTML and CSS. It is based on Node.js and Chromium and is used by Atom, GitHub Desktop, Visual Studio Code, WordPress Desktop, Eclipse Theia and many other apps. You can also use community-supported tooling to generate platform-specific tooling like Apple Disk Image (.dmg) on macOS, Windows Installer (.msi) on Windows, or RPM Package Manager (.rpm) on Linux.

If you are not familiar with Electron, I recommend you to read the Quick Start guide from Electron before continuing with this tutorial.

Project

You can find the code for this project on my Github.

Getting started

If you want to use my project as a base for another project, you can start by cloning it and installing all Node.js (install if you don’t have) packages:

git clone https://github.com/ddavidmelo/electron-arduino.git
cd electron-arduino
npm install

To run the application use the command:

npm run electron:serve

If you want to learn on your own and start a project from scratch, you’ll need to have Node.js and Vue already installed. To begin, create a new Vue project:

vue create electron-arduino    [Select Vue3]

In the project folder (cd /electron-arduino) add the following plugins:

vue add quasar                   (Vue popular framework)
vue add electron-builder@alpha   (Electron)
vue add router                   (for mapping URL routes to specific components)
vue add vuex                     (centralized store for all the components in an application)
npm install --save serialport    (to communicate via a COM port)

To run the application use the command:

npm run electron:serve

You should see an output similar to the picture below: gui electron default

Arduino serial commands

For this project I will use an Arduino to receive AT commands from the GUI interface. For that, I will use Arduino Nano 33 BLE Sense. You can find the code in here.

arduino pinout

Supported commands [?=1 Turn ON, ?=0 Turn OFF]:

AT+LED=LEDR:?
AT+LED=LEDG:?
AT+LED=LEDB:?
AT+LED=LED_BUILTIN:?
AT+LED=LED_PWR:?

Other devices can be supported, you only need to program them to accept these commands.

Electron

Configuration

Electron builder can be configured by adding a build section to the vue.config.js file:

vue.config.jsvue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
electronBuilder: {
  nodeIntegration: true,
  externals: ['serialport'],
  customFileProtocol: './',
  builderOptions: {
    appId: 'test.com',
    win: {
      icon: 'public/icon.png',
      target: "portable"
    },
    linux: {
      icon: 'public/icon.icns',
    }
  },
}
...

Remove menubar

Electron by default adds a menu bar on top of the window. To remove that you need to set autoHideMenuBar to true. electron top bar

background.jsbackground.js
1
2
3
4
5
6
7
8
9
...
async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    autoHideMenuBar: true,
    icon: './public/favicon.ico',
  })
}
...

Vue device list

In the DeviceList Vue component is where I list the devices that are connected. The method listSerialPorts() sets the serialports data variable with a list of available serial ports. The method uses the SerialPort library to retrieve a list of ports and filters the ports based on their productId and vendorId properties. It only retrieves devices with valid productId and vendorId. The noDevices data variable is used to show a legend saying that are no devices connected.

DevicesList.vueDevicesList.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
methods: {
  async listSerialPorts() {
    await SerialPort.list().then((ports, err) => {
      if (err != null || err == undefined) {
        let devices = [];
        for (const device of ports) {
          if (device.productId != undefined && device.vendorId != undefined) {
            devices.push(device);
          }
        }
        if(devices.length == 0) {
          this.noDevices = true;
        } else {
          this.noDevices = false;
        }
        this.serialports = devices;
        console.log('ports: ', ports);
      }
    })
  }
}
...

Vue set device

After selecting a device. Is necessary to store the selected port. For that, the method setDevice sets device port and navigates to a new route.

DevicesList.vueDevicesList.vue
1
2
3
4
5
6
7
8
...
setDevice(port) {
  this.$store.commit('setPort', port);     // Vue store
  this.$router.push({
    name: "device"
  });
}
...
To store the port object I am using Vuex plugin. With Vuex I am able to store and retrieve variables from any component. So, to store the port object I am using this action:
this.$store.commit('setPort', port);
I forgot to mention earlier, the port object format is the same as the default_port template object.
Vuex implementation:
store/index.jsstore/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
const default_port = { 
  locationId: "",  
  manufacturer: "",  
  path: "_",
  pnpId: "",
  productId: "",
  serialNumber: "",
  vendorId: "" 
}


export default createStore({
  state: {
    port: default_port,
  },
  getters: {
    port(state) {
      return state.port;
    },
  },
  mutations: {
    setPort(state, port) {
        state.port = port
    },
  }
})

Vue serial port functions

After selecting the device port, it is necessary to open the port and proceed with the methods for sending commands and check if the port is still open.

DeviceView.vueDeviceView.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
...
const serialports = [];
methods: {
  writeCommand(command) {
    serialport.write(command, function (err) {
      if (err) {
        return console.log('Error on write: ', err.message)
      }
      console.log(command)
    })
  },
  checkPortStatus() {
    const parser = new ReadlineParser()
    serialport.pipe(parser)
    if (serialport.isOpen) {
      this.portClosed = false;
    } else {
      this.portClosed = true;
    }
  }
},
mounted() {
  console.log("open serial: ", this.$store.state.port.path);
  serialport = new SerialPort({ path: this.$store.state.port.path, baudRate: 9600 })

  this.timer = setInterval(() => {
    this.checkPortStatus()
  }, 500)
},
unmounted() {
  clearInterval(this.timer)
  serialport.close()
  console.log("close serial: ", this.$store.state.port.path);
}
...
The mounted() function is called when the Vue component is mounted to the DOM. In there, a new SerialPort object is created, with a specific path and baud rate. It also set a timer to call the checkPortStatus() method every 500 milliseconds. The unmounted() function is called when the Vue component is unmounted from the DOM. It clears the timer using clearInterval() and closes the serial port using the serialport.close(). The method writeCommand(command) is called when the Leds are toggled, and the AT commands are emitted from the DeviceActions component.

Build

In order to build the project for distribution your package.json should look something like this:

package.jsonpackage.json
1
2
3
4
5
6
7
8
9
...
  "name": "electron-arduino",
  "version": "0.1.0",
  "private": true,
  "homepage": ".",
  "maintainer": "electron-arduino",
  "email": "electron-arduino@electron.com",
  "author": "electron-arduino, Inc <electron-arduino@electron.com>",
...
You’ll find all os-specific configurations here:

To build the Electron application for both Linux and Windows, you need to run the following command:

npm run electron:build -- --linux deb --win nsis

In the dist_electron folder you will find a Debian package (.deb) and a portable app (.exe) for Windows.
Note: To install the .deb package you need to run this command

sudo dpkg -i <debfilename>

Demo

electron app demo