New Feature: Remote Web Console

Screenshot of the new NetBurner remote web console. A tick counter outputs the system uptime every 5 seconds, and commands are shown for reboot, time, and help. After reboot the system automatically reconnects.

Tired of scrounging for a USB cable to access your NetBurner serial console? No longer!

A common problem when developing embedded applications is when you’re testing, deploying, or maintaining the program on a device or in the field and physical access is limited. USB cables only stretch so far!

Our team felt this limitation ourselves and with a name like NetBurner you’d expect a fully-network-enabled stack to be possible out of the box, right?

Perfect Timing. Try Our ARM® Embedded Dev Kit Today.

Netburner ARM Cortex M7 embedded Development Kit for IoT product development and industrial automation.

Or, learn more about NetBurner IoT.

So, over the holidays we whipped up a Remote Console powered by WebSockets, accessible over the same HTTP port used by normal NetBurner applications and customizable to your needs.

It’ll be part of the upcoming NNDK release, but we’re also making the code available on its own so you can backport this feature to your NNDK 3.4 apps.

The Code

You can access or browse the files and instructions for this blog post via Github if you prefer! Downloading a ZIP of the repository via the green Code download button at the top right might be easier for you than copy-pasting from this post, your choice.

This project was developed for NetBurner’s NNDK 3.4+ — older versions aren’t guaranteed to be compatible.

All code in this blog post and the Github repository are Copyright NetBurner, Inc, and may only be executed on NetBurner provided hardware. See LICENSE.md in the repository for more details.

Three icons showing a NetBurner microcontroller, a web server in the cloud, and a laptop or mobile device. The NetBurner is captioned "The goal: Submit data to a web server on boot and whenever the NetBurner's DIP switches are changed." The web server is captioned "The method: Create a rudimentary HTTP REST API to facilitate the communication." The laptop is captioned "The result: Data about the physical world is made available to any device with a web browser."
Figure 1: Architecture of our Switch Counter project

Backporting

  • You can skip this section and scroll down to Add Remote Console to Project if you’re living in the future and already have an NNDK version higher than 3.4.x.
  1. Create these files under the specified folders in C:\NetBurner or whatever your NNDK_ROOT is:
				
					// nbrtos\include\remoteconsole.h
#ifndef NB_RMTCONSOLE_H
#define NB_RMTCONSOLE_H
void EnableRemoteConsole();
#endif
				
			
				
					// nbrtos\source\remoteconsole.cpp
#include <init.h>
#include <nbrtos.h>
#include <system.h>
#include <remoteconsole.h>

#include <websockets.h>
#include <iointernal.h>
#include <iosys.h>
#include <utils.h>
#include <tcp.h>
#include <nbrtos.h>
#include <fdprintf.h>

extern http_wshandler *TheWSHandler;
int ws_fd = -1;

void BadRequestResponse(int sock, PCSTR url, PCSTR data);
void NotAvailableResponse(int sock, PCSTR url);
using namespace NB;
int httpstricmp(PCSTR s1, PCSTR sisupper2);

static int ShimFd;

void ShimCallBack(int fd ,FDChangeType ct,void *p);

int MyDoWSUpgrade(HTTP_Request *req, int sock, PSTR url, PSTR rxb)
{
  if (httpstricmp(url, "/STDIO"))
  {
    if (ws_fd < 0)
    {
        int rv = WSUpgrade(req, sock);
        if (rv >= 0)
        {
          ws_fd = rv;
          NB::WebSocket::ws_setoption(ws_fd, WS_SO_TEXT);
          RegisterFDCallBack(ws_fd,ShimCallBack,0); 
          return 2;
        }
        else
        {
          return 0;
        }
    }
    return 0;
  }

  NotFoundResponse(sock, url);
  return 0;
}

int shim_fd;
IoExpandStruct shim_io;
int OldStdio[3];

