Add Bluetooth audio out (A2DP) on the PicoW (#2174)

Adds a library to run classic Bluetooth A2DP source (output) audio from
the PicoW.  Simple example showing operation and callbacks.

Factor out multiple BT lock/unlock and place in the PicoW variant files.
This commit is contained in:
Earle F. Philhower, III 2024-05-26 14:30:40 -07:00 committed by GitHub
parent 367200a2c8
commit ec5e62e533
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 65754 additions and 221 deletions

View file

@ -25,7 +25,7 @@ jobs:
uses: codespell-project/actions-codespell@master
with:
skip: ./ArduinoCore-API,./libraries/ESP8266SdFat,./libraries/Adafruit_TinyUSB_Arduino,./libraries/LittleFS/lib,./tools/pyserial,./pico-sdk,./.github,./docs/i2s.rst,./cores/rp2040/api,./libraries/FreeRTOS,./tools/libbearssl/bearssl,./include,./libraries/WiFi/examples/BearSSL_Server,./ota/uzlib,./libraries/http-parser/lib,./libraries/WebServer/examples/HelloServerBearSSL/HelloServerBearSSL.ino,./libraries/HTTPUpdateServer/examples/SecureBearSSLUpdater/SecureBearSSLUpdater.ino,./.git,./libraries/FatFS/lib/fatfs,./libraries/FatFS/src/diskio.h,./libraries/FatFS/src/ff.cpp,./libraries/FatFS/src/ffconf.h,./libraries/FatFS/src/ffsystem.cpp,./libraries/FatFS/src/ff.h,./libraries/lwIP_WINC1500/src/driver,./libraries/lwIP_WINC1500/src/common,./libraries/lwIP_WINC1500/src/bus_wrapper,./libraries/lwIP_WINC1500/src/spi_flash
ignore_words_list: ser,dout
ignore_words_list: ser,dout,shiftIn
# Consistent style
astyle:

View file

@ -98,7 +98,8 @@ Read the [Contributing Guide](https://github.com/earlephilhower/arduino-pico/blo
* digitalWrite/Read, shiftIn/Out, tone, analogWrite(PWM)/Read, temperature
* Analog stereo audio in using DMA and the built-in ADC
* Analog stereo audio out using PWM hardware
* USB drive mode for data loggers (SingleFileDrive)
* Bluetooth A2DP audio source (output) on the PicoW
* USB drive mode for data loggers (SingleFileDrive, FatFSUSB)
* Peripherals: SPI master/slave, Wire(I2C) master/slave, dual UART, emulated EEPROM, I2S audio input/output, Servo
* printf (i.e. debug) output over USB serial

View file

@ -0,0 +1,32 @@
// Use to create a callback thunk from C to C++
// #define CCALLBACKNAME to a unique per-file name and #include this file
// To make a CB use a define of the form:
/*
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(<CCALLBACKNAMEvoid(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
*/
#include <functional>
// Thank you to https://stackoverflow.com/questions/66474621/multiple-non-static-callbacks-of-c-member-functions for the following beautiful hack
#ifndef CCALLBACKNAME
#define CCALLBACKNAME _CCallback
#endif
template <typename T, int tag>
struct CCALLBACKNAME;
template <typename Ret, typename... Params, int tag>
struct CCALLBACKNAME<Ret(Params...), tag> {
template <typename... Args>
static Ret callback(Args... args) {
return func(args...);
}
int _tag = tag;
static std::function<Ret(Params...)> func;
};
template <typename Ret, typename... Params, int tag>
std::function<Ret(Params...)> CCALLBACKNAME<Ret(Params...), tag>::func;

View file

@ -1212,7 +1212,7 @@ static uint16_t handle_prepare_write_request(att_connection_t * att_connection,
}
/*
@brief transcation queue of prepared writes, e.g., after disconnect
@brief transaction queue of prepared writes, e.g., after disconnect
*/
void att_clear_transaction_queue(att_connection_t * att_connection) {
(*att_write_callback)(att_connection->con_handle, 0, ATT_TRANSACTION_MODE_CANCEL, 0, NULL, 0);

View file

@ -257,7 +257,7 @@ extern "C" {
uint8_t * response_buffer);
/**
@brief transcation queue of prepared writes, e.g., after disconnect
@brief transaction queue of prepared writes, e.g., after disconnect
@return att_connection
*/
void att_clear_transaction_queue(att_connection_t * att_connection);

View file

@ -40,8 +40,8 @@ For many BTStack examples, you simply need call the included
called afterwards to start processing (in the background).
You will also need to acquire the BT ``async_context`` system lock before
calling any BTStack APIs. See the ``libraries/PicoBluetoothHID`` helper
class for an example of how to do this.
calling any BTStack APIs. ``__lockBluetooth`` and ``unlockBluetooth`` are
provided in the PicoW variant code.
Note that if you need to modify the system ``btstack_config.h`` file, do so
in the ``tools/libpico`` directory and rebuild the Pico SDK static library.

View file

@ -597,7 +597,7 @@ close
Close the file. No other operations should be performed on *File* object
after ``close`` function was called.
openNextFile (compatibiity method, not recommended for new code)
openNextFile (compatibility method, not recommended for new code)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: cpp
@ -609,7 +609,7 @@ openNextFile (compatibiity method, not recommended for new code)
Opens the next file in the directory pointed to by the File. Only valid
when ``File.isDirectory() == true``.
rewindDirectory (compatibiity method, not recommended for new code)
rewindDirectory (compatibility method, not recommended for new code)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: cpp

View file

@ -38,9 +38,9 @@ extern unsigned long __lwip_rand(void);
#define LWIP_NETIF_LINK_CALLBACK 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETCONN 0
#define MEM_STATS 0
#define SYS_STATS 0
#define MEMP_STATS 0
#define MEM_STATS 1
#define SYS_STATS 1
#define MEMP_STATS 1
#define LINK_STATS 0
// #define ETH_PAD_SIZE 2
#define LWIP_CHKSUM_ALGORITHM 0
@ -72,6 +72,8 @@ extern void __setSystemTime(unsigned long long sec, unsigned long us);
#define LWIP_DEBUG 1
#define LWIP_STATS 1
#define LWIP_STATS_DISPLAY 1
#define MEMP_STATS 1
#define MEM_STATS 1
#endif
#define ETHARP_DEBUG LWIP_DBG_OFF

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,98 @@
// Plays tones and Au Claire De La Lune over an A2DP connection
// Released to the public domain by Earle Philhower <earlephilhowwer@yahoo.com>
#include <BluetoothAudio.h>
#include "raw.h"
int16_t pcm[64 * 2];
uint32_t phase = 0;
volatile uint32_t fr = 32;
volatile uint32_t fl = 16;
volatile bool paused = false;
volatile bool playwav = false;
void avrcpCB(void *param, avrcp_operation_id_t op, int pressed) {
(void) param;
if (pressed && op == AVRCP_OPERATION_ID_FORWARD) {
// Change the sound when they hit play
fr = random(8, 64);
fl = random(8, 64);
Serial.printf("Now generating %lu, %lu\n", fr, fl);
} else if (pressed && op == AVRCP_OPERATION_ID_PLAY) {
paused = !paused;
if (paused) {
Serial.printf("Pausing\n");
} else {
Serial.printf("Resuming\n");
}
} else if (pressed && op == AVRCP_OPERATION_ID_BACKWARD) {
playwav = !playwav;
Serial.println(playwav ? "Playing 'Au Claire De La Lune'" : "Playing tones");
}
}
void volumeCB(void *param, int pct) {
(void) param;
Serial.printf("Speaker volume changed to %d%%\n", pct);
}
void connectCB(void *param, bool connected) {
(void) param;
if (connected) {
Serial.printf("A2DP connection started to %s\n", bd_addr_to_str(A2DPSource.getSinkAddress()));
} else {
Serial.printf("A2DP connection stopped\n");
}
}
void fillPCM() {
if (paused) {
bzero(pcm, sizeof(pcm));
return;
}
if (playwav) {
for (int i = 0; i < 64; i++) {
// Audio in flash is 8-bit signed mono, so shift left to make 16-bit and then play over both channels
pcm[i * 2] = auclairedelalune_raw[(i + phase) % sizeof(auclairedelalune_raw)] << 8;
pcm[i * 2 + 1] = auclairedelalune_raw[(i + phase) % sizeof(auclairedelalune_raw)] << 8;
}
} else {
for (int i = 0; i < 64; i++) {
pcm[i * 2] = ((i + phase) / fr) & 1 ? 3000 : -3000;
pcm[i * 2 + 1] = ((i + phase) / fl) & 1 ? 1000 : -1000;
}
}
phase += 64;
}
void setup() {
delay(2000);
A2DPSource.onAVRCP(avrcpCB);
A2DPSource.onVolume(volumeCB);
A2DPSource.onConnect(connectCB);
A2DPSource.begin();
Serial.printf("Starting, press BOOTSEL to pair to first found speaker\n");
Serial.printf("Use the forward button on speaker to change tones\n");
Serial.printf("Use the reverse button on speaker to alternate between tones and Au Claire De La Lune\n");
Serial.printf("Use the play button on speaker to pause/unpause the tone\n");
}
void loop() {
while ((size_t)A2DPSource.availableForWrite() > sizeof(pcm)) {
fillPCM();
A2DPSource.write((const uint8_t *)pcm, sizeof(pcm));
}
if (BOOTSEL) {
while (BOOTSEL) {
delay(1);
}
A2DPSource.disconnect();
A2DPSource.clearPairing();
Serial.printf("Connecting...");
if (A2DPSource.connect()) {
Serial.printf("Connected!\n");
} else {
Serial.printf("Failed! :(\n");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
#######################################
# Syntax Coloring Map
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
A2DPSource KEYWORD1
BTDeviceInfo KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
setFrequency KEYWORD2
setName KEYWORD2
setsetBufferSize KEYWORD2
getUnderflow KEYWORD2
getSinkAddress KEYWORD2
scan KEYWORD2
connect KEYWORD2
connected KEYWORD2
disconnect KEYWORD2
clearPairing KEYWORD2
connect KEYWORD2
connect KEYWORD2
connect KEYWORD2
onTransmit KEYWORD2
onAVRCP KEYWORD2
onBattery KEYWORD2
onVolume KEYWORD2
onConnect KEYWORD2
# BTDeviceInfo
deviceClass KEYWORD2
address KEYWORD2
addressString KEYWORD2
rssi KEYWORD2
name KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################

View file

@ -0,0 +1,10 @@
name=BluetoothAudio
version=1.0
author=Earle F. Philhower, III <earlephilhower@yahoo.com>
maintainer=Earle F. Philhower, III <earlephilhower@yahoo.com>
sentence=Plays and receives audio over Bluetooth A2DP
paragraph=Plays and receives audio over Bluetooth A2DP
category=Communication
url=http://github.com/earlephilhower/arduino-pico
architectures=rp2040
dot_a_linkage=true

View file

@ -0,0 +1,736 @@
/*
A1DP Source (Bluetooth audio sender)
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Based heavily off of the a2dp_source example from BlueKitchen BTStack
/*
Copyright (C) 2016 BlueKitchen GmbH
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the names of
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
4. Any redistribution, use, or modification is done solely for
personal benefit and not for any commercial purpose or for
monetary gain.
THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLUEKITCHEN
GMBH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Please inquire about commercial licensing options at
contact@bluekitchen-gmbh.com
*/
#include <Arduino.h>
#include "A2DPSource.h"
#define CCALLBACKNAME _A2DPSOURCECB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(_A2DPSOURCECB<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(_A2DPSOURCECB<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
#define TIMEOUTHANDLERCB(class, cbFcn) \
(_A2DPSOURCECB<void(btstack_timer_source_t *), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1), \
static_cast<void(*)(btstack_timer_source_t*)>(_A2DPSOURCECB<void(btstack_timer_source_t*), __COUNTER__ - 1>::callback))
bool A2DPSource_::begin() {
if (_running) {
return false;
}
_pcmBuffer = (int16_t *)malloc(_pcmBufferSize * sizeof(int16_t));
if (!_pcmBuffer) {
DEBUGV("A2DPSource: OOM for pcm buffer\n");
return false;
}
_pcmWriter = 0;
_pcmReader = 0;
// Request role change on reconnecting headset to always use them in slave mode
hci_set_master_slave_policy(0);
// enabled EIR
hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR);
l2cap_init();
#ifdef ENABLE_BLE
// Initialize LE Security Manager. Needed for cross-transport key derivation
sm_init();
#endif
// Initialize A2DP Source
a2dp_source_init();
a2dp_source_register_packet_handler(PACKETHANDLERCB(A2DPSource_, a2dp_source_packet_handler));
// Create stream endpoint
avdtp_stream_endpoint_t * local_stream_endpoint = a2dp_source_create_stream_endpoint(AVDTP_AUDIO, AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities), media_sbc_codec_configuration, sizeof(media_sbc_codec_configuration));
if (!local_stream_endpoint) {
DEBUGV("A2DP Source: not enough memory to create local stream endpoint\n");
return false;
}
// Store stream endpoint's SEP ID, as it is used by A2DP API to identify the stream endpoint
media_tracker.local_seid = avdtp_local_seid(local_stream_endpoint);
avdtp_source_register_delay_reporting_category(media_tracker.local_seid);
// Initialize AVRCP Service
avrcp_init();
avrcp_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_packet_handler));
// Initialize AVRCP Target
avrcp_target_init();
avrcp_target_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_target_packet_handler));
// Initialize AVRCP Controller
avrcp_controller_init();
avrcp_controller_register_packet_handler(PACKETHANDLERCB(A2DPSource_, avrcp_controller_packet_handler));
// Initialize SDP,
sdp_init();
// Create A2DP Source service record and register it with SDP
memset(sdp_a2dp_source_service_buffer, 0, sizeof(sdp_a2dp_source_service_buffer));
a2dp_source_create_sdp_record(sdp_a2dp_source_service_buffer, 0x10001, AVDTP_SOURCE_FEATURE_MASK_PLAYER, NULL, NULL);
sdp_register_service(sdp_a2dp_source_service_buffer);
// Create AVRCP Target service record and register it with SDP. We receive Category 1 commands from the headphone, e.g. play/pause
memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
uint16_t supported_features = AVRCP_FEATURE_MASK_CATEGORY_PLAYER_OR_RECORDER;
#ifdef AVRCP_BROWSING_ENABLED
supported_features |= AVRCP_FEATURE_MASK_BROWSING;
#endif
avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer, 0x10002, supported_features, NULL, NULL);
sdp_register_service(sdp_avrcp_target_service_buffer);
// Create AVRCP Controller service record and register it with SDP. We send Category 2 commands to the headphone, e.g. volume up/down
memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer));
uint16_t controller_supported_features = AVRCP_FEATURE_MASK_CATEGORY_MONITOR_OR_AMPLIFIER;
avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, 0x10003, controller_supported_features, NULL, NULL);
sdp_register_service(sdp_avrcp_controller_service_buffer);
// Register Device ID (PnP) service SDP record
memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer));
device_id_create_sdp_record(device_id_sdp_service_buffer, 0x10004, DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
sdp_register_service(device_id_sdp_service_buffer);
// Set local name with a template Bluetooth address, that will be automatically
// replaced with a actual address once it is available, i.e. when BTstack boots
// up and starts talking to a Bluetooth module.
if (!_name) {
setName("PicoW A2DP 00:00:00:00:00:00");
}
gap_set_local_name(_name);
gap_discoverable_control(1);
gap_set_class_of_device(0x200408);
// Register for HCI events.
_hci.install();
_running = true;
_hci.begin();
return true;
}
bool A2DPSource_::connect(const uint8_t *addr) {
if (!_running) {
return false;
}
while (!_hci.running()) {
delay(10);
}
uint8_t a[6];
if (addr) {
memcpy(a, addr, sizeof(a));
auto ret = !a2dp_source_establish_stream(a, &media_tracker.a2dp_cid);
return ret;
} else {
clearPairing();
auto l = scan();
for (auto e : l) {
DEBUGV("Scan connecting %s at %s ... ", e.name(), e.addressString());
memcpy(a, e.address(), sizeof(a));
if (!a2dp_source_establish_stream(a, &media_tracker.a2dp_cid)) {
DEBUGV("Connection established\n");
return true;
}
DEBUGV("Failed\n");
}
return false;
}
}
bool A2DPSource_::disconnect() {
__lockBluetooth();
a2dp_source_disconnect(media_tracker.a2dp_cid);
__unlockBluetooth();
if (!_running || !_connected) {
return false;
}
_connected = false;
return true;
}
void A2DPSource_::clearPairing() {
disconnect();
__lockBluetooth();
gap_delete_all_link_keys();
__unlockBluetooth();
}
// from Print (see notes on write() methods below)
size_t A2DPSource_::write(const uint8_t *buffer, size_t size) {
size_t count = 0;
size /= 2;
__lockBluetooth();
// First copy from writer to either end of
uint32_t start = _pcmWriter;
uint32_t end = _pcmReader > _pcmWriter ? _pcmReader : _pcmBufferSize;
if (end - start > size) {
end = start + size;
}
memcpy(_pcmBuffer + start, buffer, (end - start) * sizeof(int16_t));
count += (end - start) * sizeof(int16_t);
size -= end - start;
_pcmWriter += end - start;
_pcmWriter %= _pcmBufferSize;
// Possibly wraparound to 0 if still left
if (size) {
start = 0;
end = _pcmReader;
if (end - start > size) {
end = size;
}
memcpy(_pcmBuffer + start, buffer, (end - start) * sizeof(int16_t));
count += (end - start) * sizeof(int16_t);
size -= end - start;
_pcmWriter += end - start;
_pcmWriter %= _pcmBufferSize;
}
__unlockBluetooth();
return count;
}
int A2DPSource_::availableForWrite() {
int avail = 0;
__lockBluetooth();
if (_pcmWriter == _pcmReader) {
avail = _pcmBufferSize - 1;
} else if (_pcmReader > _pcmWriter) {
avail = _pcmReader - _pcmWriter - 1;
} else {
avail = _pcmBufferSize - _pcmWriter + _pcmReader - 1;
}
__unlockBluetooth();
return avail;
}
void A2DPSource_::dump_sbc_configuration(media_codec_configuration_sbc_t * configuration) {
(void) configuration;
DEBUGV("Received media codec configuration:\n");
DEBUGV(" - num_channels: %d\n", configuration->num_channels);
DEBUGV(" - sampling_frequency: %d\n", configuration->sampling_frequency);
DEBUGV(" - channel_mode: %d\n", configuration->channel_mode);
DEBUGV(" - block_length: %d\n", configuration->block_length);
DEBUGV(" - subbands: %d\n", configuration->subbands);
DEBUGV(" - allocation_method: %d\n", configuration->allocation_method);
DEBUGV(" - bitpool_value [%d, %d] \n", configuration->min_bitpool_value, configuration->max_bitpool_value);
}
void A2DPSource_::a2dp_timer_start(a2dp_media_sending_context_t * context) {
context->max_media_payload_size = btstack_min(a2dp_max_media_payload_size(context->a2dp_cid, context->local_seid), SBC_STORAGE_SIZE);
context->sbc_storage_count = 0;
context->sbc_ready_to_send = 0;
context->streaming = 1;
btstack_run_loop_remove_timer(&context->audio_timer);
btstack_run_loop_set_timer_handler(&context->audio_timer, TIMEOUTHANDLERCB(A2DPSource_, a2dp_audio_timeout_handler));
btstack_run_loop_set_timer_context(&context->audio_timer, context);
btstack_run_loop_set_timer(&context->audio_timer, AUDIO_TIMEOUT_MS);
btstack_run_loop_add_timer(&context->audio_timer);
}
void A2DPSource_::a2dp_timer_stop(a2dp_media_sending_context_t * context) {
context->time_audio_data_sent = 0;
context->acc_num_missed_samples = 0;
context->samples_ready = 0;
context->streaming = 1;
context->sbc_storage_count = 0;
context->sbc_ready_to_send = 0;
btstack_run_loop_remove_timer(&context->audio_timer);
}
int A2DPSource_::a2dp_fill_sbc_audio_buffer(a2dp_media_sending_context_t * context) {
// perform sbc encoding
int total_num_bytes_read = 0;
unsigned int num_audio_samples_per_sbc_buffer = btstack_sbc_encoder_num_audio_frames();
while (context->samples_ready >= num_audio_samples_per_sbc_buffer
&& (context->max_media_payload_size - context->sbc_storage_count) >= btstack_sbc_encoder_sbc_buffer_length()) {
if ((_pcmWriter / 256) != (_pcmReader / 256)) {
btstack_sbc_encoder_process_data(_pcmBuffer + _pcmReader);
asm volatile("" ::: "memory"); // Ensure the data is processed before advancing
auto next_reader = (_pcmReader + 256) % _pcmBufferSize;
asm volatile("" ::: "memory"); // Ensure the reader value is only written once, correctly
_pcmReader = next_reader;
} else {
_underflow = true;
// Just keep sending old data
btstack_sbc_encoder_process_data(_pcmBuffer + _pcmReader);
}
uint16_t sbc_frame_size = btstack_sbc_encoder_sbc_buffer_length();
uint8_t *sbc_frame = btstack_sbc_encoder_sbc_buffer();
total_num_bytes_read += num_audio_samples_per_sbc_buffer;
// first byte in sbc storage contains sbc media header
memcpy(&context->sbc_storage[1 + context->sbc_storage_count], sbc_frame, sbc_frame_size);
context->sbc_storage_count += sbc_frame_size;
context->samples_ready -= num_audio_samples_per_sbc_buffer;
}
return total_num_bytes_read;
}
void A2DPSource_::a2dp_audio_timeout_handler(btstack_timer_source_t * timer) {
a2dp_media_sending_context_t * context = (a2dp_media_sending_context_t *) btstack_run_loop_get_timer_context(timer);
btstack_run_loop_set_timer(&context->audio_timer, AUDIO_TIMEOUT_MS);
btstack_run_loop_add_timer(&context->audio_timer);
uint32_t now = btstack_run_loop_get_time_ms();
uint32_t update_period_ms = AUDIO_TIMEOUT_MS;
if (context->time_audio_data_sent > 0) {
update_period_ms = now - context->time_audio_data_sent;
}
uint32_t num_samples = (update_period_ms * _frequency) / 1000;
context->acc_num_missed_samples += (update_period_ms * _frequency) % 1000;
while (context->acc_num_missed_samples >= 1000) {
num_samples++;
context->acc_num_missed_samples -= 1000;
}
context->time_audio_data_sent = now;
context->samples_ready += num_samples;
if (context->sbc_ready_to_send) {
return;
}
a2dp_fill_sbc_audio_buffer(context);
if ((context->sbc_storage_count + btstack_sbc_encoder_sbc_buffer_length()) > context->max_media_payload_size) {
// schedule sending
context->sbc_ready_to_send = 1;
a2dp_source_stream_endpoint_request_can_send_now(context->a2dp_cid, context->local_seid);
}
}
void A2DPSource_::a2dp_send_media_packet() {
int num_bytes_in_frame = btstack_sbc_encoder_sbc_buffer_length();
int bytes_in_storage = media_tracker.sbc_storage_count;
uint8_t num_sbc_frames = bytes_in_storage / num_bytes_in_frame;
// Prepend SBC Header
media_tracker.sbc_storage[0] = num_sbc_frames; // (fragmentation << 7) | (starting_packet << 6) | (last_packet << 5) | num_frames;
a2dp_source_stream_send_media_payload_rtp(media_tracker.a2dp_cid, media_tracker.local_seid, 0,
media_tracker.rtp_timestamp,
media_tracker.sbc_storage, bytes_in_storage + 1);
// update rtp_timestamp
unsigned int num_audio_samples_per_sbc_buffer = btstack_sbc_encoder_num_audio_frames();
media_tracker.rtp_timestamp += num_sbc_frames * num_audio_samples_per_sbc_buffer;
media_tracker.sbc_storage_count = 0;
media_tracker.sbc_ready_to_send = 0;
if (_transmitCB) {
_transmitCB(_transmitData);
}
}
void A2DPSource_::a2dp_source_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint8_t status;
uint8_t local_seid;
bd_addr_t address;
uint16_t cid;
avdtp_channel_mode_t channel_mode;
uint8_t allocation_method;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_A2DP_META) {
return;
}
switch (hci_event_a2dp_meta_get_subevent_code(packet)) {
case A2DP_SUBEVENT_SIGNALING_CONNECTION_ESTABLISHED:
a2dp_subevent_signaling_connection_established_get_bd_addr(packet, address);
cid = a2dp_subevent_signaling_connection_established_get_a2dp_cid(packet);
status = a2dp_subevent_signaling_connection_established_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("A2DP Source: Connection failed, status 0x%02x, cid 0x%02x, a2dp_cid 0x%02x \n", status, cid, media_tracker.a2dp_cid);
media_tracker.a2dp_cid = 0;
break;
}
media_tracker.a2dp_cid = cid;
media_tracker.volume = 32;
memcpy(_sinkAddress, address, sizeof(_sinkAddress));
DEBUGV("A2DP Source: Connected to address %s, a2dp cid 0x%02x, local seid 0x%02x.\n", bd_addr_to_str(address), media_tracker.a2dp_cid, media_tracker.local_seid);
break;
case A2DP_SUBEVENT_SIGNALING_MEDIA_CODEC_SBC_CONFIGURATION: {
cid = avdtp_subevent_signaling_media_codec_sbc_configuration_get_avdtp_cid(packet);
if (cid != media_tracker.a2dp_cid) {
return;
}
media_tracker.remote_seid = a2dp_subevent_signaling_media_codec_sbc_configuration_get_remote_seid(packet);
sbc_configuration.reconfigure = a2dp_subevent_signaling_media_codec_sbc_configuration_get_reconfigure(packet);
sbc_configuration.num_channels = a2dp_subevent_signaling_media_codec_sbc_configuration_get_num_channels(packet);
sbc_configuration.sampling_frequency = a2dp_subevent_signaling_media_codec_sbc_configuration_get_sampling_frequency(packet);
sbc_configuration.block_length = a2dp_subevent_signaling_media_codec_sbc_configuration_get_block_length(packet);
sbc_configuration.subbands = a2dp_subevent_signaling_media_codec_sbc_configuration_get_subbands(packet);
sbc_configuration.min_bitpool_value = a2dp_subevent_signaling_media_codec_sbc_configuration_get_min_bitpool_value(packet);
sbc_configuration.max_bitpool_value = a2dp_subevent_signaling_media_codec_sbc_configuration_get_max_bitpool_value(packet);
channel_mode = (avdtp_channel_mode_t) a2dp_subevent_signaling_media_codec_sbc_configuration_get_channel_mode(packet);
allocation_method = a2dp_subevent_signaling_media_codec_sbc_configuration_get_allocation_method(packet);
DEBUGV("A2DP Source: Received SBC codec configuration, sampling frequency %u, a2dp_cid 0x%02x, local seid 0x%02x, remote seid 0x%02x.\n",
sbc_configuration.sampling_frequency, cid,
a2dp_subevent_signaling_media_codec_sbc_configuration_get_local_seid(packet),
a2dp_subevent_signaling_media_codec_sbc_configuration_get_remote_seid(packet));
// Adapt Bluetooth spec definition to SBC Encoder expected input
sbc_configuration.allocation_method = (btstack_sbc_allocation_method_t)(allocation_method - 1);
switch (channel_mode) {
case AVDTP_CHANNEL_MODE_JOINT_STEREO:
sbc_configuration.channel_mode = SBC_CHANNEL_MODE_JOINT_STEREO;
break;
case AVDTP_CHANNEL_MODE_STEREO:
sbc_configuration.channel_mode = SBC_CHANNEL_MODE_STEREO;
break;
case AVDTP_CHANNEL_MODE_DUAL_CHANNEL:
sbc_configuration.channel_mode = SBC_CHANNEL_MODE_DUAL_CHANNEL;
break;
case AVDTP_CHANNEL_MODE_MONO:
sbc_configuration.channel_mode = SBC_CHANNEL_MODE_MONO;
break;
default:
btstack_assert(false);
break;
}
dump_sbc_configuration(&sbc_configuration);
btstack_sbc_encoder_init(&sbc_encoder_state, SBC_MODE_STANDARD,
sbc_configuration.block_length, sbc_configuration.subbands,
sbc_configuration.allocation_method, sbc_configuration.sampling_frequency,
sbc_configuration.max_bitpool_value,
sbc_configuration.channel_mode);
break;
}
case A2DP_SUBEVENT_SIGNALING_DELAY_REPORTING_CAPABILITY:
DEBUGV("A2DP Source: remote supports delay report, remote seid %d\n",
avdtp_subevent_signaling_delay_reporting_capability_get_remote_seid(packet));
break;
case A2DP_SUBEVENT_SIGNALING_CAPABILITIES_DONE:
DEBUGV("A2DP Source: All capabilities reported, remote seid %d\n",
avdtp_subevent_signaling_capabilities_done_get_remote_seid(packet));
break;
case A2DP_SUBEVENT_SIGNALING_DELAY_REPORT:
DEBUGV("A2DP Source: Received delay report of %d.%0d ms, local seid %d\n",
avdtp_subevent_signaling_delay_report_get_delay_100us(packet) / 10, avdtp_subevent_signaling_delay_report_get_delay_100us(packet) % 10,
avdtp_subevent_signaling_delay_report_get_local_seid(packet));
break;
case A2DP_SUBEVENT_STREAM_ESTABLISHED:
a2dp_subevent_stream_established_get_bd_addr(packet, address);
status = a2dp_subevent_stream_established_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("A2DP Source: Stream failed, status 0x%02x.\n", status);
break;
}
local_seid = a2dp_subevent_stream_established_get_local_seid(packet);
cid = a2dp_subevent_stream_established_get_a2dp_cid(packet);
(void) local_seid;
DEBUGV("A2DP Source: Stream established a2dp_cid 0x%02x, local_seid 0x%02x, remote_seid 0x%02x\n", cid, local_seid, a2dp_subevent_stream_established_get_remote_seid(packet));
media_tracker.stream_opened = 1;
status = a2dp_source_start_stream(media_tracker.a2dp_cid, media_tracker.local_seid);
break;
case A2DP_SUBEVENT_STREAM_RECONFIGURED:
status = a2dp_subevent_stream_reconfigured_get_status(packet);
local_seid = a2dp_subevent_stream_reconfigured_get_local_seid(packet);
cid = a2dp_subevent_stream_reconfigured_get_a2dp_cid(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("A2DP Source: Stream reconfiguration failed, status 0x%02x\n", status);
break;
}
DEBUGV("A2DP Source: Stream reconfigured a2dp_cid 0x%02x, local_seid 0x%02x\n", cid, local_seid);
status = a2dp_source_start_stream(media_tracker.a2dp_cid, media_tracker.local_seid);
break;
case A2DP_SUBEVENT_STREAM_STARTED:
local_seid = a2dp_subevent_stream_started_get_local_seid(packet);
cid = a2dp_subevent_stream_started_get_a2dp_cid(packet);
play_info.status = AVRCP_PLAYBACK_STATUS_PLAYING;
if (media_tracker.avrcp_cid) {
avrcp_target_set_now_playing_info(media_tracker.avrcp_cid, &_currentTrack, _tracks);
avrcp_target_set_playback_status(media_tracker.avrcp_cid, AVRCP_PLAYBACK_STATUS_PLAYING);
}
a2dp_timer_start(&media_tracker);
DEBUGV("A2DP Source: Stream started, a2dp_cid 0x%02x, local_seid 0x%02x\n", cid, local_seid);
_connected = true;
if (_connectCB) {
_connectCB(_connectData, true);
}
break;
case A2DP_SUBEVENT_STREAMING_CAN_SEND_MEDIA_PACKET_NOW:
local_seid = a2dp_subevent_streaming_can_send_media_packet_now_get_local_seid(packet);
cid = a2dp_subevent_signaling_media_codec_sbc_configuration_get_a2dp_cid(packet);
a2dp_send_media_packet();
break;
case A2DP_SUBEVENT_STREAM_SUSPENDED:
local_seid = a2dp_subevent_stream_suspended_get_local_seid(packet);
cid = a2dp_subevent_stream_suspended_get_a2dp_cid(packet);
play_info.status = AVRCP_PLAYBACK_STATUS_PAUSED;
if (media_tracker.avrcp_cid) {
avrcp_target_set_playback_status(media_tracker.avrcp_cid, AVRCP_PLAYBACK_STATUS_PAUSED);
}
DEBUGV("A2DP Source: Stream paused, a2dp_cid 0x%02x, local_seid 0x%02x\n", cid, local_seid);
a2dp_timer_stop(&media_tracker);
break;
case A2DP_SUBEVENT_STREAM_RELEASED:
play_info.status = AVRCP_PLAYBACK_STATUS_STOPPED;
cid = a2dp_subevent_stream_released_get_a2dp_cid(packet);
local_seid = a2dp_subevent_stream_released_get_local_seid(packet);
DEBUGV("A2DP Source: Stream released, a2dp_cid 0x%02x, local_seid 0x%02x\n", cid, local_seid);
if (cid == media_tracker.a2dp_cid) {
media_tracker.stream_opened = 0;
DEBUGV("A2DP Source: Stream released.\n");
}
if (media_tracker.avrcp_cid) {
avrcp_target_set_now_playing_info(media_tracker.avrcp_cid, NULL, _tracks);
avrcp_target_set_playback_status(media_tracker.avrcp_cid, AVRCP_PLAYBACK_STATUS_STOPPED);
}
a2dp_timer_stop(&media_tracker);
break;
case A2DP_SUBEVENT_SIGNALING_CONNECTION_RELEASED:
cid = a2dp_subevent_signaling_connection_released_get_a2dp_cid(packet);
if (cid == media_tracker.a2dp_cid) {
media_tracker.avrcp_cid = 0;
media_tracker.a2dp_cid = 0;
DEBUGV("A2DP Source: Signaling released.\n\n");
_connected = false;
if (_connectCB) {
_connectCB(_connectData, false);
}
}
break;
default:
break;
}
}
void A2DPSource_::avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
bd_addr_t event_addr;
uint16_t local_cid;
uint8_t status = ERROR_CODE_SUCCESS;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
switch (packet[2]) {
case AVRCP_SUBEVENT_CONNECTION_ESTABLISHED:
local_cid = avrcp_subevent_connection_established_get_avrcp_cid(packet);
status = avrcp_subevent_connection_established_get_status(packet);
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("AVRCP: Connection failed, local cid 0x%02x, status 0x%02x\n", local_cid, status);
return;
}
media_tracker.avrcp_cid = local_cid;
avrcp_subevent_connection_established_get_bd_addr(packet, event_addr);
DEBUGV("AVRCP: Channel to %s successfully opened, avrcp_cid 0x%02x\n", bd_addr_to_str(event_addr), media_tracker.avrcp_cid);
avrcp_target_support_event(media_tracker.avrcp_cid, AVRCP_NOTIFICATION_EVENT_PLAYBACK_STATUS_CHANGED);
avrcp_target_support_event(media_tracker.avrcp_cid, AVRCP_NOTIFICATION_EVENT_TRACK_CHANGED);
avrcp_target_support_event(media_tracker.avrcp_cid, AVRCP_NOTIFICATION_EVENT_NOW_PLAYING_CONTENT_CHANGED);
avrcp_target_set_now_playing_info(media_tracker.avrcp_cid, NULL, _tracks);
DEBUGV("Enable Volume Change notification\n");
avrcp_controller_enable_notification(media_tracker.avrcp_cid, AVRCP_NOTIFICATION_EVENT_VOLUME_CHANGED);
DEBUGV("Enable Battery Status Change notification\n");
avrcp_controller_enable_notification(media_tracker.avrcp_cid, AVRCP_NOTIFICATION_EVENT_BATT_STATUS_CHANGED);
return;
case AVRCP_SUBEVENT_CONNECTION_RELEASED:
DEBUGV("AVRCP Target: Disconnected, avrcp_cid 0x%02x\n", avrcp_subevent_connection_released_get_avrcp_cid(packet));
media_tracker.avrcp_cid = 0;
return;
default:
break;
}
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("Responding to event 0x%02x failed, status 0x%02x\n", packet[2], status);
}
}
void A2DPSource_::avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
uint8_t status = ERROR_CODE_SUCCESS;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
bool button_pressed;
char const * button_state;
avrcp_operation_id_t operation_id;
switch (packet[2]) {
case AVRCP_SUBEVENT_PLAY_STATUS_QUERY:
status = avrcp_target_play_status(media_tracker.avrcp_cid, play_info.song_length_ms, play_info.song_position_ms, play_info.status);
break;
// case AVRCP_SUBEVENT_NOW_PLAYING_INFO_QUERY:
// status = avrcp_target_now_playing_info(avrcp_cid);
// break;
case AVRCP_SUBEVENT_OPERATION:
operation_id = (avrcp_operation_id_t)avrcp_subevent_operation_get_operation_id(packet);
button_pressed = avrcp_subevent_operation_get_button_pressed(packet) > 0;
button_state = button_pressed ? "PRESS" : "RELEASE";
(void) button_state;
DEBUGV("AVRCP Target: operation %s (%s)\n", avrcp_operation2str(operation_id), button_state);
if (_avrcpCB) {
_avrcpCB(_avrcpData, operation_id, button_pressed);
}
if (!button_pressed) {
break;
}
switch (operation_id) {
case AVRCP_OPERATION_ID_PLAY:
status = a2dp_source_start_stream(media_tracker.a2dp_cid, media_tracker.local_seid);
break;
case AVRCP_OPERATION_ID_PAUSE:
status = a2dp_source_pause_stream(media_tracker.a2dp_cid, media_tracker.local_seid);
break;
case AVRCP_OPERATION_ID_STOP:
status = a2dp_source_disconnect(media_tracker.a2dp_cid);
break;
default:
break;
}
break;
default:
break;
}
if (status != ERROR_CODE_SUCCESS) {
DEBUGV("Responding to event 0x%02x failed, status 0x%02x\n", packet[2], status);
}
}
void A2DPSource_::avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
UNUSED(channel);
UNUSED(size);
if (packet_type != HCI_EVENT_PACKET) {
return;
}
if (hci_event_packet_get_type(packet) != HCI_EVENT_AVRCP_META) {
return;
}
if (!media_tracker.avrcp_cid) {
return;
}
switch (packet[2]) {
case AVRCP_SUBEVENT_NOTIFICATION_VOLUME_CHANGED:
DEBUGV("AVRCP Controller: Notification Absolute Volume %d %%\n", avrcp_subevent_notification_volume_changed_get_absolute_volume(packet) * 100 / 127);
if (_volumeCB) {
_volumeCB(_volumeData, avrcp_subevent_notification_volume_changed_get_absolute_volume(packet) * 100 / 127);
}
break;
case AVRCP_SUBEVENT_NOTIFICATION_EVENT_BATT_STATUS_CHANGED:
// see avrcp_battery_status_t
DEBUGV("AVRCP Controller: Notification Battery Status 0x%02x\n", avrcp_subevent_notification_event_batt_status_changed_get_battery_status(packet));
if (_batteryCB) {
_batteryCB(_batteryData, (avrcp_battery_status_t)avrcp_subevent_notification_event_batt_status_changed_get_battery_status(packet));
}
break;
case AVRCP_SUBEVENT_NOTIFICATION_STATE:
DEBUGV("AVRCP Controller: Notification %s - %s\n",
avrcp_event2str(avrcp_subevent_notification_state_get_event_id(packet)),
avrcp_subevent_notification_state_get_enabled(packet) != 0 ? "enabled" : "disabled");
break;
default:
break;
}
}
A2DPSource_ A2DPSource;

