/* * 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 "applib/app_smartstrap.h" #include "drivers/accessory.h" #include "kernel/events.h" #include "kernel/pebble_tasks.h" #include "process_management/worker_manager.h" #include "services/common/new_timer/new_timer.h" #include "services/common/system_task.h" #include "services/normal/accessory/accessory_manager.h" #include "services/normal/accessory/smartstrap_comms.h" #include "services/normal/accessory/smartstrap_connection.h" #include "services/normal/accessory/smartstrap_profiles.h" #include "services/normal/accessory/smartstrap_state.h" #include "syscall/syscall.h" #include "syscall/syscall_internal.h" #include "system/logging.h" #include "os/mutex.h" #include "system/passert.h" #include "util/attributes.h" #include "util/crc8.h" #include "util/hdlc.h" #include "util/math.h" #include "util/mbuf_iterator.h" //! The timeout for receiving the context frame after the break characters in ms static const uint32_t NOTIFY_TIMEOUT = 100; static const uint16_t SMARTSTRAP_MAX_TIMEOUT = 1000; //! The header contains the version (1 byte), flags (4 bytes), and profile (2 bytes) fields //! The footer contains the checksum (1 byte) field #define FRAME_FOOTER_LENGTH 1 #define FRAME_MIN_LENGTH (sizeof(FrameHeader) + FRAME_FOOTER_LENGTH) typedef struct PACKED { uint8_t version; union { struct PACKED { uint8_t is_read:1; uint8_t is_master:1; uint8_t is_notify:1; uint32_t reserved:29; }; uint32_t raw; } flags; uint16_t profile; } FrameHeader; typedef struct { //! HDLC context HdlcStreamingContext hdlc_ctx; //! The total number of bytes we've read for this frame uint32_t length; //! A temporary buffer for storing the footer (checksum byte) uint8_t footer_byte; //! The checksum byte (comes after the payload in the frame) uint8_t checksum; //! Flag which is set if we find the frame is invalid bool should_drop; } ReadInfo; typedef struct { //! The profile used for the request SmartstrapProfile profile; //! The MBufIterator to read data into MBufIterator mbuf_iter; } ReadConsumer; typedef union { struct PACKED { bool success; bool is_notify; }; void *context_ptr; } ReadCompleteContext; _Static_assert(sizeof(ReadCompleteContext) == sizeof(void *), "ReadCompleteContext too big"); typedef struct { MBufIterator mbuf_iter; bool is_read; bool sent_escape; uint8_t escaped_byte; } SendInfo; //! Info on the current frame being read static ReadInfo s_read_info; //! The consumer of the next frame which is read static ReadConsumer s_read_consumer; //! MBuf for storing the header when receiving static MBuf s_header_mbuf; static uint8_t s_header_data[sizeof(FrameHeader)]; //! Info on the current frame being sent static SendInfo s_send_info; //! Timer used to enforce read timeouts static TimerID s_read_timer = TIMER_INVALID_ID; // Init //////////////////////////////////////////////////////////////////////////////// void smartstrap_comms_init(void) { s_read_timer = new_timer_create(); s_header_mbuf = MBUF_EMPTY; mbuf_set_data(&s_header_mbuf, s_header_data, sizeof(s_header_data)); } // Helper functions for static variables //////////////////////////////////////////////////////////////////////////////// static void prv_reset_read_info(void) { s_read_info = (ReadInfo) { }; hdlc_streaming_decode_reset(&s_read_info.hdlc_ctx); } static void prv_reset_read_consumer(void) { s_read_consumer = (ReadConsumer) { 0 }; mbuf_iterator_init(&s_read_consumer.mbuf_iter, NULL); } void smartstrap_comms_set_enabled(bool enabled) { if (enabled) { prv_reset_read_info(); prv_reset_read_consumer(); } else { new_timer_stop(s_read_timer); } } // Receive functions //////////////////////////////////////////////////////////////////////////////// static void prv_read_complete_system_task_cb(void *context_ptr) { PBL_ASSERT_TASK(PebbleTask_KernelBackground); ReadCompleteContext context = { .context_ptr = context_ptr }; smartstrap_state_lock(); if (smartstrap_fsm_state_get() != SmartstrapStateReadComplete) { // We could not be in a ReadComplete state if we got disconnected or if we got a complete frame // while the timeout was scheduled. mbuf_clear_next(&s_header_mbuf); smartstrap_state_unlock(); return; } // All other tasks and ISRs will be blocked while we are in the ReadComplete state and while we // hold the state lock, so we're free to access / modify static variables until we transition // the state back to ReadReady. SmartstrapProfile read_profile = s_read_consumer.profile; uint32_t read_length = 0; if (context.success) { if (context.is_notify) { // get the profile from the frame FrameHeader *header = mbuf_get_data(&s_header_mbuf); read_profile = header->profile; } PBL_ASSERTN(s_read_info.length >= FRAME_MIN_LENGTH); read_length = s_read_info.length - FRAME_MIN_LENGTH; // don't care if the timeout is already queued as the FSM state will make it a noop new_timer_stop(s_read_timer); } accessory_use_dma(false); mbuf_clear_next(&s_header_mbuf); prv_reset_read_info(); prv_reset_read_consumer(); smartstrap_fsm_state_set(SmartstrapStateReadReady); smartstrap_state_unlock(); if (context.is_notify) { smartstrap_profiles_handle_notification(context.success, read_profile); } else { smartstrap_profiles_handle_read(context.success, read_profile, read_length); } } static void prv_read_timeout(void *context) { if (smartstrap_fsm_state_test_and_set(SmartstrapStateReadInProgress, SmartstrapStateReadComplete)) { // we need to handle the timeout from KernelBG ReadCompleteContext context = { .success = false, .is_notify = false }; system_task_add_callback(prv_read_complete_system_task_cb, context.context_ptr); } } static void prv_store_byte(const uint8_t data) { // NOTE: THIS IS RUN WITHIN AN ISR // The checksum byte is the last byte in the frame. This byte could be the last byte we receive // (making it the checksum byte), so we always keep a 1 byte temporary buffer before storing the // byte in the MBuf. This avoids us potentially overrunning a conservatively sized payload buffer; if (s_read_info.length > 0) { // copy the previous byte from the footer_byte field into the payload if (!mbuf_iterator_write_byte(&s_read_consumer.mbuf_iter, s_read_info.footer_byte)) { // no room left to store this byte s_read_info.should_drop = true; } } // Store this byte in the footer_byte. Note that we will still calculate the checksum on this byte // and verify that the checksum is 0 at the end, so if this byte is the actual footer byte (aka. // the checksum), we will still include it in the checksum. s_read_info.footer_byte = data; // increment the length and run the CRC calculation s_read_info.length++; crc8_calculate_bytes_streaming((uint8_t *)&data, sizeof(data), (uint8_t *)&s_read_info.checksum, false /* !big_endian */); } static void prv_handle_complete_frame(bool *should_context_switch) { FrameHeader *header = mbuf_get_data(&s_header_mbuf); bool is_notify = header->flags.is_notify; if ((is_notify && (smartstrap_fsm_state_get() != SmartstrapStateNotifyInProgress)) || (!is_notify && (s_read_consumer.profile != header->profile))) { // We either got a notify frame in response to a normal read, or we got a response for a // different frame than we requested. s_read_info.should_drop = true; } if ((s_read_info.should_drop == false) && (header->version > 0) && (header->version <= SMARTSTRAP_PROTOCOL_VERSION) && !header->flags.is_read && !header->flags.is_master && !header->flags.reserved && (header->profile > SmartstrapProfileInvalid) && (header->profile < NumSmartstrapProfiles) && (s_read_info.length >= FRAME_MIN_LENGTH) && !s_read_info.checksum) { // If this is a notification, we shouldn't have a read consumer set. PBL_ASSERTN(!is_notify || (s_read_consumer.profile == SmartstrapProfileInvalid)); // this frame is valid - transition the FSM and queue up processing of it smartstrap_fsm_state_set(SmartstrapStateReadComplete); ReadCompleteContext context = { .success = true, .is_notify = is_notify }; system_task_add_callback_from_isr(prv_read_complete_system_task_cb, context.context_ptr, should_context_switch); } else { // Reset our context so we can try again to receive a frame in case we do happen to get a valid // one before the timeout occurs. prv_reset_read_info(); mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf); } } bool smartstrap_handle_data_from_isr(uint8_t data) { // NOTE: THIS IS RUN WITHIN AN ISR if ((smartstrap_fsm_state_get() != SmartstrapStateReadInProgress) && (smartstrap_fsm_state_get() != SmartstrapStateNotifyInProgress)) { return false; } bool should_context_switch = false; bool hdlc_err; bool should_store; bool is_complete = hdlc_streaming_decode(&s_read_info.hdlc_ctx, &data, &should_store, &hdlc_err); if (hdlc_err) { // the rest of the frame is invalid s_read_info.should_drop = true; } else if (is_complete) { prv_handle_complete_frame(&should_context_switch); } else if (should_store && !s_read_info.should_drop) { prv_store_byte(data); } return should_context_switch; } void prv_notify_timeout(void *context) { if (smartstrap_fsm_state_test_and_set(SmartstrapStateNotifyInProgress, SmartstrapStateReadComplete)) { // we need to handle the timeout from KernelBG ReadCompleteContext context = { .success = false, .is_notify = true }; system_task_add_callback(prv_read_complete_system_task_cb, context.context_ptr); } } void prv_schedule_notify_timeout(void *context) { // make sure there's still a notification pending if (smartstrap_fsm_state_get() == SmartstrapStateNotifyInProgress) { PBL_ASSERTN(new_timer_start(s_read_timer, NOTIFY_TIMEOUT, prv_notify_timeout, NULL, 0)); } } bool smartstrap_handle_break_from_isr(void) { // NOTE: THIS IS RUN WITHIN AN ISR bool should_context_switch = false; // we should only accept notifications if we're in the ReadReady state if (smartstrap_fsm_state_test_and_set(SmartstrapStateReadReady, SmartstrapStateNotifyInProgress)) { // prepare to read notification context PBL_ASSERTN(!mbuf_get_next(&s_header_mbuf)); mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf); system_task_add_callback_from_isr(prv_schedule_notify_timeout, NULL, &should_context_switch); } return should_context_switch; } // Sending functions //////////////////////////////////////////////////////////////////////////////// static bool prv_send_byte_and_check(uint8_t data) { // NOTE: THIS IS RUN WITHIN AN ISR accessory_send_byte(data); const bool bus_contention = accessory_bus_contention_detected(); if (bus_contention) { PBL_LOG(LOG_LEVEL_DEBUG, "Bus contention was detected!"); } return !bus_contention; } static bool prv_send_byte(uint8_t data) { // NOTE: THIS IS RUN WITHIN AN ISR if (hdlc_encode(&data)) { PBL_ASSERTN(!s_send_info.sent_escape); s_send_info.sent_escape = true; s_send_info.escaped_byte = data; data = HDLC_ESCAPE; } return prv_send_byte_and_check(data); } static bool prv_send_stream_callback(void *context) { // NOTE: THIS IS RUN WITHIN AN ISR if (smartstrap_fsm_state_get() != SmartstrapStateReadDisabled) { // we should no longer be sending return false; } // handle escaped bytes first if (s_send_info.sent_escape) { s_send_info.sent_escape = false; return prv_send_byte_and_check(s_send_info.escaped_byte); } // send the next byte bool result = true; MBufIterator *iter = &s_send_info.mbuf_iter; MBuf *mbuf = mbuf_iterator_get_current_mbuf(iter); uint8_t read_data; PBL_ASSERTN(mbuf_iterator_read_byte(iter, &read_data)); if (mbuf_is_flag_set(mbuf, MBUF_FLAG_IS_FRAMING)) { result = prv_send_byte_and_check(read_data); } else { result = prv_send_byte(read_data); } if (mbuf_iterator_is_finished(iter)) { // we just sent the last byte if (s_send_info.is_read) { // We just successfully sent a read request, so should move to ReadInProgress to prepare to // read the response. We do this here to ensure we don't miss any bytes of the response due to // KernelBG not getting scheduled quickly enough. PBL_ASSERTN(!mbuf_get_next(&s_header_mbuf)); mbuf_append(&s_header_mbuf, context); mbuf_iterator_init(&s_read_consumer.mbuf_iter, &s_header_mbuf); smartstrap_fsm_state_set(SmartstrapStateReadInProgress); } result = false; } if (!result) { accessory_enable_input(); } return result; } SmartstrapResult smartstrap_send(SmartstrapProfile profile, MBuf *write_mbuf, MBuf *read_mbuf, uint16_t timeout_ms) { PBL_ASSERT_TASK(PebbleTask_KernelBackground); smartstrap_state_assert_locked_by_current_task(); // we expect the arguments to be valid const bool is_read = (read_mbuf != NULL); PBL_ASSERTN((profile > SmartstrapProfileInvalid) && (profile < NumSmartstrapProfiles)); PBL_ASSERTN(!is_read || (mbuf_get_chain_length(read_mbuf) > 0)); PBL_ASSERTN((!write_mbuf && !read_mbuf) || (write_mbuf != read_mbuf)); timeout_ms = MIN(timeout_ms, SMARTSTRAP_MAX_TIMEOUT); // transition the FSM state if (!smartstrap_fsm_state_test_and_set(SmartstrapStateReadReady, SmartstrapStateReadDisabled)) { PBL_LOG(LOG_LEVEL_WARNING, "Failed to change smartstrap FSM state (%d)", smartstrap_fsm_state_get()); return SmartstrapResultBusy; } // We are now be in a state which allows us to freely modify static variables as we can be sure // that no ISR or other tasks will be allowed to access or modify them while we are in this // state. accessory_disable_input(); // NOTE: Accessory input will be re-enabled by the stream callback after we finish sending prv_reset_read_info(); prv_reset_read_consumer(); s_send_info = (SendInfo) { .is_read = is_read }; if (is_read) { // populate the read consumer info s_read_consumer.profile = profile; } // Go through and build the frame: Start_Flag | Header | Payload | Checksum | End_Flag // Start_Flag uint8_t flag_data = HDLC_FLAG; MBuf start_flag_mbuf = MBUF_EMPTY; mbuf_set_data(&start_flag_mbuf, &flag_data, sizeof(flag_data)); mbuf_set_flag(&start_flag_mbuf, MBUF_FLAG_IS_FRAMING, true); // Header FrameHeader header = (FrameHeader) { .version = SMARTSTRAP_PROTOCOL_VERSION, .flags = { .is_read = is_read, .is_master = true, }, .profile = profile }; MBuf header_mbuf = MBUF_EMPTY; mbuf_set_data(&header_mbuf, &header, sizeof(header)); mbuf_append(&start_flag_mbuf, &header_mbuf); // Payload mbuf_append(&start_flag_mbuf, write_mbuf); // Checksum uint8_t checksum = 0; for (MBuf *m = &header_mbuf; m; m = mbuf_get_next(m)) { if (!mbuf_is_flag_set(m, MBUF_FLAG_IS_FRAMING)) { crc8_calculate_bytes_streaming(m->data, m->length, &checksum, false /* !big_endian */); } } MBuf footer_mbuf = MBUF_EMPTY; mbuf_set_data(&footer_mbuf, &checksum, sizeof(checksum)); mbuf_append(&start_flag_mbuf, &footer_mbuf); // End_Flag MBuf end_flag_mbuf = MBUF_EMPTY; mbuf_set_data(&end_flag_mbuf, &flag_data, sizeof(flag_data)); mbuf_set_flag(&end_flag_mbuf, MBUF_FLAG_IS_FRAMING, true); mbuf_append(&start_flag_mbuf, &end_flag_mbuf); // send off the frame mbuf_iterator_init(&s_send_info.mbuf_iter, &start_flag_mbuf); accessory_use_dma(true); if (!accessory_send_stream(prv_send_stream_callback, (void *)read_mbuf)) { accessory_enable_input(); } if (is_read) { // If we sent the request successfully, the send ISR will have transitioned us out of // ReadDisabled. const bool was_successful = (smartstrap_fsm_state_get() != SmartstrapStateReadDisabled); if (was_successful) { // start the timer for the read timeout PBL_ASSERTN(new_timer_start(s_read_timer, timeout_ms, prv_read_timeout, NULL, 0)); } else { // clean up and return an error accessory_use_dma(false); prv_reset_read_consumer(); smartstrap_fsm_state_set(SmartstrapStateReadReady); return SmartstrapResultBusy; } } else { accessory_use_dma(false); smartstrap_fsm_state_set(SmartstrapStateReadReady); if (!mbuf_iterator_is_finished(&s_send_info.mbuf_iter)) { // The write was not successful, so return an error return SmartstrapResultBusy; } } return SmartstrapResultOk; } void smartstrap_cancel_send(void) { // Enter a critical region to prevent anybody else changing the state. portENTER_CRITICAL(); SmartstrapState state = smartstrap_fsm_state_get(); if ((state != SmartstrapStateReadDisabled) && (state != SmartstrapStateReadInProgress) && (state != SmartstrapStateReadComplete)) { // we aren't in a state where something is in progress, so there's nothing to do portEXIT_CRITICAL(); return; } smartstrap_fsm_state_reset(); new_timer_stop(s_read_timer); smartstrap_profiles_handle_read_aborted(s_read_consumer.profile); prv_reset_read_info(); prv_reset_read_consumer(); mbuf_clear_next(&s_header_mbuf); portEXIT_CRITICAL(); PBL_LOG(LOG_LEVEL_WARNING, "Canceled an in-progress request. Was in state: %d", state); }