Skip to content

Commit

Permalink
Add HTTP API and JSON parser. Fixes #168 and #169.
Browse files Browse the repository at this point in the history
  • Loading branch information
jakkra committed May 22, 2024
1 parent bb87d28 commit c3db0a7
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 73 deletions.
2 changes: 2 additions & 0 deletions app/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,5 @@ CONFIG_RETAINED_MEM_MUTEX_FORCE_DISABLE=y
CONFIG_RETENTION_BUFFER_SIZE=256

CONFIG_ZBUS_CHANNELS_SYS_INIT_PRIORITY=1

CONFIG_CJSON_LIB=y
87 changes: 23 additions & 64 deletions app/src/applications/trivia/trivia_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@

#include "trivia_ui.h"
#include "managers/zsw_app_manager.h"
#include "ble/ble_comm.h"
#include "events/ble_event.h"
#include "ui/utils/zsw_ui_utils.h"
#include <ble/ble_http.h>
#include "cJSON.h"

/*Get 1x easy question with true/false type*/
#define GB_HTTP_REQUEST "{\"t\":\"http\", \"url\":\"https://opentdb.com/api.php?amount=1&difficulty=easy&type=boolean\"} \n"
#define HTTP_REQUEST_URL "https://opentdb.com/api.php?amount=1&difficulty=easy&type=boolean"

LOG_MODULE_REGISTER(trivia_app, CONFIG_ZSW_TRIVIA_APP_LOG_LEVEL);

Expand All @@ -32,12 +32,6 @@ static void trivia_app_start(lv_obj_t *root, lv_group_t *group);
static void trivia_app_stop(void);
static void on_button_click(trivia_button_t trivia_button);
static void request_new_question(void);
static void zbus_ble_comm_data_callback(const struct zbus_channel *chan);

ZBUS_CHAN_DECLARE(ble_comm_data_chan);
ZBUS_OBS_DECLARE(trivia_app_ble_comm_lis);
ZBUS_CHAN_ADD_OBS(ble_comm_data_chan, trivia_app_ble_comm_lis, 1);
ZBUS_LISTENER_DEFINE(trivia_app_ble_comm_lis, zbus_ble_comm_data_callback);

ZSW_LV_IMG_DECLARE(quiz);

Expand All @@ -47,6 +41,7 @@ typedef struct trivia_app_question {
} trivia_app_question_t;

static trivia_app_question_t trivia_app_question;
static bool active;

static application_t app = {
.name = "Trivia",
Expand All @@ -55,68 +50,17 @@ static application_t app = {
.stop_func = trivia_app_stop
};

/// @todo This will probably be on utils after PR #134
static char *extract_value_str(char *key, const char *data, int *data_len)
{
char *start;
char *end;
char *str = strstr(data, key);

if (str == NULL) {
return NULL;
}
str += strlen(key);
if ((*str != '\"') && (*str != '\\')) {
return NULL; // Seems to be an INT?
}
str += sizeof("\\\":\\");
if (*str == '\0') {
return NULL; // Got end of data
}
end = strstr(str, "\\\"");
if (end == NULL) {
return NULL; // No end of value
}

start = str;
*data_len = end - start;

return start;
}

static void zbus_ble_comm_data_callback(const struct zbus_channel *chan)
{
const struct ble_data_event *event = zbus_chan_const_msg(chan);

if (event->data.type == BLE_COMM_DATA_TYPE_HTTP) {
int question_len = 0;
memset(&trivia_app_question, 0, sizeof(trivia_app_question));

// {"response_code":0,"results":[{"type":"boolean","difficulty":"easy","category":"Science: Computers","question":"The logo for Snapchat is a Bell.","correct_answer":"False","incorrect_answers":["True"]}]}
char *temp_value = extract_value_str("question", event->data.data.http_response.response, &question_len);
LOG_DBG("HTTP question extracted %s", temp_value);

memcpy(trivia_app_question.question, temp_value, (question_len > MAX_HTTP_FIELD_LENGTH) ? MAX_HTTP_FIELD_LENGTH :
question_len); /// @todo cast?
trivia_ui_update_question(trivia_app_question.question);

/*Get the correct answer*/
temp_value = extract_value_str("correct_answer", event->data.data.http_response.response, &question_len);
LOG_DBG("Correct answer %s", temp_value);

trivia_app_question.correct_answer = (temp_value[0] == 'F') ? false : true;
}
}

static void trivia_app_start(lv_obj_t *root, lv_group_t *group)
{
LOG_DBG("Trivia app start");
active = true;
trivia_ui_show(root, on_button_click);
request_new_question();
}

static void trivia_app_stop(void)
{
active = false;
trivia_ui_remove();
}

Expand All @@ -127,10 +71,25 @@ static int trivia_app_add(void)
return 0;
}

