Part 1 described the general idea behind Sonar project, hardware components used and Arduino sketch... This second post in "Out of Boredom" series is about C# and JavaScript programs that make it possible to display ultrasonic range sensor data in web browsers. The role of .NET application is to receive messages from Arduino over serial port and broadcast it to clients using SignalR library. JS/HTML5 clients use jquery.signalR lib to obtain information about servo position with distance to obstacles and use this data to render sonar image on canvas:
These links are in previous post, but just to remind you:
1. SonarServer
SonarServer is a .NET 4.5 console app created in Visual Studio Express 2013 for Windows Desktop. It uses Microsoft.AspNet.SignalR.SelfHost and Microsoft.Owin.Cors NuGet packages to create self-hosted SignalR server. ASP.NET SignalR is a library designed to make it easy to create applications that are able to push data to clients running in web browsers. This is in contrast to normal web pages/apps behavior where the client (browser) asks server for action by issuing a request (such as GET or POST). SignalR allows clients to listen for messages send by a server... If possible SignalR will use WebSockets to enable efficient bi-directorial connection. If that option is not available due to either browser or server limitations, it will automatically switch to other push techniques like long polling or Server-Sent Events. When I tested the code on my laptop with Windows 7 Home Premium SP1, long polling was used on IE 11 and SSE in Chrome 37. Server was sending about 20 messages per second and clients didn't have any problems with handling that load (communication was on localhost). Self-hosting means that SignalR server doesn't have to be run on a web server such as IIS - it can exist in plain old console project! If you are completely new to SignalR check this tutorial...
This is SonarServer project structure:
SonarData.cs file contains such struct:
namespace SonarServer
{
public struct SonarData
{
public byte Angle { get; set; }
public byte Distance { get; set; }
}
}
Server will send a list of such objects to clients.
SonarHub.cs contains a class derived form Hub. It doesn't declare any methods but is nonetheless useful. Library will use it to generate JavaScript proxy objects...
using Microsoft.AspNet.SignalR;
namespace SonarServer
{
public class SonarHub : Hub
{
}
}
Startup.cs file looks like this:
using Microsoft.Owin.Cors;
using Owin;
namespace SonarServer
{
class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR();
}
}
}
It configures the SignalR server, letting it support cross-domain connection thanks to UseCors call.
Below are the most important bits of Program.cs file. This code:
using (SerialPort sp = new SerialPort())
{
sp.PortName = "COM3";
sp.ReceivedBytesThreshold = 3;
sp.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
sp.Open();
Console.WriteLine("Serial port opened!");
using (WebApp.Start<Startup>("http://localhost:8080/"))
{
Console.WriteLine("Server running!");
Console.ReadKey();
}
}
is responsible for opening a connection with Arduino over serial port named "COM3" and starting SignalR server on localhost:8080. ReceivedBytesThreshold property allows us to control the amount of bytes received from Arduino before DataReceivedHandler is called. You can increase this value if you want bigger packages of data to be broadcasted by server and rendered by clients. This is the part of DataReceivedHandler method that loads serial port data into a byte array:
int count = sp.BytesToRead;
[] data = new byte[count];
sp.Read(data, 0, count);
Such array of bytes is latter on added to custom buffer and processed to create a list of SonarData objects sent to SignalR clients. Part 1 mentioned that Arduino sends data to PC in bytes (array) packages containing three elements: [255, angle, distance]. The purpose of special 255 value is to separate angle-distance pairs of values which are used to create sonar image. We can't just send [angle, distance] stream from Arduino to PC, because the Server could easily loose track of which value is angle and which is distance. This might happen due to delays, buffering etc. Sure it's not a bulletproof protocol but it work well when I tested it. Lot's not get crazy with that - it's a hobby project, remember? :) Check ProcessSonarData method in repository if you want to see how an array of bytes is turned into SonarData list (with buffering taken into account)...
The last missing piece of SonarServer puzzle is SendSonarDataToClients method:
private static void SendSonarDataToClients(List<SonarData> sonarDataForClients)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<SonarHub>();
hub.Clients.All.sonarData(sonarDataForClients);
Console.WriteLine("Sonar data items sent to clients. Samples count=" + sonarDataForClients.Count);
}
This is the thing that actually broadcasts data to clients running in web browsers. You may be wondering why SonarHub instance is not created directly with new operator and instead GetHubContext method is used. This is because SignalR is responsible for its hubs life cycle. Such code:
SonarHub sonarHub = new SonarHub();
sonarHub.Clients.All.sonarData(sonarDataForClients);
would result in the exception: System.InvalidOperationException: Using a Hub instance not created by the HubPipeline is unsupported." .
2. SonarClient
SonarClient is the subproject responsible for drawing sonar image. It's not a Visual Studio solution - just a few files:
I've tested SonarClient code in IE 11 and Chrome 37 and it worked really well. The assumption is that you run modern browser too. I didn't bother with any feature detection, it's enough for me that I have to write for IE9 at work - well, at least it's not IE 6, huh? ;) But if you want to do such thing I can recommend Modernizr library...
This is content of index.html file (with some boring parts removed for brevity):
<!DOCTYPE html>
<html>
<head>
<title>Sonar - sample code from morzel.net blog post</title>
<style>
/* more */
</style>
</head>
<body>
<div>
<canvas id="sonarImage" width="410" height="210"></canvas>
<table>
<!-- more -->
</table>
</div>
<a href="http://morzel.net" target="_blank">morzel.net</a>
<script src="lib/jquery-1.6.4.js"></script>
<script src="lib/jquery.signalR-2.1.1.js"></script>
<script src="http://localhost:8080/signalr/hubs"></script>
<script src="sonarStats.js"></script>
<script src="sonarImage.js"></script>
<script src="sonarConnection.js"></script>
<script>
$(function () {
sonarImage.init('sonarImage');
sonarConnection.init('http://localhost:8080/signalr');
});
</script>
</body>
</html>
Most important bit of this HTML5 markup is the canvas element used to create sonar image. The page imports jquery and jquery.signalR libraries that make it possible to communicate with the Server. That line is particularly interesting:
<script src="http://localhost:8080/signalr/hubs"></script>
SingalR automatically creates JavaScript proxy objects for server-client messaging, that line lets us load them into page. Last three script references are for JS modules responsible for: displaying info about data received from SonarServer, rendering sonar image and communication with server, respectively. Later there's a short script which initializes the modules after pages DOM is ready.
I will skip the description of sonarStats.js file (nothing fancy there - just filling some table cells). But sonarConnection.js should be interesting for you. This is the whole content:
var sonarConnection = (function () {
'use strict';
var sonarHub, startTime, numberOfMessages, numberOfSamples;
var processSonarData = function (sonarData) {
numberOfMessages++;
$.each(sonarData, function (index, item) {
numberOfSamples++;
sonarImage.draw(item.Angle, item.Distance);
sonarStats.fillTable(item.Angle, item.Distance, startTime, numberOfMessages, numberOfSamples);
});
};
return {
init: function (url) {
$.connection.hub.url = url;
sonarHub = $.connection.sonarHub;
if (sonarHub) {
sonarHub.client.sonarData = processSonarData;
startTime = new Date();
numberOfMessages = 0;
numberOfSamples = 0;
$.connection.hub.start();
} else {
alert('Sonar hub not found! Are you sure the server is working and URL is set correctly?');
}
}
};
}());
The init method sets SignalR hub URL along with sonarData handler and starts a connection with the .NET app. There is also some very basic hub availability check (jquery.signalR library has extensive support for connection related events but lets keep things simple here). processSonarData is invoked in response to Server calling hub.Clients.All.sonarData(sonarDataForClients). The processSonarData function receives an array of objects containing information about servo angle and distance to obstacles. SignalR takes care of proper serialization/deserialization of data - you don't have to play with JSON yourself. $.each function (part of jQuery) is used to invoke sonarImage.draw and sonarStats.fillTable methods for every item in sonarData array...
And here comes the module that changes pairs of angle-distance values into o nice sonar image (whole code of sonarImage.js):
var sonarImage = (function () {
'use strict';
var maxDistance = 100;
var canvas, context;
var fadeSonarLines = function () {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
pixels = imageData.data,
fadeStep = 1,
green,
fadedGreen;
for (var i = 0; i < pixels.length; i += 4) {
green = pixels[i + 1];
fadedGreen = green - fadeStep;
pixels[i + 1] = fadedGreen;
}
context.putImageData(imageData, 0, 0);
};
return {
init: function (canvasId) {
canvas = document.getElementById(canvasId);
context = canvas.getContext('2d');
context.lineWidth = 2;
context.strokeStyle = '#00FF00';
context.fillStyle = "#000000";
context.fillRect(0, 0, canvas.width, canvas.height);
context.translate(canvas.width / 2, 0);
context.scale(2, 2);
},
draw: function (angle, distance) {
context.save();
context.rotate((90 - angle) * Math.PI / 180);
context.beginPath();
context.moveTo(0, 0);
context.lineTo(0, distance || maxDistance); // Treat 0 as above range
context.stroke();
context.restore();
fadeSonarLines();
}
};
}());
Again we have an init method. It obtains 2d drawing context from canvas and uses it to set line width and color (green). It also sets inner color (black) and fills the whole canvas with it. context.translate call is used to move origin of coordinate system to the middle of canvas (horizontally). By default it sits in upper left corner. context.scale is used to make image two times bigger than it would be drawn in default settings. Read this post if you want to know more about canvas coordinates.
The draw method is invoked for each data sample (angle-distance pair) produced by Arduino and broadcasted with SonarServer. The distance to obstacles measured by HC-SR04 sensor is represented as a line. The bigger the distance the longer the line. This happens thanks to beginPath, moveTo, lineTo, and stroke calls. context.rotate method is responsible for showing the angle in which the sensor was pointing while measuring distance (angle of servo arm). As the servo moves around we want to change the direction in which distance line is drawn. Notice that code responsible for drawing the line is surrounded by context.save and context.restore calls. These two ensure that rotation transformation doesn't accumulate between draw method calls...
fadeSonarLines function is responsible for creating the effect of older sonar lines disappearing in a nice gradual way. context.getImageData method returns an array of RGBA values representing the pixels that create current canvas image. The function loops through image data and progressively reduces the intensity of green color component. This way sonar lines fade to black.
And viola - sonar image can be rendered in a browser :)
Isn't it amazing what is now possible on the web platform? I'm not exactly a dinosaur but I remember when displaying a div overlay on a page required use of hidden iframe (because select elements were otherwise rendered above the overlay)... Crazy times ;)