前提
当前OriginBot小车的无线模块连接在家用的路由器上,一旦小车离开了WiFi的覆盖范围,即断开连接之后便无法再接收数据或下达指令;试想一下,如果现在你拿着手机和小车在一个没有网络的环境里,你该如何使用手机控制小车运动起来呢?
其实只需要在手机和小车之间搞定两个点:
- 建立局域网
- 建立远程连接
建立局域网
在没有任何其他无线网络存在的环境下,局域网的建立只能依赖手机端或小车端发出热点,然后让另外一端连接上该热点从而建立起二者之间的连接。如果是手机端发出热点小车端连接,则小车端必须提前使用PC端远程配置好将要连接的热点信息,一旦手机热点更换,则需重复以上操作。所以最好选择从小车端发出热点,这样只要手机在小车的附近,就可以选择连接热点并保持连接状态。
OriginBot的核心板旭日X3Pi 集成了2.4GHz无线WiFi模块,支持Soft AP和Station两种模式, Station即是连接其他WiFi的模式,板子默认运行在此模式下; Soft AP即为我们将要实现的板子发出热点的模式。
地平线官方文档中关于网络配置的模块讲解了两种模式的配置方法,我这里就不重复说明了,具体请参照下面的网页:
核心板上的WiFi模块配置成Soft AP模式后,也可以通过电脑远程连接进行调试,不过此时该热点就丧失了上网的功能;X3Pi还提供了以太网,可通过网线连接进行调试,该以太网卡为固定的IP,依然是不具备上网的功能,若想要小车依然可具备上网的功能,可以在核心板上配置一个USB WiFi模块,具体参考地平线官方社区的一篇文章:
https://developer.horizon.ai/forumDetail/112555549341653767
Soft AP模式配置后,还需要进行一个开机自启动配置,关于自启动配置,官方文档也有介绍,你可以将以下命令添加到系统启动脚本中,使其在系统启动时自动执行:
sudo systemctl enable hostapd
sudo systemctl enable dnsmasq
最终配置好后的结果如下图:
在Android手机端可以连接到此热点,如下图:
问题
wlan0配置好热点后,重启系统总是会连接到之前的WiFi。
这个问题可以通过禁用网络管理器自动连接到之前的WiFi。通过编辑"/etc/NetworkManager/NetworkManager.conf"文件来完成。在该文件的“[main]”部分中添加以下行:
[keyfile]
unmanaged-devices=interface-name:wlan0
保存更改并重新启动网络服务。可以使用以下命令来重新启动网络服务:
sudo systemctl restart networking.service
sudo systemctl restart NetworkManager.service
远程启动
在使用手机端控制小车运动之前,通常需要启动小车的底层控制节点;在OriginBot上已经写好相关的 launch 文件,启动命令为: ros2 launch originbot_bringup originbot.launch.py
并可设置参数:
- use_camera:是否连接并使用摄像头
- use_lidar:是否连接并使用激光雷达
- use_imu:是否连接并使用IMU
以上参数可以在需要时加上,如果只是启动小车的底层控制则不需要设置这些参数;我之前在手机端做了实时图像流显示,所以还要在小车端运行 ros2 launch websocket hobot_websocket.launch.py
底层控制与网页端显示这两个launch文件其实可以融合为一个launch文件,这里我将hobot_websocket.launch.py也写成参数启用模式,设置参数为:use_websocket
launch.actions.IncludeLaunchDescription(
launch.launch_description_sources.PythonLaunchDescriptionSource(
os.path.join(get_package_share_directory('websocket'),
'launch','hobot_websocket.launch.py')),
condition=launch.conditions.IfCondition(
launch.substitutions.LaunchConfiguration('use_websocket')))
Android端使用 jsch 库实现SSH远程连接,新建一个 SSHManager 类来管理SSH,代码如下:
package com.example.originbot;
import android.util.Log;
import com.jcraft.jsch.*;
public class SSHManager {
private static final int TIMEOUT = 3000; // 连接超时时间,单位为毫秒
private String username;
private String password;
private String host;
private int port;
public SSHManager(String username, String password, String host, int port) {
this.username = username;
this.password = password;
this.host = host;
this.port = port;
}
public boolean executeCommand(String command) {
JSch jsch = new JSch();
Session session = null;
try {
session = jsch.getSession(username, host, port);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.setTimeout(TIMEOUT);
session.connect();
Channel channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(command);
channel.setInputStream(null);
((ChannelExec) channel).setErrStream(System.err);
channel.connect();
Log.d("SSH_LOG", "SSH连接成功");
// InputStream in = channel.getInputStream();
// byte[] buffer = new byte[1024];
// int bytesRead;
// StringBuilder result = new StringBuilder();
//
// while ((bytesRead = in.read(buffer)) != -1) {
// result.append(new String(buffer, 0, bytesRead));
// }
// System.out.println(result.toString());
// Log.d("SSH_LOG", result.toString());
channel.disconnect();
session.disconnect();
return true;
} catch (Exception e) {
e.printStackTrace();
Log.d("SSH_LOG", "SSH连接失败");
return false;
}
}
}
在上面的代码中,注释的代码原是用于打印远程执行命令后的终端输出,但是一旦执行的命令为不会主动退出的进程,则APP就会阻塞在此处;所以这里可以不用打印输出。
然后新建立了一个 SplashActivity 作为APP的启动界面,中间显示OriginBot平面图,下方通过一个按钮来建立与小车端的远程连接,连接成功后通过SSH启动底层控制以及网页展示端,然后进入 MainActivate。代码如下:
package com.example.originbot;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
public class SplashActivity extends AppCompatActivity {
private Thread bring_up_thread;
private SSHManager ssh_originbot_bringup;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getSupportActionBar().hide();
setContentView(R.layout.activity_splash);
Button connect_btn = findViewById(R.id.connect_robot);
String host = "192.168.42.10";
String username = "root";
String password = "root";
int port = 22;
ssh_originbot_bringup = new SSHManager(username, password, host, port);
connect_btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
connect_btn.setEnabled(false);
if (bring_up_thread != null && bring_up_thread.isAlive()) {
Log.d("SSH_LOG", "线程已启动");
}
bring_up_thread = new Thread(new Runnable() {//创建子线程
@Override
public void run() {
String command = "source /opt/ros/foxy/setup.bash && " +
"source /opt/tros/setup.bash &&" +
"source /userdata/dev_ws/install/local_setup.bash &&" +
"ros2 launch originbot_bringup originbot.launch.py use_websocket:=true";
boolean state = ssh_originbot_bringup.executeCommand(command);
if (state) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "连接成功", Toast.LENGTH_SHORT).show();
}
});
Intent it = new Intent(getApplicationContext(), MainActivity.class);//启动MainActivity
startActivity(it);
connect_btn.setEnabled(true);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 2000); // 延迟关闭当前活动 2 秒钟
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "连接失败", Toast.LENGTH_SHORT).show();
connect_btn.setEnabled(true);
}
});
}
}
});
bring_up_thread.start();
}
});
}
}
在上面的代码中,可以看到终端执行的命令 command 包含了几个 source ROS2环境变量的操作,这是因为通过SSH启动的 channel 并不会包含这些环境变量。
我在 MainActivate 左上方添加了一个回退按钮,用于返回activate,同时通过SSH远程杀死之前launch启动的所有相关节点;通过pkill+节点名称, 这些节点的名称可以通过在PC端启动launch文件时看到,如下图:
MainActivate 中的实现代码如下:
private SSHManager ssh_kill_node;
private Thread kill_node_thread;
private ImageButton back_splash_btn;
back_splash_btn = findViewById(R.id.back_btn);
back_splash_btn.setOnClickListener(backSplash);
String host = "192.168.42.10";
String username = "root";
String password = "root";
int port = 22;
ssh_kill_node = new SSHManager(username, password, host, port);
......
private View.OnClickListener backSplash = new View.OnClickListener() {
public void onClick(final View view) {
if (kill_node_thread != null && kill_node_thread.isAlive()) {
Log.d("SSH_LOG", "kill_node_thread 线程已启动");
}
kill_node_thread = new Thread(new Runnable(){//创建子线程
@Override
public void run() {
String command = "pkill hobot_codec_rep && pkill mono2d_body_det &&" +
"pkill mipi_cam && pkill websocket &&" +
"pkill static_transfor && pkill originbot_base";
boolean state = ssh_kill_node.executeCommand(command);
// boolean state = true;
if(state)
{
Intent it=new Intent(getApplicationContext(),SplashActivity.class); // 启动 Splash
startActivity(it);
// finish();//关闭当前活动
}
}
});
kill_node_thread.start();
}
};
@Override
protected void onDestroy() {
super.onDestroy();
// 执行一些清理操作
Thread exit_thread = new Thread(new Runnable() {
@Override
public void run() {
String command = "pkill hobot_codec_rep && pkill mono2d_body_det &&" +
"pkill mipi_cam && pkill websocket &&" +
"pkill static_transfor && pkill originbot_base";
ssh_kill_node.executeCommand(command);
}
});
exit_thread.start();
try {
exit_thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("SSH_LOG", "程序退出");
}
结果展示
初始界面:
主界面:
总结延伸
在SSH执行启动小车底层命令后,APP界面由初始界面直接跳转到主界面,暂时没有实现待小车端ROS2节点完全启动成功后跳转到主界面,也就是说进入主界面后会等待一会儿才能控制小车。
在小车端发出热点后通过SSH远程启动小车的ROS2节点只是其中一种选择;还可以将这些节点设置为开机自启动,当然这种方法缺少灵活性;还可以在小车端编写服务端新建或杀死进程来启动节点,在手机端通过建立客户端来进行控制,这种方式可以不用再使用SSH。
评论(0)
您还未登录,请登录后发表或查看评论