int ShimRead(int fd, char *buf, int nbytes)
{
  int rv=0;

  if (
    (!dataavail(OldStdio[0])) && 
    ((ws_fd<0) || (!dataavail(ws_fd)))
  ) {
    //Do a select                                               
    fd_set read_fds;                                         
    FD_ZERO(&read_fds);                                      
    FD_SET(fd, &read_fds);                         
    select(FD_SETSIZE, &read_fds,(fd_set *)0,(fd_set *)0,0); 
  }

  if((ws_fd>0)&& dataavail(ws_fd)) rv=read(ws_fd,buf,nbytes);
  else
    if(dataavail(OldStdio[0]))rv= read(OldStdio[0],buf,nbytes);

  bool da=((ws_fd>0)&& dataavail(ws_fd));
  da|=dataavail(OldStdio[0]);

  if(da)
    SetDataAvail(fd);
  else
    ClrDataAvail(fd);
  return rv;
}

int ShimWrite(int fd, const char *buf, int nbytes)
{
  write(OldStdio[1],buf,nbytes);
  if((ws_fd>0)&& (writeavail(ws_fd))) write(ws_fd,buf,nbytes); 
  return nbytes;
}

int ShimClose(int fd)
{
  return 1;
}
int ShimPeek (int fd, char *buf)
{
  return 0;
}

void ShimCallBack(int fd ,FDChangeType ct,void *p)
{
  if(dataavail(fd)) SetDataAvail(shim_fd); 
  if(fd==ws_fd) {
    switch(ct) {
      case eReadSet: break;
      case eWriteSet: break;
      case eErrorSet: {
        int ows=ws_fd;
        ws_fd=-1;
        close(ows);
      }
    }
  }
}

void InitStdioShim()
{
  shim_io.read=ShimRead;
  shim_io.write=ShimWrite;
  shim_io.close=ShimClose;
  shim_io.peek=ShimPeek;

  shim_fd=GetExtraFD(0,&shim_io);
  SetWriteAvail(shim_fd);                                  
  OldStdio[0]=ReplaceStdio(0, shim_fd); 
  OldStdio[1]=ReplaceStdio(1, shim_fd); 
  OldStdio[2]=ReplaceStdio(2, shim_fd); 
  RegisterFDCallBack(OldStdio[0],ShimCallBack,0);
}

int ServeValidResponse(int sock, HTTP_Request &pd)
{
  writestring(sock, "HTTP/1.0 200 OK\r\nPragma: no-cache\r\nContent-Type: application/json\r\n\r\n");
  if(ws_fd>0) 
    writestring(sock, "{\"Valid\":true}");
  else
    writestring(sock, "{\"Valid\":false}");
  return 1;
}
extern const unsigned long console_html_size; 
extern const unsigned char console_html_data[];

int ServeConsoleHtml(int sock,HTTP_Request &pd)
{
  SendHTMLHeader(sock);
  writeall(sock,(const char *)console_html_data,console_html_size);
  close(sock);
  return 1;
}

CallBackFunctionPageHandler ValidWS("ValidWS.json", ServeValidResponse);

CallBackFunctionPageHandler ServeConsole("console.html", ServeConsoleHtml);

void EnableRemoteConsole()
{
  InitStdioShim();
  TheWSHandler = MyDoWSUpgrade;
}

				
			
				
					<!-- nbrtos\source\console.html -->
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
var ws;
var MAX_TERMINAL_LEN = 2000;
var bReconnect=false;

function PutInTerminal(received_msg)
{
  var terminal = document.getElementById("terminal");
  var autoScroll = (terminal.scrollTop == (terminal.scrollHeight - terminal.clientHeight));
  var dataLen = received_msg.length;
  var terminalLen = terminal.value.length;
  // The following clause is to prevent browsers from crashing the page...
  if ((terminalLen + dataLen) > MAX_TERMINAL_LEN) {
    var delta = terminalLen + dataLen - MAX_TERMINAL_LEN;
    terminal.value = terminal.value.substring(delta, terminalLen);
  }
  terminal.value += received_msg;
  if (autoScroll) {
    terminal.scrollTop = terminal.scrollHeight - terminal.clientHeight;
  }
}

