TP06 : Station météo CoAP
Dans ce dernier TP, nous modifions la manière d’envoyer les mesures des capteurs vers ThingsBoard. Nous remplaçons l’interface MQTT par un API REST sur le protocole CoAP.
Afin de réaliser ce TP, vous avez le choix entre :
- Modifier le TP précédent et utiliser deux cibles : une cible de mesure et une autre cible de passerelle BLE → CoAP.
- Regrouper le tout sur la même cible et utiliser une instance de CoAPPublisher sur la cible disposant des capteurs (en supprimant le lien BLE).
Objectifs du TP
Ce TP a pour but de mettre en pratique une communication CoAP/RESTful par Wi-Fi.
À la fin de ce TP, les étudiants :
- auront implémenté un client CoAP sur Mbed OS;
- auront publié des données sur un serveur Internet;
- auront réalisé des analyses statiques de code et corrigé les erreurs rapportées ;
- auront rédigé un journal de travail et déposé le PDF sur Teams.
Les livrables sont :
- un release avec le tag “tp06” doit être créé sur votre dépôt.
- un journal de travail déposé sur Teams.
Temps accordé : 8 périodes de 45 minutes en classe + travail personnel à la maison
Délai
Les dates de rendu des TPs sont indiquées sur le plan de cours.
Configuration du projet
Configurez votre projet comme pour le TP “Station météo MQTT” (fichiers “platformio.ini”, “mbed_app.json” et le dossier “mbedignore”). Dans le fichier “platformio.ini”, ajoutez la librairie pour CoAP:
lib_deps =
...
https://github.com/heia-fr/embsys-lobaro-coap#main
...
L’interface CoAP/RESTful de ThingsBoard
L’interface CoAP/RESTful de ThingsBoard est documenté ici. Étudiez comment mettre à jour une donnée et expérimentez avec un client CoAP installé sur votre machine comme documenté (ou avec une autre méthode de votre choix). Avant de programmer votre cible, vous devez avoir validé la publication de données sur votre dashboard Thingsboard en utilisant le protocole CoAP.
Un exemple de commande afin de publier une donnée sur Thingsboard à l’aide de
l’outil coap-cli
utilisé pour les exercices est
coap post --coap-option 12,0x32 --payload "{temperature=25,humidity=45}" coap://demo.thingsboard.io/api/
v1/$ACCESS_TOKEN/telemetry
2.01
et les données doivent s’afficher dans le
dashboard correspondant.
Réalisation
Pour réaliser la publication en utilisant l’API CoAP/RESTful de
ThingsBoard, vous recevez le code de la class CoAPPublisher
, que vous devez
compléter aux endroits notés TODO. Vous devez ensuite adapter votre
application afin d’utiliser un CoAPPublisher
au lieu d’un MQTTPublisher
.
Vous devez adapter votre code de façon à pouvoir alterner entre une publication
MQTT et CoAP sur un simple changement de symbole de précompilateur. De plus,
la seule ligne qui doit changer entre les deux modes est la ligne qui crée
l’instance de IPublisher
appropriée - votre code doit donc faire usage de
l’interface IPublisher
.
Dans le code à compléter, vous devez déterminer le type de message CoAP que vous envoyez sur la plateforme ThingsBoard. Vous devez tester les deux types de message NON et CON et documenter les échanges de messages pour un envoi de message NON et un envoi de message CON. Vous devez inclure dans votre rapport les messages affichés dans la console ainsi qu’un diagramme qui explique tous les messages échangés entre le client et le serveur.
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file coap_publisher.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Class for publishing measurement using CoAP
*
* @date 2022-01-01
* @version 0.1.0
***************************************************************************/
#include "coap_publisher.hpp"
#include "mbed_trace.h"
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
#define TRACE_GROUP "CoAPPublisher"
#endif // MBED_CONF_MBED_TRACE_ENABLE
// declare static variables
Timer CoAPPublisher::timer_;
CoAPPublisher::CoAPPublisher()
: thread_(osPriorityNormal, OS_STACK_SIZE, nullptr, "CoapWork")
{
CoAP_API_t api = {
.rtc1HzCnt =
&CoAPPublisher::elapsedTime_seconds, // Function that returns a time
// in seconds
.debugPuts =
&CoAPPublisher::debugPuts, // Function to print info for debugging
.malloc = malloc, // Function for allocating memory
.free = free, // Function for freeing memory
.rand = &CoAPPublisher::generateRandom, // Function to generate random
// numbers
};
CoAP_Init(api);
}
nsapi_error_t CoAPPublisher::connect(const char* serverHostname,
const char* userName,
const char* keyOrPwd)
{
// call the base class for connecting to the network
nsapi_error_t retCode = PublisherBase::connectNetworkInterface();
if (retCode != NSAPI_ERROR_OK) {
tr_error("Could not connect to network: %d", retCode);
return retCode;
}
SocketAddress socketAddress;
tr_debug("Resolving hostname %s", serverHostname);
retCode = networkInterface_->gethostbyname(serverHostname, &socketAddress);
if (retCode != NSAPI_ERROR_OK) {
tr_error("gethostbyname couldn't resolve remote host: %s, code: %d",
serverHostname,
retCode);
return retCode;
}
// copy IP address to serverAddr_
uint8_t nbrOfU8 =
sizeof(serverAddr_.IPv4.u8) / sizeof(serverAddr_.IPv4.u8[0]);
for (uint8_t i = 0; i < nbrOfU8; i++) {
serverAddr_.IPv4.u8[i] = socketAddress.get_addr().bytes[i];
}
static constexpr uint16_t port = 5683;
serverEp_ = {IPV4, serverAddr_, port};
// store user name and key
userName_ = userName;
key_ = keyOrPwd;
// create a CoAP posix socket that we will use for sending datagrams
retCode = coapPosixCreateSocket(&m_socketHandle, IPV4);
if (retCode != NSAPI_ERROR_OK) {
tr_error("could not create CoAP Posix socket: %d", retCode);
return retCode;
}
// socket creation is happening through the worker thread
osStatus status =
thread_.start(callback(this, &CoAPPublisher::coapClientWorkTask));
if (status != osOK) {
tr_error("Could not start CoAP worker thread: %ld", status);
return NSAPI_ERROR_BUSY;
}
return NSAPI_ERROR_OK;
}
nsapi_error_t CoAPPublisher::publishMeasurement(float temperature,
float humidity,
float pressure)
{
tr_info("publishMeasurement() : Connection status: %d",
networkInterface_->get_connection_status());
// if we are not connected return immediately
if (networkInterface_->get_connection_status() != NSAPI_STATUS_GLOBAL_UP) {
tr_debug("Returning immediately");
return NSAPI_ERROR_NO_CONNECTION;
}
// TODO publish the message to ThingsBoard
// build the uri (named resourceUri) starting with "api/..."
// build the payload (named payload)
// TODO adapt the message type for testing both NON and CON messages
tr_debug("Publishing payload %s to uri %s", payload, resourceUri);
CoAP_Result_t res =
sendCoapMessage(m_socketHandle, CON, resourceUri, (uint8_t*)payload, strlen(payload));
if (res != COAP_OK) {
tr_error("Could not publish measurement: %d", res);
// need to adapt the return code
return NSAPI_ERROR_UNSUPPORTED;
}
return NSAPI_ERROR_OK;
}
bool CoAPPublisher::isConnected()
{
if (networkInterface_ == nullptr) {
return false;
}
if (networkInterface_->get_connection_status() != NSAPI_STATUS_GLOBAL_UP) {
return false;
}
return m_socketHandle != nullptr;
}
uint32_t CoAPPublisher::elapsedTime_seconds()
{
return timer_.elapsed_time().count() / 1000000;
}
void CoAPPublisher::debugPuts(const char* s) { tr_debug("%s", s); }
int CoAPPublisher::generateRandom() { return rand(); }
void CoAPPublisher::coapClientWorkTask()
{
tr_debug("#### coapClientWorkTask");
// start the timer for delivering the elapsed time to the CoAP library
timer_.start();
// Initializes the random number generator
// we should be using a true random number generator
time_t t;
srand((unsigned)time(&t));
// Create the buffer we need to read packets from the network
static constexpr uint16_t RX_BUFFER_SIZE = MAX_PAYLOAD_SIZE + 127;
static uint8_t rxBuffer[RX_BUFFER_SIZE] = {0};
// infinite loop for processing packets
while (true) {
// First read all the pending packets from the network
// interface and transfer them to the coap library
int res = 0;
do {
// Read from network interface (using Posix socket api)
UDPSocket* pUDPSocket = (UDPSocket*)m_socketHandle;
res = pUDPSocket->recv(rxBuffer, RX_BUFFER_SIZE);
// tr_debug("Received %d bytes on UDP socket (socketHandle 0x%0x)", res,
// (unsigned int) m_socketHandle);
if (res > 0) {
tr_debug("New CoAP packet received on interface, bytes read = %d", res);
// Format the packet to the proper structure
NetPacket_t pckt;
memset(&pckt, 0, sizeof(pckt));
pckt.pData = rxBuffer;
pckt.size = res;
pckt.remoteEp = serverEp_;
// Feed the received packet to the CoAP library
// Note: this will copy the data to a new
// buffer (we can reuse the rxBuffer)
CoAP_HandleIncomingPacket(m_socketHandle, &pckt);
}
} while (res > 0);
// Then process any pending work
CoAP_doWork();
// Then sleep for 100 ms
ThisThread::sleep_for(100ms);
}
}
// Function to send a packet to the network interface
bool CoAPPublisher::coapPosixSendDatagram(SocketHandle_t socketHandle,
NetPacket_t* pckt)
{
tr_debug("CoAP_Posix_SendDatagram (socketHandle 0x%0x)",
(unsigned int)socketHandle);
UDPSocket* pUDPSocket = (UDPSocket*)socketHandle;
// Format the endpoint info from the pckt to the right structure
// that we need in our specific network (Posix socket api)
SocketAddress socketAddress;
if (pckt->remoteEp.NetType == IPV4) {
socketAddress.set_port(pckt->remoteEp.NetPort);
nsapi_addr_t nsapiAddr;
nsapiAddr.version = NSAPI_IPv4;
uint8_t nbrOfU8 = sizeof(pckt->remoteEp.NetAddr.IPv4.u8) /
sizeof(pckt->remoteEp.NetAddr.IPv4.u8[0]);
for (uint8_t i = 0; i < nbrOfU8; i++) {
nsapiAddr.bytes[i] = pckt->remoteEp.NetAddr.IPv4.u8[i];
}
socketAddress.set_addr(nsapiAddr);
} else {
tr_error("Unsupported NetType : %d\n", pckt->remoteEp.NetType);
return false;
}
// Actually send the packet to the network (Posix socket api)
nsapi_size_or_error_t retCode =
pUDPSocket->sendto(socketAddress, pckt->pData, pckt->size);
if (retCode < 0) {
tr_error("sendto() returned %d\n", retCode);
return false;
}
return retCode > 0;
}
// Function to create a "CoAP" socket that can be used with the CoAP library
// Returns true and sets the `handle` on success
// Returns false if the socket could not be created
nsapi_error_t CoAPPublisher::coapPosixCreateSocket(SocketHandle_t* handle,
NetInterfaceType_t type)
{
tr_debug("CoAP_Posix_CreateSocket");
if (type == IPV4) {
// Create the actual Posix socket
UDPSocket* pUDPSocket = new UDPSocket();
if (pUDPSocket == nullptr) {
tr_error("Could not create socket, nullptr");
return NSAPI_ERROR_NO_SOCKET;
}
// make the socket non blocking
pUDPSocket->set_blocking(false);
nsapi_error_t retCode = pUDPSocket->open(networkInterface_);
if (retCode != NSAPI_ERROR_OK) {
ERROR("open() failed, code: %d", retCode);
return retCode;
}
// Allocate a new CoAP_Socket_t space for this socket
CoAP_Socket_t* newSocket = AllocSocket();
if (newSocket == nullptr) {
tr_error("Could not allocate memory for new socket");
pUDPSocket->close();
delete pUDPSocket;
pUDPSocket = nullptr;
return NSAPI_ERROR_NO_SOCKET;
}
newSocket->Handle = pUDPSocket;
newSocket->Tx =
&CoAPPublisher::coapPosixSendDatagram; // Function to transmit packets
newSocket->Alive = true; // UDP sockets don't need to be connected
*handle = pUDPSocket;
} else {
tr_error("Unsupported net type %d", type);
return NSAPI_ERROR_UNSUPPORTED;
}
return NSAPI_ERROR_OK;
}
// Response handler function
CoAP_Result_t CoAPPublisher::coapRespHandler(CoAP_Message_t* pRespMsg,
CoAP_Message_t* pReqMsg,
NetEp_t* sender)
{
tr_debug("CoAP_RespHandler_fn");
if (pRespMsg == NULL) {
tr_error(
"CoAP message transmission failed after all retries (timeout) for "
"MessageId %d",
pReqMsg->MessageID);
return COAP_OK;
}
tr_debug("Got a reply for MiD: %d", pRespMsg->MessageID);
CoAP_PrintMsg(pRespMsg);
return COAP_OK;
}
CoAP_Result_t CoAPPublisher::sendCoapMessage(SocketHandle_t socketHandle,
CoAP_MessageType_t msgType,
const char* coap_uri_path,
uint8_t* data,
size_t length)
{
tr_debug("sendCoapMessage");
// Send a CoAP message
CoAP_Result_t result = CoAP_StartNewRequest(
REQ_POST, // (CoAP_MessageCode_t)
msgType, // CoAP_MessageType_t
coap_uri_path, // (char*)
socketHandle, // (SocketHandle_t)
&serverEp_, // (NetEp_t*)
&CoAPPublisher::coapRespHandler, // The function that will be called
// when the message gets a response
// or fails to be sent
data, // Message data buffer (uint8_t *)
length // Message data length (size_t)
);
return result;
}
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file mqtt_publisher.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Class for publishing measurements using CoAP
*
* @date 2022-01-01
* @version 0.1.0
***************************************************************************/
#ifndef COAP_PUBLISHER_HPP_
#define COAP_PUBLISHER_HPP_
#include <MQTTClientMbedOs.h>
#include <UDPSocket.h>
#include "publisher_base.hpp"
#include "src/coap.h"
class CoAPPublisher : public PublisherBase {
public:
CoAPPublisher();
// IPublisher
virtual nsapi_error_t connect(const char* serverHostname,
const char* userName,
const char* keyOrPwd) override;
virtual nsapi_error_t publishMeasurement(float temperature,
float humidity,
float pressure) override;
virtual bool isConnected() override;
private:
// private methods
static uint32_t elapsedTime_seconds();
static void debugPuts(const char* s);
static int generateRandom();
void coapClientWorkTask();
static bool coapPosixSendDatagram(SocketHandle_t socketHandle,
NetPacket_t* pckt);
nsapi_error_t coapPosixCreateSocket(SocketHandle_t* handle,
NetInterfaceType_t type);
static CoAP_Result_t coapRespHandler(CoAP_Message_t* pRespMsg,
CoAP_Message_t* pReqMsg,
NetEp_t* sender);
CoAP_Result_t sendCoapMessage(SocketHandle_t socketHandle,
CoAP_MessageType_t msgType,
const char* coap_uri_path,
uint8_t* data,
size_t length);
Thread thread_;
UDPSocket udpSocket_;
SocketHandle_t m_socketHandle = nullptr;
std::string userName_;
std::string key_;
static Timer timer_;
NetAddr_t serverAddr_ = {};
NetEp_t serverEp_ = {};
};
#endif // COAP_PUBLISHER_HPP_
Tests et validations
Vous devez publier des données environnementales vers le serveur Thingsboard pendant au moins 6 heures et donnez un accès public à votre dashboard dans le rapport. Le lien vers le dashboard public doit fonctionner sans s’identifier sur la plateforme Thingsboard.
Comme pour le TP précédent, configurez l’analyse statique de code de manière plus stricte.
À ne pas oublier
- Choisissez de bons noms pour les classes, les méthodes et les variables.
- Implémentez les bibliothèques avec un haut niveau d’abstraction pour pouvoir réutiliser les méthodes dans d’autres projets.
- Faites des “git commit” régulièrement avec de bons commentaires.
- Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.
- Validez tous les codes de retour des méthodes/fonctions utilisées.
Journal de travail
- Rédigez un rapport (journal de travail) avec les indications suivantes :
- Une page de titre avec au minimum :
- le nom et le logo officiel de l’école
- le nom du cours : Module Acquisition et traitement de données : Internet des Objets
- le titre de votre document : Travail Pratique 6 : Station météo CoAP
- le numéro de votre groupe
- les noms des auteurs (vous) avec la classe dans laquelle vous êtes
- la date à laquelle vous avez terminé le rapport
- éventuellement la version du rapport
- Une introduction pour poser le contexte
- Un résumé des notions que vous avez apprises pendant ce TP en précisant si c’est
- non acquis
- acquis, mais encore à exercer
- parfaitement acquis
- Un résumé des points qui vous semblent importants et que vous devez retenir.
- Une copie des messages affichés par votre application lors de l’envoi des messages NON et CON, avec un diagramme expliquant les messages échangés.
- Le code source bien formaté et avec du “syntax highlighting” de votre code source.
- Une conclusion par laquelle vous donnez vos impressions sur le TP, ce que vous avez aimé, ce que vous avez moins aimé, et éventuellement des suggestions pour des changements. Indiquez également le nombre d’heures que vous avez passées, par personne, en dehors des heures de TP en classe.
Important
Déposez votre rapport dans le devoir Teams correspondant avec le nom report06.pdf
.