View file

@ -0,0 +1,253 @@
/*
A1DP Source (Bluetooth audio sender)
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <Arduino.h>
#include "BluetoothHCI.h"
#include <functional>
#include <list>
#include <memory>
class A2DPSource_ : public Stream {
public:
A2DPSource_() {
}
bool setFrequency(uint32_t rate) {
if (_running || ((rate != 44100) && (rate != 48000))) {
return false;
}
_frequency = rate;
return true;
}
bool setName(const char *name) {
if (_running) {
return false;
}
free(_name);
_name = strdup(name);
return true;
}
void onTransmit(void (*cb)(void *), void *cbData = nullptr) {
_transmitCB = cb;
_transmitData = cbData;
}
void onAVRCP(void (*cb)(void *, avrcp_operation_id_t, int), void *cbData = nullptr) {
_avrcpCB = cb;
_avrcpData = cbData;
}
void onBattery(void (*cb)(void *, avrcp_battery_status_t), void *cbData = nullptr) {
_batteryCB = cb;
_batteryData = cbData;
}
void onVolume(void (*cb)(void *, int), void *cbData = nullptr) {
_volumeCB = cb;
_volumeData = cbData;
}
void onConnect(void (*cb)(void *, bool), void *cbData = nullptr) {
_connectCB = cb;
_connectData = cbData;
}
bool setBufferSize(size_t size) {
if (_running || (size & 127) || (size < 1024)) {
return false;
}
_pcmBufferSize = size;
return true;
}
bool getUnderflow() {
if (!_running) {
return false;
}
__lockBluetooth();
auto ret = _underflow;
_underflow = false;
__unlockBluetooth();
return ret;
}
const uint8_t *getSinkAddress() {
if (!_connected) {
return nullptr;
} else {
return _sinkAddress;
}
}
bool begin();
std::list<BTDeviceInfo> scan(uint32_t mask = BluetoothHCI_::speaker_cod, int scanTimeSec = 5, bool async = false) {
return _hci.scan(mask, scanTimeSec, async);
}
bool connect(const uint8_t *addr = nullptr);
bool connected() {
return _connected;
}
bool disconnect();
void clearPairing();
// from Stream
virtual int available() override {
return 0; // Unreadable, this is output only
}
virtual int read() override {
return 0;
}
virtual int peek() override {
return 0;
}
virtual void flush() override {
}
// from Print (see notes on write() methods below)
virtual size_t write(const uint8_t *buffer, size_t size) override;
virtual int availableForWrite() override;
virtual size_t write(uint8_t s) override {
(void) s;
return 0; // Never any reason to write 8-bit data, make it always fail.
}
private:
BluetoothHCI_ _hci;
bool _running = false;
bool _connected = false;
char *_name = nullptr;
int _frequency = 44100;
// Callbacks
void (*_transmitCB)(void *) = nullptr;
void *_transmitData;
void (*_avrcpCB)(void *, avrcp_operation_id_t, int) = nullptr;
void *_avrcpData;
void (*_batteryCB)(void *, avrcp_battery_status_t) = nullptr;
void *_batteryData;
void (*_volumeCB)(void *, int) = nullptr;
void *_volumeData;
void (*_connectCB)(void *, bool) = nullptr;
void *_connectData;
const uint8_t media_sbc_codec_capabilities[4] = {
(AVDTP_SBC_44100 << 4) | AVDTP_SBC_STEREO,
0xFF,//(AVDTP_SBC_BLOCK_LENGTH_16 << 4) | (AVDTP_SBC_SUBBANDS_8 << 2) | AVDTP_SBC_ALLOCATION_METHOD_LOUDNESS,
2, 53
};
uint8_t media_sbc_codec_configuration[4];
enum {SBC_STORAGE_SIZE = 1030, AUDIO_TIMEOUT_MS = 5};
typedef struct {
uint16_t a2dp_cid;
uint8_t local_seid;
uint8_t remote_seid;
uint8_t stream_opened;
uint16_t avrcp_cid;
uint32_t time_audio_data_sent; // ms
uint32_t acc_num_missed_samples;
uint32_t samples_ready;
btstack_timer_source_t audio_timer;
uint8_t streaming;
int max_media_payload_size;
uint32_t rtp_timestamp;
uint8_t sbc_storage[SBC_STORAGE_SIZE];
uint16_t sbc_storage_count;
uint8_t sbc_ready_to_send;
uint8_t volume;
} a2dp_media_sending_context_t;
typedef struct {
int reconfigure;
int num_channels;
int sampling_frequency;
int block_length;
int subbands;
int min_bitpool_value;
int max_bitpool_value;
btstack_sbc_channel_mode_t channel_mode;
btstack_sbc_allocation_method_t allocation_method;
} media_codec_configuration_sbc_t;
uint8_t sdp_a2dp_source_service_buffer[150];
uint8_t sdp_avrcp_target_service_buffer[200];
uint8_t sdp_avrcp_controller_service_buffer[200];
uint8_t device_id_sdp_service_buffer[100];
media_codec_configuration_sbc_t sbc_configuration;
btstack_sbc_encoder_state_t sbc_encoder_state;
a2dp_media_sending_context_t media_tracker;
typedef struct {
uint8_t track_id[8];
uint32_t song_length_ms;
avrcp_playback_status_t status;
uint32_t song_position_ms; // 0xFFFFFFFF if not supported
} avrcp_play_status_info_t;
avrcp_play_status_info_t play_info;
void dump_sbc_configuration(media_codec_configuration_sbc_t * configuration);
void a2dp_timer_start(a2dp_media_sending_context_t * context);
void a2dp_timer_stop(a2dp_media_sending_context_t * context);
int16_t *_pcmBuffer = nullptr;
size_t _pcmBufferSize = 4096; // Multiple of 128 required
uint32_t _pcmWriter;
uint32_t _pcmReader;
bool _underflow;
int a2dp_fill_sbc_audio_buffer(a2dp_media_sending_context_t * context);
void a2dp_audio_timeout_handler(btstack_timer_source_t * timer);
void a2dp_send_media_packet();
int _tracks = 1;
avrcp_track_t _currentTrack;
uint8_t _sinkAddress[6];
void a2dp_source_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
void avrcp_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
};
extern A2DPSource_ A2DPSource;

View file

@ -0,0 +1,3 @@
#include "BluetoothDevice.h"
#include "BluetoothHCI.h"
#include "A2DPSource.h"

View file

@ -0,0 +1,66 @@
/*
Bluetooth device helper class
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
class BTDeviceInfo {
public:
BTDeviceInfo(uint32_t dc, const uint8_t addr[6], int rssi, const char *name) {
_deviceClass = dc;
memcpy(_address, addr, sizeof(_address));
sprintf(_addressString, "%02x:%02x:%02x:%02x:%02x:%02x", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
_rssi = rssi;
_name = strdup(name);
}
// Copy constructor to ensure we deep-copy the string
BTDeviceInfo(const BTDeviceInfo &b) {
_deviceClass = b._deviceClass;
memcpy(_address, b._address, sizeof(_address));
memcpy(_addressString, b._addressString, sizeof(_addressString));
_rssi = b._rssi;
_name = strdup(b._name);
}
~BTDeviceInfo() {
free(_name);
}
uint32_t deviceClass() {
return _deviceClass;
}
const uint8_t *address() {
return _address;
}
const char *addressString() {
return _addressString;
}
int rssi() {
return _rssi;
}
const char *name() {
return _name;
}
private:
uint32_t _deviceClass;
uint8_t _address[6];
char _addressString[18];
int8_t _rssi;
char *_name;
};

View file

@ -0,0 +1,162 @@
/*
Bluetooth HCI packet handler class
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <Arduino.h>
#include "btstack.h"
#include <list>
#include <memory>
#include "BluetoothHCI.h"
#define CCALLBACKNAME _BTHCICB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(_BTHCICB<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(_BTHCICB<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
void BluetoothHCI_::install() {
// Register for HCI events.
hci_event_callback_registration.callback = PACKETHANDLERCB(BluetoothHCI_, hci_packet_handler);
hci_add_event_handler(&hci_event_callback_registration);
}
void BluetoothHCI_::begin() {
_running = true;
hci_power_control(HCI_POWER_ON);
}
void BluetoothHCI_::uninstall() {
__lockBluetooth();
hci_remove_event_handler(&hci_event_callback_registration);
_running = false;
__unlockBluetooth();
}
bool BluetoothHCI_::running() {
return _hciRunning;
}
std::list<BTDeviceInfo> BluetoothHCI_::scan(uint32_t mask, int scanTimeSec, bool async) {
_scanMask = mask;
_btdList.clear();
if (!_running) {
return _btdList;
}
_scanning = true;
while (!_hciRunning) {
DEBUGV("HCI::scan(): Waiting for HCI to come up\n");
delay(10);
}
int inquiryTime = (scanTimeSec * 1000) / 1280; // divide by 1.280
if (!inquiryTime) {
inquiryTime = 1;
}
DEBUGV("HCI::scan(): inquiry start\n");
__lockBluetooth();
gap_inquiry_start(inquiryTime);
__unlockBluetooth();
if (async) {
return _btdList;
}
while (_scanning) {
delay(10);
}
DEBUGV("HCI::scan(): inquiry end\n");
return _btdList;
}
bool BluetoothHCI_::scanAsyncDone() {
return _scanning;
}
std::list<BTDeviceInfo> BluetoothHCI_::scanAsyncResult() {
return _btdList;
}
void BluetoothHCI_::hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
(void)channel;
(void)size;
if (packet_type != HCI_EVENT_PACKET) {
return;
}
bd_addr_t address;
uint32_t cod;
int8_t rssi;
const char *name;
char name_buffer[241];
switch (hci_event_packet_get_type(packet)) {
case BTSTACK_EVENT_STATE:
_hciRunning = (btstack_event_state_get_state(packet) == HCI_STATE_WORKING);
break;
case HCI_EVENT_PIN_CODE_REQUEST:
hci_event_pin_code_request_get_bd_addr(packet, address);
gap_pin_code_response(address, "0000");
break;
case GAP_EVENT_INQUIRY_RESULT:
if (!_scanning) {
return;
}
gap_event_inquiry_result_get_bd_addr(packet, address);
cod = gap_event_inquiry_result_get_class_of_device(packet);
if (gap_event_inquiry_result_get_rssi_available(packet)) {
rssi = gap_event_inquiry_result_get_rssi(packet);
} else {
rssi = -128;
}
if (gap_event_inquiry_result_get_name_available(packet)) {
int name_len = gap_event_inquiry_result_get_name_len(packet);
memcpy(name_buffer, gap_event_inquiry_result_get_name(packet), name_len);
name_buffer[name_len] = 0;
name = name_buffer;
} else {
name = "";
}
DEBUGV("HCI: Scan found '%s', COD 0x%08X, RSSI %d, MAC %02X:%02X:%02X:%02X:%02X:%02X\n", name, (unsigned int)cod, rssi, address[0], address[1], address[2], address[3], address[4], address[5]);
if ((_scanMask & cod) == _scanMask) {
// Sometimes we get multiple reports for the same MAC, so remove any old reports since newer will have newer RSSI
bool updated = false;
for (auto itr = _btdList.begin(); (itr != _btdList.end()) && !updated; itr++) {
if (!memcmp(itr->address(), address, sizeof(address))) {
// Sometimes the name is missing on reports, so if we found it once preserve it
if (!name_buffer[0] && itr->name()[0]) {
strcpy(name_buffer, itr->name());
}
_btdList.erase(itr);
updated = true;
}
}
BTDeviceInfo btd(cod, address, rssi, name);
_btdList.push_back(btd);
}
break;
case GAP_EVENT_INQUIRY_COMPLETE:
_scanning = false;
break;
default:
break;
}
}

View file

@ -0,0 +1,52 @@
/*
Bluetooth HCI packet handler class
Copyright (c) 2024 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include <list>
#include <memory>
#include "BluetoothDevice.h"
#include <btstack.h>
class BluetoothHCI_ {
public:
void install();
void begin();
void uninstall();
bool running();
static const uint32_t speaker_cod = 0x200000 | 0x040000 | 0x000400; // Service Class: Rendering | Audio, Major Device Class: Audio
static const uint32_t any_cod = 0;
std::list<BTDeviceInfo> scan(uint32_t mask, int scanTimeSec = 5, bool async = false);
bool scanAsyncDone();
std::list<BTDeviceInfo> scanAsyncResult();
private:
void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
btstack_packet_callback_registration_t hci_event_callback_registration;
volatile bool _hciRunning = false;
uint32_t _scanMask;
std::list<BTDeviceInfo> _btdList;
volatile bool _scanning = false;
bool _running = false;
};

View file

@ -29,7 +29,7 @@ const char *softAP_password = APPSK;
/* hostname for mDNS. Should work at least on windows. Try http://picow.local */
const char *myHostname = "picow";
/* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */
/* Don't set this wifi credentials. They are configured at runtime and stored on EEPROM */
char ssid[33] = "";
char password[65] = "";

