summaryrefslogtreecommitdiff
path: root/src/fake_api.cc
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-21 23:14:25 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-22 23:47:20 +0200
commit00dc057ea5d9244f1df9457a316fd193c54dbfb0 (patch)
tree9d4d517a2951c0ac315bba85b8bb19815d9a3956 /src/fake_api.cc
parent8377be960d5d0e65227cc179aded1f5edbf8de79 (diff)
main: Split out main api/server part from main
Add fake-api, sharing api/server implementation with main but fakes bluetooth events, to simplify working on the UI.
Diffstat (limited to 'src/fake_api.cc')
-rw-r--r--src/fake_api.cc409
1 files changed, 409 insertions, 0 deletions
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;
+}