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:
parent
367200a2c8
commit
ec5e62e533
40 changed files with 65754 additions and 221 deletions
2
.github/workflows/pull-request.yml
vendored
2
.github/workflows/pull-request.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
32
cores/rp2040/ctocppcallback.h
Normal file
32
cores/rp2040/ctocppcallback.h
Normal 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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
lib/libpico.a
BIN
lib/libpico.a
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
98
libraries/BluetoothAudio/examples/A2DPSource/A2DPSource.ino
Normal file
98
libraries/BluetoothAudio/examples/A2DPSource/A2DPSource.ino
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
64067
libraries/BluetoothAudio/examples/A2DPSource/raw.h
Normal file
64067
libraries/BluetoothAudio/examples/A2DPSource/raw.h
Normal file
File diff suppressed because it is too large
Load diff
47
libraries/BluetoothAudio/keywords.txt
Normal file
47
libraries/BluetoothAudio/keywords.txt
Normal 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)
|
||||
#######################################
|
||||
10
libraries/BluetoothAudio/library.properties
Normal file
10
libraries/BluetoothAudio/library.properties
Normal 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
|
||||
736
libraries/BluetoothAudio/src/A2DPSource.cpp
Normal file
736
libraries/BluetoothAudio/src/A2DPSource.cpp
Normal 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;
|
||||
253
libraries/BluetoothAudio/src/A2DPSource.h
Normal file
253
libraries/BluetoothAudio/src/A2DPSource.h
Normal 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;
|
||||
3
libraries/BluetoothAudio/src/BluetoothAudio.h
Normal file
3
libraries/BluetoothAudio/src/BluetoothAudio.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#include "BluetoothDevice.h"
|
||||
#include "BluetoothHCI.h"
|
||||
#include "A2DPSource.h"
|
||||
66
libraries/BluetoothAudio/src/BluetoothDevice.h
Normal file
66
libraries/BluetoothAudio/src/BluetoothDevice.h
Normal 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;
|
||||
};
|
||||
162
libraries/BluetoothAudio/src/BluetoothHCI.cpp
Normal file
162
libraries/BluetoothAudio/src/BluetoothHCI.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
52
libraries/BluetoothAudio/src/BluetoothHCI.h
Normal file
52
libraries/BluetoothAudio/src/BluetoothHCI.h
Normal 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;
|
||||
};
|
||||
|
|
@ -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] = "";
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ setCanSendNowCB KEYWORD2
|
|||
startHID KEYWORD2
|
||||
connected KEYWORD2
|
||||
send KEYWORD2
|
||||
lockBluetooth KEYWORD2
|
||||
unlockBluetooth KEYWORD2
|
||||
getCID KEYWORD2
|
||||
setBattery KEYWORD2
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue