implement Milestone 16: Bluetooth interface with user consent

This commit is contained in:
2026-01-18 16:21:06 +01:00
parent 4ab5e52259
commit 00b9ceb467
6 changed files with 1529 additions and 1 deletions

View File

@@ -0,0 +1,595 @@
// bluetooth_interface.cpp - Bluetooth interface implementation for Lua sandbox
// Milestone 16: Bluetooth discovery and pairing with user consent
#include "bluetooth_interface.h"
#include <algorithm>
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace mosis {
// ============================================================================
// BluetoothConnection Implementation
// ============================================================================
BluetoothConnection::BluetoothConnection(int id, const std::string& address)
: m_id(id)
, m_address(address)
{
m_connected = true; // In mock mode, connections are immediately "connected"
}
BluetoothConnection::~BluetoothConnection() {
Close();
}
bool BluetoothConnection::Send(const std::vector<uint8_t>& data, std::string& error) {
if (!m_connected) {
error = "Not connected";
return false;
}
// In mock mode, send always succeeds
// Real implementation would send via BluetoothSocket
return true;
}
void BluetoothConnection::Close() {
bool was_connected = m_connected.exchange(false);
if (was_connected && m_on_disconnect) {
m_on_disconnect();
}
}
void BluetoothConnection::SimulateData(const std::vector<uint8_t>& data) {
if (m_connected && m_on_data) {
m_on_data(data);
}
}
void BluetoothConnection::SimulateDisconnect() {
Close();
}
// ============================================================================
// BluetoothInterface Implementation
// ============================================================================
BluetoothInterface::BluetoothInterface(const std::string& app_id)
: m_app_id(app_id)
{
}
BluetoothInterface::~BluetoothInterface() {
Shutdown();
}
bool BluetoothInterface::StartDiscovery(
const DiscoveryOptions& options,
std::function<void(const std::vector<BluetoothDevice>&)> success,
std::function<void(BluetoothError, const std::string&)> error,
std::string& out_error
) {
// Check permission
if (!m_has_bluetooth_permission) {
out_error = "Bluetooth permission denied";
return false;
}
// Check user consent (required for discovery)
if (!m_has_user_consent) {
out_error = "User consent required for Bluetooth discovery (user gesture needed)";
return false;
}
// Check if already discovering
bool expected = false;
if (!m_is_discovering.compare_exchange_strong(expected, true)) {
out_error = "Discovery already in progress";
return false;
}
if (m_mock_mode) {
// In mock mode, immediately return mock devices
m_is_discovering = false;
if (success) {
success(m_mock_devices);
}
return true;
}
// Real implementation would use BluetoothAdapter via JNI
m_is_discovering = false;
out_error = "Real Bluetooth not implemented";
return false;
}
void BluetoothInterface::StopDiscovery() {
m_is_discovering = false;
// Real implementation would cancel BluetoothAdapter discovery
}
std::shared_ptr<BluetoothConnection> BluetoothInterface::Connect(
const std::string& address,
const ConnectionOptions& options,
std::string& error
) {
std::lock_guard<std::mutex> lock(m_mutex);
// Check permission
if (!m_has_bluetooth_permission) {
error = "Bluetooth permission denied";
return nullptr;
}
// Clean up closed connections first
CleanupClosedConnections();
// Check connection limit
if (m_connections.size() >= MAX_CONNECTIONS) {
error = "Maximum connection limit reached (5)";
return nullptr;
}
// Create connection
int id = m_next_connection_id++;
auto connection = std::make_shared<BluetoothConnection>(id, address);
m_connections[id] = connection;
return connection;
}
void BluetoothInterface::Disconnect(int connection_id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_connections.find(connection_id);
if (it != m_connections.end()) {
it->second->Close();
m_connections.erase(it);
}
}
void BluetoothInterface::DisconnectAll() {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto& [id, connection] : m_connections) {
connection->Close();
}
m_connections.clear();
}
size_t BluetoothInterface::GetActiveConnectionCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
size_t count = 0;
for (const auto& [id, connection] : m_connections) {
if (connection->IsConnected()) {
count++;
}
}
return count;
}
void BluetoothInterface::Shutdown() {
StopDiscovery();
DisconnectAll();
}
std::shared_ptr<BluetoothConnection> BluetoothInterface::GetConnection(int id) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_connections.find(id);
if (it != m_connections.end()) {
return it->second;
}
return nullptr;
}
void BluetoothInterface::CleanupClosedConnections() {
// Called with lock held
for (auto it = m_connections.begin(); it != m_connections.end();) {
if (!it->second->IsConnected()) {
it = m_connections.erase(it);
} else {
++it;
}
}
}
// ============================================================================
// Lua API Implementation
// ============================================================================
// Weak reference to connection stored in userdata
struct LuaConnectionRef {
std::weak_ptr<BluetoothConnection> connection;
BluetoothInterface* bluetooth;
};
static const char* CONNECTION_METATABLE = "mosis.BluetoothConnection";
static LuaConnectionRef* GetConnectionRef(lua_State* L, int idx) {
return static_cast<LuaConnectionRef*>(luaL_checkudata(L, idx, CONNECTION_METATABLE));
}
static std::shared_ptr<BluetoothConnection> GetConnection(lua_State* L, int idx) {
auto ref = GetConnectionRef(L, idx);
return ref->connection.lock();
}
// Connection methods
static int LuaConnection_close(lua_State* L) {
auto ref = GetConnectionRef(L, 1);
auto connection = ref->connection.lock();
if (connection && ref->bluetooth) {
ref->bluetooth->Disconnect(connection->GetId());
}
return 0;
}
static int LuaConnection_getId(lua_State* L) {
auto connection = GetConnection(L, 1);
if (connection) {
lua_pushinteger(L, connection->GetId());
} else {
lua_pushinteger(L, 0);
}
return 1;
}
static int LuaConnection_getAddress(lua_State* L) {
auto connection = GetConnection(L, 1);
if (connection) {
lua_pushstring(L, connection->GetAddress().c_str());
} else {
lua_pushstring(L, "");
}
return 1;
}
static int LuaConnection_isConnected(lua_State* L) {
auto connection = GetConnection(L, 1);
lua_pushboolean(L, connection && connection->IsConnected());
return 1;
}
static int LuaConnection_send(lua_State* L) {
auto connection = GetConnection(L, 1);
if (!connection) {
lua_pushboolean(L, 0);
lua_pushstring(L, "Connection closed");
return 2;
}
// Get data from table argument
if (!lua_istable(L, 2)) {
return luaL_error(L, "send() requires a table of bytes");
}
std::vector<uint8_t> data;
lua_pushnil(L);
while (lua_next(L, 2) != 0) {
if (lua_isnumber(L, -1)) {
data.push_back(static_cast<uint8_t>(lua_tointeger(L, -1)));
}
lua_pop(L, 1);
}
std::string error;
bool ok = connection->Send(data, error);
if (!ok) {
lua_pushboolean(L, 0);
lua_pushstring(L, error.c_str());
return 2;
}
lua_pushboolean(L, 1);
return 1;
}
static int LuaConnection_gc(lua_State* L) {
auto ref = GetConnectionRef(L, 1);
ref->~LuaConnectionRef();
return 0;
}
static const luaL_Reg connection_methods[] = {
{"close", LuaConnection_close},
{"getId", LuaConnection_getId},
{"getAddress", LuaConnection_getAddress},
{"isConnected", LuaConnection_isConnected},
{"send", LuaConnection_send},
{nullptr, nullptr}
};
static void CreateConnectionMetatable(lua_State* L) {
luaL_newmetatable(L, CONNECTION_METATABLE);
// __index = methods table
lua_newtable(L);
luaL_setfuncs(L, connection_methods, 0);
lua_setfield(L, -2, "__index");
// __gc
lua_pushcfunction(L, LuaConnection_gc);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
}
static void PushConnection(lua_State* L, std::shared_ptr<BluetoothConnection> connection, BluetoothInterface* bluetooth) {
auto ref = static_cast<LuaConnectionRef*>(lua_newuserdata(L, sizeof(LuaConnectionRef)));
new (ref) LuaConnectionRef{connection, bluetooth};
luaL_setmetatable(L, CONNECTION_METATABLE);
}
static const char* BluetoothErrorToString(BluetoothError err) {
switch (err) {
case BluetoothError::PermissionDenied: return "PERMISSION_DENIED";
case BluetoothError::UserConsentRequired: return "USER_CONSENT_REQUIRED";
case BluetoothError::DiscoveryFailed: return "DISCOVERY_FAILED";
case BluetoothError::ConnectionFailed: return "CONNECTION_FAILED";
case BluetoothError::NotConnected: return "NOT_CONNECTED";
case BluetoothError::ConnectionLimitReached: return "CONNECTION_LIMIT";
case BluetoothError::Timeout: return "TIMEOUT";
case BluetoothError::AlreadyDiscovering: return "ALREADY_DISCOVERING";
default: return "UNKNOWN";
}
}
// bluetooth.startDiscovery(successCallback, errorCallback, options)
static int LuaBluetooth_startDiscovery(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!bluetooth) {
lua_pushboolean(L, 0);
lua_pushstring(L, "Bluetooth interface not available");
return 2;
}
// Validate callbacks
if (!lua_isfunction(L, 1)) {
return luaL_error(L, "First argument must be a success callback function");
}
// Parse options from third argument (optional table)
DiscoveryOptions options;
if (lua_istable(L, 3)) {
lua_getfield(L, 3, "timeout");
if (lua_isnumber(L, -1)) {
options.timeout_seconds = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
// Store callbacks in registry
lua_pushvalue(L, 1); // success callback
int success_ref = luaL_ref(L, LUA_REGISTRYINDEX);
int error_ref = LUA_NOREF;
if (lua_isfunction(L, 2)) {
lua_pushvalue(L, 2); // error callback
error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
}
std::string err;
bool ok = bluetooth->StartDiscovery(
options,
[L, success_ref](const std::vector<BluetoothDevice>& devices) {
lua_rawgeti(L, LUA_REGISTRYINDEX, success_ref);
if (lua_isfunction(L, -1)) {
// Create devices array
lua_newtable(L);
for (size_t i = 0; i < devices.size(); i++) {
lua_newtable(L);
lua_pushstring(L, devices[i].name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, devices[i].address.c_str());
lua_setfield(L, -2, "address");
lua_rawseti(L, -2, static_cast<lua_Integer>(i + 1));
}
lua_pcall(L, 1, 0, 0);
} else {
lua_pop(L, 1);
}
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
},
[L, error_ref](BluetoothError error, const std::string& message) {
if (error_ref != LUA_NOREF) {
lua_rawgeti(L, LUA_REGISTRYINDEX, error_ref);
if (lua_isfunction(L, -1)) {
lua_pushstring(L, BluetoothErrorToString(error));
lua_pushstring(L, message.c_str());
lua_pcall(L, 2, 0, 0);
} else {
lua_pop(L, 1);
}
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
}
},
err
);
if (!ok) {
luaL_unref(L, LUA_REGISTRYINDEX, success_ref);
if (error_ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, error_ref);
}
lua_pushboolean(L, 0);
lua_pushstring(L, err.c_str());
return 2;
}
lua_pushboolean(L, 1);
return 1;
}
// bluetooth.stopDiscovery()
static int LuaBluetooth_stopDiscovery(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (bluetooth) {
bluetooth->StopDiscovery();
}
return 0;
}
// bluetooth.isDiscovering()
static int LuaBluetooth_isDiscovering(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
lua_pushboolean(L, bluetooth && bluetooth->IsDiscovering());
return 1;
}
// bluetooth.connect(address, onDataCallback, onErrorCallback, options)
static int LuaBluetooth_connect(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (!bluetooth) {
lua_pushnil(L);
lua_pushstring(L, "Bluetooth interface not available");
return 2;
}
// Get address
const char* address = luaL_checkstring(L, 1);
// Parse options from fourth argument (optional table)
ConnectionOptions options;
if (lua_istable(L, 4)) {
lua_getfield(L, 4, "timeout");
if (lua_isnumber(L, -1)) {
options.timeout_ms = static_cast<int>(lua_tointeger(L, -1));
}
lua_pop(L, 1);
}
std::string err;
auto connection = bluetooth->Connect(address, options, err);
if (!connection) {
lua_pushnil(L);
lua_pushstring(L, err.c_str());
return 2;
}
// Set up data callback if provided
if (lua_isfunction(L, 2)) {
lua_pushvalue(L, 2);
int data_ref = luaL_ref(L, LUA_REGISTRYINDEX);
connection->SetOnData([L, data_ref](const std::vector<uint8_t>& data) {
lua_rawgeti(L, LUA_REGISTRYINDEX, data_ref);
if (lua_isfunction(L, -1)) {
// Create data table
lua_newtable(L);
for (size_t i = 0; i < data.size(); i++) {
lua_pushinteger(L, data[i]);
lua_rawseti(L, -2, static_cast<lua_Integer>(i + 1));
}
lua_pcall(L, 1, 0, 0);
} else {
lua_pop(L, 1);
}
});
}
// Set up error callback if provided
if (lua_isfunction(L, 3)) {
lua_pushvalue(L, 3);
int error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
connection->SetOnError([L, error_ref](BluetoothError error, const std::string& message) {
lua_rawgeti(L, LUA_REGISTRYINDEX, error_ref);
if (lua_isfunction(L, -1)) {
lua_pushstring(L, BluetoothErrorToString(error));
lua_pushstring(L, message.c_str());
lua_pcall(L, 2, 0, 0);
} else {
lua_pop(L, 1);
}
});
}
PushConnection(L, connection, bluetooth);
return 1;
}
// bluetooth.disconnectAll()
static int LuaBluetooth_disconnectAll(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (bluetooth) {
bluetooth->DisconnectAll();
}
return 0;
}
// bluetooth.getConnectionCount()
static int LuaBluetooth_getConnectionCount(lua_State* L) {
auto bluetooth = static_cast<BluetoothInterface*>(lua_touserdata(L, lua_upvalueindex(1)));
if (bluetooth) {
lua_pushinteger(L, static_cast<lua_Integer>(bluetooth->GetActiveConnectionCount()));
} else {
lua_pushinteger(L, 0);
}
return 1;
}
// Helper to set a global in the real _G (bypassing any proxy)
static void SetGlobalInRealG(lua_State* L, const char* name) {
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);
if (lua_getmetatable(L, -1)) {
lua_getfield(L, -1, "__index");
if (lua_istable(L, -1)) {
lua_pushvalue(L, -4);
lua_setfield(L, -2, name);
lua_pop(L, 4);
return;
}
lua_pop(L, 2);
}
lua_pushvalue(L, -2);
lua_setfield(L, -2, name);
lua_pop(L, 2);
}
void RegisterBluetoothAPI(lua_State* L, BluetoothInterface* bluetooth) {
// Create connection metatable
CreateConnectionMetatable(L);
// Create bluetooth table
lua_newtable(L);
// bluetooth.startDiscovery
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_startDiscovery, 1);
lua_setfield(L, -2, "startDiscovery");
// bluetooth.stopDiscovery
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_stopDiscovery, 1);
lua_setfield(L, -2, "stopDiscovery");
// bluetooth.isDiscovering
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_isDiscovering, 1);
lua_setfield(L, -2, "isDiscovering");
// bluetooth.connect
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_connect, 1);
lua_setfield(L, -2, "connect");
// bluetooth.disconnectAll
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_disconnectAll, 1);
lua_setfield(L, -2, "disconnectAll");
// bluetooth.getConnectionCount
lua_pushlightuserdata(L, bluetooth);
lua_pushcclosure(L, LuaBluetooth_getConnectionCount, 1);
lua_setfield(L, -2, "getConnectionCount");
// Set as global (bypasses sandbox proxy)
SetGlobalInRealG(L, "bluetooth");
}
} // namespace mosis

