/* * 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 "fake_session.h" #include "comm/bt_lock.h" #include "kernel/pbl_malloc.h" #include "system/logging.h" #include "services/common/comm_session/protocol.h" #include "services/common/comm_session/session_send_buffer.h" #include "services/common/system_task.h" #include "util/circular_buffer.h" #include "system/hexdump.h" #include "clar_asserts.h" #include "util/list.h" #include extern void fake_system_task_callbacks_invoke_pending(void); typedef struct CommSession { ListNode node; Transport *transport; const TransportImplementation *transport_imp; bool is_send_next_call_pending; TransportDestination destination; uint8_t *temp_write_buffer; uint16_t endpoint_id; uint16_t bytes_written; uint16_t max_out_payload_length; CircularBuffer send_buffer; uint8_t storage[1024]; } CommSession; static CommSession *s_session_head; static int s_session_close_call_count; static int s_session_open_call_count; bool comm_session_is_valid(const CommSession *session) { return list_contains((ListNode *) s_session_head, &session->node); } static bool prv_find_session_is_system_filter(ListNode *found_node, void *data) { const CommSessionType requested_type = (const bool) (uintptr_t) data; const TransportDestination destination = ((const CommSession *) found_node)->destination; switch (requested_type) { case CommSessionTypeApp: return destination == TransportDestinationApp || destination == TransportDestinationHybrid; case CommSessionTypeSystem: return destination == TransportDestinationSystem || destination == TransportDestinationHybrid; default: return false; } } bool comm_session_has_capability(CommSession *session, CommSessionCapability capability){ return true; } CommSession * comm_session_get_by_type(CommSessionType type) { // TODO: This is not going to fly with multiple app sessions CommSession *session; bt_lock(); { session = (CommSession *) list_find((ListNode *) s_session_head, prv_find_session_is_system_filter, (void *) (uintptr_t) type); } bt_unlock(); return session; } CommSession* comm_session_get_system_session(void) { // TODO: What if Pebble App is connected via iSPP *and* PPoGATT ? return comm_session_get_by_type(CommSessionTypeSystem); } CommSession* comm_session_get_current_app_session(void) { // TODO: What if App is connected via iSPP *and* PPoGATT ? return comm_session_get_by_type(CommSessionTypeApp); } void comm_session_close(CommSession *session, CommSessionCloseReason reason) { cl_assert(list_contains((const ListNode *) s_session_head, &session->node)); if (session->temp_write_buffer) { kernel_free(session->temp_write_buffer); } list_remove(&session->node, (ListNode **) &s_session_head, NULL); kernel_free(session); ++s_session_close_call_count; } void comm_session_receive_router_write(CommSession *session, const uint8_t *received_data, size_t num_bytes_to_copy) { PBL_LOG(LOG_LEVEL_DEBUG, "Received Data:"); PBL_HEXDUMP(LOG_LEVEL_DEBUG, received_data, num_bytes_to_copy); } bool comm_session_send_data(CommSession *session, uint16_t endpoint_id, const uint8_t *data, size_t length, uint32_t timeout_ms) { SendBuffer *sb = comm_session_send_buffer_begin_write(session, endpoint_id, length, timeout_ms); if (!sb) { return false; } comm_session_send_buffer_write(sb, data, length); comm_session_send_buffer_end_write(sb); return true; } CommSession * comm_session_open(Transport *transport, const TransportImplementation *implementation, TransportDestination destination) { ++s_session_open_call_count; CommSession *session = kernel_malloc(sizeof(CommSession)); memset(session, 0, sizeof(*session)); *session = (const CommSession) { .transport = transport, .transport_imp = implementation, .destination = destination, .max_out_payload_length = COMM_MAX_OUTBOUND_PAYLOAD_SIZE, }; const size_t max_pp_msg_size = session->max_out_payload_length + sizeof(PebbleProtocolHeader); // If this fails, you need to bump up the size of the storage[] array in the fake CommSession cl_assert(sizeof(session->storage) >= max_pp_msg_size); circular_buffer_init(&session->send_buffer, session->storage, max_pp_msg_size); s_session_head = (CommSession *) list_prepend((ListNode *) s_session_head, &session->node); return session; } size_t comm_session_send_queue_get_length(const CommSession *session) { cl_assert(list_contains((const ListNode *) s_session_head, &session->node)); return circular_buffer_get_read_space_remaining(&session->send_buffer); } size_t comm_session_send_queue_copy(CommSession *session, uint32_t start_off, size_t length, uint8_t *data_out) { cl_assert(data_out); cl_assert(list_contains((const ListNode *) s_session_head, &session->node)); return circular_buffer_copy_offset(&session->send_buffer, start_off, data_out, length); } void comm_session_send_queue_consume(CommSession *session, size_t length) { circular_buffer_consume(&session->send_buffer, length); } static void prv_send_next_kernel_bg_cb(void *data) { CommSession *session = (CommSession *) data; if (!list_contains((const ListNode *) s_session_head, (const ListNode *) session)) { // Session closed in the mean time return; } // Flip the flag before the send_next callback, so it can schedule again if needed. session->is_send_next_call_pending = false; // Kick the transport to send out the next bytes from the send buffer const size_t read_space = comm_session_send_queue_get_length(session); if (read_space) { session->transport_imp->send_next(session->transport); } } void comm_session_send_next(CommSession *session) { if (session->is_send_next_call_pending) { return; } system_task_add_callback(prv_send_next_kernel_bg_cb, session); session->is_send_next_call_pending = true; } bool prv_filter_by_transport_callback(ListNode *node, void *data) { return (((CommSession *) node)->transport == data); } CommSession * prv_find_session_by_transport(Transport *transport) { return (CommSession *) list_find((ListNode *) s_session_head, prv_filter_by_transport_callback, transport); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Send buffer fakes size_t comm_session_send_buffer_get_max_payload_length(const CommSession *session) { size_t max_length = 0; if (comm_session_is_valid(session)) { max_length = session->send_buffer.buffer_size - sizeof(PebbleProtocolHeader); } return max_length; } SendBuffer * comm_session_send_buffer_begin_write(CommSession *session, uint16_t endpoint_id, size_t required_free_length, uint32_t timeout_ms) { if (!session) { return NULL; } if (!comm_session_is_valid(session)) { return NULL; } if (required_free_length + sizeof(PebbleProtocolHeader) > circular_buffer_get_write_space_remaining(&session->send_buffer)) { return NULL; } if (session->temp_write_buffer) { // Already writing, fake doesn't support multiple tasks trying to write at the same time return NULL; } session->temp_write_buffer = (uint8_t *) kernel_malloc(session->max_out_payload_length); session->bytes_written = 0; session->endpoint_id = endpoint_id; return (SendBuffer *) session; } bool comm_session_send_buffer_write(SendBuffer *sb, const uint8_t *data, size_t length) { CommSession *session = (CommSession *) sb; cl_assert(session); cl_assert(session->temp_write_buffer); cl_assert(length + session->bytes_written <= session->max_out_payload_length); memcpy(session->temp_write_buffer + session->bytes_written, data, length); session->bytes_written += length; return true; } void comm_session_send_buffer_end_write(SendBuffer *sb) { CommSession *session = (CommSession *) sb; cl_assert(session); cl_assert(session->temp_write_buffer); const PebbleProtocolHeader pp_header = { .length = session->bytes_written, .endpoint_id = session->endpoint_id, }; circular_buffer_write(&session->send_buffer, (const uint8_t *) &pp_header, sizeof(pp_header)); circular_buffer_write(&session->send_buffer, session->temp_write_buffer, session->bytes_written); kernel_free(session->temp_write_buffer); session->temp_write_buffer = NULL; session->endpoint_id = ~0; session->bytes_written = 0; } static uint32_t s_responsiveness_max_period_s; static bool s_responsiveness_latency_is_reduced; static ResponsivenessGrantedHandler s_last_responsiveness_granted_handler; void comm_session_set_responsiveness( CommSession *session, BtConsumer consumer, ResponseTimeState state, uint16_t max_period_secs) { comm_session_set_responsiveness_ext(session, consumer, state, max_period_secs, NULL); } void comm_session_set_responsiveness_ext(CommSession *session, BtConsumer consumer, ResponseTimeState state, uint16_t max_period_secs, ResponsivenessGrantedHandler granted_handler) { s_responsiveness_max_period_s = max_period_secs; if (state == ResponseTimeMiddle) { s_responsiveness_latency_is_reduced = true; } else if (state == ResponseTimeMax) { s_responsiveness_latency_is_reduced = false; } s_last_responsiveness_granted_handler = granted_handler; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Session related functions ResponsivenessGrantedHandler fake_comm_session_get_last_responsiveness_granted_handler(void) { return s_last_responsiveness_granted_handler; } int fake_comm_session_open_call_count(void) { return s_session_open_call_count; } int fake_comm_session_close_call_count(void) { return s_session_close_call_count; } void fake_comm_session_process_send_next(void) { CommSession *session = s_session_head; while (session) { CommSession *next = (CommSession *) session->node.next; comm_session_send_next(session); session = next; } fake_system_task_callbacks_invoke_pending(); } uint32_t fake_comm_session_get_responsiveness_max_period(void) { return s_responsiveness_max_period_s; } uint32_t fake_comm_session_is_latency_reduced(void) { return s_responsiveness_latency_is_reduced; } //////////////////////////////////////////////////////////////////////////////////////////////////// // Transport mock typedef struct { ListNode node; uint16_t endpoint_id; size_t length; uint8_t data[]; } DataNode; typedef struct { ListNode node; TransportDestination destination; FakeTransportSentCallback sent_cb; Uuid app_uuid; CommSession *session; //! When no sent_cb is used, data is appended to this list DataNode *sent_data; } FakeTransport; static FakeTransport *s_fake_transport_head; static void prv_fake_transport_send_next(Transport *transport) { FakeTransport *fake_transport = (FakeTransport *) transport; cl_assert_equal_b(list_contains((const ListNode *) s_fake_transport_head, (const ListNode *) fake_transport), true); CommSession *session = fake_transport->session; PebbleProtocolHeader pp_header; uint8_t *buffer = kernel_malloc(1024); while (circular_buffer_copy(&session->send_buffer, (uint8_t *) &pp_header, sizeof(pp_header)) == sizeof(pp_header)) { circular_buffer_copy_offset(&session->send_buffer, sizeof(pp_header), buffer, pp_header.length); if (fake_transport->sent_cb) { fake_transport->sent_cb(pp_header.endpoint_id, buffer, pp_header.length); } else { PBL_LOG(LOG_LEVEL_DEBUG, "Sending Data to PP endpoint %u (0x%x):", pp_header.endpoint_id, pp_header.endpoint_id); PBL_HEXDUMP(LOG_LEVEL_DEBUG, buffer, pp_header.length); DataNode *data_node = kernel_malloc(sizeof(DataNode) + pp_header.length); list_init(&data_node->node); data_node->endpoint_id = pp_header.endpoint_id; data_node->length = pp_header.length; memcpy(data_node->data, buffer, pp_header.length); fake_transport->sent_data = (DataNode *) list_prepend(&fake_transport->sent_data->node, &data_node->node); } circular_buffer_consume(&session->send_buffer, sizeof(pp_header) + pp_header.length); } kernel_free(buffer); } static void prv_fake_transport_reset(Transport *transport) { cl_assert_(false, "Not implemented: prv_fake_transport_reset"); } static const TransportImplementation s_fake_transport_implementation = { .send_next = prv_fake_transport_send_next, .reset = prv_fake_transport_reset, }; Transport *fake_transport_create(TransportDestination destination, const Uuid *app_uuid, FakeTransportSentCallback sent_cb) { if (app_uuid == NULL) { cl_assert_(TransportDestinationSystem == destination || TransportDestinationHybrid == TransportDestinationSystem, "When passing NULL app_uuid, the destination can only be System or Hybrid"); } else { cl_assert_(TransportDestinationSystem == destination || TransportDestinationHybrid == TransportDestinationSystem, "When passing an app_uuid, the destination can only be App or Hybrid"); } FakeTransport *transport = (FakeTransport *) kernel_malloc(sizeof(FakeTransport)); *transport = (const FakeTransport) { .destination = destination, .sent_cb = sent_cb, }; if (app_uuid) { transport->app_uuid = *app_uuid; } s_fake_transport_head = (FakeTransport *) list_prepend((ListNode *) s_fake_transport_head, &transport->node); return (Transport *) transport; } CommSession *fake_transport_set_connected(Transport *transport, bool connected) { FakeTransport *fake_transport = (FakeTransport *) transport; if (connected) { cl_assert_equal_p(fake_transport->session, NULL); fake_transport->session = comm_session_open(transport, &s_fake_transport_implementation, fake_transport->destination); return fake_transport->session; } else { cl_assert(fake_transport->session); comm_session_close(fake_transport->session, 0); fake_transport->session = NULL; return NULL; } } void fake_transport_set_sent_cb(Transport *transport, FakeTransportSentCallback sent_cb) { cl_assert(transport); FakeTransport *fake_transport = (FakeTransport *) transport; fake_transport->sent_cb = sent_cb; } void fake_transport_assert_sent(Transport *transport, uint16_t index, uint16_t endpoint_id, const uint8_t data[], size_t length) { cl_assert(transport); FakeTransport *fake_transport = (FakeTransport *)transport; DataNode *data_node = fake_transport->sent_data; for (uint16_t i = 0; i <= index; ++i) { cl_assert_(data_node, "Sent out too few packets"); if (i == index) { cl_assert_equal_i(data_node->endpoint_id, endpoint_id); cl_assert_equal_i(data_node->length, length); cl_assert_equal_m(data_node->data, data, length); } data_node = (DataNode *) data_node->node.next; } } void fake_transport_assert_nothing_sent(Transport *transport) { cl_assert(transport); FakeTransport *fake_transport = (FakeTransport *)transport; DataNode *data_node = fake_transport->sent_data; cl_assert_equal_p(data_node, NULL); } void fake_transport_destroy(Transport *transport) { FakeTransport *fake_transport = (FakeTransport *) transport; cl_assert(transport); cl_assert_equal_b(list_contains((const ListNode *)s_fake_transport_head, (const ListNode *)fake_transport), true); if (fake_transport->session) { // Causes clean up of CommSession: fake_transport_set_connected((Transport *)fake_transport, false /* connected */); } list_remove((ListNode *) fake_transport, (ListNode **) &s_fake_transport_head, NULL); DataNode *data_node = fake_transport->sent_data; while (data_node) { DataNode *next_data_node = (DataNode *)data_node->node.next; kernel_free(data_node); data_node = next_data_node; } kernel_free(fake_transport); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Transport helper functions bool fake_comm_session_send_buffer_write_raw_by_transport(Transport *transport, const uint8_t *data, size_t length) { CommSession *session = prv_find_session_by_transport(transport); cl_assert(session); return circular_buffer_write(&session->send_buffer, data, length); } //////////////////////////////////////////////////////////////////////////////////////////////////// // Fake life cycle void fake_comm_session_init(void) { cl_assert_(s_fake_transport_head == NULL, "Didn't clean up the fake transports? \ Call fake_comm_session_cleanup() if you don't want to clean them up manually."); s_session_close_call_count = 0; s_session_open_call_count = 0; s_last_responsiveness_granted_handler = NULL; } void fake_comm_session_cleanup(void) { FakeTransport *fake_transport = s_fake_transport_head; while (fake_transport) { FakeTransport *next = (FakeTransport *) fake_transport->node.next; fake_transport_destroy((Transport *) fake_transport); fake_transport = next; } }