[OoB] Arduino、C#、JavaScript 和 HTML5 声纳 (第 2 部分)
第一部分描述了 Sonar 项目的总体思路、使用的硬件组件和 Arduino 草图……“Out of Boredom”系列的第二篇文章是关于 C# 和 JavaScript 程序,它们可以显示超声波测距传感器数据在浏览器中。
第一部分描述了 Sonar 项目的总体思路、使用的硬件组件和 Arduino 草图……“Out of Boredom”系列的第二篇文章是关于C# 和 JavaScript 程序,它们可以显示超声波测距传感器数据在浏览器中。 .NET 应用程序的作用是通过串行端口接收来自 Arduino 的消息,并使用 SignalR 库将其广播给客户端。JS/HTML5 客户端使用jquery.signalR库来获取有关伺服器位置和到障碍物距离的信息,并使用这些数据在canvas上渲染声纳图像:
这些链接在上一篇文章中,但仅供您参考
- 这个 GitHub 存储库包含 Sonar 项目的所有代码。
- 这个短视频展示了工作的 Sonar。
1. SonarServer
SonarServer 是一个.NET 4.5 控制台应用程序,在 Visual Studio Express 2013 for Windows Desktop 中创建。它使用Microsoft.AspNet.SignalR.SelfHost和Microsoft.Owin.Cors NuGet 包来创建自托管的 SignalR 服务器。ASP.NET SignalR 是一个库,旨在轻松创建能够将数据推送到 Web 浏览器中运行的客户端的应用程序。这与常规的网页/应用程序行为相反,在常规行为中,客户端(浏览器)通过发出请求(如 GET 或 POST)来请求服务器执行操作。SignalR 允许客户端监听服务器发送的消息……如果可能,SignalR 将使用WebSockets来实现高效的双向连接。如果由于浏览器或服务器限制而无法使用此选项,它将自动切换到其他推送技术,如长轮询或服务器发送事件。当我用 Windows 7 Home Premium SP1 的笔记本电脑测试代码时,IE 11 使用长轮询,Chrome 37 使用 SSE。服务器每秒发送约 20 条消息,客户端处理这些消息(通信在本地主机上)没有任何问题。自托管意味着 SignalR 服务器不必运行在 Web 服务器(如 IIS)上——它可以存在于普通的控制台项目中!如果您是 SignalR 的新手,请查看此教程……
这是 SonarServer 项目结构
SonarData.cs 文件包含这样的结构
namespace SonarServer { public struct SonarData { public byte Angle { get; set; } public byte Distance { get; set; } } }
服务器会将此类对象的列表发送给客户端。
SonarHub.cs 包含一个派生自 Hub 的类。它不声明任何方法,但仍然很有用。库将使用它来生成 JavaScript 代理对象……
using Microsoft.AspNet.SignalR; namespace SonarServer { public class SonarHub : Hub { } }
Startup.cs 文件如下所示
using Microsoft.Owin.Cors; using Owin; namespace SonarServer { class Startup { public void Configuration(IAppBuilder app) { app.UseCors(CorsOptions.AllowAll); app.MapSignalR(); } } }
它配置了 SignalR 服务器,通过UseCors
调用使其支持跨域连接。
以下是Program.cs文件中最重要的部分。这段代码
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>("https://:8080/")) { Console.WriteLine("Server running!"); Console.ReadKey(); } }
负责通过名为“COM3”的串行端口打开与 Arduino 的连接,并在localhost:8080
上启动 SignalR 服务器。ReceivedBytesThreshold
属性允许我们控制在调用DataReceivedHandler
之前从 Arduino 接收的字节数。如果您希望服务器广播更大的数据包并通过客户端渲染,可以增加此值。这是DataReceivedHandler
方法中将串行端口数据加载到字节数组的部分
int count = sp.BytesToRead; [] data = new byte[count]; sp.Read(data, 0, count);
这种字节数组之后会被添加到自定义缓冲区并进行处理,以创建发送给 SignalR 客户端的SonarData
对象列表。第一部分提到 Arduino 以字节(数组)包的形式向 PC 发送数据,其中包含三个元素:[255, angle, distance]。特殊255值的目的是分隔用于创建声纳图像的角度-距离对。我们不能仅仅从 Arduino 发送[angle, distance]流到 PC,因为服务器很容易丢失哪个值是角度,哪个值是距离。这可能是由于延迟、缓冲等原因。当然,它不是一个万无一失的协议,但在我测试时效果很好。不要对此过于纠结——这是个爱好项目,还记得吗?:) 查看存储库中的ProcessSonarData
方法,如果您想了解字节数组如何转换为SonarData
列表(考虑到缓冲)……
SonarServer 拼图中最后缺失的一块是SendSonarDataToClients
方法
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); }
这是实际将数据广播给 Web 浏览器中运行的客户端的内容。您可能想知道为什么不直接使用new运算符创建SonarHub
实例,而是使用GetHubContext
方法。这是因为 SignalR 负责其 Hub 的生命周期。这样的代码
SonarHub sonarHub = new SonarHub(); sonarHub.Clients.All.sonarData(sonarDataForClients);
将导致异常:“System.InvalidOperationException: Using a Hub instance not created by the HubPipeline is unsupported.”
2. SonarClient
SonarClient 是负责绘制声纳图像的子项目。它不是 Visual Studio 解决方案——只是一些文件
我在 IE 11 和 Chrome 37 中测试了 SonarClient 代码,效果很好。假设您也使用现代浏览器。我没有费心进行任何功能检测,我必须为 IE9 编写代码已经够了——至少不是 IE 6,嗯?;) 但如果您想这样做,我可以推荐 Modernizr 库……
这是index.html文件的内容(为了简洁起见,省略了一些枯燥的部分)
<!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="https://: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('https://:8080/signalr');
});
</script>
</body>
</html>
这个 HTML5 标记中最重要的部分是用于创建声纳图像的canvas
元素。该页面导入了jquery和jquery.signalR库,它们使得与服务器通信成为可能。那一行尤其有趣
<script src="https://:8080/signalr/hubs"></script>
SingalR 会自动为服务器-客户端消息创建 JavaScript 代理对象,该行允许我们将它们加载到页面中。最后三个脚本引用是负责显示从 SonarServer 接收的数据信息、渲染声纳图像以及与服务器通信的 JS 模块。之后有一个简短的脚本,在页面 DOM 就绪后初始化模块。
我将跳过对sonarStats.js文件的描述(没什么特别的——只是填充一些表格单元格)。但sonarConnection.js应该很有趣。这是全部内容
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?'); } } }; }());
init方法设置了 SignalR中心 URL以及sonarData处理程序并与 .NET 应用程序建立连接。还有一个非常基础的中心可用性检查(jquery.signalR库对连接相关的事件有广泛的支持,但我们还是保持简单吧)。当服务器调用hub.Clients.All.sonarData(sonarDataForClients)
时,会调用processSonarData
。processSonarData
函数接收一个对象数组,其中包含有关伺服器角度和到障碍物距离的信息。SignalR 负责数据的正确序列化/反序列化——您无需自己处理 JSON。$.each函数(jQuery 的一部分)用于为sonarData数组中的每个项目调用sonarImage.draw
和sonarStats.fillTable
方法……
现在是改变角度-距离对并生成精美的声纳图像的模块(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(); } }; }());
我们再次有一个init方法。它从canvas获取2d 绘图上下文,并使用它来设置线条宽度和颜色(绿色)。它还设置了内部颜色(黑色)并用它填充整个canvas。context.translate
调用用于将坐标系的原点移动到canvas的中心(水平方向)。默认情况下,它位于左上角。context.scale
用于使图像比默认设置绘制的大小大两倍。如果您想了解更多关于canvas坐标的信息,请阅读这篇帖子。
draw
方法是为 Arduino 生成的每个数据样本(角度-距离对)调用的,并由 SonarServer 广播。HC-SR04 传感器测量的到障碍物的距离表示为一条线。距离越大,线越长。这要归功于beginPath
、moveTo
、lineTo
和stroke
调用。context.rotate
方法负责显示传感器在测量距离时所指的角度(伺服器臂的角度)。当伺服器移动时,我们希望改变绘制距离线的方向。请注意,负责绘制线的代码被包含在context.save和context.restore调用中。这两个调用确保在draw方法调用之间,旋转变换不会累积……
fadeSonarLines
函数负责创建旧声纳线以渐进方式消失的效果。context.getImageData
方法返回一个 RGBA 值数组,代表构成当前 canvas 图像的像素。该函数循环遍历图像数据并逐渐减小绿色颜色分量的强度。这样,声纳线就会淡入黑色。
瞧——声纳图像可以在浏览器中渲染了:)
Web 平台现在能够做到这些,难道不令人惊叹吗?我不是恐龙,但我还记得当时要在页面上显示一个 div 叠加层需要使用隐藏的 iframe(因为否则 select 元素会渲染在叠加层之上)……真是疯狂的时代;)