Rework Mqtt Implementation

This commit is contained in:
2025-12-29 17:56:17 +01:00
parent b1bec24bc3
commit 6cdff81296
12 changed files with 231 additions and 59 deletions

View File

@@ -8,6 +8,10 @@ services:
- "8000:8080" - "8000:8080"
- "5005:5005" - "5005:5005"
environment: environment:
BROKER_URL: "tcp://api-broker:1883"
BROKER_SECURE_URL: "tcp://api-broker:8883"
BROKER_USERNAME: "board-mate-api"
BROKER_PASSWORD: "hepl"
JWT_SECRET: "enY3OWU4djFyMTByNTZhcG9uY3Z0djQ5cnY0eDhhNWM0bjg5OTRjNDhidA==" JWT_SECRET: "enY3OWU4djFyMTByNTZhcG9uY3Z0djQ5cnY0eDhhNWM0bjg5OTRjNDhidA=="
SSL_KEYSTORE_PATH: "/certs/keystore.p12" SSL_KEYSTORE_PATH: "/certs/keystore.p12"
SPRING_DATA_MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db" SPRING_DATA_MONGODB_URI: "mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db"
@@ -50,7 +54,7 @@ services:
mongodb: mongodb:
image: mongo:latest image: mongo:latest
container_name: mongo-db container_name: api-database
environment: environment:
- MONGO_INITDB_DATABASE=board-mate-db - MONGO_INITDB_DATABASE=board-mate-db
- MONGO_INITDB_ROOT_PASSWORD=secret - MONGO_INITDB_ROOT_PASSWORD=secret
@@ -63,10 +67,11 @@ services:
mosquitto: mosquitto:
image: eclipse-mosquitto:latest image: eclipse-mosquitto:latest
container_name: mosquitto container_name: api-broker
ports: ports:
- "1883:1883" - "1883:1883"
- "8883:8883" - "8883:8883"
command: ["sh", "/mosquitto/config/init-mosquitto.sh"]
volumes: volumes:
- ./mosquitto/config:/mosquitto/config - ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data - ./mosquitto/data:/mosquitto/data

View File

