first commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"idf.currentSetup": "/home/tomasg/esp/v5.3.4/esp-idf"
|
||||||
|
}
|
||||||
3
CMakeLists.txt
Normal file
3
CMakeLists.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16.0)
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(thread_node_c6)
|
||||||
196
README.md
Normal file
196
README.md
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# Cviceni: Thread a CoAP na ESP32-C6
|
||||||
|
|
||||||
|
Tento projekt je pripraven jako kostra pro pocitacove cviceni. Cilem je:
|
||||||
|
|
||||||
|
1. nastavit uzel do site `Thread` pomoci aktivniho datasetu,
|
||||||
|
2. vytvorit CoAP `POST` pozadavek,
|
||||||
|
3. odeslat simulovana senzorova data ve formatu JSON.
|
||||||
|
|
||||||
|
V projektu jsou nektere casti jiz hotove a nektere jsou pripraveny jako `TODO` pro doplneni.
|
||||||
|
|
||||||
|
## Co je Thread
|
||||||
|
|
||||||
|
Thread je sitovy protokol pro zarizeni nad IEEE 802.15.4. Umoznuje IPv6 komunikaci mezi uzly s malou rezii a nizkou spotrebou. V siti se mohou objevit napriklad role:
|
||||||
|
|
||||||
|
- `leader`
|
||||||
|
- `router`
|
||||||
|
- `child`
|
||||||
|
|
||||||
|
Aby se uzel pripojil do konkretni site, musi znat parametry aktivniho datasetu:
|
||||||
|
|
||||||
|
- `Network Name`
|
||||||
|
- `Channel`
|
||||||
|
- `PAN ID`
|
||||||
|
- `Network Key`
|
||||||
|
- `Extended PAN ID`
|
||||||
|
- `Mesh-Local Prefix`
|
||||||
|
|
||||||
|
Tyto hodnoty se v projektu vyplnuji do [src/thread_dataset.h](src/thread_dataset.h).
|
||||||
|
|
||||||
|
## Co je CoAP
|
||||||
|
|
||||||
|
CoAP je lehky aplikacni protokol pro vestavene systemy. Bezi nad UDP a pripomina zjednodusene HTTP. V tomto cviceni budeme pouzivat:
|
||||||
|
|
||||||
|
- metodu `POST`
|
||||||
|
- URI cestu `/sensor`
|
||||||
|
- payload ve formatu JSON
|
||||||
|
|
||||||
|
Server, na ktery bude klient odesilat data, ma adresu:
|
||||||
|
|
||||||
|
```text
|
||||||
|
coap://[fd11:2233:4455:6677::1]:5683/sensor
|
||||||
|
```
|
||||||
|
|
||||||
|
Parsovani teto URL je v projektu jiz hotove. Studenti implementuji jen vytvoreni JSON payloadu a sestaveni CoAP zpravy.
|
||||||
|
|
||||||
|
## Postup cviceni
|
||||||
|
|
||||||
|
### 1. Doplneni Thread datasetu
|
||||||
|
|
||||||
|
Otevri [src/thread_dataset.h](src/thread_dataset.h). V souboru jsou placeholdery, ktere je potreba nahradit hodnotami ze zadani.
|
||||||
|
|
||||||
|
Vypln:
|
||||||
|
|
||||||
|
- `THREAD_NETWORK_NAME = "OpenThread-ESP"`
|
||||||
|
- `THREAD_CHANNEL = 15`
|
||||||
|
- `THREAD_PANID = 0x1234`
|
||||||
|
- `THREAD_MESH_LOCAL_PREFIX_STRING = "fd11:2233:4455:6677"`
|
||||||
|
- `THREAD_GATEWAY_STATIC_ADDR = "fd11:2233:4455:6677::1"`
|
||||||
|
- `THREAD_NODE_STATIC_ADDR = "fd11:2233:4455:6677::10"`
|
||||||
|
- `THREAD_GATEWAY_COAP_URL = "coap://[fd11:2233:4455:6677::1]:5683/sensor"`
|
||||||
|
|
||||||
|
`Network Key`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{
|
||||||
|
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||||
|
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Extended PAN ID`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{
|
||||||
|
0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0xca, 0xfe,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Mesh-Local Prefix` jako pole bajtu:
|
||||||
|
|
||||||
|
```c
|
||||||
|
{
|
||||||
|
0xfd, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Po doplneni by mel byt uzel schopny se pripojit do site Thread.
|
||||||
|
|
||||||
|
### 2. Sestaveni JSON payloadu
|
||||||
|
|
||||||
|
Otevri [src/coap_client.c](src/coap_client.c) a dopln funkci `coap_client_build_json_payload()`.
|
||||||
|
|
||||||
|
Tato funkce ma:
|
||||||
|
|
||||||
|
- vzit hodnoty ze struktury `sensor_data_t`
|
||||||
|
- vytvorit z nich textovy JSON
|
||||||
|
- ulozit vysledek do bufferu `payload`
|
||||||
|
- vratit `true`, pokud se payload podarilo vytvorit
|
||||||
|
|
||||||
|
Ocekavany format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"node_id":"node_1","temp":23.4,"humidity":48,"button":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
K tomu je vhodne pouzit funkci `snprintf()`.
|
||||||
|
|
||||||
|
`snprintf()`:
|
||||||
|
|
||||||
|
- zapisuje formatovany text do bufferu
|
||||||
|
- hlida maximalni velikost bufferu
|
||||||
|
- vraci pocet znaku, ktere by byly zapsany
|
||||||
|
|
||||||
|
Co je potreba zkontrolovat:
|
||||||
|
|
||||||
|
- navratova hodnota neni zaporna
|
||||||
|
- vysledny text se vesel do `payload_size`
|
||||||
|
|
||||||
|
### 3. Vytvoreni CoAP POST zpravy
|
||||||
|
|
||||||
|
Ve stejnem souboru dopln funkci `coap_client_create_post_request()`.
|
||||||
|
|
||||||
|
Tato funkce ma z vytvoreneho JSON payloadu pripravit CoAP zpravu pro OpenThread.
|
||||||
|
|
||||||
|
Postup:
|
||||||
|
|
||||||
|
1. vytvorit zpravu
|
||||||
|
2. nastavit typ zpravy
|
||||||
|
3. nastavit kod `POST`
|
||||||
|
4. pridat URI path
|
||||||
|
5. pridat informaci, ze payload je `JSON`
|
||||||
|
6. pridat samotny payload
|
||||||
|
|
||||||
|
Pouzij tyto OpenThread funkce:
|
||||||
|
|
||||||
|
- `otCoapNewMessage()`
|
||||||
|
Vytvori novou CoAP zpravu.
|
||||||
|
- `otCoapMessageInit()`
|
||||||
|
Nastavi typ a kod zpravy. Zde pouzij `OT_COAP_TYPE_CONFIRMABLE` a `OT_COAP_CODE_POST`.
|
||||||
|
- `otCoapMessageGenerateToken()`
|
||||||
|
Vygeneruje token, podle ktereho lze sparovat odpoved.
|
||||||
|
- `otCoapMessageAppendUriPathOptions()`
|
||||||
|
Prida URI cestu, napriklad `sensor`.
|
||||||
|
- `otCoapMessageAppendContentFormatOption()`
|
||||||
|
Nastavi format obsahu. Zde pouzij `OT_COAP_OPTION_CONTENT_FORMAT_JSON`.
|
||||||
|
- `otCoapMessageSetPayloadMarker()`
|
||||||
|
Oddeli CoAP options od samotneho payloadu.
|
||||||
|
- `otMessageAppend()`
|
||||||
|
Prida do zpravy data payloadu.
|
||||||
|
|
||||||
|
Pokud nektery krok selze:
|
||||||
|
|
||||||
|
- uvolni zpravu pomoci `otMessageFree(message)`
|
||||||
|
- vrat `NULL`
|
||||||
|
|
||||||
|
### 4. Prepinani intervalu pomoci D1
|
||||||
|
|
||||||
|
Na kitu je pripojen pin `D1`, ktery bude slouzit pro prepinani periody odesilani.
|
||||||
|
|
||||||
|
V [src/main.c](src/main.c) je pripraveno:
|
||||||
|
|
||||||
|
- inicializovani vstupu pro tlacitko na `D1`
|
||||||
|
- funkce `application_get_send_interval_ms()`, kterou je potreba doplnit
|
||||||
|
|
||||||
|
Student ma dopsat logiku:
|
||||||
|
|
||||||
|
- pokud je tlacitko stisknute, pouzij rychly interval `1000 ms`
|
||||||
|
- pokud tlacitko stisknute neni, pouzij pomaly interval `5000 ms`
|
||||||
|
|
||||||
|
Napoveda:
|
||||||
|
|
||||||
|
- pro cteni vstupu lze pouzit `gpio_get_level()`
|
||||||
|
- pri zapnutem `pull-up` byva stisk tlacitka casto reprezentovan hodnotou `0`
|
||||||
|
|
||||||
|
### 5. Otestovani
|
||||||
|
|
||||||
|
Po doplneni sleduj log na seriove lince. Zajimaji te hlavne:
|
||||||
|
|
||||||
|
- zmena role uzlu v Thread siti
|
||||||
|
- informace o inicializaci CoAP klienta
|
||||||
|
- vypis odesilaneho payloadu
|
||||||
|
- zmena intervalu po stisku tlacitka `D1`
|
||||||
|
- pripadna odpoved od CoAP serveru
|
||||||
|
|
||||||
|
## Co uz je hotove
|
||||||
|
|
||||||
|
V projektu je jiz pripraveno:
|
||||||
|
|
||||||
|
- spusteni OpenThread stacku v [src/main.c](src/main.c)
|
||||||
|
- nacitani datasetu a start site v [src/main.c](src/main.c)
|
||||||
|
- parsovani CoAP URL v [src/coap_client.c](src/coap_client.c)
|
||||||
|
- volani `coap_send_data()` a odesilani periodickych dat
|
||||||
|
|
||||||
|
## Poznamka
|
||||||
|
|
||||||
|
V [src/thread_dataset.h](src/thread_dataset.h) jsou zamerne placeholdery. Projekt se zkompiluje, ale bez spravnych hodnot se uzel nepripoji do pozadovane Thread site.
|
||||||
37
include/README
Normal file
37
include/README
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the convention is to give header files names that end with `.h'.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||||
46
lib/README
Normal file
46
lib/README
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into the executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a separate directory
|
||||||
|
("lib/your_library_name/[Code]").
|
||||||
|
|
||||||
|
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
Example contents of `src/main.c` using Foo and Bar:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries by scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||||
21
platformio.ini
Normal file
21
platformio.ini
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32-c6-devkitc-1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-c6-devkitc-1
|
||||||
|
framework = espidf
|
||||||
|
monitor_speed = 115200
|
||||||
|
board_build.flash_mode = dio
|
||||||
|
board_build.flash_size = 4MB
|
||||||
|
board_upload.flash_size = 4MB
|
||||||
|
build_flags =
|
||||||
|
-UOPENTHREAD_BUILD_DATETIME
|
||||||
|
-DOPENTHREAD_BUILD_DATETIME=\"unknown\"
|
||||||
9
sdkconfig.defaults
Normal file
9
sdkconfig.defaults
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CONFIG_OPENTHREAD_ENABLED=y
|
||||||
|
CONFIG_OPENTHREAD_FTD=y
|
||||||
|
CONFIG_OPENTHREAD_RADIO_NATIVE=y
|
||||||
|
CONFIG_OPENTHREAD_COAP=y
|
||||||
|
CONFIG_IEEE802154_ENABLED=y
|
||||||
|
CONFIG_LWIP_IPV6=y
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
|
||||||
|
# CONFIG_OPENTHREAD_BORDER_ROUTER is not set
|
||||||
2538
sdkconfig.esp32-c6-devkitc-1
Normal file
2538
sdkconfig.esp32-c6-devkitc-1
Normal file
File diff suppressed because it is too large
Load Diff
2236
sdkconfig.esp32-c6-devkitc-1.old
Normal file
2236
sdkconfig.esp32-c6-devkitc-1.old
Normal file
File diff suppressed because it is too large
Load Diff
10
src/CMakeLists.txt
Normal file
10
src/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# This file was automatically generated for projects
|
||||||
|
# without default 'CMakeLists.txt' file.
|
||||||
|
|
||||||
|
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${app_sources}
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES mqtt esp_wifi esp_event esp_netif nvs_flash openthread vfs
|
||||||
|
)
|
||||||
169
src/coap_client.c
Normal file
169
src/coap_client.c
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
#include "coap_client.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "openthread/coap.h"
|
||||||
|
#include "openthread/ip6.h"
|
||||||
|
#include "openthread/message.h"
|
||||||
|
#include "openthread/thread.h"
|
||||||
|
|
||||||
|
static const char *TAG = "CoAPClient";
|
||||||
|
|
||||||
|
bool coap_client_parse_url(const char *url, otIp6Address *peer_addr, uint16_t *peer_port, const char **uri_path)
|
||||||
|
{
|
||||||
|
const char *scheme = "coap://[";
|
||||||
|
const char *host_start;
|
||||||
|
const char *host_end;
|
||||||
|
const char *port_start;
|
||||||
|
const char *path_start;
|
||||||
|
char host[OT_IP6_ADDRESS_STRING_SIZE];
|
||||||
|
unsigned int parsed_port = OT_DEFAULT_COAP_PORT;
|
||||||
|
int host_len;
|
||||||
|
|
||||||
|
if (strncmp(url, scheme, strlen(scheme)) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
host_start = url + strlen(scheme);
|
||||||
|
host_end = strchr(host_start, ']');
|
||||||
|
if (host_end == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
host_len = (int)(host_end - host_start);
|
||||||
|
if (host_len <= 0 || host_len >= (int)sizeof(host)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(host, host_start, (size_t)host_len);
|
||||||
|
host[host_len] = '\0';
|
||||||
|
|
||||||
|
if (otIp6AddressFromString(host, peer_addr) != OT_ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host_end[1] == ':') {
|
||||||
|
port_start = host_end + 2;
|
||||||
|
path_start = strchr(port_start, '/');
|
||||||
|
if (path_start == NULL || sscanf(port_start, "%u", &parsed_port) != 1 || parsed_port > 65535) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (host_end[1] == '/') {
|
||||||
|
path_start = host_end + 1;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path_start[0] != '/' || path_start[1] == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*peer_port = (uint16_t)parsed_port;
|
||||||
|
*uri_path = path_start + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void coap_response_handler(void *context, otMessage *message, const otMessageInfo *message_info, otError error)
|
||||||
|
{
|
||||||
|
char peer_addr[OT_IP6_ADDRESS_STRING_SIZE];
|
||||||
|
|
||||||
|
(void)context;
|
||||||
|
|
||||||
|
if (error != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGW(TAG, "CoAP response error: %s", otThreadErrorToString(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
otIp6AddressToString(&message_info->mPeerAddr, peer_addr, sizeof(peer_addr));
|
||||||
|
ESP_LOGI(TAG, "CoAP response from %s, code=%u", peer_addr, otCoapMessageGetCode(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
void coap_client_init(otInstance *instance) {
|
||||||
|
if (otCoapStart(instance, OT_DEFAULT_COAP_PORT) != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to start CoAP client");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "CoAP client initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool coap_client_build_json_payload(const sensor_data_t *data, char *payload, size_t payload_size)
|
||||||
|
{
|
||||||
|
(void)data;
|
||||||
|
(void)payload;
|
||||||
|
(void)payload_size;
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "TODO: doplnit sestaveni JSON payloadu");
|
||||||
|
//TO DO
|
||||||
|
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
otMessage *coap_client_create_post_request(otInstance *instance, const char *uri_path, const char *payload)
|
||||||
|
{
|
||||||
|
(void)uri_path;
|
||||||
|
(void)payload;
|
||||||
|
|
||||||
|
otMessage *message = otCoapNewMessage(instance, NULL);
|
||||||
|
if (message == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate CoAP message");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "TODO: doplnit vytvoreni CoAP POST zpravy");
|
||||||
|
//TO DO
|
||||||
|
|
||||||
|
|
||||||
|
otMessageFree(message);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void coap_send_data(otInstance *instance, const char *url, sensor_data_t *data) {
|
||||||
|
otMessage *message;
|
||||||
|
otMessageInfo message_info;
|
||||||
|
otIp6Address peer_addr_bin;
|
||||||
|
char peer_addr[OT_IP6_ADDRESS_STRING_SIZE];
|
||||||
|
char payload[128];
|
||||||
|
uint16_t peer_port;
|
||||||
|
const char *uri_path;
|
||||||
|
|
||||||
|
if (otThreadGetDeviceRole(instance) == OT_DEVICE_ROLE_DISABLED ||
|
||||||
|
otThreadGetDeviceRole(instance) == OT_DEVICE_ROLE_DETACHED) {
|
||||||
|
ESP_LOGW(TAG, "Thread node is not attached yet, skipping CoAP send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!coap_client_build_json_payload(data, payload, sizeof(payload))) {
|
||||||
|
ESP_LOGW(TAG, "Payload nebyl vytvoren, CoAP odeslani se preskakuje");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!coap_client_parse_url(url, &peer_addr_bin, &peer_port, &uri_path)) {
|
||||||
|
ESP_LOGW(TAG, "Invalid CoAP URL: %s", url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = coap_client_create_post_request(instance, uri_path, payload);
|
||||||
|
if (message == NULL) {
|
||||||
|
ESP_LOGW(TAG, "CoAP zprava nebyla vytvorena");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&message_info, 0, sizeof(message_info));
|
||||||
|
message_info.mPeerAddr = peer_addr_bin;
|
||||||
|
message_info.mPeerPort = peer_port;
|
||||||
|
message_info.mSockAddr = *otThreadGetMeshLocalEid(instance);
|
||||||
|
message_info.mSockPort = OT_DEFAULT_COAP_PORT;
|
||||||
|
|
||||||
|
otIp6AddressToString(&peer_addr_bin, peer_addr, sizeof(peer_addr));
|
||||||
|
ESP_LOGI(TAG, "CoAP POST to [%s]:%u/%s payload=%s", peer_addr, peer_port, uri_path, payload);
|
||||||
|
|
||||||
|
if (otCoapSendRequest(instance, message, &message_info, coap_response_handler, NULL) != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send CoAP request");
|
||||||
|
otMessageFree(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/coap_client.h
Normal file
19
src/coap_client.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#ifndef COAP_CLIENT_H
|
||||||
|
#define COAP_CLIENT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "sensor_sim.h"
|
||||||
|
#include "openthread/instance.h"
|
||||||
|
#include "openthread/ip6.h"
|
||||||
|
#include "openthread/message.h"
|
||||||
|
|
||||||
|
void coap_client_init(otInstance *instance);
|
||||||
|
bool coap_client_build_json_payload(const sensor_data_t *data, char *payload, size_t payload_size);
|
||||||
|
bool coap_client_parse_url(const char *url, otIp6Address *peer_addr, uint16_t *peer_port, const char **uri_path);
|
||||||
|
otMessage *coap_client_create_post_request(otInstance *instance, const char *uri_path, const char *payload);
|
||||||
|
void coap_send_data(otInstance *instance, const char *url, sensor_data_t *data);
|
||||||
|
|
||||||
|
#endif
|
||||||
184
src/main.c
Normal file
184
src/main.c
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "sensor_sim.h"
|
||||||
|
#include "coap_client.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_openthread.h"
|
||||||
|
#include "esp_openthread_lock.h"
|
||||||
|
#include "esp_openthread_netif_glue.h"
|
||||||
|
#include "esp_vfs_eventfd.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "openthread/dataset.h"
|
||||||
|
#include "openthread/instance.h"
|
||||||
|
#include "openthread/ip6.h"
|
||||||
|
#include "openthread/thread.h"
|
||||||
|
#include "thread_dataset.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ThreadNode";
|
||||||
|
|
||||||
|
#define LAB_INTERVAL_BUTTON_GPIO GPIO_NUM_1
|
||||||
|
#define LAB_SEND_INTERVAL_SLOW_MS 5000
|
||||||
|
#define LAB_SEND_INTERVAL_FAST_MS 1000
|
||||||
|
|
||||||
|
static const char *ot_role_to_string(otDeviceRole role)
|
||||||
|
{
|
||||||
|
return otThreadDeviceRoleToString(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ESP_OPENTHREAD_DEFAULT_NODE_RADIO_CONFIG() \
|
||||||
|
{ \
|
||||||
|
.radio_mode = RADIO_MODE_NATIVE, \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ESP_OPENTHREAD_DEFAULT_NODE_HOST_CONFIG() \
|
||||||
|
{ \
|
||||||
|
.host_connection_mode = HOST_CONNECTION_MODE_NONE, \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ESP_OPENTHREAD_DEFAULT_NODE_PORT_CONFIG() \
|
||||||
|
{ \
|
||||||
|
.storage_partition_name = "nvs", \
|
||||||
|
.netif_queue_size = 10, \
|
||||||
|
.task_queue_size = 10, \
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_node_state(otInstance *instance, const char *reason)
|
||||||
|
{
|
||||||
|
ESP_LOGI(
|
||||||
|
TAG,
|
||||||
|
"%s role=%s commissioned=%s channel=%u panid=0x%04x rloc16=0x%04x net='%s'",
|
||||||
|
reason,
|
||||||
|
ot_role_to_string(otThreadGetDeviceRole(instance)),
|
||||||
|
otDatasetIsCommissioned(instance) ? "yes" : "no",
|
||||||
|
otLinkGetChannel(instance),
|
||||||
|
otLinkGetPanId(instance),
|
||||||
|
otThreadGetRloc16(instance),
|
||||||
|
otThreadGetNetworkName(instance)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ot_state_changed_callback(otChangedFlags flags, void *context)
|
||||||
|
{
|
||||||
|
otInstance *instance = context;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "state changed flags=0x%08" PRIx32, (uint32_t)flags);
|
||||||
|
log_node_state(instance, "callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ensure_static_thread_address(otInstance *instance)
|
||||||
|
{
|
||||||
|
otError error = thread_add_static_unicast_address(instance, THREAD_NODE_STATIC_ADDR);
|
||||||
|
|
||||||
|
if (error == OT_ERROR_NONE) {
|
||||||
|
ESP_LOGI(TAG, "Configured stable Mesh-Local address %s", THREAD_NODE_STATIC_ADDR);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Failed to configure stable Mesh-Local address %s: %s",
|
||||||
|
THREAD_NODE_STATIC_ADDR, otThreadErrorToString(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void thread_start_network(otInstance *instance, otOperationalDatasetTlvs *dataset_tlvs)
|
||||||
|
{
|
||||||
|
if (otSetStateChangedCallback(instance, ot_state_changed_callback, instance) != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGW(TAG, "Failed to register OpenThread state callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
log_node_state(instance, "before_auto_start");
|
||||||
|
ensure_static_thread_address(instance);
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_auto_start(dataset_tlvs));
|
||||||
|
log_node_state(instance, "after_auto_start");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void interval_button_init(void)
|
||||||
|
{
|
||||||
|
const gpio_config_t button_config = {
|
||||||
|
.pin_bit_mask = 1ULL << LAB_INTERVAL_BUTTON_GPIO,
|
||||||
|
.mode = GPIO_MODE_INPUT,
|
||||||
|
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(gpio_config(&button_config));
|
||||||
|
ESP_LOGI(TAG, "Interval button initialized on GPIO %d", LAB_INTERVAL_BUTTON_GPIO);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t application_get_send_interval_ms(void)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* TODO :
|
||||||
|
* - prectete stav tlacitka na pinu D1
|
||||||
|
* - pri stisku prepnite na rychly interval
|
||||||
|
* - bez stisku pouzijte pomaly interval
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return LAB_SEND_INTERVAL_SLOW_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_send_sensor_sample(otInstance *instance)
|
||||||
|
{
|
||||||
|
sensor_data_t data = sensor_read();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Node %s sending data: temp=%.1f hum=%d button=%d",
|
||||||
|
data.node_id, data.temp, data.humidity, data.button);
|
||||||
|
|
||||||
|
coap_send_data(instance, THREAD_GATEWAY_COAP_URL, &data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_main(void)
|
||||||
|
{
|
||||||
|
esp_err_t ret;
|
||||||
|
esp_vfs_eventfd_config_t eventfd_config = {
|
||||||
|
.max_fds = 4,
|
||||||
|
};
|
||||||
|
otOperationalDatasetTlvs dataset_tlvs;
|
||||||
|
static esp_openthread_config_t ot_config = {
|
||||||
|
.netif_config = ESP_NETIF_DEFAULT_OPENTHREAD(),
|
||||||
|
.platform_config = {
|
||||||
|
.radio_config = ESP_OPENTHREAD_DEFAULT_NODE_RADIO_CONFIG(),
|
||||||
|
.host_config = ESP_OPENTHREAD_DEFAULT_NODE_HOST_CONFIG(),
|
||||||
|
.port_config = ESP_OPENTHREAD_DEFAULT_NODE_PORT_CONFIG(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
otInstance *instance;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initializing Thread node...");
|
||||||
|
ret = nvs_flash_init();
|
||||||
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
|
ret = nvs_flash_init();
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(ret);
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_start(&ot_config));
|
||||||
|
|
||||||
|
instance = esp_openthread_get_instance();
|
||||||
|
thread_dataset_get_tlvs(&dataset_tlvs);
|
||||||
|
|
||||||
|
sensor_init();
|
||||||
|
interval_button_init();
|
||||||
|
esp_openthread_lock_acquire(portMAX_DELAY);
|
||||||
|
thread_start_network(instance, &dataset_tlvs);
|
||||||
|
coap_client_init(instance);
|
||||||
|
esp_openthread_lock_release();
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
uint32_t delay_ms = application_get_send_interval_ms();
|
||||||
|
|
||||||
|
esp_openthread_lock_acquire(portMAX_DELAY);
|
||||||
|
application_send_sensor_sample(instance);
|
||||||
|
esp_openthread_lock_release();
|
||||||
|
ESP_LOGI(TAG, "Next send in %" PRIu32 " ms", delay_ms);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(delay_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/sensor_sim.c
Normal file
17
src/sensor_sim.c
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#include "sensor_sim.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h> // 👈 DŮLEŽITÉ
|
||||||
|
|
||||||
|
void sensor_init() {
|
||||||
|
// nic speciálního
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor_data_t sensor_read() {
|
||||||
|
sensor_data_t d;
|
||||||
|
snprintf(d.node_id, sizeof(d.node_id), "node_%d", rand()%10);
|
||||||
|
d.temp = 20.0 + (rand() % 1000)/100.0;
|
||||||
|
d.humidity = 40 + rand() % 20;
|
||||||
|
d.button = rand() % 2;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
14
src/sensor_sim.h
Normal file
14
src/sensor_sim.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#ifndef SENSOR_SIM_H
|
||||||
|
#define SENSOR_SIM_H
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char node_id[16];
|
||||||
|
float temp;
|
||||||
|
int humidity;
|
||||||
|
int button;
|
||||||
|
} sensor_data_t;
|
||||||
|
|
||||||
|
void sensor_init();
|
||||||
|
sensor_data_t sensor_read();
|
||||||
|
|
||||||
|
#endif
|
||||||
83
src/thread_dataset.h
Normal file
83
src/thread_dataset.h
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#ifndef THREAD_DATASET_H
|
||||||
|
#define THREAD_DATASET_H
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "openthread/dataset.h"
|
||||||
|
#include "openthread/ip6.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO pro studenty:
|
||||||
|
* - nastavte parametry site Thread podle README.md
|
||||||
|
*/
|
||||||
|
#define THREAD_NETWORK_NAME "TODO_NETWORK_NAME"
|
||||||
|
#define THREAD_CHANNEL 0
|
||||||
|
#define THREAD_PANID 0x0000
|
||||||
|
#define THREAD_MESH_LOCAL_PREFIX_STRING "fd00:0000:0000:0000"
|
||||||
|
#define THREAD_GATEWAY_STATIC_ADDR THREAD_MESH_LOCAL_PREFIX_STRING "::1"
|
||||||
|
#define THREAD_NODE_STATIC_ADDR THREAD_MESH_LOCAL_PREFIX_STRING "::10"
|
||||||
|
#define THREAD_GATEWAY_COAP_URL "coap://[" THREAD_GATEWAY_STATIC_ADDR "]:5683/sensor"
|
||||||
|
|
||||||
|
static inline void thread_dataset_get_active(otOperationalDataset *dataset)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* TODO:
|
||||||
|
* - doplnte Network Key podle README.md
|
||||||
|
* - doplnte Extended PAN ID podle README.md
|
||||||
|
* - zkontrolujte Mesh-Local prefix podle README.md
|
||||||
|
*/
|
||||||
|
static const uint8_t network_key[OT_NETWORK_KEY_SIZE] = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
static const uint8_t ext_pan_id[OT_EXT_PAN_ID_SIZE] = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
static const uint8_t mesh_local_prefix[OT_MESH_LOCAL_PREFIX_SIZE] = {
|
||||||
|
0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
memset(dataset, 0, sizeof(*dataset));
|
||||||
|
|
||||||
|
dataset->mActiveTimestamp.mSeconds = 1;
|
||||||
|
dataset->mActiveTimestamp.mTicks = 0;
|
||||||
|
dataset->mActiveTimestamp.mAuthoritative = true;
|
||||||
|
memcpy(dataset->mNetworkKey.m8, network_key, sizeof(network_key));
|
||||||
|
memcpy(dataset->mExtendedPanId.m8, ext_pan_id, sizeof(ext_pan_id));
|
||||||
|
memcpy(dataset->mMeshLocalPrefix.m8, mesh_local_prefix, sizeof(mesh_local_prefix));
|
||||||
|
memcpy(dataset->mNetworkName.m8, THREAD_NETWORK_NAME, sizeof(THREAD_NETWORK_NAME));
|
||||||
|
dataset->mPanId = THREAD_PANID;
|
||||||
|
dataset->mChannel = THREAD_CHANNEL;
|
||||||
|
|
||||||
|
dataset->mComponents.mIsActiveTimestampPresent = true;
|
||||||
|
dataset->mComponents.mIsNetworkKeyPresent = true;
|
||||||
|
dataset->mComponents.mIsNetworkNamePresent = true;
|
||||||
|
dataset->mComponents.mIsExtendedPanIdPresent = true;
|
||||||
|
dataset->mComponents.mIsMeshLocalPrefixPresent = true;
|
||||||
|
dataset->mComponents.mIsPanIdPresent = true;
|
||||||
|
dataset->mComponents.mIsChannelPresent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline otError thread_add_static_unicast_address(otInstance *instance, const char *address)
|
||||||
|
{
|
||||||
|
otIp6Address parsed_address;
|
||||||
|
otIp6InterfaceIdentifier iid;
|
||||||
|
|
||||||
|
/* Studenti si zde mohou overit, jak se IPv6 adresa mapuje do IID. */
|
||||||
|
if (otIp6AddressFromString(address, &parsed_address) != OT_ERROR_NONE) {
|
||||||
|
return OT_ERROR_INVALID_ARGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(iid.mFields.m8, &parsed_address.mFields.m8[8], sizeof(iid.mFields.m8));
|
||||||
|
return otIp6SetMeshLocalIid(instance, &iid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void thread_dataset_get_tlvs(otOperationalDatasetTlvs *dataset_tlvs)
|
||||||
|
{
|
||||||
|
otOperationalDataset dataset;
|
||||||
|
|
||||||
|
thread_dataset_get_active(&dataset);
|
||||||
|
otDatasetConvertToTlvs(&dataset, dataset_tlvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
11
test/README
Normal file
11
test/README
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
This directory is intended for PlatformIO Test Runner and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PlatformIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||||
Reference in New Issue
Block a user