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:

  1. Send keypresses over serial (just like Neil's pyserial program or picocom)
  2. Serve a hardcoded string of html as a website.
  3. 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>

Author: Dylan Holmes

Created: 2018-11-29 Thu 22:36

Validate