Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ jobs:
- name: Build Attributes Sample
run: |
west build --pristine -b nrf9160dk/nrf9160/ns thingsboard/samples/attributes

- name: Build RPC Sample
run: |
west build --pristine -b nrf9160dk/nrf9160/ns thingsboard/samples/rpc
6 changes: 6 additions & 0 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ config THINGSBOARD_FOTA_CHUNK_SIZE
help
"Must be smaller than COAP_CLIENT_MSG_LEN"

config THINGSBOARD_RPC_TIMEOUT
int "Time to wait for an RPC call in ms"
default 5000
help
"If no answer was received, this tells how long to wait before accepting the next call"

config COAP_SERVER_HOSTNAME
string "Coap Server hostname"
default "10.101.45.222"
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ interpret the values in the actual field value, the contents are undefined.

### RPC calls - device to cloud

This functionality is implemented, but not exposed in a general fashion. The module uses this functionality to get the
current time from the server.
This functionality is implemented, users can perform RPC calls with `thingsboard_rpc` function.

### RPC calls - cloud to device

Expand Down
14 changes: 14 additions & 0 deletions include/thingsboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ time_t thingsboard_time_msec(void);
*/
int thingsboard_send_telemetry(const void *payload, size_t sz);

/**
* This callback will be called when the response for an RPC call
* was received from the Thingsboard server.
*/
typedef void (*rpc_callback_t)(const uint8_t *data, size_t len);

/**
* Send RPC call.
* 'method' is required, a callback in case the call returns data, too.
* Parameters are an optional string in JSON format as the variadic arguments
* See https://thingsboard.io/docs/reference/coap-api/#client-side-rpc for details.
*/
int thingsboard_rpc(const char *method, rpc_callback_t cb, const char *params);

