消息队列应用:树莓派、PC与Arduino的交互实践
1. 树莓派与PC间的数据共享
1.1 项目背景
我们知道树莓派上运行着Mosquitto,通常可以使用终端结合测试程序来发送和接收消息。现在,我们将通过JavaFX应用程序在PC上实现相同的功能,这样能让操作更加直观。
1.2 修改项目配置
首先,从一个最小化的JavaFX应用程序开始,对
pom.xml
和
module-info.java
进行修改。
-
修改
pom.xml
:
<artifactId>javafx-mosquitto</artifactId>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
-
修改
module-info.java:
module be.webtechie {
requires javafx.controls;
requires org.eclipse.paho.client.mqttv3;
exports be.webtechie.javafxmosquitto;
}
1.3 连接并发布消息到Mosquitto
要连接到Mosquitto,需要知道树莓派的IP地址。以下是连接和发布消息的代码示例:
MqttClient client = new MqttClient("tcp://" + ipAddress + ":1883",
MqttClient.generateClientId());
client.connect();
String messageText = "Hello from PC";
MqttMessage message = new MqttMessage();
message.setPayload(messageText.getBytes());
client.publish("testing/TestTopic", message);
完整的带有错误处理的代码可以在
QueueClient.java
中找到。
1.4 订阅Mosquitto的消息
在
QueueClient
脚本中可以实现对主题的订阅:
ObservableList<String> queueItems = FXCollections.observableArrayList();
client.setCallback(new ClientCallback(this.queueItems));
client.subscribe("testing/TestTopic");
这里使用了
ObservableList
,方便后续在UI中显示接收到的消息。
ClientCallback
类实现了
MqttCallback
接口,每当有新消息发布到
testing/TestTopic
时,该类的方法就会被调用:
public class ClientCallback implements MqttCallback {
private ObservableList<String> queueItems;
public ClientCallback(ObservableList<String> queueItems) {
this.queueItems = queueItems;
}
@Override
public void connectionLost(Throwable throwable) {
System.out.println("Connection to MQTT broker lost!");
}
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
String message = new String(mqttMessage.getPayload());
System.out.println("Message received:\n\t" + message);
Platform.runLater(() -> {
queueItems.add(message);
});
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
System.out.println("Delivery complete");
}
}
为了避免Java应用程序的连接线程和更新屏幕的JavaFX线程之间出现线程错误,使用了
Platform.runLater
方法。
1.5 用户界面
用户界面使用了
ListView
、
TextInput
和
Button
。当运行这个应用程序时,它既是发布者又是订阅者,向队列发送消息后,消息会同时显示在列表中。在树莓派上也能看到相同的消息,并且可以向主题推送消息,这些消息会同时显示在PC上。
2. 通过Mosquitto使用JavaFX控制Arduino
2.1 项目概述
接下来,我们将Arduino加入到系统中,用于控制LED灯带。这个项目会更复杂,涉及到多个组件:
- 带有WiFi功能的Arduino,可控制LED灯带实现单色显示或颜色动画。
- 树莓派上运行着Mosquitto代理应用程序和Java应用程序,Java应用程序包含一个网页用于选择LED效果,还有一个JavaFX界面可以选择颜色和动画。
- 一个或多个带有网页浏览器的PC,可以加载网页并与树莓派上的Java应用程序进行交互。
2.2 消息定义
首先,需要确定通过Mosquitto交换的消息结构和主题。消息结构使用字符串,以“:”作为不同值的分隔符。具体结构如下:
- Mosquitto中的主题为“ledCommand”,新命令将发布到该主题。
- 命令格式为“COMMAND_ID:SPEED:R1:G1:B1:R2:G2:B2”,各部分含义如下:
-
COMMAND_ID
:所需LED效果的编号。
-
SPEED
:效果之间的间隔时间(毫秒)。
-
RGB
:两组红、绿、蓝颜色值,范围在0到255之间。
| ID | 名称 | 是否使用速度 | 是否使用RGB1 | 是否使用RGB2 |
|---|---|---|---|---|
| 1 | 固定颜色(RGB1) | 否 | 是 | 否 |
| 2 | 从RGB1到RGB2的静态渐变 | 否 | 是 | 是 |
| 3 | 闪烁(在RGB1和RGB2之间) | 是 | 是 | 是 |
| 4 | 跑动效果(RGB1逐个显示,其他为RGB2) | 是 | 是 | 是 |
| 5 | 使用颜色渐变的渐变彩虹 | 是 | 否 | 否 |
| 6 | 从RGB1到RGB2的静态彩虹 | 否 | 是 | 是 |
| 98 | 全白 | 否 | 否 | 否 |
| 99 | 全灭 | 否 | 否 | 否 |
以下是一些命令示例:
-
1:20:255:0:0
:所有LED固定为全红色。
-
3:2500:255:0:0:0:0:255
:所有LED每2.5秒在全红色和全蓝色之间切换。
-
5:50:255:0:0:0:0:255
:所有LED为蓝色,一个跑动的LED为红色,持续50毫秒。
2.3 Arduino部分
2.3.1 硬件连接
使用WS2812B类型的LED灯带,该灯带只需一个引脚控制,连接到Arduino的引脚6。电源连接到5V和接地引脚。需要注意的是,不要连接过多的LED,以免功率过大。如果要控制长灯带,需要单独的电源,并将Arduino板的接地与电源的接地相连,否则可能会出现意外的LED效果。
2.3.2 安装库
在Arduino IDE的“Tools”菜单中,选择“Manage libraries…”,搜索并安装以下库:
- AdaFruit NeoPixel by Adafruit
- WiFiNINA by Arduino
- ArduinoMqttClient by Arduino
2.3.3 代码拆分
为了使Arduino代码更易于理解,将功能拆分为不同的“ino”文件,其中
WifiMosquittoListener.ino
是主文件:
-
WifiMosquittoListener.ino
:包含
setup()
和
loop()
函数的主文件。
-
ConnectionHandler
:处理传入的Mosquitto消息。
-
LedEffects
:生成不同的LED动画代码。
-
MessageHandler
:将从Mosquitto和串口接收到的命令转换为
LedEffects
使用的值。
-
SerialFunctions
:检查串口数据(测试命令)是否可用。
2.3.4 主代码
Arduino项目有两个必需的函数:
-
setup()
:在板启动或重置时调用一次,用于初始化库、变量、引脚等。
void setup() {
// 配置串口速度并等待其可用
Serial.begin(9600);
while (!Serial) {
; // 等待串口连接,仅适用于原生USB端口
}
// 初始化LED
initLeds();
// 连接到WiFi
Serial.println("--- Connecting to WiFi ---");
while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
// 失败,重试
Serial.print(".");
delay(5000);
}
// 连接到Mosquitto
Serial.println("--- Connecting to Mosquitto ---");
mqttClient.onMessage(onMqttMessage);
if (!mqttClient.connect(broker, port)) {
Serial.print("MQTT connection failed! Error code = ");
Serial.println(mqttClient.connectError());
} else {
mqttClient.subscribe(topic);
Serial.println("MQTT connection ready");
}
// 设置初始LED效果
String message = "2:0:14:255:0:255:5:0";
message.toCharArray(input, 50);
}
-
loop():持续循环以主动控制板。
void loop() {
mqttClient.poll();
checkSerial();
handleMessage();
currentLoop++;
// 仅当循环次数超过定义的动画速度时执行LED效果
if (currentLoop >= animationSpeed) {
// 根据命令ID调用正确的LED效果
if (commandId == 1) {
setStaticColor();
} else if (commandId == 2) {
setStaticFade();
} else if (commandId == 3) {
setBlinking();
} else if (commandId == 4) {
// ... 为每个命令ID调用正确的函数
}
currentLoop = 0;
}
delay(5);
}
2.3.5 命令字符串转换
在
MessageHandler.ino
中,将接收到的字符串消息解析为数值:
void handleMessage() {
char* part = strtok(input, ":");
int position = 0;
while (part != 0) {
int value = atoi(part);
if (position == 0) {
commandId = value;
} else if (position == 1) {
animationSpeed = value;
} else if (position == 2) {
r1 = value;
} else if (position == 3) {
// ... 处理每个部分并赋值
}
position++;
}
}
2.3.6 接收命令
- 通过MQTT接收命令 :
void onMqttMessage(int messageSize) {
while (mqttClient.available()) {
mqttClient.read(input, sizeof(input));
input[50] = 0;
}
}
- 通过串口接收命令 :
void checkSerial() {
if (Serial.available() > 0) {
String message = Serial.readString();
message.toCharArray(input, 50);
input[50] = 0;
}
}
在这两个函数中,都确保最后一个字节为0,以避免处理输入时陷入无限循环。
2.3.7 LED效果
在
LedEffects.ino
中,包含了之前表格中定义的LED效果函数,你可以根据需要进行扩展。以下是一些示例:
- 初始化LED灯带:
void initLeds() {
strip.begin();
strip.setBrightness(100);
strip.clear();
strip.show(); // 初始化所有像素为“关闭”状态
}
- 跑动LED效果:
void setRunningLight() {
if (currentAction >= NUMBER_OF_LEDS) {
currentAction = 0;
}
// 显示颜色1
strip.setPixelColor(currentAction, rgb1);
strip.show();
// 为下一次循环重置为颜色2
strip.setPixelColor(currentAction, rgb2);
currentAction++;
}
此外,还有两个辅助函数用于计算渐变颜色和彩虹颜色:
// 计算颜色1和2之间的渐变颜色
uint32_t getGradientColor(uint16_t pos) {
...
}
// 获取全彩色轮的颜色
uint32_t getWheelColor(byte pos) {
...
}
2.4 Java应用程序
2.4.1 项目概述
我们将使用JavaFX和Undertow Web服务器构建一个控制器应用程序。这样,该应用程序既可以在树莓派上作为Web服务器运行,通过浏览器在任何PC上更改LED设置,也可以在PC上运行进行测试。
2.4.2 依赖库
使用以下两个库来实现所需的功能:
- Undertow:轻量级但非常灵活的Java Web服务器,可以完全集成到应用程序中。
- JavaFX:用于创建UI,选择颜色和动画,并可视化Arduino上的活动颜色和动画。
2.4.3 EventManager
在应用程序中添加了
EventManager
,它允许两个不同的UI部分监听传入的LED命令:
public class EventManager {
private List<EventListener> eventListeners = new ArrayList<>();
public void addListener(EventListener eventListener) {
this.eventListeners.add(eventListener);
}
public void sendEvent(LedCommand ledCommand) {
this.eventListeners.forEach(l -> l.onQueueMessage(ledCommand));
}
}
public interface EventListener {
void onQueueMessage(LedCommand ledCommand);
}
2.4.4 Mosquitto连接
复用了前面示例中的代码,但使用
EventManager
将接收到的消息转发给应用程序中添加到监听器列表的不同组件。在初始化LED面板时进行如下操作:
LedControlPanel ledControlPanel = new LedControlPanel(queueClient);
eventManager.addListener(ledControlPanel);
this.led = new Group(ledControlPanel);
2.4.5 LED效果选择UI
该UI会根据接收到的命令更改颜色轮和速度值。更改颜色或按下按钮时,会向Mosquitto发送命令,Arduino会接收并执行这些命令。UI使用了一组JavaFX组件,并添加了一些CSS样式。以下是LED效果选择按钮的CSS样式:
.ledButton {
-fx-font: 20px "Sans";
-fx-padding: 10;
-fx-background-color: #041E37;
-fx-text-fill: #728CA6;
-fx-font-weight: bold;
-fx-min-width: 200;
-fx-min-height: 30;
}
.ledButton:hover {
-fx-text-fill: yellow;
}
.ledButton:selected {
-fx-text-fill: #041E37;
-fx-background-color: yellow, #728CA6;
-fx-background-insets: 0 0 0 0, 2 2 2 2;
}
通过更改CSS样式,可以实现更多的背景颜色组合。
2.4.6 颜色选择面板
颜色选择轮由Gerrit Grunwald制作,该组件需要实现
EventListener
接口,以便在接收到新消息时做出响应:
public class LedControlPanel extends HBox implements EventListener {
...
}
以下是该UI的部分代码:
public LedControlPanel(QueueClient queueClient) {
this.queueClient = queueClient;
this.setSpacing(25);
VBox colorSelectors = new VBox();
colorSelectors.setSpacing(25);
this.getChildren().add(colorSelectors);
this.colorSelector1 = new ColorSelector();
this.colorSelector1.setPrefSize(250, 250);
this.colorSelector1.selectedColorProperty().addListener(e -> this.sendMessage());
this.colorSelector1.setSelectedColor(Color.BLUE);
colorSelectors.getChildren().add(this.colorSelector1);
GridPane effectButtons = new GridPane();
effectButtons.setHgap(10);
effectButtons.setVgap(10);
this.getChildren().add(effectButtons);
this.btStatic = new Button("Fixed");
this.btStatic.getStyleClass().add("ledButton");
this.btStatic.setOnAction(e -> this.setEffect(LedEffect.STATIC));
effectButtons.add(this.btStatic, 0, 0);
...
Label lblSpeed = new Label("Speed");
lblSpeed.getStyleClass().add("ledSpeed");
GridPane.setHalignment(lblSpeed, HPos.CENTER);
effectButtons.add(lblSpeed, 0, 5, 2, 1);
this.slider = new Slider();
this.slider.setShowTickLabels(true);
this.slider.setShowTickMarks(true);
this.slider.valueProperty().addListener((o, old, new) -> this.sendMessage());
effectButtons.add(this.slider, 0, 6, 2, 1);
this.setEffect(LedEffect.ALL_OUT);
}
当接收到来自Mosquitto的消息时,会更改颜色轮和滑块的值:
@Override
public void onQueueMessage(LedCommand ledCommand) {
this.blockSending = true;
this.setEffect(ledCommand.getLedEffect());
this.slider.setValue(ledCommand.getSpeed());
this.colorSelector1.setSelectedColor(ledCommand.getColor1());
this.colorSelector2.setSelectedColor(ledCommand.getColor2());
this.blockSending = false;
}
根据选择的
LedEffect
,启用或禁用可用的UI元素,并突出显示所选效果的按钮:
private void setEffect(LedEffect ledEffect) {
this.selectedLedEffect = ledEffect;
this.colorSelector1.setDisable(!ledEffect.useColor1());
this.colorSelector2.setDisable(!ledEffect.useColor2());
this.slider.setDisable(!ledEffect.useSpeed());
this.slider.setMin(ledEffect.getMinimumSpeed());
this.slider.setMax(ledEffect.getMaximumSpeed());
this.btStatic.setSelected(ledEffect == LedEffect.STATIC);
this.btStaticFade.setSelected(ledEffect == LedEffect.STATIC_FADE);
this.btBlinking.setSelected(ledEffect == LedEffect.BLINKING);
this.btRunning.setSelected(ledEffect == LedEffect.RUNNING);
this.btStaticRainbow.setSelected(ledEffect == LedEffect.STATIC_RAINBOW);
this.btFadingRainbow.setSelected(ledEffect == LedEffect.FADING_RAINBOW);
this.btWhite.setSelected(ledEffect == LedEffect.ALL_WHITE);
this.btClear.setSelected(ledEffect == LedEffect.ALL_OUT);
this.sendMessage();
}
当按钮被点击或颜色改变时,会向Mosquitto发布新命令:
private void sendMessage() {
if (this.blockSending) {
return;
}
LedCommand ledCommand = new LedCommand(
this.selectedLedEffect,
(int) this.slider.getValue(),
this.colorSelector1.getSelectedColor(),
this.colorSelector2.getSelectedColor()
);
if (this.queueClient != null) {
this.queueClient.sendMessage(ledCommand);
}
}
2.4.7 队列日志UI
QueueMessagesList
UI作为第二个
EventListener
示例,显示所有接收到的命令的历史记录表格。该类实现了
EventListener
接口,并将接收到的消息存储在
ObservableList
中:
public class QueueMessagesList extends TableView implements EventListener {
private ObservableList<ReceivedMessage> list =
FXCollections.observableArrayList();
@Override
public void onQueueMessage(LedCommand ledCommand) {
this.list.add(0, new ReceivedMessage(ledCommand));
}
}
ReceivedMessage
是一个数据类,包含时间戳和
LedCommand
:
public class ReceivedMessage {
private final String timestamp;
private final LedCommand ledCommand;
private final DateTimeFormatter dateFormat =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public ReceivedMessage(LedCommand ledCommand) {
this.timestamp = LocalDateTime.now().format(dateFormat);
this.ledCommand = ledCommand;
}
public String getTimestamp() {
return timestamp;
}
public LedCommand getLedCommand() {
return ledCommand;
}
}
表格在类构造函数中初始化,每个列都有自己的
setCellFactory
,可以根据
LedCommand
的值更改单元格的背景颜色。
2.4.8 网页界面
为了能在与树莓派同一网络的任何PC上更改LED灯带设置,我们使用网页界面。为此,需要在
pom.xml
文件中添加Undertow依赖:
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.0.26.FINAL</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<version>2.0.26.FINAL</version>
</dependency>
在主启动函数中初始化并配置服务器:
Undertow server = Undertow.builder()
.addHttpListener(8080, "IP_ADDRESS_OF_YOUR_PI")
.setHandler(new WebHandler(queueClient))
.build();
server.start();
WebHandler
类实现了
HttpHandler
接口,根据请求路径发送不同的LED命令:
public class WebHandler implements HttpHandler {
final QueueClient queueClient;
public WebHandler(QueueClient queueClient) {
this.queueClient = queueClient;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
String path = exchange.getRequestPath();
if (this.queueClient == null) {
this.returnError(exchange, StatusCodes.INTERNAL_SERVER_ERROR,
"Sorry, Message Queue Client is not available!"
+ " Can not send your request...");
} else if (path.equals("/red-alert")) {
this.queueClient.sendMessage(
new LedCommand(LedEffect.BLINKING, 1000, Color.RED, Color.WHITE)
);
this.returnSuccess(exchange, "RED ALERT message has been sent");
} else if (path.equals("/all-white")) {
this.queueClient.sendMessage(new LedCommand(LedEffect.ALL_WHITE));
this.returnSuccess(exchange, "ALL WHITE message has been sent");
} else if (path.equals("/all-out")) {
this.queueClient.sendMessage(new LedCommand(LedEffect.ALL_OUT));
this.returnSuccess(exchange, "ALL OUT message has been sent");
} else {
this.returnError(exchange, StatusCodes.NOT_FOUND,
"The requested path is not available");
}
}
private void returnSuccess(HttpServerExchange exchange, String message) {
this.returnPage(exchange, StatusCodes.OK, "LED command handled", message);
}
private void returnError(HttpServerExchange exchange, int statusCode,
String message) {
this.returnPage(exchange, statusCode, "Error", message);
}
private void returnPage(HttpServerExchange exchange, int statusCode,
String title, String content) {
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n")
.append("<html>\n")
.append("<head>\n")
.append("<title>").append(title).append("</title>\n")
.append("<style>\n")
.append("// CSS styling")
.append("</style>\n")
.append("</head>\n")
.append("<body>\n")
.append("<ul>\n")
.append("<li><a href='/red-alert'>RED ALERT</a></li>\n")
.append("<li><a href='/all-white'>ALL WHITE</a></li>\n")
.append("<li><a href='/all-out'>ALL OUT</a></li>\n")
.append("</ul>\n")
.append("<p>").append(content).append("</p>\n")
.append("</body>\n ")
.append("</html>");
}
}
通过以上步骤,我们实现了树莓派、PC和Arduino之间的交互,利用消息队列和JavaFX、Web服务器等技术,实现了对LED灯带的灵活控制。这个项目不仅展示了如何使用消息队列进行数据传输和设备控制,还提供了一个完整的项目架构和代码示例,你可以根据自己的需求进行扩展和修改。
2.5 项目总结与拓展思路
2.5.1 项目总结
本项目通过消息队列(Mosquitto)实现了树莓派、PC和Arduino之间的交互,成功控制了LED灯带。主要步骤包括在树莓派上运行Mosquitto代理,在PC上使用JavaFX应用程序进行消息的发布和订阅,以及在Arduino上接收并执行来自Mosquitto的命令。同时,还搭建了Web界面,方便在同一网络内的任何PC上通过浏览器控制LED灯带。
整个项目涉及多个技术领域,如MQTT协议、JavaFX UI设计、Arduino编程、Web服务器搭建等,展示了如何将不同的硬件和软件组件集成在一起,实现一个完整的控制系统。
2.5.2 拓展思路
-
增加更多LED效果
:在
LedEffects.ino中,可以根据自己的创意和编程能力添加更多的LED效果,如动态彩虹、呼吸灯等。 - 多设备控制 :可以扩展项目,控制多个Arduino设备或多个LED灯带,实现更复杂的灯光场景。
- 安全优化 :在实际应用中,需要考虑消息传输的安全性,可以使用SSL/TLS加密来保护MQTT通信。
- 移动应用支持 :开发移动应用程序,通过手机或平板电脑远程控制LED灯带,增加控制的便捷性。
2.6 项目流程图
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A(开始):::process --> B(树莓派运行Mosquitto):::process
B --> C(PC运行JavaFX应用):::process
C --> D(连接到Mosquitto):::process
D --> E{是否发布消息?}:::decision
E -- 是 --> F(发布消息到Mosquitto):::process
F --> G(Arduino接收消息):::process
G --> H(解析消息并执行LED效果):::process
E -- 否 --> I{是否订阅消息?}:::decision
I -- 是 --> J(订阅Mosquitto主题):::process
J --> K(PC接收消息并显示):::process
I -- 否 --> L(结束):::process
H --> M{是否继续控制?}:::decision
M -- 是 --> E
M -- 否 --> L
3. 常见问题与解决方案
3.1 Arduino连接问题
- 问题描述 :Arduino无法连接到WiFi或Mosquitto。
-
解决方案
:
-
检查WiFi网络配置,确保
ssid和pass正确。 - 检查Mosquitto的IP地址和端口号是否正确。
- 检查Arduino的网络连接是否正常,可以通过串口输出调试信息来排查问题。
-
检查WiFi网络配置,确保
3.2 消息接收问题
- 问题描述 :PC或Arduino无法接收到消息。
-
解决方案
:
- 检查订阅的主题是否正确。
- 检查消息的格式是否符合定义。
- 检查网络连接是否稳定,是否存在丢包现象。
3.3 LED效果异常
- 问题描述 :LED灯带显示的效果与预期不符。
-
解决方案
:
- 检查LED灯带的硬件连接是否正确,是否存在短路或断路情况。
-
检查
LedEffects.ino中的代码是否正确,是否存在逻辑错误。 - 检查接收到的命令是否正确,是否包含无效的参数。
4. 总结
通过本项目的实践,我们深入了解了消息队列的应用,掌握了如何使用MQTT协议实现设备之间的通信。同时,我们还学习了如何使用JavaFX和Arduino进行UI设计和硬件控制,以及如何搭建Web服务器实现远程控制。
这个项目不仅提供了一个完整的项目示例,还为我们提供了一个拓展和创新的基础。你可以根据自己的需求和兴趣,对项目进行进一步的优化和扩展,实现更多有趣的功能。希望本项目能对你有所帮助,让你在物联网和嵌入式开发的道路上迈出坚实的一步。
5. 参考资源
虽然本文没有直接引用具体的参考资料,但在项目开发过程中,以下资源可能会对你有所帮助:
-
MQTT官方文档
:了解MQTT协议的详细信息和使用方法。
-
JavaFX官方文档
:学习JavaFX的UI设计和开发技巧。
-
Arduino官方文档
:掌握Arduino的编程和硬件控制方法。
-
Undertow官方文档
:了解Undertow Web服务器的使用和配置。
你可以通过访问这些官方网站,获取更多的技术资料和教程,帮助你更好地完成项目开发。
超级会员免费看
2905

被折叠的 条评论
为什么被折叠?



