刷到过一个坐在家里遥控远程小车去店里取餐的视频,一直想自己复刻一个,参考了网上资料发现大多数资料发现都在教使用MQTT通讯来做,但是亲自尝试之后发现MQTT延时太高,无法做到实时画面的传输,在踩了众多坑之后,终于实现了远程无限距离控制、带实时摄像头画面的小车制作,分享下主要的制作细节
大体思路是:使用FRP工具穿透阿里云服务器与本地计算机,穿透之后即可实现计算机与手机之间公网TCP通讯,手机端开发APP发送实时摄像头画面(用不用的旧安卓手机就行,手机需要连WIFI或者插卡),并接收来自电脑端的电机控制指令,手机APP使用OGT数据线和串口模块实现电机控制指令下发到小车单片机。
1.阿里云服务器的穿透
首次开通阿里云服务器ECS会有一段时间的免费额度,还好在额度使用完之前下班之余抽空开发出来了,大家直接去阿里云官网开通就行,具体的配置思路可以参考下面这篇文章FRP穿透,我的云服务ECS和本地电脑都是windows端。在进行下面的步骤之前,这一步一定要先完成。
2.本地个人电脑TCP通讯的程序(Visual Studio,C#语言)
这里我写了四个标志位,分别是WSAD四个按键来表示小车的前行、后退、左转和右转,并且使用openCV实时显示摄像头画面。(偷懒没做UI界面,而且后续看能不能把这部分移植到手机端吧,毕竟在户外带着手机操作要方便点)
using System.Diagnostics;
using OpenCvSharp;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
namespace TCPServer
{
class Program
{
// 用于检测按键状态的WinAPI函数
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
static bool isWPressed = false;
static bool isSPressed = false;
static bool isAPressed = false;
static bool isDPressed = false;
static void Main(string[] args)
{
int var1 = 0;
int var2 = 0;
// 启动按键监听线程
Thread keyListenerThread = new Thread(KeyListener);
keyListenerThread.IsBackground = true;
keyListenerThread.Start();
// 设置TCP服务器
IPAddress localAddr = IPAddress.Any;
TcpListener server = new TcpListener(localAddr, 22);
server.Start();
Console.WriteLine("Server started.");
TcpClient client = server.AcceptTcpClient();
NetworkStream stream = client.GetStream();
try
{
while (true)
{
// 根据按键状态设置 var1 的值
if (isWPressed)
var1 = 2;
else if (isSPressed)
var1 = 1;
else
var1 = 0;
if (isAPressed)
var2 = 2;
else if (isDPressed)
var2 = 1;
else
var2 = 0;
// 读取数据长度
byte[] lengthBuffer = new byte[4];
if (ReadFull(stream, lengthBuffer, 0, 4) != 4)
throw new Exception("Failed to read length");
int length = BitConverter.ToInt32(lengthBuffer, 0);
if (length < 0 || length > 10 * 1024 * 1024) // 添加长度校验,防止分配过大内存
throw new Exception("Invalid length");
// 读取数据
byte[] buffer = new byte[length];
if (ReadFull(stream, buffer, 0, length) != length)
throw new Exception("Failed to read image data");
Mat image = Cv2.ImDecode(buffer, ImreadModes.Color); // 解码图像
if (!image.Empty())
{
Cv2.ImShow("TCP Server", image);
Cv2.WaitKey(1); // 更新窗口
}
// 将 var1 和 var2 拼接为四字节字符串并发送
string message = var1.ToString("D2") + var2.ToString("D2");
byte[] data = Encoding.ASCII.GetBytes(message);
stream.Write(data, 0, data.Length);
}
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
stream.Close();
client.Close();
server.Stop();
}
// 一个用于从NetworkStream中读取指定数量字节的辅助函数
static int ReadFull(NetworkStream stream, byte[] buffer, int offset, int size)
{
int totalRead = 0;
while (totalRead < size)
{
int read = stream.Read(buffer, offset + totalRead, size - totalRead);
if (read == 0)
{
break; // 连接可能已关闭
}
totalRead += read;
}
return totalRead;
}
static void KeyListener()
{
while (true)
{
// 检测按键状态
if ((GetAsyncKeyState(0x57) & 0x8000) != 0) // w键
{
isWPressed = true;
isSPressed = false;
}
else if ((