struct tb_fw_id {
/** Title of your firmware, e.g. <project>-prod. This
* must match to what you configure on your thingsboard
Expand Down
13 changes: 13 additions & 0 deletions samples/rpc/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(rpc-sample)

target_sources(app PRIVATE src/main.c)

target_compile_options(app PRIVATE
-Wall
-Werror
-Wno-unused-parameter
)
32 changes: 32 additions & 0 deletions samples/rpc/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CONFIG_LOG=y
CONFIG_SHELL=y
CONFIG_SHELL_MINIMAL=y
CONFIG_COAP_INIT_ACK_TIMEOUT_MS=4000

# Cellular connectivity
CONFIG_NRF_MODEM_LIB=y
CONFIG_NRF_MODEM_LIB_LOG_FW_VERSION_UUID=y
CONFIG_LTE_LINK_CONTROL=y
CONFIG_LTE_NETWORK_MODE_LTE_M_NBIOT=y
CONFIG_LTE_MODE_PREFERENCE_LTE_M=y
CONFIG_LTE_PSM_REQ=y
CONFIG_LTE_EDRX_REQ=y
CONFIG_NET_SOCKETS_OFFLOAD=y

# Requirements for Thingsboard SDK
CONFIG_COAP=y
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_SOCKETS=y
CONFIG_JSON_LIBRARY=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y

# Thingsboard SDK
CONFIG_THINGSBOARD=y
CONFIG_THINGSBOARD_USE_PROVISIONING=n
# Set server name and access token
# CONFIG_COAP_SERVER_HOSTNAME=""
# CONFIG_THINGSBOARD_ACCESS_TOKEN=""
71 changes: 71 additions & 0 deletions samples/rpc/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include <thingsboard.h>

#include <modem/lte_lc.h>
#include <modem/nrf_modem_lib.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <zephyr/shell/shell.h>

#include <string.h>

LOG_MODULE_REGISTER(main);

static struct tb_fw_id fw_id = {
.fw_title = "rpc-sample", .fw_version = "v1.0.0", .device_name = "sample-device"};

int main(void)
{
int err = 0;

LOG_INF("Initializing modem library");
err = nrf_modem_lib_init();
if (err) {
LOG_ERR("Failed to initialize the modem library, error (%d): %s", err,
strerror(-err));
return err;
}

err = lte_lc_func_mode_set(LTE_LC_FUNC_MODE_ACTIVATE_LTE);
if (err) {
LOG_ERR("Failed to activate LTE");
return err;
}

LOG_INF("Connecting to LTE network");
err = lte_lc_connect();
if (err) {
LOG_ERR("Could not establish LTE connection, error (%d): %s", err, strerror(-err));
return err;
}
LOG_INF("LTE connection established");

LOG_INF("Connecting to Thingsboards");
err = thingsboard_init(NULL, &fw_id);
if (err) {
LOG_ERR("Could not initialize thingsboard connection, error (%d) :%s", err,
strerror(-err));
return err;
}
}

static void rpc_callback(const uint8_t *data, size_t len)
{
LOG_HEXDUMP_INF(data, len, "RPC repsonse: ");
}

static int cmd_do_rpc(const struct shell *shell, size_t argc, char **argv)
{
int err;

const char *params = "{\"name\":\"gcx\"}";
err = thingsboard_rpc("hello", rpc_callback, params);
if (err) {
LOG_ERR("Could not send RPC, error (%d): %s", err, strerror(-err));
return err;
}

return 0;
}

SHELL_CMD_REGISTER(rpc, NULL, "Send a RPC request", cmd_do_rpc);
128 changes: 95 additions & 33 deletions src/thingsboard.c
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#include "thingsboard.h"

#include <string.h>
#include <stdio.h>

#include <zephyr/kernel.h>
#include <zephyr/net/coap.h>
#include <zephyr/net/http/client.h>
#include <thingsboard_attr_parser.h>

#include "coap_client.h"
Expand All @@ -19,9 +21,17 @@ static struct {
int64_t last_request; // uptime when time was last requested in ms
} tb_time;

#define RPC_PARAMS_SIZE 256
#define RPC_PAYLOAD_SIZE 512

K_SEM_DEFINE(time_sem, 0, 1);
K_SEM_DEFINE(rpc_sem, 0, 1);

static void rcp_reset(struct k_timer *timer_id);
K_TIMER_DEFINE(rcp_timer, rcp_reset, NULL);

static attr_write_callback_t attribute_cb;
static rpc_callback_t rpc_cb;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback will be overwritten by another one if the user calls thingsboad_rpc multiple times without waiting for the response. When the response arrives the wrong callback would be called.
Avoiding multiple RPCs in flight with a semaphore approach that you mentioned yesterday would probably solve this. Or just reset rpc_cb to NULL after it was called and return with an error from thingsboad_rpc() if rpc_cb is not NULL?

A nicer solution would be to allow simultaneous RPC calls by using a memory slab to store multiple callbacks (the same way coap_client.c manages multiple requests).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, the limitation is obvious. I will think about a solution, being able to do multiple RPC calls simultaneously seems like something that's not often used, but could come in handy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be fine with having this limitations as it is probably okay for most use cases. We should just document this and somehow enforce the "only one rpc at a time" rule to avoid weird behaviour (like wrong callback called) for the user.


static void client_request_time(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(work_time, client_request_time);
Expand Down Expand Up @@ -94,38 +104,14 @@ static int timestamp_from_buf(int64_t *value, const void *buf, size_t sz)
return 0;
}

static int client_handle_time_response(struct coap_client_request *req,
struct coap_packet *response)
void client_handle_time_response(const uint8_t *data, size_t len)
{
int64_t ts = 0;
const uint8_t *payload;
uint16_t payload_len;
uint8_t code;
char code_str[5];
char expected_code_str[5];
int err;

LOG_INF("%s", __func__);

code = coap_header_get_code(response);
if (code != COAP_RESPONSE_CODE_CONTENT) {
coap_response_code_to_str(code, code_str);
coap_response_code_to_str(COAP_RESPONSE_CODE_CONTENT, expected_code_str);
LOG_ERR("Unexpected response code for timestamp request: got %s, expected %s",
code_str, expected_code_str);
return -1;
}

payload = coap_packet_get_payload(response, &payload_len);
if (!payload_len) {
LOG_ERR("Received empty timestamp");
return payload_len;
}

err = timestamp_from_buf(&ts, payload, payload_len);
int err = timestamp_from_buf(&ts, data, len);
if (err) {
LOG_ERR("Parsing of time response failed");
return err;
return;
}

tb_time.tb_time = ts;
Expand All @@ -136,7 +122,7 @@ static int client_handle_time_response(struct coap_client_request *req,
k_work_reschedule(&work_time, K_SECONDS(CONFIG_THINGSBOARD_TIME_REFRESH_INTERVAL_SECONDS));

k_sem_give(&time_sem);
return 0;
return;
}

static int client_subscribe_to_attributes(void)
Expand Down Expand Up @@ -174,11 +160,7 @@ static void client_request_time(struct k_work *work)
{
int err;

static const char *payload = "{\"method\": \"getCurrentTime\", \"params\": {}}";
const uint8_t *uri[] = {"api", "v1", access_token, "rpc", NULL};

err = coap_client_make_request(uri, payload, strlen(payload), COAP_TYPE_CON,
COAP_METHOD_POST, client_handle_time_response);
err = thingsboard_rpc("getCurrentTime", client_handle_time_response, NULL);
if (err) {
LOG_ERR("Failed to request time");
}
Expand All @@ -189,6 +171,81 @@ static void client_request_time(struct k_work *work)
k_work_reschedule(k_work_delayable_from_work(work), K_SECONDS(10));
}

static int client_handle_rpc_response(struct coap_client_request *req, struct coap_packet *response)
{
const uint8_t *payload;
uint16_t payload_len;
uint8_t code;
char code_str[5];
char expected_code_str[5];

LOG_INF("%s", __func__);

code = coap_header_get_code(response);
if (code != COAP_RESPONSE_CODE_CONTENT) {
coap_response_code_to_str(code, code_str);
coap_response_code_to_str(COAP_RESPONSE_CODE_CONTENT, expected_code_str);
LOG_ERR("Unexpected response code for RPC request: got %s, expected %s", code_str,
expected_code_str);
k_sem_give(&rpc_sem);
return -1;
}

payload = coap_packet_get_payload(response, &payload_len);
if (!payload_len) {
LOG_ERR("Received an empty RCP response");
k_sem_give(&rpc_sem);
return payload_len;
}

if (rpc_cb) {
rpc_cb(payload, payload_len);
}

k_sem_give(&rpc_sem);

return 0;
}

static void rcp_reset(struct k_timer *timer_id)
{
k_sem_give(&rpc_sem);
}

int thingsboard_rpc(const char *method, rpc_callback_t cb, const char* params)
{
int err;
char payload[RPC_PAYLOAD_SIZE] = {0};

if (method == NULL || strlen(method) == 0) {
LOG_ERR("method name must not be 'NULL' or empty");
return -EINVAL;
}

bool params_exist = (params != NULL && strlen(params) > 0) ? true : false;
if (!params_exist) {
params = "{}";
}

k_sem_take(&rpc_sem, K_FOREVER);
k_timer_start(&rcp_timer, K_MSEC(CONFIG_THINGSBOARD_RPC_TIMEOUT), K_NO_WAIT);

snprintf(payload, sizeof(payload), "{\"method\":\"%s\", \"params\": %s}", method, params);

const uint8_t *uri[] = {"api", "v1", access_token, "rpc", NULL};

err = coap_client_make_request(uri, payload, strlen(payload), COAP_TYPE_CON,
COAP_METHOD_POST, client_handle_rpc_response);
if (err) {
LOG_ERR("Failed to perform RPC");
return err;
}

rpc_cb = cb;

return 0;
}

int thingsboard_send_telemetry(const void *payload, size_t sz)
{
int err;
Expand Down Expand Up @@ -253,10 +310,15 @@ static void start_client(void)
int thingsboard_init(attr_write_callback_t cb, const struct tb_fw_id *fw_id)
{
attribute_cb = cb;
if (attribute_cb) {
return 0;
}
int ret;

current_fw = fw_id;

k_sem_give(&rpc_sem);

ret = coap_client_init(start_client);
if (ret != 0) {
LOG_ERR("Failed to initialize CoAP client (%d)", ret);
Expand Down
Loading
Loading