implement Milestone 16: Bluetooth interface with user consent
This commit is contained in:
595
src/main/cpp/sandbox/bluetooth_interface.cpp
Normal file
595
src/main/cpp/sandbox/bluetooth_interface.cpp
Normal 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
|
||||
152
src/main/cpp/sandbox/bluetooth_interface.h
Normal file
152
src/main/cpp/sandbox/bluetooth_interface.h
Normal 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
|
||||
Reference in New Issue
Block a user