TP05 : RESTful API sur CoAP

Dans ce dernier TP, nous modifions la manière d’envoyer les mesures ambiantes 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 WiFi.
À la fin de ce TP, les étudiants :
- auront réutilisé le code de base du TP 04;
- auront implémenté un client CoAP/RESTful 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 dans le dépôt git.
Les livrables sont :
- un projet git (tp05) dans votre groupe sur gitlab.forge.hefr.ch avec le code du TP ;
- un journal de travail déposé sur gitlab.
Temps accordé : 4 périodes de 45 minutes en classe + travail personnel à la maison
Délai
Le TP doit être rendu au plus tard 6 jours après la séance en classe à 23:59.
Configuration du projet
Configurez votre projet comme pour le TP précédent (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.
Pour publier avec CoAP sur ThingsBoard, vous devez utiliser l’authentification par token. Veillez donc à adapter votre device sur ThingsBoard en conséquence.
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(double temperature,
double humidity,
double 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(double temperature,
double humidity,
double 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
Comme pour le TP précédent, configurez l’analyse statique de code de manière plus stricte.
À ne pas oublier
Gardez toujours en têtes les bonnes pratiques ainsi que les dix commandements du bon programmeur.
- 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.
- Configurez le CI/CD de gitlab et testez automatiquement le plus de choses possibles.
- Implémentez des tests unitaires.
- Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.
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 : Informatique Embarquée
- le titre de votre document : Travail Pratique 3 : Station météo connectée
- 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
- Les réponses aux questions.
- 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 un dossier /docs de votre dépôt git (tp05) avec le nom report05.pdf (le chemin complet vers votre
rapport est donc /docs/report05.pdf)