diff --git a/Friend/firmware/firmware_v1.0/src/button.c b/Friend/firmware/firmware_v1.0/src/button.c index 0d729e569..5e39941e0 100644 --- a/Friend/firmware/firmware_v1.0/src/button.c +++ b/Friend/firmware/firmware_v1.0/src/button.c @@ -9,6 +9,8 @@ #include "button.h" #include "transport.h" #include "speaker.h" +#include "codec.h" +#include "storage.h" LOG_MODULE_REGISTER(button, CONFIG_LOG_DEFAULT_LEVEL); bool is_off = false; @@ -27,7 +29,13 @@ static struct bt_gatt_attr button_service_attr[] = { static struct bt_gatt_service button_service = BT_GATT_SERVICE(button_service_attr); -static void button_ccc_config_changed_handler(const struct bt_gatt_attr *attr, uint16_t value) +static inline void notify_tap(void); +static inline void notify_press(void); +static inline void notify_unpress(void); +static inline void notify_double_tap(void); +static inline void notify_long_tap(void); + +static void button_ccc_config_changed_handler(const struct bt_gatt_attr *attr, uint16_t value) { if (value == BT_GATT_CCC_NOTIFY) { @@ -43,8 +51,8 @@ static void button_ccc_config_changed_handler(const struct bt_gatt_attr *attr, u } } -struct gpio_dt_spec d4_pin = {.port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), .pin=4, .dt_flags = GPIO_OUTPUT_ACTIVE}; //3.3 -struct gpio_dt_spec d5_pin_input = {.port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), .pin=5, .dt_flags = GPIO_INT_EDGE_RISING}; +struct gpio_dt_spec d4_pin = {.port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), .pin=4, .dt_flags = GPIO_OUTPUT}; +struct gpio_dt_spec d5_pin_input = { .port = DEVICE_DT_GET(DT_NODELABEL(gpio0)), .pin = 5, .dt_flags = GPIO_INPUT | GPIO_INT_EDGE_BOTH }; static uint32_t current_button_time = 0; static uint32_t previous_button_time = 0; @@ -52,6 +60,9 @@ static uint32_t previous_button_time = 0; const int max_debounce_interval = 700; static bool was_pressed = false; +// Add voice interaction state to button FSM +static bool voice_capture_active = false; + // // button // @@ -83,15 +94,6 @@ void check_button_level(struct k_work *work_item); K_WORK_DELAYABLE_DEFINE(button_work, check_button_level); -#define DEFAULT_STATE 0 -#define SINGLE_TAP 1 -#define DOUBLE_TAP 2 -#define LONG_TAP 3 -#define BUTTON_PRESS 4 -#define BUTTON_RELEASE 5 - - -// 4 is button down, 5 is button up static FSM_STATE_T current_button_state = IDLE; static uint32_t inc_count_1 = 0; static uint32_t inc_count_0 = 0; @@ -115,13 +117,16 @@ static inline void notify_press() } } -static inline void notify_unpress() -{ - final_button_state[0] = BUTTON_RELEASE; - LOG_INF("unpressed"); +static inline void notify_unpress() { + if (voice_interaction_active) { + LOG_INF("Button released, stopping voice interaction"); + stop_voice_interaction(); + k_msleep(10); // Give time for cleanup + } + + final_button_state[0] = BUTTON_RELEASE; struct bt_conn *conn = get_current_connection(); - if (conn != NULL) - { + if (conn != NULL) { bt_gatt_notify(conn, &button_service.attrs[1], &final_button_state, sizeof(final_button_state)); } } @@ -129,7 +134,7 @@ static inline void notify_unpress() static inline void notify_tap() { final_button_state[0] = SINGLE_TAP; - LOG_INF("tap"); + LOG_INF("single tap"); struct bt_conn *conn = get_current_connection(); if (conn != NULL) { @@ -137,26 +142,66 @@ static inline void notify_tap() } } -static inline void notify_double_tap() -{ - final_button_state[0] = DOUBLE_TAP; //button press - LOG_INF("double tap"); - struct bt_conn *conn = get_current_connection(); - if (conn != NULL) - { - bt_gatt_notify(conn, &button_service.attrs[1], &final_button_state, sizeof(final_button_state)); +static inline void notify_double_tap() { + // Only start voice interaction if device is not in sleep mode + if (!is_off) { + final_button_state[0] = DOUBLE_TAP; + LOG_INF("Double tap detected"); + + struct bt_conn *conn = get_current_connection(); + if (conn != NULL) { + bt_gatt_notify(conn, &button_service.attrs[1], &final_button_state, sizeof(final_button_state)); + + if (!voice_interaction_active) { + // Start voice interaction + start_voice_interaction(); + k_msleep(10); // Give time for state change + } else { + // Stop if already active + stop_voice_interaction(); + k_msleep(10); + } + } } + + // Reset state + current_button_state = GRACE; + reset_count(); } -static inline void notify_long_tap() -{ - final_button_state[0] = LONG_TAP; //button press - LOG_INF("long tap"); +static inline void notify_long_tap() { + // If voice interaction is active, stop it before sleep + if (voice_interaction_active) { + LOG_INF("Stopping voice interaction before sleep"); + stop_voice_interaction(); + } + + final_button_state[0] = LONG_TAP; + LOG_INF("long tap - toggling sleep mode"); + struct bt_conn *conn = get_current_connection(); - if (conn != NULL) - { + if (conn != NULL) { bt_gatt_notify(conn, &button_service.attrs[1], &final_button_state, sizeof(final_button_state)); } + + // Handle sleep mode + is_off = !is_off; + play_haptic_milli(100); + + if (is_off) { + LOG_INF("Entering sleep mode"); + bt_disable(); + int err = bt_le_adv_stop(); + if (err) { + LOG_ERR("Failed to stop Bluetooth %d", err); + } + } else { + int err = bt_enable(NULL); + if (err) { + LOG_ERR("Failed to enable Bluetooth %d", err); + } + bt_on(); + } } #define LONG_PRESS_INTERVAL 25 @@ -307,6 +352,9 @@ void check_button_level(struct k_work *work_item) if (inc_count_0 == 0 && (inc_count_1 > 0)) { notify_unpress(); + + // End voice interaction if it was active + stop_voice_interaction(); } inc_count_0++; if (inc_count_0 > 1) @@ -362,7 +410,7 @@ int button_init() return -1; } - int err2 = gpio_pin_configure_dt(&d5_pin_input,GPIO_INPUT); + int err2 = gpio_pin_configure_dt(&d5_pin_input, GPIO_INPUT | GPIO_INT_EDGE_BOTH); if (err2 != 0) { @@ -405,4 +453,4 @@ void register_button_service() FSM_STATE_T get_current_button_state() { return current_button_state; -} \ No newline at end of file +} diff --git a/Friend/firmware/firmware_v1.0/src/button.h b/Friend/firmware/firmware_v1.0/src/button.h index 7108889b0..3f3c0398c 100644 --- a/Friend/firmware/firmware_v1.0/src/button.h +++ b/Friend/firmware/firmware_v1.0/src/button.h @@ -1,8 +1,16 @@ #ifndef BUTTON_H #define BUTTON_H +// Button states +#define DEFAULT_STATE 0 +#define SINGLE_TAP 1 // Quick press and release +#define DOUBLE_TAP 2 // Two quick presses - Currently used for voice interaction +#define LONG_TAP 3 // Long press - Currently used for sleep/wake +#define BUTTON_PRESS 4 // Button down event +#define BUTTON_RELEASE 5 // Button up event + typedef enum { - IDLE, + IDLE, ONE_PRESS, TWO_PRESS, GRACE @@ -11,7 +19,6 @@ typedef enum { int button_init(); void activate_button_work(); void register_button_service(); - FSM_STATE_T get_current_button_state(); -#endif \ No newline at end of file +#endif diff --git a/Friend/firmware/firmware_v1.0/src/codec.c b/Friend/firmware/firmware_v1.0/src/codec.c index c99d0f71c..8f89702dc 100644 --- a/Friend/firmware/firmware_v1.0/src/codec.c +++ b/Friend/firmware/firmware_v1.0/src/codec.c @@ -62,6 +62,21 @@ static uint8_t m_opus_encoder[OPUS_ENCODER_SIZE]; static OpusEncoder *const m_opus_state = (OpusEncoder *)m_opus_encoder; #endif +// Add voice mode configuration +static bool voice_mode = false; + +void set_voice_mode(bool enabled) { + voice_mode = enabled; + if (enabled) { + // Configure OPUS for voice settings + opus_encoder_ctl(m_opus_state, OPUS_SET_BITRATE(24000)); + opus_encoder_ctl(m_opus_state, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE)); + } else { + // Restore normal settings + opus_encoder_ctl(m_opus_state, OPUS_SET_BITRATE(CODEC_OPUS_BITRATE)); + } +} + void codec_entry() { diff --git a/Friend/firmware/firmware_v1.0/src/codec.h b/Friend/firmware/firmware_v1.0/src/codec.h index bb542dd0f..fc07207ba 100644 --- a/Friend/firmware/firmware_v1.0/src/codec.h +++ b/Friend/firmware/firmware_v1.0/src/codec.h @@ -1,9 +1,19 @@ #ifndef CODEC_H #define CODEC_H -#include + +#include +#include +#include +#include + +// Expose codec ring buffer for voice interaction +extern struct ring_buf codec_ring_buf; + +// Voice mode functions +void set_voice_mode(bool enabled); // Callback -typedef void (*codec_callback)(uint8_t *data, size_t len); +typedef void (*codec_callback)(uint8_t *data, size_t size); void set_codec_callback(codec_callback callback); // Integration diff --git a/Friend/firmware/firmware_v1.0/src/mic.c b/Friend/firmware/firmware_v1.0/src/mic.c index 3cf951a4e..04abbe1b5 100644 --- a/Friend/firmware/firmware_v1.0/src/mic.c +++ b/Friend/firmware/firmware_v1.0/src/mic.c @@ -97,7 +97,41 @@ int mic_start() return 0; } -void set_mic_callback(mix_handler callback) +void set_mic_callback(mix_handler callback) { _callback = callback; } + +// Add a public function to be called from transport.c +int mic_configure_for_voice(void) { + // Stop PDM first + nrfx_pdm_stop(); + k_msleep(10); // Give hardware time to stop + + // Uninit current config + nrfx_pdm_uninit(); + k_msleep(10); + + // Configure for voice + nrfx_pdm_config_t voice_config = NRFX_PDM_DEFAULT_CONFIG(PDM_CLK_PIN, PDM_DIN_PIN); + voice_config.gain_l = VOICE_GAIN; + voice_config.gain_r = VOICE_GAIN; + voice_config.clock_freq = NRF_PDM_FREQ_1032K; + voice_config.mode = NRF_PDM_MODE_MONO; + voice_config.edge = NRF_PDM_EDGE_LEFTFALLING; + + // Initialize with new config + if (nrfx_pdm_init(&voice_config, pdm_irq_handler) != NRFX_SUCCESS) { + LOG_ERR("Failed to initialize PDM for voice"); + return -1; + } + + // Start PDM + if (nrfx_pdm_start() != NRFX_SUCCESS) { + LOG_ERR("Failed to start PDM for voice"); + nrfx_pdm_uninit(); + return -1; + } + + return 0; +} diff --git a/Friend/firmware/firmware_v1.0/src/mic.h b/Friend/firmware/firmware_v1.0/src/mic.h index 33865c77a..de2052e19 100644 --- a/Friend/firmware/firmware_v1.0/src/mic.h +++ b/Friend/firmware/firmware_v1.0/src/mic.h @@ -1,16 +1,17 @@ #ifndef MIC_H #define MIC_H -typedef void (*mix_handler)(int16_t *); +#include -/** - * @brief Initialize the Microphone - * - * Initializes the Microphone - * - * @return 0 if successful, negative errno code if error - */ -int mic_start(); -void set_mic_callback(mix_handler _callback); +// Add voice configuration +#define VOICE_GAIN 0x50 // Adjusted gain for voice capture -#endif \ No newline at end of file +// Existing declarations... +typedef void (*mix_handler)(int16_t *data); +void set_mic_callback(mix_handler callback); +int mic_start(void); + +// Change return type to int +int mic_configure_for_voice(void); + +#endif diff --git a/Friend/firmware/firmware_v1.0/src/speaker.c b/Friend/firmware/firmware_v1.0/src/speaker.c index 746395319..a21ea998e 100644 --- a/Friend/firmware/firmware_v1.0/src/speaker.c +++ b/Friend/firmware/firmware_v1.0/src/speaker.c @@ -36,7 +36,13 @@ static uint16_t offset; struct gpio_dt_spec haptic_gpio_pin = {.port = DEVICE_DT_GET(DT_NODELABEL(gpio1)), .pin=11, .dt_flags = GPIO_INT_DISABLE}; -int speaker_init() +#define STREAM_BUFFER_SIZE 8192 +static uint8_t stream_buffer[STREAM_BUFFER_SIZE]; +static size_t stream_buffer_pos = 0; + +extern bool is_off; + +int speaker_init() { LOG_INF("Speaker init"); audio_speaker = device_get_binding("I2S_0"); @@ -84,10 +90,16 @@ int speaker_init() uint16_t speak(uint16_t len, const void *buf) //direct from bt { - uint16_t amount = 0; + // Don't process audio if device is sleeping + if (is_off) { + return 0; + } + + uint16_t amount = 0; amount = len; - if (len == 4) //if stage 1 - { + + if (len == 4) //if stage 1 + { current_length = ((uint32_t *)buf)[0]; LOG_INF("About to write %u bytes", current_length); ptr2 = (int16_t *)rx_buffer; @@ -149,6 +161,47 @@ uint16_t speak(uint16_t len, const void *buf) //direct from bt return amount; } +// Add new function for streaming audio +uint16_t speak_stream(uint16_t len, const void *buf) { + if (is_off) { + LOG_WRN("Device is off, cannot play audio"); + return 0; + } + + uint16_t amount = len; + LOG_DBG("Processing stream data: %d bytes", len); + + // Handle streaming audio + if (stream_buffer_pos + len > STREAM_BUFFER_SIZE) { + LOG_INF("Buffer full (%d bytes), playing current data", stream_buffer_pos); + int res = i2s_write(audio_speaker, stream_buffer, stream_buffer_pos); + if (res < 0) { + LOG_ERR("Failed to write stream data: %d", res); + } + i2s_trigger(audio_speaker, I2S_DIR_TX, I2S_TRIGGER_START); + i2s_trigger(audio_speaker, I2S_DIR_TX, I2S_TRIGGER_DRAIN); + stream_buffer_pos = 0; + } + + // Add new data to buffer + LOG_DBG("Adding %d bytes to stream buffer at position %d", len, stream_buffer_pos); + memcpy(stream_buffer + stream_buffer_pos, buf, len); + stream_buffer_pos += len; + + // If we have enough data or this is the end, play it + if (stream_buffer_pos >= 1024 || len < 400) { + LOG_INF("Playing %d bytes of audio data", stream_buffer_pos); + int res = i2s_write(audio_speaker, stream_buffer, stream_buffer_pos); + if (res < 0) { + LOG_ERR("Failed to write stream data: %d", res); + } + i2s_trigger(audio_speaker, I2S_DIR_TX, I2S_TRIGGER_START); + i2s_trigger(audio_speaker, I2S_DIR_TX, I2S_TRIGGER_DRAIN); + stream_buffer_pos = 0; + } + + return amount; +} void generate_gentle_chime(int16_t *buffer, int num_samples) { diff --git a/Friend/firmware/firmware_v1.0/src/speaker.h b/Friend/firmware/firmware_v1.0/src/speaker.h index c67887524..79a5ae02a 100644 --- a/Friend/firmware/firmware_v1.0/src/speaker.h +++ b/Friend/firmware/firmware_v1.0/src/speaker.h @@ -50,4 +50,17 @@ int init_haptic_pin(); * @return a sound hopefully */ void play_haptic_milli(uint32_t duration); -#endif \ No newline at end of file + +/** + * @brief Endpoint function for streaming audio + * + * Call this function in the following way (Via ble) + * 1. Send a 2 byte packet containing the audio data size + * 2. Send to the ble notify id 400 byte packets (with notify), with each 2 bytes being the audio data + * 3. Repeat step 2 until the audio data is sent. Then the speaker will automatically play the sound + * when the audio data sent is equal to the audio data size sent in step 1 + * + * @return The amount of data successfully sent in bytes. + */ +uint16_t speak_stream(uint16_t len, const void *buf); +#endif diff --git a/Friend/firmware/firmware_v1.0/src/transport.c b/Friend/firmware/firmware_v1.0/src/transport.c index b1bbb9344..1bccda20c 100644 --- a/Friend/firmware/firmware_v1.0/src/transport.c +++ b/Friend/firmware/firmware_v1.0/src/transport.c @@ -21,8 +21,16 @@ #include "button.h" #include "lib/battery/battery.h" // #include "friend.h" +#include "mic.h" +#include "codec.h" +#include + LOG_MODULE_REGISTER(transport, CONFIG_LOG_DEFAULT_LEVEL); +#define STREAM_BUFFER_SIZE 8192 +static uint8_t stream_buffer[STREAM_BUFFER_SIZE]; +static size_t stream_buffer_pos = 0; + extern bool is_connected; extern bool storage_is_on; extern uint8_t file_count; @@ -45,6 +53,10 @@ static ssize_t audio_codec_read_characteristic(struct bt_conn *conn, const struc static void dfu_ccc_config_changed_handler(const struct bt_gatt_attr *attr, uint16_t value); static ssize_t dfu_control_point_write_handler(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags); +static ssize_t voice_interaction_write_handler(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, uint16_t offset, uint8_t flags); +static const char *phy2str(uint8_t phy); + + // // Service and Characteristic // @@ -59,16 +71,47 @@ static struct bt_uuid_128 audio_characteristic_data_uuid = BT_UUID_INIT_128(BT_U static struct bt_uuid_128 audio_characteristic_format_uuid = BT_UUID_INIT_128(BT_UUID_128_ENCODE(0x19B10002, 0xE8F2, 0x537E, 0x4F6C, 0xD104768A1214)); static struct bt_uuid_128 audio_characteristic_speaker_uuid = BT_UUID_INIT_128(BT_UUID_128_ENCODE(0x19B10003, 0xE8F2, 0x537E, 0x4F6C, 0xD104768A1214)); +static struct bt_uuid_128 voice_interaction_uuid = BT_UUID_INIT_128(BT_UUID_128_ENCODE(0x19B10004, 0xE8F2, 0x537E, 0x4F6C, 0xD104768A1214)); +static struct bt_uuid_128 voice_interaction_rx_uuid = BT_UUID_INIT_128(BT_UUID_128_ENCODE(0x19B10005, 0xE8F2, 0x537E, 0x4F6C, 0xD104768A1214)); + static struct bt_gatt_attr audio_service_attr[] = { BT_GATT_PRIMARY_SERVICE(&audio_service_uuid), - BT_GATT_CHARACTERISTIC(&audio_characteristic_data_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ, audio_data_read_characteristic, NULL, NULL), - BT_GATT_CCC(audio_ccc_config_changed_handler, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), - BT_GATT_CHARACTERISTIC(&audio_characteristic_format_uuid.uuid, BT_GATT_CHRC_READ, BT_GATT_PERM_READ, audio_codec_read_characteristic, NULL, NULL), + BT_GATT_CHARACTERISTIC(&audio_characteristic_data_uuid.uuid, + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ, + audio_data_read_characteristic, + NULL, + NULL), + BT_GATT_CCC(audio_ccc_config_changed_handler, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + BT_GATT_CHARACTERISTIC(&audio_characteristic_format_uuid.uuid, + BT_GATT_CHRC_READ, + BT_GATT_PERM_READ, + audio_codec_read_characteristic, + NULL, + NULL), #ifdef CONFIG_ENABLE_SPEAKER - BT_GATT_CHARACTERISTIC(&audio_characteristic_speaker_uuid.uuid, BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_WRITE, NULL, audio_data_write_handler, NULL), - BT_GATT_CCC(audio_ccc_config_changed_handler, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), // + BT_GATT_CHARACTERISTIC(&audio_characteristic_speaker_uuid.uuid, + BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_WRITE, + NULL, + audio_data_write_handler, + NULL), + BT_GATT_CCC(audio_ccc_config_changed_handler, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), #endif - + + BT_GATT_CHARACTERISTIC(&voice_interaction_uuid.uuid, + BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ, + NULL, NULL, NULL), + BT_GATT_CCC(audio_ccc_config_changed_handler, + BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), + + BT_GATT_CHARACTERISTIC(&voice_interaction_rx_uuid.uuid, + BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_WRITE, + NULL, voice_interaction_write_handler, NULL), }; static struct bt_gatt_service audio_service = BT_GATT_SERVICE(audio_service_attr); @@ -89,6 +132,15 @@ static struct bt_gatt_service dfu_service = BT_GATT_SERVICE(dfu_service_attr); //Acceleration data //this code activates the onboard accelerometer. some cute ideas may include shaking the necklace to color strobe // +struct sensors { + struct sensor_value a_x; + struct sensor_value a_y; + struct sensor_value a_z; + struct sensor_value g_x; + struct sensor_value g_y; + struct sensor_value g_z; +}; + static struct sensors mega_sensor; static struct device *lsm6dsl_dev; //Arbritrary uuid, feel free to change @@ -540,7 +592,6 @@ static bool push_to_gatt(struct bt_conn *conn) #define OPUS_PADDED_LENGTH 80 #define MAX_WRITE_SIZE 440 static uint8_t storage_temp_data[MAX_WRITE_SIZE]; -static uint32_t offset = 0; static uint16_t buffer_offset = 0; // bool write_to_storage(void) // { @@ -609,10 +660,8 @@ bool write_to_storage(void) {//max possible packing } extern bool is_off; -static bool use_storage = true; #define MAX_FILES 10 #define MAX_AUDIO_FILE_SIZE 300000 -static int recent_file_size_updated = 0; void update_file_size() { @@ -706,9 +755,11 @@ extern struct bt_gatt_service storage_service; int bt_on() { int err = bt_enable(NULL); - bt_le_adv_start(BT_LE_ADV_CONN, bt_ad, ARRAY_SIZE(bt_ad), bt_sd, ARRAY_SIZE(bt_sd)); - // bt_gatt_service_register(&storage_service); - + if (err) { + return err; + } + err = bt_le_adv_start(BT_LE_ADV_CONN, bt_ad, ARRAY_SIZE(bt_ad), bt_sd, ARRAY_SIZE(bt_sd)); + return err; } //periodic advertising @@ -804,9 +855,146 @@ struct bt_conn *get_current_connection() int broadcast_audio_packets(uint8_t *buffer, size_t size) { - while (!write_to_tx_queue(buffer, size)) - { + if (voice_interaction_active) { + return handle_voice_data(buffer, size); + } + + if (is_off) { + LOG_WRN("Device is off, cannot broadcast audio"); + return -EPERM; + } + + // Normal audio handling + LOG_DBG("Broadcasting normal audio data: %d bytes", size); + while (!write_to_tx_queue(buffer, size)) { k_sleep(K_MSEC(1)); } return 0; } + +static ssize_t voice_interaction_write_handler(struct bt_conn *conn, + const struct bt_gatt_attr *attr, + const void *buf, + uint16_t len, + uint16_t offset, + uint8_t flags) { + if (!is_off) { + LOG_INF("Received voice response data: %d bytes", len); + if (len == 4) { + // This is the size packet + uint32_t size = *((uint32_t *)buf); + LOG_INF("Expected voice response size: %d bytes", size); + return len; + } + + // Process audio data + LOG_INF("Processing audio chunk of %d bytes", len); + uint16_t result = speak_stream(len, buf); + LOG_INF("Processed %d bytes", result); + return result; + } + LOG_WRN("Device is off, ignoring voice data"); + return len; +} + +bool voice_interaction_active = false; + +static atomic_t voice_state = ATOMIC_INIT(0); + +void start_voice_interaction(void) { + if (is_off || !current_connection) { + LOG_ERR("Cannot start voice interaction - device off or not connected"); + return; + } + + // Use atomic operation to prevent race conditions + if (!atomic_cas(&voice_state, 0, 1)) { + LOG_WRN("Voice interaction already active"); + return; + } + + LOG_INF("Starting voice interaction"); + + // Stop any ongoing audio processing + storage_is_on = false; + + // Clear buffers + ring_buf_reset(&ring_buf); + + // Configure mic first + int err = mic_configure_for_voice(); + if (err) { + LOG_ERR("Failed to configure mic: %d", err); + atomic_clear(&voice_state); + return; + } + + // Start mic last after everything is configured + voice_interaction_active = true; + err = mic_start(); + if (err) { + LOG_ERR("Failed to start mic: %d", err); + voice_interaction_active = false; + atomic_clear(&voice_state); + return; + } + + LOG_INF("Voice interaction started successfully"); +} + +void stop_voice_interaction(void) { + if (!atomic_cas(&voice_state, 1, 0)) { + LOG_WRN("Voice interaction not active"); + return; + } + + LOG_INF("Stopping voice interaction"); + + // Stop voice mode first + voice_interaction_active = false; + + // Reset mic + mic_start(); + + // Clear buffers + ring_buf_reset(&ring_buf); + + // Resume normal operation last + storage_is_on = true; + + LOG_INF("Voice interaction stopped"); +} + +int handle_voice_data(uint8_t *data, size_t len) { + if (!atomic_get(&voice_state)) { + LOG_WRN("Voice interaction not active"); + return -EPERM; + } + + if (!current_connection) { + LOG_ERR("No BLE connection"); + return -ENOTCONN; + } + + // Use voice interaction characteristic + int err = bt_gatt_notify(current_connection, &audio_service.attrs[6], + data, len); + if (err) { + LOG_ERR("Failed to send voice data: %d", err); + } + return err; +} + +static const char *phy2str(uint8_t phy) +{ + switch (phy) { + case BT_GAP_LE_PHY_1M: + return "1M"; + case BT_GAP_LE_PHY_2M: + return "2M"; + case BT_GAP_LE_PHY_CODED: + return "Coded"; + default: + return "Unknown"; + } +} diff --git a/Friend/firmware/firmware_v1.0/src/transport.h b/Friend/firmware/firmware_v1.0/src/transport.h index 5f54089e7..a64122889 100644 --- a/Friend/firmware/firmware_v1.0/src/transport.h +++ b/Friend/firmware/firmware_v1.0/src/transport.h @@ -1,26 +1,22 @@ #ifndef TRANSPORT_H #define TRANSPORT_H -#include -typedef struct sensors { +#include +#include +#include - struct sensor_value a_x; - struct sensor_value a_y; - struct sensor_value a_z; - struct sensor_value g_x; - struct sensor_value g_y; - struct sensor_value g_z; +// Voice interaction functions +extern bool voice_interaction_active; +void start_voice_interaction(void); +void stop_voice_interaction(void); -}; -/** - * @brief Initialize the BLE transport logic - * - * Initializes the BLE Logic - * - * @return 0 if successful, negative errno code if error - */ -int transport_start(); +// Declare handle_voice_data as non-static +int handle_voice_data(uint8_t *data, size_t len); + +// Existing declarations... +int transport_start(void); +struct bt_conn *get_current_connection(void); int broadcast_audio_packets(uint8_t *buffer, size_t size); -struct bt_conn *get_current_connection(); -int bt_on(); -#endif \ No newline at end of file +int bt_on(void); + +#endif diff --git a/Friend/firmware/testing/discover_devices.py b/Friend/firmware/testing/discover_devices.py index b78cbf582..cdc294edc 100644 --- a/Friend/firmware/testing/discover_devices.py +++ b/Friend/firmware/testing/discover_devices.py @@ -1,9 +1,95 @@ import asyncio from bleak import BleakScanner +from typing import Optional +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +FRIEND_NAME = "Friend" +FRIEND_SERVICE_UUID = "19B10000-E8F2-537E-4F6C-D104768A1214" + +async def find_friend_device(timeout: int = 5) -> Optional[str]: + """Specifically scan for Friend devices.""" + logger.info(f"Scanning for {FRIEND_NAME} devices...") + + try: + devices = await BleakScanner.discover( + timeout=timeout, + return_adv=True, + service_uuids=[FRIEND_SERVICE_UUID] + ) + + for d, adv_data in devices.values(): + logger.debug(f"Found device: {d.name} ({d.address})") + + if d.name and FRIEND_NAME in d.name: + logger.info("\nFound Friend Device!") + logger.info("─" * 40) + logger.info(f"Name: {d.name}") + logger.info(f"Address: {d.address}") + logger.info(f"RSSI: {adv_data.rssi}dBm") + logger.info("\nServices:") + for uuid in adv_data.service_uuids: + logger.info(f" • {uuid}") + logger.info("─" * 40) + return d.address + + logger.warning(f"No {FRIEND_NAME} devices found") + return None + + except Exception as e: + logger.error(f"Error during device discovery: {e}") + return None + +async def scan_all_devices(timeout: int = 5): + """Scan and display all available BLE devices""" + logger.info("Scanning for all BLE devices...") + + try: + devices = await BleakScanner.discover( + timeout=timeout, + return_adv=True + ) + + if not devices: + logger.info("No BLE devices found") + return + + logger.info("\nDiscovered Devices:") + for d, adv_data in devices.values(): + logger.info("─" * 40) + logger.info(f"Name: {d.name or 'Unknown'}") + logger.info(f"Address: {d.address}") + logger.info(f"RSSI: {adv_data.rssi}dBm") + if adv_data.service_uuids: + logger.info("\nServices:") + for uuid in adv_data.service_uuids: + logger.info(f" • {uuid}") + logger.info("─" * 40) + + except Exception as e: + logger.error(f"Error during device discovery: {e}") async def main(): - devices = await BleakScanner.discover() - for d in devices: - print(d) + """Main function with menu for different scanning options""" + while True: + print("\nBLE Scanner Menu:") + print("1. Find Friend device") + print("2. Scan all BLE devices") + print("3. Exit") + + choice = input("Select an option (1-3): ") + + if choice == "1": + await find_friend_device() + elif choice == "2": + await scan_all_devices() + elif choice == "3": + print("Exiting...") + break + else: + print("Invalid choice. Please select 1-3") -asyncio.run(main()) \ No newline at end of file +if __name__ == "__main__": + asyncio.run(main()) diff --git a/Friend/firmware/testing/talk_audio_on_friend.py b/Friend/firmware/testing/talk_audio_on_friend.py new file mode 100644 index 000000000..056c7c458 --- /dev/null +++ b/Friend/firmware/testing/talk_audio_on_friend.py @@ -0,0 +1,182 @@ +import asyncio +import bleak +import wave +import numpy as np +from bleak import BleakClient +from deepgram import DeepgramClient, SpeakOptions +import os +from dotenv import load_dotenv + +load_dotenv() + +# Configuration +DEVICE_ID = "817D48F6-FAF0-A566-D013-D05916B5D7B8" # Your device ID +DEEPGRAM_API_KEY = "f2e9ebf2f223ae423c88bf601ce1a157699d3005" # Your Deepgram API key +BUTTON_READ_UUID = "23BA7925-0000-1000-7450-346EAC492E92" # Button characteristic + +# Updated UUIDs to match firmware +VOICE_INTERACTION_UUID = "19B10004-E8F2-537E-4F6C-D104768A1214" # Voice data characteristic for sending audio to cloud +VOICE_INTERACTION_RX_UUID = "19B10005-E8F2-537E-4F6C-D104768A1214" # Voice response characteristic for receiving TTS audio + +# Button states +SINGLE_TAP = 1 +DOUBLE_TAP = 2 +LONG_TAP = 3 +BUTTON_PRESS = 4 +BUTTON_RELEASE = 5 + +# Audio settings +MAX_ALLOWED_SAMPLES = 50000 +GAIN = 5 +PACKET_SIZE = 400 + +class VoiceInteractionClient: + def __init__(self): + self.audio_data = bytearray() + self.is_recording = False + self.deepgram = DeepgramClient(api_key=DEEPGRAM_API_KEY) + self.client = None + self.is_connected = False + + async def connect(self): + while True: + try: + if not self.is_connected: + print("Attempting to connect...") + self.client = BleakClient(DEVICE_ID, disconnected_callback=self.handle_disconnect) + await self.client.connect() + self.is_connected = True + print(f"Connected to {self.client.address}") + await self.setup_notifications() + print("Ready for voice interaction. Double-tap to start recording.") + return + except Exception as e: + print(f"Connection failed: {e}") + await asyncio.sleep(5) # Wait before retrying + + def handle_disconnect(self, client): + print("Device disconnected!") + self.is_connected = False + asyncio.create_task(self.reconnect()) + + async def reconnect(self): + print("Attempting to reconnect...") + await self.connect() + + async def setup_notifications(self): + try: + # Button notifications + await self.client.start_notify(BUTTON_READ_UUID, self.on_button_change) + print("Button notifications set up") + + # Voice data notifications + await self.client.start_notify(VOICE_INTERACTION_UUID, self.on_voice_data) + print("Voice notifications set up") + + except Exception as e: + print(f"Error setting up notifications: {e}") + + def on_button_change(self, sender, data): + button_state = int.from_bytes(data, byteorder='little') + print(f"Button state: {button_state}") + + if button_state == DOUBLE_TAP: + print("Starting voice recording...") + self.is_recording = True + self.audio_data = bytearray() + elif button_state == BUTTON_RELEASE and self.is_recording: + print("Stopping voice recording...") + self.is_recording = False + asyncio.create_task(self.process_voice_command()) + + def on_voice_data(self, sender, data): + if self.is_recording: + # Skip the first 3 bytes (header) + self.audio_data.extend(data[3:]) + print(f"Received {len(data)} bytes of audio data") + + async def process_voice_command(self): + if len(self.audio_data) == 0: + print("No audio data recorded") + return + + # Save audio data to temporary WAV file + with wave.open('temp_input.wav', 'wb') as wav_file: + wav_file.setnchannels(1) + wav_file.setsampwidth(2) + wav_file.setframerate(8000) + wav_file.writeframes(self.audio_data) + + # Process with Deepgram + try: + # First, convert audio to text + # TODO: Add Deepgram STT here + text_prompt = "This is a test response" # Replace with actual STT result + print(f"Processing voice command: {text_prompt}") + + # Generate response audio + options = SpeakOptions( + model="aura-stella-en", + encoding="linear16", + container="wav" + ) + + print("Generating audio response...") + response = self.deepgram.speak.v("1").save("temp_output.wav", + {"text": f"You said: {text_prompt}"}, + options) + + # Process and send the audio response + print("Sending audio response...") + await self.send_audio_response("temp_output.wav") + + except Exception as e: + print(f"Error processing voice command: {e}") + + async def send_audio_response(self, filename): + # Read and process the audio file + with wave.open(filename, 'rb') as wav_file: + frames = wav_file.readframes(wav_file.getnframes()) + audio_data = np.frombuffer(frames, dtype=np.int16) + + # Downsample to 8kHz + third_samples = audio_data[::3] * GAIN + audio_bytes = third_samples.tobytes() + + try: + # Send size first + size_bytes = len(audio_bytes).to_bytes(4, byteorder='little') + await self.client.write_gatt_char(VOICE_INTERACTION_RX_UUID, size_bytes, response=True) + await asyncio.sleep(0.1) + + # Send audio data in chunks + for i in range(0, len(audio_bytes), PACKET_SIZE): + chunk = audio_bytes[i:i + PACKET_SIZE] + await self.client.write_gatt_char(VOICE_INTERACTION_RX_UUID, chunk, response=True) + print(f"Sent chunk of {len(chunk)} bytes") + await asyncio.sleep(0.01) + + except Exception as e: + print(f"Error sending audio response: {e}") + if not self.is_connected: + await self.reconnect() + + async def run(self): + try: + await self.connect() + while True: + if not self.is_connected: + await self.connect() + await asyncio.sleep(1) + except Exception as e: + print(f"Error: {e}") + finally: + if self.client and self.client.is_connected: + await self.client.disconnect() + +async def main(): + client = VoiceInteractionClient() + await client.run() + +if __name__ == "__main__": + asyncio.run(main())