Notes on Interfaces and Applications
ffmpeg -i dxh_websockets_switch_large.mp4 -vf scale=400:-2 -vcodec libx264 -crf 20 dxh_websockets_switch.mp4
This week, I set up a web-server-based light switch: On a desktop computer, you activate a web server and webpage with a button; when you click the button on the page, it sends a message to the server, which sends a message to a board over a serial FTDI connection, which makes the board toggle its LED.
1 Installing nodejs
For this project, I had to install nodejs (for running Javascript as a
standalone programming environment) and some nodejs packages,
including serialport
(open serial ports), express@4.15.2
(host web
server), http
(serve http), readline
(keyboard interaction), ws
(web sockets) and express-ws (integrate ws and express). I installed
these packages using the nodejs package manager (npm
).
npm install serialport, express@4.15.2, http, readline, ws, express-ws
2 Example nodejs programs
Here are some useful helper subroutines I've written. Using nodejs, you can:
- Send keypresses over serial (just like Neil's
pyserial
program orpicocom
) - Serve a hardcoded string of html as a website.
- Serve a file as a website.
First I set some global parameters for serial and http; details on
your machine (e.g. the value of serial_port
) may differ:
const server_port = 8081 const client_address = '127.0.0.1' const serial_port = "/dev/ttyUSB0" var baud = 115200
Next, I define the relevant subroutines.
You can send keypresses over serial
(This is just what Neil's pyserial
or picocom
program does; you
can try it out with an echo hello world board.)
example_terminal = function() { // Create a terminal for speaking with a pcb board over serial. // Performs the same function as term.py (from class) or picocom. // Set up serial connection const SerialPort = require('serialport') const Readline = require('@serialport/parser-readline') const port = new SerialPort(serial_port, {baudRate: baud, databits:8, dtr: true, }) // Serial-opening errors will be emitted as an error event port.on('error', function(err) { console.log('Error: ', err.message) }) // Whenever serial data is received, interpret the data as // keycodes and print as a string. port.on('data', function(data) { console.log(data.toString('utf8')) }) // Listen to keyboard in nodejs terminal const readline = require('readline') readline.emitKeypressEvents(process.stdin) process.stdin.setRawMode(true) console.log("Ready for input: ") process.stdin.on('keypress', (str, key) => { // Ctrl-c exits the terminal if(key.ctrl && key.name === 'c') process.exit() var debug = false if (debug) console.log("You pressed the '",str,"' key.", key) port.write(key.sequence) }) }; example_terminal();
You can serve html from a hardcoded string or a file
Run either of these two functions, then visit localhost:8081 to see
the webpage. (If you change server_port
from 8081 to another value,
the page will be hosted on a different port.)
example_http_server_1 = function() { // Create an http server on http://localhost:8081/. // Serve a one-line hardcoded string as an HTML page. var app = require('express')() var http = require('http').Server(app) app.get('/', function(req, res){ res.send('<h1>Hello world</h1>') }) http.listen(server_port, function(){ console.log('listening on *:'+(server_port).toString()); }) }; example_http_server_2 = function(html_file='/example_page.html') { // Create an http server on http://localhost:8081/. // Serve a file as an HTML page. var app = require('express')() var http = require('http').Server(app) app.get('/', function(req, res){ res.sendFile(__dirname + html_file) }); http.listen(server_port, function(){ console.log('listening on *:'+(server_port).toString()) }); };
The second http server serves the simple two-line file named example_page.html
:
<h1>I am an example page.</h1> I'm an html file located at <tt>example_page.html</tt>.
3 Web-enabled serial commands
You can set up an echo server through websockets
Web sockets are a protocol which allow servers to send commands to clients and receive them in return. You can use them to make websites that dynamically update, for example, or that allow users to send chat messages to each other.
We'll use nodejs to open a web socket that will listen for messages. We'll also design a webpage that connects to the opened websocket and allows you to send messages to it. The code for each is pretty simple.
Here's nodejs code for running a server. If you run it, the page
http://localhost:8081
will show an html messsage saying that the
echo server is running. More importantly, it will open a web socket at
http://localhost:8081/echo
that webpages can connect and send
messages to.
example_echo_server = function() { const express = require('express') const app = express() require('express-ws')(app); app.get('/', (req, res) => res.send('(The echo server is running.)')) app.listen(server_port, () => console.log(`Listening on port :${server_port}`)) app.ws('/echo', function(ws, req) { ws.on('message', function(msg) { ws.send(msg) // echo it back }); }); }
The html page is contained in a single file. It contains html elements for the inputs, and client-side javascript code for connecting to the web socket we've just opened.
(! Actually, conveniently, http://websocket.org maintains its own echo server. If you want to try their server before or while troubleshooting your own, you can set the websocket to their echo address; see code below.)
<!--- hello.websockets.echo.html !---> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>WebSockets Echo Demo</title> <script type="text/javascript"> // see: https://blog.teamtreehouse.com/an-introduction-to-websockets window.onload = function() { var status = document.getElementById('status'), textsent = document.getElementById('text_sent'), textreceived = document.getElementById('text_received'), submit = document.getElementById('submit'), form = document.getElementById('submissionform'), inputfield = document.getElementById('inputfield') // ------------------------------------------------------- // For the echo server, you can use either the websocket.org // server, or you can run your own using the // example_echo_server function in hello-serial.js var socket = new WebSocket('ws://echo.websocket.org') var server_port = 8081 var socket = new WebSocket('ws://127.0.0.1:'+server_port.toString()+'/echo') // ------------------------------------------------------- socket.onopen = function(event) { status.innerHTML = "Connected to: " + event.currentTarget.url } socket.onerror = function(error) { console.log("WebSockets error: "+error) } socket.onmessage = function(event) { var message = event.data; textreceived.innerHTML += '<li><span>Received:</span>' + message + '</li>'; } submit.onclick = function(event) { event.preventDefault() // Retrieve the message from the textarea. var message = inputfield.value // Send it to the remote server socket.send(message) // Add it to the log of sent messages textsent.innerHTML += '<li class="sent"><span>Sent:</span>' + message + '</li>'; // Clear the input field inputfield.value = '' return false } } </script> </head> <body> <div id="status">Not connected to anything.</div> <form id="submissionform"> <input id="inputfield"/> <input type="button" id="submit" value="Send"> <br/><br/> </form> <div id="text_sent"></div> <hr/> <div id="text_received"></div> </body> </html>
If you run the echo server in node, then open the above html file in your browser, the page in your browser will enable you to send messages to the echo server and get them back.
(You can try it with and without your echo server running to see the difference it makes.)
You can send serial data over websockets
To put our examples together, let's replace the keyboard input from the serial example with keyboard input in a page running in the browser.
Keypresses on the html webpage will be sent over websockets to the listening nodejs server, which will use our previous function to send them to the microcontroller over serial.
In the reverse direction, when the microcontroller sends serial data to the nodejs server, the data will be pushed over websockets into the waiting webpage.
Here's the code for now; it still has a few weird bugs (can't connect multiple webpages to serial; refreshing the page breaks connection. Serial responses lag behind by one message.) but does work.
Nodejs code:
example_serial_websocket_server = function() { // Set up the web socket server const express = require('express') const app = express() require('express-ws')(app); // Connections maintain a list of who has connected to the // websocket server, so we know who needs to hear serial messages. var connections = [] app.get('/', (req, res) => res.send('(The sockets-serial server is running.)')) app.listen(server_port, () => console.log(`Listening on port :${server_port}`)) // Set up serial connection const SerialPort = require('serialport') const Readline = require('@serialport/parser-readline') const port = new SerialPort(serial_port, {baudRate: baud, databits:8, dtr: true, }) port.on('error', function(err) { console.log('Error: ', err.message) }) // Enable communication between sockets and serial. This is a // little messy (nested), because we have to do the sockets <-- // serial connection in a variable scope where ws is defined. app.ws('/ftdi', function(ws, req) { // sockets <-- serial port.on('data', function(data) { console.log("serial connection sent:", data.toString('utf8')) ws.send(data.toString('utf8')) }) // sockets --> serial ws.on('message', function(msg) { console.log("html page sent:", msg) port.write(Buffer.from(msg)) port.flush() }); }); } example_serial_websocket_server()
And here's the html page:
<!DOCTYPE html> <!--- dxh.websockets.serial.js --> <html lang="en"> <head> <meta charset="utf-8"> <title>WebSockets-to-Serial Demo</title> <script type="text/javascript"> // see: https://blog.teamtreehouse.com/an-introduction-to-websockets window.onload = function() { var status = document.getElementById('status'), inputfield = document.getElementById('inputfield'), outputfield = document.getElementById('outputfield') // The port and address are defined in the nodejs server code. // Here, we're connected to :8081/ftdi var server_port = "8081" var socket = new WebSocket('ws://127.0.0.1:'+server_port+'/ftdi') socket.onopen = function(event) { status.innerHTML = "Connected to: " + event.currentTarget.url } socket.onerror = function(error) { console.log("WebSockets error: "+error) } socket.onmessage = function(event) { var message = event.data; outputfield.innerHTML += message + '<br/>\n'; } inputfield.onkeypress = function(event) { var message = inputfield.value socket.send(message) // textsent.innerHTML += '<li class="sent"><span>Sent:</span>' + message + '</li>'; inputfield.value = '' return true } } </script> </head> <body> <div id="status">Not connected to anything.</div> <input id="inputfield"/> <div id="outputfield"><h2>Received:</h2></div> </body> </html>
Toggle board LED every other character
If your hello-world board has an LED, we can turn this websockets-serial connection into a web based interface for turning it on and off. We'll alter the C code on the board so that whenever it receives a character, it toggles the light:
I modify the main function of Neil's hello.ftdi.44.echo.c
code as
follows. (Note that if your LED pin is not at pin PB2, you'll have to
change all references to PORTB
, DDRB
, PB2
accordingly.)
int main(void) { // // main // static char chr; static char buffer[max_buffer] = {0}; static int index; int light_on = 0; set(PORTB, (1 << PB2)); output(DDRB, (1 << PB2)); // // set clock divider to /1 // CLKPR = (1 << CLKPCE); CLKPR = (0 << CLKPS3) | (0 << CLKPS2) | (0 << CLKPS1) | (0 << CLKPS0); // // initialize output pins // set(serial_port, serial_pin_out); output(serial_direction, serial_pin_out); // // main loop // index = 0; while (1) { // dxh clear(PORTB, (1 << PB2)); set(PORTB, (light_on << PB2)); get_char(&serial_pins, serial_pin_in, &chr); light_on = 1 - light_on; put_string(&serial_port, serial_pin_out, "hello.ftdi.44.echo.c: you typed \""); buffer[index++] = chr; if (index == (max_buffer-1)) index = 0; put_string(&serial_port, serial_pin_out, buffer); put_char(&serial_port, serial_pin_out, '\"'); put_char(&serial_port, serial_pin_out, 10); // new line } }
And since we're ignoring all of the textual information, here's a
streamlined html page that just has a button. It sends the character
'0'
every time the button is pressed; the C code toggles the light
every time it gets a character. (The html page requires the same
example_serial_websocket_server
nodejs backend as the other
websockets-to-serial demo.)
<!DOCTYPE html> <!--- dxh.lightswitch.html !---> <html lang="en"> <head> <meta charset="utf-8"> <title>WebSockets Light Switch Demo</title> <script type="text/javascript"> // see: https://blog.teamtreehouse.com/an-introduction-to-websockets window.onload = function() { var status = document.getElementById('status'), inputfield = document.getElementById('inputfield') // The port and address are defined in the nodejs server code. // Here, we're connected to :8081/ftdi var server_port = "8081" var socket = new WebSocket('ws://127.0.0.1:'+server_port+'/ftdi') socket.onopen = function(event) { status.innerHTML = "Connected to: " + event.currentTarget.url } socket.onerror = function(error) { console.log("WebSockets error: "+error) } socket.onmessage = function(event) { console.log(event.data) } inputfield.onclick = function(event) { socket.send('0') return true } } </script> <style type="text/css"> body, input { font-size:200px; } input { outline:none; border:0.1em solid #888; border-radius:0.1em; } </style> </head> <body> <div id="status">Not connected to anything.</div> <br/> <input id="inputfield" type="button" value="Click me!"/> </body> </html>