View file

@ -19,8 +19,6 @@ setCanSendNowCB KEYWORD2
startHID KEYWORD2
connected KEYWORD2
send KEYWORD2
lockBluetooth KEYWORD2
unlockBluetooth KEYWORD2
getCID KEYWORD2
setBattery KEYWORD2

View file

@ -20,4 +20,80 @@
#include "PicoBluetoothBLEHID.h"
#define CCALLBACKNAME _BLEHIDCB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
bool PicoBluetoothBLEHID_::startHID(const char *localName, const char *hidName, uint16_t appearance, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize, int battery) {
if (_running) {
return false;
}
_running = true;
_buildAdvData(localName, appearance);
_buildAttdb(hidName);
_battery = battery;
// Setup L2CAP
l2cap_init();
// Setup SM
sm_init();
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING);
// Setup ATT server
att_server_init(_attdb, NULL, NULL);
// Setup battery service
battery_service_server_init(battery);
// Setup device information service
device_information_service_server_init();
// Setup HID Device service, depending on activated reports
uint8_t numreports = 1; //start with 1 (feature report)
if (__BLEInstallKeyboard) {
numreports += 2; //add keycodes + consumer keys
}
if (__BLEInstallMouse) {
numreports += 1;
}
if (__BLEInstallJoystick) {
numreports += 1;
}
//allocate memory for hid reports
_reportStorage = (hids_device_report_t *) malloc(sizeof(hids_device_report_t) * numreports);
hids_device_init_with_storage(0, hidDescriptor, hidDescriptorSize, numreports, _reportStorage);
// Setup advertisements
uint16_t adv_int_min = 0x0030;
uint16_t adv_int_max = 0x0030;
uint8_t adv_type = 0;
bd_addr_t null_addr;
memset(null_addr, 0, 6);
gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
gap_advertisements_set_data(_advDataLen, _advData);
gap_advertisements_enable(1);
// Register for HCI events
_hci_event_callback_registration.callback = PACKETHANDLERCB(PicoBluetoothBLEHID_, packetHandler);
hci_add_event_handler(&_hci_event_callback_registration);
// Register for SM events
_sm_event_callback_registration.callback = PACKETHANDLERCB(PicoBluetoothBLEHID_, packetHandler);
sm_add_event_handler(&_sm_event_callback_registration);
// Register for HIDS events
hids_device_register_packet_handler(PACKETHANDLERCB(PicoBluetoothBLEHID_, packetHandler));
// GO!
hci_power_control(HCI_POWER_ON);
return true;
}
PicoBluetoothBLEHID_ PicoBluetoothBLEHID;

