Hardware components
Software apps and online services
Github ElectronJS
Story
Hello everybody! I wanted to share my latest creation on Hackster, which is a an app using ElectronJS to play songs on a buzzer using an
Arduino! This project was a bit challenging as this is my first attempt at making an Electron app.
ElectronJS is software that allows you to make native apps using JavaScript — which makes it super easy to build a desktop app to work with our Arduino!
Idea Background
This idea came to me as I wanted to make something using a buzzer, as I didn't experiment with buzzers often, so I decided to make something. Then it hit me — why not make an application to play music on it?
I also noticed there is a library of songs that can play songs on a buzzer using Johnny-Five, so why not make an app to control a selection of songs over the Arduino? In fact, why not an Electron app??
And that's when I started making my idea!
Wait wait wait...Electron?
Some people may not be aware as to what ElectronJS is. To put it simply, it's software that enables you to make Native applications using the Blink engine, which is the same engine Google Chrome uses! This allows us to make applications that use HTML, CSS, and JavaScript, and allow it to work with Node.js to work with entire file systems if we want to.
For this project, we're simply making an app that communicates down to our server, which is at the same level as our Arduino.
Setting Up The Application
For this project, it's very simple and very bare. The main objectives of the project were to make a working menu bar app, that communicates via websockets to start and stop the songs playing on the buzzer. Also, the app has to know when a song has finished playing to update the interface so the user knows the song is finished.
Making a menubar application is a big step for someone that's new to ElectronJS, as opposed to making a simple Chrome-based application using a typical window. This in itself was quite challenging, but it's not impossible.
Application Structure
For this project, I set out to fragment the application to work as an app that works with websockets, in order for the commands to be sent down to the server, which was on the same level as the robot's programming.
project
├─-app
| └
├
├
├
└
In this case, main.js is where all of our Electron's application scripts go. It calls on an index.html file to use as the front end, with app/index.js being the front end's scripting. The robot.js file is used for the Arduino and also the server.
There is also a config file, which is used to store the hostname and port for these files.
Making the Electron Tray
Since this project is merely a menubar app, this is needing some configuration to not only show the window, but also make what is known as a 'tray'.
let win;
let tray;
app.on('ready', () => {
makeTray();
makeWindow();
});
const appIcon = path.join(__dirname, 'static/images/tray-icon.png');
const appIconHighlighted = path.join(__dirname, 'static/images/tray-icon-highlight.png');
const makeTray = () => {
tray = new Tray(appIcon);
tray.setToolTip(config.appName);
tray.on('click', function(event) {
toggleWindow();
if (win.isVisible() && process.defaultApp && event.metaKey) {
win.openDevTools({ mode: 'detach' })
}
});
if (process.platform == 'darwin') {
tray.setPressedImage(appIconHighlighted);
}
}
const makeWindow = () => {
win = new BrowserWindow({
width: 300,
height: 570,
show: false,
frame: false,
resizable: false,
fullscreen: false,
transparent: true,
title: config.appName
});
win.loadURL(`file://${path.join(__dirname, 'index.html')}`);
win.on('blur', () => {
if(!win.webContents.isDevToolsOpened) {
win.hide();
}
});
}
const toggleWindow = () => {
if (win.isVisible()) {
win.hide()
} else {
showWindow();
}
}
const showWindow = () => {
const trayPos = tray.getBounds();
const winPos = win.getBounds();
let x, y = 0;
if (process.platform === 'darwin') {
x = Math.round(trayPos.x + (trayPos.width / 2) - (winPos.width / 2));
y = Math.round(trayPos.y + trayPos.height);
}
win.setPosition(x, y, false);
win.show();
win.focus();
}
Using the code above in main.js, the Electron app will make a menubar tray, which, when toggled, will show or hide our application. On MacOS/OS X, this will place our application window directly beneath the centre of our app's tray icon!
One other thing required for the menu tray to work is to show it during an event fired from the ipcRenderer, from our application front end, so under our scripts for app/main.js we send the 'show-window' event once the application's DOM (Document Object Model) has been loaded.
// app/index.js
const { ipcRenderer } = require('electron');
document.addEventListener('DOMContentLoaded', () => {
// Fire the `show-window` event for the ipc in Electron
ipcRenderer.send('show-window');
});
ipcMain.on('show-window', () => {
showWindow();
});
And now we've got the application running as a menubar application!
Making Our Arduino's Server
In `robot.js`, a server can be made that's exclusively for taking data sent from the application's front end, and vice versa. The idea is to build a server that runs under a specific port, with Websockets running on the server.
const { Board, Piezo, Led } = require('johnny-five');
const express = require('express');
const { Server } = require('http');
const socketIO = require('socket.io');
const songs = require('j5-songs');
const config = require('./config');
const app = express();
const http = Server(app);
const io = socketIO.listen(http);
const board = new Board();
http.listen(config.port, () => {
console.log(`Server Running under *:${config.port}. Remember to run 'yarn start' to run the app.`);
});
board.on('ready', function() {
console.log('board ready');
const buzzer = new Piezo(3);
io.on('connect', (client) => {
client.on('join', handshake => {
io.emit('robot-connected', 'Robot Connected');
console.log(handshake);
});
client.on('play-song', (song) => {
buzzer.stop();
buzzer.play(songs.load(song), (songEnded) => {
if(songEnded) {
io.emit('song-ended');
}
});
});
client.on('stop-song', () => {
buzzer.stop();
});
});
});
Once the Arduino board is connected, it will wait for the front end to connect and, when successful, will let the front end know the app is connected to the Arduino. It's necessary for the Arduino board to be running alongside the server in order to run the app.
Using Johnny Five and Julian Duque's j5-songs library, sockets can be set up to wait for the front end to press a button, and receive the button's value to play a song on the buzzer. In addition, the server will let the front end know when the song is finished.
const socketIOClient = require('socket.io-client');
const config = require('../config');
const io = socketIOClient(`http://${config.hostName}:${config.port}`);
Another thing to add in the application's front end scripts is to set up the Websockets client, and where the sockets should connect to!
Building The Front End and Making It Communicate
Now that the app and server is running, a front end has to be built using HTML, CSS, and JavaScript. The main part of our HTML is an unordered list that contains a set number of songs for buzzer.
<ul class="playlist app-controls" id="playlist">
<li class="playlist__item">
<div class="playlist__item__label">
<span>Super Mario</span>
</div>
<div class="playlist__item__button">
<button class="c-button" data-song="mario-fanfare">Play</button>
</div>
</li>
<li class="playlist__item">
<div class="playlist__item__label">
<span>Star Wars</span>
</div>
<div class="playlist__item__button">
<button class="c-button" data-song="starwars-theme">Play</button>
</div>
</li>
<li class="playlist__item">
<div class="playlist__item__label">
<span>Never Gonna Give You Up</span>
</div>
<div class="playlist__item__button">
<button class="c-button" data-song="never-gonna-give-you-up">Play</button>
</div>
</li>
<li class="playlist__item">
<div class="playlist__item__label">
<span>Nyan Cat</span>
</div>
<div class="playlist__item__button">
<button class="c-button" data-song="nyan-melody">Play</button>
</div>
</li>
<li class="playlist__item">
<div class="playlist__item__label">
<span>Tetris</span>
</div>
<div class="playlist__item__button">
<button class="c-button" data-song="tetris-theme">Play</button>
</div>
</li>
</ul>
In the button of each item, there's a data attribute called data-song that contains the Song ID of the list item's song, as specified in the Songs table in the j5-songs repository. The value stored in the data-song attribute will be sent to the server so the buzzer knows which song to play!