static void http_rsp_cb(ble_http_status_code_t status, cJSON *response)
{
if (status == BLE_HTTP_STATUS_OK && active) {
cJSON *question = cJSON_GetObjectItem(response, "question");
cJSON *correct_answer = cJSON_GetObjectItem(response, "correct_answer");
if (question == NULL || correct_answer == NULL) {
LOG_ERR("Failed to parse JSON data");
return;
}
memset(trivia_app_question.question, 0, sizeof(trivia_app_question.question));
strncpy(trivia_app_question.question, question->valuestring, sizeof(trivia_app_question.question));
trivia_app_question.correct_answer = (correct_answer->valuestring[0] == 'F') ? false : true;
trivia_ui_update_question(trivia_app_question.question);
}
}

static void request_new_question()
{
// Gadgetbridge does not like the null character.
ble_comm_send(GB_HTTP_REQUEST, sizeof(GB_HTTP_REQUEST) - 1);
zsw_ble_http_get(HTTP_REQUEST_URL, http_rsp_cb);
}

static void on_button_click(trivia_button_t trivia_button)
Expand Down
1 change: 1 addition & 0 deletions app/src/ble/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ target_sources(app PRIVATE ble_ancs.c)
target_sources(app PRIVATE ble_cts.c)
target_sources(app PRIVATE gadgetbridge/ble_gadgetbridge.c)
target_sources(app PRIVATE ble_hid.c)
target_sources(app PRIVATE ble_http.c)
target_sources(app PRIVATE zsw_gatt_sensor_server.c)
1 change: 1 addition & 0 deletions app/src/ble/ble_comm.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ typedef struct ble_comm_remote_control {
typedef struct ble_comm_http_response {
char err[MAX_HTTP_FIELD_LENGTH + 1];
char response[MAX_HTTP_FIELD_LENGTH + 1];
int id;
} ble_comm_http_response_t;

typedef struct ble_comm_cb_data {
Expand Down
113 changes: 113 additions & 0 deletions app/src/ble/ble_http.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#include "ble_http.h"
#include <zephyr/kernel.h>
#include <string.h>
#include <stdio.h>
#include <zephyr/logging/log.h>
#include <zephyr/zbus/zbus.h>
#include <events/ble_event.h>
#include <cJSON.h>

LOG_MODULE_REGISTER(ble_http, LOG_LEVEL_DBG);

#define GB_HTTP_REQUEST_FMT "{\"t\":\"http\", \"url\":\"%s\", id:\"%d\"} \n"

#define HTTP_TIMEOUT_SECONDS 5

static void zbus_ble_comm_data_callback(const struct zbus_channel *chan);
static void ble_http_timeout_handler(struct k_work *work);

ZBUS_LISTENER_DEFINE(ble_http_lis, zbus_ble_comm_data_callback);
ZBUS_CHAN_DECLARE(ble_comm_data_chan);
ZBUS_CHAN_ADD_OBS(ble_comm_data_chan, ble_http_lis, 1);

static bool request_pending;
static uint16_t request_id;
static ble_http_callback ble_http_cb;

K_WORK_DELAYABLE_DEFINE(ble_http_timeout_work, ble_http_timeout_handler);

static void ble_http_timeout_handler(struct k_work *work)
{
LOG_WRN("HTTP Timeout");
ble_http_cb(BLE_HTTP_STATUS_TIMEOUT, NULL);
}

static void zbus_ble_comm_data_callback(const struct zbus_channel *chan)
{
const struct ble_data_event *event = zbus_chan_const_msg(chan);

if (event->data.type == BLE_COMM_DATA_TYPE_HTTP) {
if (event->data.data.http_response.id != request_id) {
LOG_WRN("Not the expected response ID, was: %d, expected: %d", event->data.data.http_response.id, request_id);
return;
}
struct k_work_sync sync;
k_work_cancel_delayable_sync(&ble_http_timeout_work, &sync);
request_pending = false;

if (strlen(event->data.data.http_response.err) > 0) {
LOG_WRN("HTTP request failed: %s", event->data.data.http_response.err);
} else if (strlen(event->data.data.http_response.response) > 0) {
char *fixed_rsp = k_malloc(strlen(event->data.data.http_response.response) + 1);
__ASSERT(fixed_rsp, "Failed to allocate memory for fixed_rsp");
// As the response from Gadgetbride contains two characters like this[\\, "] instead of just one character [\"],
// we need to remove them for it to be avalid JSON accepted by cJSON
int i;
int j;
for (i = 0, j = 0; i < strlen(event->data.data.http_response.response) - 1;) {
if (event->data.data.http_response.response[i] == '\\' && event->data.data.http_response.response[i + 1] == '"') {
fixed_rsp[j] = '\"';
j++;
i += 2;
} else {
fixed_rsp[j] = event->data.data.http_response.response[i];
j++;
i++;
}
}
fixed_rsp[j - 1] = '\0'; // Remove the last " as it belongs not to the JSON
cJSON *gb_rsp = cJSON_Parse(fixed_rsp);
if (gb_rsp == NULL) {
LOG_ERR("Failed to parse JSON rsp data from GB");
} else {
cJSON *results = cJSON_GetObjectItem(gb_rsp, "results");
if (cJSON_GetArraySize(results) == 1) {
ble_http_cb(BLE_HTTP_STATUS_OK, cJSON_GetArrayItem(results, 0));
} else {
LOG_ERR("Unexpected number of results: %d, expected 1", cJSON_GetArraySize(results));
}
}
cJSON_Delete(gb_rsp);
k_free(fixed_rsp);
}
}
}

int zsw_ble_http_get(char *url, ble_http_callback cb)
{
int ret;
char *request;

if (request_pending) {
return -EBUSY;
}

request_id++;

request = k_malloc(strlen(url) + strlen(GB_HTTP_REQUEST_FMT) + 1);
__ASSERT(request, "Failed to allocate memory for request URL");

snprintf(request, strlen(url) + strlen(GB_HTTP_REQUEST_FMT) + 1, GB_HTTP_REQUEST_FMT, url, request_id);
ret = ble_comm_send(request, strlen(request));
k_free(request);
if (ret != 0) {
return ret;
}

ble_http_cb = cb;
request_pending = true;

k_work_schedule(&ble_http_timeout_work, K_SECONDS(HTTP_TIMEOUT_SECONDS));

return 0;
}
32 changes: 32 additions & 0 deletions app/src/ble/ble_http.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <stdint.h>
#include "cJSON.h"

typedef enum {
BLE_HTTP_STATUS_OK,
BLE_HTTP_STATUS_TIMEOUT,
BLE_HTTP_STATUS_BUSY,
} ble_http_status_code_t;

/**
* @brief Callback function for HTTP GET requests.
*
* This callback function is invoked when an HTTP GET request has been completed.
*
* @param status The status of the HTTP GET request.
* @param response The response data in JSON format. The cJSON object parameter will be deleted automatically after the callback function returns.
*/
typedef void (*ble_http_callback)(ble_http_status_code_t status, cJSON *response);

/**
* @brief Sends an HTTP GET request to the specified URL.
*
* This function sends an HTTP GET request to the specified URL and invokes the provided callback function
* when the response is received. The callback function is passed the status code and the response data in
* JSON format. The cJSON object parameter will be deleted automatically after the callback function returns.
* Hence it shall not attempt to deleted in the callback.
*
* @param url The URL to send the GET request to.
* @param cb The callback function to invoke when the response is received.
* @return Returns 0 on success, or a negative error code on failure.
*/
int zsw_ble_http_get(char *url, ble_http_callback cb);
32 changes: 23 additions & 9 deletions app/src/ble/gadgetbridge/ble_gadgetbridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -519,22 +519,36 @@ static int parse_httpstate(char *data, int len)

cb.type = BLE_COMM_DATA_TYPE_HTTP;

temp_value = extract_value_str("\"id\":", data, &temp_len);
if (temp_value) {
errno = 0;
char *end_data;
cb.data.http_response.id = strtol(temp_value, &end_data, 10);
if (temp_value == end_data || errno != 0) {
LOG_WRN("Failed parsing http request id");
cb.data.http_response.id = -1;
}
} else {
cb.data.http_response.id = -1;
}

// {"t":"http","err":"Internet access not enabled in this Gadgetbridge build"}
temp_value = extract_value_str("\"err\":", data, &temp_len);

if (temp_value != NULL) {
LOG_ERR("HTTP err: %s", temp_value);
memcpy(cb.data.http_response.err, temp_value, strlen(cb.data.http_response.err));
return -1;
memcpy(cb.data.http_response.err, temp_value, temp_len);
send_ble_data_event(&cb);
} else {
temp_value = extract_value_str("\"resp\":", data, &temp_len);
if (temp_value) {
LOG_DBG("HTTP response: %s", temp_value);
memcpy(cb.data.http_response.response, temp_value,
(strlen(temp_value) > MAX_HTTP_FIELD_LENGTH) ? MAX_HTTP_FIELD_LENGTH : strlen(temp_value)); /// @todo cast?
send_ble_data_event(&cb);
}
}

temp_value = extract_value_str("\"resp\":", data, &temp_len);
LOG_DBG("HTTP response: %s", temp_value);
memcpy(cb.data.http_response.response, temp_value,
(strlen(temp_value) > MAX_HTTP_FIELD_LENGTH) ? MAX_HTTP_FIELD_LENGTH : strlen(temp_value)); /// @todo cast?

send_ble_data_event(&cb);

return 0;
}

Expand Down

0 comments on commit c3db0a7

Please sign in to comment.