function MakeDataSocket(resource, query)
{
  if ("WebSocket" in window) {
      // Let us open a web socket
      host = window.location.hostname;
      port = (window.location.port!='') ? (':'+window.location.port) : '';
      ws = new WebSocket("ws://"+host+port+"/"+resource+"?"+query);

      ws.onopen = function(){};
      ws.onmessage = function (evt)
      {
        var received_msg = evt.data;
        bReconnect=false; 
        PutInTerminal(received_msg);
      };

      ws.onclose = function(e) {
        console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
        delete ws;
        setTimeout(function() {
          MakeDataSocket('stdio');
        }, 1000);
      };

      ws.onerror = function(err) {
        console.error('Socket encountered error: ', err.message, 'Closing socket');
        ws.close();
      };
  } else {
    // The browser doesn't support WebSocket
    alert("WebSocket NOT supported by your Browser!");
  }
}

function closeWebSocket()
{
  if (ws) {
    ws.close();
  }
}

function clearLog()
{
  var terminal = document.getElementById("terminal");
  terminal.value = "";
}

function Checksocket()
{
  fetch('ValidWS.json')
  .then(res => res.json())
  .then((out) => {
    if(out.Valid) return;
    if (ws) 
    {
      if(!bReconnect)PutInTerminal("\nRECONNECTING\n");
      bReconnect=true;
    }
    MakeDataSocket('stdio');
  }).catch(err=>console.log(err));
}

function TextInput()
{
  var input = document.getElementById("inputfield");
  var data = input.value;
  ws.send(data);
  //PutInTerminal(data); // local echo
  input.value = "";
}


window.onload = function() 
{ 
  MakeDataSocket('stdio');
  var interval = setInterval(Checksocket, 5000);
};
</script>
</head>
<body>
<div id="sse">
   <a href="javascript:clearLog()">Clear Log</a>
</div>
<div id="DIV_Term">
<textarea name="terminal" id="terminal" rows="30" cols="100" disabled ></textarea>
</div>
<div>
<textarea name="inputfield" id="inputfield" rows="1" cols="100" oninput="TextInput()"></textarea>
</div>
</body>
</html>

				
			

2. Add these lines to nbrtos\source\library.mak: Make sure the second line is indented with a tab, not spaces.

				
					# nbrtos\source\library.mak

$(OBJDIR)/CONSOLE_html.cpp: $(subst $(NNDK_ROOT)/,,$(LIB_PATH))/console.html
	compfile $< CONSOLE_html_data CONSOLE_html_size $@
	
#...
	
LIB_CPP_SRC		+= $(addprefix $(OBJDIR)/, CONSOLE_html.cpp)


				
			

Add remote console to project

If you prefer, you can also use example-console/main.cpp from the Github repo in a new NetBurner project or copy-pasted as an example.

Add the new include to the top of your project:#include <remoteconsole.h>