@@ -1,5 +1,7 @@
db = db.getSiblingDB("board-mate-db"); db = db.getSiblingDB("board-mate-db");
db.createCollection("clients");
db.createCollection("telemetry");
db.createCollection("games"); db.createCollection("games");
db.createUser({ db.createUser({

View File

@@ -0,0 +1,18 @@
#!/bin/sh
PASSWORD_FILE=/mosquitto/config/passwords
echo "Creating password file with pre-registered users..."
if [ ! -f "$PASSWORD_FILE" ]; then
touch "$PASSWORD_FILE"
chmod 600 "$PASSWORD_FILE"
# Add pre-registered users
mosquitto_passwd -b "$PASSWORD_FILE" board-mate-api hepl
echo "Password file created !"
else
echo "Password file exists, skipping this step"
fi
echo "Starting mosquitto..."
exec mosquitto -c /mosquitto/config/mosquitto.conf -v

Binary file not shown.

View File

@@ -183,3 +183,49 @@ To fix this, use `chmod 0700 /mosquitto/config/passwords`.
1766955294: Saving in-memory database to /mosquitto/data//mosquitto.db. 1766955294: Saving in-memory database to /mosquitto/data//mosquitto.db.
1766955496: mosquitto version 2.0.22 terminating 1766955496: mosquitto version 2.0.22 terminating
1766955496: Saving in-memory database to /mosquitto/data//mosquitto.db. 1766955496: Saving in-memory database to /mosquitto/data//mosquitto.db.
1767024228: mosquitto version 2.0.22 starting
1767024228: Config loaded from /mosquitto/config/mosquitto.conf.
1767024228: Warning: File /mosquitto/config/passwords has world readable permissions. Future versions will refuse to load this file.
To fix this, use `chmod 0700 /mosquitto/config/passwords`.
1767024228: Opening ipv4 listen socket on port 1883.
1767024228: Opening ipv6 listen socket on port 1883.
1767024228: Opening websockets listen socket on port 9001.
1767024228: Opening ipv4 listen socket on port 8883.
1767024228: Opening ipv6 listen socket on port 8883.
1767024228: mosquitto version 2.0.22 running
1767025666: mosquitto version 2.0.22 terminating
1767025666: Saving in-memory database to /mosquitto/data//mosquitto.db.
1767025725: mosquitto version 2.0.22 starting
1767025725: Config loaded from /mosquitto/config/mosquitto.conf.
1767025725: Warning: File /mosquitto/config/passwords has world readable permissions. Future versions will refuse to load this file.
To fix this, use `chmod 0700 /mosquitto/config/passwords`.
1767025725: Opening ipv4 listen socket on port 1883.
1767025725: Opening ipv6 listen socket on port 1883.
1767025725: Opening websockets listen socket on port 9001.
1767025725: Opening ipv4 listen socket on port 8883.
1767025725: Opening ipv6 listen socket on port 8883.
1767025725: mosquitto version 2.0.22 running
1767027264: mosquitto version 2.0.22 terminating
1767027264: Saving in-memory database to /mosquitto/data//mosquitto.db.
1767027266: mosquitto version 2.0.22 starting
1767027266: Config loaded from /mosquitto/config/mosquitto.conf.
1767027266: Warning: File /mosquitto/config/passwords has world readable permissions. Future versions will refuse to load this file.
To fix this, use `chmod 0700 /mosquitto/config/passwords`.
1767027266: Opening ipv4 listen socket on port 1883.
1767027266: Opening ipv6 listen socket on port 1883.
1767027266: Opening websockets listen socket on port 9001.
1767027266: Opening ipv4 listen socket on port 8883.
1767027266: Opening ipv6 listen socket on port 8883.
1767027266: mosquitto version 2.0.22 running
1767027325: mosquitto version 2.0.22 terminating
1767027325: Saving in-memory database to /mosquitto/data//mosquitto.db.
1767027327: mosquitto version 2.0.22 starting
1767027327: Config loaded from /mosquitto/config/mosquitto.conf.
1767027327: Warning: File /mosquitto/config/passwords has world readable permissions. Future versions will refuse to load this file.
To fix this, use `chmod 0700 /mosquitto/config/passwords`.
1767027327: Opening ipv4 listen socket on port 1883.
1767027327: Opening ipv6 listen socket on port 1883.
1767027327: Opening websockets listen socket on port 9001.
1767027327: Opening ipv4 listen socket on port 8883.
1767027327: Opening ipv6 listen socket on port 8883.
1767027327: mosquitto version 2.0.22 running

View File

@@ -0,0 +1,45 @@
import be.naaturel.boardmateapi.common.helpers.Logger;
import be.naaturel.boardmateapi.services.MqttService;
import jakarta.annotation.PreDestroy;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class MqttStarter {
private final MqttService service;
public MqttStarter(MqttService service){
this.service = service;
}
@EventListener(ApplicationReadyEvent.class)
public void start(){
try {
setCallback();
service.subscribe("/board-mate/+/telemetry");
} catch (Exception e){
System.err.println(Arrays.toString(e.getStackTrace()));
}
}
@PreDestroy
public void stop() {
service.disconnect();
}
private void setCallback(){
service.onConnectionLost((cause) -> {
Logger.displayError("Connection lost: " + cause.getMessage());
});
service.onMessageReceived((msg) -> {
Logger.displayInfo("Received message on topic " + msg.getTopic() + ": " + msg.getContent());
});
service.registerCallback();
}
}

View File

@@ -0,0 +1,24 @@
package be.naaturel.boardmateapi.common.models;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import java.nio.charset.StandardCharsets;
public class MqttMessageWrapper {
private final String topic;
private final MqttMessage message;
public MqttMessageWrapper(String topic, MqttMessage message){
this.topic = topic;
this.message = message;
}
public String getContent() {
return new String(message.getPayload(), StandardCharsets.UTF_8);
}
public String getTopic() {
return topic;
}
}

View File

@@ -29,26 +29,7 @@ public class MqttConfig {
@Bean("mqttSubscriber") @Bean("mqttSubscriber")
public MqttClient mqttSubscriber() throws MqttException { public MqttClient mqttSubscriber() throws MqttException {
String subscriberId = properties.getClientId() + "-sub"; String subscriberId = properties.getClientId() + "-sub";
MqttClient client = new MqttClient(properties.getBrokerUrl(), subscriberId); return new MqttClient(properties.getBrokerUrl(), subscriberId);
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
Logger.displayError("Connection lost: " + cause.getMessage());
}
@Override
public void messageArrived(String topic, MqttMessage message) {
Logger.displayInfo("Received message on topic " + topic + ": " + message.toString());
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// Not needed for subscriber
}
});
return client;
} }
} }

View File

@@ -6,9 +6,12 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class MqttProperies { public class MqttProperies {
@Value("${mqtt.broker-url}") @Value("${mqtt.plain.broker-url}")
private String brokerUrl; private String brokerUrl;
@Value("${mqtt.ssl.broker-url}")
private String brokerSecureUrl;
@Value("${mqtt.client-id}") @Value("${mqtt.client-id}")
private String clientId; private String clientId;
@@ -18,12 +21,9 @@ public class MqttProperies {
@Value("${mqtt.password}") @Value("${mqtt.password}")
private String password; private String password;
@Value("${mqtt.topic}")
private String topic;
public String getBrokerUrl() { return brokerUrl; } public String getBrokerUrl() { return brokerUrl; }
public String getBrokerSecureUrl() { return brokerSecureUrl; }
public String getClientId() { return clientId; } public String getClientId() { return clientId; }
public String getUsername() { return username; } public String getUsername() { return username; }
public String getPassword() { return password; } public String getPassword() { return password; }
public String getTopic() { return topic; }
} }

View File

@@ -21,7 +21,7 @@ public class BrokerController {
this.service = service; this.service = service;
} }
@PostMapping("/publish/{topic}") /*@PostMapping("/publish/{topic}")
public ResponseEntity<ResponseBody<?>> publish(@PathVariable String topic, @RequestBody String message){ public ResponseEntity<ResponseBody<?>> publish(@PathVariable String topic, @RequestBody String message){
ResponseBody<?> body = ResponseBody.createEmpty(); ResponseBody<?> body = ResponseBody.createEmpty();
try { try {
@@ -38,6 +38,6 @@ public class BrokerController {
.status(HttpStatus.INTERNAL_SERVER_ERROR) .status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(body); .body(body);
} }
} }*/
} }

