/* * Copyright 2024 Google LLC * * 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. */ #include "dls_private.h" #include "dls_endpoint.h" #include "dls_list.h" #include "dls_storage.h" #include "services/common/analytics/analytics.h" #include "services/common/comm_session/protocol.h" #include "services/common/comm_session/session_send_buffer.h" #include "services/common/system_task.h" #include "services/common/new_timer/new_timer.h" #include "services/normal/data_logging/data_logging_service.h" #include "kernel/pbl_malloc.h" #include "system/logging.h" #include "system/passert.h" #include "util/attributes.h" #include "util/legacy_checksum.h" #include "util/math.h" #include #include "FreeRTOS.h" #include "timers.h" typedef struct { ListNode list_node; DataLoggingSession *session; // Session metadata to make sure the session pointer corresponds to // the same session that was added to the reopen list. This guards // against the session being destroyed and another getting allocated // to the same address. Uuid app_uuid; time_t timestamp; uint32_t tag; } DataLoggingReopenEntry; static struct { PebbleMutex * mutex; TimerID ack_timer; bool report_in_progress; } s_endpoint_data; typedef struct PACKED { uint8_t command; uint8_t session_id; } DataLoggingCloseSessionMessage; typedef struct PACKED { uint8_t command; uint8_t session_id; Uuid app_uuid; uint32_t timestamp; uint32_t logging_session_tag; DataLoggingItemType data_item_type:8; uint16_t data_item_size; } DataLoggingOpenSessionMessage; static const uint16_t ENDPOINT_ID_DATA_LOGGING = 0x1a7a; #define ACK_NACK_TIMEOUT_TICKS (30 * RTC_TICKS_HZ) static const uint8_t MAX_NACK_COUNT = 20; static void reschedule_ack_timeout(void); static void update_session_state(DataLoggingSession *session, DataLoggingSessionCommState new_state, bool reschedule) { session->comm.state = new_state; switch (new_state) { case DataLoggingSessionCommStateOpening: case DataLoggingSessionCommStateSending: // These states need an ack from the phone. session->comm.ack_timeout = rtc_get_ticks() + ACK_NACK_TIMEOUT_TICKS; break; case DataLoggingSessionCommStateIdle: session->comm.ack_timeout = 0; break; } if (reschedule) { reschedule_ack_timeout(); } } static void send_timeout_msg(void *session_id_param) { uint8_t session_id = (uint8_t)(uintptr_t)session_id_param; CommSession *session = comm_session_get_system_session(); if (!session) { // timed out because of lost connection return; } DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id); struct PACKED { uint8_t command; uint8_t session_id; } msg = { .command = DataLoggingEndpointCmdTimeout, .session_id = logging_session->comm.session_id, }; comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg, sizeof(msg), COMM_SESSION_DEFAULT_TIMEOUT); } static bool check_ack_timeout_for_session(DataLoggingSession *session, void *data) { RtcTicks *current_ticks = (RtcTicks*) data; if (session->comm.ack_timeout != 0 && session->comm.ack_timeout <= *current_ticks) { PBL_LOG(LOG_LEVEL_DEBUG, "session %"PRIu8" timeout", session->comm.session_id); // Send timeout msg from system task because it could take a while and also require // more stack space than provided by the timer task. system_task_add_callback(send_timeout_msg, (void*)(uintptr_t)(session->comm.session_id)); // Set reschedule to false because: 1.) we don't need to reschedule the timer since all // we did was process one that already expired, 2.) it can cause an infinite recursion // because reschedule_ack_timeout() will call check_ack_timeout() (which we are already in) if // any other timers have already expired. update_session_state(session, DataLoggingSessionCommStateIdle, false /*reschedule*/); } return true; } static void check_ack_timeout(void) { RtcTicks current_ticks = rtc_get_ticks(); dls_list_for_each_session(check_ack_timeout_for_session, ¤t_ticks); reschedule_ack_timeout(); } static void ack_timer_cb(void *cb_data) { mutex_lock(s_endpoint_data.mutex); check_ack_timeout(); mutex_unlock(s_endpoint_data.mutex); } static bool find_soonest_ack_timeout_cb(DataLoggingSession *session, void *data) { RtcTicks *soonest_ack_timeout = (RtcTicks*) data; if (session->comm.ack_timeout != 0 && (session->comm.ack_timeout < *soonest_ack_timeout || *soonest_ack_timeout == 0)) { *soonest_ack_timeout = session->comm.ack_timeout; } return true; } static void reschedule_ack_timeout(void) { RtcTicks soonest_ack_timeout = 0; dls_list_for_each_session(find_soonest_ack_timeout_cb, &soonest_ack_timeout); if (soonest_ack_timeout == 0) { // No one is waiting for ack, just stop the timer new_timer_stop(s_endpoint_data.ack_timer); return; } RtcTicks current_ticks = rtc_get_ticks(); if (soonest_ack_timeout < current_ticks) { // Handle the timeout immediately. This will result the in the timer being rescheduled if we're still // waiting for an ack. check_ack_timeout(); return; } // Convert from ticks to ms for the timer RtcTicks ticks_until_timeout = soonest_ack_timeout - current_ticks; uint32_t ms_until_timeout = ((uint64_t) ticks_until_timeout * 1000) / RTC_TICKS_HZ; bool success = new_timer_start(s_endpoint_data.ack_timer, ms_until_timeout, ack_timer_cb, NULL, 0 /*flags*/); PBL_ASSERTN(success); } static void dls_endpoint_print_message(uint8_t *message, int num_bytes) { PBL_ASSERTN(message != NULL); switch (message[0]) { case DataLoggingEndpointCmdClose: { DataLoggingCloseSessionMessage *msg = (DataLoggingCloseSessionMessage *)message; PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Closing session %d", msg->session_id); break; } case DataLoggingEndpointCmdOpen: { DataLoggingOpenSessionMessage *msg = (DataLoggingOpenSessionMessage *)message; PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Opening session %u with tag %"PRIu32", type %u, size %hu", msg->session_id, msg->logging_session_tag, msg->data_item_type, msg->data_item_size); break; } case DataLoggingEndpointCmdData: { DataLoggingSendDataMessage *msg = (DataLoggingSendDataMessage *)message; PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Sending data with session_id %"PRIu8", items remaining %"PRIu32", crc 0x%"PRIx32", num_bytes %d", msg->session_id, msg->items_left_hereafter, msg->crc32, num_bytes); break; } default: PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Message type 0x%x not recognized", message[0]); } } bool dls_endpoint_open_session(DataLoggingSession *session) { CommSession *comm_session = comm_session_get_system_session(); if (!session) { return false; } DataLoggingOpenSessionMessage msg = { .command = DataLoggingEndpointCmdOpen, .session_id = session->comm.session_id, .app_uuid = session->app_uuid, .timestamp = session->session_created_timestamp, .logging_session_tag = session->tag, .data_item_type = session->item_type, .data_item_size = session->item_size, }; dls_endpoint_print_message((uint8_t *)&msg, 0); update_session_state(session, DataLoggingSessionCommStateOpening, true /*reschedule*/); return (comm_session_send_data(comm_session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg, sizeof(DataLoggingOpenSessionMessage), COMM_SESSION_DEFAULT_TIMEOUT)); } void dls_endpoint_close_session(uint8_t session_id) { CommSession *session = comm_session_get_system_session(); if (!session) { return; } DataLoggingCloseSessionMessage msg = { .command = DataLoggingEndpointCmdClose, .session_id = session_id, }; dls_endpoint_print_message((uint8_t *)&msg, 0); comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg, sizeof(DataLoggingCloseSessionMessage), COMM_SESSION_DEFAULT_TIMEOUT); } bool dls_endpoint_send_data(DataLoggingSession *logging_session, const uint8_t *data, unsigned int num_bytes) { if (num_bytes < 1) { // not sending anything return true; } CommSession *session = comm_session_get_system_session(); if (!session) { return false; } mutex_lock(s_endpoint_data.mutex); if (logging_session->comm.state != DataLoggingSessionCommStateIdle) { mutex_unlock(s_endpoint_data.mutex); // logging_session is waiting for an ack, we'll send next time around // don't return a failure, this is pretty innocuous. return true; } const uint32_t total_length = sizeof(DataLoggingSendDataMessage) + num_bytes; const uint32_t timeout_ms = 500; SendBuffer *sb = comm_session_send_buffer_begin_write(session, ENDPOINT_ID_DATA_LOGGING, total_length, timeout_ms); if (!sb) { mutex_unlock(s_endpoint_data.mutex); return false; } analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_ENDPOINT_SENDS, AnalyticsClient_System); const DataLoggingSendDataMessage header = (const DataLoggingSendDataMessage) { .command = DataLoggingEndpointCmdData, .session_id = logging_session->comm.session_id, .items_left_hereafter = 0xffff, // FIXME: logging_session->storage.num_bytes - num_bytes, .crc32 = legacy_defective_checksum_memory(data, num_bytes), }; comm_session_send_buffer_write(sb, (const uint8_t *) &header, sizeof(header)); comm_session_send_buffer_write(sb, data, num_bytes); comm_session_send_buffer_end_write(sb); dls_endpoint_print_message((uint8_t *) &header, num_bytes); DLS_HEXDUMP(data, MIN(num_bytes, 64)); logging_session->comm.num_bytes_pending = num_bytes; update_session_state(logging_session, DataLoggingSessionCommStateSending, true /*reschedule*/); mutex_unlock(s_endpoint_data.mutex); unsigned int data_buffer_length = sizeof(DataLoggingSendDataMessage) + num_bytes; if (!uuid_is_system(&logging_session->app_uuid)) { analytics_inc_for_uuid(ANALYTICS_APP_METRIC_LOG_OUT_COUNT, &logging_session->app_uuid); analytics_add_for_uuid(ANALYTICS_APP_METRIC_LOG_BYTE_OUT_COUNT, data_buffer_length, &logging_session->app_uuid); } return true; } static void prv_dls_endpoint_handle_ack(uint8_t session_id) { DataLoggingSession *session = dls_list_find_by_session_id(session_id); if (session == NULL) { PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_WARNING, "Received ack for non-existent session id: %"PRIu8, session_id); return; } mutex_lock(s_endpoint_data.mutex); PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Received ACK for id: %"PRIu8" state: %u", session->comm.session_id, session->comm.state); switch (session->comm.state) { case DataLoggingSessionCommStateIdle: PBL_LOG(LOG_LEVEL_ERROR, "Unexpected ACK"); break; case DataLoggingSessionCommStateOpening: update_session_state(session, DataLoggingSessionCommStateIdle, true /*reschedule*/); break; case DataLoggingSessionCommStateSending: session->comm.nack_count = 0; update_session_state(session, DataLoggingSessionCommStateIdle, true /*reschedule*/); mutex_unlock(s_endpoint_data.mutex); // unlock for time consuming activities dls_storage_consume(session, session->comm.num_bytes_pending); session->comm.num_bytes_pending = 0; // the bt session is likely already active so continue to flush data dls_private_send_session(session, true); return; } mutex_unlock(s_endpoint_data.mutex); } static void prv_dls_endpoint_handle_nack(uint8_t session_id) { PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Received NACK for id: %"PRIu8, session_id); DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id); if (!logging_session) { PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_WARNING, "Received nack for non-existent session id: %"PRIu8, session_id); return; } mutex_lock(s_endpoint_data.mutex); switch (logging_session->comm.state) { case DataLoggingSessionCommStateIdle: case DataLoggingSessionCommStateOpening: //Currently, these messages never get NACK'd PBL_LOG(LOG_LEVEL_ERROR, "Unexpected NACK"); break; case DataLoggingSessionCommStateSending: //Maybe queue a resend logging_session->comm.num_bytes_pending = 0; if (++logging_session->comm.nack_count > MAX_NACK_COUNT) { PBL_LOG(LOG_LEVEL_ERROR, "Too many nacks. Flushing..."); dls_storage_consume(logging_session, logging_session->storage.num_bytes); analytics_inc(ANALYTICS_DEVICE_METRIC_DATA_LOGGING_FLUSH_COUNT, AnalyticsClient_System); logging_session->comm.nack_count = 0; } break; } update_session_state(logging_session, DataLoggingSessionCommStateIdle, true /*reschedule*/); mutex_unlock(s_endpoint_data.mutex); // reopen the session that was NACK'ed dls_endpoint_open_session(logging_session); } //! System task callback executed which reopens the next session in the list built up by report_cmd_system_task_cb static void prv_reopen_next_session_system_task_cb(void* data) { DataLoggingReopenEntry *entry = (DataLoggingReopenEntry *)data; if (!entry) { s_endpoint_data.report_in_progress = false; return; } DataLoggingReopenEntry *new_head = (DataLoggingReopenEntry *)list_pop_head((ListNode *)entry); // Try and reopen this session bool success = false; if (dls_list_is_session_valid(entry->session) && uuid_equal(&entry->app_uuid, &entry->session->app_uuid) && entry->timestamp == entry->session->session_created_timestamp && entry->tag == entry->session->tag) { PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Reopening session %d", entry->session->comm.session_id); success = (dls_endpoint_open_session(entry->session) && dls_private_send_session(entry->session, false)); } else { // Session has disappeared between the time that the reopen list was // created and now. This ideally shouldn't happen, but there's a lot // that's broken about datalogging. See PBL-37078. success = true; } kernel_free(entry); if (success) { // Schedule next one if (new_head) { bool result = system_task_add_callback(prv_reopen_next_session_system_task_cb, new_head); PBL_ASSERTN(result); } else { s_endpoint_data.report_in_progress = false; } } else { s_endpoint_data.report_in_progress = false; // If we failed, give up on the remaining ones PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Aborting all remaining open requests"); while (new_head) { DataLoggingReopenEntry *entry = new_head; new_head = (DataLoggingReopenEntry *)list_pop_head((ListNode *)new_head); kernel_free(entry); } } } //! For use with dls_list_for_each_session. Appends this session to our list of sesions we need to open. //! On entry, 'data' points to the variable holding the head of the list. static bool dls_endpoint_add_reopen_sessions_cb(DataLoggingSession *session, void *data) { DataLoggingReopenEntry **head_ptr = (DataLoggingReopenEntry **)data; DataLoggingReopenEntry *entry = kernel_malloc_check(sizeof(DataLoggingReopenEntry)); *entry = (DataLoggingReopenEntry) { .session = session, .app_uuid = session->app_uuid, .timestamp = session->session_created_timestamp, .tag = session->tag, }; PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "adding session %d to reopen list", session->comm.session_id); *head_ptr = (DataLoggingReopenEntry *)list_insert_before((ListNode *)(*head_ptr), &entry->list_node); return true; } static void prv_handle_report_cmd(const uint8_t *session_ids, size_t num_sessions) { for (size_t i = 0; i < num_sessions; ++i) { const uint8_t session_id = session_ids[i]; DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id); PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Phone reported session %u opened", session_id); // If the phone thinks we're open and we're not, send a close message. if (logging_session == NULL) { dls_endpoint_close_session(session_id); } } // If the bluetooth connection is flaky, a session reopen could take a few seconds, so we will chain them // and only do 1 re-open per system callback so that we don't trigger a watchdog timeout. DataLoggingReopenEntry *head = NULL; dls_list_for_each_session(dls_endpoint_add_reopen_sessions_cb, (void *)&head); // Re-open the first one and reschedule the next one prv_reopen_next_session_system_task_cb((void *)head); } //! Empty a session by session id static void prv_empty_session(uint8_t session_id) { PBL_LOG_D(LOG_DOMAIN_DATA_LOGGING, LOG_LEVEL_DEBUG, "Phone requested empty of session %u", session_id); DataLoggingSession *logging_session = dls_list_find_by_session_id(session_id); if (logging_session) { dls_private_send_session(logging_session, true /*empty_all_data*/); } } //! data_logging_protocol_msg_callback runs on Bluetooth task. Keep it quick. void data_logging_protocol_msg_callback(CommSession *session, const uint8_t *data, size_t length) { // consume the first byte to read the command uint8_t command = data[0]; --length; // the length now reflects the sizeof the payload // All commands from the phone have their high bit set. if ((command & ~DLS_ENDPOINT_CMD_MASK) == 0) { PBL_LOG(LOG_LEVEL_ERROR, "Invalid data logging endpoint command 0x%x", command); // TODO: send some error code back? return; } switch (command & DLS_ENDPOINT_CMD_MASK) { case (DataLoggingEndpointCmdAck): prv_dls_endpoint_handle_ack(data[1]); break; case (DataLoggingEndpointCmdNack): prv_dls_endpoint_handle_nack(data[1]); break; case (DataLoggingEndpointCmdReport): if (s_endpoint_data.report_in_progress) { PBL_LOG(LOG_LEVEL_INFO, "Report already in progress"); } else { s_endpoint_data.report_in_progress = true; prv_handle_report_cmd(&data[1], length); } break; case (DataLoggingEndpointCmdEmptySession): prv_empty_session(data[1]); break; case (DataLoggingEndpointCmdGetSendEnableReq): { bool enabled = dls_get_send_enable(); struct PACKED { uint8_t command; uint8_t enabled; } msg = { .command = DataLoggingEndpointCmdGetSendEnableRsp, .enabled = enabled, }; comm_session_send_data(session, ENDPOINT_ID_DATA_LOGGING, (uint8_t *)&msg, sizeof(msg), COMM_SESSION_DEFAULT_TIMEOUT); } break; case (DataLoggingEndpointCmdSetSendEnable): dls_set_send_enable_pp(data[1]); break; } } void dls_endpoint_init(void) { s_endpoint_data.mutex = mutex_create(); s_endpoint_data.ack_timer = new_timer_create(); } static bool prv_handle_disconnect_cb(DataLoggingSession *session, void *data) { session->comm.state = DataLoggingSessionCommStateIdle; return true; } void dls_private_handle_disconnect(void *data) { mutex_lock(s_endpoint_data.mutex); dls_list_for_each_session(prv_handle_disconnect_cb, 0); mutex_unlock(s_endpoint_data.mutex); }