View file

@ -79,79 +79,9 @@ public:
_onCanSendNow = cb;
}
bool startHID(const char *localName, const char *hidName, uint16_t appearance, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize, int battery = 100) {
if (_running) {
return false;
}
_running = true;
_buildAdvData(localName, appearance);
_buildAttdb(hidName);
_battery = battery;
// Setup L2CAP
l2cap_init();
// Setup SM
sm_init();
sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING);
// Setup ATT server
att_server_init(_attdb, NULL, NULL);
// Setup battery service
battery_service_server_init(battery);
// Setup device information service
device_information_service_server_init();
// Setup HID Device service, depending on activated reports
uint8_t numreports = 1; //start with 1 (feature report)
if (__BLEInstallKeyboard) {
numreports += 2; //add keycodes + consumer keys
}
if (__BLEInstallMouse) {
numreports += 1;
}
if (__BLEInstallJoystick) {
numreports += 1;
}
//allocate memory for hid reports
_reportStorage = (hids_device_report_t *) malloc(sizeof(hids_device_report_t) * numreports);
hids_device_init_with_storage(0, hidDescriptor, hidDescriptorSize, numreports, _reportStorage);
// Setup advertisements
uint16_t adv_int_min = 0x0030;
uint16_t adv_int_max = 0x0030;
uint8_t adv_type = 0;
bd_addr_t null_addr;
memset(null_addr, 0, 6);
gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
gap_advertisements_set_data(_advDataLen, _advData);
gap_advertisements_enable(1);
// Register for HCI events
_hci_event_callback_registration.callback = PacketHandlerWrapper;
hci_add_event_handler(&_hci_event_callback_registration);
// Register for SM events
_sm_event_callback_registration.callback = PacketHandlerWrapper;
sm_add_event_handler(&_sm_event_callback_registration);
// Register for HIDS events
hids_device_register_packet_handler(PacketHandlerWrapper);
// GO!
hci_power_control(HCI_POWER_ON);
return true;
}
static void PacketHandlerWrapper(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t packet_size) {
PicoBluetoothBLEHID.packetHandler(packet_type, channel, packet, packet_size);
}
bool startHID(const char *localName, const char *hidName, uint16_t appearance, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize, int battery = 100);
private:
void packetHandler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
uint8_t result;
uint8_t reportID;
@ -214,14 +144,6 @@ public:
if (result) {
Serial.printf("Error sending %d - report ID: %d\n", result, reportID);
}
//else Serial.printf("Sent report for ID: %d\n",reportID);
#if 0
Serial.printf("Sending report for ID %d, len: %d:\n", reportID, _sendReportLen);
for (uint8_t i = 0; i < _sendReportLen; i++) {
Serial.printf("0x%02X - ", ((const uint8_t *)_sendReport)[i]);
}
Serial.println("");
#endif
break;
default:
break;
@ -237,7 +159,7 @@ public:
break;
}
}
public:
bool end() {
if (_running) {
hci_power_control(HCI_POWER_OFF);
@ -259,9 +181,9 @@ public:
_needToSend = true;
_sendReport = rpt;
_sendReportLen = len;
lockBluetooth();
__lockBluetooth();
hids_device_request_can_send_now_event(_con_handle);
unlockBluetooth();
__unlockBluetooth();
while (connected() && _needToSend) {
/* noop busy wait */
}
@ -276,14 +198,6 @@ public:
return true;
}
static void lockBluetooth() {
async_context_acquire_lock_blocking(cyw43_arch_async_context());
}
static void unlockBluetooth() {
async_context_release_lock(cyw43_arch_async_context());
}
uint8_t *_attdb = nullptr;
int _attdbLen = 0;

View file

@ -20,4 +20,81 @@
#include "PicoBluetoothHID.h"
#define CCALLBACKNAME _BTHIDCB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
bool PicoBluetoothHID_::startHID(const char *localName, const char *hidName, uint16_t hidClass, uint8_t hidSubclass, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize) {
if (_running) {
return false;
}
_running = true;
// Allow finding via inquiry
gap_discoverable_control(1);
// Use Limited Discoverable Mode; Peripheral; Keyboard as CoD
gap_set_class_of_device(hidClass);
// Set local name to be identified - zeroes will be replaced by actual BD ADDR
gap_set_local_name(localName);
// Allow for role switch in general and sniff mode
gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE);
// Allow for role switch on outgoing connections - this allow HID Host to become master when we re-connect to it
gap_set_allow_role_switch(true);
// L2CAP
l2cap_init();
#ifdef ENABLE_BLE
// Initialize LE Security Manager. Needed for cross-transport key derivation
sm_init();
#endif
// SDP Server
sdp_init();
bzero(_hid_service_buffer, sizeof(_hid_service_buffer));
const uint8_t hid_boot_device = 0;
const uint8_t hid_virtual_cable = 0;
const uint8_t hid_remote_wake = 1;
const uint8_t hid_reconnect_initiate = 1;
const uint8_t hid_normally_connectable = 1;
// When not set to 0xffff, sniff and sniff subrating are enabled
const uint16_t host_max_latency = 1600;
const uint16_t host_min_timeout = 3200;
hid_sdp_record_t hid_params = {
hidClass, hidSubclass,
hid_virtual_cable, hid_remote_wake,
hid_reconnect_initiate, (bool)hid_normally_connectable,
(bool)hid_boot_device,
host_max_latency, host_min_timeout,
3200,
hidDescriptor,
hidDescriptorSize,
hidName
};
hid_create_sdp_record(_hid_service_buffer, 0x10001, &hid_params);
sdp_register_service(_hid_service_buffer);
// See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID
// device info: BlueKitchen GmbH, product 1, version 1
device_id_create_sdp_record(_device_id_sdp_service_buffer, 0x10003, DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
sdp_register_service(_device_id_sdp_service_buffer);
// HID Device
hid_device_init(hid_boot_device, hidDescriptorSize, hidDescriptor);
// register for HCI events
_hci_event_callback_registration.callback = PACKETHANDLERCB(PicoBluetoothHID_, packetHandler);
hci_add_event_handler(&_hci_event_callback_registration);
// register for HID events
hid_device_register_packet_handler(PACKETHANDLERCB(PicoBluetoothHID_, packetHandler));
hci_power_control(HCI_POWER_ON);
return true;
}
PicoBluetoothHID_ PicoBluetoothHID;

View file

@ -68,80 +68,9 @@ public:
_onCanSendNow = cb;
}
bool startHID(const char *localName, const char *hidName, uint16_t hidClass, uint8_t hidSubclass, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize) {
if (_running) {
return false;
}
_running = true;
// Allow finding via inquiry
gap_discoverable_control(1);
// Use Limited Discoverable Mode; Peripheral; Keyboard as CoD
gap_set_class_of_device(hidClass);
// Set local name to be identified - zeroes will be replaced by actual BD ADDR
gap_set_local_name(localName);
// Allow for role switch in general and sniff mode
gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE);
// Allow for role switch on outgoing connections - this allow HID Host to become master when we re-connect to it
gap_set_allow_role_switch(true);
// L2CAP
l2cap_init();
#ifdef ENABLE_BLE
// Initialize LE Security Manager. Needed for cross-transport key derivation
sm_init();
#endif
// SDP Server
sdp_init();
bzero(_hid_service_buffer, sizeof(_hid_service_buffer));
const uint8_t hid_boot_device = 0;
const uint8_t hid_virtual_cable = 0;
const uint8_t hid_remote_wake = 1;
const uint8_t hid_reconnect_initiate = 1;
const uint8_t hid_normally_connectable = 1;
// When not set to 0xffff, sniff and sniff subrating are enabled
const uint16_t host_max_latency = 1600;
const uint16_t host_min_timeout = 3200;
hid_sdp_record_t hid_params = {
hidClass, hidSubclass,
hid_virtual_cable, hid_remote_wake,
hid_reconnect_initiate, (bool)hid_normally_connectable,
(bool)hid_boot_device,
host_max_latency, host_min_timeout,
3200,
hidDescriptor,
hidDescriptorSize,
hidName
};
hid_create_sdp_record(_hid_service_buffer, 0x10001, &hid_params);
sdp_register_service(_hid_service_buffer);
// See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID
// device info: BlueKitchen GmbH, product 1, version 1
device_id_create_sdp_record(_device_id_sdp_service_buffer, 0x10003, DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
sdp_register_service(_device_id_sdp_service_buffer);
// HID Device
hid_device_init(hid_boot_device, hidDescriptorSize, hidDescriptor);
// register for HCI events
_hci_event_callback_registration.callback = PacketHandlerWrapper;
hci_add_event_handler(&_hci_event_callback_registration);
// register for HID events
hid_device_register_packet_handler(PacketHandlerWrapper);
hci_power_control(HCI_POWER_ON);
return true;
}
static void PacketHandlerWrapper(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t packet_size) {
PicoBluetoothHID.packetHandler(packet_type, channel, packet, packet_size);
}
bool startHID(const char *localName, const char *hidName, uint16_t hidClass, uint8_t hidSubclass, const uint8_t *hidDescriptor, uint16_t hidDescriptorSize);
private:
void packetHandler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
uint8_t status;
if (type != HCI_EVENT_PACKET) {
@ -198,7 +127,7 @@ public:
}
}
}
public:
bool end() {
if (_running) {
hci_power_control(HCI_POWER_OFF);
@ -217,23 +146,15 @@ public:
_sendReportID = id;
_sendReport = rpt;
_sendReportLen = len;
lockBluetooth();
__lockBluetooth();
hid_device_request_can_send_now_event(getCID());
unlockBluetooth();
__unlockBluetooth();
while (connected() && _needToSend) {
/* noop busy wait */
}
return connected();
}
static void lockBluetooth() {
async_context_acquire_lock_blocking(cyw43_arch_async_context());
}
static void unlockBluetooth() {
async_context_release_lock(cyw43_arch_async_context());
}
uint16_t getCID() {
return _hid_cid;
}

View file

@ -1336,7 +1336,7 @@ bool MDNSResponder::_writeMDNSHostDomain(const char* p_pcHostname, bool p_bPrepe
A very simple form of name compression is applied here: see '_writeMDNSHostDomain'
The cache differentiates of course between service domains which includes
the instance name (p_bIncludeName is set) and thoose who don't.
the instance name (p_bIncludeName is set) and those who don't.
*/
bool

View file

@ -20,6 +20,13 @@
#include "SerialBT.h"
#include <CoreMutex.h>
#define CCALLBACKNAME _SERIALBTCB
#include <ctocppcallback.h>
#define PACKETHANDLERCB(class, cbFcn) \
(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__>::func = std::bind(&class::cbFcn, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), \
static_cast<btstack_packet_handler_t>(CCALLBACKNAME<void(uint8_t, uint16_t, uint8_t*, uint16_t), __COUNTER__ - 1>::callback))
bool SerialBT_::setFIFOSize(size_t size) {
if (!size || _running) {
@ -48,7 +55,7 @@ void SerialBT_::begin(unsigned long baud, uint16_t config) {
_reader = 0;
// register for HCI events
_hci_event_callback_registration.callback = &SerialBT_::PacketHandlerWrapper;
_hci_event_callback_registration.callback = PACKETHANDLERCB(SerialBT_, packetHandler);
hci_add_event_handler(&_hci_event_callback_registration);
l2cap_init();
@ -59,7 +66,7 @@ void SerialBT_::begin(unsigned long baud, uint16_t config) {
#endif
rfcomm_init();
rfcomm_register_service(SerialBT_::PacketHandlerWrapper, RFCOMM_SERVER_CHANNEL, 0xffff); // reserved channel, mtu limited by l2cap
rfcomm_register_service(PACKETHANDLERCB(SerialBT_, packetHandler), RFCOMM_SERVER_CHANNEL, 0xffff); // reserved channel, mtu limited by l2cap
// init SDP, create record for SPP and register with SDP
sdp_init();
@ -84,9 +91,9 @@ void SerialBT_::end() {
_running = false;
hci_power_control(HCI_POWER_OFF);
lockBluetooth();
__lockBluetooth();
delete[] _queue;
unlockBluetooth();
__unlockBluetooth();
}
int SerialBT_::peek() {
@ -121,10 +128,10 @@ bool SerialBT_::overflow() {
return false;
}
lockBluetooth();
__lockBluetooth();
bool ovf = _overflow;
_overflow = false;
unlockBluetooth();
__unlockBluetooth();
return ovf;
}
@ -160,9 +167,9 @@ size_t SerialBT_::write(const uint8_t *p, size_t len) {
}
_writeBuff = p;
_writeLen = len;
lockBluetooth();
__lockBluetooth();
rfcomm_request_can_send_now_event(_channelID);
unlockBluetooth();
__unlockBluetooth();
while (_connected && _writeLen) {
/* noop busy wait */
}

View file

@ -60,26 +60,13 @@ public:
(void) unused;
}
static void PacketHandlerWrapper(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t packet_size) {
SerialBT.packetHandler(packet_type, channel, packet, packet_size);
}
void packetHandler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size);
static void lockBluetooth() {
async_context_acquire_lock_blocking(cyw43_arch_async_context());
}
static void unlockBluetooth() {
async_context_release_lock(cyw43_arch_async_context());
}
private:
bool _running = false;
mutex_t _mutex;
bool _overflow = false;
volatile bool _connected = false;
void packetHandler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size);
// Lockless, IRQ-handled circular queue
uint32_t _writer;

View file

@ -104,11 +104,14 @@ void CYW43::end() {
_netif = nullptr;
cyw43_deinit(&cyw43_state);
}
int fails = 0;
int calls = 0;
uint16_t CYW43::sendFrame(const uint8_t* data, uint16_t datalen) {
calls++;
if (0 == cyw43_send_ethernet(_self, _itf, datalen, data, false)) {
return datalen;
}
fails++;
return 0;
}

View file

@ -81,7 +81,7 @@ rm exclude.txt
# Get previous release name
curl --silent https://api.github.com/repos/earlephilhower/arduino-pico/releases > releases.json
# Previous final release (prerelase == false)
# Previous final release (prerelease == false)
prev_release=$(jq -r '. | map(select(.draft == false and .prerelease == false)) | sort_by(.created_at | - fromdateiso8601) | .[0].tag_name' releases.json)
# Previous release (possibly a pre-release)
prev_any_release=$(jq -r '. | map(select(.draft == false)) | sort_by(.created_at | - fromdateiso8601) | .[0].tag_name' releases.json)

View file

@ -15,7 +15,7 @@ for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleF
./libraries/JoystickBLE ./libraries/KeyboardBLE ./libraries/MouseBLE \
./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60 \
./libraries/SPISlave ./libraries/lwIP_ESPHost ./libraries/FatFS\
./libraries/FatFSUSB; do
./libraries/FatFSUSB ./libraries/BluetoothAudio; do
find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" \) -a \! -path '*api*' -exec astyle --suffix=none --options=./tests/astyle_core.conf \{\} \;
find $dir -type f -name "*.ino" -exec astyle --suffix=none --options=./tests/astyle_examples.conf \{\} \;
done

View file

@ -65,9 +65,9 @@ def compile(tmp_dir, sketch, cache, tools_dir, hardware_dir, ide_path, f, args):
'dbgport={dbgport},' \
'dbglvl={dbglvl},' \
'usbstack={usbstack}'.format(**vars(args))
if ("libraries/WiFi" in sketch) or ("/ArduinoOTA" in sketch) or ("/HTTPClient" in sketch) or ('/HTTPUpdate' in sketch) or ('/WebServer' in sketch) or ('/DNSServer' in sketch) or ('/BT' in sketch) or ('/BLE' in sketch):
if ("libraries/WiFi" in sketch) or ("/ArduinoOTA" in sketch) or ("/HTTPClient" in sketch) or ('/HTTPUpdate' in sketch) or ('/WebServer' in sketch) or ('/DNSServer' in sketch) or ('/BT' in sketch) or ('/BLE' in sketch) or ('/Bluetooth' in sketch):
fqbn = fqbn.replace("rpipico", "rpipicow")
if ('/BT' in sketch) or ('/BLE' in sketch):
if ('/BT' in sketch) or ('/BLE' in sketch) or ('/Bluetooth' in sketch):
fqbn = fqbn + ",ipbtstack=ipv4btcble"
cmd += [fqbn]
cmd += ['-built-in-libraries', ide_path + '/libraries']

View file

@ -139,6 +139,8 @@ set(picow_bt_link_libraries
pico_btstack_cyw43
pico_btstack_ble
pico_btstack_classic
pico_btstack_sbc_encoder
pico_btstack_sbc_decoder
)
target_link_libraries(picow-noipv6-btc-ble

View file

@ -38,9 +38,9 @@ extern unsigned long __lwip_rand(void);
#define LWIP_NETIF_LINK_CALLBACK 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETCONN 0
#define MEM_STATS 0
#define SYS_STATS 0
#define MEMP_STATS 0
#define MEM_STATS 1
#define SYS_STATS 1
#define MEMP_STATS 1
#define LINK_STATS 0
// #define ETH_PAD_SIZE 2
#define LWIP_CHKSUM_ALGORITHM 0
@ -72,6 +72,8 @@ extern void __setSystemTime(unsigned long long sec, unsigned long us);
#define LWIP_DEBUG 1
#define LWIP_STATS 1
#define LWIP_STATS_DISPLAY 1
#define MEMP_STATS 1
#define MEM_STATS 1
#endif
#define ETHARP_DEBUG LWIP_DBG_OFF

View file

@ -65,3 +65,11 @@ extern "C" void initVariant() {
cyw43_arch_init_with_country(WIFICC);
}
}
extern "C" void __lockBluetooth() {
async_context_acquire_lock_blocking(cyw43_arch_async_context());
}
extern "C" void __unlockBluetooth() {
async_context_release_lock(cyw43_arch_async_context());
}

View file

@ -3,7 +3,16 @@
// Pin definitions taken from:
// https://datasheets.raspberrypi.org/pico/pico-datasheet.pdf
extern bool __isPicoW;
extern bool __isPicoW;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
extern void __lockBluetooth();
extern void __unlockBluetooth();
#ifdef __cplusplus
}; // extern "C"
#endif // __cplusplus
// LEDs
#define PIN_LED (32u)