View File

@@ -2,15 +2,21 @@ package be.naaturel.boardmateapi.services;
import be.naaturel.boardmateapi.common.exceptions.ServiceException; import be.naaturel.boardmateapi.common.exceptions.ServiceException;
import be.naaturel.boardmateapi.common.helpers.Logger; import be.naaturel.boardmateapi.common.helpers.Logger;
import be.naaturel.boardmateapi.common.models.MqttMessageWrapper;
import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.*;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
@Service @Service
public class MqttService { public class MqttService {
private final MqttClient publisher; private final MqttClient publisher;
private final MqttClient subscriber; private final MqttClient subscriber;
private Consumer<Throwable> onConnectionLost;
private Consumer<MqttMessageWrapper> onMessageReceived;
public MqttService( public MqttService(
@Qualifier("mqttPublisher") MqttClient publisher, @Qualifier("mqttPublisher") MqttClient publisher,
@@ -18,43 +24,88 @@ public class MqttService {
) { ) {
this.publisher = publisher; this.publisher = publisher;
this.subscriber = subscriber; this.subscriber = subscriber;
this.onConnectionLost = null;
this.onMessageReceived = null;
} }
public void publish(String topic, String payload) throws ServiceException { public void onConnectionLost(Consumer<Throwable> consumer){
this.onConnectionLost = consumer;
}
public void onMessageReceived(Consumer<MqttMessageWrapper> consumer){
this.onMessageReceived = consumer;
}
public void disconnect() {
try { try {
MqttMessage message = new MqttMessage(payload.getBytes()); disconnect(publisher);
disconnect(subscriber);
} catch (ServiceException e) {
Logger.displayError("Failed to disconnect MQTT client" + e.getMessage());
}
}
public void publish(String topic, String payload) {
try {
connect(publisher, null);
MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
message.setQos(1); message.setQos(1);
if(!publisher.isConnected()){
publisher.connect();
}
publisher.publish(topic, message); publisher.publish(topic, message);
Logger.displayInfo("Published message: " + payload);
} catch (MqttException e) { } catch (MqttException e) {
throw new ServiceException("Unable to publish message", e); throw new RuntimeException(e);
} finally {
try{
if (publisher.isConnected()) publisher.disconnect();
} catch (MqttException e){
Logger.displayError("Failed to disconnect MQTT client: " + e.getMessage());
}
} }
} }
public void subscribe(String topic) throws ServiceException { public void subscribe(String topic) {
try { try {
MqttConnectOptions options = new MqttConnectOptions(); MqttConnectOptions options = new MqttConnectOptions();
options.setAutomaticReconnect(true); options.setAutomaticReconnect(true);
options.setCleanSession(true); options.setCleanSession(false);
connect(subscriber, options);
if(!subscriber.isConnected()){
subscriber.connect(options);
}
subscriber.subscribe(topic, 1); subscriber.subscribe(topic, 1);
Logger.displayInfo("Subscribed to topic: " + topic);
} catch (MqttException e) { } catch (MqttException e) {
throw new ServiceException("Unable to subscribe", e); throw new RuntimeException(e);
} }
} }
public void registerCallback(){
this.subscriber.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
onConnectionLost.accept(cause);
}
@Override
public void messageArrived(String topic, MqttMessage message) {
MqttMessageWrapper msg = new MqttMessageWrapper(topic, message);
onMessageReceived.accept(msg);
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// Not needed for subscriber
}
});
}
private void disconnect(MqttClient client) throws ServiceException {
if(client.isConnected()){
try {
client.disconnect();
} catch (MqttException e) {
throw new ServiceException("Failed to disconnect the broker", e);
}
}
}
private void connect(MqttClient client, MqttConnectOptions options) throws MqttException {
if (client.isConnected()) return;
if (options == null) {
client.connect();
} else {
client.connect(options);
}
}
} }

View File

@@ -19,13 +19,13 @@ server.ssl.key-store-type=PKCS12
server.ssl.key-alias=board-mate-api server.ssl.key-alias=board-mate-api
#=============MQTT============= #=============MQTT=============
mqtt.broker-url=tcp://test.mosquitto.org:1883 mqtt.plain.broker-url=${BROKER_URL}
mqtt.client-id=board-mate-client mqtt.ssl.broker-url=${BROKER_SECURE_URL}
mqtt.client-id=board-mate-api
mqtt.topic=board-mate-test/topic mqtt.username=${BROKER_USERNAME}
mqtt.password=${BROKER_PASSWORD}
mqtt.username=yourUsername
mqtt.password=yourPassword
#=============METRICS============= #=============METRICS=============
management.endpoint.health.show-details=always management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*