遥控迷你巡线小车的Windows程序

最近拿到了小喵科技的迷你巡线小车。

作为“寻常不走路”的DIY人员,关注的绝对不是寻线功能,而是研究如何接下来研究如何实现电脑对小车的遥控。

在经过一番折腾之后,我成功的连接了手机和小车能够实现手机的遥控。从原理上来说,WIFI 模块在这个过程中充当了透明网关的角色,对于手机遥控端来说,它在和TCP/IP
设备打交道;对于小车的主控来说,它是在接受串口指令而已。反编译他们的
App只能看懂他们用了TCP 做连接,代码中使用到的UDP大约只是用来扫描而已。接着找技术支持群,疑似开发人员留下了一句话使用:23端口,就不见了。想象中,他刚说完这句就因为管理员担心泄密直接打晕拖走…….仍然留下一头雾水的我。

忽然想起来,1024以下的端口都是有固定用途的,比如:ftp 是21。而23是 Telnet的。然后直接用系统自带的直接 telnet 上去。每次我在Telnet
上发送消息,小车的串口都会收到对应的消息。为了便于实验,我先刷上默认的代码,其中有一些控制命令可以从代码中看出来:


M0 显示当前版本

M6 后面带2个参数,控制前方的LED开关

M8 返回当前电池电压

M13 后面带4个参数 第一个LED 然后是 R G
B的色彩分量

M18 后面带2个参数 第一个是频率,第二个是播放时长

M19 和上面的M18类似

M200 后面带2个参数,设置左马达和右马达的速度

M202 后面三个参数,左马达和右侧马达速度,持续时间


下面就实验一下直接使用 Windows自带的Telnet来实现控制,小车当前的IP可以从控制的APP中看到,当然也可以从你家路由器的配置界面看到:

下面就连接上了,我输入M0(无回显),小车返回下面的字符给我

接下来,就可以像电影的黑客一样输入字符来控制小车啦。

退出当前 telnet 连接的方法是使用 ctrl+],再输入quit。当然直接关闭窗口也可以。

虽然这样的方法看起来很酷,但是比较麻烦,所以接下来使用C#编写一个 Windows程序来进行控制。选择 C# 的最主要是因为语言简单,特别是在界面开发所见即所得。如果说20年前的Delphi 是 VB Killer的话,眼下 C# 也能称得上 Delphi Killer了。不由得发出“风轮流转”的感叹。

从界面入手,介绍功能:左右放置两个TrackBar用来控制左右轮子的速度,上方放置2个控制LED的Button(具体的做法是Button 上显示图片,通过 ImageList来进行切换),一个 Text输入框用来输入小车的IP,剩下的大按钮是用来控制车前灯。

除了这些可见的控件之外,还有下面三个不可见的控件,2个ImageList 是给Button显示图标用的,一个 Timer是用来每隔100ms发送命令用到(发出的命令是左右轮子转动加持续100ms,
这样就能做到连续运行)。同时Trackbar如果有一个数值低于30就直接停止。

完整代码如下:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Forms;

using System.Net.Sockets;

using System.IO;


namespace Tempshow

