summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--meson.build41
-rw-r--r--src/fake_api.cc409
-rw-r--r--src/main.cc392
-rw-r--r--src/server.cc397
-rw-r--r--src/server.hh50
5 files changed, 899 insertions, 390 deletions
diff --git a/meson.build b/meson.build
index 58f38a0..fd24f0a 100644
--- a/meson.build
+++ b/meson.build
@@ -256,6 +256,41 @@ websocket_dep = declare_dependency(
dependencies: [base64_dep, looper_dep, logger_dep, str_dep, sha1_dep],
)
+server_lib = library(
+ 'server',
+ sources: [
+ 'src/server.cc',
+ 'src/server.hh',
+ ],
+ include_directories: inc,
+ dependencies : [
+ http_dep,
+ json_dep,
+ looper_dep,
+ uri_dep,
+ websocket_dep,
+ ],
+)
+server_dep = declare_dependency(
+ link_with: server_lib,
+ dependencies: [http_dep, json_dep, looper_dep, uri_dep, websocket_dep],
+)
+
+fake_api = executable(
+ 'fake-api',
+ sources: [
+ 'src/fake_api.cc',
+ ],
+ include_directories: inc,
+ install : true,
+ dependencies : [
+ args_dep,
+ cfg_dep,
+ signals_dep,
+ server_dep,
+ ],
+)
+
bluetooth_jukebox = executable(
'bluetooth-jukebox',
sources: [
@@ -267,12 +302,8 @@ bluetooth_jukebox = executable(
args_dep,
bt_dep,
cfg_dep,
- http_dep,
- json_dep,
- looper_dep,
signals_dep,
- uri_dep,
- websocket_dep,
+ server_dep,
],
)
diff --git a/src/fake_api.cc b/src/fake_api.cc
new file mode 100644
index 0000000..f61ac8f
--- /dev/null
+++ b/src/fake_api.cc
@@ -0,0 +1,409 @@
+#include "args.hh"
+#include "bt.hh"
+#include "cfg.hh"
+#include "config.h"
+#include "http.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "server.hh"
+#include "signals.hh"
+#include "websocket.hh"
+
+#include <algorithm>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <ranges>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#ifndef VERSION
+# define VERSION "unknown"
+#endif
+
+namespace {
+
+class FakeConfig : public cfg::Config {
+ public:
+ [[nodiscard]]
+ std::optional<std::string_view> get(
+ std::string_view /* name */) const override {
+ return std::nullopt;
+ }
+};
+
+class FakeApi {
+ public:
+ virtual ~FakeApi() = default;
+
+ [[nodiscard]]
+ virtual std::vector<bt::Device*> devices() = 0;
+
+ virtual void update_adapter() = 0;
+ virtual void update_device(bt::Device& device) = 0;
+ virtual void update_player(bt::Player& player) = 0;
+
+ protected:
+ FakeApi() = default;
+};
+
+class FakeAdapter : public bt::Adapter {
+ public:
+ explicit FakeAdapter(FakeApi& api) : api_(api) {}
+
+ [[nodiscard]]
+ std::string const& address() const override {
+ return address_;
+ }
+
+ [[nodiscard]]
+ std::string const& name() const override {
+ return name_;
+ }
+
+ [[nodiscard]]
+ bool discoverable() const override {
+ return discoverable_;
+ }
+
+ [[nodiscard]]
+ uint32_t discoverable_timeout_seconds() const override {
+ return discoverable_timeout_seconds_;
+ }
+
+ [[nodiscard]]
+ bool pairable() const override {
+ return true;
+ }
+
+ [[nodiscard]]
+ uint32_t pairable_timeout_seconds() const override {
+ return pairable_timeout_seconds_;
+ }
+
+ [[nodiscard]]
+ bool pairing() const override {
+ return false;
+ }
+
+ [[nodiscard]]
+ bool powered() const override {
+ return true;
+ }
+
+ [[nodiscard]]
+ bool connectable() const override {
+ return true;
+ }
+
+ [[nodiscard]]
+ std::vector<bt::Device*> devices() const override {
+ return api_.devices();
+ }
+
+ void set_discoverable(bool discoverable) override {
+ if (discoverable == discoverable_)
+ return;
+
+ discoverable_ = discoverable;
+ api_.update_adapter();
+ }
+
+ void set_discoverable_timeout_seconds(uint32_t timeout) override {
+ if (discoverable_timeout_seconds_ == timeout)
+ return;
+
+ discoverable_timeout_seconds_ = timeout;
+ api_.update_adapter();
+ }
+
+ void set_pairable_timeout_seconds(uint32_t timeout) override {
+ if (pairable_timeout_seconds_ == timeout)
+ return;
+
+ pairable_timeout_seconds_ = timeout;
+ api_.update_adapter();
+ }
+
+ private:
+ FakeApi& api_;
+ std::string const address_{"00:11:22:33:44:66"};
+ std::string const name_{"Fake adapter"};
+ bool discoverable_{false};
+ uint32_t discoverable_timeout_seconds_{180};
+ uint32_t pairable_timeout_seconds_{0};
+};
+
+class FakeDevice : public bt::Device {
+ public:
+ FakeDevice(FakeApi& api, std::string address, std::string name)
+ : api_(api), address_(std::move(address)), name_(std::move(name)) {}
+
+ [[nodiscard]]
+ std::string const& address() const override {
+ return address_;
+ }
+
+ [[nodiscard]]
+ std::string const& name() const override {
+ return name_;
+ }
+
+ [[nodiscard]]
+ bool paired() const override {
+ return paired_;
+ }
+
+ [[nodiscard]]
+ bool connected() const override {
+ return player_ != nullptr;
+ }
+
+ // Returns null if there are no players.
+ // Otherwise, returns the "primary" player.
+ [[nodiscard]]
+ bt::Player* player() const override {
+ return player_;
+ }
+
+ void set_paired(bool paired) {
+ if (paired_ == paired)
+ return;
+
+ paired_ = paired;
+ api_.update_device(*this);
+ }
+
+ void set_player(bt::Player* player) {
+ if (player_ == player)
+ return;
+
+ player_ = player;
+ api_.update_device(*this);
+ }
+
+ private:
+ FakeApi& api_;
+ std::string const address_;
+ std::string const name_;
+ bool paired_{false};
+ bt::Player* player_{nullptr};
+};
+
+class FakePlayer : public bt::Player {
+ public:
+ explicit FakePlayer(FakeApi& api) : api_(api) {}
+
+ [[nodiscard]]
+ Status status() const override {
+ return status_;
+ }
+
+ [[nodiscard]]
+ std::string const& track_title() const override {
+ return title_;
+ }
+
+ [[nodiscard]]
+ std::string const& track_artist() const override {
+ return artist_;
+ }
+
+ [[nodiscard]]
+ std::string const& track_album() const override {
+ return album_;
+ }
+
+ void play() override {
+ if (status_ == Status::kPlaying)
+ return;
+ status_ = Status::kPlaying;
+ api_.update_player(*this);
+ }
+
+ void pause() override {
+ if (status_ == Status::kPaused)
+ return;
+ status_ = Status::kPaused;
+ api_.update_player(*this);
+ }
+
+ void set(std::string const& title, std::string const& artist,
+ std::string const& album) {
+ if (title_ == title && artist_ == artist && album_ == album)
+ return;
+ title_ = title;
+ artist_ = artist;
+ album_ = album;
+ api_.update_player(*this);
+ }
+
+ private:
+ FakeApi& api_;
+ Status status_{Status::kStopped};
+ std::string title_;
+ std::string artist_;
+ std::string album_;
+};
+
+class FakeBluetoothManager : public FakeApi {
+ public:
+ FakeBluetoothManager(looper::Looper& looper, bt::Manager::Delegate& delegate)
+ : looper_(looper), delegate_(delegate) {
+ devices_.emplace_back(std::make_unique<FakeDevice>(
+ *this, "11:22:33:44:55:66", "Fake device 1"));
+ devices_.emplace_back(std::make_unique<FakeDevice>(
+ *this, "66:55:44:33:22:11", "Fake device 2"));
+
+ players_.emplace_back(std::make_unique<FakePlayer>(*this));
+ players_.emplace_back(std::make_unique<FakePlayer>(*this));
+
+ delegate_.new_adapter(&adapter_);
+
+ schedule_next();
+ }
+
+ ~FakeBluetoothManager() override {
+ if (timer_)
+ looper_.cancel(timer_);
+ }
+
+ std::vector<bt::Device*> devices() override {
+ std::vector<bt::Device*> ret;
+ std::ranges::copy(std::views::transform(
+ devices_, [](auto& device) { return device.get(); }),
+ std::back_inserter(ret));
+ return ret;
+ }
+
+ void update_adapter() override { delegate_.updated_adapter(adapter_); }
+
+ void update_device(bt::Device& device) override {
+ delegate_.updated_device(device);
+ }
+
+ void update_player(bt::Player& player) override {
+ auto it = std::ranges::find_if(devices_, [&player](auto& device) {
+ return device->player() == &player;
+ });
+ if (it != devices_.end()) {
+ delegate_.updated_player(**it, player);
+ }
+ }
+
+ private:
+ void schedule_next() {
+ timer_ = looper_.schedule(5, [this](uint32_t /* timer */) {
+ step();
+ schedule_next();
+ });
+ }
+
+ void step() {
+ switch (step_++) {
+ case 0:
+ devices_[0]->set_paired(true);
+ break;
+ case 1:
+ devices_[1]->set_paired(true);
+ break;
+ case 2:
+ devices_[1]->set_player(players_[1].get());
+ delegate_.added_player(*devices_[1], *players_[1]);
+ break;
+ case 3:
+ players_[1]->play();
+ break;
+ case 4:
+ players_[1]->pause();
+ break;
+ case 5:
+ devices_[1]->set_player(nullptr);
+ delegate_.removed_player(*devices_[1]);
+ break;
+ case 6:
+ devices_[1]->set_paired(false);
+ break;
+ case 7:
+ devices_[0]->set_paired(false);
+ break;
+ default:
+ step_ = 0;
+ break;
+ }
+ }
+
+ FakeAdapter adapter_{*this};
+ std::vector<std::unique_ptr<FakeDevice>> devices_;
+ std::vector<std::unique_ptr<FakePlayer>> players_;
+ looper::Looper& looper_;
+ bt::Manager::Delegate& delegate_;
+ uint32_t timer_{0};
+ uint32_t step_{0};
+};
+
+bool run(logger::Logger& logger, std::unique_ptr<http::OpenPort> port) {
+ FakeConfig cfg;
+ auto looper = looper::create();
+ auto signaler = server::create_signaler(logger, cfg, *looper);
+ auto bt_delegate = server::create_bt_delegate(logger, *signaler);
+ auto http_delegate = server::create_http_delegate(*bt_delegate, *signaler);
+ auto server = http::create_server(logger, cfg, *looper, std::move(port),
+ *http_delegate);
+ FakeBluetoothManager fake_bt(*looper, *bt_delegate);
+ auto sigint_handler = signals::Handler::create(
+ *looper, signals::Signal::INT, [&looper, &logger]() {
+ logger.info("Received SIGINT, quitting...");
+ looper->quit();
+ });
+ auto sigterm_handler = signals::Handler::create(
+ *looper, signals::Signal::TERM, [&looper, &logger]() {
+ logger.info("Received SIGTERM, quitting...");
+ looper->quit();
+ });
+ return looper->run(logger);
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto opt_help = args->option('h', "help", "display this text and exit.");
+ auto opt_version = args->option('V', "version", "display version and exit.");
+ auto opt_bind =
+ args->option_argument('B', "bind", "HOST:PORT",
+ "HOST:PORT to bind to, defaults to localhost:5555");
+ if (!args->run(argc, argv)) {
+ args->print_error(std::cerr);
+ std::cerr << "Try 'fake-api --help' for more information.\n";
+ return 1;
+ }
+ if (opt_help->is_set()) {
+ std::cout << "Usage: fake-api [OPTION...]\n"
+ << "\n";
+ args->print_help(std::cout);
+ return 0;
+ }
+ if (opt_version->is_set()) {
+ std::cout << "bluetooth-jukebox " << VERSION
+ << " written by Joel Klinghed <the_jk@spawned.biz>.\n";
+ return 0;
+ }
+
+ auto logger = logger::stderr(/* verbose */ true);
+
+ std::string_view bind = "localhost:5555";
+ if (opt_bind->is_set())
+ bind = opt_bind->argument();
+
+ auto port = http::open_port(bind, *logger);
+ if (!port)
+ return 1;
+
+ return run(*logger, std::move(port)) ? 0 : 1;
+}
diff --git a/src/main.cc b/src/main.cc
index 1f4bfd4..ff05a55 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -3,15 +3,13 @@
#include "cfg.hh"
#include "config.h"
#include "http.hh"
-#include "json.hh"
#include "logger.hh"
#include "looper.hh"
+#include "server.hh"
#include "signals.hh"
-#include "uri.hh"
#include "websocket.hh"
#include <cerrno>
-#include <cstdint>
#include <cstring>
#include <format>
#include <functional>
@@ -30,391 +28,15 @@
namespace {
-class Api {
- public:
- virtual ~Api() = default;
-
- [[nodiscard]]
- virtual bt::Adapter* adapter() const = 0;
-
- protected:
- Api() = default;
-};
-
-const std::string_view kSignalUpdateAdapter("controller/update");
-const std::string kSignalUpdateDevicePrefix("device/update/");
-
-class Signaler {
- public:
- virtual ~Signaler() = default;
-
- virtual void send(std::string_view signal) = 0;
- virtual std::unique_ptr<http::Response> handle(
- http::Request const& request) = 0;
-
- protected:
- Signaler() = default;
-};
-
-std::optional<bool> match_bool(std::string_view str) {
- if (str == "true")
- return true;
- if (str == "false")
- return false;
- return std::nullopt;
-}
-
-class HttpServerDelegate : public http::Server::Delegate {
- public:
- HttpServerDelegate(Api& api, Signaler& signaler)
- : api_(api),
- signaler_(signaler),
- json_writer_(json::writer(json_tmp_)),
- json_mimetype_(http::MimeType::create("application", "json")) {}
-
- std::unique_ptr<http::Response> handle(
- http::Request const& request) override {
- if (request.path().starts_with("/api/v1/")) {
- auto path = request.path().substr(8);
- if (path == "status") {
- if (request.method() != "GET") {
- return http::Response::status(http::StatusCode::kMethodNotAllowed);
- }
-
- return status_ok();
- }
-
- if (path == "controller") {
- if (request.method() != "GET") {
- return http::Response::status(http::StatusCode::kMethodNotAllowed);
- }
-
- auto const* adapter = api_.adapter();
- json_writer_->clear();
- write_adapter(adapter);
-
- return http::Response::content(json_tmp_, *json_mimetype_);
- }
-
- if (path == "controller/discoverable") {
- if (request.method() != "POST") {
- return http::Response::status(http::StatusCode::kMethodNotAllowed);
- }
- std::optional<bool> value = match_bool(request.body());
- auto* adapter = api_.adapter();
- if (adapter && value.has_value()) {
- adapter->set_discoverable(value.value());
- return status_ok();
- }
- return status_error("Bad state");
- }
-
- if (path.starts_with("device/")) {
- if (request.method() != "GET") {
- return http::Response::status(http::StatusCode::kMethodNotAllowed);
- }
-
- std::string tmp;
- auto address = uri::decode(path.substr(7), tmp);
- auto const* adapter = api_.adapter();
- if (adapter) {
- for (auto* device : adapter->devices()) {
- if (device->address() == address) {
- json_writer_->clear();
- write_device(*device);
- return http::Response::content(json_tmp_, *json_mimetype_);
- }
- }
- }
- return http::Response::status(http::StatusCode::kNotFound);
- }
-
- if (path == "events") {
- if (request.method() != "GET") {
- return http::Response::status(http::StatusCode::kMethodNotAllowed);
- }
-
- auto resp = signaler_.handle(request);
- if (resp)
- return resp;
- return http::Response::status(http::StatusCode::kBadRequest);
- }
- }
-
- return http::Response::status(http::StatusCode::kNotFound);
- }
-
- private:
- std::unique_ptr<http::Response> status_ok() {
- json_writer_->clear();
- json_writer_->start_object();
- json_writer_->key("status");
- json_writer_->value("OK");
- json_writer_->end_object();
- return http::Response::content(json_tmp_, *json_mimetype_);
- }
-
- std::unique_ptr<http::Response> status_error(std::string_view message) {
- json_writer_->clear();
- json_writer_->start_object();
- json_writer_->key("status");
- json_writer_->value("error");
- json_writer_->key("message");
- json_writer_->value(message);
- json_writer_->end_object();
- return http::Response::content(json_tmp_, *json_mimetype_);
- }
-
- void write_adapter(bt::Adapter const* adapter) {
- json_writer_->start_object();
- json_writer_->key("name");
- json_writer_->value(adapter ? adapter->name() : "unknown");
- json_writer_->key("pairable");
- json_writer_->value(adapter ? adapter->pairable() : false);
- json_writer_->key("discoverable");
- json_writer_->value(adapter ? adapter->discoverable() : false);
- json_writer_->key("discoverable_timeout_seconds");
- json_writer_->value(adapter ? adapter->discoverable_timeout_seconds() : 0);
- json_writer_->key("pairing");
- json_writer_->value(adapter ? adapter->pairing() : false);
-
- json_writer_->key("devices");
- json_writer_->start_array();
-
- if (adapter) {
- for (auto* device : adapter->devices()) {
- write_device(*device);
- }
- }
-
- json_writer_->end_array();
- json_writer_->end_object();
- }
-
- void write_device(bt::Device const& device) {
- json_writer_->start_object();
- json_writer_->key("name");
- json_writer_->value(device.name());
- json_writer_->key("address");
- json_writer_->value(device.address());
- json_writer_->key("paired");
- json_writer_->value(device.paired());
- json_writer_->key("connected");
- json_writer_->value(device.connected());
-
- json_writer_->key("playing");
- if (auto* player = device.player()) {
- json_writer_->start_object();
- json_writer_->key("status");
- switch (player->status()) {
- case bt::Player::Status::kPlaying:
- case bt::Player::Status::kForwardSeek:
- case bt::Player::Status::kReverseSeek:
- json_writer_->value("playing");
- break;
- case bt::Player::Status::kStopped:
- json_writer_->value("stopped");
- break;
- case bt::Player::Status::kPaused:
- json_writer_->value("paused");
- break;
- case bt::Player::Status::kError:
- json_writer_->value("error");
- break;
- }
- json_writer_->key("title");
- json_writer_->value(player->track_title());
- json_writer_->key("album");
- json_writer_->value(player->track_album());
- json_writer_->key("artist");
- json_writer_->value(player->track_artist());
- json_writer_->end_object();
- } else {
- json_writer_->value(nullptr);
- }
-
- json_writer_->end_object();
- }
-
- Api& api_;
- Signaler& signaler_;
- std::unique_ptr<json::Writer> json_writer_;
- std::string json_tmp_;
- std::unique_ptr<http::MimeType> json_mimetype_;
-};
-
-class BluetoothManagerDelegate : public bt::Manager::Delegate, public Api {
- public:
- BluetoothManagerDelegate(logger::Logger& logger, Signaler& signaler)
- : logger_(logger), signaler_(signaler) {}
-
- [[nodiscard]]
- bt::Adapter* adapter() const override {
- return adapter_;
- }
-
- void new_adapter(bt::Adapter* adapter) override {
- adapter_ = adapter;
-
- signaler_.send(kSignalUpdateAdapter);
-
- if (adapter) {
- logger_.info(std::format("New adapter: {} [{}]", adapter->name(),
- adapter->address()));
-
- // Assuming pairable doesn't have a timeout - make it so.
- if (adapter_->pairable_timeout_seconds() > 0) {
- adapter_->set_pairable_timeout_seconds(0);
- }
-
- // Assuming discoverable has one
- if (adapter_->discoverable_timeout_seconds() == 0) {
- adapter_->set_discoverable_timeout_seconds(180);
- }
- } else {
- logger_.info("No adapter");
- }
- }
-
- void updated_adapter(bt::Adapter& adapter) override {
- if (adapter_ == &adapter)
- signaler_.send(kSignalUpdateAdapter);
- }
-
- void added_device(bt::Device& device) override {
- logger_.info(
- std::format("New device: {} [{}]", device.name(), device.address()));
-
- if (adapter_)
- signaler_.send(kSignalUpdateAdapter);
- }
-
- void removed_device(std::string const& address) override {
- logger_.info(std::format("Remove device: [{}]", address));
-
- if (adapter_)
- signaler_.send(kSignalUpdateAdapter);
- }
-
- void added_player(bt::Device& device, bt::Player& /* player */) override {
- logger_.info(std::format("New player for {}", device.name()));
-
- if (adapter_)
- signaler_.send(kSignalUpdateDevicePrefix + device.address());
- }
-
- void removed_player(bt::Device& device) override {
- logger_.info(std::format("Remove player for {}", device.name()));
-
- if (adapter_)
- signaler_.send(kSignalUpdateDevicePrefix + device.address());
- }
-
- void updated_player(bt::Device& device, bt::Player& /* player */) override {
- if (adapter_)
- signaler_.send(kSignalUpdateDevicePrefix + device.address());
- }
-
- void agent_request_pincode(
- bt::Device& device,
- std::function<void(std::optional<std::string>)> callback) override {
- logger_.dbg(std::format("Device request pincode: {}", device.name()));
- callback(std::nullopt);
- }
-
- void agent_display_pincode(bt::Device& device,
- std::string const& pincode) override {
- logger_.dbg(std::format("Device pincode: {} {}", device.name(), pincode));
- }
-
- void agent_request_passkey(
- bt::Device& device,
- std::function<void(std::optional<uint32_t>)> callback) override {
- logger_.dbg(std::format("Device request passkey: {}", device.name()));
- callback(std::nullopt);
- }
-
- void agent_display_passkey(bt::Device& device, uint32_t passkey,
- uint16_t /* entered */) override {
- logger_.dbg(std::format("Device passkey: {} {}", device.name(), passkey));
- }
-
- void agent_request_confirmation(bt::Device& device, uint32_t passkey,
- std::function<void(bool)> callback) override {
- logger_.dbg(std::format("Device request confirmation: {} {}", device.name(),
- passkey));
- // Confirm all
- callback(true);
- }
-
- void agent_request_authorization(
- bt::Device& device, std::function<void(bool)> callback) override {
- logger_.dbg(std::format("Device request authorization: {}", device.name()));
- callback(false);
- }
-
- void agent_authorize_service(bt::Device& device, std::string const& uuid,
- std::function<void(bool)> callback) override {
- if (uuid == "0000110d-0000-1000-8000-00805f9b34fb") {
- // Advanced Audio Distribution Profile
- callback(true);
- return;
- }
- if (uuid == "0000111e-0000-1000-8000-00805f9b34fb") {
- // Hands-Free Service Class and Profile
- // Not interrested.
- callback(false);
- return;
- }
-
- logger_.dbg(std::format("Device request authorize unknown service: {} {}",
- device.name(), uuid));
-
- // Do not authorize unknown services.
- callback(false);
- }
-
- void agent_cancel() override {}
-
- private:
- logger::Logger& logger_;
- Signaler& signaler_;
- bt::Adapter* adapter_{nullptr};
-};
-
-class SignalerImpl : public Signaler, ws::Server::Delegate {
- public:
- SignalerImpl(logger::Logger& logger, cfg::Config const& cfg,
- looper::Looper& looper)
- : server_(ws::create_server(logger, cfg, looper, *this)) {}
-
- void send(std::string_view signal) override {
- server_->send_text_to_all(signal);
- }
-
- std::unique_ptr<http::Response> handle(
- http::Request const& request) override {
- return server_->handle(request);
- }
-
- std::unique_ptr<ws::Message> handle(ws::Message const& /* msg */) override {
- // Ignore anything sent by clients
- return nullptr;
- }
-
- private:
- std::unique_ptr<ws::Server> server_;
-};
-
bool run(logger::Logger& logger, cfg::Config const& cfg,
std::unique_ptr<http::OpenPort> port) {
auto looper = looper::create();
- SignalerImpl signaler(logger, cfg, *looper);
- BluetoothManagerDelegate bt_delegate(logger, signaler);
- HttpServerDelegate http_delegate(bt_delegate, signaler);
- auto server =
- http::create_server(logger, cfg, *looper, std::move(port), http_delegate);
- auto manager = bt::create_manager(logger, cfg, *looper, bt_delegate);
+ auto signaler = server::create_signaler(logger, cfg, *looper);
+ auto bt_delegate = server::create_bt_delegate(logger, *signaler);
+ auto http_delegate = server::create_http_delegate(*bt_delegate, *signaler);
+ auto server = http::create_server(logger, cfg, *looper, std::move(port),
+ *http_delegate);
+ auto manager = bt::create_manager(logger, cfg, *looper, *bt_delegate);
auto sigint_handler = signals::Handler::create(
*looper, signals::Signal::INT, [&looper, &logger]() {
logger.info("Received SIGINT, quitting...");
diff --git a/src/server.cc b/src/server.cc
new file mode 100644
index 0000000..9b556b0
--- /dev/null
+++ b/src/server.cc
@@ -0,0 +1,397 @@
+#include "server.hh"
+
+#include "bt.hh"
+#include "cfg.hh"
+#include "config.h"
+#include "http.hh"
+#include "json.hh"
+#include "logger.hh"
+#include "looper.hh"
+#include "signals.hh"
+#include "uri.hh"
+#include "websocket.hh"
+
+#include <cstdint>
+#include <format>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+
+namespace server {
+
+namespace {
+
+const std::string_view kSignalUpdateAdapter("controller/update");
+const std::string kSignalUpdateDevicePrefix("device/update/");
+
+std::optional<bool> match_bool(std::string_view str) {
+ if (str == "true")
+ return true;
+ if (str == "false")
+ return false;
+ return std::nullopt;
+}
+
+class HttpServerDelegate : public http::Server::Delegate {
+ public:
+ HttpServerDelegate(Api& api, Signaler& signaler)
+ : api_(api),
+ signaler_(signaler),
+ json_writer_(json::writer(json_tmp_)),
+ json_mimetype_(http::MimeType::create("application", "json")) {}
+
+ std::unique_ptr<http::Response> handle(
+ http::Request const& request) override {
+ if (request.path().starts_with("/api/v1/")) {
+ auto path = request.path().substr(8);
+ if (path == "status") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ return status_ok();
+ }
+
+ if (path == "controller") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ auto const* adapter = api_.adapter();
+ json_writer_->clear();
+ write_adapter(adapter);
+
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ if (path == "controller/discoverable") {
+ if (request.method() != "POST") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+ std::optional<bool> value = match_bool(request.body());
+ auto* adapter = api_.adapter();
+ if (adapter && value.has_value()) {
+ adapter->set_discoverable(value.value());
+ return status_ok();
+ }
+ return status_error("Bad state");
+ }
+
+ if (path.starts_with("device/")) {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ std::string tmp;
+ auto address = uri::decode(path.substr(7), tmp);
+ auto const* adapter = api_.adapter();
+ if (adapter) {
+ for (auto* device : adapter->devices()) {
+ if (device->address() == address) {
+ json_writer_->clear();
+ write_device(*device);
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+ }
+ }
+ return http::Response::status(http::StatusCode::kNotFound);
+ }
+
+ if (path == "events") {
+ if (request.method() != "GET") {
+ return http::Response::status(http::StatusCode::kMethodNotAllowed);
+ }
+
+ auto resp = signaler_.handle(request);
+ if (resp)
+ return resp;
+ return http::Response::status(http::StatusCode::kBadRequest);
+ }
+ }
+
+ return http::Response::status(http::StatusCode::kNotFound);
+ }
+
+ private:
+ std::unique_ptr<http::Response> status_ok() {
+ json_writer_->clear();
+ json_writer_->start_object();
+ json_writer_->key("status");
+ json_writer_->value("OK");
+ json_writer_->end_object();
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ std::unique_ptr<http::Response> status_error(std::string_view message) {
+ json_writer_->clear();
+ json_writer_->start_object();
+ json_writer_->key("status");
+ json_writer_->value("error");
+ json_writer_->key("message");
+ json_writer_->value(message);
+ json_writer_->end_object();
+ return http::Response::content(json_tmp_, *json_mimetype_);
+ }
+
+ void write_adapter(bt::Adapter const* adapter) {
+ json_writer_->start_object();
+ json_writer_->key("name");
+ json_writer_->value(adapter ? adapter->name() : "unknown");
+ json_writer_->key("pairable");
+ json_writer_->value(adapter ? adapter->pairable() : false);
+ json_writer_->key("discoverable");
+ json_writer_->value(adapter ? adapter->discoverable() : false);
+ json_writer_->key("discoverable_timeout_seconds");
+ json_writer_->value(adapter ? adapter->discoverable_timeout_seconds() : 0);
+ json_writer_->key("pairing");
+ json_writer_->value(adapter ? adapter->pairing() : false);
+
+ json_writer_->key("devices");
+ json_writer_->start_array();
+
+ if (adapter) {
+ for (auto* device : adapter->devices()) {
+ write_device(*device);
+ }
+ }
+
+ json_writer_->end_array();
+ json_writer_->end_object();
+ }
+
+ void write_device(bt::Device const& device) {
+ json_writer_->start_object();
+ json_writer_->key("name");
+ json_writer_->value(device.name());
+ json_writer_->key("address");
+ json_writer_->value(device.address());
+ json_writer_->key("paired");
+ json_writer_->value(device.paired());
+ json_writer_->key("connected");
+ json_writer_->value(device.connected());
+
+ json_writer_->key("playing");
+ if (auto* player = device.player()) {
+ json_writer_->start_object();
+ json_writer_->key("status");
+ switch (player->status()) {
+ case bt::Player::Status::kPlaying:
+ case bt::Player::Status::kForwardSeek:
+ case bt::Player::Status::kReverseSeek:
+ json_writer_->value("playing");
+ break;
+ case bt::Player::Status::kStopped:
+ json_writer_->value("stopped");
+ break;
+ case bt::Player::Status::kPaused:
+ json_writer_->value("paused");
+ break;
+ case bt::Player::Status::kError:
+ json_writer_->value("error");
+ break;
+ }
+ json_writer_->key("title");
+ json_writer_->value(player->track_title());
+ json_writer_->key("album");
+ json_writer_->value(player->track_album());
+ json_writer_->key("artist");
+ json_writer_->value(player->track_artist());
+ json_writer_->end_object();
+ } else {
+ json_writer_->value(nullptr);
+ }
+
+ json_writer_->end_object();
+ }
+
+ Api& api_;
+ Signaler& signaler_;
+ std::unique_ptr<json::Writer> json_writer_;
+ std::string json_tmp_;
+ std::unique_ptr<http::MimeType> json_mimetype_;
+};
+
+class BluetoothManagerDelegate : public Api {
+ public:
+ BluetoothManagerDelegate(logger::Logger& logger, Signaler& signaler)
+ : logger_(logger), signaler_(signaler) {}
+
+ [[nodiscard]]
+ bt::Adapter* adapter() const override {
+ return adapter_;
+ }
+
+ void new_adapter(bt::Adapter* adapter) override {
+ adapter_ = adapter;
+
+ signaler_.send(kSignalUpdateAdapter);
+
+ if (adapter) {
+ logger_.info(std::format("New adapter: {} [{}]", adapter->name(),
+ adapter->address()));
+
+ // Assuming pairable doesn't have a timeout - make it so.
+ if (adapter_->pairable_timeout_seconds() > 0) {
+ adapter_->set_pairable_timeout_seconds(0);
+ }
+
+ // Assuming discoverable has one
+ if (adapter_->discoverable_timeout_seconds() == 0) {
+ adapter_->set_discoverable_timeout_seconds(180);
+ }
+ } else {
+ logger_.info("No adapter");
+ }
+ }
+
+ void updated_adapter(bt::Adapter& adapter) override {
+ if (adapter_ == &adapter)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void added_device(bt::Device& device) override {
+ logger_.info(
+ std::format("New device: {} [{}]", device.name(), device.address()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void removed_device(std::string const& address) override {
+ logger_.info(std::format("Remove device: [{}]", address));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateAdapter);
+ }
+
+ void added_player(bt::Device& device, bt::Player& /* player */) override {
+ logger_.info(std::format("New player for {}", device.name()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void removed_player(bt::Device& device) override {
+ logger_.info(std::format("Remove player for {}", device.name()));
+
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void updated_player(bt::Device& device, bt::Player& /* player */) override {
+ if (adapter_)
+ signaler_.send(kSignalUpdateDevicePrefix + device.address());
+ }
+
+ void agent_request_pincode(
+ bt::Device& device,
+ std::function<void(std::optional<std::string>)> callback) override {
+ logger_.dbg(std::format("Device request pincode: {}", device.name()));
+ callback(std::nullopt);
+ }
+
+ void agent_display_pincode(bt::Device& device,
+ std::string const& pincode) override {
+ logger_.dbg(std::format("Device pincode: {} {}", device.name(), pincode));
+ }
+
+ void agent_request_passkey(
+ bt::Device& device,
+ std::function<void(std::optional<uint32_t>)> callback) override {
+ logger_.dbg(std::format("Device request passkey: {}", device.name()));
+ callback(std::nullopt);
+ }
+
+ void agent_display_passkey(bt::Device& device, uint32_t passkey,
+ uint16_t /* entered */) override {
+ logger_.dbg(std::format("Device passkey: {} {}", device.name(), passkey));
+ }
+
+ void agent_request_confirmation(bt::Device& device, uint32_t passkey,
+ std::function<void(bool)> callback) override {
+ logger_.dbg(std::format("Device request confirmation: {} {}", device.name(),
+ passkey));
+ // Confirm all
+ callback(true);
+ }
+
+ void agent_request_authorization(
+ bt::Device& device, std::function<void(bool)> callback) override {
+ logger_.dbg(std::format("Device request authorization: {}", device.name()));
+ callback(false);
+ }
+
+ void agent_authorize_service(bt::Device& device, std::string const& uuid,
+ std::function<void(bool)> callback) override {
+ if (uuid == "0000110d-0000-1000-8000-00805f9b34fb") {
+ // Advanced Audio Distribution Profile
+ callback(true);
+ return;
+ }
+ if (uuid == "0000111e-0000-1000-8000-00805f9b34fb") {
+ // Hands-Free Service Class and Profile
+ // Not interrested.
+ callback(false);
+ return;
+ }
+
+ logger_.dbg(std::format("Device request authorize unknown service: {} {}",
+ device.name(), uuid));
+
+ // Do not authorize unknown services.
+ callback(false);
+ }
+
+ void agent_cancel() override {}
+
+ private:
+ logger::Logger& logger_;
+ Signaler& signaler_;
+ bt::Adapter* adapter_{nullptr};
+};
+
+class SignalerImpl : public Signaler, ws::Server::Delegate {
+ public:
+ SignalerImpl(logger::Logger& logger, cfg::Config const& cfg,
+ looper::Looper& looper)
+ : server_(ws::create_server(logger, cfg, looper, *this)) {}
+
+ void send(std::string_view signal) override {
+ server_->send_text_to_all(signal);
+ }
+
+ std::unique_ptr<http::Response> handle(
+ http::Request const& request) override {
+ return server_->handle(request);
+ }
+
+ std::unique_ptr<ws::Message> handle(ws::Message const& /* msg */) override {
+ // Ignore anything sent by clients
+ return nullptr;
+ }
+
+ private:
+ std::unique_ptr<ws::Server> server_;
+};
+
+} // namespace
+
+std::unique_ptr<Signaler> create_signaler(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper) {
+ return std::make_unique<SignalerImpl>(logger, cfg, looper);
+}
+
+std::unique_ptr<Api> create_bt_delegate(logger::Logger& logger,
+ Signaler& signaler) {
+ return std::make_unique<BluetoothManagerDelegate>(logger, signaler);
+}
+
+std::unique_ptr<http::Server::Delegate> create_http_delegate(
+ Api& api, Signaler& signaler) {
+ return std::make_unique<HttpServerDelegate>(api, signaler);
+}
+
+} // namespace server
diff --git a/src/server.hh b/src/server.hh
new file mode 100644
index 0000000..0601ba8
--- /dev/null
+++ b/src/server.hh
@@ -0,0 +1,50 @@
+#ifndef SERVER_HH
+#define SERVER_HH
+
+#include "bt.hh"
+#include "cfg.hh"
+#include "http.hh"
+#include "logger.hh"
+#include "looper.hh"
+
+#include <memory>
+#include <string_view>
+
+namespace server {
+
+class Api : public bt::Manager::Delegate {
+ public:
+ virtual ~Api() = default;
+
+ [[nodiscard]]
+ virtual bt::Adapter* adapter() const = 0;
+
+ protected:
+ Api() = default;
+};
+
+class Signaler {
+ public:
+ virtual ~Signaler() = default;
+
+ virtual void send(std::string_view signal) = 0;
+ virtual std::unique_ptr<http::Response> handle(
+ http::Request const& request) = 0;
+
+ protected:
+ Signaler() = default;
+};
+
+std::unique_ptr<Signaler> create_signaler(logger::Logger& logger,
+ cfg::Config const& cfg,
+ looper::Looper& looper);
+
+std::unique_ptr<Api> create_bt_delegate(logger::Logger& logger,
+ Signaler& signaler);
+
+std::unique_ptr<http::Server::Delegate> create_http_delegate(
+ Api& api, Signaler& signaler);
+
+} // namespace server
+
+#endif // SERVER_HH