View File

@@ -0,0 +1,152 @@
// bluetooth_interface.h - Bluetooth interface for Lua sandbox
// Milestone 16: Bluetooth discovery and pairing with user consent
#pragma once
#include <string>
#include <memory>
#include <functional>
#include <mutex>
#include <atomic>
#include <vector>
#include <unordered_map>
#include <cstdint>
struct lua_State;
namespace mosis {
struct BluetoothDevice {
std::string name; // Device display name
std::string address; // MAC address
};
struct DiscoveryOptions {
int timeout_seconds = 30; // Discovery timeout
};
struct ConnectionOptions {
int timeout_ms = 10000; // Connection timeout
};
enum class BluetoothError {
None,
PermissionDenied,
UserConsentRequired,
DiscoveryFailed,
ConnectionFailed,
NotConnected,
ConnectionLimitReached,
Timeout,
AlreadyDiscovering
};
class BluetoothConnection {
public:
using DataCallback = std::function<void(const std::vector<uint8_t>&)>;
using ErrorCallback = std::function<void(BluetoothError, const std::string&)>;
BluetoothConnection(int id, const std::string& address);
~BluetoothConnection();
int GetId() const { return m_id; }
std::string GetAddress() const { return m_address; }
bool IsConnected() const { return m_connected; }
bool Send(const std::vector<uint8_t>& data, std::string& error);
void Close();
void SetOnData(DataCallback cb) { m_on_data = std::move(cb); }
void SetOnError(ErrorCallback cb) { m_on_error = std::move(cb); }
void SetOnDisconnect(std::function<void()> cb) { m_on_disconnect = std::move(cb); }
// For mock mode - simulate data receive
void SimulateData(const std::vector<uint8_t>& data);
void SimulateDisconnect();
private:
int m_id;
std::string m_address;
std::atomic<bool> m_connected{false};
DataCallback m_on_data;
ErrorCallback m_on_error;
std::function<void()> m_on_disconnect;
mutable std::mutex m_mutex;
};
class BluetoothInterface {
public:
BluetoothInterface(const std::string& app_id);
~BluetoothInterface();
// Permission checks
bool HasBluetoothPermission() const { return m_has_bluetooth_permission; }
void SetBluetoothPermission(bool granted) { m_has_bluetooth_permission = granted; }
// User gesture tracking
bool HasUserConsent() const { return m_has_user_consent; }
void SetUserConsent(bool consent) { m_has_user_consent = consent; }
// Start device discovery
// Requires permission AND user consent
bool StartDiscovery(
const DiscoveryOptions& options,
std::function<void(const std::vector<BluetoothDevice>&)> success,
std::function<void(BluetoothError, const std::string&)> error,
std::string& out_error
);
// Stop current discovery
void StopDiscovery();
// Check if discovery is in progress
bool IsDiscovering() const { return m_is_discovering; }
// Connect to a device
std::shared_ptr<BluetoothConnection> Connect(
const std::string& address,
const ConnectionOptions& options,
std::string& error
);
// Disconnect a specific connection
void Disconnect(int connection_id);
// Disconnect all connections
void DisconnectAll();
// Get active connection count
size_t GetActiveConnectionCount() const;
// Cleanup on app stop
void Shutdown();
// For testing
void SetMockMode(bool enabled) { m_mock_mode = enabled; }
bool IsMockMode() const { return m_mock_mode; }
// Mock mode: set discovered devices
void SetMockDevices(const std::vector<BluetoothDevice>& devices) { m_mock_devices = devices; }
// Get connection by ID
std::shared_ptr<BluetoothConnection> GetConnection(int id);
private:
std::string m_app_id;
std::unordered_map<int, std::shared_ptr<BluetoothConnection>> m_connections;
mutable std::mutex m_mutex;
bool m_mock_mode = true;
bool m_has_bluetooth_permission = false;
bool m_has_user_consent = false;
std::atomic<bool> m_is_discovering{false};
std::vector<BluetoothDevice> m_mock_devices;
int m_next_connection_id = 1;
static constexpr int MAX_CONNECTIONS = 5;
void CleanupClosedConnections();
};
// Register bluetooth.* APIs as globals
void RegisterBluetoothAPI(lua_State* L, BluetoothInterface* bluetooth);
} // namespace mosis