-
Notifications
You must be signed in to change notification settings - Fork 91
RCRemote
目前市面上能见到的遥控玩具啦,通常类似的玩具遥控都工作的无线电频率在27Mhz
,49Mhz
,极个别在40Mhz
这几个频段上
我们其实可以软件无线电的方式对这个遥控数据进行分析,按照惯例我们通过GQRX
观察这几个频点的频谱来确定目前手上的遥控工作在27Mhz
然后这个就是我通过电视棒采样原始信号,之后用Audacity
分析一下这里面的信号数据内容
前导
用于告诉接收有信号过来注意接收,后面的有效数据
就是控制小车的前后左右
,你会发现这个有效数据中并没有出现地址码之类的识别标志数据位(意思就是你买了两个相同的玩具,两个遥控发射出来的信号是相同
的,可以互相的影响对方的汽车),这就代表我们只要频率能对上然后在伪造出我们想要的有效数据就能够去挟持小朋友的遥控汽车啦,然后我们来说这个有效数据重的编码规则
有效数据中控制方向的其实数据中这个短的的脉冲的数量
来操作的,经过我们多次采样数据进行分析我们得到的控制小车的脉冲数量,前进:10
,后退:40
,转左:58
,转右:64
,打开车门:22
然后我们只要模拟出这个信号的脉冲数据就可以去挟持小车了,接下来我们来介绍一下如何去产生这个27Mhz
的信号
先介绍一下这个DDS模块,能够输出正弦波
和方波
,可以输出0-40MHz
范围内的信号,如果是使用AD9851
的话频率可以到0-70Mhz
,因为是AD9850
的时钟频率最高 125MHz
,AD9851
的时钟频率最高180MHz
,输出最高频率 AD9851
要比 AD9850
高,并且AD9850
没有6倍频
而AD9851有,这个模块甚至能到一个扫频器用,但是这里我们使用型号AD9850
去产生这个27Mhz的控制的信号
我们先来看看怎么使用HackCubeSpecial
驱动这个DDS模块
去产生控制信号的,我们先来看看这个接线,是吧正弦波输出引脚2(ZOUT2)
接上红色面包线,当做这个信号的发射天线,然后复用SPI引脚
当做数据的输入引脚,需要主意是可能会和板子上的射频模块互相干扰
看下这个AD9850
芯片的Datasheet
,发现输出的频率写入的方式是通过串行进行写入,也可以通过SPI
的方式吧这个频率写入到DDS模块
中,或者是像我这样通过Arduino
下面的shiftOut
函数写入频率数据
int W_CLK = 13;
int FQ_UD = 12;
int DATA = 11;// 定义连接DDS模块引脚
uint32_t deltaphase = 0;//频率
uint8_t phase = 0;
double calibFreq = 125000000;
void pulse(int pin) { //翻转引脚 复位使用
digitalWrite(pin, HIGH);
digitalWrite(pin, LOW);
}
void dds_begin() { //引脚初始化
pinMode(W_CLK, OUTPUT);
pinMode(FQ_UD, OUTPUT);
pinMode(DATA, OUTPUT); //定义引脚输出状态
pulse(W_CLK);
pulse(FQ_UD);//翻转引脚
}
/*
shiftOut(dataPin,clockPin,bitOrder,val) //函数能够将数据通过串行的方式在引脚上输出,相当于一般意义上的同步串行通信,这是控制器与控制器、控制器与传感器之间常用的一种通信方式。
dataPin // 数据引脚 连接DDS 模块中的 DATA
clockPin // 时钟引脚 连接DDS 模块中的 W_CLK
bitOrder //分别有 MSBFIRST 与 LSBFIRST 区分是高写入还是低位写入
val //所要写入的值,一个字节
*/
void update() { //将要产生的频率数据通过 shiftOut 串行 写入到DDS 模块中
for (int i = 0; i < 4; i++, deltaphase >>= 8) { //deltaphase被定义成了uint32_t类型 但是shiftOut每次只能写入8个bit(1字节),分四次写入
shiftOut(DATA, W_CLK, LSBFIRST, deltaphase & 0xFF); //shiftOut 写入
}
shiftOut(DATA, W_CLK, LSBFIRST, phase & 0xFF);
pulse(FQ_UD);
}
void setfreq(double f, uint8_t p) {
deltaphase = f * 4294967296.0 / calibFreq; //计算出频率的值
phase = p << 3;
update();
}
void down() {
pulse(FQ_UD);
shiftOut(DATA, W_CLK, LSBFIRST, 0x04);
pulse(FQ_UD);
}
void calibrate(double TrimFreq)
{
calibFreq = TrimFreq;
}
double freq = 27000000; //要产生的频率 27Mhz
double trimFreq = 124999500;
void setup() {
dds_begin();
setfreq(freq, phase);
}
void loop(){
}
然后这个就是在正弦波输出引脚2(ZOUT2)
上产生27Mhz
信号的Arduino程序
我们用示波器去观察一下ZOUT2
引脚上产生出来的频率波形是否与我们写入的频率一致,一致的话就可以进行下一步的工作了,产生出对应频率的控制方向信号
int time=550; //信号脉冲的时间
void preample (int len) { //产生前导码的信号
for (int i = 0; i < len; i++) {
setfreq(freq, phase);//产生27Mhz正弦波信号
delayMicroseconds (time * 3 -100); //前导码宽脉冲的时间 -100是因为被写入数据和底层操作消耗的
down(); //关闭正弦波信号
delayMicroseconds (1); //实际上延迟550 US 因为被写入数据和底层操作消耗的
}
}
void command (int len) { //产生有效数据信号,len=有效数据中的脉冲数量,通过这个数量控制前后左右
for (int i = 0; i < len; i++) {
setfreq(freq, phase); //产生27Mhz正弦波信号
delayMicroseconds (time -124); //产生有效数据中的时间
down(); //关闭正弦波信号
delayMicroseconds (1);//实际上延迟550 US 因为被写入数据和底层操作消耗的
}
}
void sendcommand (int n) //传入有效脉冲的个数来控制 小车的上下左右
{
for (int i = 0; i < 95; i++) {
preample (4); // 先产生出4个前导信号
command (n); // 然后产生有效数据中的脉冲数量
}
}
void loop(){
sendcommand(10); //调用发射函数
}
我们就可以通过示波器器抓到这个信号了
之后就可以看到信号了,然后小车就可以动起来了~然后可以修改有效数据中的脉冲数量(N)
去控制小车啦
可以通过HackCubeSpecial
上的ESP8266
配合ATmega32u4
单片机对DDS模块进行配置,我们在ESP8266 上面跑一个WEB 端,就是上面这个,通过WebSocket
的方式送Web端的摇杆角度,然后ESP8266
收到后根据摇杆角度对ATmega32u4
发送串口指令执行不同的操作
<!DOCTYPE html>
<html>
<head>
<title>ESP8266 test</title>
<meta name="viewport" content="width=device-width, inicioial-scale=0.7, maximum-scale=0.7">
<meta charset="utf-8">
<style>
/*body { text-align: center; font-size: width/2pt;background-color: #A9E2F3; }*/
body { text-align: center; font-size: width/2pt;background-color: #A9E2F3; }
/* #remote { background-color: #A9E2F3; }
#remote{ background:black;} */
h1 { font-weight: bold; font-size: width/2pt; }
h2 { font-weight: bold; font-size: width/2pt; }
button { font-weight: bold; font-size: width/2pt; }
</style>
<script>
var canvas_width = 400, canvas_height = 400;
var radio_base = 150;
var radio_handle = 72;
var radio_shaft = 120;
var rango = canvas_width/2 - 10;
var step = 18;
var ws;
var joystick = {x:0, y:0};
var click_state = 0;
var ratio = 1;
function inicio()
{
var width = window.innerWidth;
var height = window.innerHeight;
/* width=1000;
height=500;
console.log("incio");
console.log(width);
console.log(height);
*/
if(width < height)
ratio = (width - 50) / canvas_width;
else
ratio = (height - 50) / canvas_width;
ratio=1.5;
/* canvas_width = Math.round(canvas_width*ratio);
canvas_height = Math.round(canvas_height*ratio);*/
canvas_width = Math.round(canvas_width);
canvas_width = Math.round(500);
canvas_height = Math.round(500);
console.log("canvas_width")
console.log(canvas_width)
radio_base = Math.round(radio_base*ratio);
radio_handle = Math.round(radio_handle*ratio);
radio_shaft = Math.round(radio_shaft*ratio);
rango = Math.round(rango*ratio);
step = Math.round(step*ratio);
var canvas = document.getElementById("remote");
canvas.width = canvas_width;
canvas.height = canvas_height;
canvas.addEventListener("touchstart", mouse_down);
canvas.addEventListener("touchend", mouse_up);
canvas.addEventListener("touchmove", mouse_move);
canvas.addEventListener("mousedown", mouse_down);
canvas.addEventListener("mouseup", mouse_up);
canvas.addEventListener("mousemove", mouse_move);
var ctx = canvas.getContext("2d");
ctx.translate(canvas_width/2, canvas_height/2);
ctx.shadowBlur = 20;
ctx.shadowColor = "LightGray";
ctx.lineCap="round";
ctx.lineJoin="round";
actualizarVista();
}
function conectar()
{
if(ws == null)
{
ws = new WebSocket('ws://' + window.location.hostname + ':81');
//ws = new WebSocket('ws://' + "192.168.1.20" + ':81');
document.getElementById("ws_state").innerHTML = "CONECTANDO";
ws.onopen = ws_onopen;
ws.onclose = ws_onclose;
ws.onmessage = ws_onmessage;
}
else
ws.close();
}
function ws_onopen()
{
//document.getElementById("ws_state").innerHTML = "<font color='blue'>CONECTADO</font>";
document.getElementById("bt_connect").innerHTML = "DESCONECTADO";
actualizarVista();
}
function ws_onclose()
{
// document.getElementById("ws_state").innerHTML = "<font color='gray'>CERRADO</font>";
document.getElementById("bt_connect").innerHTML = "CONECTADO";
ws.onopen = null;
ws.onclose = null;
ws.onmessage = null;
ws = null;
actualizarVista();
}
function ws_onmessage(e_msg)
{
e_msg = e_msg || window.event; // MessageEvent
}
function enviarDatos()
{
var x = joystick.x, y = joystick.y;
var joystick_rango = rango - radio_handle;
x = Math.round(x*100/joystick_rango);
y = Math.round(-(y*100/joystick_rango));
if(ws != null)
ws.send(x + ":" + y + "\r\n");
}
function actualizarVista()
{
var x = joystick.x, y = joystick.y;
var canvas = document.getElementById("remote");
var ctx = canvas.getContext("2d");
ctx.clearRect(-canvas_width/2, -canvas_height/2, canvas_width, canvas_height);
ctx.lineWidth = 3;
ctx.strokeStyle="black";
/* ctx.fillStyle = "hsl(0, 0%, 0%)";*/
//ctx.fillStyle = "#A9E2F3";
ctx.fillStyle = "#A9E2F3";
ctx.beginPath();
ctx.fillRect(-250,-250,500,500);
ctx.stroke();
ctx.fill();
ctx.strokeStyle="black";
ctx.fillStyle = "hsl(0, 0%, 35%)";
ctx.beginPath();
ctx.arc(0, 0, radio_base-50, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.strokeStyle="white";
var lineWidth = radio_shaft;
var pre_x = pre_y = 0;
var x_end = x/5;
var y_end = y/5;
var max_count = (radio_shaft - 10)/step;
var count = 1;
while(lineWidth >= 10)
{
var cur_x = Math.round(count * x_end / max_count);
var cur_y = Math.round(count * y_end / max_count);
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.lineTo(pre_x, pre_y);
ctx.lineTo(cur_x, cur_y);
ctx.stroke();
lineWidth -= step;
pre_x = cur_x;
pre_y = cur_y;
count++;
}
var x_start = Math.round(x / 3);
var y_start = Math.round(y / 3);
lineWidth += step;
ctx.beginPath();
ctx.lineTo(pre_x, pre_y);
ctx.lineTo(x_start, y_start);
ctx.stroke();
count = 1;
pre_x = x_start;
pre_y = y_start;
while(lineWidth < radio_shaft)
{
var cur_x = Math.round(x_start + count * (x - x_start) / max_count);
var cur_y = Math.round(y_start + count * (y - y_start) / max_count);
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.lineTo(pre_x, pre_y);
ctx.lineTo(cur_x, cur_y);
ctx.stroke();
lineWidth += step;
pre_x = cur_x;
pre_y = cur_y;
count++;
}
var grd = ctx.createRadialGradient(x, y, 0, x, y, radio_handle);
for(var i = 85; i >= 50; i-=5)
grd.addColorStop((85 - i)/35, "hsl(0, 100%, "+ i + "%)");
/* ctx.strokeStyle="black";
ctx.fillStyle = "hsl(0, 0%, 100%)";
ctx.beginPath();
ctx.fillRect(-250,-250,500,500);
ctx.stroke();
ctx.fill();*/
ctx.fillStyle = "#89E2F3";;
ctx.beginPath();
ctx.arc(x, y, radio_handle, 0, 2 * Math.PI);
ctx.fill();
}
function procesarEvento(event)
{
var pos_x, pos_y;
if(event.offsetX)
{
pos_x = event.offsetX - canvas_width/2;
pos_y = event.offsetY - canvas_height/2;
}
else if(event.layerX)
{
pos_x = event.layerX - canvas_width/2;
pos_y = event.layerY - canvas_height/2;
}
else
{
pos_x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - canvas_width/2;
pos_y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - canvas_height/2;
}
return {x:pos_x, y:pos_y}
}
function mouse_down()
{
if(ws == null)
return;
event.preventDefault();
var pos = procesarEvento(event);
var delta_x = pos.x - joystick.x;
var delta_y = pos.y - joystick.y;
var dist = Math.sqrt(delta_x*delta_x + delta_y*delta_y);
if(dist > radio_handle)
return;
click_state = 1;
var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);
if(radio <(rango - radio_handle))
{
joystick = pos;
enviarDatos();
actualizarVista();
}
}
function mouse_up()
{
event.preventDefault();
click_state = 0;
var pos = procesarEvento(event);
pos.x=0;
pos.y=0;
var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);
if(radio <(rango - radio_handle))
{
joystick = pos;
enviarDatos();
actualizarVista();
}
}
function mouse_move()
{
if(ws == null)
return;
event.preventDefault();
if(!click_state)
return;
var pos = procesarEvento(event);
var radio = Math.sqrt(pos.x*pos.x + pos.y*pos.y);
if(radio <(rango - radio_handle))
{
joystick = pos;
enviarDatos();
actualizarVista();
}
}
window.onload = inicio;
</script>
</head>
<body>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
</br>
<canvas id="remote"></canvas>
<h2>
<p>
Action : <span id="ws_state">null</span>
</p>
<!-- <button id="bt_connect" type="button" onclick="conectar();">CONECTAR</button> -->
</h2>
</body>
</html>
<script type="text/javascript">
conectar();
</script>
这个是控制前端页面和JS,主要的作用是和ESP8266在81建立一个WebSocket
的链接,然后在摇杆发生后,将摇杆的X,Y轴
的数值传给ESP8266
,然后ESP8266
就可以根据这个数值判断摇杆的角度从而执行对应的功能去操控
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <WebSocketsServer.h>
#include <Hash.h>
#include <FS.h>
const char* ssid = "HackCube";
const char* password = "unicorn_team";
int OUTPUT4 = 16;
int OUTPUT3 = 5;
int OUTPUT2 = 4;
int OUTPUT1 = 0;
long duracion = 0;
WebSocketsServer webSocket = WebSocketsServer(81);
ESP8266WebServer server(80);
void setup(void) {
delay(1000);
//Velocidad
Serial.begin(115200);
//Configuracion de salidas
pinMode (OUTPUT1, OUTPUT);
pinMode (OUTPUT2, OUTPUT);
pinMode (OUTPUT3, OUTPUT);
pinMode (OUTPUT4, OUTPUT);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
IPAddress myIP = WiFi.localIP();
Serial.print("IP: ");
Serial.println(myIP);
SPIFFS.begin();
webSocket.begin();
webSocket.onEvent(webSocketEvent);
server.onNotFound([]() {
if (!handleFileRead(server.uri()))
server.send(404, "text/plain", "Archivo no encontrado");
});
//Servidor Web Iniciado
server.begin();
Serial.println("Servidor HTTP iniciado");
}
void loop(void) {
webSocket.loop();
server.handleClient();
}
//Funcion predefinida de un WebSocket
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) {
switch (type) {
//En caso de que un cliente se desconecte del websocket
case WStype_DISCONNECTED: {
Serial.printf("Usuario #%u - Desconectado\n", num);
break;
}
//Cuando un cliente se conecta al websocket presenta la información del cliente conectado, IP y ID
case WStype_CONNECTED: {
IPAddress ip = webSocket.remoteIP(num);
Serial.printf("Nueva conexión: %d.%d.%d.%d Nombre: %s ID: %u\n", ip[0], ip[1], ip[2], ip[3], payload, num);
break;
}
//Caso para recibir información que se enviar vía el servidor Websocket
case WStype_TEXT: {
String entrada = "";
for (int i = 0; i < lenght; i++) {
entrada.concat((char)payload[i]);
}
String data = entrada;
if (data) {
int pos = data.indexOf(':');
long x = data.substring(0, pos).toInt();
long y = data.substring(pos + 1).toInt();
if (y > 35 && x > 35) {
Serial.println("^>");
uprigth();
} else if (y > 25 && x < -25) {
Serial.println("<^");
upleft();
} else if (y < -25 && x < -25) {
Serial.println("<-");
downleft();
} else if (y < -35 && x > 35) {
Serial.println("->");
downrigth();
} else if (y > 35) {
Serial.println("^");
up();
} else if (y < -35) {
Serial.println("-");
down();
} else {
car_reset();
}
}
break;
}
}
}
这个就是ESP8266
上的代码,通过SPIFFS
放置刚刚的前端页面,然后连接上我们的路由器后,我们可以用用手机访问ESP8266的ip,就看看到我们的前端的页面了,然后我们通过WebSocket
方式与ESP8266
进行通信,因为平常每次操作都需要发起一次HTTP请求
,数据时延
上面操作会受到影响机会导致,操作不同步的现象出现,然后我们用WebSocket
只需要发起一次HTTP请求就能保持长链接,并且每次每次操作不需要增加额外的数据内容,这个X,Y轴
就是前端页面中摇杆传递过来的数值了,可以根据这个去对小车进行控制
后期可能也会考虑在板子加上DDS模块或者使用YX-4116
芯片的遥控模块供大家使用的
https://yoamoprogramar.com/2018/02/12/carrito-wifi-nodemcu-esp8266-websocket-webserver/
https://www.riyas.org/2014/06/computer-controlling-27mhz-remote-control-car-ad9850-dds.html