{

public partial class Form1 : Form

{

// Telnet 对象

TelnetConnection tc;

// 前灯是否有改变的标记,如果有改变我们才发送命令给小车

Boolean LedChanged = false;


public Form1()

{

InitializeComponent();


}


//左车灯按钮

private void
button1_Click(object sender, EventArgs e)

{

// ImageIndex 为 0 表示关,1表示开

if (this.button1.ImageIndex
== 0) { this.button1.ImageIndex = 1; }

else { this.button1.ImageIndex
= 0; }

LedChanged = true;

}


//右车灯按钮

private void
button2_Click(object sender, EventArgs e)

{

// ImageIndex 为 0 表示关,1表示开

if (this.button2.ImageIndex
== 0) { this.button2.ImageIndex = 1; }

else { this.button2.ImageIndex
= 0; }

LedChanged = true;

}


//连接小车的按钮

private void
button3_Click(object sender, EventArgs e)

{

if (this.button3.ImageIndex
== 0) {

this.button3.ImageIndex = 1;

this.textBox1.Enabled = true;

timer1.Enabled = false;

if (tc.IsConnected) {

tc.DisConnect();

}

}

else {

// 创建 Telnet 连接

tc = new TelnetConnection(textBox1.Text, 23);

string s = tc.Login(" ", " ", 100);


// 正确连接之后

if (tc.IsConnected) {

// 改变连接的图标

button3.ImageIndex = 0;

// 不可以输入 Ip

textBox1.Enabled = false;

//打开定时器

timer1.Enabled = true;

}


}

}


//每隔100ms发生一次的定时器

private void
timer1_Tick(object sender, EventArgs e)

{

string led="M6 ";

if (LedChanged) {

if (button1.ImageIndex==0) {

//发送开左灯

led += "0 ";

} else

{

//发送关左灯

led += "1 ";

}


if (button2.ImageIndex == 0)

{

//发送开右灯

led += "0";

}else

{

//发送关右灯

led += "1";

}

tc.WriteLine(led);

LedChanged = false;

}


if ((trackBar1.Value<30) || (trackBar2.Value <30)) {

tc.WriteLine("M200 0 0 0");

}

else {

string Motor = "M200 " + trackBar1.Value.ToString() + "
"+trackBar2.Value.ToString() + " 100";

tc.WriteLine(Motor);

}

}


}


enum Verbs

{

WILL = 251,

WONT = 252,

DO = 253,

DONT = 254,

IAC = 255

}


enum Options

{

SGA = 3

}


class TelnetConnection

{

TcpClient tcpSocket;


int TimeOutMs = 1 * 1000;


public TelnetConnection(String Hostname, int Port)

{

tcpSocket = new TcpClient(Hostname, Port);


}


public string Login(string Username, string Password, int LoginTimeOutMs)

{

int oldTimeOutMs = TimeOutMs;

TimeOutMs = LoginTimeOutMs;

// string s;

// string s =
Read();

// if
(!s.TrimEnd().EndsWith(":"))

// throw new Exception("Failed to
connect : no login prompt");

//
WriteLine(Username);


//s += Read();

//if
(!s.TrimEnd().EndsWith(":"))

// throw new Exception("Failed to connect
: no password prompt");

//
WriteLine(Password);

WriteLine("M0");

string s = Read();

TimeOutMs = oldTimeOutMs;


return s;

}


public void
DisConnect()

{

if (tcpSocket != null)

{

if (tcpSocket.Connected)

{

tcpSocket.Client.Disconnect(true);

}

}

}


public void
WriteLine(string cmd)

{

Write(cmd + "\r\n");

}


public void Write(string cmd)

{

if (!tcpSocket.Connected) return;

byte[] buf = System.Text.ASCIIEncoding.ASCII.GetBytes(cmd.Replace("\0xFF", "\0xFF\0xFF"));

tcpSocket.GetStream().Write(buf, 0,
buf.Length);

}


public string Read()

{

if (!tcpSocket.Connected) return null;

StringBuilder sb = new StringBuilder();

do

{

ParseTelnet(sb);

System.Threading.Thread.Sleep(TimeOutMs);

} while (tcpSocket.Available > 0);


return ConvertToGB2312(sb.ToString());

}


public bool
IsConnected

{

get { return
tcpSocket.Connected; }

}


void ParseTelnet(StringBuilder sb)

{

while (tcpSocket.Available > 0)

{

int input = tcpSocket.GetStream().ReadByte();

switch (input)

{

case -1:

break;

case (int)Verbs.IAC:

// interpret as command

int inputverb =
tcpSocket.GetStream().ReadByte();

if (inputverb == -1) break;

switch (inputverb)

{

case (int)Verbs.IAC:

//literal IAC = 255 escaped, so append char 255 to string

sb.Append(inputverb);

break;

case (int)Verbs.DO:

case (int)Verbs.DONT:

case (int)Verbs.WILL:

case (int)Verbs.WONT:

// reply to all commands with "WONT", unless it is SGA
(suppres go ahead)

int inputoption =
tcpSocket.GetStream().ReadByte();

if (inputoption == -1) break;

tcpSocket.GetStream().WriteByte((byte)Verbs.IAC);

if (inputoption == (int)Options.SGA)

tcpSocket.GetStream().WriteByte(inputverb == (int)Verbs.DO ? (byte)Verbs.WILL : (byte)Verbs.DO);

else

tcpSocket.GetStream().WriteByte(inputverb == (int)Verbs.DO ? (byte)Verbs.WONT : (byte)Verbs.DONT);

tcpSocket.GetStream().WriteByte((byte)inputoption);

break;

default:

break;

}

break;

default:

sb.Append((char)input);

break;

}

}

}


private string
ConvertToGB2312(string
str_origin)

{

char[] chars = str_origin.ToCharArray();

byte[] bytes = new byte[chars.Length];

for (int i = 0; i
< chars.Length; i++)

{

int c = (int)chars[i];

bytes[i] = (byte)c;

}

Encoding Encoding_GB2312 = Encoding.GetEncoding("GB2312");

string str_converted = Encoding_GB2312.GetString(bytes);

return str_converted;

}

}


}

Windows平板和普通的PC 笔记本没有差别,只是更轻薄


实验的视频

https://www.zhihu.com/video/930443224208707584

编辑于 2017-12-30

文章被以下专栏收录