From 5ac1aba64ce4bc4bfc11b99c07da01ad162c5c2d Mon Sep 17 00:00:00 2001 From: Ronald Date: Fri, 18 Oct 2024 21:20:44 +0100 Subject: [PATCH] Time to add this to a repository. This is an MVP, the goal is that this will be able to print to a simple Raspberry Pi screen, however, right now this will print the glucose levels to stdout. --- .gitignore | 5 + .gitmodules | 3 + CMakeLists.txt | 23 +++ libs/log.c | 1 + src/main.c | 436 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.h | 26 +++ 6 files changed, 494 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 160000 libs/log.c create mode 100644 src/main.c create mode 100644 src/main.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2297134 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +compile_commands.json +config.ini +Todo.md +.cache diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a47b23 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/log.c"] + path = libs/log.c + url = https://github.com/rxi/log.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c650095 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.25) +project(GlucoseMonitor C) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Find the pkg-config package +find_package(PkgConfig REQUIRED) + +# Use pkg-config to find cJSON +pkg_check_modules(CJSON REQUIRED libcjson) + +# Use pkg-config to find curl +pkg_check_modules(LIBCURL REQUIRED libcurl) + +# Add the executable +add_executable(glucose_monitor src/main.c libs/log.c/src/log.c) + +# Link against the cJSON library +target_link_libraries(glucose_monitor PRIVATE ${LIBCURL_LIBRARIES} ${CJSON_LIBRARIES}) + +# Include cJSON's include directories +target_include_directories(glucose_monitor PRIVATE include libs/log.c/src ${LIBCURL_LIBRARIES} ${CJSON_INCLUDE_DIRS}) + diff --git a/libs/log.c b/libs/log.c new file mode 160000 index 0000000..f9ea349 --- /dev/null +++ b/libs/log.c @@ -0,0 +1 @@ +Subproject commit f9ea34994bd58ed342d2245cd4110bb5c6790153 diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..42789f4 --- /dev/null +++ b/src/main.c @@ -0,0 +1,436 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "log.h" + +#include "main.h" + +void handle_sigint(int signal) +{ + struct timespec ts = {10, 0}; // 2 seconds, 0 nanoseconds + + log_info("Received SIGINT gracefully shutting down..."); + nanosleep(&ts, NULL); + log_info("Finished cleaning up"); + exit(1); +} + +size_t write_chunk(void *data, size_t size, size_t nmemb, void *clientp) +{ + size_t real_size; + struct Response *response; + char *ptr; + + real_size = size * nmemb; + response = (struct Response *) clientp; + + ptr = realloc(response->string, response->size + real_size + 1); + if (ptr == NULL) { + return CURL_WRITEFUNC_ERROR; + } + + response->string = ptr; + memcpy(&(response->string[response->size]), data, real_size); + response->size += real_size; + response->string[response->size] = '\0'; // ensure that string is NULL-terminated + + return real_size; +} + +/* + * Ensure that the response returned is freed +*/ +struct Response* post_request(const struct Request *request) +{ + CURL *curl; + + curl = curl_easy_init(); + if (curl == NULL) { + fprintf(stderr, "curl_easy_init failed"); + return NULL; + } + + struct Response *response = (struct Response *)malloc(sizeof(struct Response)); + response->string = malloc(1); + response->size = 0; + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_URL, request->endpoint); + + curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.txt"); + curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + curl_easy_setopt(curl, CURLOPT_USERAGENT, "PostmanRuntime/7.28.4"); + + /* + * Set userdata to response struct defined earlier + * This allows us to store the response of the rqeuest in the response structure + */ + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_chunk); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) response); + + struct curl_slist *headers; + headers = NULL; + headers = curl_slist_append(headers, "version: 4.7"); + headers = curl_slist_append(headers, "product: llu.ios"); + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, "Accept: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLE_OK) { + log_error("curl_easy_perform failed: %s\n", curl_easy_strerror(result)); + return NULL; + } + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return response; +} + +/* + * Ensure that the response returned is freed +*/ +struct Response* get_request(const struct Request *request) +{ + CURL *curl; + + curl = curl_easy_init(); + if (curl == NULL) { + fprintf(stderr, "curl_easy_init failed"); + return NULL; + } + + struct Response *response = (struct Response *)malloc(sizeof(struct Response)); + response->string = malloc(1); + response->size = 0; + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_URL, request->endpoint); + + curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.txt"); + curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + curl_easy_setopt(curl, CURLOPT_USERAGENT, "PostmanRuntime/7.28.4"); + + /* + * Set userdata to response struct defined earlier + * This allows us to store the response of the rqeuest in the response structure + */ + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_chunk); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) response); + + struct curl_slist *headers; + headers = NULL; + char authorization_header[BUFFER_SIZE]; + sprintf(authorization_header, "Authorization: Bearer %s", request->token); + headers = curl_slist_append(headers, authorization_header); + headers = curl_slist_append(headers, "version: 4.7"); + headers = curl_slist_append(headers, "product: llu.ios"); + headers = curl_slist_append(headers, "Accept: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLE_OK) { + log_error("curl_easy_perform failed: %s\n", curl_easy_strerror(result)); + return NULL; + } + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + return response; +} + +size_t get_auth_token(const char *email, const char *password, char *auth_token) +{ + int body_size; + + struct Request request; + request.endpoint = "https://api.libreview.io/llu/auth/login"; + + body_size = sprintf( + request.body, + "{\n \"email\": \"%s\",\n \"password\": \"%s\"\n}", + email, + password + ); + if (body_size == 0) { + return 0; + } + + Response *response = post_request(&request); + + cJSON *json = cJSON_Parse(response->string); + if (json == NULL) { + fprintf(stderr, "error parsing JSON\n"); + free(response->string); + free(response); + return 0; + } + + cJSON *data = cJSON_GetObjectItem(json, "data"); + if (data == NULL) { + log_error("failed to parse JSON object data"); + free(response->string); + free(response); + cJSON_Delete(json); + return 0; + } + + cJSON *auth_ticket = cJSON_GetObjectItem(data, "authTicket"); + if (auth_ticket == NULL) { + printf("No authTicket found\n"); + free(response->string); + free(response); + cJSON_Delete(json); + return 0; + } + + cJSON *token = cJSON_GetObjectItem(auth_ticket, "token"); + if (token == NULL) { + printf("No token found\n"); + free(response->string); + free(response); + cJSON_Delete(json); + return 0; + } + + size_t token_length = strlen(token->valuestring)+1; + strncpy(auth_token, token->valuestring, token_length); + + cJSON_Delete(json); + free(response->string); + free(response); + + return strlen(auth_token); +} + +size_t get_patient_id(const char *token, char *patient_id) +{ + CURL *curl; + + curl = curl_easy_init(); + if (curl == NULL) { + fprintf(stderr, "curl_easy_init failed"); + return 0; + } + + struct Request request; + request.endpoint = "https://api.libreview.io/llu/connections"; + request.token = token; + request.body = ""; + + Response *response = get_request(&request); + if (response->string == NULL) { + patient_id = NULL; + log_error("get_request returned null"); + return 0; + } + + cJSON *json = cJSON_Parse(response->string); + if (json == NULL) { + fprintf(stderr, "error parsing JSON\n"); + free(response->string); + return 0; + } + + cJSON *data = cJSON_GetObjectItem(json, "data"); + if (data == NULL) { + fprintf(stderr, "no data found\n"); + free(response->string); + cJSON_Delete(json); + return 0; + } + + int number_of_connections = cJSON_GetArraySize(data); + if (number_of_connections < 1) { + fprintf(stderr, "no connections found\n"); + free(response->string); + cJSON_Delete(json); + return 0; + } else if (number_of_connections > 1) { + fprintf(stderr, "more than one connection found, sorry program only supports accounts with one connection"); + free(response->string); + cJSON_Delete(json); + exit(-1); + } + + cJSON *sub_item = cJSON_GetArrayItem(data, 0); + cJSON *patient_id_object = cJSON_GetObjectItem(sub_item, "patientId"); + if (patient_id_object == NULL) { + fprintf(stderr, "failed to get patient ID"); + free(response->string); + cJSON_Delete(json); + return 0; + } + + size_t patient_id_length = strlen(patient_id_object->valuestring); + strncpy(patient_id, patient_id_object->valuestring, patient_id_length); + + free(response->string); + cJSON_Delete(json); + + return patient_id_length; +} + +double get_glucose_units(const char *token, const char *patient_id) +{ + CURL *curl; + + curl = curl_easy_init(); + if (curl == NULL) { + fprintf(stderr, "curl_easy_init failed"); + return 0; + } + + struct Response response; + response.string = malloc(1); + response.size = 0; + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + + char url[BUFFER_SIZE]; + /* + * Having the null byte in the string means it ends up in the URL + * this makes libcurl very sad, as the URL is no longer valid. + * We therefore copy everything except the NULL byte to non_null_terinated_patient_id + * Why is this -2? Shouldn't it just be -1 to get rid of the NULL byte? + */ + size_t length_of_patient_id_without_null_byte = strlen(patient_id)-2; + char non_null_terminated_patient_id[length_of_patient_id_without_null_byte]; + memcpy(non_null_terminated_patient_id, patient_id, length_of_patient_id_without_null_byte); + sprintf(url, "https://api.libreview.io/llu/connections/%s/graph", non_null_terminated_patient_id); + curl_easy_setopt(curl, CURLOPT_URL, url); + + curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.txt"); + curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt"); + + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + curl_easy_setopt(curl, CURLOPT_USERAGENT, "Postmanuntime/7.28.4"); + + /* + * Set userdata to response struct defined earlier + * This allows us to store the response of the rqeuest in the response structure + */ + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_chunk); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &response); + + struct curl_slist *headers; + headers = NULL; + char authorization_header[BUFFER_SIZE]; + sprintf(authorization_header, "Authorization: Bearer %s", token); + headers = curl_slist_append(headers, authorization_header); + headers = curl_slist_append(headers, "version: 4.7"); + headers = curl_slist_append(headers, "product: llu.ios"); + headers = curl_slist_append(headers, "Accept: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode result = curl_easy_perform(curl); + if (result != CURLE_OK) { + fprintf(stderr, "curl_easy_perform failed: %s\n", curl_easy_strerror(result)); + return 0; + } + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + cJSON *json = cJSON_Parse(response.string); + if (json == NULL) { + fprintf(stderr, "error parsing JSON\n"); + free(response.string); + return 0; + } + + cJSON *data = cJSON_GetObjectItem(json, "data"); + if (data == NULL) { + fprintf(stderr, "error parsing JSON for data object\n"); + free(response.string); + cJSON_Delete(json); + return 0; + } + + cJSON *connection = cJSON_GetObjectItem(data, "connection"); + if (connection == NULL) { + fprintf(stderr, "error parsing JSON for connection\n"); + free(response.string); + cJSON_Delete(json); + return 0; + } + + cJSON *glucose_measurement = cJSON_GetObjectItem(connection, "glucoseMeasurement"); + if (glucose_measurement == NULL) { + fprintf(stderr, "error parsing JSON for glucoseMeasurement\n"); + free(response.string); + cJSON_Delete(json); + return 0; + } + + cJSON *value = cJSON_GetObjectItem(glucose_measurement, "Value"); + if (value == NULL) { + fprintf(stderr, "error parsing JSON\n"); + free(response.string); + cJSON_Delete(json); + return 0; + } + + cJSON_Delete(json); + free(response.string); + + return value->valuedouble; +} + + +int main(void) +{ + signal(SIGINT, handle_sigint); + + curl_global_init(CURL_GLOBAL_DEFAULT); + + char token[BUFFER_SIZE]; ; + size_t token_size; + + token_size = get_auth_token(USERNAME, PASSWORD, token); + if (token_size < 1) { + log_fatal("get_auth_token failed to return a token"); + exit(-1); + } + log_debug("token: %s\n", token); + + char patient_id[BUFFER_SIZE]; + size_t patient_id_length; + + patient_id_length = get_patient_id(token, patient_id); + if (patient_id_length < 1) { + log_fatal("failed to get patient ID, exiting..."); + exit(-1); + } + + log_debug("patient ID: %s\n", patient_id); + + struct timespec ts = {5, 0}; // 5 seconds, 0 nanoseconds + time_t t; + char time_str[BUFFER_SIZE]; + while (true) { + time(&t); + strftime(time_str, BUFFER_SIZE, "%H:%M:%S %A %d %B %Y", localtime(&t)); + + printf("Glucose units at %s are %lf\n", time_str, get_glucose_units(token, patient_id)); + + nanosleep(&ts, NULL); + } + + curl_global_cleanup(); +} diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..e0c662c --- /dev/null +++ b/src/main.h @@ -0,0 +1,26 @@ +#include + +#define BUFFER_SIZE 1024 + +#define USERNAME "" +#define PASSWORD "" + +typedef struct Response { + char *string; + size_t size; +} Response; + +typedef struct Request { + const char *endpoint; + const char *token; + char *body; +} Request; + + +void handle_signal(int signal); +size_t write_chunk(void *data, size_t size, size_t nmemb, void *userdata); +struct Response* post_request(const struct Request *request); +struct Response* get_request(const struct Request *request); +size_t get_patient_id(const char *token, char *patient_id); +double get_glucose_units(const char *token, const char *patient_id); +int main(void);