mirror of
https://github.com/google/pebble
synced 2026-10-16 16:24:12 +02:00
699 lines
22 KiB
C
699 lines
22 KiB
C
/*
|
|
* 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 "gap_le_advert.h"
|
|
#include "gap_le_connect.h"
|
|
|
|
#include <bluetooth/bt_driver_advert.h>
|
|
|
|
#include "comm/ble/ble_log.h"
|
|
#include "comm/bt_lock.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "services/common/analytics/analytics.h"
|
|
#include "services/common/regular_timer.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/list.h"
|
|
|
|
//! CC2564 / HCI Advertising Limitation:
|
|
//! ------------------------------
|
|
//! The Bluetooth chip can accept only one advertising payload, one
|
|
//! corresponding scan response and one set of intervals. However, we need to
|
|
//! juggle multiple advertising payloads for different needs. For example,
|
|
//! to be discoverable we need to advertise, to be reconnectable we need to
|
|
//! advertise something else, to be an iBeacon we need to advertise yet
|
|
//! something different, etc.
|
|
//! Unfortunately, the Ti CC2564 Bluetooth controller does not offer built-in
|
|
//! functionality to cope with this, so we need to implement a scheduling
|
|
//! mechanism in the firmware of the host / microcontroller.
|
|
//!
|
|
//! Advertisement Scheduling:
|
|
//! -------------------------
|
|
//! The advertisement scheduling is pretty dumb and works as follows:
|
|
//! The scheduler has "cycles" which are fixed size windows in time, during
|
|
//! which one of the scheduled jobs is set to advertise.
|
|
//!
|
|
//! At the beginning of a cycle, the scheduler decides which job to advertise
|
|
//! next. It will just round-robin through the jobs to advertise.
|
|
//!
|
|
//! Note that only one job is advertising at a time. Even though a job might
|
|
//! have such a long interval that another job could be squeezed in between,
|
|
//! clever things like that are not considered for simplicity's sake.
|
|
//!
|
|
//! To-Do's:
|
|
//! --------
|
|
//! - ble_discoverability/pairability.c
|
|
//! - Use private addresses for privacy / harder tracebility.
|
|
|
|
#define GAP_LE_ADVERT_LOG_LEVEL LOG_LEVEL_DEBUG
|
|
|
|
typedef struct GAPLEAdvertisingJob {
|
|
ListNode node;
|
|
|
|
//! The callback to call when this job is unscheduled.
|
|
GAPLEAdvertisingJobUnscheduleCallback unscheduled_callback;
|
|
//! The data to pass into the unscheduled callback.
|
|
void *unscheduled_callback_data;
|
|
|
|
//! The number of seconds the current term has been on air.
|
|
uint16_t term_time_elapsed_secs;
|
|
|
|
uint8_t cur_term;
|
|
uint8_t num_terms;
|
|
//! The terms are run in the order that they appear in this array
|
|
GAPLEAdvertisingJobTerm *terms;
|
|
|
|
GAPLEAdvertisingJobTag tag:8;
|
|
|
|
//! The advertisement and scan response data
|
|
BLEAdData payload;
|
|
} GAPLEAdvertisingJob;
|
|
// -----------------------------------------------------------------------------
|
|
// Static Variables -- MUST be protected with bt_lock/unlock!
|
|
|
|
static bool s_gap_le_advert_is_initialized;
|
|
static bool s_deinit_in_progress;
|
|
|
|
//! Circular list! Pointing to the current job that needs air-time.
|
|
static GAPLEAdvertisingJob *s_jobs;
|
|
|
|
//! Job that is currently on air.
|
|
static GAPLEAdvertisingJob *s_current;
|
|
|
|
//! Advertising data that was last configured into the controller.
|
|
//! @note This pointer may be dangling, don't try to reference!
|
|
static const BLEAdData *s_current_ad_data;
|
|
|
|
//! The regular timer that marks the end of a cycle and triggers the next job
|
|
//! to be aired.
|
|
static RegularTimerInfo s_cycle_regular_timer;
|
|
|
|
static bool s_is_advertising;
|
|
|
|
//! Cache of the last advertising transmission power in dBm. A cache is kept in
|
|
//! case the API call fails, for example because Bluetooth is disabled.
|
|
//! 12 dBm is what the PAN1315 Bluetooth module reports.
|
|
static int8_t s_tx_power_cached = 12;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
//! Prototypes
|
|
|
|
static void prv_perform_next_job(bool force_refresh);
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
static const char * prv_string_for_debug_tag(GAPLEAdvertisingJobTag tag) {
|
|
switch (tag) {
|
|
case GAPLEAdvertisingJobTagDiscovery: return "DIS";
|
|
case GAPLEAdvertisingJobTagReconnection: return "RCN";
|
|
case GAPLEAdvertisingJobTagiOSAppLaunch: return "iOS";
|
|
default: return "?";
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
//! Helpers to manage the s_jobs list
|
|
//! bt_lock is expected to be taken with all of them!
|
|
|
|
static bool prv_is_current_term_infinite(const GAPLEAdvertisingJob *job) {
|
|
return (job->terms[job->cur_term].duration_secs == GAPLE_ADVERTISING_DURATION_INFINITE);
|
|
}
|
|
|
|
static bool prv_is_current_term_silent(GAPLEAdvertisingJob *job) {
|
|
return (job->terms[job->cur_term].min_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS);
|
|
}
|
|
|
|
//! Links the job into the ring of jobs. Will make the new job the start (s_jobs) of the ring only
|
|
//! if the first term isn't silent.
|
|
//! @return True if the new job was made the start of the ring, false if not.
|
|
static bool prv_link_job(GAPLEAdvertisingJob *job) {
|
|
if (!s_jobs) {
|
|
// First job, make it point to itself:
|
|
job->node.next = &job->node;
|
|
job->node.prev = &job->node;
|
|
s_jobs = job;
|
|
return true;
|
|
}
|
|
|
|
list_insert_after(&s_jobs->node, &job->node);
|
|
// Make it the next one up, unless the first term is silent:
|
|
if (!prv_is_current_term_silent(job)) {
|
|
s_jobs = job;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void prv_unlink_job(GAPLEAdvertisingJob *job) {
|
|
if (job->node.next == &job->node) {
|
|
// Last job left...
|
|
job->node.next = NULL;
|
|
job->node.prev = NULL;
|
|
s_jobs = NULL;
|
|
} else {
|
|
list_remove(&job->node, (ListNode **) &s_jobs, NULL);
|
|
}
|
|
|
|
// Part of CC2564 advertising bug work-around:
|
|
if (job == s_current) {
|
|
bt_driver_advert_client_set_cycled(false);
|
|
}
|
|
}
|
|
|
|
static bool prv_is_registered_job(const GAPLEAdvertisingJob *job) {
|
|
if (!job) {
|
|
return false;
|
|
}
|
|
|
|
// Search jobs (can't use list_contains(), because circular):
|
|
ListNode *node = &s_jobs->node;
|
|
while (node) {
|
|
if (node == (const ListNode *) job) {
|
|
return true;
|
|
}
|
|
node = node->next;
|
|
if (node == &s_jobs->node) {
|
|
// wrapped around
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void prv_increment_elapsed_time_for_job(GAPLEAdvertisingJob **job_ptr, bool *has_new_term) {
|
|
GAPLEAdvertisingJob *job = *job_ptr;
|
|
if (prv_is_current_term_infinite(job)) {
|
|
return;
|
|
}
|
|
// Increment the `time elapsed` counter:
|
|
++(job->term_time_elapsed_secs);
|
|
|
|
if (job->term_time_elapsed_secs >= job->terms[job->cur_term].duration_secs) {
|
|
// The current term has elapsed
|
|
job->cur_term++;
|
|
// Schedule the next term
|
|
if (job->cur_term < job->num_terms) {
|
|
// Take care of GAPLE_ADVERTISING_DURATION_LOOP_AROUND:
|
|
if (job->terms[job->cur_term].duration_secs == GAPLE_ADVERTISING_DURATION_LOOP_AROUND) {
|
|
BLE_LOG_DEBUG("Job looped around to term %"PRIu16,
|
|
job->terms[job->cur_term].loop_around_index);
|
|
job->cur_term = job->terms[job->cur_term].loop_around_index;
|
|
}
|
|
|
|
job->term_time_elapsed_secs = 0;
|
|
BLE_LOG_DEBUG("Job is performing next advertising term (%d/%d)",
|
|
job->cur_term, job->num_terms);
|
|
// force an update to make sure the new requested term takes
|
|
if (has_new_term) {
|
|
*has_new_term = true;
|
|
}
|
|
} else {
|
|
// Job's done, remove done job:
|
|
|
|
// If it's the last, this will update s_jobs to NULL as well:
|
|
prv_unlink_job(job);
|
|
|
|
// Call the unscheduled callback:
|
|
if (job->unscheduled_callback) {
|
|
job->unscheduled_callback(job, true /* completed */, job->unscheduled_callback_data);
|
|
}
|
|
|
|
BLE_LOG_DEBUG("Unscheduled advertising completed job: %s",
|
|
prv_string_for_debug_tag(job->tag));
|
|
kernel_free(job->terms);
|
|
kernel_free(job);
|
|
*job_ptr = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void prv_increment_time_elapsed_for_all_silent_terms_except_current(void) {
|
|
if (!s_jobs) {
|
|
return;
|
|
}
|
|
GAPLEAdvertisingJob *job = s_jobs;
|
|
do {
|
|
GAPLEAdvertisingJob *next = (GAPLEAdvertisingJob *) job->node.next;
|
|
if (job != s_current && prv_is_current_term_silent(job)) {
|
|
prv_increment_elapsed_time_for_job(&job, NULL);
|
|
}
|
|
job = next;
|
|
} while (job != s_jobs);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
//! Cycle timer callback.
|
|
//! It increments the air-time counter of the job's current term.
|
|
//! Updates the job's term if the term is done.
|
|
//! It removes the job if it's done.
|
|
//! It updates the s_jobs list.
|
|
//! It calls prv_perform_next_job() to set up the next job.
|
|
static void prv_cycle_timer_callback(void *unused) {
|
|
bool force_update = false;
|
|
|
|
bt_lock();
|
|
{
|
|
if (!s_current || !bt_driver_advert_client_has_cycled() || !s_gap_le_advert_is_initialized) {
|
|
// Job got removed in the meantime.
|
|
goto unlock;
|
|
}
|
|
|
|
prv_increment_time_elapsed_for_all_silent_terms_except_current();
|
|
|
|
GAPLEAdvertisingJob *job = s_current;
|
|
BLE_LOG_DEBUG("Currently running job: %s (non-connectable=%u)",
|
|
prv_string_for_debug_tag(job->tag), gap_le_connect_is_connected_as_slave());
|
|
|
|
// Set to next job (round-robin) that isn't silent (unless there is no non-silent one):
|
|
s_jobs = (GAPLEAdvertisingJob *) job->node.next;
|
|
while (prv_is_current_term_silent(s_jobs) &&
|
|
s_jobs != job /* looped around */) {
|
|
s_jobs = (GAPLEAdvertisingJob *) s_jobs->node.next;
|
|
};
|
|
|
|
prv_increment_elapsed_time_for_job(&job, &force_update);
|
|
|
|
prv_perform_next_job(force_update);
|
|
}
|
|
unlock:
|
|
bt_unlock();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
//! Timer start / stop utilities
|
|
//! bt_lock is expected to be taken!
|
|
static void prv_timer_start(void) {
|
|
if (regular_timer_is_scheduled(&s_cycle_regular_timer)) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Advertising timer already started");
|
|
regular_timer_remove_callback(&s_cycle_regular_timer);
|
|
}
|
|
|
|
regular_timer_add_seconds_callback(&s_cycle_regular_timer);
|
|
}
|
|
|
|
static void prv_timer_stop(void) {
|
|
regular_timer_remove_callback(&s_cycle_regular_timer);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
//! Airs the next advertisement job.
|
|
//! It sends the ad & scan response data to the Bluetooth controller and
|
|
//! enables/disables advertising.
|
|
//! It sets up / cleans up the cycle timer.
|
|
//! It updates the s_current pointer.
|
|
//! It does *not* mutate the s_jobs list.
|
|
//! bt_lock is expected to be taken!
|
|
//! @param force_refresh If true, the advertisement job will be re-setup even
|
|
//! though the current job has not changed. This is (only) useful when the
|
|
//! connectability mode has changed.
|
|
static void prv_perform_next_job(bool force_refresh) {
|
|
|
|
// Part of work-around for CC2564 bug (see comment with prv_work_around_should_not_cycle).
|
|
// When forced, we've just connected or disconnected, in that case cycle as well, because
|
|
// otherwise we'll continue to advertise non-connectable if we've just disconnected and there
|
|
// was already a job being advertised.
|
|
if (!force_refresh && bt_driver_advert_should_not_cycle()) {
|
|
return;
|
|
}
|
|
|
|
// Pick the next job:
|
|
GAPLEAdvertisingJob *next = s_jobs;
|
|
|
|
// s_is_dangling is checked here, in case the next job happens to have been allocated at the
|
|
// same address as the old s_current:
|
|
const bool is_same_job = (next == s_current && bt_driver_advert_client_has_cycled());
|
|
|
|
if (is_same_job && !force_refresh && s_is_advertising) {
|
|
// No change in job to give air time, keep going.
|
|
return;
|
|
}
|
|
|
|
if (s_current) {
|
|
// Clean up old job:
|
|
|
|
if (!next) {
|
|
// No more jobs. Stop timer:
|
|
prv_timer_stop();
|
|
}
|
|
|
|
if (s_is_advertising) {
|
|
// Controller needs to stop advertising before we can start a new job:
|
|
PBL_LOG(GAP_LE_ADVERT_LOG_LEVEL, "Disable last Ad job");
|
|
bt_driver_advert_advertising_disable();
|
|
s_is_advertising = false;
|
|
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT);
|
|
}
|
|
}
|
|
|
|
if (next) {
|
|
// Set up the next job to be on air:
|
|
|
|
if (!s_current) {
|
|
// No current job, start timer:
|
|
prv_timer_start();
|
|
}
|
|
|
|
if (!prv_is_current_term_silent(next)) {
|
|
const bool enable_scan_resp = (next->payload.scan_resp_data_length > 0);
|
|
|
|
if (s_current_ad_data != &next->payload) {
|
|
// Give the advertisement data to the BT controller:
|
|
bt_driver_advert_set_advertising_data(&next->payload);
|
|
s_current_ad_data = &next->payload;
|
|
}
|
|
|
|
// One slot is 625us:
|
|
const uint32_t min_interval_ms = ((next->terms[next->cur_term].min_interval_slots * 5) / 8);
|
|
const uint32_t max_interval_ms = ((next->terms[next->cur_term].max_interval_slots * 5) / 8);
|
|
|
|
BLE_LOG_DEBUG("Enable Ad job %s", prv_string_for_debug_tag(next->tag));
|
|
bool result = bt_driver_advert_advertising_enable(min_interval_ms, max_interval_ms,
|
|
enable_scan_resp);
|
|
if (result) {
|
|
s_is_advertising = true;
|
|
PBL_LOG(GAP_LE_ADVERT_LOG_LEVEL, "Airing advertising job: %s ",
|
|
prv_string_for_debug_tag(next->tag));
|
|
|
|
// Use average interval ms. BT controller does not report back what it uses.
|
|
const uint32_t interval_ms = (min_interval_ms + max_interval_ms) / 2;
|
|
|
|
// The ad data is fixed in size. See below.
|
|
// The scan response data size is omitted here, because we can't tell how
|
|
// often a scan request happens. BT controller does not report it either.
|
|
const uint32_t size = next->payload.ad_data_length /* ad data */ + 10 /* packet overhead */;
|
|
const uint32_t bytes_per_second = (size * 1000 /* ms */) / interval_ms;
|
|
|
|
analytics_stopwatch_start_at_rate(
|
|
ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT,
|
|
bytes_per_second, AnalyticsClient_System);
|
|
}
|
|
}
|
|
}
|
|
|
|
s_current = next;
|
|
bt_driver_advert_client_set_cycled(true);
|
|
}
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
GAPLEAdvertisingJobRef gap_le_advert_schedule(const BLEAdData *payload,
|
|
const GAPLEAdvertisingJobTerm *terms,
|
|
uint8_t num_terms,
|
|
GAPLEAdvertisingJobUnscheduleCallback callback,
|
|
void *callback_data,
|
|
GAPLEAdvertisingJobTag tag) {
|
|
// Sanity check payload:
|
|
if (!payload ||
|
|
payload->ad_data_length > GAP_LE_AD_REPORT_DATA_MAX_LENGTH ||
|
|
payload->scan_resp_data_length > GAP_LE_AD_REPORT_DATA_MAX_LENGTH) {
|
|
return NULL;
|
|
}
|
|
|
|
// Each job must have at least 1 term
|
|
if (num_terms == 0 || terms == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
// Minimum interval is 32 slots (20ms), or 160 slots (100ms) when there is a
|
|
// scan response:
|
|
const uint16_t min_threshold = payload->scan_resp_data_length ? 160 : 32;
|
|
for (int i = 0; i < num_terms; i++) {
|
|
// Loop-around term:
|
|
const bool is_loop_around = (terms[i].duration_secs == GAPLE_ADVERTISING_DURATION_LOOP_AROUND);
|
|
if (is_loop_around) {
|
|
if (i == 0) {
|
|
// First term cannot be loop-around
|
|
return NULL;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Silent term:
|
|
const bool is_silent =
|
|
(terms[i].min_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS &&
|
|
terms[i].max_interval_slots == GAPLE_ADVERTISING_SILENCE_INTERVAL_SLOTS);
|
|
if (is_silent) {
|
|
if (terms[i].duration_secs == GAPLE_ADVERTISING_DURATION_INFINITE) {
|
|
// Can't be silent forever
|
|
return NULL;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Normal term, verify min and max interval values:
|
|
if (terms[i].min_interval_slots < min_threshold ||
|
|
terms[i].max_interval_slots < terms[i].min_interval_slots) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Create the job data structure:
|
|
GAPLEAdvertisingJob *job = kernel_malloc_check(sizeof(GAPLEAdvertisingJob) +
|
|
payload->ad_data_length +
|
|
payload->scan_resp_data_length);
|
|
|
|
*job = (const GAPLEAdvertisingJob) {
|
|
.unscheduled_callback = callback,
|
|
.unscheduled_callback_data = callback_data,
|
|
.term_time_elapsed_secs = 0,
|
|
.num_terms = num_terms,
|
|
.tag = tag,
|
|
.payload = {
|
|
.ad_data_length = payload->ad_data_length,
|
|
.scan_resp_data_length = payload->scan_resp_data_length,
|
|
},
|
|
};
|
|
|
|
job->terms = kernel_malloc_check(sizeof(GAPLEAdvertisingJobTerm) * num_terms);
|
|
|
|
memcpy(job->terms, terms, sizeof(GAPLEAdvertisingJobTerm) * num_terms);
|
|
|
|
memcpy(job->payload.data, payload->data,
|
|
payload->ad_data_length + payload->scan_resp_data_length);
|
|
|
|
PBL_LOG(LOG_LEVEL_INFO, "Scheduling advertising job: %s",
|
|
prv_string_for_debug_tag(job->tag));
|
|
|
|
// Schedule
|
|
bt_lock();
|
|
{
|
|
if (s_gap_le_advert_is_initialized && !s_deinit_in_progress) {
|
|
if (prv_link_job(job)) {
|
|
prv_perform_next_job(false);
|
|
}
|
|
} else {
|
|
kernel_free(job->terms);
|
|
kernel_free(job);
|
|
job = NULL;
|
|
}
|
|
}
|
|
bt_unlock();
|
|
|
|
return job;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
void gap_le_advert_unschedule(GAPLEAdvertisingJobRef job) {
|
|
if (!job) {
|
|
return;
|
|
}
|
|
|
|
bool is_registered = false;
|
|
|
|
bt_lock();
|
|
{
|
|
if (!s_gap_le_advert_is_initialized) {
|
|
goto unlock;
|
|
}
|
|
|
|
is_registered = prv_is_registered_job(job);
|
|
|
|
if (is_registered) {
|
|
PBL_LOG(LOG_LEVEL_INFO, "Unscheduling advertising job: %s",
|
|
prv_string_for_debug_tag(job->tag));
|
|
|
|
prv_unlink_job(job);
|
|
prv_perform_next_job(false);
|
|
|
|
// Call the unscheduled callback:
|
|
if (job->unscheduled_callback) {
|
|
job->unscheduled_callback(job, false /* completed */,
|
|
job->unscheduled_callback_data);
|
|
}
|
|
|
|
// In case the payload pointer of a future jobs ends up being the same, ensure the adv data
|
|
// will get updated in that case:
|
|
if (s_current_ad_data == &job->payload) {
|
|
s_current_ad_data = NULL;
|
|
}
|
|
}
|
|
}
|
|
unlock:
|
|
bt_unlock();
|
|
|
|
if (is_registered) {
|
|
kernel_free(job->terms);
|
|
kernel_free(job);
|
|
}
|
|
}
|
|
|
|
void gap_le_advert_unschedule_job_types(
|
|
GAPLEAdvertisingJobTag *tag_types, size_t num_types) {
|
|
bt_lock();
|
|
|
|
ListNode *first_node = bt_driver_advert_client_has_cycled() ? &s_current->node : &s_jobs->node;
|
|
|
|
// get the last job in the list
|
|
ListNode *curr_node = list_get_prev(first_node);
|
|
|
|
// Note: We attempt to get the currently running job and walk through the
|
|
// list backwards so that we don't keep updating the running job as we remove
|
|
// advertisements from our list
|
|
|
|
while (curr_node) {
|
|
GAPLEAdvertisingJob *job = (GAPLEAdvertisingJob *)curr_node;
|
|
ListNode *prev_node = job->node.prev;
|
|
|
|
for (size_t i = 0; i < num_types; i++) {
|
|
if (job->tag == tag_types[i]) {
|
|
BLE_LOG_DEBUG("Removing advertisement of type %s",
|
|
prv_string_for_debug_tag(job->tag));
|
|
gap_le_advert_unschedule(job);
|
|
}
|
|
}
|
|
|
|
if (curr_node == first_node) {
|
|
break; // we have cycled through all the jobs
|
|
}
|
|
|
|
curr_node = prev_node;
|
|
}
|
|
|
|
bt_unlock();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
int8_t gap_le_advert_get_tx_power(void) {
|
|
int8_t tx_power;
|
|
bt_lock();
|
|
{
|
|
// In case this API call fails, (e.g. Airplane Mode),
|
|
// the s_tx_power_cached is untouched:
|
|
if (bt_driver_advert_client_get_tx_power(&tx_power)) {
|
|
s_tx_power_cached = tx_power;
|
|
}
|
|
}
|
|
bt_unlock();
|
|
return tx_power;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
void gap_le_advert_init(void) {
|
|
bt_lock();
|
|
{
|
|
if (s_gap_le_advert_is_initialized) {
|
|
PBL_LOG(LOG_LEVEL_ERROR, "gap le advert has already been initialized");
|
|
goto unlock;
|
|
}
|
|
|
|
s_deinit_in_progress = false;
|
|
s_jobs = NULL;
|
|
s_current = NULL;
|
|
s_current_ad_data = NULL;
|
|
s_cycle_regular_timer = (const RegularTimerInfo) {
|
|
.cb = prv_cycle_timer_callback,
|
|
};
|
|
|
|
s_is_advertising = false;
|
|
s_gap_le_advert_is_initialized = true;
|
|
}
|
|
unlock:
|
|
bt_unlock();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
void gap_le_advert_deinit(void) {
|
|
bt_lock();
|
|
{
|
|
s_deinit_in_progress = true;
|
|
|
|
while (s_jobs) {
|
|
gap_le_advert_unschedule(s_jobs);
|
|
}
|
|
|
|
PBL_ASSERTN(!regular_timer_is_scheduled(&s_cycle_regular_timer) ||
|
|
regular_timer_pending_deletion(&s_cycle_regular_timer));
|
|
s_gap_le_advert_is_initialized = false;
|
|
}
|
|
bt_unlock();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
void gap_le_advert_handle_connect_as_slave(void) {
|
|
bt_lock();
|
|
{
|
|
if (!s_gap_le_advert_is_initialized) {
|
|
goto unlock;
|
|
}
|
|
// The link layer state machine inside the Bluetooth controller
|
|
// automatically stops advertising when transitioning to "connected", so
|
|
// update our own state. See 7.8.9 of Bluetooth Specification
|
|
//
|
|
// We don't instantly cycle the advertisements because our LE client
|
|
// handler (kernel_le_client.c) will unschedule jobs accordingly and we
|
|
// want to avoid unnecessary refreshes of the advertising state
|
|
s_is_advertising = false;
|
|
|
|
if (!bt_driver_advert_client_has_cycled()) {
|
|
s_current = NULL;
|
|
bt_driver_advert_client_set_cycled(true);
|
|
}
|
|
|
|
analytics_stopwatch_stop(ANALYTICS_DEVICE_METRIC_BLE_ESTIMATED_BYTES_ADVERTISED_COUNT);
|
|
}
|
|
unlock:
|
|
bt_unlock();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
void gap_le_advert_handle_disconnect_as_slave(void) {
|
|
bt_lock();
|
|
{
|
|
if (!s_gap_le_advert_is_initialized) {
|
|
goto unlock;
|
|
}
|
|
// Call prv_perform_next_job() to trigger refreshing the configuration of
|
|
// the controller: it can advertise connectable packets again.
|
|
prv_perform_next_job(true /* force refresh, connectability mode changed */);
|
|
}
|
|
unlock:
|
|
bt_unlock();
|
|
}
|
|
|
|
GAPLEAdvertisingJobRef gap_le_advert_get_current_job(void) {
|
|
return s_current;
|
|
}
|
|
|
|
GAPLEAdvertisingJobRef gap_le_advert_get_jobs(void) {
|
|
return s_jobs;
|
|
}
|
|
|
|
GAPLEAdvertisingJobTag gap_le_advert_get_job_tag(GAPLEAdvertisingJobRef job) {
|
|
return job->tag;
|
|
}
|
|
|
|
#undef GAP_LE_ADVERT_LOG_LEVEL
|