Add to your UserMain function if not already:

				
					// UserMain()

  init();                                       // Initialize network stack
  // Start web server, default port 80, required for remote console
  StartHttp();
  WaitForActiveNetwork(TICKS_PER_SECOND * 5);   // Wait for DHCP address
  // Enable remote HTTP "serial" console. Not PW protected.
  EnableRemoteConsole();
				
			
  • Clean your NetBurner System Library (make -j20 clean-nblibs)
  • Run As NetBurner Application (make load)
  • Once uploaded to your NetBurner device, browse to /console.html (all lowercase) at the device’s IP (i.e. http://192.168.1.100/console.html)
  • Any stdout like getchar()‘s output or printf() will be displayed on the console. Stdin is available via charavail() and getchar(). You can implement your own I/O or continue on for a full-fledged example.

Adding custom I/O handling

What’s a remote console without some powerful I/O? This program stores user input in a command buffer and returns data or executes actions based on that input. It’s also available as example-console/main.cpp in the Github repo if you prefer. We’ve implemented “time” and “reboot” commands, as well as debug output every 5 seconds, but you can customize as desired. Obviously this console page will be available without a username or password or encryption to anyone able to access your device’s webpage, so either only use this carefully for debugging/development on trusted networks or implement your own security best practices for access control. We suggest being very careful about handling what’s essentially a web-connected stdin and allowing only extremely limited commands/inputs. We’ve avoided storing over 100 characters in the command buffer for example. Restricting access to HTTPS only and requiring a password to do sensitive things like reboot would be good next steps. When done, your /console.html should look like this:
Screenshot of the new NetBurner remote web console. A tick counter outputs the system uptime every 5 seconds, and commands are shown for reboot, time, and help. After reboot the system automatically reconnects.
				
					// main.cpp
#include <init.h>
#include <nbrtos.h>
#include <iosys.h>
#include <nbstring.h>
#include <remoteconsole.h>
#include <hal.h>
#include <ShutDownNotifications.h>

#define SHUTDOWN_CUSTOM_REBOOT_REASON 100

void StoreCmdBuf(NBString &s, char c) {
  // 100-char command limit for safety
  if (s.length() < 100)
    s += c;
}
void ExecuteCmdBuf(NBString &s) {
  if (s == "time"){
    printf("> Uptime is %lu seconds.\r\n",Secs);
  } else if (s == "reboot") {
    if( NBApproveShutdown(SHUTDOWN_CUSTOM_REBOOT_REASON))
    {
      printf("> Rebooting.\r\n");
      OSTimeDly(TICKS_PER_SECOND * 5);
      ForceReboot();
    }
  } else {
    printf("\r\n> HELP:\r\n> (You typed [%s])\r\n", s.c_str());
    printf("> time: get time\r\n> reboot: reboot\r\n");
  }
}
void ClearCmdBuf(NBString &s) {
  s.clear();
}

void OutputTask(void * pd)
{
  while(1) {
    iprintf("> Tick at %lu\r\n",Secs);
    OSTimeDly(TICKS_PER_SECOND * 5); // delay 5 seconds
  }
}

/*-----------------------------------------------------------------------------
 * UserMain
 *----------------------------------------------------------------------------*/
void UserMain(void * pd)
{
  NBString cmdBuf; // store user-entered commands in a buffer

  init();                                       // Initialize network stack
  //Enable system diagnostics. Probably should remove for production code.
  EnableSystemDiagnostics();
  StartHttp();                                  // Start web server, default port 80
  WaitForActiveNetwork(TICKS_PER_SECOND * 5);   // Wait for DHCP address

  // Enable console.html piped to stdin/stdout
  EnableRemoteConsole();

  // Make a background task for outputting some text without blocking
  OSSimpleTaskCreatewName(OutputTask,20,"Output");

  // Main app loop
  while (1)
  {
    char c=getchar(); // getchar will block and also echo each char to stdout
    if(c=='\n') { // newline executes command and clears buffer
      ExecuteCmdBuf(cmdBuf);
      ClearCmdBuf(cmdBuf);
    } else { // all other chars add to buffer
      StoreCmdBuf(cmdBuf,c);
    }
  }
}
				
			

Changing the console HTML

If the default example console HTML/Javascript isn’t quite to your liking, you can edit the console.html file we put in the nbrtos folder above. It just needs to be compiled into console_html.cpp afterwards for upload to the device.

If you encounter issues with the console.html not updating, run compfile console.html console_html_data console_html_size console_html.cpp manually in your terminal after making any HTML changes. Try to keep the total HTML file size small to avoid issues.

Wrap-up

We hope you enjoy this powerful new ability to manage, monitor, develop and debug your NetBurner projects over the network without cables!

Remember that with great power comes great responsibility, think thrice before exposing network-enabled tools to semi-public or untrusted networks or the Internet! If using these remote access/control features in a production or sensitive environment, please follow OWASP best practices or other industry infosec standards.

Share this post

Subscribe to our Newsletter

Get monthly updates from our Learn Blog with the latest in IoT and Embedded technology news, trends, tutorial and best practices. Or just opt in for product change notifications.

Leave a Reply
Click to access the login or register cheese