[OoB] 使用 Spring Boot REST 服务(Java/Arduino)控制炮塔
这是我“打发时间”系列中关于 Arduino 业余项目的第五篇文章。
这是我“打发时间”系列中关于 Arduino 业余项目的第五篇文章。之前的文章都是基于 .NET 的。
- [OoB] 使用摇杆和伺服电机移动摄像头(Arduino/SharpDX/WinForms)
- [OoB] 使用继电器、Arduino 和 .NET WinForms 射击彩弹枪
- [OoB] 使用 Arduino、C#、JavaScript 和 HTML5 进行声纳探测(第二部分)
- [OoB] 使用 Arduino、C#、JavaScript 和 HTML5 进行声纳探测
现在是时候来点 Java 了!
我将向您展示如何使用 Spring Boot 框架快速创建一个 RESTful Web 服务,该服务可以接收 JSON 请求并向 Arduino 发送命令。目标是控制一个基于伺服电机和继电器的炮塔,就像这个一样。
点击这里,了解此类炮塔的运作方式。视频展示了一个基于 ASG 手枪的原型。别担心,我的目标很和平:我想制造一个彩弹枪炮塔 :)
在这篇文章中,我**不会**讨论任何电子设置或 Arduino 代码。请参阅上面链接的文章,了解有关 PC-Arduino 通信、控制伺服电机和构建继电器电路的信息……我假设您知道 Spring Boot 是什么,但不需要高级知识也能理解本文。
项目的完整代码可在 GitHub 上获取(包括 Java 应用程序和 Arduino 草图)……
创建基本可用的 Spring Boot 项目的最简单方法是使用 Spring Initializr。我通过填写表单来启动我的项目,如下所示:
请注意,Gradle 用于创建(fat)Jar 包,使用的是 Java 1.8,并且唯一必需的依赖项是 Web。此设置将生成一个在嵌入式 Tomcat 上运行的应用程序,因此无需安装任何 Web 服务器。该应用程序支持 REST 控制器,因此我们可以轻松地以清晰的方式处理基于 JSON 的通信……
废话不多说,这是负责接收客户端消息的代码片段:
package springarduino;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TurretController {
private final ArduinoConnection arduino;
@Autowired
public TurretController(ArduinoConnection arduino) {
this.arduino = arduino;
}
@RequestMapping(value = "turret/execute",
method = RequestMethod.POST, consumes = "application/json")
public TurretResponse executeTurretAction(@RequestBody TurretRequest request) {
if (request.getPan() < 0 || request.getPan() > 180) {
throw new IllegalArgumentException
("Pan out of 0..180 range (" + request.getPan() + ")");
}
if (request.getTilt() < 0 || request.getTilt() > 180) {
throw new IllegalArgumentException
("Tilt out of 0..180 range (" + request.getTilt() + ")");
}
boolean sent = arduino.controlTurret(request.getPan(), request.getTilt(), request.isFire());
if (!sent) {
throw new RuntimeException("Command not sent :(");
}
return new TurretResponse(request.getId(), "Command sent :)");
}
}
TurretController
类带有 @RestController
注解和一个标记有 @RequestMapping
的 public
方法。该映射指定,当客户端向 .../turret/execute URL 发送 POST 请求时,将调用 executeTurretAction
方法。任何能够发送 POST
请求并设置 Content-Type="application/json"
的 HTTP 客户端都可以与此类 Spring 控制器方法通信。它可以是某个桌面应用程序,也可以是带有少量 jQuery 的简单 HTML 页面。我使用 Postman Chrome App 来准备请求。在下一篇文章中,我将介绍如何从运行 PhoneGap/AngularJS 应用程序的智能手机与此类控制器通信……
executeTurretAction
方法需要一个 TurretRequest
类型的参数。
package springarduino;
public class TurretRequest {
private int id;
private int pan;
private int tilt;
private boolean fire;
// public getters and setters hidden for brevity
}
如果客户端发送如下 JSON 数据:
{ "id": "311", "pan": "111", "tilt": "99", "fire": "true" }
Spring 将负责创建 TurretRequest
对象。我们的服务方法返回 TurretResponse
。
package springarduino;
public class TurretResponse {
private int id;
private String message;
public TurretResponse(int id, String message) {
this.id = id;
this.message = message;
}
// public getters and setters hidden for brevity
}
如果一切顺利,这些数据将发送回客户端:
{
"id": 19,
"message": "Command sent :)"
}
您无需做任何特殊事情即可实现此目的。Spring 选择 HttpMessageConverter
实现来以客户端期望的格式创建响应。我们 @RestController
的另一个很棒的功能是错误处理。假设客户端提供了无效的倾斜角度值,那么返回给客户端的响应就是这样的:
{
"timestamp": 1424382893952,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"message": "Tilt out of 0..180 range (222)",
"path": "/turret/execute"
}
这种消息很容易在错误回调中处理(由于 500 HTTP 状态码),并且包含有用的属性,例如 message
和 exception
。
请注意,executeTurretAction
方法内部使用了类型为 ArduinoConnection
的 arduino
对象。ArduinoConnecton
是一个负责通过串行端口与 Arduino 通信的 Spring Bean。
我们的控制器通过 Spring 的 IoC 容器获得了对正确对象的引用。TurretController
的构造函数带有 @Autowired
注解,因此 Spring 知道需要注入 ArduinoConnection
对象。
这是负责与 Arduino 通信的类:
package springarduino;
import gnu.io.NRSerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.DataOutputStream;
@Component
public class ArduinoConnection {
private static final int MESSAGE_SEPARATOR = 255;
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Value("${arduinoPortName}")
private String portName;
@Value("${arduinoBaudRate}")
private int baudRate;
private NRSerialPort serial;
@PostConstruct
public void connect() {
log.info("ArduinoConnection PostConstruct callback: connecting to Arduino...");
serial = new NRSerialPort(portName, baudRate);
serial.connect();
if (serial.isConnected()) {
log.info("Arduino connection opened!");
}
}
@PreDestroy
public void disconnect() {
log.info("ArduinoConnection PreDestroy callback: disconnecting from Arduino...");
if (serial != null && serial.isConnected()) {
serial.disconnect();
if (!serial.isConnected()) {
log.info("Arduino connection closed!");
}
}
}
public boolean controlTurret(int pan, int tilt, boolean fire){
try {
// Actual values sent to Arduino will be in proper unsigned byte range (0..255)
byte[] message = new byte[]{(byte) pan, (byte) tilt, (byte) (fire ? 1 : 0), (byte) MESSAGE_SEPARATOR};
DataOutputStream stream = new DataOutputStream(serial.getOutputStream());
stream.write(message);
log.info("Turret control message sent (pan={}, tilt={}, fire={})!", pan, tilt, fire);
return true;
} catch (Exception ex) {
log.error("Error while sending control message: ", ex);
return false;
}
}
}
@Component
注解用于向 Spring 表明 ArduinoConnection
是一个 Bean,因此 Spring 应负责其生命周期和作为依赖项的使用。默认情况下,Spring 以单例作用域创建 Bean。这对我们来说没问题——我们只需要一个这样的对象。connect
方法标记有 @PostConstruct
。这使得该方法成为一个初始化回调,在创建 ArduinoConnection
对象时(应用程序启动时)会被调用。@PreDestroy
用于 disconnect
方法,以确保在程序关闭时释放与串行端口的连接。
controlTurret
方法是负责将炮塔动作请求发送到 Arduino 的代码片段。还记得吗?它在 TurretController.executeTurretAction
中使用。它使用 NRSerialPort 实例通过串行端口进行通信(gnu.io.NRSerialPort
包支持此功能)。它来自 NeuronRobotics/nrjavaserial 库,该库是 RXTX 的一个分支,极大地简化了串行端口的访问。nrjavaserial
负责加载访问端口所需的正确本地库(它在我的 Windows 7 x64 上运行良好)。如前所述,我不会在这篇文章中讨论 Arduino 通信和微控制器代码。我只想指出,在创建消息数组时,您不必担心将 int 转换为 byte。这很遗憾,但 Java 没有无符号字节,所以调试时它会显示 (byte)MESSAGE_SEPARATOR
(即 255)为 -1
,但正确的值将通过线路发送到 Arduino。看看这个来自 Free Device Monitoring Studio 的屏幕截图(您可以使用此工具检查通过串行端口发送到 Arduino 的数据)。
让我们回到 ArduinoConnection
类:portName
和 baudRate
属性标记有 @Value
注解。这样,就可以非常方便地从配置文件中获取设置。您所要做的就是在 /src/main/resources 目录下创建一个名为 application.properties
的文件,并将值自动从 config 中加载。这是 application.properties 文件的内容:
server.address = 192.168.0.17
server.port = 8090
arduinoPortName = COM3
arduinoBaudRate = 9600
除了上述 Arduino 设置外,还有另外两个元素,即:server.address
和 server.port
。默认情况下,Spring Boot 在 localhost:8080
上运行应用程序。我已经将其更改为自定义设置,以便从连接到我的 WiFi 网络的设备轻松访问应用程序……从这些设备访问 TurretController
是我将以下 CorsFilter
类添加到项目的原因。
package springarduino;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
得益于此跨域资源共享过滤器,可以轻松地向 executeTurretAction
方法发出 Ajax 调用,而无需使用 JSONP 等技巧来规避同源策略限制。