65.9K
CodeProject 正在变化。 阅读更多。
Home

[OoB] 使用 Spring Boot REST 服务(Java/Arduino)控制炮塔

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.14/5 (5投票s)

2015年3月6日

CPOL

5分钟阅读

viewsIcon

14162

这是我“打发时间”系列中关于 Arduino 业余项目的第五篇文章。

这是我“打发时间”系列中关于 Arduino 业余项目的第五篇文章。之前的文章都是基于 .NET 的。

现在是时候来点 Java 了!

我将向您展示如何使用 Spring Boot 框架快速创建一个 RESTful Web 服务,该服务可以接收 JSON 请求并向 Arduino 发送命令。目标是控制一个基于伺服电机和继电器的炮塔,就像这个一样。

Gun turret prototype... Click to enlarge...

点击这里,了解此类炮塔的运作方式。视频展示了一个基于 ASG 手枪的原型。别担心,我的目标很和平:我想制造一个彩弹枪炮塔 :)

在这篇文章中,我**不会**讨论任何电子设置或 Arduino 代码。请参阅上面链接的文章,了解有关 PC-Arduino 通信、控制伺服电机和构建继电器电路的信息……我假设您知道 Spring Boot 是什么,但不需要高级知识也能理解本文。

项目的完整代码可在 GitHub 上获取(包括 Java 应用程序和 Arduino 草图)……

创建基本可用的 Spring Boot 项目的最简单方法是使用 Spring Initializr。我通过填写表单来启动我的项目,如下所示:

Spring Initializr settings... Click to enlarge...

请注意,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 注解和一个标记有 @RequestMappingpublic 方法。该映射指定,当客户端向 .../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 状态码),并且包含有用的属性,例如 messageexception

请注意,executeTurretAction 方法内部使用了类型为 ArduinoConnectionarduino 对象。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 的数据)。

Bytes sent to Arduino... Click to enlarge...

让我们回到 ArduinoConnection 类:portNamebaudRate 属性标记有 @Value 注解。这样,就可以非常方便地从配置文件中获取设置。您所要做的就是在 /src/main/resources 目录下创建一个名为 application.properties 的文件,并将值自动从 config 中加载。这是 application.properties 文件的内容:

server.address = 192.168.0.17
server.port = 8090

arduinoPortName = COM3
arduinoBaudRate = 9600

除了上述 Arduino 设置外,还有另外两个元素,即:server.addressserver.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 等技巧来规避同源策略限制。

就是这样!Java Web 应用程序的所有有趣元素都已讨论完毕。完整代码可在 GitHub 上获取。由于这是一个基于 Gradle 的项目,运行它的方法就像键入 gradlew run 一样简单。我在 存储库中包含了 Gradle Wrapper,因此您甚至不需要安装 Gradle……
© . All rights reserved.