Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
*.egg-info/
dist/
build/
cpp/build*/
.eggs/
.venv/
.pytest_cache/
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ PYTHON = $(VENV)/bin/python

$(VENV)/bin/activate:
python3 -m venv $(VENV)
$(PIP) install -q --upgrade pip
$(PIP) install -q pytest
@if [ "$$(uname -s)" = "Darwin" ]; then \
echo "Installing macOS Bluetooth dependencies (PyObjC) in virtualenv..."; \
$(PIP) install -q pyobjc-core pyobjc-framework-Cocoa pyobjc-framework-IOBluetooth; \
fi

python-setup: $(VENV)/bin/activate ## Set up Python virtualenv
$(PIP) install -q -e $(PYTHON_DIR)
Expand Down
58 changes: 38 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
[![Python 3](https://img.shields.io/badge/Python-3-3572A5.svg)](python/)
[![Rust](https://img.shields.io/badge/Rust-1.70+-DEA584.svg)](rust/)
[![C++17](https://img.shields.io/badge/C++-17-f34b7d.svg)](cpp/)
[![Platform: Linux](https://img.shields.io/badge/Platform-Linux-orange.svg)](https://kernel.org)
[![Platform: Linux / macOS](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS-blue.svg)](#)

**Control Bose headphones from Linux — no app, no cloud, no account.**
**Control Bose headphones from Linux and macOS — no app, no cloud, no account.**

![bosectl CLI](docs/media/screenshot.png)

Expand All @@ -23,28 +23,28 @@ connection to the headphones.

## Supported Devices

| Device | NC Control | EQ | Spatial | Profiles | Buttons | Status |
|--------|-----------|-----|---------|----------|---------|--------|
| **QC Ultra Headphones 2** | CNC 0-10 slider | 3-band | room/head | 7 custom slots | Shortcut remap | Verified |
| **QuietComfort 35 / 35 II** | ANR off/high/wind/low | — | — | — | Action remap (VPA/ANC) | Verified |
| Device | NC Control | EQ | Spatial | Profiles | Buttons | Status |
| --------------------------- | --------------------- | ------ | --------- | -------------- | ---------------------- | -------- |
| **QC Ultra Headphones 2** | CNC 0-10 slider | 3-band | room/head | 7 custom slots | Shortcut remap | Verified |
| **QuietComfort 35 / 35 II** | ANR off/high/wind/low | — | — | — | Action remap (VPA/ANC) | Verified |

### Device Roadmap

The library includes a [device catalog](docs/architecture.md#device-catalog)
of all known BMAP-capable Bose products. These are recognized by Bluetooth
product ID but don't have tested configurations yet — contributions welcome:

| Device | Codename | Category | PID |
|--------|----------|----------|-----|
| Noise Cancelling Headphones 700 | goodyear | Headphones | `0x4024` |
| QuietComfort 45 | duran | Headphones | `0x4039` |
| QuietComfort Headphones | prince | Headphones | `0x4075` |
| QuietComfort Ultra Headphones | lonestarr | Headphones | `0x4066` |
| QuietComfort Earbuds II | smalls | Earbuds | `0x4064` |
| QuietComfort Ultra Earbuds | scotty | Earbuds | `0x4072` |
| Ultra Open Earbuds | serena | Earbuds | `0x4068` |
| SoundLink Flex | phelps | Speaker | `0xBC59` |
| SoundLink Flex 2 | mathers | Speaker | `0xBC61` |
| Device | Codename | Category | PID |
| ------------------------------- | --------- | ---------- | -------- |
| Noise Cancelling Headphones 700 | goodyear | Headphones | `0x4024` |
| QuietComfort 45 | duran | Headphones | `0x4039` |
| QuietComfort Headphones | prince | Headphones | `0x4075` |
| QuietComfort Ultra Headphones | lonestarr | Headphones | `0x4066` |
| QuietComfort Earbuds II | smalls | Earbuds | `0x4064` |
| QuietComfort Ultra Earbuds | scotty | Earbuds | `0x4072` |
| Ultra Open Earbuds | serena | Earbuds | `0x4068` |
| SoundLink Flex | phelps | Speaker | `0xBC59` |
| SoundLink Flex 2 | mathers | Speaker | `0xBC61` |

Adding a new device is a configuration entry — no library code changes needed.
See [Adding a New Device](docs/architecture.md#adding-a-new-device).
Expand Down Expand Up @@ -120,11 +120,29 @@ pybmap.known_devices() # full catalog

### Prerequisites

#### Linux

- **Linux** with BlueZ (standard Bluetooth stack)
- **Bluetooth** adapter (built-in or USB)
- **Bose headphones** paired via `bluetoothctl`

### From Release Binaries
#### macOS

- **macOS** 10.15+
- **Bluetooth** enabled in System Settings
- **Bose headphones** paired/connected via System Settings

### Installation (macOS & Linux Python CLI)

Run the included installer to set up dependencies (such as PyObjC on macOS) and symlink `bosectl` globally:

```bash
git clone https://github.com/aaronsb/bosectl.git
cd bosectl
./macOS_install.sh
```

### From Release Binaries (Linux)

```bash
# Download from GitHub releases
Expand All @@ -140,7 +158,7 @@ sudo cp bmapctl-rust-linux-x86_64 /usr/local/bin/bmapctl
```bash
git clone https://github.com/aaronsb/bosectl.git
cd bosectl
make test # Run all tests (Python + Rust + C++)
make test # Run all tests (Python + Rust + C++; automatically installs macOS test deps and handles missing toolchains)
make artifacts # Build release binaries + SHA256SUMS
```

Expand Down Expand Up @@ -217,7 +235,7 @@ Full protocol reference: **[NOTES.md](NOTES.md)**
## Building & Releasing

```bash
make test # All tests (119 Python, 59 Rust, 51 C++)
make test # All tests (121 Python, 63 Rust, 54 C++)
make artifacts # Build + strip + SHA256SUMS in dist/
make release VERSION=v0.2.0 # Test → build → gh release create
make clean # Remove all build artifacts
Expand Down
2 changes: 1 addition & 1 deletion bosectl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import sys
try:
import pybmap # noqa: F401
except ImportError:
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "python"))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "python"))

from pybmap.cli import main
main()
15 changes: 13 additions & 2 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,21 @@ endif()
# CLI
add_executable(bmapctl src/main.cpp)
target_compile_definitions(bmapctl PRIVATE GIT_HASH="${GIT_HASH}" BMAP_VERSION="${PROJECT_VERSION}")
target_link_libraries(bmapctl PRIVATE bmap bluetooth)
if(APPLE)
target_link_libraries(bmapctl PRIVATE bmap)
else()
target_link_libraries(bmapctl PRIVATE bmap bluetooth)
endif()

# Tests
enable_testing()
add_executable(bmap_tests tests/test_main.cpp tests/test_protocol.cpp tests/test_parsers.cpp tests/test_connection.cpp tests/test_catalog.cpp)
add_executable(bmap_tests
tests/test_main.cpp
tests/test_protocol.cpp
tests/test_parsers.cpp
tests/test_connection.cpp
tests/test_catalog.cpp
tests/test_transport.cpp
)
target_link_libraries(bmap_tests PRIVATE bmap)
add_test(NAME bmap_tests COMMAND bmap_tests)
10 changes: 10 additions & 0 deletions cpp/src/discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "discovery.h"
#include "catalog.h"

#ifndef __APPLE__
#include <array>
#include <cstdio>
#include <memory>
Expand Down Expand Up @@ -72,3 +73,12 @@ std::optional<std::pair<std::string, std::string>> find_bmap_device() {
}

} // namespace bmap
#else // __APPLE__
namespace bmap {

std::optional<std::pair<std::string, std::string>> find_bmap_device() {
return std::nullopt;
}

} // namespace bmap
#endif
29 changes: 29 additions & 0 deletions cpp/src/transport.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// RFCOMM transport implementation using raw Linux Bluetooth sockets.
#include "transport.h"

#ifndef __APPLE__
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>
#include <cerrno>
Expand Down Expand Up @@ -100,3 +101,31 @@ void RfcommTransport::set_recv_timeout(int ms) {
}

} // namespace bmap

#else // __APPLE__
#include <stdexcept>

namespace bmap {

RfcommTransport::RfcommTransport(const std::string&, uint8_t) {
throw std::runtime_error("Bluetooth RFCOMM sockets not supported on macOS in C++ library. Use Python bosectl instead.");
}

RfcommTransport::~RfcommTransport() {}

std::vector<uint8_t> RfcommTransport::send_recv(const std::vector<uint8_t>&) {
throw std::runtime_error("Not supported on macOS");
}

std::vector<uint8_t> RfcommTransport::send_recv_drain(const std::vector<uint8_t>&) {
throw std::runtime_error("Not supported on macOS");
}

std::vector<uint8_t> RfcommTransport::send_recv_inner(const std::vector<uint8_t>&, bool) {
throw std::runtime_error("Not supported on macOS");
}

void RfcommTransport::set_recv_timeout(int) {}

} // namespace bmap
#endif
24 changes: 24 additions & 0 deletions cpp/tests/test_transport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "test_common.h"
#include "../src/transport.h"
#include "../src/discovery.h"
#include <stdexcept>

using namespace bmap;

#ifdef __APPLE__
TEST(macos_transport_throws) {
bool threw = false;
try {
RfcommTransport transport("00:11:22:33:44:55", 2);
} catch (const std::runtime_error& e) {
threw = true;
ASSERT_TRUE(std::string(e.what()).find("not supported on macOS") != std::string::npos);
}
ASSERT_TRUE(threw);
}

TEST(macos_discovery_returns_nullopt) {
auto dev = find_bmap_device();
ASSERT_FALSE(dev.has_value());
}
#endif
34 changes: 34 additions & 0 deletions macOS_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
# bosectl macOS Installer

set -e

echo "=== bosectl macOS Installer ==="
echo ""

# 1. Install dependencies
echo "Installing macOS Bluetooth dependencies (PyObjC)..."
# ponytail: unpinned — 9.2 has no wheels for Python 3.13+; pin again only if an API break shows up
python3 -m pip install --user pyobjc-core pyobjc-framework-Cocoa pyobjc-framework-IOBluetooth

# 2. Make bosectl script executable
echo "Configuring executable permissions..."
chmod +x bosectl

# 3. Create symlink in /usr/local/bin
echo ""
echo "To make 'bosectl' accessible from anywhere on your system,"
echo "we will create a symlink in /usr/local/bin/bosectl."
echo "This requires administrator privileges."
echo ""

if sudo ln -sf "$(pwd)/bosectl" /usr/local/bin/bosectl; then
echo ""
echo "=== Installation Successful! ==="
echo "You can now run 'bosectl' from any terminal session."
else
echo ""
echo "=== Symlink Failed ==="
echo "Could not create symlink in /usr/local/bin."
echo "You can still run it locally using: $(pwd)/bosectl"
fi
Loading